@bsv/sdk 2.0.12 → 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 (77) 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/transaction/MerklePath.js +132 -0
  7. package/dist/cjs/src/transaction/MerklePath.js.map +1 -1
  8. package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
  9. package/dist/esm/src/auth/clients/__tests__/AuthFetch.additional.test.js +825 -0
  10. package/dist/esm/src/auth/clients/__tests__/AuthFetch.additional.test.js.map +1 -0
  11. package/dist/esm/src/auth/transports/__tests__/SimplifiedFetchTransport.additional.test.js +619 -0
  12. package/dist/esm/src/auth/transports/__tests__/SimplifiedFetchTransport.additional.test.js.map +1 -0
  13. package/dist/esm/src/transaction/MerklePath.js +132 -0
  14. package/dist/esm/src/transaction/MerklePath.js.map +1 -1
  15. package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
  16. package/dist/types/src/auth/clients/__tests__/AuthFetch.additional.test.d.ts +21 -0
  17. package/dist/types/src/auth/clients/__tests__/AuthFetch.additional.test.d.ts.map +1 -0
  18. package/dist/types/src/auth/transports/__tests__/SimplifiedFetchTransport.additional.test.d.ts +2 -0
  19. package/dist/types/src/auth/transports/__tests__/SimplifiedFetchTransport.additional.test.d.ts.map +1 -0
  20. package/dist/types/src/transaction/MerklePath.d.ts +27 -0
  21. package/dist/types/src/transaction/MerklePath.d.ts.map +1 -1
  22. package/dist/types/tsconfig.types.tsbuildinfo +1 -1
  23. package/dist/umd/bundle.js +1 -1
  24. package/dist/umd/bundle.js.map +1 -1
  25. package/docs/reference/storage.md +1 -1
  26. package/docs/reference/transaction.md +40 -0
  27. package/package.json +1 -1
  28. package/src/auth/clients/__tests__/AuthFetch.additional.test.ts +1131 -0
  29. package/src/auth/transports/__tests__/SimplifiedFetchTransport.additional.test.ts +770 -0
  30. package/src/compat/__tests/Mnemonic.additional.test.ts +64 -0
  31. package/src/identity/__tests/IdentityClient.additional.test.ts +767 -0
  32. package/src/kvstore/__tests/LocalKVStore.additional.test.ts +611 -0
  33. package/src/kvstore/__tests/kvStoreInterpreter.test.ts +327 -0
  34. package/src/overlay-tools/__tests/HostReputationTracker.additional.test.ts +561 -0
  35. package/src/overlay-tools/__tests/LookupResolver.additional.test.ts +612 -0
  36. package/src/overlay-tools/__tests/withDoubleSpendRetry.test.ts +278 -0
  37. package/src/primitives/__tests/BigNumber.additional.test.ts +79 -0
  38. package/src/primitives/__tests/Curve.additional.test.ts +208 -0
  39. package/src/primitives/__tests/ECDSA.additional.test.ts +122 -0
  40. package/src/primitives/__tests/Hash.additional.test.ts +59 -0
  41. package/src/primitives/__tests/JacobianPoint.test.ts +308 -0
  42. package/src/primitives/__tests/Point.additional.test.ts +503 -0
  43. package/src/primitives/__tests/PublicKey.additional.test.ts +383 -0
  44. package/src/primitives/__tests/Random.additional.test.ts +262 -0
  45. package/src/primitives/__tests/Signature.test.ts +333 -0
  46. package/src/primitives/__tests/TransactionSignature.additional.test.ts +241 -0
  47. package/src/registry/__tests/RegistryClient.additional.test.ts +750 -0
  48. package/src/remittance/__tests/BasicBRC29.additional.test.ts +657 -0
  49. package/src/remittance/__tests/RemittanceManager.additional.test.ts +1272 -0
  50. package/src/script/__tests/LockingUnlockingScript.test.ts +79 -0
  51. package/src/script/__tests/Script.additional.test.ts +100 -0
  52. package/src/script/__tests/ScriptEvaluationError.test.ts +98 -0
  53. package/src/script/__tests/Spend.additional.test.ts +837 -0
  54. package/src/script/templates/__tests/RPuzzle.test.ts +134 -0
  55. package/src/transaction/MerklePath.ts +155 -0
  56. package/src/transaction/__tests/BeefParty.additional.test.ts +22 -0
  57. package/src/transaction/__tests/Broadcaster.test.ts +159 -0
  58. package/src/transaction/__tests/MerklePath.bench.test.ts +105 -0
  59. package/src/transaction/__tests/MerklePath.test.ts +80 -0
  60. package/src/transaction/__tests/Transaction.additional.test.ts +225 -0
  61. package/src/transaction/broadcasters/__tests/ARC.additional.test.ts +585 -0
  62. package/src/transaction/broadcasters/__tests/Teranode.test.ts +349 -0
  63. package/src/transaction/chaintrackers/__tests/BlockHeadersService.test.ts +253 -0
  64. package/src/transaction/chaintrackers/__tests/DefaultChainTracker.test.ts +44 -0
  65. package/src/transaction/chaintrackers/__tests/WhatsOnChain.additional.test.ts +193 -0
  66. package/src/transaction/fee-models/__tests/SatoshisPerKilobyte.test.ts +262 -0
  67. package/src/transaction/http/__tests/BinaryFetchClient.test.ts +212 -0
  68. package/src/transaction/http/__tests/DefaultHttpClient.additional.test.ts +192 -0
  69. package/src/transaction/http/__tests/DefaultHttpClient.test.ts +71 -0
  70. package/src/wallet/__tests/ProtoWallet.additional.test.ts +134 -0
  71. package/src/wallet/__tests/WERR.test.ts +212 -0
  72. package/src/wallet/__tests/WalletClient.additional.test.ts +699 -0
  73. package/src/wallet/__tests/WalletClient.substrate.test.ts +759 -0
  74. package/src/wallet/__tests/WalletError.test.ts +290 -0
  75. package/src/wallet/__tests/validationHelpers.test.ts +1218 -0
  76. package/src/wallet/substrates/__tests/HTTPWalletJSON.test.ts +496 -0
  77. package/src/wallet/substrates/__tests/HTTPWalletWire.test.ts +273 -0
