@bsv/sdk 1.1.25 → 1.1.26

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.
@@ -31,7 +31,7 @@ import { BEEF_MAGIC } from './Beef.js'
31
31
  * earliest time or block height at which the transaction can be added to the block chain.
32
32
  * @property {Record<string, any>} metadata - A key-value store for attaching additional data to
33
33
  * the transaction object, not included in the transaction itself. Useful for adding descriptions, internal reference numbers, or other information.
34
- * @property {MerkleProof} [merkleProof] - Optional. A merkle proof demonstrating the transaction's
34
+ * @property {MerkleProof} [merklePath] - Optional. A merkle proof demonstrating the transaction's
35
35
  * inclusion in a block. Useful for transaction verification using SPV.
36
36
  *
37
37
  * @example
@@ -60,44 +60,109 @@ export default class Transaction {
60
60
 
61
61
  /**
62
62
  * Creates a new transaction, linked to its inputs and their associated merkle paths, from a BEEF (BRC-62) structure.
63
- * @param beef A binary representation of a transaction in BEEF format.
63
+ * Optionally, you can provide a specific TXID to retrieve a particular transaction from the BEEF data.
64
+ * If the TXID is provided but not found in the BEEF data, an error will be thrown.
65
+ * If no TXID is provided, the last transaction in the BEEF data is returned.
66
+ * @param beef A binary representation of transactions in BEEF format.
67
+ * @param txid Optional TXID of the transaction to retrieve from the BEEF data.
64
68
  * @returns An anchored transaction, linked to its associated inputs populated with merkle paths.
65
69
  */
66
- static fromBEEF (beef: number[]): Transaction {
70
+ static fromBEEF(beef: number[], txid?: string): Transaction {
67
71
  const reader = new Reader(beef)
68
- // Read the version
69
- const version = reader.readUInt32LE()
70
- if (version !== BEEF_MAGIC) {
71
- throw new Error(`Invalid BEEF version. Expected 4022206465, received ${version}.`)
72
+ const { transactions, BUMPs } = Transaction.parseBEEFData(reader)
73
+
74
+ // The last transaction in the BEEF data can be used if txid is not provided
75
+ const txids = Object.keys(transactions)
76
+ const lastTXID = txids[txids.length - 1]
77
+ const targetTXID = txid || lastTXID
78
+
79
+ if (!transactions[targetTXID]) {
80
+ throw new Error(`Transaction with TXID ${targetTXID} not found in BEEF data.`)
72
81
  }
73
82
 
74
- // Read the BUMPs
75
- const numberOfBUMPs = reader.readVarIntNum()
76
- const BUMPs = []
77
- for (let i = 0; i < numberOfBUMPs; i++) {
78
- BUMPs.push(MerklePath.fromReader(reader))
83
+ // Recursive function for adding merkle proofs or input transactions
84
+ const addPathOrInputs = (obj: { pathIndex?: number, tx: Transaction }): void => {
85
+ if (typeof obj.pathIndex === 'number') {
86
+ const path = BUMPs[obj.pathIndex]
87
+ if (typeof path !== 'object') {
88
+ throw new Error('Invalid merkle path index found in BEEF!')
89
+ }
90
+ obj.tx.merklePath = path
91
+ } else {
92
+ for (let i = 0; i < obj.tx.inputs.length; i++) {
93
+ const input = obj.tx.inputs[i]
94
+ const sourceObj = transactions[input.sourceTXID]
95
+ if (typeof sourceObj !== 'object') {
96
+ throw new Error(`Reference to unknown TXID in BEEF: ${input.sourceTXID}`)
97
+ }
98
+ input.sourceTransaction = sourceObj.tx
99
+ addPathOrInputs(sourceObj)
100
+ }
101
+ }
79
102
  }
80
103
 
81
- // Read all transactions into an object
82
- // The object has keys of TXIDs and values of objects with transactions and BUMP indexes
83
- const numberOfTransactions = reader.readVarIntNum()
84
- const transactions: Record<string, { pathIndex?: number, tx: Transaction }> = {}
85
- let lastTXID: string
86
- for (let i = 0; i < numberOfTransactions; i++) {
87
- const tx = Transaction.fromReader(reader)
88
- const obj: { pathIndex?: number, tx: Transaction } = { tx }
89
- const txid = tx.id('hex')
90
- if (i + 1 === numberOfTransactions) { // The last tXID is stored for later
91
- lastTXID = txid
104
+ addPathOrInputs(transactions[targetTXID])
105
+
106
+ return transactions[targetTXID].tx
107
+ }
108
+
109
+ /**
110
+ * Creates a new transaction from an Atomic BEEF (BRC-95) structure.
111
+ * Extracts the subject transaction and ensures that all transactions within the BEEF data
112
+ * are part of the dependency graph of the subject transaction.
113
+ * Throws errors if the Atomic BEEF data does not strictly adhere to the BRC-95 specification.
114
+ *
115
+ * @param beef A binary representation of an Atomic BEEF structure.
116
+ * @returns The subject transaction, linked to its associated inputs populated with merkle paths.
117
+ */
118
+ static fromAtomicBEEF(beef: number[]): Transaction {
119
+ const reader = new Reader(beef)
120
+ // Read the Atomic BEEF prefix
121
+ const prefix = reader.readUInt32LE()
122
+ if (prefix !== 0x01010101) {
123
+ throw new Error(`Invalid Atomic BEEF prefix. Expected 0x01010101, received ${prefix.toString(16)}.`)
124
+ }
125
+
126
+ // Read the subject TXID
127
+ const subjectTXIDArray = reader.read(32)
128
+ const subjectTXID = toHex(subjectTXIDArray)
129
+
130
+ // The remaining data is the BEEF data
131
+ const beefReader = new Reader(reader.read())
132
+ const { transactions, BUMPs } = Transaction.parseBEEFData(beefReader)
133
+
134
+ // Ensure that the subject transaction exists
135
+ if (!transactions[subjectTXID]) {
136
+ throw new Error(`Subject transaction with TXID ${subjectTXID} not found in Atomic BEEF data.`)
137
+ }
138
+
139
+ // Ensure that all transactions are part of the dependency graph of the subject transaction
140
+ const validTxids = new Set<string>()
141
+ const traverseDependencies = (txid: string) => {
142
+ if (validTxids.has(txid)) {
143
+ return
92
144
  }
93
- const hasBump = Boolean(reader.readUInt8())
94
- if (hasBump) {
95
- obj.pathIndex = reader.readVarIntNum()
145
+ validTxids.add(txid)
146
+ const tx = transactions[txid].tx
147
+ for (const input of tx.inputs) {
148
+ const inputTxid = input.sourceTXID
149
+ if (!transactions[inputTxid]) {
150
+ throw new Error(`Input transaction with TXID ${inputTxid} is missing in Atomic BEEF data.`)
151
+ }
152
+ traverseDependencies(inputTxid)
96
153
  }
97
- transactions[txid] = obj
98
154
  }
99
155
 
100
- // Recursive function for adding merkle proofs or input transactions
156
+ traverseDependencies(subjectTXID)
157
+
158
+ // Check for any unrelated transactions
159
+ for (const txid in transactions) {
160
+ if (!validTxids.has(txid)) {
161
+ throw new Error(`Unrelated transaction with TXID ${txid} found in Atomic BEEF data.`)
162
+ }
163
+ }
164
+
165
+ // Build the transaction by linking inputs and merkle paths
101
166
  const addPathOrInputs = (obj: { pathIndex?: number, tx: Transaction }): void => {
102
167
  if (typeof obj.pathIndex === 'number') {
103
168
  const path = BUMPs[obj.pathIndex]
@@ -110,7 +175,7 @@ export default class Transaction {
110
175
  const input = obj.tx.inputs[i]
111
176
  const sourceObj = transactions[input.sourceTXID]
112
177
  if (typeof sourceObj !== 'object') {
113
- throw new Error(`Reference to unknown TXID in BUMP: ${input.sourceTXID}`)
178
+ throw new Error(`Reference to unknown TXID in BEEF: ${input.sourceTXID}`)
114
179
  }
115
180
  input.sourceTransaction = sourceObj.tx
116
181
  addPathOrInputs(sourceObj)
@@ -118,9 +183,47 @@ export default class Transaction {
118
183
  }
119
184
  }
120
185
 
121
- // Read the final transaction and Add inputs and merkle proofs to the final transaction, returning it
122
- addPathOrInputs(transactions[lastTXID])
123
- return transactions[lastTXID].tx
186
+ addPathOrInputs(transactions[subjectTXID])
187
+
188
+ return transactions[subjectTXID].tx
189
+ }
190
+
191
+ /**
192
+ * Parses BEEF data from a Reader and returns the transactions and BUMPs.
193
+ *
194
+ * @param reader The Reader positioned at the start of BEEF data.
195
+ * @returns An object containing the transactions and BUMPs.
196
+ */
197
+ private static parseBEEFData(reader: Reader): { transactions: Record<string, { pathIndex?: number, tx: Transaction }>, BUMPs: MerklePath[] } {
198
+ // Read the version
199
+ const version = reader.readUInt32LE()
200
+ if (version !== BEEF_MAGIC) {
201
+ throw new Error(`Invalid BEEF version. Expected ${BEEF_MAGIC}, received ${version}.`)
202
+ }
203
+
204
+ // Read the BUMPs
205
+ const numberOfBUMPs = reader.readVarIntNum()
206
+ const BUMPs = []
207
+ for (let i = 0; i < numberOfBUMPs; i++) {
208
+ BUMPs.push(MerklePath.fromReader(reader))
209
+ }
210
+
211
+ // Read all transactions into an object
212
+ // The object has keys of TXIDs and values of objects with transactions and BUMP indexes
213
+ const numberOfTransactions = reader.readVarIntNum()
214
+ const transactions: Record<string, { pathIndex?: number, tx: Transaction }> = {}
215
+ for (let i = 0; i < numberOfTransactions; i++) {
216
+ const tx = Transaction.fromReader(reader)
217
+ const obj: { pathIndex?: number, tx: Transaction } = { tx }
218
+ const txid = tx.id('hex')
219
+ const hasBump = Boolean(reader.readUInt8())
220
+ if (hasBump) {
221
+ obj.pathIndex = reader.readVarIntNum()
222
+ }
223
+ transactions[txid] = obj
224
+ }
225
+
226
+ return { transactions, BUMPs }
124
227
  }
125
228
 
126
229
  /**
@@ -128,7 +231,7 @@ export default class Transaction {
128
231
  * @param ef A binary representation of a transaction in EF format.
129
232
  * @returns An extended transaction, linked to its associated inputs by locking script and satoshis amounts only.
130
233
  */
131
- static fromEF (ef: number[]): Transaction {
234
+ static fromEF(ef: number[]): Transaction {
132
235
  const br = new Reader(ef)
133
236
  const version = br.readUInt32LE()
134
237
  if (toHex(br.read(6)) !== '0000000000ef') throw new Error('Invalid EF marker')
@@ -190,7 +293,7 @@ export default class Transaction {
190
293
  * outputs: { vout: number, offset: number, length: number }[]
191
294
  * }
192
295
  */
193
- static parseScriptOffsets (bin: number[]): {
296
+ static parseScriptOffsets(bin: number[]): {
194
297
  inputs: Array<{ vin: number, offset: number, length: number }>
195
298
  outputs: Array<{ vout: number, offset: number, length: number }>
196
299
  } {
@@ -216,7 +319,7 @@ export default class Transaction {
216
319
  return { inputs, outputs }
217
320
  }
218
321
 
219
- static fromReader (br: Reader): Transaction {
322
+ static fromReader(br: Reader): Transaction {
220
323
  const version = br.readUInt32LE()
221
324
  const inputsLength = br.readVarIntNum()
222
325
  const inputs: TransactionInput[] = []
@@ -257,7 +360,7 @@ export default class Transaction {
257
360
  * @param {number[]} bin - The binary array representation of the transaction.
258
361
  * @returns {Transaction} - A new Transaction instance.
259
362
  */
260
- static fromBinary (bin: number[]): Transaction {
363
+ static fromBinary(bin: number[]): Transaction {
261
364
  const br = new Reader(bin)
262
365
  return Transaction.fromReader(br)
263
366
  }
@@ -269,7 +372,7 @@ export default class Transaction {
269
372
  * @param {string} hex - The hexadecimal string representation of the transaction.
270
373
  * @returns {Transaction} - A new Transaction instance.
271
374
  */
272
- static fromHex (hex: string): Transaction {
375
+ static fromHex(hex: string): Transaction {
273
376
  return Transaction.fromBinary(toArray(hex, 'hex'))
274
377
  }
275
378
 
@@ -280,22 +383,26 @@ export default class Transaction {
280
383
  * @param {string} hex - The hexadecimal string representation of the transaction EF.
281
384
  * @returns {Transaction} - A new Transaction instance.
282
385
  */
283
- static fromHexEF (hex: string): Transaction {
386
+ static fromHexEF(hex: string): Transaction {
284
387
  return Transaction.fromEF(toArray(hex, 'hex'))
285
388
  }
286
389
 
287
390
  /**
288
391
  * Creates a Transaction instance from a hexadecimal string encoded BEEF.
392
+ * Optionally, you can provide a specific TXID to retrieve a particular transaction from the BEEF data.
393
+ * If the TXID is provided but not found in the BEEF data, an error will be thrown.
394
+ * If no TXID is provided, the last transaction in the BEEF data is returned.
289
395
  *
290
396
  * @static
291
397
  * @param {string} hex - The hexadecimal string representation of the transaction BEEF.
398
+ * @param {string} [txid] - Optional TXID of the transaction to retrieve from the BEEF data.
292
399
  * @returns {Transaction} - A new Transaction instance.
293
400
  */
294
- static fromHexBEEF (hex: string): Transaction {
295
- return Transaction.fromBEEF(toArray(hex, 'hex'))
401
+ static fromHexBEEF(hex: string, txid?: string): Transaction {
402
+ return Transaction.fromBEEF(toArray(hex, 'hex'), txid)
296
403
  }
297
404
 
298
- constructor (
405
+ constructor(
299
406
  version: number = 1,
300
407
  inputs: TransactionInput[] = [],
301
408
  outputs: TransactionOutput[] = [],
@@ -317,7 +424,7 @@ export default class Transaction {
317
424
  * @param {TransactionInput} input - The TransactionInput object to add to the transaction.
318
425
  * @throws {Error} - If the input does not have a sourceTXID or sourceTransaction defined.
319
426
  */
320
- addInput (input: TransactionInput): void {
427
+ addInput(input: TransactionInput): void {
321
428
  if (
322
429
  typeof input.sourceTXID === 'undefined' &&
323
430
  typeof input.sourceTransaction === 'undefined'
@@ -337,7 +444,7 @@ export default class Transaction {
337
444
  *
338
445
  * @param {TransactionOutput} output - The TransactionOutput object to add to the transaction.
339
446
  */
340
- addOutput (output: TransactionOutput): void {
447
+ addOutput(output: TransactionOutput): void {
341
448
  this.cachedHash = undefined
342
449
  this.outputs.push(output)
343
450
  }
@@ -347,7 +454,7 @@ export default class Transaction {
347
454
  *
348
455
  * @param {Record<string, any>} metadata - The metadata object to merge into the existing metadata.
349
456
  */
350
- updateMetadata (metadata: Record<string, any>): void {
457
+ updateMetadata(metadata: Record<string, any>): void {
351
458
  this.metadata = {
352
459
  ...this.metadata,
353
460
  ...metadata
@@ -365,7 +472,7 @@ export default class Transaction {
365
472
  *
366
473
  * TODO: Benford's law change distribution.
367
474
  */
368
- async fee (modelOrFee?: FeeModel | number, changeDistribution: 'equal' | 'random' = 'equal'): Promise<void> {
475
+ async fee(modelOrFee?: FeeModel | number, changeDistribution: 'equal' | 'random' = 'equal'): Promise<void> {
369
476
  this.cachedHash = undefined
370
477
  if (typeof modelOrFee === 'undefined') {
371
478
  modelOrFee = new SatoshisPerKilobyte(10)
@@ -426,7 +533,7 @@ export default class Transaction {
426
533
  *
427
534
  * @returns The current transaction fee
428
535
  */
429
- getFee (): number {
536
+ getFee(): number {
430
537
  let totalIn = 0
431
538
  for (const input of this.inputs) {
432
539
  if (typeof input.sourceTransaction !== 'object') {
@@ -444,7 +551,7 @@ export default class Transaction {
444
551
  /**
445
552
  * Signs a transaction, hydrating all its unlocking scripts based on the provided script templates where they are available.
446
553
  */
447
- async sign (): Promise<void> {
554
+ async sign(): Promise<void> {
448
555
  this.cachedHash = undefined
449
556
  for (const out of this.outputs) {
450
557
  if (typeof out.satoshis === 'undefined') {
@@ -475,7 +582,7 @@ export default class Transaction {
475
582
  * @param broadcaster The Broadcaster instance wwhere the transaction will be sent
476
583
  * @returns A BroadcastResponse or BroadcastFailure from the Broadcaster
477
584
  */
478
- async broadcast (broadcaster: Broadcaster = defaultBroadcaster()): Promise<BroadcastResponse | BroadcastFailure> {
585
+ async broadcast(broadcaster: Broadcaster = defaultBroadcaster()): Promise<BroadcastResponse | BroadcastFailure> {
479
586
  return await broadcaster.broadcast(this)
480
587
  }
481
588
 
@@ -484,7 +591,7 @@ export default class Transaction {
484
591
  *
485
592
  * @returns {number[]} - The binary array representation of the transaction.
486
593
  */
487
- toBinary (): number[] {
594
+ toBinary(): number[] {
488
595
  const writer = new Writer()
489
596
  writer.writeUInt32LE(this.version)
490
597
  writer.writeVarIntNum(this.inputs.length)
@@ -516,7 +623,7 @@ export default class Transaction {
516
623
  *
517
624
  * @returns {number[]} - The BRC-30 EF representation of the transaction.
518
625
  */
519
- toEF (): number[] {
626
+ toEF(): number[] {
520
627
  const writer = new Writer()
521
628
  writer.writeUInt32LE(this.version)
522
629
  writer.write([0, 0, 0, 0, 0, 0xef])
@@ -556,7 +663,7 @@ export default class Transaction {
556
663
  *
557
664
  * @returns {string} - The hexadecimal string representation of the transaction EF.
558
665
  */
559
- toHexEF (): string {
666
+ toHexEF(): string {
560
667
  return toHex(this.toEF())
561
668
  }
562
669
 
@@ -565,7 +672,7 @@ export default class Transaction {
565
672
  *
566
673
  * @returns {string} - The hexadecimal string representation of the transaction.
567
674
  */
568
- toHex (): string {
675
+ toHex(): string {
569
676
  return toHex(this.toBinary())
570
677
  }
571
678
 
@@ -574,17 +681,26 @@ export default class Transaction {
574
681
  *
575
682
  * @returns {string} - The hexadecimal string representation of the transaction BEEF.
576
683
  */
577
- toHexBEEF (): string {
684
+ toHexBEEF(): string {
578
685
  return toHex(this.toBEEF())
579
686
  }
580
687
 
688
+ /**
689
+ * Converts the transaction to a hexadecimal string Atomic BEEF.
690
+ *
691
+ * @returns {string} - The hexadecimal string representation of the transaction Atomic BEEF.
692
+ */
693
+ toHexAtomicBEEF(): string {
694
+ return toHex(this.toAtomicBEEF())
695
+ }
696
+
581
697
  /**
582
698
  * Calculates the transaction's hash.
583
699
  *
584
700
  * @param {'hex' | undefined} enc - The encoding to use for the hash. If 'hex', returns a hexadecimal string; otherwise returns a binary array.
585
701
  * @returns {string | number[]} - The hash of the transaction in the specified format.
586
702
  */
587
- hash (enc?: 'hex'): number[] | string {
703
+ hash(enc?: 'hex'): number[] | string {
588
704
  let hash
589
705
  if (this.cachedHash) {
590
706
  hash = this.cachedHash
@@ -603,21 +719,21 @@ export default class Transaction {
603
719
  *
604
720
  * @returns {number[]} - The ID of the transaction in the binary array format.
605
721
  */
606
- id (): number[]
722
+ id(): number[]
607
723
  /**
608
724
  * Calculates the transaction's ID in hexadecimal format.
609
725
  *
610
726
  * @param {'hex'} enc - The encoding to use for the ID. If 'hex', returns a hexadecimal string.
611
727
  * @returns {string} - The ID of the transaction in the hex format.
612
728
  */
613
- id (enc: 'hex'): string
729
+ id(enc: 'hex'): string
614
730
  /**
615
731
  * Calculates the transaction's ID.
616
732
  *
617
733
  * @param {'hex' | undefined} enc - The encoding to use for the ID. If 'hex', returns a hexadecimal string; otherwise returns a binary array.
618
734
  * @returns {string | number[]} - The ID of the transaction in the specified format.
619
735
  */
620
- id (enc?: 'hex'): number[] | string {
736
+ id(enc?: 'hex'): number[] | string {
621
737
  const id = [...this.hash() as number[]]
622
738
  id.reverse()
623
739
  if (enc === 'hex') {
@@ -635,7 +751,7 @@ export default class Transaction {
635
751
  *
636
752
  * @example tx.verify(new WhatsOnChain(), new SatoshisPerKilobyte(1))
637
753
  */
638
- async verify (
754
+ async verify(
639
755
  chainTracker: ChainTracker | 'scripts only' = defaultChainTracker(),
640
756
  feeModel?: FeeModel
641
757
  ): Promise<boolean> {
@@ -746,7 +862,7 @@ export default class Transaction {
746
862
  *
747
863
  * @returns The serialized BEEF structure
748
864
  */
749
- toBEEF (): number[] {
865
+ toBEEF(): number[] {
750
866
  const writer = new Writer()
751
867
  writer.writeUInt32LE(4022206465)
752
868
  const BUMPs: MerklePath[] = []
@@ -817,4 +933,24 @@ export default class Transaction {
817
933
  }
818
934
  return writer.toArray()
819
935
  }
936
+
937
+ /**
938
+ * Serializes this transaction and its inputs into the Atomic BEEF (BRC-95) format.
939
+ * The Atomic BEEF format starts with a 4-byte prefix `0x01010101`, followed by the TXID of the subject transaction,
940
+ * and then the BEEF data containing only the subject transaction and its dependencies.
941
+ * This format ensures that the BEEF structure is atomic and contains no unrelated transactions.
942
+ *
943
+ * @returns {number[]} - The serialized Atomic BEEF structure.
944
+ */
945
+ toAtomicBEEF(): number[] {
946
+ const writer = new Writer()
947
+ // Write the Atomic BEEF prefix
948
+ writer.writeUInt32LE(0x01010101)
949
+ // Write the subject TXID (big-endian)
950
+ writer.write(this.id() as number[])
951
+ // Append the BEEF data
952
+ const beefData = this.toBEEF()
953
+ writer.write(beefData)
954
+ return writer.toArray()
955
+ }
820
956
  }