@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.
Files changed (106) hide show
  1. package/dist/cjs/package.json +1 -1
  2. package/dist/cjs/src/auth/clients/__tests__/AuthFetch.additional.test.js +827 -0
  3. package/dist/cjs/src/auth/clients/__tests__/AuthFetch.additional.test.js.map +1 -0
  4. package/dist/cjs/src/auth/transports/__tests__/SimplifiedFetchTransport.additional.test.js +654 -0
  5. package/dist/cjs/src/auth/transports/__tests__/SimplifiedFetchTransport.additional.test.js.map +1 -0
  6. package/dist/cjs/src/overlay-tools/HostReputationTracker.js +21 -13
  7. package/dist/cjs/src/overlay-tools/HostReputationTracker.js.map +1 -1
  8. package/dist/cjs/src/primitives/PrivateKey.js +3 -3
  9. package/dist/cjs/src/primitives/PrivateKey.js.map +1 -1
  10. package/dist/cjs/src/script/Spend.js +17 -9
  11. package/dist/cjs/src/script/Spend.js.map +1 -1
  12. package/dist/cjs/src/storage/StorageDownloader.js +6 -6
  13. package/dist/cjs/src/storage/StorageDownloader.js.map +1 -1
  14. package/dist/cjs/src/storage/StorageUtils.js +1 -1
  15. package/dist/cjs/src/storage/StorageUtils.js.map +1 -1
  16. package/dist/cjs/src/transaction/MerklePath.js +168 -27
  17. package/dist/cjs/src/transaction/MerklePath.js.map +1 -1
  18. package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
  19. package/dist/esm/src/auth/clients/__tests__/AuthFetch.additional.test.js +825 -0
  20. package/dist/esm/src/auth/clients/__tests__/AuthFetch.additional.test.js.map +1 -0
  21. package/dist/esm/src/auth/transports/__tests__/SimplifiedFetchTransport.additional.test.js +619 -0
  22. package/dist/esm/src/auth/transports/__tests__/SimplifiedFetchTransport.additional.test.js.map +1 -0
  23. package/dist/esm/src/overlay-tools/HostReputationTracker.js +21 -13
  24. package/dist/esm/src/overlay-tools/HostReputationTracker.js.map +1 -1
  25. package/dist/esm/src/primitives/PrivateKey.js +3 -3
  26. package/dist/esm/src/primitives/PrivateKey.js.map +1 -1
  27. package/dist/esm/src/script/Spend.js +17 -9
  28. package/dist/esm/src/script/Spend.js.map +1 -1
  29. package/dist/esm/src/storage/StorageDownloader.js +6 -6
  30. package/dist/esm/src/storage/StorageDownloader.js.map +1 -1
  31. package/dist/esm/src/storage/StorageUtils.js +1 -1
  32. package/dist/esm/src/storage/StorageUtils.js.map +1 -1
  33. package/dist/esm/src/transaction/MerklePath.js +168 -27
  34. package/dist/esm/src/transaction/MerklePath.js.map +1 -1
  35. package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
  36. package/dist/types/src/auth/clients/__tests__/AuthFetch.additional.test.d.ts +21 -0
  37. package/dist/types/src/auth/clients/__tests__/AuthFetch.additional.test.d.ts.map +1 -0
  38. package/dist/types/src/auth/transports/__tests__/SimplifiedFetchTransport.additional.test.d.ts +2 -0
  39. package/dist/types/src/auth/transports/__tests__/SimplifiedFetchTransport.additional.test.d.ts.map +1 -0
  40. package/dist/types/src/overlay-tools/HostReputationTracker.d.ts.map +1 -1
  41. package/dist/types/src/script/Spend.d.ts.map +1 -1
  42. package/dist/types/src/transaction/MerklePath.d.ts +27 -0
  43. package/dist/types/src/transaction/MerklePath.d.ts.map +1 -1
  44. package/dist/types/tsconfig.types.tsbuildinfo +1 -1
  45. package/dist/umd/bundle.js +3 -3
  46. package/dist/umd/bundle.js.map +1 -1
  47. package/docs/reference/storage.md +1 -1
  48. package/docs/reference/transaction.md +40 -0
  49. package/package.json +1 -1
  50. package/src/auth/clients/__tests__/AuthFetch.additional.test.ts +1131 -0
  51. package/src/auth/transports/__tests__/SimplifiedFetchTransport.additional.test.ts +770 -0
  52. package/src/auth/utils/__tests/validateCertificates.test.ts +12 -9
  53. package/src/compat/__tests/Mnemonic.additional.test.ts +64 -0
  54. package/src/identity/__tests/IdentityClient.additional.test.ts +767 -0
  55. package/src/kvstore/__tests/LocalKVStore.additional.test.ts +611 -0
  56. package/src/kvstore/__tests/LocalKVStore.test.ts +4 -6
  57. package/src/kvstore/__tests/kvStoreInterpreter.test.ts +327 -0
  58. package/src/overlay-tools/HostReputationTracker.ts +17 -14
  59. package/src/overlay-tools/__tests/HostReputationTracker.additional.test.ts +561 -0
  60. package/src/overlay-tools/__tests/LookupResolver.additional.test.ts +612 -0
  61. package/src/overlay-tools/__tests/withDoubleSpendRetry.test.ts +278 -0
  62. package/src/primitives/PrivateKey.ts +3 -3
  63. package/src/primitives/__tests/BigNumber.additional.test.ts +79 -0
  64. package/src/primitives/__tests/Curve.additional.test.ts +208 -0
  65. package/src/primitives/__tests/ECDSA.additional.test.ts +122 -0
  66. package/src/primitives/__tests/Hash.additional.test.ts +59 -0
  67. package/src/primitives/__tests/JacobianPoint.test.ts +308 -0
  68. package/src/primitives/__tests/Point.additional.test.ts +503 -0
  69. package/src/primitives/__tests/PublicKey.additional.test.ts +383 -0
  70. package/src/primitives/__tests/Random.additional.test.ts +262 -0
  71. package/src/primitives/__tests/Signature.test.ts +333 -0
  72. package/src/primitives/__tests/TransactionSignature.additional.test.ts +241 -0
  73. package/src/registry/__tests/RegistryClient.additional.test.ts +750 -0
  74. package/src/remittance/__tests/BasicBRC29.additional.test.ts +657 -0
  75. package/src/remittance/__tests/RemittanceManager.additional.test.ts +1272 -0
  76. package/src/script/Spend.ts +19 -11
  77. package/src/script/__tests/LockingUnlockingScript.test.ts +79 -0
  78. package/src/script/__tests/Script.additional.test.ts +100 -0
  79. package/src/script/__tests/ScriptEvaluationError.test.ts +98 -0
  80. package/src/script/__tests/Spend.additional.test.ts +837 -0
  81. package/src/script/templates/__tests/RPuzzle.test.ts +134 -0
  82. package/src/storage/StorageDownloader.ts +6 -6
  83. package/src/storage/StorageUtils.ts +1 -1
  84. package/src/transaction/MerklePath.ts +196 -36
  85. package/src/transaction/__tests/BeefParty.additional.test.ts +22 -0
  86. package/src/transaction/__tests/Broadcaster.test.ts +159 -0
  87. package/src/transaction/__tests/MerklePath.bench.test.ts +105 -0
  88. package/src/transaction/__tests/MerklePath.test.ts +232 -21
  89. package/src/transaction/__tests/Transaction.additional.test.ts +225 -0
  90. package/src/transaction/broadcasters/__tests/ARC.additional.test.ts +585 -0
  91. package/src/transaction/broadcasters/__tests/Teranode.test.ts +349 -0
  92. package/src/transaction/chaintrackers/__tests/BlockHeadersService.test.ts +253 -0
  93. package/src/transaction/chaintrackers/__tests/DefaultChainTracker.test.ts +44 -0
  94. package/src/transaction/chaintrackers/__tests/WhatsOnChain.additional.test.ts +193 -0
  95. package/src/transaction/fee-models/__tests/SatoshisPerKilobyte.test.ts +262 -0
  96. package/src/transaction/http/__tests/BinaryFetchClient.test.ts +212 -0
  97. package/src/transaction/http/__tests/DefaultHttpClient.additional.test.ts +192 -0
  98. package/src/transaction/http/__tests/DefaultHttpClient.test.ts +71 -0
  99. package/src/wallet/__tests/ProtoWallet.additional.test.ts +134 -0
  100. package/src/wallet/__tests/WERR.test.ts +212 -0
  101. package/src/wallet/__tests/WalletClient.additional.test.ts +699 -0
  102. package/src/wallet/__tests/WalletClient.substrate.test.ts +759 -0
  103. package/src/wallet/__tests/WalletError.test.ts +290 -0
  104. package/src/wallet/__tests/validationHelpers.test.ts +1218 -0
  105. package/src/wallet/substrates/__tests/HTTPWalletJSON.test.ts +496 -0
  106. package/src/wallet/substrates/__tests/HTTPWalletWire.test.ts +273 -0
