@bsv/sdk 1.1.5 → 1.1.7

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 (54) hide show
  1. package/dist/cjs/mod.js +1 -0
  2. package/dist/cjs/mod.js.map +1 -1
  3. package/dist/cjs/package.json +1 -1
  4. package/dist/cjs/src/compat/Utxo.js.map +1 -1
  5. package/dist/cjs/src/primitives/Hash.js +38 -1
  6. package/dist/cjs/src/primitives/Hash.js.map +1 -1
  7. package/dist/cjs/src/totp/index.js +18 -0
  8. package/dist/cjs/src/totp/index.js.map +1 -0
  9. package/dist/cjs/src/totp/totp.js +94 -0
  10. package/dist/cjs/src/totp/totp.js.map +1 -0
  11. package/dist/cjs/src/transaction/MerklePath.js +8 -4
  12. package/dist/cjs/src/transaction/MerklePath.js.map +1 -1
  13. package/dist/cjs/src/transaction/Transaction.js +12 -2
  14. package/dist/cjs/src/transaction/Transaction.js.map +1 -1
  15. package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
  16. package/dist/esm/mod.js +1 -0
  17. package/dist/esm/mod.js.map +1 -1
  18. package/dist/esm/src/compat/Utxo.js.map +1 -1
  19. package/dist/esm/src/primitives/Hash.js +38 -0
  20. package/dist/esm/src/primitives/Hash.js.map +1 -1
  21. package/dist/esm/src/totp/index.js +2 -0
  22. package/dist/esm/src/totp/index.js.map +1 -0
  23. package/dist/esm/src/totp/totp.js +87 -0
  24. package/dist/esm/src/totp/totp.js.map +1 -0
  25. package/dist/esm/src/transaction/MerklePath.js +8 -4
  26. package/dist/esm/src/transaction/MerklePath.js.map +1 -1
  27. package/dist/esm/src/transaction/Transaction.js +12 -2
  28. package/dist/esm/src/transaction/Transaction.js.map +1 -1
  29. package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
  30. package/dist/types/mod.d.ts +1 -0
  31. package/dist/types/mod.d.ts.map +1 -1
  32. package/dist/types/src/compat/Utxo.d.ts +2 -2
  33. package/dist/types/src/compat/Utxo.d.ts.map +1 -1
  34. package/dist/types/src/primitives/Hash.d.ts +9 -0
  35. package/dist/types/src/primitives/Hash.d.ts.map +1 -1
  36. package/dist/types/src/totp/index.d.ts +2 -0
  37. package/dist/types/src/totp/index.d.ts.map +1 -0
  38. package/dist/types/src/totp/totp.d.ts +42 -0
  39. package/dist/types/src/totp/totp.d.ts.map +1 -0
  40. package/dist/types/src/transaction/MerklePath.d.ts.map +1 -1
  41. package/dist/types/src/transaction/Transaction.d.ts +3 -1
  42. package/dist/types/src/transaction/Transaction.d.ts.map +1 -1
  43. package/dist/types/tsconfig.types.tsbuildinfo +1 -1
  44. package/docs/primitives.md +26 -9
  45. package/docs/transaction.md +8 -2
  46. package/mod.ts +1 -0
  47. package/package.json +11 -1
  48. package/src/compat/Utxo.ts +31 -31
  49. package/src/primitives/Hash.ts +46 -0
  50. package/src/totp/__tests/totp.test.ts +80 -0
  51. package/src/totp/index.ts +1 -0
  52. package/src/totp/totp.ts +142 -0
  53. package/src/transaction/MerklePath.ts +25 -31
  54. package/src/transaction/Transaction.ts +38 -29
