@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.
- package/dist/cjs/mod.js +1 -0
- package/dist/cjs/mod.js.map +1 -1
- package/dist/cjs/package.json +1 -1
- package/dist/cjs/src/compat/Utxo.js.map +1 -1
- package/dist/cjs/src/primitives/Hash.js +38 -1
- package/dist/cjs/src/primitives/Hash.js.map +1 -1
- package/dist/cjs/src/totp/index.js +18 -0
- package/dist/cjs/src/totp/index.js.map +1 -0
- package/dist/cjs/src/totp/totp.js +94 -0
- package/dist/cjs/src/totp/totp.js.map +1 -0
- package/dist/cjs/src/transaction/MerklePath.js +8 -4
- package/dist/cjs/src/transaction/MerklePath.js.map +1 -1
- package/dist/cjs/src/transaction/Transaction.js +12 -2
- package/dist/cjs/src/transaction/Transaction.js.map +1 -1
- package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
- package/dist/esm/mod.js +1 -0
- package/dist/esm/mod.js.map +1 -1
- package/dist/esm/src/compat/Utxo.js.map +1 -1
- package/dist/esm/src/primitives/Hash.js +38 -0
- package/dist/esm/src/primitives/Hash.js.map +1 -1
- package/dist/esm/src/totp/index.js +2 -0
- package/dist/esm/src/totp/index.js.map +1 -0
- package/dist/esm/src/totp/totp.js +87 -0
- package/dist/esm/src/totp/totp.js.map +1 -0
- package/dist/esm/src/transaction/MerklePath.js +8 -4
- package/dist/esm/src/transaction/MerklePath.js.map +1 -1
- package/dist/esm/src/transaction/Transaction.js +12 -2
- package/dist/esm/src/transaction/Transaction.js.map +1 -1
- package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
- package/dist/types/mod.d.ts +1 -0
- package/dist/types/mod.d.ts.map +1 -1
- package/dist/types/src/compat/Utxo.d.ts +2 -2
- package/dist/types/src/compat/Utxo.d.ts.map +1 -1
- package/dist/types/src/primitives/Hash.d.ts +9 -0
- package/dist/types/src/primitives/Hash.d.ts.map +1 -1
- package/dist/types/src/totp/index.d.ts +2 -0
- package/dist/types/src/totp/index.d.ts.map +1 -0
- package/dist/types/src/totp/totp.d.ts +42 -0
- package/dist/types/src/totp/totp.d.ts.map +1 -0
- package/dist/types/src/transaction/MerklePath.d.ts.map +1 -1
- package/dist/types/src/transaction/Transaction.d.ts +3 -1
- package/dist/types/src/transaction/Transaction.d.ts.map +1 -1
- package/dist/types/tsconfig.types.tsbuildinfo +1 -1
- package/docs/primitives.md +26 -9
- package/docs/transaction.md +8 -2
- package/mod.ts +1 -0
- package/package.json +11 -1
- package/src/compat/Utxo.ts +31 -31
- package/src/primitives/Hash.ts +46 -0
- package/src/totp/__tests/totp.test.ts +80 -0
- package/src/totp/index.ts +1 -0
- package/src/totp/totp.ts +142 -0
- package/src/transaction/MerklePath.ts +25 -31
- 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'
|
package/src/totp/totp.ts
ADDED
|
@@ -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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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)
|
|
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[])
|
|
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[] = []
|
|
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
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
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[] = []
|