@bsv/sdk 1.2.8 → 1.2.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/dist/cjs/package.json +1 -1
  2. package/dist/cjs/src/overlay-tools/SHIPBroadcaster.js +2 -2
  3. package/dist/cjs/src/overlay-tools/SHIPBroadcaster.js.map +1 -1
  4. package/dist/cjs/src/primitives/PublicKey.js +1 -1
  5. package/dist/cjs/src/primitives/PublicKey.js.map +1 -1
  6. package/dist/cjs/src/transaction/Beef.js +38 -13
  7. package/dist/cjs/src/transaction/Beef.js.map +1 -1
  8. package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
  9. package/dist/esm/src/overlay-tools/SHIPBroadcaster.js +2 -2
  10. package/dist/esm/src/overlay-tools/SHIPBroadcaster.js.map +1 -1
  11. package/dist/esm/src/primitives/PublicKey.js +1 -1
  12. package/dist/esm/src/primitives/PublicKey.js.map +1 -1
  13. package/dist/esm/src/transaction/Beef.js +38 -13
  14. package/dist/esm/src/transaction/Beef.js.map +1 -1
  15. package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
  16. package/dist/types/src/overlay-tools/SHIPBroadcaster.d.ts +9 -4
  17. package/dist/types/src/overlay-tools/SHIPBroadcaster.d.ts.map +1 -1
  18. package/dist/types/src/primitives/PublicKey.d.ts.map +1 -1
  19. package/dist/types/src/transaction/Beef.d.ts +12 -1
  20. package/dist/types/src/transaction/Beef.d.ts.map +1 -1
  21. package/dist/types/tsconfig.types.tsbuildinfo +1 -1
  22. package/dist/umd/bundle.js +1 -1
  23. package/docs/overlay-tools.md +11 -1
  24. package/docs/transaction.md +19 -1
  25. package/package.json +1 -1
  26. package/src/overlay-tools/SHIPBroadcaster.ts +13 -6
  27. package/src/overlay-tools/__tests/SHIPBroadcaster.test.ts +82 -0
  28. package/src/primitives/PublicKey.ts +12 -12
  29. package/src/transaction/Beef.ts +39 -13
  30. package/src/transaction/__tests/Beef.test.ts +18 -0
@@ -26,6 +26,7 @@ Instructs the Overlay Services Engine about which outputs to admit and which pre
26
26
  export interface AdmittanceInstructions {
27
27
  outputsToAdmit: number[];
28
28
  coinsToRetain: number[];
29
+ coinsRemoved?: number[];
29
30
  }
30
31
  ```
31
32
 
@@ -33,6 +34,15 @@ export interface AdmittanceInstructions {
33
34
 
34
35
  <summary>Interface AdmittanceInstructions Details</summary>
35
36
 
37
+ #### Property coinsRemoved
38
+
39
+ The indices of all inputs from the provided transaction which reference previously-admitted outputs,
40
+ which are now considered spent and have been removed from the managed topic.
41
+
42
+ ```ts
43
+ coinsRemoved?: number[]
44
+ ```
45
+
36
46
  #### Property coinsToRetain
37
47
 
38
48
  The indices of all inputs from the provided transaction which spend previously-admitted outputs that should be retained for historical record-keeping.
@@ -43,7 +53,7 @@ coinsToRetain: number[]
43
53
 
44
54
  #### Property outputsToAdmit
45
55
 
46
- The indices of all admissable outputs into the managed topic from the provided transaction.
56
+ The indices of all admissible outputs into the managed topic from the provided transaction.
47
57
 
48
58
  ```ts
49
59
  outputsToAdmit: number[]