@@ -0,0 +1,611 @@
1
+ /**
2
+ * Additional tests for LocalKVStore targeting branches missed by LocalKVStore.test.ts.
3
+ *
4
+ * Covered gaps (29 missed lines → aim for ~80 %+):
5
+ * - constructor: originator and acceptDelayedBroadcast params
6
+ * - get: real lookupValue path (no BEEF, BEEF missing error, PushDrop decode,
7
+ * wrong fields count, encrypted path, non-encrypted path)
8
+ * - set: value-unchanged early-return path
9
+ * createAction returns txid directly when no inputs (signableTransaction null)
10
+ * acceptDelayedBroadcast=true option forwarding
11
+ * - remove: pagination loop (outputs.length < totalOutputs → loop again)
12
+ * signAction returns undefined txid → throws
13
+ * createAction returns no signableTransaction → throws
14
+ * - getOutputs: limit param forwarding
15
+ * - queueOperationOnKey: concurrent requests queue correctly
16
+ */
17
+
18
+ import LocalKVStore from '../LocalKVStore.js'
19
+ import LockingScript from '../../script/LockingScript.js'
20
+ import PushDrop from '../../script/templates/PushDrop.js'
21
+ import * as Utils from '../../primitives/utils.js'
22
+ import {
23
+ WalletInterface,
24
+ ListOutputsResult,
25
+ WalletEncryptResult,
26
+ WalletDecryptResult,
27
+ CreateActionResult,
28
+ SignActionResult
29
+ } from '../../wallet/Wallet.interfaces.js'
30
+ import Transaction from '../../transaction/Transaction.js'
31
+
32
+ // ---- Constants mirrored from the existing test ----
33
+ const testLockingScriptHex = 'mockLockingScriptHex'
34
+ const testUnlockingScriptHex = 'mockUnlockingScriptHex'
35
+ const testEncryptedValue = Buffer.from('encryptedData')
36
+ const testRawValue = 'myTestDataValue'
37
+ const testRawValueBuffer = Buffer.from(testRawValue)
38
+
39
+ jest.mock('../../script/LockingScript.js', () => {
40
+ const mockLockingScriptInstance = { toHex: jest.fn(() => testLockingScriptHex) }
41
+ return { fromHex: jest.fn(() => mockLockingScriptInstance) }
42
+ })
43
+
44
+ jest.mock('../../script/templates/PushDrop.js', () => {
45
+ const mockLockingScriptInstance = { toHex: jest.fn(() => testLockingScriptHex) }
46
+ const mockUnlockerInstance = {
47
+ sign: jest.fn().mockResolvedValue({ toHex: jest.fn(() => testUnlockingScriptHex) })
48
+ }
49
+ const mockPushDropInstance = {
50
+ lock: jest.fn().mockResolvedValue(mockLockingScriptInstance),
51
+ unlock: jest.fn().mockReturnValue(mockUnlockerInstance)
52
+ }
53
+ const mockPushDropDecode = jest.fn()
54
+ return Object.assign(
55
+ jest.fn(() => mockPushDropInstance),
56
+ { decode: mockPushDropDecode }
57
+ )
58
+ })
59
+
60
+ jest.mock('../../transaction/Transaction.js', () => ({
61
+ fromAtomicBEEF: jest.fn(() => ({}))
62
+ }))
63
+
64
+ jest.mock('../../primitives/utils.js', () => ({
65
+ toArray: jest.fn((str: string, encoding = 'utf8') => Array.from(Buffer.from(str, encoding as BufferEncoding))),
66
+ toUTF8: jest.fn((arr: number[] | Uint8Array) => Buffer.from(arr).toString('utf8'))
67
+ }))
68
+
69
+ jest.mock('../../wallet/WalletClient.js', () => jest.fn())
70
+
71
+ // ---- Typed mock aliases ----
72
+ const MockedPushDrop = PushDrop as jest.MockedClass<typeof PushDrop> & { decode: jest.Mock<any, any> }
73
+ const MockedPushDropDecode = MockedPushDrop.decode
74
+ const MockedUtils = Utils as jest.Mocked<typeof Utils>
75
+ const MockedTransaction = Transaction as jest.Mocked<typeof Transaction>
76
+
77
+ // ---- Beef mock ----
78
+ // LocalKVStore uses `Beef.fromBinary` internally. We mock only the relevant behaviour.
79
+ jest.mock('../../transaction/Beef.js', () => ({
80
+ Beef: {
81
+ fromBinary: jest.fn(() => ({
82
+ findTxid: jest.fn(() => ({
83
+ tx: {
84
+ outputs: [
85
+ { lockingScript: { toHex: () => testLockingScriptHex } }
86
+ ]
87
+ }
88
+ }))
89
+ }))
90
+ }
91
+ }))
92
+
93
+ // ---- Helper ----
94
+ const createMockWallet = (): jest.Mocked<WalletInterface> => ({
95
+ listOutputs: jest.fn(),
96
+ encrypt: jest.fn(),
97
+ decrypt: jest.fn(),
98
+ createAction: jest.fn(),
99
+ signAction: jest.fn(),
100
+ relinquishOutput: jest.fn()
101
+ } as unknown as jest.Mocked<WalletInterface>)
102
+
103
+ const testContext = 'test-kv-context'
104
+ const testKey = 'myTestKey'
105
+ const testValue = 'myTestDataValue'
106
+ const testOutpoint = 'txid123.0'
107
+
108
+ describe('LocalKVStore – additional coverage', () => {
109
+ let mockWallet: jest.Mocked<WalletInterface>
110
+ let kvStore: LocalKVStore
111
+
112
+ beforeEach(() => {
113
+ jest.clearAllMocks()
114
+ mockWallet = createMockWallet()
115
+ kvStore = new LocalKVStore(mockWallet, testContext, true)
116
+ MockedPushDropDecode.mockClear()
117
+ })
118
+
119
+ // ---------------------------------------------------------------------------
120
+ // Constructor edge cases
121
+ // ---------------------------------------------------------------------------
122
+
123
+ describe('constructor', () => {
124
+ it('stores originator when provided', () => {
125
+ const store = new LocalKVStore(mockWallet, testContext, true, 'test.com')
126
+ expect((store as any).originator).toBe('test.com')
127
+ })
128
+
129
+ it('sets acceptDelayedBroadcast when provided', () => {
130
+ const store = new LocalKVStore(mockWallet, testContext, true, undefined, true)
131
+ expect(store.acceptDelayedBroadcast).toBe(true)
132
+ })
133
+ })
134
+
135
+ // ---------------------------------------------------------------------------
136
+ // get / lookupValue – real path (not mocked via private injection)
137
+ // ---------------------------------------------------------------------------
138
+
139
+ describe('get – real lookupValue path', () => {
140
+ it('returns defaultValue when outputs array is empty', async () => {
141
+ mockWallet.listOutputs.mockResolvedValue({ outputs: [], totalOutputs: 0, BEEF: undefined })
142
+ const result = await kvStore.get(testKey, 'fallback')
143
+ expect(result).toBe('fallback')
144
+ })
145
+
146
+ it('throws when BEEF is undefined but outputs exist', async () => {
147
+ mockWallet.listOutputs.mockResolvedValue({
148
+ outputs: [{ outpoint: `${testOutpoint}`, satoshis: 1, spendable: true }],
149
+ totalOutputs: 1,
150
+ BEEF: undefined
151
+ })
152
+
153
+ await expect(kvStore.get(testKey)).rejects.toThrow(
154
+ 'Invalid value found'
155
+ )
156
+ })
157
+
158
+ it('returns decoded non-encrypted value when encrypt=false', async () => {
159
+ const kvStoreNoEnc = new LocalKVStore(mockWallet, testContext, false)
160
+
161
+ // Provide a real BEEF-like binary so Beef.fromBinary is called
162
+ const fakeBEEF = [1, 2, 3]
163
+ mockWallet.listOutputs.mockResolvedValue({
164
+ outputs: [{ outpoint: 'txhash.0', satoshis: 1, spendable: true }],
165
+ totalOutputs: 1,
166
+ BEEF: fakeBEEF
167
+ } as any)
168
+
169
+ // PushDrop.decode returns 1 field (valid)
170
+ const rawValueBytes = Array.from(Buffer.from(testRawValue))
171
+ MockedPushDropDecode.mockReturnValue({ fields: [rawValueBytes] })
172
+ MockedUtils.toUTF8.mockReturnValue(testRawValue)
173
+
174
+ const result = await kvStoreNoEnc.get(testKey)
175
+ expect(result).toBe(testRawValue)
176
+ expect(MockedUtils.toUTF8).toHaveBeenCalledWith(rawValueBytes)
177
+ })
178
+
179
+ it('returns decrypted value when encrypt=true', async () => {
180
+ const fakeBEEF = [1, 2, 3]
181
+ mockWallet.listOutputs.mockResolvedValue({
182
+ outputs: [{ outpoint: 'txhash.0', satoshis: 1, spendable: true }],
183
+ totalOutputs: 1,
184
+ BEEF: fakeBEEF
185
+ } as any)
186
+
187
+ const ciphertextBytes = Array.from(testEncryptedValue)
188
+ MockedPushDropDecode.mockReturnValue({ fields: [ciphertextBytes] })
189
+
190
+ const plaintextBytes = Array.from(Buffer.from(testRawValue))
191
+ mockWallet.decrypt.mockResolvedValue({ plaintext: plaintextBytes } as WalletDecryptResult)
192
+ MockedUtils.toUTF8.mockReturnValue(testRawValue)
193
+
194
+ const result = await kvStore.get(testKey)
195
+ expect(result).toBe(testRawValue)
196
+ expect(mockWallet.decrypt).toHaveBeenCalledWith(
197
+ { protocolID: [2, testContext], keyID: testKey, ciphertext: ciphertextBytes },
198
+ undefined
199
+ )
200
+ })
201
+
202
+ it('throws when PushDrop.decode returns wrong number of fields (0 fields)', async () => {
203
+ const fakeBEEF = [1, 2, 3]
204
+ mockWallet.listOutputs.mockResolvedValue({
205
+ outputs: [{ outpoint: 'txhash.0', satoshis: 1, spendable: true }],
206
+ totalOutputs: 1,
207
+ BEEF: fakeBEEF
208
+ } as any)
209
+
210
+ // 0 fields is invalid (must be 1 or 2)
211
+ MockedPushDropDecode.mockReturnValue({ fields: [] })
212
+
213
+ await expect(kvStore.get(testKey)).rejects.toThrow('Invalid value found')
214
+ })
215
+
216
+ it('throws when PushDrop.decode returns too many fields (3 fields)', async () => {
217
+ const fakeBEEF = [1, 2, 3]
218
+ mockWallet.listOutputs.mockResolvedValue({
219
+ outputs: [{ outpoint: 'txhash.0', satoshis: 1, spendable: true }],
220
+ totalOutputs: 1,
221
+ BEEF: fakeBEEF
222
+ } as any)
223
+
224
+ // 3 fields is also invalid
225
+ MockedPushDropDecode.mockReturnValue({ fields: [[1], [2], [3]] })
226
+
227
+ await expect(kvStore.get(testKey)).rejects.toThrow('Invalid value found')
228
+ })
229
+
230
+ it('uses the last output when multiple outputs are present', async () => {
231
+ const kvStoreNoEnc = new LocalKVStore(mockWallet, testContext, false)
232
+ const fakeBEEF = [1, 2, 3]
233
+ mockWallet.listOutputs.mockResolvedValue({
234
+ outputs: [
235
+ { outpoint: 'old.0', satoshis: 1, spendable: true },
236
+ { outpoint: 'newer.0', satoshis: 1, spendable: true }
237
+ ],
238
+ totalOutputs: 2,
239
+ BEEF: fakeBEEF
240
+ } as any)
241
+
242
+ const rawValueBytes = Array.from(Buffer.from('latestValue'))
243
+ MockedPushDropDecode.mockReturnValue({ fields: [rawValueBytes] })
244
+ MockedUtils.toUTF8.mockReturnValue('latestValue')
245
+
246
+ const result = await kvStoreNoEnc.get(testKey)
247
+ expect(result).toBe('latestValue')
248
+ })
249
+ })
250
+
251
+ // ---------------------------------------------------------------------------
252
+ // set – value-unchanged early-return
253
+ // ---------------------------------------------------------------------------
254
+
255
+ describe('set – value unchanged early-return', () => {
256
+ it('returns existing outpoint without creating a transaction when value is unchanged', async () => {
257
+ const existingOutpoint = 'samevalue-txid.0'
258
+ const mockedLor: ListOutputsResult = {
259
+ totalOutputs: 1,
260
+ outputs: [{ satoshis: 1, spendable: true, outpoint: existingOutpoint }],
261
+ BEEF: [1, 2, 3]
262
+ }
263
+
264
+ const lookupValueReal = kvStore['lookupValue']
265
+ kvStore['lookupValue'] = jest.fn().mockResolvedValue({
266
+ value: testValue, // same value as what we're setting
267
+ outpoint: existingOutpoint,
268
+ lor: mockedLor
269
+ })
270
+
271
+ const result = await kvStore.set(testKey, testValue)
272
+
273
+ kvStore['lookupValue'] = lookupValueReal
274
+
275
+ expect(result).toBe(existingOutpoint)
276
+ expect(mockWallet.createAction).not.toHaveBeenCalled()
277
+ expect(mockWallet.signAction).not.toHaveBeenCalled()
278
+ })
279
+
280
+ it('throws when value matches but outpoint is undefined (invalid state)', async () => {
281
+ const mockedLor: ListOutputsResult = {
282
+ totalOutputs: 0,
283
+ outputs: [],
284
+ BEEF: undefined
285
+ }
286
+
287
+ const lookupValueReal = kvStore['lookupValue']
288
+ kvStore['lookupValue'] = jest.fn().mockResolvedValue({
289
+ value: testValue, // same as what we want to set
290
+ outpoint: undefined, // but no outpoint – invalid state
291
+ lor: mockedLor
292
+ })
293
+
294
+ await expect(kvStore.set(testKey, testValue)).rejects.toThrow(
295
+ 'outpoint must be valid when value is valid and unchanged'
296
+ )
297
+
298
+ kvStore['lookupValue'] = lookupValueReal
299
+ })
300
+ })
301
+
302
+ // ---------------------------------------------------------------------------
303
+ // set – acceptDelayedBroadcast forwarding
304
+ // ---------------------------------------------------------------------------
305
+
306
+ describe('set – acceptDelayedBroadcast=true', () => {
307
+ it('forwards acceptDelayedBroadcast=true to createAction options', async () => {
308
+ const delayedKvStore = new LocalKVStore(mockWallet, testContext, false, undefined, true)
309
+
310
+ const valueArray = Array.from(testRawValueBuffer)
311
+ MockedUtils.toArray.mockReturnValue(valueArray)
312
+ mockWallet.createAction.mockResolvedValue({ txid: 'delayedTxId' } as CreateActionResult)
313
+
314
+ const lookupValueReal = delayedKvStore['lookupValue']
315
+ delayedKvStore['lookupValue'] = jest.fn().mockResolvedValue({
316
+ value: 'different', // force actual createAction call
317
+ outpoint: undefined,
318
+ lor: { outputs: [], totalOutputs: 0, BEEF: undefined }
319
+ })
320
+
321
+ await delayedKvStore.set(testKey, testValue)
322
+ delayedKvStore['lookupValue'] = lookupValueReal
323
+
324
+ expect(mockWallet.createAction).toHaveBeenCalledWith(
325
+ expect.objectContaining({
326
+ options: expect.objectContaining({ acceptDelayedBroadcast: true })
327
+ }),
328
+ undefined
329
+ )
330
+ })
331
+ })
332
+
333
+ // ---------------------------------------------------------------------------
334
+ // set – createAction returns txid with no signableTransaction (no existing outputs)
335
+ // ---------------------------------------------------------------------------
336
+
337
+ describe('set – createAction returns txid directly (no inputs to sign)', () => {
338
+ it('returns txid.0 when signableTransaction is null and outputs.length is 0', async () => {
339
+ const valueArray = Array.from(testRawValueBuffer)
340
+ MockedUtils.toArray.mockReturnValue(valueArray)
341
+ // kvStore has encrypt=true, so wallet.encrypt must be mocked
342
+ mockWallet.encrypt.mockResolvedValue({ ciphertext: valueArray } as any)
343
+ mockWallet.createAction.mockResolvedValue({ txid: 'newDirectTxId' } as CreateActionResult)
344
+
345
+ // Stub lookupValue to return no existing value
346
+ const lookupValueReal = kvStore['lookupValue']
347
+ kvStore['lookupValue'] = jest.fn().mockResolvedValue({
348
+ value: undefined,
349
+ outpoint: undefined,
350
+ lor: { outputs: [], totalOutputs: 0, BEEF: undefined }
351
+ })
352
+
353
+ const result = await kvStore.set(testKey, testValue)
354
+ kvStore['lookupValue'] = lookupValueReal
355
+
356
+ expect(result).toBe('newDirectTxId.0')
357
+ expect(mockWallet.signAction).not.toHaveBeenCalled()
358
+ })
359
+ })
360
+
361
+ // ---------------------------------------------------------------------------
362
+ // set – throws when signableTransaction not returned but outputs exist
363
+ // ---------------------------------------------------------------------------
364
+
365
+ describe('set – throws when signableTransaction missing for existing outputs', () => {
366
+ it('throws "Wallet did not return a signable transaction when expected" when outputs exist but no signableTransaction', async () => {
367
+ const valueArray = Array.from(testRawValueBuffer)
368
+ const encryptedArray = Array.from(testEncryptedValue)
369
+ MockedUtils.toArray.mockReturnValue(valueArray)
370
+ mockWallet.encrypt.mockResolvedValue({ ciphertext: encryptedArray } as WalletEncryptResult)
371
+
372
+ // createAction returns txid (not signable) but we have inputs
373
+ mockWallet.createAction.mockResolvedValue({ txid: 'shouldNotReach' } as CreateActionResult)
374
+
375
+ const existingOutpoint = 'existing.0'
376
+ const lookupValueReal = kvStore['lookupValue']
377
+ kvStore['lookupValue'] = jest.fn().mockResolvedValue({
378
+ value: 'old',
379
+ outpoint: existingOutpoint,
380
+ lor: {
381
+ outputs: [{ satoshis: 1, spendable: true, outpoint: existingOutpoint }],
382
+ totalOutputs: 1,
383
+ BEEF: [1, 2, 3]
384
+ }
385
+ })
386
+
387
+ await expect(kvStore.set(testKey, testValue)).rejects.toThrow(
388
+ 'outputs with tag'
389
+ )
390
+
391
+ kvStore['lookupValue'] = lookupValueReal
392
+ })
393
+ })
394
+
395
+ // ---------------------------------------------------------------------------
396
+ // set – originator forwarding
397
+ // ---------------------------------------------------------------------------
398
+
399
+ describe('set – originator is forwarded to wallet calls', () => {
400
+ it('passes originator to encrypt, createAction, and signAction', async () => {
401
+ const storeWithOriginator = new LocalKVStore(mockWallet, testContext, true, 'my.app')
402
+
403
+ const valueArray = Array.from(testRawValueBuffer)
404
+ const encryptedArray = Array.from(testEncryptedValue)
405
+ MockedUtils.toArray.mockReturnValue(valueArray)
406
+ mockWallet.encrypt.mockResolvedValue({ ciphertext: encryptedArray } as WalletEncryptResult)
407
+ mockWallet.createAction.mockResolvedValue({ txid: 'origTxId' } as CreateActionResult)
408
+
409
+ const lookupValueReal = storeWithOriginator['lookupValue']
410
+ storeWithOriginator['lookupValue'] = jest.fn().mockResolvedValue({
411
+ value: undefined,
412
+ outpoint: undefined,
413
+ lor: { outputs: [], totalOutputs: 0, BEEF: undefined }
414
+ })
415
+
416
+ await storeWithOriginator.set(testKey, testValue)
417
+ storeWithOriginator['lookupValue'] = lookupValueReal
418
+
419
+ expect(mockWallet.encrypt).toHaveBeenCalledWith(expect.any(Object), 'my.app')
420
+ expect(mockWallet.createAction).toHaveBeenCalledWith(expect.any(Object), 'my.app')
421
+ })
422
+ })
423
+
424
+ // ---------------------------------------------------------------------------
425
+ // remove – pagination loop
426
+ // ---------------------------------------------------------------------------
427
+
428
+ describe('remove – pagination (outputs.length < totalOutputs → loops)', () => {
429
+ it('calls getOutputs in a loop until all outputs are processed', async () => {
430
+ const outpoint1 = 'page1-tx.0'
431
+ const output1 = { outpoint: outpoint1, satoshis: 1, spendable: true }
432
+ const mockBEEF = [1, 2, 3, 4]
433
+ const signableRef = 'ref-page'
434
+ const signableTx: any[] = []
435
+ const txId1 = 'removal-tx-1'
436
+ const txId2 = 'removal-tx-2'
437
+
438
+ // First call: outputs.length (1) < totalOutputs (2) → process output, then loop
439
+ // Second call: outputs.length (1) < totalOutputs (2) → process output, then loop
440
+ // Third call: outputs.length (0) === totalOutputs (0) → skip processing, break
441
+ mockWallet.listOutputs
442
+ .mockResolvedValueOnce({ outputs: [output1], totalOutputs: 2, BEEF: mockBEEF } as any)
443
+ .mockResolvedValueOnce({ outputs: [{ outpoint: 'page2-tx.0', satoshis: 1, spendable: true }], totalOutputs: 2, BEEF: mockBEEF } as any)
444
+ .mockResolvedValueOnce({ outputs: [], totalOutputs: 0, BEEF: undefined } as any)
445
+
446
+ MockedTransaction.fromAtomicBEEF.mockReturnValue({} as any)
447
+ mockWallet.createAction
448
+ .mockResolvedValueOnce({ signableTransaction: { reference: signableRef, tx: signableTx } } as CreateActionResult)
449
+ .mockResolvedValueOnce({ signableTransaction: { reference: signableRef, tx: signableTx } } as CreateActionResult)
450
+ mockWallet.signAction
451
+ .mockResolvedValueOnce({ txid: txId1 } as SignActionResult)
452
+ .mockResolvedValueOnce({ txid: txId2 } as SignActionResult)
453
+
454
+ const result = await kvStore.remove(testKey)
455
+
456
+ expect(result).toContain(txId1)
457
+ expect(result).toContain(txId2)
458
+ expect(mockWallet.listOutputs).toHaveBeenCalledTimes(3)
459
+ })
460
+ })
461
+
462
+ // ---------------------------------------------------------------------------
463
+ // remove – signAction returns undefined txid → throws
464
+ // ---------------------------------------------------------------------------
465
+
466
+ describe('remove – signAction returns undefined txid', () => {
467
+ it('throws "signAction must return a valid txid" when txid is undefined', async () => {
468
+ const outpoint = 'und-txid.0'
469
+ const output = { outpoint, satoshis: 1, spendable: true }
470
+ const mockBEEF = [9, 9, 9]
471
+
472
+ mockWallet.listOutputs.mockResolvedValue({ outputs: [output], totalOutputs: 1, BEEF: mockBEEF } as any)
473
+ MockedTransaction.fromAtomicBEEF.mockReturnValue({} as any)
474
+ mockWallet.createAction.mockResolvedValue({
475
+ signableTransaction: { reference: 'ref', tx: [] }
476
+ } as CreateActionResult)
477
+ // signAction returns object with undefined txid
478
+ mockWallet.signAction.mockResolvedValue({ txid: undefined } as any)
479
+
480
+ await expect(kvStore.remove(testKey)).rejects.toThrow('cannot be unlocked')
481
+ })
482
+ })
483
+
484
+ // ---------------------------------------------------------------------------
485
+ // remove – createAction returns no signableTransaction → throws
486
+ // ---------------------------------------------------------------------------
487
+
488
+ describe('remove – createAction returns txid (no signableTransaction)', () => {
489
+ it('throws "Wallet did not return a signable transaction when expected" when outputs exist but no signableTransaction returned', async () => {
490
+ const outpoint = 'missing-signable.0'
491
+ const output = { outpoint, satoshis: 1, spendable: true }
492
+ const mockBEEF = [1, 2]
493
+
494
+ mockWallet.listOutputs.mockResolvedValue({ outputs: [output], totalOutputs: 1, BEEF: mockBEEF } as any)
495
+ // createAction returns only txid (not a signable) - simulates a non-signable-tx wallet response
496
+ mockWallet.createAction.mockResolvedValue({ txid: 'tx-no-sign' } as CreateActionResult)
497
+
498
+ await expect(kvStore.remove(testKey)).rejects.toThrow('cannot be unlocked')
499
+ })
500
+ })
501
+
502
+ // ---------------------------------------------------------------------------
503
+ // getOutputs – limit parameter forwarding
504
+ // ---------------------------------------------------------------------------
505
+
506
+ describe('getOutputs – limit forwarding', () => {
507
+ it('passes the limit parameter to wallet.listOutputs', async () => {
508
+ mockWallet.listOutputs.mockResolvedValue({ outputs: [], totalOutputs: 0, BEEF: undefined })
509
+
510
+ // Call the private method directly via bracket notation
511
+ await (kvStore as any).getOutputs(testKey, 5)
512
+
513
+ expect(mockWallet.listOutputs).toHaveBeenCalledWith(
514
+ expect.objectContaining({ limit: 5 }),
515
+ undefined
516
+ )
517
+ })
518
+
519
+ it('omits limit when not provided', async () => {
520
+ mockWallet.listOutputs.mockResolvedValue({ outputs: [], totalOutputs: 0, BEEF: undefined })
521
+
522
+ await (kvStore as any).getOutputs(testKey)
523
+
524
+ expect(mockWallet.listOutputs).toHaveBeenCalledWith(
525
+ expect.objectContaining({ limit: undefined }),
526
+ undefined
527
+ )
528
+ })
529
+ })
530
+
531
+ // ---------------------------------------------------------------------------
532
+ // Concurrency – queueOperationOnKey serialises concurrent set() calls
533
+ // ---------------------------------------------------------------------------
534
+
535
+ describe('concurrency – queueOperationOnKey serialises operations on the same key', () => {
536
+ it('processes two concurrent set() calls sequentially on the same key', async () => {
537
+ const callOrder: number[] = []
538
+
539
+ // Each call resolves in order so we can detect interleaving
540
+ let resolveFirst!: () => void
541
+ const firstStarted = new Promise<void>((r) => { resolveFirst = r })
542
+
543
+ const valueArray = Array.from(testRawValueBuffer)
544
+ MockedUtils.toArray.mockReturnValue(valueArray)
545
+
546
+ // Simulate sequential wallet responses
547
+ let callCount = 0
548
+ const lookupValueReal = kvStore['lookupValue']
549
+ kvStore['lookupValue'] = jest.fn().mockImplementation(async () => {
550
+ const myCall = ++callCount
551
+ callOrder.push(myCall)
552
+ if (myCall === 1) resolveFirst()
553
+ // Yield to allow the second set() to try to acquire the lock
554
+ await new Promise((r) => setTimeout(r, 5))
555
+ return { value: undefined, outpoint: undefined, lor: { outputs: [], totalOutputs: 0, BEEF: undefined } }
556
+ })
557
+
558
+ mockWallet.encrypt.mockResolvedValue({ ciphertext: Array.from(testEncryptedValue) } as WalletEncryptResult)
559
+ mockWallet.createAction.mockResolvedValue({ txid: 'concurrent-tx' } as CreateActionResult)
560
+
561
+ const p1 = kvStore.set(testKey, 'value1')
562
+ const p2 = kvStore.set(testKey, 'value2')
563
+
564
+ await Promise.all([p1, p2])
565
+
566
+ kvStore['lookupValue'] = lookupValueReal
567
+
568
+ // Both calls must have completed
569
+ expect(callOrder).toHaveLength(2)
570
+ // Second call must start AFTER first call finishes (sequential, not interleaved)
571
+ expect(callOrder[0]).toBe(1)
572
+ expect(callOrder[1]).toBe(2)
573
+ })
574
+ })
575
+
576
+ // ---------------------------------------------------------------------------
577
+ // getLockingScript – throws when txid not found in BEEF
578
+ // ---------------------------------------------------------------------------
579
+
580
+ describe('getLockingScript – throws when txid not found in BEEF', () => {
581
+ it('throws "beef must contain txid" when findTxid returns null', async () => {
582
+ // Override the Beef mock for this test to return null for findTxid
583
+ const BeefModule = require('../../transaction/Beef.js')
584
+ BeefModule.Beef.fromBinary.mockReturnValueOnce({
585
+ findTxid: jest.fn(() => null)
586
+ })
587
+
588
+ const fakeBEEF = [1, 2, 3]
589
+ mockWallet.listOutputs.mockResolvedValue({
590
+ outputs: [{ outpoint: 'missingtxid.0', satoshis: 1, spendable: true }],
591
+ totalOutputs: 1,
592
+ BEEF: fakeBEEF
593
+ } as any)
594
+
595
+ MockedPushDropDecode.mockReturnValue({ fields: [[1, 2, 3]] })
596
+
597
+ await expect(kvStore.get(testKey)).rejects.toThrow('Invalid value found')
598
+ })
599
+ })
600
+
601
+ // ---------------------------------------------------------------------------
602
+ // getProtocol – uses context as protocolID namespace
603
+ // ---------------------------------------------------------------------------
604
+
605
+ describe('getProtocol', () => {
606
+ it('returns correct protocolID tuple using context', () => {
607
+ const protocol = (kvStore as any).getProtocol('some-key')
608
+ expect(protocol).toEqual({ protocolID: [2, testContext], keyID: 'some-key' })
609
+ })
610
+ })
611
+ })