@@ -0,0 +1,80 @@
1
+ import { toArray } from "../../../dist/cjs/src/primitives/utils.js";
2
+ import { TOTP } from "../../../dist/cjs/src/totp/totp.js";
3
+
4
+ const secret = toArray("48656c6c6f21deadbeef", 'hex');
5
+ const period = 30; //sec
6
+ const periodMS = 30 * 1000; //ms
7
+ const options = {
8
+ digits: 6,
9
+ period,
10
+ algorithm: "SHA-1",
11
+ };
12
+
13
+ describe("totp generation and validation", () => {
14
+ beforeEach(() => jest.useFakeTimers());
15
+ afterEach(() => jest.resetAllMocks());
16
+
17
+ test.each([
18
+ {
19
+ time: 0,
20
+ expected: "282760",
21
+ description: "should generate token at Unix epoch start",
22
+ },
23
+ {
24
+ time: 1465324707000,
25
+ expected: "341128",
26
+ description: "should generate token for a specific timestamp in 2016",
27
+ },
28
+ {
29
+ time: 1665644340000 + 1,
30
+ expected: "886842",
31
+ description: "should generate correct token at the start of the cycle",
32
+ },
33
+ {
34
+ time: 1665644340000 - 1,
35
+ expected: "134996",
36
+ description: "should generate correct token at the end of the cycle",
37
+ },
38
+ {
39
+ time: 1365324707000,
40
+ expected: "089029",
41
+ description: "should generate token with a leading zero",
42
+ },
43
+ ])("$description", async ({ time, expected }) => {
44
+ jest.setSystemTime(time);
45
+
46
+ //check if expected passcode is generated
47
+ const passcode = TOTP.generate(secret, options);
48
+ expect(passcode).toEqual(expected);
49
+
50
+ expect(TOTP.validate(secret, "000000", options)).toEqual(false); //this passcode should not be valid for any of above test cases
51
+
52
+ //should not be valid for only a part of passcode
53
+ expect(TOTP.validate(secret, passcode.slice(1), options)).toEqual(false);
54
+
55
+ expect(TOTP.validate(secret, passcode, options)).toEqual(true);
56
+
57
+ const checkAdjacentWindow = (
58
+ timeOfGeneration: number,
59
+ expected: boolean
60
+ ) => {
61
+ jest.setSystemTime(timeOfGeneration);
62
+ const adjacentTimewindowPasscode = TOTP.generate(secret, options);
63
+
64
+ jest.setSystemTime(time);
65
+ expect(
66
+ TOTP.validate(secret, adjacentTimewindowPasscode, options)
67
+ ).toEqual(expected);
68
+ };
69
+
70
+ //because the 'skew' is '1' by default, the passcode for the next window also should be valid
71
+ checkAdjacentWindow(time + periodMS, true);
72
+ checkAdjacentWindow(time - periodMS, true);
73
+
74
+ //for 'skew': 1, other passcodes for further timewindows should not be valid
75
+ for (let i = 2; i < 10; i++) {
76
+ checkAdjacentWindow(time + i * periodMS, false);
77
+ checkAdjacentWindow(time - i * periodMS, false);
78
+ }
79
+ });
80
+ });
@@ -0,0 +1 @@
1
+ export * from './totp.js'
@@ -0,0 +1,142 @@
1
+ import { SHA1HMAC, SHA256HMAC, SHA512HMAC } from '../primitives/Hash.js'
2
+ import BigNumber from '../primitives/BigNumber.js'
3
+
4
+ export type TOTPAlgorithm = 'SHA-1' | 'SHA-256' | 'SHA-512'
5
+
6
+ /**
7
+ * Options for TOTP generation.
8
+ * @param {number} [digits=6] - The number of digits in the OTP.
9
+ * @param {TOTPAlgorithm} [algorithm="SHA-1"] - Algorithm used for hashing.
10
+ * @param {number} [period=30] - The time period for OTP validity in seconds.
11
+ * @param {number} [timestamp=Date.now()] - The current timestamp.
12
+ */
13
+ export interface TOTPOptions {
14
+ digits?: number
15
+ algorithm?: TOTPAlgorithm
16
+ period?: number
17
+ timestamp?: number
18
+ }
19
+
20
+ /**
21
+ * Options for TOTP validation.
22
+ * @param {number} [skew=1] - The number of time periods to check before and after the current time period.
23
+ */
24
+ export type TOTPValidateOptions = TOTPOptions & {
25
+ skew?: number
26
+ }
27
+
28
+ export class TOTP {
29
+ /**
30
+ * Generates a Time-based One-Time Password (TOTP).
31
+ * @param {number[]} secret - The secret key for TOTP.
32
+ * @param {TOTPOptions} options - Optional parameters for TOTP.
33
+ * @returns {string} The generated TOTP.
34
+ */
35
+ static generate (secret: number[], options?: TOTPOptions): string {
36
+ const _options = this.withDefaultOptions(options)
37
+
38
+ const counter = this.getCounter(_options.timestamp, _options.period)
39
+ const otp = generateHOTP(secret, counter, _options)
40
+ return otp
41
+ }
42
+
43
+ /**
44
+ * Validates a Time-based One-Time Password (TOTP).
45
+ * @param {number[]} secret - The secret key for TOTP.
46
+ * @param {string} passcode - The passcode to validate.
47
+ * @param {TOTPValidateOptions} options - Optional parameters for TOTP validation.
48
+ * @returns {boolean} A boolean indicating whether the passcode is valid.
49
+ */
50
+ static validate (
51
+ secret: number[],
52
+ passcode: string,
53
+ options?: TOTPValidateOptions
54
+ ): boolean {
55
+ const _options = this.withDefaultValidateOptions(options)
56
+ passcode = passcode.trim()
57
+ if (passcode.length != _options.digits) {
58
+ return false
59
+ }
60
+
61
+ const counter = this.getCounter(_options.timestamp, _options.period)
62
+
63
+ const counters = [counter]
64
+ for (let i = 1; i <= _options.skew; i++) {
65
+ counters.push(counter + i)
66
+ counters.push(counter - i)
67
+ }
68
+
69
+ for (const c of counters) {
70
+ if (passcode === generateHOTP(secret, c, _options)) {
71
+ return true
72
+ }
73
+ }
74
+
75
+ return false
76
+ }
77
+
78
+ private static getCounter (timestamp: number, period: number): number {
79
+ const epochSeconds = Math.floor(timestamp / 1000)
80
+ const counter = Math.floor(epochSeconds / period)
81
+ return counter
82
+ }
83
+
84
+ private static withDefaultOptions (
85
+ options?: TOTPOptions
86
+ ): Required<TOTPOptions> {
87
+ return {
88
+ digits: 2,
89
+ algorithm: 'SHA-1',
90
+ period: 30,
91
+ timestamp: Date.now(),
92
+ ...options
93
+ }
94
+ }
95
+
96
+ private static withDefaultValidateOptions (
97
+ options?: TOTPValidateOptions
98
+ ): Required<TOTPValidateOptions> {
99
+ return { skew: 1, ...this.withDefaultOptions(options) }
100
+ }
101
+ }
102
+
103
+ function generateHOTP (
104
+ secret: number[],
105
+ counter: number,
106
+ options: Required<TOTPOptions>
107
+ ): string {
108
+ const timePad = new BigNumber(counter).toArray('be', 8)
109
+ console.log({ timePad })
110
+ const hmac = calcHMAC(secret, timePad, options.algorithm)
111
+
112
+ const signature = hmac.digest()
113
+ const signatureHex = hmac.digestHex()
114
+
115
+ // RFC 4226 https://datatracker.ietf.org/doc/html/rfc4226#section-5.4
116
+ const offset = signature[signature.length - 1] & 0x0f // offset is the last byte in the hmac
117
+ const fourBytesRange = signature.slice(offset, offset + 4) // starting from offset, get 4 bytes
118
+ const mask = 0x7fffffff // 32-bit number with a leading 0 followed by 31 ones [0111 (...) 1111]
119
+ const masked = new BigNumber(fourBytesRange).toNumber() & mask
120
+
121
+ console.log({ signatureHex, signature, offset, fourBytesRange, mask, masked })
122
+
123
+ const otp = masked.toString().slice(-options.digits)
124
+ return otp
125
+ }
126
+
127
+ function calcHMAC (
128
+ secret: number[],
129
+ timePad: number[],
130
+ algorithm: TOTPAlgorithm
131
+ ) {
132
+ switch (algorithm) {
133
+ case 'SHA-1':
134
+ return new SHA1HMAC(secret).update(timePad)
135
+ case 'SHA-256':
136
+ return new SHA256HMAC(secret).update(timePad)
137
+ case 'SHA-512':
138
+ return new SHA512HMAC(secret).update(timePad)
139
+ default:
140
+ throw new Error('unsupported HMAC algorithm')
141
+ }
142
+ }
@@ -3,10 +3,10 @@ import { hash256 } from '../primitives/Hash.js'
3
3
  import ChainTracker from './ChainTracker.js'
