@bsv/sdk 2.0.11 → 2.0.13
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/package.json +1 -1
- package/dist/cjs/src/auth/clients/__tests__/AuthFetch.additional.test.js +827 -0
- package/dist/cjs/src/auth/clients/__tests__/AuthFetch.additional.test.js.map +1 -0
- package/dist/cjs/src/auth/transports/__tests__/SimplifiedFetchTransport.additional.test.js +654 -0
- package/dist/cjs/src/auth/transports/__tests__/SimplifiedFetchTransport.additional.test.js.map +1 -0
- package/dist/cjs/src/overlay-tools/HostReputationTracker.js +21 -13
- package/dist/cjs/src/overlay-tools/HostReputationTracker.js.map +1 -1
- package/dist/cjs/src/primitives/PrivateKey.js +3 -3
- package/dist/cjs/src/primitives/PrivateKey.js.map +1 -1
- package/dist/cjs/src/script/Spend.js +17 -9
- package/dist/cjs/src/script/Spend.js.map +1 -1
- package/dist/cjs/src/storage/StorageDownloader.js +6 -6
- package/dist/cjs/src/storage/StorageDownloader.js.map +1 -1
- package/dist/cjs/src/storage/StorageUtils.js +1 -1
- package/dist/cjs/src/storage/StorageUtils.js.map +1 -1
- package/dist/cjs/src/transaction/MerklePath.js +168 -27
- package/dist/cjs/src/transaction/MerklePath.js.map +1 -1
- package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
- package/dist/esm/src/auth/clients/__tests__/AuthFetch.additional.test.js +825 -0
- package/dist/esm/src/auth/clients/__tests__/AuthFetch.additional.test.js.map +1 -0
- package/dist/esm/src/auth/transports/__tests__/SimplifiedFetchTransport.additional.test.js +619 -0
- package/dist/esm/src/auth/transports/__tests__/SimplifiedFetchTransport.additional.test.js.map +1 -0
- package/dist/esm/src/overlay-tools/HostReputationTracker.js +21 -13
- package/dist/esm/src/overlay-tools/HostReputationTracker.js.map +1 -1
- package/dist/esm/src/primitives/PrivateKey.js +3 -3
- package/dist/esm/src/primitives/PrivateKey.js.map +1 -1
- package/dist/esm/src/script/Spend.js +17 -9
- package/dist/esm/src/script/Spend.js.map +1 -1
- package/dist/esm/src/storage/StorageDownloader.js +6 -6
- package/dist/esm/src/storage/StorageDownloader.js.map +1 -1
- package/dist/esm/src/storage/StorageUtils.js +1 -1
- package/dist/esm/src/storage/StorageUtils.js.map +1 -1
- package/dist/esm/src/transaction/MerklePath.js +168 -27
- package/dist/esm/src/transaction/MerklePath.js.map +1 -1
- package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
- package/dist/types/src/auth/clients/__tests__/AuthFetch.additional.test.d.ts +21 -0
- package/dist/types/src/auth/clients/__tests__/AuthFetch.additional.test.d.ts.map +1 -0
- package/dist/types/src/auth/transports/__tests__/SimplifiedFetchTransport.additional.test.d.ts +2 -0
- package/dist/types/src/auth/transports/__tests__/SimplifiedFetchTransport.additional.test.d.ts.map +1 -0
- package/dist/types/src/overlay-tools/HostReputationTracker.d.ts.map +1 -1
- package/dist/types/src/script/Spend.d.ts.map +1 -1
- package/dist/types/src/transaction/MerklePath.d.ts +27 -0
- package/dist/types/src/transaction/MerklePath.d.ts.map +1 -1
- package/dist/types/tsconfig.types.tsbuildinfo +1 -1
- package/dist/umd/bundle.js +3 -3
- package/dist/umd/bundle.js.map +1 -1
- package/docs/reference/storage.md +1 -1
- package/docs/reference/transaction.md +40 -0
- package/package.json +1 -1
- package/src/auth/clients/__tests__/AuthFetch.additional.test.ts +1131 -0
- package/src/auth/transports/__tests__/SimplifiedFetchTransport.additional.test.ts +770 -0
- package/src/auth/utils/__tests/validateCertificates.test.ts +12 -9
- package/src/compat/__tests/Mnemonic.additional.test.ts +64 -0
- package/src/identity/__tests/IdentityClient.additional.test.ts +767 -0
- package/src/kvstore/__tests/LocalKVStore.additional.test.ts +611 -0
- package/src/kvstore/__tests/LocalKVStore.test.ts +4 -6
- package/src/kvstore/__tests/kvStoreInterpreter.test.ts +327 -0
- package/src/overlay-tools/HostReputationTracker.ts +17 -14
- package/src/overlay-tools/__tests/HostReputationTracker.additional.test.ts +561 -0
- package/src/overlay-tools/__tests/LookupResolver.additional.test.ts +612 -0
- package/src/overlay-tools/__tests/withDoubleSpendRetry.test.ts +278 -0
- package/src/primitives/PrivateKey.ts +3 -3
- package/src/primitives/__tests/BigNumber.additional.test.ts +79 -0
- package/src/primitives/__tests/Curve.additional.test.ts +208 -0
- package/src/primitives/__tests/ECDSA.additional.test.ts +122 -0
- package/src/primitives/__tests/Hash.additional.test.ts +59 -0
- package/src/primitives/__tests/JacobianPoint.test.ts +308 -0
- package/src/primitives/__tests/Point.additional.test.ts +503 -0
- package/src/primitives/__tests/PublicKey.additional.test.ts +383 -0
- package/src/primitives/__tests/Random.additional.test.ts +262 -0
- package/src/primitives/__tests/Signature.test.ts +333 -0
- package/src/primitives/__tests/TransactionSignature.additional.test.ts +241 -0
- package/src/registry/__tests/RegistryClient.additional.test.ts +750 -0
- package/src/remittance/__tests/BasicBRC29.additional.test.ts +657 -0
- package/src/remittance/__tests/RemittanceManager.additional.test.ts +1272 -0
- package/src/script/Spend.ts +19 -11
- package/src/script/__tests/LockingUnlockingScript.test.ts +79 -0
- package/src/script/__tests/Script.additional.test.ts +100 -0
- package/src/script/__tests/ScriptEvaluationError.test.ts +98 -0
- package/src/script/__tests/Spend.additional.test.ts +837 -0
- package/src/script/templates/__tests/RPuzzle.test.ts +134 -0
- package/src/storage/StorageDownloader.ts +6 -6
- package/src/storage/StorageUtils.ts +1 -1
- package/src/transaction/MerklePath.ts +196 -36
- package/src/transaction/__tests/BeefParty.additional.test.ts +22 -0
- package/src/transaction/__tests/Broadcaster.test.ts +159 -0
- package/src/transaction/__tests/MerklePath.bench.test.ts +105 -0
- package/src/transaction/__tests/MerklePath.test.ts +232 -21
- package/src/transaction/__tests/Transaction.additional.test.ts +225 -0
- package/src/transaction/broadcasters/__tests/ARC.additional.test.ts +585 -0
- package/src/transaction/broadcasters/__tests/Teranode.test.ts +349 -0
- package/src/transaction/chaintrackers/__tests/BlockHeadersService.test.ts +253 -0
- package/src/transaction/chaintrackers/__tests/DefaultChainTracker.test.ts +44 -0
- package/src/transaction/chaintrackers/__tests/WhatsOnChain.additional.test.ts +193 -0
- package/src/transaction/fee-models/__tests/SatoshisPerKilobyte.test.ts +262 -0
- package/src/transaction/http/__tests/BinaryFetchClient.test.ts +212 -0
- package/src/transaction/http/__tests/DefaultHttpClient.additional.test.ts +192 -0
- package/src/transaction/http/__tests/DefaultHttpClient.test.ts +71 -0
- package/src/wallet/__tests/ProtoWallet.additional.test.ts +134 -0
- package/src/wallet/__tests/WERR.test.ts +212 -0
- package/src/wallet/__tests/WalletClient.additional.test.ts +699 -0
- package/src/wallet/__tests/WalletClient.substrate.test.ts +759 -0
- package/src/wallet/__tests/WalletError.test.ts +290 -0
- package/src/wallet/__tests/validationHelpers.test.ts +1218 -0
- package/src/wallet/substrates/__tests/HTTPWalletJSON.test.ts +496 -0
- package/src/wallet/substrates/__tests/HTTPWalletWire.test.ts +273 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import MerklePath from '../../transaction/MerklePath'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Generate a random 32-byte hex hash.
|
|
5
|
+
*/
|
|
6
|
+
function randomHash (): string {
|
|
7
|
+
const bytes = new Uint8Array(32)
|
|
8
|
+
for (let i = 0; i < 32; i++) bytes[i] = Math.floor(Math.random() * 256)
|
|
9
|
+
return Array.from(bytes, b => b.toString(16).padStart(2, '0')).join('')
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Build a full-block compound MerklePath with `count` transactions at level 0.
|
|
14
|
+
* All leaves are txid: true; if count is odd the last leaf gets duplicate: true.
|
|
15
|
+
*/
|
|
16
|
+
function buildFullBlockPath (count: number): { mp: MerklePath, txids: string[] } {
|
|
17
|
+
const txids: string[] = []
|
|
18
|
+
const leaves: Array<{ offset: number, hash?: string, txid?: boolean, duplicate?: boolean }> = []
|
|
19
|
+
for (let i = 0; i < count; i++) {
|
|
20
|
+
const h = randomHash()
|
|
21
|
+
txids.push(h)
|
|
22
|
+
leaves.push({ offset: i, hash: h, txid: true })
|
|
23
|
+
}
|
|
24
|
+
if (count % 2 === 1) {
|
|
25
|
+
leaves.push({ offset: count, duplicate: true })
|
|
26
|
+
}
|
|
27
|
+
const mp = new MerklePath(1, [leaves])
|
|
28
|
+
return { mp, txids }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Pick `n` random items from `arr` without replacement.
|
|
33
|
+
*/
|
|
34
|
+
function pickRandom<T> (arr: T[], n: number): T[] {
|
|
35
|
+
const copy = [...arr]
|
|
36
|
+
const result: T[] = []
|
|
37
|
+
for (let i = 0; i < n && copy.length > 0; i++) {
|
|
38
|
+
const idx = Math.floor(Math.random() * copy.length)
|
|
39
|
+
result.push(copy.splice(idx, 1)[0])
|
|
40
|
+
}
|
|
41
|
+
return result
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
describe('MerklePath.extract() benchmarks', () => {
|
|
45
|
+
// Pre-build paths once so construction time is excluded from extract timing.
|
|
46
|
+
let path101: { mp: MerklePath, txids: string[] }
|
|
47
|
+
let path501: { mp: MerklePath, txids: string[] }
|
|
48
|
+
let path999: { mp: MerklePath, txids: string[] }
|
|
49
|
+
|
|
50
|
+
beforeAll(() => {
|
|
51
|
+
path101 = buildFullBlockPath(101)
|
|
52
|
+
path501 = buildFullBlockPath(501)
|
|
53
|
+
path999 = buildFullBlockPath(999)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
const runBench = (
|
|
57
|
+
label: string,
|
|
58
|
+
getPath: () => { mp: MerklePath, txids: string[] },
|
|
59
|
+
extractCount: number,
|
|
60
|
+
iterations: number = 5
|
|
61
|
+
): void => {
|
|
62
|
+
it(label, () => {
|
|
63
|
+
const { mp, txids } = getPath()
|
|
64
|
+
const targets = pickRandom(txids, extractCount)
|
|
65
|
+
|
|
66
|
+
const times: number[] = []
|
|
67
|
+
for (let i = 0; i < iterations; i++) {
|
|
68
|
+
const start = performance.now()
|
|
69
|
+
const extracted = mp.extract(targets)
|
|
70
|
+
const elapsed = performance.now() - start
|
|
71
|
+
times.push(elapsed)
|
|
72
|
+
// Correctness check on every iteration
|
|
73
|
+
for (const txid of targets) {
|
|
74
|
+
expect(extracted.computeRoot(txid)).toBe(mp.computeRoot(txid))
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const avg = times.reduce((a, b) => a + b, 0) / times.length
|
|
79
|
+
const min = Math.min(...times)
|
|
80
|
+
const max = Math.max(...times)
|
|
81
|
+
console.log(
|
|
82
|
+
`[${label}] avg=${avg.toFixed(1)}ms min=${min.toFixed(1)}ms max=${max.toFixed(1)}ms (${iterations} runs)`
|
|
83
|
+
)
|
|
84
|
+
})
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// --- 101 txids ---
|
|
88
|
+
runBench('101 txids, extract 1', () => path101, 1)
|
|
89
|
+
runBench('101 txids, extract 5', () => path101, 5)
|
|
90
|
+
runBench('101 txids, extract 10', () => path101, 10)
|
|
91
|
+
runBench('101 txids, extract 50', () => path101, 50)
|
|
92
|
+
|
|
93
|
+
// --- 501 txids ---
|
|
94
|
+
runBench('501 txids, extract 1', () => path501, 1)
|
|
95
|
+
runBench('501 txids, extract 5', () => path501, 5)
|
|
96
|
+
runBench('501 txids, extract 10', () => path501, 10)
|
|
97
|
+
runBench('501 txids, extract 50', () => path501, 50)
|
|
98
|
+
|
|
99
|
+
// --- 999 txids ---
|
|
100
|
+
runBench('999 txids, extract 1', () => path999, 1)
|
|
101
|
+
runBench('999 txids, extract 5', () => path999, 5)
|
|
102
|
+
runBench('999 txids, extract 10', () => path999, 10)
|
|
103
|
+
runBench('999 txids, extract 50', () => path999, 50)
|
|
104
|
+
runBench('999 txids, extract 100', () => path999, 100, 3)
|
|
105
|
+
})
|
|
@@ -112,6 +112,24 @@ const BRC74JSONTrimmed = {
|
|
|
112
112
|
}
|
|
113
113
|
BRC74JSONTrimmed.path[1] = []
|
|
114
114
|
|
|
115
|
+
const BLOCK_125632 = {
|
|
116
|
+
height: 125632,
|
|
117
|
+
merkleroot: '205b2e27c58601fc1a8de04c83b6b0c46f89c16b2161c93441b7e9269cf6bc4a',
|
|
118
|
+
tx: [
|
|
119
|
+
'17cba98da71fe75862aac894392f2ff604356db386767fec364877a5a9ff200c',
|
|
120
|
+
'14ce64bd223ec9bb42662b74fdcf94f96a209a1aee72b7ba7639db503150ec2e',
|
|
121
|
+
'90a2de85351cfadd2326b9b0098e9c453af09b2980835f57a1429bbb44beb872',
|
|
122
|
+
'a31f2ddfea7ddd4581dca3007ee99e58ea6baa97a8ac3b32bb4610baac9f7206',
|
|
123
|
+
'c36eeed6fbc0259d30804f59f804dfcda35a54461157d6ac9c094f0ea378f35c',
|
|
124
|
+
'17752483868c52a98407a0e226d73b42e214e0fad548541619d858e1fd4a9549',
|
|
125
|
+
'3b8c4460412cfc55be0d50308ba704a859bd6f83bfed01b0828c9b067cd69246',
|
|
126
|
+
'a3f1b9d4b3ef3b061af352fdc2d02048417030fef9282c36da689cd899437cdb',
|
|
127
|
+
'66e2b022da877621ef197e02c3ef7d3f820d33a86ead2e72bf966432ea6776f1',
|
|
128
|
+
'e988b5d7a2cec8e0759ade2e151737d1cdfdde68accff42938583ad12eb98b99',
|
|
129
|
+
'5e7a8a8ec3f912ac1c4e90279c04263f170ed055c0411c8d490b846f01e6a99e'
|
|
130
|
+
]
|
|
131
|
+
}
|
|
132
|
+
|
|
115
133
|
const BRC74Root =
|
|
116
134
|
'57aab6e6fb1b697174ffb64e062c4728f2ffd33ddcfa02a43b64d8cd29b483b4'
|
|
117
135
|
const BRC74TXID1 =
|
|
@@ -200,10 +218,9 @@ describe('MerklePath', () => {
|
|
|
200
218
|
it('Serializes and deserializes a combined trimmed path', () => {
|
|
201
219
|
const [pathA, pathB] = buildSplitPaths()
|
|
202
220
|
pathA.combine(pathB)
|
|
203
|
-
|
|
204
|
-
expect(
|
|
205
|
-
expect(deserialized
|
|
206
|
-
expect(deserialized!.computeRoot(BRC74TXID3)).toEqual(BRC74Root)
|
|
221
|
+
const deserialized = MerklePath.fromHex(pathA.toHex())
|
|
222
|
+
expect(deserialized.computeRoot(BRC74TXID2)).toEqual(BRC74Root)
|
|
223
|
+
expect(deserialized.computeRoot(BRC74TXID3)).toEqual(BRC74Root)
|
|
207
224
|
})
|
|
208
225
|
it('Constructs a compound path from all txids at level 0 only', () => {
|
|
209
226
|
// A single-level compound path: all txids for a block given at level 0, no higher levels.
|
|
@@ -213,24 +230,20 @@ describe('MerklePath', () => {
|
|
|
213
230
|
const tx2 = 'cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc'
|
|
214
231
|
const tx3 = 'dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd'
|
|
215
232
|
const root4 = merkleHash(merkleHash(tx3 + tx2) + merkleHash(tx1 + tx0))
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
expect(mp
|
|
226
|
-
expect(mp!.computeRoot(tx1)).toEqual(root4)
|
|
227
|
-
expect(mp!.computeRoot(tx2)).toEqual(root4)
|
|
228
|
-
expect(mp!.computeRoot(tx3)).toEqual(root4)
|
|
233
|
+
const mp = new MerklePath(100, [[
|
|
234
|
+
{ offset: 0, txid: true, hash: tx0 },
|
|
235
|
+
{ offset: 1, txid: true, hash: tx1 },
|
|
236
|
+
{ offset: 2, txid: true, hash: tx2 },
|
|
237
|
+
{ offset: 3, txid: true, hash: tx3 }
|
|
238
|
+
]])
|
|
239
|
+
expect(mp.computeRoot(tx0)).toEqual(root4)
|
|
240
|
+
expect(mp.computeRoot(tx1)).toEqual(root4)
|
|
241
|
+
expect(mp.computeRoot(tx2)).toEqual(root4)
|
|
242
|
+
expect(mp.computeRoot(tx3)).toEqual(root4)
|
|
229
243
|
// Serializing and deserializing a single-level compound path should also work
|
|
230
|
-
|
|
231
|
-
expect(
|
|
232
|
-
expect(deserialized
|
|
233
|
-
expect(deserialized!.computeRoot(tx3)).toEqual(root4)
|
|
244
|
+
const deserialized = MerklePath.fromHex(mp.toHex())
|
|
245
|
+
expect(deserialized.computeRoot(tx0)).toEqual(root4)
|
|
246
|
+
expect(deserialized.computeRoot(tx3)).toEqual(root4)
|
|
234
247
|
})
|
|
235
248
|
it('Rejects invalid bumps', () => {
|
|
236
249
|
for (const invalid of invalidBumps) {
|
|
@@ -284,4 +297,202 @@ describe('MerklePath', () => {
|
|
|
284
297
|
)
|
|
285
298
|
expect(isValid).toBe(false)
|
|
286
299
|
})
|
|
300
|
+
it('constructs a compound MerklePath from all txids in a block with odd tree levels', () => {
|
|
301
|
+
const { height, merkleroot, tx } = BLOCK_125632
|
|
302
|
+
const leafs = tx.map((hash, offset) => ({ hash, txid: true, offset }))
|
|
303
|
+
if (leafs.length % 2) leafs.push({ offset: leafs.length, duplicate: true } as any)
|
|
304
|
+
const mp = new MerklePath(height, [leafs])
|
|
305
|
+
expect(mp.computeRoot()).toBe(merkleroot)
|
|
306
|
+
})
|
|
307
|
+
it('compound path for 3 txids trims, round-trips through hex, and splits into per-txid proofs', () => {
|
|
308
|
+
const { height, merkleroot, tx } = BLOCK_125632
|
|
309
|
+
|
|
310
|
+
// Precompute the full Merkle tree for block 125632.
|
|
311
|
+
// merkleHash(right + left) matches the SDK's internal hash convention.
|
|
312
|
+
const L1 = [
|
|
313
|
+
merkleHash(tx[1] + tx[0]),
|
|
314
|
+
merkleHash(tx[3] + tx[2]),
|
|
315
|
+
merkleHash(tx[5] + tx[4]),
|
|
316
|
+
merkleHash(tx[7] + tx[6]),
|
|
317
|
+
merkleHash(tx[9] + tx[8]),
|
|
318
|
+
merkleHash(tx[10] + tx[10]) // tx[10] duplicated — odd count at level 0
|
|
319
|
+
]
|
|
320
|
+
const L2 = [
|
|
321
|
+
merkleHash(L1[1] + L1[0]),
|
|
322
|
+
merkleHash(L1[3] + L1[2]),
|
|
323
|
+
merkleHash(L1[5] + L1[4])
|
|
324
|
+
]
|
|
325
|
+
const L3 = [
|
|
326
|
+
merkleHash(L2[1] + L2[0]),
|
|
327
|
+
merkleHash(L2[2] + L2[2]) // L2 count = 3 (odd) — last node duplicated
|
|
328
|
+
]
|
|
329
|
+
expect(merkleHash(L3[1] + L3[0])).toBe(merkleroot)
|
|
330
|
+
|
|
331
|
+
// Build minimal per-txid MerklePaths for tx[2], tx[5], and tx[8].
|
|
332
|
+
// tx[8] exercises the odd-level duplication at level 2 ({offset:3, duplicate:true}).
|
|
333
|
+
const mpTx2 = new MerklePath(height, [
|
|
334
|
+
[{ offset: 2, txid: true, hash: tx[2] }, { offset: 3, hash: tx[3] }],
|
|
335
|
+
[{ offset: 0, hash: L1[0] }],
|
|
336
|
+
[{ offset: 1, hash: L2[1] }],
|
|
337
|
+
[{ offset: 1, hash: L3[1] }]
|
|
338
|
+
])
|
|
339
|
+
const mpTx5 = new MerklePath(height, [
|
|
340
|
+
[{ offset: 4, hash: tx[4] }, { offset: 5, txid: true, hash: tx[5] }],
|
|
341
|
+
[{ offset: 3, hash: L1[3] }],
|
|
342
|
+
[{ offset: 0, hash: L2[0] }],
|
|
343
|
+
[{ offset: 1, hash: L3[1] }]
|
|
344
|
+
])
|
|
345
|
+
const mpTx8 = new MerklePath(height, [
|
|
346
|
+
[{ offset: 8, txid: true, hash: tx[8] }, { offset: 9, hash: tx[9] }],
|
|
347
|
+
[{ offset: 5, hash: L1[5] }],
|
|
348
|
+
[{ offset: 3, duplicate: true }], // tx[8] is last odd node at level 2
|
|
349
|
+
[{ offset: 0, hash: L3[0] }]
|
|
350
|
+
])
|
|
351
|
+
expect(mpTx2.computeRoot(tx[2])).toBe(merkleroot)
|
|
352
|
+
expect(mpTx5.computeRoot(tx[5])).toBe(merkleroot)
|
|
353
|
+
expect(mpTx8.computeRoot(tx[8])).toBe(merkleroot)
|
|
354
|
+
|
|
355
|
+
// Combine into one compound path (combine() trims automatically)
|
|
356
|
+
const compound = new MerklePath(height, mpTx2.path.map(l => [...l]))
|
|
357
|
+
compound.combine(mpTx5)
|
|
358
|
+
compound.combine(mpTx8)
|
|
359
|
+
expect(compound.computeRoot(tx[2])).toBe(merkleroot)
|
|
360
|
+
expect(compound.computeRoot(tx[5])).toBe(merkleroot)
|
|
361
|
+
expect(compound.computeRoot(tx[8])).toBe(merkleroot)
|
|
362
|
+
|
|
363
|
+
// Serialize and deserialize
|
|
364
|
+
const deserialized = MerklePath.fromHex(compound.toHex())
|
|
365
|
+
expect(deserialized.computeRoot(tx[2])).toBe(merkleroot)
|
|
366
|
+
expect(deserialized.computeRoot(tx[5])).toBe(merkleroot)
|
|
367
|
+
expect(deserialized.computeRoot(tx[8])).toBe(merkleroot)
|
|
368
|
+
|
|
369
|
+
// Split the deserialized compound path into standalone per-txid proofs.
|
|
370
|
+
// findOrComputeLeaf reconstructs sibling hashes that were trimmed away.
|
|
371
|
+
const splitProof = (source: MerklePath, txOffset: number, txHash: string): MerklePath => {
|
|
372
|
+
const levels = source.path.map((_, h) => {
|
|
373
|
+
const sibOffset = (txOffset >> h) ^ 1
|
|
374
|
+
if (h === 0) {
|
|
375
|
+
const sib = source.findOrComputeLeaf(0, sibOffset)
|
|
376
|
+
if (sib == null) throw new Error('Missing sibling at level 0')
|
|
377
|
+
return [{ offset: txOffset, txid: true, hash: txHash }, sib].sort((a, b) => a.offset - b.offset)
|
|
378
|
+
}
|
|
379
|
+
const sib = source.findOrComputeLeaf(h, sibOffset)
|
|
380
|
+
return sib == null ? [] : [sib]
|
|
381
|
+
})
|
|
382
|
+
return new MerklePath(source.blockHeight, levels)
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const splitTx2 = splitProof(deserialized, 2, tx[2])
|
|
386
|
+
const splitTx5 = splitProof(deserialized, 5, tx[5])
|
|
387
|
+
const splitTx8 = splitProof(deserialized, 8, tx[8])
|
|
388
|
+
|
|
389
|
+
// Each standalone proof computes the same root — no data was lost through the pipeline
|
|
390
|
+
expect(splitTx2.computeRoot(tx[2])).toBe(merkleroot)
|
|
391
|
+
expect(splitTx5.computeRoot(tx[5])).toBe(merkleroot)
|
|
392
|
+
expect(splitTx8.computeRoot(tx[8])).toBe(merkleroot)
|
|
393
|
+
})
|
|
394
|
+
describe('extract()', () => {
|
|
395
|
+
it('extracts a single-txid proof from a full block compound path', () => {
|
|
396
|
+
const { height, merkleroot, tx } = BLOCK_125632
|
|
397
|
+
const leafs = tx.map((hash, offset) => ({ hash, txid: true, offset }))
|
|
398
|
+
if (leafs.length % 2) leafs.push({ offset: leafs.length, duplicate: true } as any)
|
|
399
|
+
const fullBlock = new MerklePath(height, [leafs])
|
|
400
|
+
|
|
401
|
+
const extracted = fullBlock.extract([tx[2]])
|
|
402
|
+
expect(extracted.computeRoot(tx[2])).toBe(merkleroot)
|
|
403
|
+
})
|
|
404
|
+
|
|
405
|
+
it('extracts a multi-txid compound proof from a full block compound path', () => {
|
|
406
|
+
const { height, merkleroot, tx } = BLOCK_125632
|
|
407
|
+
const leafs = tx.map((hash, offset) => ({ hash, txid: true, offset }))
|
|
408
|
+
if (leafs.length % 2) leafs.push({ offset: leafs.length, duplicate: true } as any)
|
|
409
|
+
const fullBlock = new MerklePath(height, [leafs])
|
|
410
|
+
|
|
411
|
+
const extracted = fullBlock.extract([tx[2], tx[5], tx[8]])
|
|
412
|
+
expect(extracted.computeRoot(tx[2])).toBe(merkleroot)
|
|
413
|
+
expect(extracted.computeRoot(tx[5])).toBe(merkleroot)
|
|
414
|
+
expect(extracted.computeRoot(tx[8])).toBe(merkleroot)
|
|
415
|
+
})
|
|
416
|
+
|
|
417
|
+
it('extracted path serializes and deserializes correctly', () => {
|
|
418
|
+
const { height, merkleroot, tx } = BLOCK_125632
|
|
419
|
+
const leafs = tx.map((hash, offset) => ({ hash, txid: true, offset }))
|
|
420
|
+
if (leafs.length % 2) leafs.push({ offset: leafs.length, duplicate: true } as any)
|
|
421
|
+
const fullBlock = new MerklePath(height, [leafs])
|
|
422
|
+
|
|
423
|
+
const extracted = fullBlock.extract([tx[2], tx[8]])
|
|
424
|
+
const roundTripped = MerklePath.fromHex(extracted.toHex())
|
|
425
|
+
expect(roundTripped.computeRoot(tx[2])).toBe(merkleroot)
|
|
426
|
+
expect(roundTripped.computeRoot(tx[8])).toBe(merkleroot)
|
|
427
|
+
})
|
|
428
|
+
|
|
429
|
+
it('extracted path is smaller than the full block path', () => {
|
|
430
|
+
const { height, tx } = BLOCK_125632
|
|
431
|
+
const leafs = tx.map((hash, offset) => ({ hash, txid: true, offset }))
|
|
432
|
+
if (leafs.length % 2) leafs.push({ offset: leafs.length, duplicate: true } as any)
|
|
433
|
+
const fullBlock = new MerklePath(height, [leafs])
|
|
434
|
+
|
|
435
|
+
const extracted = fullBlock.extract([tx[2], tx[5]])
|
|
436
|
+
expect(extracted.toBinary().length).toBeLessThan(fullBlock.toBinary().length)
|
|
437
|
+
})
|
|
438
|
+
|
|
439
|
+
it('extract from a trimmed multi-level compound path also works', () => {
|
|
440
|
+
const { height, merkleroot, tx } = BLOCK_125632
|
|
441
|
+
const [pathA, pathB] = buildSplitPaths()
|
|
442
|
+
pathA.combine(pathB)
|
|
443
|
+
// pathA is now a trimmed multi-level compound path for tx[2] and tx[3]
|
|
444
|
+
const txid2 = BRC74TXID2
|
|
445
|
+
const compound = new MerklePath(BRC74JSON.blockHeight, BRC74JSON.path)
|
|
446
|
+
const extracted = compound.extract([txid2])
|
|
447
|
+
expect(extracted.computeRoot(txid2)).toBe(compound.computeRoot(txid2))
|
|
448
|
+
// BLOCK_125632 variant
|
|
449
|
+
const leafs = tx.map((hash, offset) => ({ hash, txid: true, offset }))
|
|
450
|
+
if (leafs.length % 2) leafs.push({ offset: leafs.length, duplicate: true } as any)
|
|
451
|
+
const fullBlock = new MerklePath(height, [leafs])
|
|
452
|
+
const ex = fullBlock.extract([tx[0], tx[10]])
|
|
453
|
+
expect(ex.computeRoot(tx[0])).toBe(merkleroot)
|
|
454
|
+
expect(ex.computeRoot(tx[10])).toBe(merkleroot)
|
|
455
|
+
})
|
|
456
|
+
|
|
457
|
+
it('throws when no txids are provided', () => {
|
|
458
|
+
const { height, tx } = BLOCK_125632
|
|
459
|
+
const leafs = tx.map((hash, offset) => ({ hash, txid: true, offset }))
|
|
460
|
+
if (leafs.length % 2) leafs.push({ offset: leafs.length, duplicate: true } as any)
|
|
461
|
+
const fullBlock = new MerklePath(height, [leafs])
|
|
462
|
+
expect(() => fullBlock.extract([])).toThrow('At least one txid must be provided')
|
|
463
|
+
})
|
|
464
|
+
|
|
465
|
+
it('throws when a txid is not in the path', () => {
|
|
466
|
+
const { height, tx } = BLOCK_125632
|
|
467
|
+
const leafs = tx.map((hash, offset) => ({ hash, txid: true, offset }))
|
|
468
|
+
if (leafs.length % 2) leafs.push({ offset: leafs.length, duplicate: true } as any)
|
|
469
|
+
const fullBlock = new MerklePath(height, [leafs])
|
|
470
|
+
expect(() => fullBlock.extract(['deadbeef'.repeat(8)])).toThrow()
|
|
471
|
+
})
|
|
472
|
+
})
|
|
473
|
+
|
|
474
|
+
it('findOrComputeLeaf duplicates leaf0 when leaf1 carries both a hash and duplicate=true', () => {
|
|
475
|
+
// Covers the leaf1.duplicate === true branch inside findOrComputeLeaf.
|
|
476
|
+
// That branch is reached when leaf1.hash is non-null (bypassing the null-check above it)
|
|
477
|
+
// but leaf1.duplicate is also true — an unusual but valid interface state.
|
|
478
|
+
const tx0 = 'aa'.repeat(32)
|
|
479
|
+
const tx1 = 'bb'.repeat(32)
|
|
480
|
+
|
|
481
|
+
// Build a minimal valid path so the constructor does not throw.
|
|
482
|
+
const mp = new MerklePath(1, [[
|
|
483
|
+
{ offset: 0, txid: true, hash: tx0 },
|
|
484
|
+
{ offset: 1, hash: tx1 }
|
|
485
|
+
]])
|
|
486
|
+
|
|
487
|
+
// Mutate: give the sibling leaf at offset 1 both a hash and duplicate=true.
|
|
488
|
+
// findOrComputeLeaf(1, 0) will:
|
|
489
|
+
// - not find offset 0 in path[1] (path.length === 1, no higher levels)
|
|
490
|
+
// - recurse to level 0: leaf0 = tx0 (offset 0), leaf1 = {hash:tx1, duplicate:true}
|
|
491
|
+
// - leaf1.hash is non-null → skips the null-branch
|
|
492
|
+
// - leaf1.duplicate === true → line 349: workinghash = hash(leaf0 + leaf0)
|
|
493
|
+
mp.path[0][1] = { offset: 1, hash: tx1, duplicate: true }
|
|
494
|
+
|
|
495
|
+
const result = mp.findOrComputeLeaf(1, 0)
|
|
496
|
+
expect(result?.hash).toBe(merkleHash(tx0 + tx0))
|
|
497
|
+
})
|
|
287
498
|
})
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import Transaction from '../Transaction'
|
|
2
|
+
import LockingScript from '../../script/LockingScript'
|
|
3
|
+
import UnlockingScript from '../../script/UnlockingScript'
|
|
4
|
+
import { toArray } from '../../primitives/utils'
|
|
5
|
+
|
|
6
|
+
// Known EF-format transaction hex (BRC-30)
|
|
7
|
+
const KNOWN_EF_HEX =
|
|
8
|
+
'010000000000000000ef01ac4e164f5bc16746bb0868404292ac8318bbac3800e4aad13a014da427adce3e000000006a47304402203a61a2e931612b4bda08d541cfb980885173b8dcf64a3471238ae7abcd368d6402204cbf24f04b9aa2256d8901f0ed97866603d2be8324c2bfb7a37bf8fc90edd5b441210263e2dee22b1ddc5e11f6fab8bcd2378bdd19580d640501ea956ec0e786f93e76ffffffff3e660000000000001976a9146bfd5c7fbe21529d45803dbcf0c87dd3c71efbc288ac013c660000000000001976a9146bfd5c7fbe21529d45803dbcf0c87dd3c71efbc288ac00000000'
|
|
9
|
+
|
|
10
|
+
// Known BEEF V1 hex (non-Atomic, has no atomicTxid)
|
|
11
|
+
const KNOWN_BEEF_V1_HEX =
|
|
12
|
+
'0100beef01fe636d0c0007021400fe507c0c7aa754cef1f7889d5fd395cf1f785dd7de98eed895dbedfe4e5bc70d1502ac4e164f5bc16746bb0868404292ac8318bbac3800e4aad13a014da427adce3e010b00bc4ff395efd11719b277694cface5aa50d085a0bb81f613f70313acd28cf4557010400574b2d9142b8d28b61d88e3b2c3f44d858411356b49a28a4643b6d1a6a092a5201030051a05fc84d531b5d250c23f4f886f6812f9fe3f402d61607f977b4ecd2701c19010000fd781529d58fc2523cf396a7f25440b409857e7e221766c57214b1d38c7b481f01010062f542f45ea3660f86c013ced80534cb5fd4c19d66c56e7e8c5d4bf2d40acc5e010100b121e91836fd7cd5102b654e9f72f3cf6fdbfd0b161c53a9c54b12c841126331020100000001cd4e4cac3c7b56920d1e7655e7e260d31f29d9a388d04910f1bbd72304a79029010000006b483045022100e75279a205a547c445719420aa3138bf14743e3f42618e5f86a19bde14bb95f7022064777d34776b05d816daf1699493fcdf2ef5a5ab1ad710d9c97bfb5b8f7cef3641210263e2dee22b1ddc5e11f6fab8bcd2378bdd19580d640501ea956ec0e786f93e76ffffffff013e660000000000001976a9146bfd5c7fbe21529d45803dbcf0c87dd3c71efbc288ac0000000001000100000001ac4e164f5bc16746bb0868404292ac8318bbac3800e4aad13a014da427adce3e000000006a47304402203a61a2e931612b4bda08d541cfb980885173b8dcf64a3471238ae7abcd368d6402204cbf24f04b9aa2256d8901f0ed97866603d2be8324c2bfb7a37bf8fc90edd5b441210263e2dee22b1ddc5e11f6fab8bcd2378bdd19580d640501ea956ec0e786f93e76ffffffff013c660000000000001976a9146bfd5c7fbe21529d45803dbcf0c87dd3c71efbc288ac0000000000'
|
|
13
|
+
|
|
14
|
+
describe('Transaction – additional coverage', () => {
|
|
15
|
+
describe('fromHexEF', () => {
|
|
16
|
+
it('parses a known EF hex string', () => {
|
|
17
|
+
const tx = Transaction.fromHexEF(KNOWN_EF_HEX)
|
|
18
|
+
expect(tx).toBeInstanceOf(Transaction)
|
|
19
|
+
expect(tx.inputs).toHaveLength(1)
|
|
20
|
+
expect(tx.outputs).toHaveLength(1)
|
|
21
|
+
})
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
describe('fromAtomicBEEF – non-atomic BEEF', () => {
|
|
25
|
+
it('throws when passed a regular BEEF with no atomicTxid', () => {
|
|
26
|
+
const beefBytes = toArray(KNOWN_BEEF_V1_HEX, 'hex')
|
|
27
|
+
expect(() => Transaction.fromAtomicBEEF(beefBytes)).toThrow(
|
|
28
|
+
'beef must conform to BRC-95 and must contain the subject txid.'
|
|
29
|
+
)
|
|
30
|
+
})
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
describe('addInput', () => {
|
|
34
|
+
it('throws when both sourceTXID and sourceTransaction are undefined', () => {
|
|
35
|
+
const tx = new Transaction()
|
|
36
|
+
expect(() =>
|
|
37
|
+
tx.addInput({
|
|
38
|
+
sourceOutputIndex: 0,
|
|
39
|
+
unlockingScript: new UnlockingScript(),
|
|
40
|
+
sequence: 0xffffffff
|
|
41
|
+
})
|
|
42
|
+
).toThrow('A reference to an an input transaction is required')
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('sets sequence to 0xffffffff when not provided', () => {
|
|
46
|
+
const tx = new Transaction()
|
|
47
|
+
tx.addInput({
|
|
48
|
+
sourceTXID: '00'.repeat(32),
|
|
49
|
+
sourceOutputIndex: 0,
|
|
50
|
+
unlockingScript: new UnlockingScript()
|
|
51
|
+
})
|
|
52
|
+
expect(tx.inputs[0].sequence).toBe(0xffffffff)
|
|
53
|
+
})
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
describe('addOutput', () => {
|
|
57
|
+
it('throws when satoshis is undefined and change is not true', () => {
|
|
58
|
+
const tx = new Transaction()
|
|
59
|
+
expect(() =>
|
|
60
|
+
tx.addOutput({
|
|
61
|
+
lockingScript: new LockingScript()
|
|
62
|
+
})
|
|
63
|
+
).toThrow('either satoshis must be defined or change must be set to true')
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('throws when satoshis is negative', () => {
|
|
67
|
+
const tx = new Transaction()
|
|
68
|
+
expect(() =>
|
|
69
|
+
tx.addOutput({
|
|
70
|
+
lockingScript: new LockingScript(),
|
|
71
|
+
satoshis: -1
|
|
72
|
+
})
|
|
73
|
+
).toThrow('satoshis must be a positive integer or zero')
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('throws when lockingScript is null', () => {
|
|
77
|
+
const tx = new Transaction()
|
|
78
|
+
expect(() =>
|
|
79
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
80
|
+
tx.addOutput({ satoshis: 100, lockingScript: null as any })
|
|
81
|
+
).toThrow('lockingScript must be defined')
|
|
82
|
+
})
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
describe('addP2PKHOutput', () => {
|
|
86
|
+
it('adds a change output when satoshis is omitted', () => {
|
|
87
|
+
const tx = new Transaction()
|
|
88
|
+
// Pass a 20-byte hash directly to avoid base58 parsing
|
|
89
|
+
const pubKeyHash = new Array(20).fill(0x01)
|
|
90
|
+
tx.addP2PKHOutput(pubKeyHash)
|
|
91
|
+
expect(tx.outputs).toHaveLength(1)
|
|
92
|
+
expect(tx.outputs[0].change).toBe(true)
|
|
93
|
+
})
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
describe('hash / id', () => {
|
|
97
|
+
it('returns hex string from hash("hex")', () => {
|
|
98
|
+
const tx = new Transaction()
|
|
99
|
+
const h = tx.hash('hex')
|
|
100
|
+
expect(typeof h).toBe('string')
|
|
101
|
+
expect((h as string)).toHaveLength(64)
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('returns binary array from id() without enc', () => {
|
|
105
|
+
const tx = new Transaction()
|
|
106
|
+
const id = tx.id()
|
|
107
|
+
expect(Array.isArray(id)).toBe(true)
|
|
108
|
+
expect(id).toHaveLength(32)
|
|
109
|
+
})
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
describe('toHexAtomicBEEF', () => {
|
|
113
|
+
it('produces a hex string from toHexAtomicBEEF()', () => {
|
|
114
|
+
const sourceTx = new Transaction(
|
|
115
|
+
1,
|
|
116
|
+
[],
|
|
117
|
+
[{ lockingScript: new LockingScript(), satoshis: 1000 }],
|
|
118
|
+
0
|
|
119
|
+
)
|
|
120
|
+
const tx = new Transaction(1, [], [{ lockingScript: new LockingScript(), satoshis: 900 }], 0)
|
|
121
|
+
tx.addInput({
|
|
122
|
+
sourceTXID: sourceTx.id('hex'),
|
|
123
|
+
sourceTransaction: sourceTx,
|
|
124
|
+
sourceOutputIndex: 0,
|
|
125
|
+
unlockingScript: new UnlockingScript(),
|
|
126
|
+
sequence: 0xffffffff
|
|
127
|
+
})
|
|
128
|
+
const hex = tx.toHexAtomicBEEF()
|
|
129
|
+
expect(typeof hex).toBe('string')
|
|
130
|
+
expect(hex.length).toBeGreaterThan(0)
|
|
131
|
+
})
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
describe('getFee', () => {
|
|
135
|
+
it('throws when an input has no sourceTransaction', () => {
|
|
136
|
+
const tx = new Transaction()
|
|
137
|
+
tx.addInput({
|
|
138
|
+
sourceTXID: '00'.repeat(32),
|
|
139
|
+
sourceOutputIndex: 0,
|
|
140
|
+
unlockingScript: new UnlockingScript()
|
|
141
|
+
})
|
|
142
|
+
expect(() => tx.getFee()).toThrow(
|
|
143
|
+
'Source transactions or sourceSatoshis are required for all inputs to calculate fee'
|
|
144
|
+
)
|
|
145
|
+
})
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
describe('sign', () => {
|
|
149
|
+
it('throws when an output has undefined satoshis and change is not set', async () => {
|
|
150
|
+
const tx = new Transaction(
|
|
151
|
+
1,
|
|
152
|
+
[],
|
|
153
|
+
[{ lockingScript: new LockingScript(), satoshis: undefined, change: false }],
|
|
154
|
+
0
|
|
155
|
+
)
|
|
156
|
+
await expect(tx.sign()).rejects.toThrow(
|
|
157
|
+
'One or more transaction outputs is missing an amount'
|
|
158
|
+
)
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
it('throws when an output has undefined satoshis and change is true (uncomputed change)', async () => {
|
|
162
|
+
const tx = new Transaction(
|
|
163
|
+
1,
|
|
164
|
+
[],
|
|
165
|
+
[{ lockingScript: new LockingScript(), satoshis: undefined, change: true }],
|
|
166
|
+
0
|
|
167
|
+
)
|
|
168
|
+
await expect(tx.sign()).rejects.toThrow(
|
|
169
|
+
'There are still change outputs with uncomputed amounts'
|
|
170
|
+
)
|
|
171
|
+
})
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
describe('toEF / writeEF error paths', () => {
|
|
175
|
+
it('throws when an input has no sourceTransaction during EF serialization', () => {
|
|
176
|
+
// sourceTXID is defined so addInput passes, but sourceTransaction is undefined
|
|
177
|
+
const tx = new Transaction()
|
|
178
|
+
tx.addInput({
|
|
179
|
+
sourceTXID: '00'.repeat(32),
|
|
180
|
+
sourceOutputIndex: 0,
|
|
181
|
+
unlockingScript: new UnlockingScript()
|
|
182
|
+
})
|
|
183
|
+
expect(() => tx.toEF()).toThrow(
|
|
184
|
+
'All inputs must have source transactions when serializing to EF format'
|
|
185
|
+
)
|
|
186
|
+
})
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
describe('toBinary / writeTransactionBody error paths', () => {
|
|
190
|
+
it('throws when an input has no sourceTXID and no sourceTransaction', () => {
|
|
191
|
+
// Bypass addInput validation by constructing directly
|
|
192
|
+
const tx = new Transaction(
|
|
193
|
+
1,
|
|
194
|
+
[
|
|
195
|
+
{
|
|
196
|
+
sourceOutputIndex: 0,
|
|
197
|
+
unlockingScript: new UnlockingScript(),
|
|
198
|
+
sequence: 0xffffffff
|
|
199
|
+
// no sourceTXID, no sourceTransaction
|
|
200
|
+
}
|
|
201
|
+
],
|
|
202
|
+
[],
|
|
203
|
+
0
|
|
204
|
+
)
|
|
205
|
+
expect(() => tx.toBinary()).toThrow('sourceTransaction is undefined')
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
it('throws when an input has no unlockingScript during serialization', () => {
|
|
209
|
+
const tx = new Transaction(
|
|
210
|
+
1,
|
|
211
|
+
[
|
|
212
|
+
{
|
|
213
|
+
sourceTXID: '00'.repeat(32),
|
|
214
|
+
sourceOutputIndex: 0,
|
|
215
|
+
sequence: 0xffffffff
|
|
216
|
+
// no unlockingScript
|
|
217
|
+
}
|
|
218
|
+
],
|
|
219
|
+
[],
|
|
220
|
+
0
|
|
221
|
+
)
|
|
222
|
+
expect(() => tx.toBinary()).toThrow('unlockingScript is undefined')
|
|
223
|
+
})
|
|
224
|
+
})
|
|
225
|
+
})
|