@@ -592,6 +592,7 @@ export class Beef {
592
592
  constructor(version?: BeefVersion)
593
593
  get magic(): number
594
594
  findTxid(txid: string): BeefTx | undefined
595
+ makeTxidOnly(txid: string): BeefTx | undefined
595
596
  findBump(txid: string): MerklePath | undefined
596
597
  findTransactionForSigning(txid: string): Transaction | undefined
597
598
  findAtomicTransaction(txid: string): Transaction | undefined
@@ -779,6 +780,23 @@ Argument Details
779
780
  + **allowTxidOnly**
780
781
  + optional. If true, transaction txid only is assumed valid
781
782
 
783
+ #### Method makeTxidOnly
784
+
785
+ Replaces `BeefTx` for this txid with txidOnly.
786
+
787
+ Replacement is done so that a `clone()` can be
788
+ updated by this method without affecting the
789
+ original.
790
+
791
+ ```ts
792
+ makeTxidOnly(txid: string): BeefTx | undefined
793
+ ```
794
+ See also: [BeefTx](#class-beeftx)
795
+
796
+ Returns
797
+
798
+ undefined if txid is unknown.
799
+
782
800
  #### Method mergeBump
783
801
 
784
802
  Merge a MerklePath that is assumed to be fully valid.
@@ -847,7 +865,7 @@ Argument Details
847
865
  #### Method sortTxs
848
866
 
849
867
  Sort the `txs` by input txid dependency order:
850
- - Oldest Tx Anchored by Path
868
+ - Oldest Tx Anchored by Path or txid only
851
869
  - Newer Txs depending on Older parents
852
870
  - Newest Tx
853
871
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bsv/sdk",
3
- "version": "1.2.8",
3
+ "version": "1.2.10",
4
4
  "type": "module",
5
5
  "description": "BSV Blockchain Software Development Kit",
6
6
  "main": "dist/cjs/mod.js",
@@ -18,14 +18,20 @@ export interface TaggedBEEF {
18
18
  */
19
19
  export interface AdmittanceInstructions {
20
20
  /**
21
- * The indices of all admissable outputs into the managed topic from the provided transaction.
22
- */
21
+ * The indices of all admissible outputs into the managed topic from the provided transaction.
22
+ */
23
23
  outputsToAdmit: number[]
24
24
 
25
25
  /**
26
- * The indices of all inputs from the provided transaction which spend previously-admitted outputs that should be retained for historical record-keeping.
27
- */
26
+ * The indices of all inputs from the provided transaction which spend previously-admitted outputs that should be retained for historical record-keeping.
27
+ */
28
28
  coinsToRetain: number[]
29
+
30
+ /**
31
+ * The indices of all inputs from the provided transaction which reference previously-admitted outputs,
32
+ * which are now considered spent and have been removed from the managed topic.
33
+ */
34
+ coinsRemoved?: number[]
29
35
  }
30
36
 
31
37
  /**
@@ -180,8 +186,9 @@ export default class SHIPCast implements Broadcaster {
180
186
  for (const [topic, instructions] of Object.entries(steak)) {
181
187
  const outputsToAdmit = instructions.outputsToAdmit
182
188
  const coinsToRetain = instructions.coinsToRetain
183
- // TODO: Account for coins removed property on STEAK
184
- if ((outputsToAdmit && outputsToAdmit.length > 0) || (coinsToRetain && coinsToRetain.length > 0)) {
189
+ const coinsRemoved = instructions.coinsRemoved
190
+
191
+ if ((outputsToAdmit?.length > 0) || (coinsToRetain?.length > 0) || (coinsRemoved?.length > 0)) {
185
192
  acknowledgedTopics.add(topic)
186
193
  }
187
194
  }
@@ -652,6 +652,88 @@ describe('SHIPCast', () => {
652
652
  })
653
653
  })
654
654
 
655
+ it('should succeed when interested hosts only remove coins in a transaction broadcast', async () => {
656
+ const shipHostKey1 = new PrivateKey(42)
657
+ const shipWallet1 = new ProtoWallet(shipHostKey1)
658
+ const shipLib1 = new OverlayAdminTokenTemplate(shipWallet1)
659
+ const shipScript1 = await shipLib1.lock('SHIP', 'https://shiphost1.com', 'tm_foo')
660
+ const shipTx1 = new Transaction(1, [], [{
661
+ lockingScript: shipScript1,
662
+ satoshis: 1
663
+ }], 0)
664
+
665
+ const shipHostKey2 = new PrivateKey(43)
666
+ const shipWallet2 = new ProtoWallet(shipHostKey2)
667
+ const shipLib2 = new OverlayAdminTokenTemplate(shipWallet2)
668
+ const shipScript2 = await shipLib2.lock('SHIP', 'https://shiphost2.com', 'tm_bar')
669
+ const shipTx2 = new Transaction(1, [], [{
670
+ lockingScript: shipScript2,
671
+ satoshis: 1
672
+ }], 0)
673
+
674
+ // Resolver returns two hosts
675
+ mockResolver.query.mockReturnValueOnce({
676
+ type: 'output-list',
677
+ outputs: [
678
+ { beef: shipTx1.toBEEF(), outputIndex: 0 },
679
+ { beef: shipTx2.toBEEF(), outputIndex: 0 }
680
+ ]
681
+ })
682
+
683
+ // First host acknowledges 'tm_foo' with coinsRemoved
684
+ mockFacilitator.send.mockImplementationOnce(async (host, { beef, topics }) => {
685
+ const steak = {}
686
+ for (const topic of topics) {
687
+ steak[topic] = {
688
+ outputsToAdmit: [],
689
+ coinsToRetain: [],
690
+ coinsRemoved: topic === 'tm_foo' ? [0] : []
691
+ }
692
+ }
693
+ return steak
694
+ })
695
+
696
+ // Second host does not acknowledge 'tm_bar'
697
+ mockFacilitator.send.mockImplementationOnce(async (host, { beef, topics }) => {
698
+ const steak = {}
699
+ for (const topic of topics) {
700
+ steak[topic] = {
701
+ outputsToAdmit: [],
702
+ coinsToRetain: [],
703
+ coinsRemoved: []
704
+ }
705
+ }
706
+ return steak
707
+ })
708
+
709
+ const b = new SHIPCast(['tm_foo', 'tm_bar'], {
710
+ facilitator: mockFacilitator,
711
+ resolver: mockResolver as unknown as LookupResolver,
712
+ requireAcknowledgmentFromSpecificHostsForTopics: {
713
+ 'https://shiphost1.com': ['tm_foo']
714
+ },
715
+ requireAcknowledgmentFromAllHostsForTopics: [],
716
+ requireAcknowledgmentFromAnyHostForTopics: []
717
+ })
718
+
719
+ const testTx = new Transaction(1, [], [], 0)
720
+ const response = await b.broadcast(testTx)
721
+
722
+ expect(response).toEqual({
723
+ status: 'success',
724
+ txid: testTx.id('hex'),
725
+ message: 'Sent to 2 Overlay Services hosts.'
726
+ })
727
+
728
+ // Verify the resolver was queried correctly
729
+ expect(mockResolver.query).toHaveBeenCalledWith({
730
+ service: 'ls_ship',
731
+ query: {
732
+ topics: ['tm_foo', 'tm_bar']
733
+ }
734
+ }, 1000)
735
+ })
736
+
655
737
  it('should fail when specific hosts do not acknowledge required topics', async () => {
656
738
  const shipHostKey1 = new PrivateKey(42)
657
739
  const shipWallet1 = new ProtoWallet(shipHostKey1)
@@ -30,7 +30,7 @@ export default class PublicKey extends Point {
30
30
  * const myPrivKey = new PrivateKey(...)
31
31
  * const myPubKey = PublicKey.fromPrivateKey(myPrivKey)
32
32
  */
33
- static fromPrivateKey(key: PrivateKey): PublicKey {
33
+ static fromPrivateKey (key: PrivateKey): PublicKey {
34
34
  const c = new Curve()
35
35
  const p = c.g.mul(key)
36
36
  return new PublicKey(p.x, p.y)
@@ -46,7 +46,7 @@ export default class PublicKey extends Point {
46
46
  * @example
47
47
  * const myPubKey = PublicKey.fromString("03....")
48
48
  */
49
- static fromString(str: string): PublicKey {
49
+ static fromString (str: string): PublicKey {
50
50
  const p = Point.fromString(str)
51
51
  return new PublicKey(p.x, p.y)
52
52
  }
@@ -61,7 +61,7 @@ export default class PublicKey extends Point {
61
61
  * @example
62
62
  * const myPubKey = PublicKey.fromString("03....")
63
63
  */
64
- static fromDER(bytes: number[]): PublicKey {
64
+ static fromDER (bytes: number[]): PublicKey {
65
65
  const p = Point.fromDER(bytes)
66
66
  return new PublicKey(p.x, p.y)
67
67
  }
@@ -76,7 +76,7 @@ export default class PublicKey extends Point {
76
76
  * new PublicKey(point1);
77
77
  * new PublicKey('abc123', 'def456');
78
78
  */
79
- constructor(
79
+ constructor (
80
80
  x: Point | BigNumber | number | number[] | string | null,
81
81
  y: BigNumber | number | number[] | string | null = null,
82
82
  isRed: boolean = true
@@ -85,7 +85,7 @@ export default class PublicKey extends Point {
85
85
  super(x.getX(), x.getY())
86
86
  } else {
87
87
  // Common gotcha: constructing PublicKey with a DER value when you should use .fromString()
88
- if (y === null && isRed === true && typeof x === 'string') {
88
+ if (y === null && isRed && typeof x === 'string') {
89
89
  if (x.length === 66 || x.length === 130) {
90
90
  throw new Error('You are using the "new PublicKey()" constructor with a DER hex string. You need to use "PublicKey.fromString()" instead.')
91
91
  }
@@ -108,7 +108,7 @@ export default class PublicKey extends Point {
108
108
  * const myPrivKey = new PrivateKey(...)
109
109
  * const sharedSecret = myPubKey.deriveSharedSecret(myPrivKey)
110
110
  */
111
- deriveSharedSecret(priv: PrivateKey): Point {
111
+ deriveSharedSecret (priv: PrivateKey): Point {
112
112
  if (!this.validate()) {
113
113
  throw new Error('Public key not valid for ECDH secret derivation')
114
114
  }
@@ -129,7 +129,7 @@ export default class PublicKey extends Point {
129
129
  * const mySignature = new Signature(...)
130
130
  * const isVerified = myPubKey.verify(myMessage, mySignature)
131
131
  */
132
- verify(msg: number[] | string, sig: Signature, enc?: 'hex' | 'utf8'): boolean {
132
+ verify (msg: number[] | string, sig: Signature, enc?: 'hex' | 'utf8'): boolean {
133
133
  const msgHash = new BigNumber(sha256(msg, enc), 16)
134
134
  return verify(msgHash, sig, this)
135
135
  }
@@ -144,7 +144,7 @@ export default class PublicKey extends Point {
144
144
  * @example
145
145
  * const derPublicKey = myPubKey.toDER()
146
146
  */
147
- toDER(enc?: 'hex' | undefined): number[] | string {
147
+ toDER (enc?: 'hex' | undefined): number[] | string {
148
148
  if (enc === 'hex') return this.encode(true, enc) as string
149
149
  return this.encode(true) as number[]
150
150
  }
@@ -157,7 +157,7 @@ export default class PublicKey extends Point {
157
157
  * @example
158
158
  * const publicKeyHash = pubkey.toHash()
159
159
  */
160
- toHash(enc?: 'hex'): number[] | string {
160
+ toHash (enc?: 'hex'): number[] | string {
161
161
  const pkh = hash160(this.encode(true))
162
162
  if (enc === 'hex') {
163
163
  return toHex(pkh)
@@ -179,7 +179,7 @@ export default class PublicKey extends Point {
179
179
  * const testnetAddress = pubkey.toAddress([0x6f])
180
180
  * const testnetAddress = pubkey.toAddress('testnet')
181
181
  */
182
- toAddress(prefix: number[] | string = [0x00]): string {
182
+ toAddress (prefix: number[] | string = [0x00]): string {
183
183
  if (typeof prefix === 'string') {
184
184
  if (prefix === 'testnet' || prefix === 'test') {
185
185
  prefix = [0x6f]
@@ -198,7 +198,7 @@ export default class PublicKey extends Point {
198
198
  * @param invoiceNumber The invoice number used to derive the child key
199
199
  * @returns The derived child key.
200
200
  */
201
- deriveChild(privateKey: PrivateKey, invoiceNumber: string): PublicKey {
201
+ deriveChild (privateKey: PrivateKey, invoiceNumber: string): PublicKey {
202
202
  const sharedSecret = this.deriveSharedSecret(privateKey)
203
203
  const invoiceNumberBin = toArray(invoiceNumber, 'utf8')
204
204
  const hmac = sha256hmac(sharedSecret.encode(true), invoiceNumberBin)
@@ -225,7 +225,7 @@ export default class PublicKey extends Point {
225
225
  * @example
226
226
  * const publicKey = Signature.fromMsgHashAndCompactSignature(msgHash, 'IMOl2mVKfDgsSsHT4uIYBNN4e...', 'base64');
227
227
  */
228
- static fromMsgHashAndCompactSignature(msgHash: BigNumber, signature: number[] | string, enc?: 'hex' | 'base64'): PublicKey {
228
+ static fromMsgHashAndCompactSignature (msgHash: BigNumber, signature: number[] | string, enc?: 'hex' | 'base64'): PublicKey {
229
229
  const data = toArray(signature, enc)
230
230
  if (data.length !== 65) {
231
231
  throw new Error('Invalid Compact Signature')
@@ -98,6 +98,26 @@ export class Beef {
98
98
  return this.txs.find(tx => tx.txid === txid)
99
99
  }
100
100
 
101
+ /**
102
+ * Replaces `BeefTx` for this txid with txidOnly.
103
+ *
104
+ * Replacement is done so that a `clone()` can be
105
+ * updated by this method without affecting the
106
+ * original.
107
+ *
108
+ * @param txid
109
+ * @returns undefined if txid is unknown.
110
+ */
111
+ makeTxidOnly (txid: string): BeefTx | undefined {
112
+ const i = this.txs.findIndex(tx => tx.txid === txid)
113
+ if (i === -1) return undefined
114
+ let btx = this.txs[i]
115
+ if (btx.isTxidOnly) { return btx }
116
+ this.txs.slice(i, i + 1)
117
+ btx = this.mergeTxidOnly(txid)
118
+ return btx
119
+ }
120
+
101
121
  /**
102
122
  * @returns `MerklePath` with level zero hash equal to txid or undefined.
103
123
  */
@@ -518,7 +538,7 @@ export class Beef {
518
538
 
519
539
  /**
520
540
  * Sort the `txs` by input txid dependency order:
521
- * - Oldest Tx Anchored by Path
541
+ * - Oldest Tx Anchored by Path or txid only
522
542
  * - Newer Txs depending on Older parents
523
543
  * - Newest Tx
524
544
  *
@@ -553,7 +573,10 @@ export class Beef {
553
573
  if (tx.isValid) {
554
574
  validTxids[tx.txid] = true
555
575
  result.push(tx)
556
- } else if (tx.isTxidOnly) { txidOnly.push(tx) } else { queue.push(tx) }
576
+ } else if (tx.isTxidOnly && tx.inputTxids.length === 0) {
577
+ validTxids[tx.txid] = true
578
+ txidOnly.push(tx)
579
+ } else { queue.push(tx) }
557
580
  }
558
581
 
559
582
  // Hashtable of unknown input txids used to fund transactions without their own proof.
@@ -564,17 +587,18 @@ export class Beef {
564
587
  const possiblyMissingInputs = queue
565
588
  queue = []
566
589
 
590
+ // all tx are isValid false, hasProof false.
591
+ // if isTxidOnly then has inputTxids
567
592
  for (const tx of possiblyMissingInputs) {
568
593
  let hasMissingInput = false
569
- if (!tx.isValid) {
570
- // For all the unproven transactions,
571
- // link their inputs that exist in this beef,
572
- // make a note of missing inputs.
573
- for (const inputTxid of tx.inputTxids) {
574
- if (!txidToTx[inputTxid]) {
575
- missingInputs[inputTxid] = true
576
- hasMissingInput = true
577
- }
594
+
595
+ // For all the unproven transactions,
596
+ // link their inputs that exist in this beef,
597
+ // make a note of missing inputs.
598
+ for (const inputTxid of tx.inputTxids) {
599
+ if (!txidToTx[inputTxid]) {
600
+ missingInputs[inputTxid] = true
601
+ hasMissingInput = true
578
602
  }
579
603
  }
580
604
  if (hasMissingInput) { txsMissingInputs.push(tx) } else { queue.push(tx) }
@@ -584,6 +608,8 @@ export class Beef {
584
608
  while (queue.length > 0) {
585
609
  const oldQueue = queue
586
610
  queue = []
611
+ // all tx are isValid false, hasProof false.
612
+ // if isTxidOnly then has inputTxids
587
613
  for (const tx of oldQueue) {
588
614
  if (tx.inputTxids.every(txid => validTxids[txid])) {
589
615
  validTxids[tx.txid] = true
@@ -596,8 +622,8 @@ export class Beef {
596
622
  // transactions that don't have proofs and don't chain to proofs
597
623
  const txsNotValid = queue
598
624
 
599
- // New order of txs is sorted, unsortable, txidOnly (no raw transaction)
600
- this.txs = result.concat(queue).concat(txidOnly).concat(txsMissingInputs)
625
+ // New order of txs is unsortable (missing inputs or depends on missing inputs), txidOnly, sorted (so newest sorted is last)
626
+ this.txs = txsMissingInputs.concat(txsNotValid).concat(txidOnly).concat(result)
601
627
 
602
628
  return {
603
629
  missingInputs: Object.keys(missingInputs),
@@ -305,6 +305,24 @@ describe('Beef tests', () => {
305
305
  expect(atomic).toEqual(beef2)
306
306
  }
307
307
  })
308
+ test('9_sortTxs', async () => {
309
+ {
310
+ const beef = new Beef()
311
+ beef.mergeTxidOnly('a')
312
+ const btx = beef.mergeTxidOnly('b')
313
+ btx.inputTxids = ['a']
314
+ beef.sortTxs()
315
+ expect(beef.txs[1].txid).toBe('b')
316
+ }
317
+ {
318
+ const beef = new Beef()
319
+ const atx = beef.mergeTxidOnly('a')
320
+ const btx = beef.mergeTxidOnly('b')
321
+ atx.inputTxids = ['b']
322
+ beef.sortTxs()
323
+ expect(beef.txs[1].txid).toBe('a')
324
+ }
325
+ })
308
326
  })
309
327
 
310
328
  const txs: string[] = [