4
4
 
5
5
  export interface MerklePathLeaf {
6
- offset: number
7
- hash?: string
8
- txid?: boolean
9
- duplicate?: boolean
6
+ offset: number
7
+ hash?: string
8
+ txid?: boolean
9
+ duplicate?: boolean
10
10
  }
11
11
 
12
12
  /**
@@ -218,13 +218,13 @@ export default class MerklePath {
218
218
 
219
219
  /**
220
220
  * Find leaf with `offset` at `height` or compute from level below, recursively.
221
- *
221
+ *
222
222
  * Does not add computed leaves to path.
223
- *
223
+ *
224
224
  * @param height
225
- * @param offset
225
+ * @param offset
226
226
  */
227
- findOrComputeLeaf(height: number, offset: number) : MerklePathLeaf | undefined {
227
+ findOrComputeLeaf (height: number, offset: number): MerklePathLeaf | undefined {
228
228
  const hash = (m: string): string => toHex((
229
229
  hash256(toArray(m, 'hex').reverse())
230
230
  ).reverse())
@@ -245,10 +245,7 @@ export default class MerklePath {
245
245
  if (!leaf1) return undefined
246
246
 
247
247
  let workinghash: string
248
- if (leaf1.duplicate)
249
- workinghash = hash(leaf0.hash + leaf0.hash)
250
- else
251
- workinghash = hash(leaf1.hash + leaf0.hash)
248
+ if (leaf1.duplicate) { workinghash = hash(leaf0.hash + leaf0.hash) } else { workinghash = hash(leaf1.hash + leaf0.hash) }
252
249
  leaf = {
253
250
  offset,
254
251
  hash: workinghash
@@ -312,21 +309,19 @@ export default class MerklePath {
312
309
  * Assumes that at least all required nodes are present.
313
310
  * Leaves all levels sorted by increasing offset.
314
311
  */
315
- trim() {
312
+ trim () {
316
313
  const pushIfNew = (v: number, a: number[]) => {
317
- if (a.length === 0 || a.slice(-1)[0] !== v)
318
- a.push(v)
314
+ if (a.length === 0 || a.slice(-1)[0] !== v) { a.push(v) }
319
315
  }
320
316
 
321
317
  const dropOffsetsFromLevel = (dropOffsets: number[], level: number) => {
322
318
  for (let i = dropOffsets.length; i >= 0; i--) {
323
319
  const l = this.path[level].findIndex(n => n.offset === dropOffsets[i])
324
- if (l >= 0)
325
- this.path[level].splice(l, 1)
320
+ if (l >= 0) { this.path[level].splice(l, 1) }
326
321
  }
327
322
  }
328
323
 
329
- const nextComputedOffsets = (cos: number[]) : number[] => {
324
+ const nextComputedOffsets = (cos: number[]): number[] => {
330
325
  const ncos: number[] = []
331
326
  for (const o of cos) {
332
327
  pushIfNew(o >> 1, ncos)
@@ -334,25 +329,25 @@ export default class MerklePath {
334
329
  return ncos
335
330
  }
336
331
 
337
- let computedOffsets: number[] = []; // in next level
332
+ let computedOffsets: number[] = [] // in next level
338
333
  let dropOffsets: number[] = []
339
334
  for (let h = 0; h < this.path.length; h++) {
340
335
  // Sort each level by increasing offset order
341
336
  this.path[h].sort((a, b) => a.offset - b.offset)
342
337
  }
343
338
  for (let l = 0; l < this.path[0].length; l++) {
344
- const n = this.path[0][l]
345
- if (n.txid) {
346
- // level 0 must enable computing level 1 for txid nodes
347
- pushIfNew(n.offset >> 1, computedOffsets)
348
- } else {
349
- const isOdd = n.offset % 2 === 1
350
- const peer = this.path[0][l + (isOdd ? -1 : 1)]
351
- if (!peer.txid) {
352
- // drop non-txid level 0 nodes without a txid peer
353
- pushIfNew(peer.offset, dropOffsets)
354
- }
339
+ const n = this.path[0][l]
340
+ if (n.txid) {
341
+ // level 0 must enable computing level 1 for txid nodes
342
+ pushIfNew(n.offset >> 1, computedOffsets)
343
+ } else {
344
+ const isOdd = n.offset % 2 === 1
345
+ const peer = this.path[0][l + (isOdd ? -1 : 1)]
346
+ if (!peer.txid) {
347
+ // drop non-txid level 0 nodes without a txid peer
348
+ pushIfNew(peer.offset, dropOffsets)
355
349
  }
350
+ }
356
351
  }
357
352
  dropOffsetsFromLevel(dropOffsets, 0)
358
353
  for (let h = 1; h < this.path.length; h++) {
@@ -361,5 +356,4 @@ export default class MerklePath {
361
356
  dropOffsetsFromLevel(dropOffsets, h)
362
357
  }
363
358
  }
364
-
365
359
  }
@@ -62,7 +62,7 @@ export default class Transaction {
62
62
  * @param beef A binary representation of a transaction in BEEF format.
63
63
  * @returns An anchored transaction, linked to its associated inputs populated with merkle paths.
64
64
  */
65
- static fromBEEF(beef: number[]): Transaction {
65
+ static fromBEEF (beef: number[]): Transaction {
66
66
  const reader = new Reader(beef)
67
67
  // Read the version
68
68
  const version = reader.readUInt32LE()
@@ -122,13 +122,12 @@ export default class Transaction {
122
122
  return transactions[lastTXID].tx
123
123
  }
124
124
 
125
-
126
125
  /**
127
126
  * Creates a new transaction, linked to its inputs and their associated merkle paths, from a EF (BRC-30) structure.
128
127
  * @param ef A binary representation of a transaction in EF format.
129
128
  * @returns An extended transaction, linked to its associated inputs by locking script and satoshis amounts only.
130
129
  */
131
- static fromEF(ef: number[]): Transaction {
130
+ static fromEF (ef: number[]): Transaction {
132
131
  const br = new Reader(ef)
133
132
  const version = br.readUInt32LE()
134
133
  if (toHex(br.read(6)) !== '0000000000ef') throw new Error('Invalid EF marker')
@@ -190,7 +189,7 @@ export default class Transaction {
190
189
  * outputs: { vout: number, offset: number, length: number }[]
191
190
  * }
192
191
  */
193
- static parseScriptOffsets(bin: number[]): {
192
+ static parseScriptOffsets (bin: number[]): {
194
193
  inputs: Array<{ vin: number, offset: number, length: number }>
195
194
  outputs: Array<{ vout: number, offset: number, length: number }>
196
195
  } {
@@ -216,7 +215,7 @@ export default class Transaction {
216
215
  return { inputs, outputs }
217
216
  }
218
217
 
219
- private static fromReader(br: Reader): Transaction {
218
+ private static fromReader (br: Reader): Transaction {
220
219
  const version = br.readUInt32LE()
221
220
  const inputsLength = br.readVarIntNum()
222
221
  const inputs: TransactionInput[] = []
@@ -257,7 +256,7 @@ export default class Transaction {
257
256
  * @param {number[]} bin - The binary array representation of the transaction.
258
257
  * @returns {Transaction} - A new Transaction instance.
259
258
  */
260
- static fromBinary(bin: number[]): Transaction {
259
+ static fromBinary (bin: number[]): Transaction {
261
260
  const br = new Reader(bin)
262
261
  return Transaction.fromReader(br)
263
262
  }
@@ -269,7 +268,7 @@ export default class Transaction {
269
268
  * @param {string} hex - The hexadecimal string representation of the transaction.
270
269
  * @returns {Transaction} - A new Transaction instance.
271
270
  */
272
- static fromHex(hex: string): Transaction {
271
+ static fromHex (hex: string): Transaction {
273
272
  return Transaction.fromBinary(toArray(hex, 'hex'))
274
273
  }
275
274
 
@@ -280,7 +279,7 @@ export default class Transaction {
280
279
  * @param {string} hex - The hexadecimal string representation of the transaction EF.
281
280
  * @returns {Transaction} - A new Transaction instance.
282
281
  */
283
- static fromHexEF(hex: string): Transaction {
282
+ static fromHexEF (hex: string): Transaction {
284
283
  return Transaction.fromEF(toArray(hex, 'hex'))
285
284
  }
286
285
 
@@ -291,11 +290,11 @@ export default class Transaction {
291
290
  * @param {string} hex - The hexadecimal string representation of the transaction BEEF.
292
291
  * @returns {Transaction} - A new Transaction instance.
293
292
  */
294
- static fromHexBEEF(hex: string): Transaction {
293
+ static fromHexBEEF (hex: string): Transaction {
295
294
  return Transaction.fromBEEF(toArray(hex, 'hex'))
296
295
  }
297
296
 
298
- constructor(
297
+ constructor (
299
298
  version: number = 1,
300
299
  inputs: TransactionInput[] = [],
301
300
  outputs: TransactionOutput[] = [],
@@ -317,7 +316,7 @@ export default class Transaction {
317
316
  * @param {TransactionInput} input - The TransactionInput object to add to the transaction.
318
317
  * @throws {Error} - If the input does not have a sourceTXID or sourceTransaction defined.
319
318
  */
320
- addInput(input: TransactionInput): void {
319
+ addInput (input: TransactionInput): void {
321
320
  if (
322
321
  typeof input.sourceTXID === 'undefined' &&
323
322
  typeof input.sourceTransaction === 'undefined'
@@ -337,7 +336,7 @@ export default class Transaction {
337
336
  *
338
337
  * @param {TransactionOutput} output - The TransactionOutput object to add to the transaction.
339
338
  */
340
- addOutput(output: TransactionOutput): void {
339
+ addOutput (output: TransactionOutput): void {
341
340
  this.cachedHash = undefined
342
341
  this.outputs.push(output)
343
342
  }
@@ -347,7 +346,7 @@ export default class Transaction {
347
346
  *
348
347
  * @param {Record<string, any>} metadata - The metadata object to merge into the existing metadata.
349
348
  */
350
- updateMetadata(metadata: Record<string, any>): void {
349
+ updateMetadata (metadata: Record<string, any>): void {
351
350
  this.metadata = {
352
351
  ...this.metadata,
353
352
  ...metadata
@@ -365,7 +364,7 @@ export default class Transaction {
365
364
  *
366
365
  * TODO: Benford's law change distribution.
367
366
  */
368
- async fee(modelOrFee?: FeeModel | number, changeDistribution: 'equal' | 'random' = 'equal'): Promise<void> {
367
+ async fee (modelOrFee?: FeeModel | number, changeDistribution: 'equal' | 'random' = 'equal'): Promise<void> {
369
368
  this.cachedHash = undefined
370
369
  if (typeof modelOrFee === 'undefined') {
371
370
  modelOrFee = new SatoshisPerKilobyte(10)
@@ -426,7 +425,7 @@ export default class Transaction {
426
425
  *
427
426
  * @returns The current transaction fee
428
427
  */
429
- getFee(): number {
428
+ getFee (): number {
430
429
  let totalIn = 0
431
430
  for (const input of this.inputs) {
432
431
  if (typeof input.sourceTransaction !== 'object') {
@@ -444,7 +443,7 @@ export default class Transaction {
444
443
  /**
445
444
  * Signs a transaction, hydrating all its unlocking scripts based on the provided script templates where they are available.
446
445
  */
447
- async sign(): Promise<void> {
446
+ async sign (): Promise<void> {
448
447
  this.cachedHash = undefined
449
448
  for (const out of this.outputs) {
450
449
  if (typeof out.satoshis === 'undefined') {
@@ -475,7 +474,7 @@ export default class Transaction {
475
474
  * @param broadcaster The Broadcaster instance wwhere the transaction will be sent
476
475
  * @returns A BroadcastResponse or BroadcastFailure from the Broadcaster
477
476
  */
478
- async broadcast(broadcaster: Broadcaster = defaultBroadcaster()): Promise<BroadcastResponse | BroadcastFailure> {
477
+ async broadcast (broadcaster: Broadcaster = defaultBroadcaster()): Promise<BroadcastResponse | BroadcastFailure> {
479
478
  return await broadcaster.broadcast(this)
480
479
  }
481
480
 
@@ -484,7 +483,7 @@ export default class Transaction {
484
483
  *
485
484
  * @returns {number[]} - The binary array representation of the transaction.
486
485
  */
487
- toBinary(): number[] {
486
+ toBinary (): number[] {
488
487
  const writer = new Writer()
489
488
  writer.writeUInt32LE(this.version)
490
489
  writer.writeVarIntNum(this.inputs.length)
@@ -516,7 +515,7 @@ export default class Transaction {
516
515
  *
517
516
  * @returns {number[]} - The BRC-30 EF representation of the transaction.
518
517
  */
519
- toEF(): number[] {
518
+ toEF (): number[] {
520
519
  const writer = new Writer()
521
520
  writer.writeUInt32LE(this.version)
522
521
  writer.write([0, 0, 0, 0, 0, 0xef])
@@ -556,7 +555,7 @@ export default class Transaction {
556
555
  *
557
556
  * @returns {string} - The hexadecimal string representation of the transaction EF.
558
557
  */
559
- toHexEF(): string {
558
+ toHexEF (): string {
560
559
  return toHex(this.toEF())
561
560
  }
562
561
 
@@ -565,7 +564,7 @@ export default class Transaction {
565
564
  *
566
565
  * @returns {string} - The hexadecimal string representation of the transaction.
567
566
  */
568
- toHex(): string {
567
+ toHex (): string {
569
568
  return toHex(this.toBinary())
570
569
  }
571
570
 
@@ -574,7 +573,7 @@ export default class Transaction {
574
573
  *
575
574
  * @returns {string} - The hexadecimal string representation of the transaction BEEF.
576
575
  */
577
- toHexBEEF(): string {
576
+ toHexBEEF (): string {
578
577
  return toHex(this.toBEEF())
579
578
  }
580
579
 
@@ -584,7 +583,7 @@ export default class Transaction {
584
583
  * @param {'hex' | undefined} enc - The encoding to use for the hash. If 'hex', returns a hexadecimal string; otherwise returns a binary array.
585
584
  * @returns {string | number[]} - The hash of the transaction in the specified format.
586
585
  */
587
- hash(enc?: 'hex'): number[] | string {
586
+ hash (enc?: 'hex'): number[] | string {
588
587
  let hash
589
588
  if (this.cachedHash) {
590
589
  hash = this.cachedHash
@@ -604,21 +603,21 @@ export default class Transaction {
604
603
  *
605
604
  * @returns {number[]} - The ID of the transaction in the binary array format.
606
605
  */
607
- id(): number[]
606
+ id (): number[]
608
607
  /**
609
608
  * Calculates the transaction's ID in hexadecimal format.
610
609
  *
611
610
  * @param {'hex'} enc - The encoding to use for the ID. If 'hex', returns a hexadecimal string.
612
611
  * @returns {string} - The ID of the transaction in the hex format.
613
612
  */
614
- id(enc: 'hex'): string
613
+ id (enc: 'hex'): string
615
614
  /**
616
615
  * Calculates the transaction's ID.
617
616
  *
618
617
  * @param {'hex' | undefined} enc - The encoding to use for the ID. If 'hex', returns a hexadecimal string; otherwise returns a binary array.
619
618
  * @returns {string | number[]} - The ID of the transaction in the specified format.
620
619
  */
621
- id(enc?: 'hex'): number[] | string {
620
+ id (enc?: 'hex'): number[] | string {
622
621
  const id = [...this.hash() as number[]]
623
622
  id.reverse()
624
623
  if (enc === 'hex') {
@@ -633,8 +632,10 @@ export default class Transaction {
633
632
  * @param chainTracker - An instance of ChainTracker, a Bitcoin block header tracker. If the value is set to 'scripts only', headers will not be verified. If not provided then the default chain tracker will be used.
634
633
  *
635
634
  * @returns Whether the transaction is valid according to the rules of SPV.
635
+ *
636
+ * @example tx.verify(new WhatsOnChain(), new SatoshisPerKilobyte(1))
636
637
  */
637
- async verify(chainTracker: ChainTracker | 'scripts only' = defaultChainTracker()): Promise<boolean> {
638
+ async verify (chainTracker: ChainTracker | 'scripts only' = defaultChainTracker(), feeModel?: FeeModel): Promise<boolean> {
638
639
  // If the transaction has a valid merkle path, verification is complete.
639
640
  if (typeof this.merklePath === 'object' && chainTracker !== 'scripts only') {
640
641
  const proofValid = await this.merklePath.verify(
@@ -647,6 +648,14 @@ export default class Transaction {
647
648
  }
648
649
  }
649
650
 
651
+ if (typeof feeModel !== 'undefined') {
652
+ const cpTx = Transaction.fromHexEF(this.toHexEF())
653
+ delete cpTx.outputs[0].satoshis
654
+ cpTx.outputs[0].change = true
655
+ await cpTx.fee(feeModel)
656
+ if (this.getFee() < cpTx.getFee()) throw new Error(`Verification failed because the transaction ${this.id('hex')} has an insufficient fee and has not been mined.`)
657
+ }
658
+
650
659
  // Verify each input transaction and evaluate the spend events.
651
660
  // Also, keep a total of the input amounts for later.
652
661
  let inputTotal = 0
@@ -660,7 +669,7 @@ export default class Transaction {
660
669
  }
661
670
  const sourceOutput = input.sourceTransaction.outputs[input.sourceOutputIndex]
662
671
  inputTotal += sourceOutput.satoshis
663
- const inputVerified = await input.sourceTransaction.verify(chainTracker)
672
+ const inputVerified = await input.sourceTransaction.verify(chainTracker, feeModel)
664
673
  if (!inputVerified) {
665
674
  return false
666
675
  }
@@ -702,7 +711,7 @@ export default class Transaction {
702
711
  *
703
712
  * @returns The serialized BEEF structure
704
713
  */
705
- toBEEF(): number[] {
714
+ toBEEF (): number[] {
706
715
  const writer = new Writer()
707
716
  writer.writeUInt32LE(4022206465)
708
717
  const BUMPs: MerklePath[] = []