@@ -0,0 +1,657 @@
1
+ import type { ModuleContext } from '../types.js'
2
+ import type { WalletInterface } from '../../wallet/Wallet.interfaces.js'
3
+ import {
4
+ Brc29RemittanceModule,
5
+ DefaultNonceProvider,
6
+ DefaultLockingScriptProvider
7
+ } from '../modules/BasicBRC29.js'
8
+
9
+ const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation(() => { })
10
+
11
+ const makeContext = (wallet: WalletInterface): ModuleContext => ({
12
+ wallet,
13
+ originator: 'example.com',
14
+ now: () => 123
15
+ })
16
+
17
+ const makeWallet = (overrides: Partial<WalletInterface> = {}): WalletInterface => ({
18
+ getPublicKey: jest.fn(async () => ({ publicKey: '02deadbeef' })),
19
+ createAction: jest.fn(async () => ({ tx: [1, 2, 3] })),
20
+ internalizeAction: jest.fn(async () => ({ ok: true })),
21
+ ...overrides
22
+ } as unknown as WalletInterface)
23
+
24
+ const validSettlement = {
25
+ customInstructions: { derivationPrefix: 'prefix', derivationSuffix: 'suffix' },
26
+ transaction: [1, 2, 3],
27
+ amountSatoshis: 1000
28
+ }
29
+
30
+ // ---------------------------------------------------------------------------
31
+ // buildSettlement – option validation edge cases
32
+ // ---------------------------------------------------------------------------
33
+
34
+ describe('Brc29RemittanceModule – buildSettlement option validation', () => {
35
+ it('terminates when option is null', async () => {
36
+ const module = new Brc29RemittanceModule()
37
+ const result = await module.buildSettlement(
38
+ { threadId: 'tid', option: null as any },
39
+ makeContext(makeWallet())
40
+ )
41
+ expect(result.action).toBe('terminate')
42
+ if (result.action === 'terminate') {
43
+ expect(result.termination.code).toBe('brc29.invalid_option')
44
+ }
45
+ })
46
+
47
+ it('terminates when option is a non-object primitive', async () => {
48
+ const module = new Brc29RemittanceModule()
49
+ const result = await module.buildSettlement(
50
+ { threadId: 'tid', option: 'not-an-object' as any },
51
+ makeContext(makeWallet())
52
+ )
53
+ expect(result.action).toBe('terminate')
54
+ })
55
+
56
+ it('terminates when amountSatoshis is a float (non-integer)', async () => {
57
+ const module = new Brc29RemittanceModule()
58
+ const result = await module.buildSettlement(
59
+ { threadId: 'tid', option: { amountSatoshis: 1.5, payee: 'payee' } },
60
+ makeContext(makeWallet())
61
+ )
62
+ expect(result.action).toBe('terminate')
63
+ if (result.action === 'terminate') {
64
+ expect(result.termination.code).toBe('brc29.invalid_option')
65
+ }
66
+ })
67
+
68
+ it('terminates when outputIndex is a negative integer', async () => {
69
+ const module = new Brc29RemittanceModule()
70
+ const result = await module.buildSettlement(
71
+ { threadId: 'tid', option: { amountSatoshis: 100, payee: 'pk', outputIndex: -1 } },
72
+ makeContext(makeWallet())
73
+ )
74
+ expect(result.action).toBe('terminate')
75
+ })
76
+
77
+ it('terminates when outputIndex is a float', async () => {
78
+ const module = new Brc29RemittanceModule()
79
+ const result = await module.buildSettlement(
80
+ { threadId: 'tid', option: { amountSatoshis: 100, payee: 'pk', outputIndex: 0.5 } },
81
+ makeContext(makeWallet())
82
+ )
83
+ expect(result.action).toBe('terminate')
84
+ })
85
+
86
+ it('terminates when protocolID is not an array', async () => {
87
+ const module = new Brc29RemittanceModule()
88
+ const result = await module.buildSettlement(
89
+ { threadId: 'tid', option: { amountSatoshis: 100, payee: 'pk', protocolID: 'bad' as any } },
90
+ makeContext(makeWallet())
91
+ )
92
+ expect(result.action).toBe('terminate')
93
+ })
94
+
95
+ it('terminates when protocolID has wrong array length', async () => {
96
+ const module = new Brc29RemittanceModule()
97
+ const result = await module.buildSettlement(
98
+ { threadId: 'tid', option: { amountSatoshis: 100, payee: 'pk', protocolID: [1] as any } },
99
+ makeContext(makeWallet())
100
+ )
101
+ expect(result.action).toBe('terminate')
102
+ })
103
+
104
+ it('terminates when protocolID has negative protocol number', async () => {
105
+ const module = new Brc29RemittanceModule()
106
+ const result = await module.buildSettlement(
107
+ { threadId: 'tid', option: { amountSatoshis: 100, payee: 'pk', protocolID: [-1, 'proto'] as any } },
108
+ makeContext(makeWallet())
109
+ )
110
+ expect(result.action).toBe('terminate')
111
+ })
112
+
113
+ it('terminates when protocolID string is empty', async () => {
114
+ const module = new Brc29RemittanceModule()
115
+ const result = await module.buildSettlement(
116
+ { threadId: 'tid', option: { amountSatoshis: 100, payee: 'pk', protocolID: [2, ' '] as any } },
117
+ makeContext(makeWallet())
118
+ )
119
+ expect(result.action).toBe('terminate')
120
+ })
121
+
122
+ it('terminates when labels contains an empty string', async () => {
123
+ const module = new Brc29RemittanceModule()
124
+ const result = await module.buildSettlement(
125
+ { threadId: 'tid', option: { amountSatoshis: 100, payee: 'pk', labels: ['valid', ''] } },
126
+ makeContext(makeWallet())
127
+ )
128
+ expect(result.action).toBe('terminate')
129
+ })
130
+
131
+ it('terminates when labels is not an array', async () => {
132
+ const module = new Brc29RemittanceModule()
133
+ const result = await module.buildSettlement(
134
+ { threadId: 'tid', option: { amountSatoshis: 100, payee: 'pk', labels: 'single-label' as any } },
135
+ makeContext(makeWallet())
136
+ )
137
+ expect(result.action).toBe('terminate')
138
+ })
139
+
140
+ it('terminates when description is an empty string', async () => {
141
+ const module = new Brc29RemittanceModule()
142
+ const result = await module.buildSettlement(
143
+ { threadId: 'tid', option: { amountSatoshis: 100, payee: 'pk', description: ' ' } },
144
+ makeContext(makeWallet())
145
+ )
146
+ expect(result.action).toBe('terminate')
147
+ })
148
+ })
149
+
150
+ // ---------------------------------------------------------------------------
151
+ // buildSettlement – wallet-side failure paths
152
+ // ---------------------------------------------------------------------------
153
+
154
+ describe('Brc29RemittanceModule – buildSettlement wallet failures', () => {
155
+ it('terminates when getPublicKey returns an empty publicKey string', async () => {
156
+ const wallet = makeWallet({
157
+ getPublicKey: jest.fn(async () => ({ publicKey: '' }))
158
+ })
159
+ const module = new Brc29RemittanceModule({
160
+ nonceProvider: { createNonce: jest.fn().mockResolvedValue('nonce') },
161
+ lockingScriptProvider: { pubKeyToP2PKHLockingScript: jest.fn(async () => '76a914abcd88ac') }
162
+ })
163
+
164
+ const result = await module.buildSettlement(
165
+ { threadId: 'tid', option: { amountSatoshis: 1000, payee: 'pk' } },
166
+ makeContext(wallet)
167
+ )
168
+ expect(result.action).toBe('terminate')
169
+ if (result.action === 'terminate') {
170
+ expect(result.termination.code).toBe('brc29.public_key_missing')
171
+ }
172
+ })
173
+
174
+ it('terminates when getPublicKey returns a whitespace-only publicKey', async () => {
175
+ const wallet = makeWallet({
176
+ getPublicKey: jest.fn(async () => ({ publicKey: ' ' }))
177
+ })
178
+ const module = new Brc29RemittanceModule({
179
+ nonceProvider: { createNonce: jest.fn().mockResolvedValue('nonce') },
180
+ lockingScriptProvider: { pubKeyToP2PKHLockingScript: jest.fn(async () => 'script') }
181
+ })
182
+
183
+ const result = await module.buildSettlement(
184
+ { threadId: 'tid', option: { amountSatoshis: 1000, payee: 'pk' } },
185
+ makeContext(wallet)
186
+ )
187
+ expect(result.action).toBe('terminate')
188
+ if (result.action === 'terminate') {
189
+ expect(result.termination.code).toBe('brc29.public_key_missing')
190
+ }
191
+ })
192
+
193
+ it('terminates when lockingScriptProvider returns an empty string', async () => {
194
+ const wallet = makeWallet({
195
+ getPublicKey: jest.fn(async () => ({ publicKey: '02deadbeef' }))
196
+ })
197
+ const module = new Brc29RemittanceModule({
198
+ nonceProvider: { createNonce: jest.fn().mockResolvedValue('nonce') },
199
+ lockingScriptProvider: { pubKeyToP2PKHLockingScript: jest.fn(async () => '') }
200
+ })
201
+
202
+ const result = await module.buildSettlement(
203
+ { threadId: 'tid', option: { amountSatoshis: 1000, payee: 'pk' } },
204
+ makeContext(wallet)
205
+ )
206
+ expect(result.action).toBe('terminate')
207
+ if (result.action === 'terminate') {
208
+ expect(result.termination.code).toBe('brc29.locking_script_missing')
209
+ }
210
+ })
211
+
212
+ it('terminates when lockingScriptProvider returns a whitespace-only string', async () => {
213
+ const wallet = makeWallet({
214
+ getPublicKey: jest.fn(async () => ({ publicKey: '02deadbeef' }))
215
+ })
216
+ const module = new Brc29RemittanceModule({
217
+ nonceProvider: { createNonce: jest.fn().mockResolvedValue('nonce') },
218
+ lockingScriptProvider: { pubKeyToP2PKHLockingScript: jest.fn(async () => ' ') }
219
+ })
220
+
221
+ const result = await module.buildSettlement(
222
+ { threadId: 'tid', option: { amountSatoshis: 1000, payee: 'pk' } },
223
+ makeContext(wallet)
224
+ )
225
+ expect(result.action).toBe('terminate')
226
+ if (result.action === 'terminate') {
227
+ expect(result.termination.code).toBe('brc29.locking_script_missing')
228
+ }
229
+ })
230
+
231
+ it('settles successfully when createAction returns signableTransaction.tx instead of direct tx', async () => {
232
+ const wallet = makeWallet({
233
+ getPublicKey: jest.fn(async () => ({ publicKey: '02deadbeef' })),
234
+ createAction: jest.fn(async () => ({
235
+ signableTransaction: { tx: [4, 5, 6], reference: 'cmVm' }
236
+ }))
237
+ })
238
+ const module = new Brc29RemittanceModule({
239
+ nonceProvider: {
240
+ createNonce: jest.fn()
241
+ .mockResolvedValueOnce('prefix')
242
+ .mockResolvedValueOnce('suffix')
243
+ },
244
+ lockingScriptProvider: { pubKeyToP2PKHLockingScript: jest.fn(async () => '76a914deadbeef88ac') }
245
+ })
246
+
247
+ const result = await module.buildSettlement(
248
+ { threadId: 'tid', option: { amountSatoshis: 500, payee: 'pk' } },
249
+ makeContext(wallet)
250
+ )
251
+ // signableTransaction.tx = [4, 5, 6] is a valid atomic BEEF array
252
+ expect(result.action).toBe('settle')
253
+ if (result.action === 'settle') {
254
+ expect(result.artifact.transaction).toEqual([4, 5, 6])
255
+ }
256
+ })
257
+
258
+ it('terminates when tx is not a valid byte array (contains non-byte values)', async () => {
259
+ const wallet = makeWallet({
260
+ getPublicKey: jest.fn(async () => ({ publicKey: '02deadbeef' })),
261
+ createAction: jest.fn(async () => ({ tx: [256, 1, 2] })) // 256 is out of byte range
262
+ })
263
+ const module = new Brc29RemittanceModule({
264
+ nonceProvider: {
265
+ createNonce: jest.fn()
266
+ .mockResolvedValueOnce('p')
267
+ .mockResolvedValueOnce('s')
268
+ },
269
+ lockingScriptProvider: { pubKeyToP2PKHLockingScript: jest.fn(async () => '76a914abcd88ac') }
270
+ })
271
+
272
+ const result = await module.buildSettlement(
273
+ { threadId: 'tid', option: { amountSatoshis: 100, payee: 'pk' } },
274
+ makeContext(wallet)
275
+ )
276
+ expect(result.action).toBe('terminate')
277
+ if (result.action === 'terminate') {
278
+ expect(result.termination.code).toBe('brc29.invalid_tx')
279
+ }
280
+ })
281
+
282
+ it('terminates when tx is an empty array', async () => {
283
+ const wallet = makeWallet({
284
+ getPublicKey: jest.fn(async () => ({ publicKey: '02deadbeef' })),
285
+ createAction: jest.fn(async () => ({ tx: [] })) // empty
286
+ })
287
+ const module = new Brc29RemittanceModule({
288
+ nonceProvider: {
289
+ createNonce: jest.fn()
290
+ .mockResolvedValueOnce('p')
291
+ .mockResolvedValueOnce('s')
292
+ },
293
+ lockingScriptProvider: { pubKeyToP2PKHLockingScript: jest.fn(async () => '76a914abcd88ac') }
294
+ })
295
+
296
+ const result = await module.buildSettlement(
297
+ { threadId: 'tid', option: { amountSatoshis: 100, payee: 'pk' } },
298
+ makeContext(wallet)
299
+ )
300
+ expect(result.action).toBe('terminate')
301
+ if (result.action === 'terminate') {
302
+ expect(result.termination.code).toBe('brc29.invalid_tx')
303
+ }
304
+ })
305
+
306
+ it('terminates when createAction throws an unexpected error', async () => {
307
+ const wallet = makeWallet({
308
+ getPublicKey: jest.fn(async () => ({ publicKey: '02deadbeef' })),
309
+ createAction: jest.fn(async () => { throw new Error('unexpected wallet error') })
310
+ })
311
+ const module = new Brc29RemittanceModule({
312
+ nonceProvider: {
313
+ createNonce: jest.fn()
314
+ .mockResolvedValueOnce('p')
315
+ .mockResolvedValueOnce('s')
316
+ },
317
+ lockingScriptProvider: { pubKeyToP2PKHLockingScript: jest.fn(async () => '76a914abcd88ac') }
318
+ })
319
+
320
+ const result = await module.buildSettlement(
321
+ { threadId: 'tid', option: { amountSatoshis: 100, payee: 'pk' } },
322
+ makeContext(wallet)
323
+ )
324
+ expect(result.action).toBe('terminate')
325
+ if (result.action === 'terminate') {
326
+ expect(result.termination.code).toBe('brc29.build_failed')
327
+ expect(result.termination.message).toContain('unexpected wallet error')
328
+ }
329
+ })
330
+
331
+ it('terminates when createNonce throws', async () => {
332
+ const wallet = makeWallet()
333
+ const module = new Brc29RemittanceModule({
334
+ nonceProvider: {
335
+ createNonce: jest.fn(async () => { throw new Error('nonce error') })
336
+ },
337
+ lockingScriptProvider: { pubKeyToP2PKHLockingScript: jest.fn(async () => 'script') }
338
+ })
339
+
340
+ const result = await module.buildSettlement(
341
+ { threadId: 'tid', option: { amountSatoshis: 500, payee: 'pk' } },
342
+ makeContext(wallet)
343
+ )
344
+ expect(result.action).toBe('terminate')
345
+ if (result.action === 'terminate') {
346
+ expect(result.termination.code).toBe('brc29.build_failed')
347
+ }
348
+ })
349
+
350
+ it('uses option-level protocolID, labels, and description overrides', async () => {
351
+ const wallet = makeWallet({
352
+ createAction: jest.fn(async () => ({ tx: [7, 8, 9] }))
353
+ })
354
+ const module = new Brc29RemittanceModule({
355
+ nonceProvider: {
356
+ createNonce: jest.fn()
357
+ .mockResolvedValueOnce('pref')
358
+ .mockResolvedValueOnce('suf')
359
+ },
360
+ lockingScriptProvider: { pubKeyToP2PKHLockingScript: jest.fn(async () => '76a914abcd88ac') }
361
+ })
362
+
363
+ const result = await module.buildSettlement(
364
+ {
365
+ threadId: 'tid',
366
+ option: {
367
+ amountSatoshis: 777,
368
+ payee: 'pk',
369
+ protocolID: [1, 'custom-proto'],
370
+ labels: ['my-label'],
371
+ description: 'Custom description',
372
+ outputIndex: 2
373
+ }
374
+ },
375
+ makeContext(wallet)
376
+ )
377
+ expect(result.action).toBe('settle')
378
+ if (result.action === 'settle') {
379
+ expect(result.artifact.outputIndex).toBe(2)
380
+ expect(result.artifact.amountSatoshis).toBe(777)
381
+ }
382
+
383
+ // Verify getPublicKey was called with the option's protocolID
384
+ expect(wallet.getPublicKey).toHaveBeenCalledWith(
385
+ expect.objectContaining({ protocolID: [1, 'custom-proto'] }),
386
+ 'example.com'
387
+ )
388
+
389
+ // Verify createAction was called with option's labels and description
390
+ const createArgs = (wallet.createAction as jest.Mock).mock.calls[0][0]
391
+ expect(createArgs.labels).toEqual(['my-label'])
392
+ expect(createArgs.description).toBe('Custom description')
393
+ })
394
+ })
395
+
396
+ // ---------------------------------------------------------------------------
397
+ // acceptSettlement – settlement validation edge cases
398
+ // ---------------------------------------------------------------------------
399
+
400
+ describe('Brc29RemittanceModule – acceptSettlement validation', () => {
401
+ it('terminates when settlement is null', async () => {
402
+ const module = new Brc29RemittanceModule()
403
+ const result = await module.acceptSettlement(
404
+ { threadId: 'tid', settlement: null as any, sender: 'pk' },
405
+ makeContext(makeWallet())
406
+ )
407
+ expect(result.action).toBe('terminate')
408
+ if (result.action === 'terminate') {
409
+ expect(result.termination.code).toBe('brc29.internalize_failed')
410
+ }
411
+ })
412
+
413
+ it('terminates when settlement is a non-object primitive', async () => {
414
+ const module = new Brc29RemittanceModule()
415
+ const result = await module.acceptSettlement(
416
+ { threadId: 'tid', settlement: 'not-an-object' as any, sender: 'pk' },
417
+ makeContext(makeWallet())
418
+ )
419
+ expect(result.action).toBe('terminate')
420
+ })
421
+
422
+ it('terminates when customInstructions is missing', async () => {
423
+ const module = new Brc29RemittanceModule()
424
+ const result = await module.acceptSettlement(
425
+ {
426
+ threadId: 'tid',
427
+ settlement: { transaction: [1, 2, 3], amountSatoshis: 100 } as any,
428
+ sender: 'pk'
429
+ },
430
+ makeContext(makeWallet())
431
+ )
432
+ expect(result.action).toBe('terminate')
433
+ })
434
+
435
+ it('terminates when derivationPrefix is empty', async () => {
436
+ const module = new Brc29RemittanceModule()
437
+ const result = await module.acceptSettlement(
438
+ {
439
+ threadId: 'tid',
440
+ settlement: {
441
+ customInstructions: { derivationPrefix: '', derivationSuffix: 'suffix' },
442
+ transaction: [1, 2, 3],
443
+ amountSatoshis: 100
444
+ },
445
+ sender: 'pk'
446
+ },
447
+ makeContext(makeWallet())
448
+ )
449
+ expect(result.action).toBe('terminate')
450
+ })
451
+
452
+ it('terminates when derivationSuffix is empty', async () => {
453
+ const module = new Brc29RemittanceModule()
454
+ const result = await module.acceptSettlement(
455
+ {
456
+ threadId: 'tid',
457
+ settlement: {
458
+ customInstructions: { derivationPrefix: 'prefix', derivationSuffix: '' },
459
+ transaction: [1, 2, 3],
460
+ amountSatoshis: 100
461
+ },
462
+ sender: 'pk'
463
+ },
464
+ makeContext(makeWallet())
465
+ )
466
+ expect(result.action).toBe('terminate')
467
+ })
468
+
469
+ it('terminates when amountSatoshis is zero', async () => {
470
+ const module = new Brc29RemittanceModule()
471
+ const result = await module.acceptSettlement(
472
+ {
473
+ threadId: 'tid',
474
+ settlement: {
475
+ customInstructions: { derivationPrefix: 'p', derivationSuffix: 's' },
476
+ transaction: [1, 2, 3],
477
+ amountSatoshis: 0
478
+ },
479
+ sender: 'pk'
480
+ },
481
+ makeContext(makeWallet())
482
+ )
483
+ expect(result.action).toBe('terminate')
484
+ })
485
+
486
+ it('terminates when amountSatoshis is negative', async () => {
487
+ const module = new Brc29RemittanceModule()
488
+ const result = await module.acceptSettlement(
489
+ {
490
+ threadId: 'tid',
491
+ settlement: {
492
+ customInstructions: { derivationPrefix: 'p', derivationSuffix: 's' },
493
+ transaction: [1, 2, 3],
494
+ amountSatoshis: -1
495
+ },
496
+ sender: 'pk'
497
+ },
498
+ makeContext(makeWallet())
499
+ )
500
+ expect(result.action).toBe('terminate')
501
+ })
502
+
503
+ it('terminates when outputIndex is negative', async () => {
504
+ const module = new Brc29RemittanceModule()
505
+ const result = await module.acceptSettlement(
506
+ {
507
+ threadId: 'tid',
508
+ settlement: {
509
+ customInstructions: { derivationPrefix: 'p', derivationSuffix: 's' },
510
+ transaction: [1, 2, 3],
511
+ amountSatoshis: 1000,
512
+ outputIndex: -2
513
+ },
514
+ sender: 'pk'
515
+ },
516
+ makeContext(makeWallet())
517
+ )
518
+ expect(result.action).toBe('terminate')
519
+ })
520
+
521
+ it('terminates when transaction is not a byte array (invalid bytes)', async () => {
522
+ const module = new Brc29RemittanceModule()
523
+ const result = await module.acceptSettlement(
524
+ {
525
+ threadId: 'tid',
526
+ settlement: {
527
+ customInstructions: { derivationPrefix: 'p', derivationSuffix: 's' },
528
+ transaction: [256, 0, 1], // 256 is out of range
529
+ amountSatoshis: 1000
530
+ },
531
+ sender: 'pk'
532
+ },
533
+ makeContext(makeWallet())
534
+ )
535
+ expect(result.action).toBe('terminate')
536
+ })
537
+
538
+ it('terminates when transaction is an empty array', async () => {
539
+ const module = new Brc29RemittanceModule()
540
+ const result = await module.acceptSettlement(
541
+ {
542
+ threadId: 'tid',
543
+ settlement: {
544
+ customInstructions: { derivationPrefix: 'p', derivationSuffix: 's' },
545
+ transaction: [],
546
+ amountSatoshis: 1000
547
+ },
548
+ sender: 'pk'
549
+ },
550
+ makeContext(makeWallet())
551
+ )
552
+ expect(result.action).toBe('terminate')
553
+ })
554
+
555
+ it('uses outputIndex=0 by default when outputIndex is undefined', async () => {
556
+ const internalizeAction = jest.fn(async () => ({ accepted: true as const }))
557
+ const wallet = makeWallet({ internalizeAction })
558
+ const module = new Brc29RemittanceModule()
559
+
560
+ const result = await module.acceptSettlement(
561
+ { threadId: 'tid', settlement: { ...validSettlement }, sender: 'sender-key' },
562
+ makeContext(wallet)
563
+ )
564
+ expect(result.action).toBe('accept')
565
+ expect(internalizeAction).toHaveBeenCalledWith(
566
+ expect.objectContaining({
567
+ outputs: [expect.objectContaining({ outputIndex: 0 })]
568
+ }),
569
+ 'example.com'
570
+ )
571
+ })
572
+
573
+ it('uses basket insertion internalizeProtocol when configured', async () => {
574
+ const internalizeAction = jest.fn(async () => ({ accepted: true as const }))
575
+ const wallet = makeWallet({ internalizeAction })
576
+ const module = new Brc29RemittanceModule({ internalizeProtocol: 'basket insertion' })
577
+
578
+ const result = await module.acceptSettlement(
579
+ { threadId: 'tid', settlement: { ...validSettlement }, sender: 'sender-key' },
580
+ makeContext(wallet)
581
+ )
582
+ expect(result.action).toBe('accept')
583
+ expect(internalizeAction).toHaveBeenCalledWith(
584
+ expect.objectContaining({
585
+ outputs: [expect.objectContaining({ protocol: 'basket insertion' })]
586
+ }),
587
+ 'example.com'
588
+ )
589
+ })
590
+ })
591
+
592
+ // ---------------------------------------------------------------------------
593
+ // Constructor defaults
594
+ // ---------------------------------------------------------------------------
595
+
596
+ describe('Brc29RemittanceModule – constructor defaults', () => {
597
+ it('has expected default property values', () => {
598
+ const module = new Brc29RemittanceModule()
599
+ expect(module.id).toBe('brc29.p2pkh')
600
+ expect(module.name).toBe('BSV (BRC-29 derived P2PKH)')
601
+ expect(module.allowUnsolicitedSettlements).toBe(true)
602
+ expect((module as any).protocolID).toEqual([2, '3241645161d8'])
603
+ expect((module as any).labels).toEqual(['brc29'])
604
+ expect((module as any).description).toBe('BRC-29 payment')
605
+ expect((module as any).outputDescription).toBe('Payment for remittance invoice')
606
+ expect((module as any).refundFeeSatoshis).toBe(1000)
607
+ expect((module as any).minRefundSatoshis).toBe(1000)
608
+ expect((module as any).internalizeProtocol).toBe('wallet payment')
609
+ })
610
+
611
+ it('accepts config overrides for all properties', () => {
612
+ const customNonce = { createNonce: jest.fn() }
613
+ const customScript = { pubKeyToP2PKHLockingScript: jest.fn() }
614
+ const module = new Brc29RemittanceModule({
615
+ protocolID: [1, 'custom'],
616
+ labels: ['lbl'],
617
+ description: 'desc',
618
+ outputDescription: 'out-desc',
619
+ refundFeeSatoshis: 500,
620
+ minRefundSatoshis: 200,
621
+ internalizeProtocol: 'basket insertion',
622
+ nonceProvider: customNonce,
623
+ lockingScriptProvider: customScript
624
+ })
625
+ expect((module as any).protocolID).toEqual([1, 'custom'])
626
+ expect((module as any).labels).toEqual(['lbl'])
627
+ expect((module as any).description).toBe('desc')
628
+ expect((module as any).outputDescription).toBe('out-desc')
629
+ expect((module as any).refundFeeSatoshis).toBe(500)
630
+ expect((module as any).minRefundSatoshis).toBe(200)
631
+ expect((module as any).internalizeProtocol).toBe('basket insertion')
632
+ expect((module as any).nonceProvider).toBe(customNonce)
633
+ expect((module as any).lockingScriptProvider).toBe(customScript)
634
+ })
635
+ })
636
+
637
+ // ---------------------------------------------------------------------------
638
+ // DefaultNonceProvider and DefaultLockingScriptProvider are exported;
639
+ // test that they satisfy the interfaces (smoke tests only – actual crypto
640
+ // tested elsewhere).
641
+ // ---------------------------------------------------------------------------
642
+
643
+ describe('DefaultNonceProvider and DefaultLockingScriptProvider', () => {
644
+ it('DefaultNonceProvider.createNonce delegates to createNonce util', async () => {
645
+ const fakeWallet = {
646
+ createHmac: jest.fn(async () => ({ data: new Array(32).fill(0) }))
647
+ } as unknown as WalletInterface
648
+ // createNonce will fail without real wallet; just ensure the function exists and is async
649
+ await expect(
650
+ DefaultNonceProvider.createNonce(fakeWallet, 'self', 'example.com')
651
+ ).rejects.toBeDefined() // real createNonce needs full wallet
652
+ })
653
+
654
+ it('DefaultLockingScriptProvider has pubKeyToP2PKHLockingScript method', () => {
655
+ expect(typeof DefaultLockingScriptProvider.pubKeyToP2PKHLockingScript).toBe('function')
656
+ })
657
+ })