@bsv/sdk 1.4.15 → 1.4.18

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 (64) hide show
  1. package/dist/cjs/mod.js +1 -0
  2. package/dist/cjs/mod.js.map +1 -1
  3. package/dist/cjs/package.json +9 -9
  4. package/dist/cjs/src/kvstore/LocalKVStore.js +279 -0
  5. package/dist/cjs/src/kvstore/LocalKVStore.js.map +1 -0
  6. package/dist/cjs/src/kvstore/index.js +9 -0
  7. package/dist/cjs/src/kvstore/index.js.map +1 -0
  8. package/dist/cjs/src/wallet/WERR_REVIEW_ACTIONS.js +29 -0
  9. package/dist/cjs/src/wallet/WERR_REVIEW_ACTIONS.js.map +1 -0
  10. package/dist/cjs/src/wallet/WalletError.js +4 -3
  11. package/dist/cjs/src/wallet/WalletError.js.map +1 -1
  12. package/dist/cjs/src/wallet/index.js +4 -1
  13. package/dist/cjs/src/wallet/index.js.map +1 -1
  14. package/dist/cjs/src/wallet/substrates/HTTPWalletJSON.js +13 -6
  15. package/dist/cjs/src/wallet/substrates/HTTPWalletJSON.js.map +1 -1
  16. package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
  17. package/dist/esm/mod.js +1 -0
  18. package/dist/esm/mod.js.map +1 -1
  19. package/dist/esm/src/kvstore/LocalKVStore.js +273 -0
  20. package/dist/esm/src/kvstore/LocalKVStore.js.map +1 -0
  21. package/dist/esm/src/kvstore/index.js +2 -0
  22. package/dist/esm/src/kvstore/index.js.map +1 -0
  23. package/dist/esm/src/wallet/WERR_REVIEW_ACTIONS.js +31 -0
  24. package/dist/esm/src/wallet/WERR_REVIEW_ACTIONS.js.map +1 -0
  25. package/dist/esm/src/wallet/WalletError.js +3 -2
  26. package/dist/esm/src/wallet/WalletError.js.map +1 -1
  27. package/dist/esm/src/wallet/index.js +2 -0
  28. package/dist/esm/src/wallet/index.js.map +1 -1
  29. package/dist/esm/src/wallet/substrates/HTTPWalletJSON.js +13 -6
  30. package/dist/esm/src/wallet/substrates/HTTPWalletJSON.js.map +1 -1
  31. package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
  32. package/dist/types/mod.d.ts +1 -0
  33. package/dist/types/mod.d.ts.map +1 -1
  34. package/dist/types/src/kvstore/LocalKVStore.d.ts +85 -0
  35. package/dist/types/src/kvstore/LocalKVStore.d.ts.map +1 -0
  36. package/dist/types/src/kvstore/index.d.ts +2 -0
  37. package/dist/types/src/kvstore/index.d.ts.map +1 -0
  38. package/dist/types/src/wallet/WERR_REVIEW_ACTIONS.d.ts +23 -0
  39. package/dist/types/src/wallet/WERR_REVIEW_ACTIONS.d.ts.map +1 -0
  40. package/dist/types/src/wallet/Wallet.interfaces.d.ts +22 -0
  41. package/dist/types/src/wallet/Wallet.interfaces.d.ts.map +1 -1
  42. package/dist/types/src/wallet/WalletError.d.ts +4 -3
  43. package/dist/types/src/wallet/WalletError.d.ts.map +1 -1
  44. package/dist/types/src/wallet/index.d.ts +1 -0
  45. package/dist/types/src/wallet/index.d.ts.map +1 -1
  46. package/dist/types/src/wallet/substrates/HTTPWalletJSON.d.ts.map +1 -1
  47. package/dist/types/tsconfig.types.tsbuildinfo +1 -1
  48. package/dist/umd/bundle.js +1 -1
  49. package/docs/identity.md +225 -0
  50. package/docs/kvstore.md +133 -0
  51. package/docs/registry.md +383 -0
  52. package/docs/transaction.md +3 -3
  53. package/docs/wallet.md +146 -38
  54. package/mod.ts +2 -1
  55. package/package.json +19 -9
  56. package/src/kvstore/LocalKVStore.ts +287 -0
  57. package/src/kvstore/__tests/LocalKVStore.test.ts +614 -0
  58. package/src/kvstore/index.ts +1 -0
  59. package/src/wallet/WERR_REVIEW_ACTIONS.ts +30 -0
  60. package/src/wallet/Wallet.interfaces.ts +24 -0
  61. package/src/wallet/WalletError.ts +4 -2
  62. package/src/wallet/index.ts +2 -0
  63. package/src/wallet/substrates/HTTPWalletJSON.ts +12 -6
  64. package/docs/wallet-substrates.md +0 -1194
@@ -0,0 +1,614 @@
1
+ /** eslint-env jest */
2
+ import LocalKVStore from '../LocalKVStore.js'
3
+ import LockingScript from '../../script/LockingScript.js'
4
+ import PushDrop from '../../script/templates/PushDrop.js'
5
+ import * as Utils from '../../primitives/utils.js'
6
+ import {
7
+ WalletInterface,
8
+ ListOutputsResult,
9
+ WalletDecryptResult,
10
+ WalletEncryptResult,
11
+ CreateActionResult,
12
+ SignActionResult
13
+ } from '../../wallet/Wallet.interfaces.js'
14
+ import Transaction from '../../transaction/Transaction.js'
15
+ import { Beef } from '../../transaction/Beef.js'
16
+ import { mock } from 'node:test'
17
+
18
+ // --- Constants for Mock Values ---
19
+ const testLockingScriptHex = 'mockLockingScriptHex'
20
+ const testUnlockingScriptHex = 'mockUnlockingScriptHex'
21
+ const testEncryptedValue = Buffer.from('encryptedData') // Use Buffer for ciphertext
22
+ const testRawValue = 'myTestDataValue'
23
+ const testRawValueBuffer = Buffer.from(testRawValue) // Buffer for raw value
24
+
25
+ jest.mock('../../script/LockingScript.js', () => {
26
+ const mockLockingScriptInstance = {
27
+ toHex: jest.fn(() => testLockingScriptHex) // Default value
28
+ }
29
+ return {
30
+ fromHex: jest.fn(() => mockLockingScriptInstance) // Static method returns mock instance
31
+ }
32
+ })
33
+
34
+ jest.mock('../../script/templates/PushDrop.js', () => {
35
+ const mockLockingScriptInstance = {
36
+ toHex: jest.fn(() => testLockingScriptHex) // Default value
37
+ }
38
+ const mockUnlockerInstance = {
39
+ // Default sign behavior returns an object with a toHex mock
40
+ sign: jest.fn().mockResolvedValue({ toHex: jest.fn(() => testUnlockingScriptHex) })
41
+ }
42
+ // --- Define the mock instance returned by the PushDrop constructor ---
43
+ const mockPushDropInstance = {
44
+ // Default lock behavior returns the mock script
45
+ lock: jest.fn().mockResolvedValue(mockLockingScriptInstance),
46
+ // Default unlock behavior returns the mock unlocker
47
+ unlock: jest.fn().mockReturnValue(mockUnlockerInstance)
48
+ // Add a mock for the static decode method directly here if needed,
49
+ // or manage it separately as done below.
50
+ }
51
+
52
+ // --- Define the mock for the static decode method ---
53
+ // It needs to be separate because it's static, not on the instance.
54
+ const mockPushDropDecode = jest.fn()
55
+ return Object.assign(
56
+ jest.fn(() => mockPushDropInstance), // Constructor mock
57
+ { decode: mockPushDropDecode } // Static method mock
58
+ )
59
+ })
60
+ jest.mock('../../transaction/Transaction.js', () => ({
61
+ // Static method returns a minimal mock object
62
+ fromAtomicBEEF: jest.fn(() => ({ /* mock tx object if needed */ }))
63
+ }))
64
+
65
+ jest.mock('../../primitives/utils.js', () => ({
66
+ // Ensure toArray returns Array<number> or Uint8Array
67
+ toArray: jest.fn((str: string, encoding = 'utf8') => Array.from(Buffer.from(str, encoding as BufferEncoding))),
68
+ toUTF8: jest.fn((arr: number[] | Uint8Array) => Buffer.from(arr).toString('utf8'))
69
+ }))
70
+
71
+ jest.mock('../../wallet/WalletClient.js', () => jest.fn())
72
+
73
+ // --- Typed Mocks for SDK Components ---
74
+ const MockedLockingScript = LockingScript as jest.Mocked<typeof LockingScript>
75
+ // Use MockedClass for the constructor and add static methods separately
76
+ const MockedPushDrop = PushDrop as jest.MockedClass<typeof PushDrop> & {
77
+ decode: jest.Mock<any, any>
78
+ }
79
+ // Access the static mock assigned during jest.mock
80
+ const MockedPushDropDecode = MockedPushDrop.decode
81
+ const MockedUtils = Utils as jest.Mocked<typeof Utils>
82
+ const MockedTransaction = Transaction as jest.Mocked<typeof Transaction>
83
+
84
+ // --- Mock Wallet Setup ---
85
+ const createMockWallet = (): jest.Mocked<WalletInterface> => ({
86
+ listOutputs: jest.fn(),
87
+ encrypt: jest.fn(),
88
+ decrypt: jest.fn(),
89
+ createAction: jest.fn(),
90
+ signAction: jest.fn(),
91
+ relinquishOutput: jest.fn()
92
+ } as unknown as jest.Mocked<WalletInterface>)
93
+
94
+ describe('localKVStore', () => {
95
+ let mockWallet: jest.Mocked<WalletInterface>
96
+ let kvStore: LocalKVStore
97
+ const testContext = 'test-kv-context'
98
+ const testKey = 'myTestKey'
99
+ const testValue = 'myTestDataValue' // Raw value string used in tests
100
+ // Use the constants defined above for mock results
101
+ // const testEncryptedValue = Buffer.from('encryptedData'); // Defined above
102
+ const testOutpoint = 'txid123.0'
103
+ // const testLockingScriptHex = 'mockLockingScriptHex'; // Defined above
104
+ // const testUnlockingScriptHex = 'mockUnlockingScriptHex'; // Defined above
105
+
106
+ beforeEach(() => {
107
+ // Reset mocks before each test (clears calls and resets implementations)
108
+ jest.clearAllMocks()
109
+
110
+ // Create a fresh mock wallet for each test
111
+ mockWallet = createMockWallet()
112
+
113
+ // Create a kvStore instance with the mock wallet
114
+ // Default encrypt=true unless specified otherwise in a test block
115
+ kvStore = new LocalKVStore(mockWallet, testContext, true)
116
+
117
+ // Reset specific mock implementations if needed after clearAllMocks
118
+ // (e.g., if a test overrides a default implementation)
119
+ MockedPushDropDecode.mockClear() // Clear calls/results for static decode
120
+ })
121
+
122
+ // --- Constructor Tests ---
123
+ describe('constructor', () => {
124
+ it('should create an instance with default wallet and encrypt=true', () => {
125
+ // We need to mock the default WalletClient if the SUT uses it
126
+ const MockedWalletClient = require('../../../mod.js').WalletClient
127
+ const store = new LocalKVStore(undefined, 'default-context')
128
+ expect(store).toBeInstanceOf(LocalKVStore)
129
+ expect(MockedWalletClient).toHaveBeenCalledTimes(1) // Verify default was created
130
+ expect((store as any).context).toEqual('default-context')
131
+ expect((store as any).encrypt).toBe(true)
132
+ })
133
+
134
+ it('should create an instance with provided wallet, context, and encrypt=false', () => {
135
+ const customWallet = createMockWallet()
136
+ const store = new LocalKVStore(customWallet, 'custom-context', false)
137
+ expect(store).toBeInstanceOf(LocalKVStore)
138
+ expect((store as any).wallet).toBe(customWallet)
139
+ expect((store as any).context).toEqual('custom-context')
140
+ expect((store as any).encrypt).toBe(false)
141
+ })
142
+
143
+ it('should throw an error if context is missing or empty', () => {
144
+ expect(() => new LocalKVStore(mockWallet, '')).toThrow('A context in which to operate is required.')
145
+ expect(() => new LocalKVStore(mockWallet, null as any)).toThrow('A context in which to operate is required.')
146
+ })
147
+ })
148
+
149
+ // --- Get Method Tests ---
150
+ describe('get', () => {
151
+ it('should return defaultValue if no output is found', async () => {
152
+ const defaultValue = 'default'
153
+
154
+ const mockedLor: ListOutputsResult = {
155
+ totalOutputs: 0,
156
+ outputs: [],
157
+ BEEF: undefined
158
+ }
159
+
160
+ const lookupValueReal = kvStore['lookupValue']
161
+ kvStore['lookupValue'] = jest.fn().mockResolvedValue({
162
+ value: defaultValue,
163
+ outpoint: undefined,
164
+ lor: mockedLor
165
+ })
166
+
167
+
168
+ const result = await kvStore.get(testKey, defaultValue)
169
+ kvStore['lookupValue'] = lookupValueReal
170
+
171
+ expect(result).toBe(defaultValue)
172
+ })
173
+
174
+ it('should return undefined if no output is found and no defaultValue provided', async () => {
175
+ const defaultValue = undefined
176
+
177
+ const mockedLor: ListOutputsResult = {
178
+ totalOutputs: 0,
179
+ outputs: [],
180
+ BEEF: undefined
181
+ }
182
+
183
+ const lookupValueReal = kvStore['lookupValue']
184
+ kvStore['lookupValue'] = jest.fn().mockResolvedValue({
185
+ value: defaultValue,
186
+ outpoint: undefined,
187
+ lor: mockedLor
188
+ })
189
+
190
+
191
+ const result = await kvStore.get(testKey, defaultValue)
192
+ kvStore['lookupValue'] = lookupValueReal
193
+
194
+ expect(result).toBe(defaultValue)
195
+ })
196
+ })
197
+
198
+ // --- Set Method Tests ---
199
+ describe('set', () => {
200
+ let pushDropInstance: PushDrop // To access the instance methods
201
+
202
+ beforeEach(() => {
203
+ // Get the mock instance that will be created by `new PushDrop()`
204
+ pushDropInstance = new (PushDrop as any)()
205
+ })
206
+
207
+ it('should create a new encrypted output if none exists', async () => {
208
+ const valueArray = Array.from(testRawValueBuffer)
209
+ const encryptedArray = Array.from(testEncryptedValue)
210
+ MockedUtils.toArray.mockReturnValue(valueArray) // Mock toArray -> Array<number>
211
+ mockWallet.encrypt.mockResolvedValue({ ciphertext: encryptedArray } as WalletEncryptResult) // Encrypt returns Array<number>
212
+ mockWallet.listOutputs.mockResolvedValue({ outputs: [], totalOutputs: 0, BEEF: undefined })
213
+ mockWallet.createAction.mockResolvedValue({ txid: 'newTxId' } as CreateActionResult)
214
+
215
+ // Get the mock instance returned by the constructor
216
+ const mockPDInstance = new MockedPushDrop(mockWallet)
217
+
218
+ const result = await kvStore.set(testKey, testValue)
219
+
220
+ expect(result).toBe('newTxId.0')
221
+ expect(MockedUtils.toArray).toHaveBeenCalledWith(testValue, 'utf8')
222
+ expect(mockWallet.encrypt).toHaveBeenCalledWith({
223
+ plaintext: valueArray, // Should be Array<number>
224
+ protocolID: [2, testContext],
225
+ keyID: testKey
226
+ })
227
+ // Check the mock instance's lock method
228
+ expect(mockPDInstance.lock).toHaveBeenCalledWith(
229
+ // The lock function expects Array<number[] | Uint8Array>
230
+ // Ensure the encrypted value is passed correctly (as Uint8Array or Array<number>)
231
+ [(encryptedArray)], // Pass buffer derived from encrypted array
232
+ [2, testContext],
233
+ testKey,
234
+ 'self'
235
+ )
236
+ //expect(mockWallet.listOutputs).toHaveBeenCalledWith({ basket: testContext, tags: [testKey], include: 'entire transactions' })
237
+ // Verify createAction for NEW output
238
+ expect(mockWallet.createAction).toHaveBeenCalledWith({
239
+ description: `Update ${testKey} in ${testContext}`,
240
+ inputBEEF: undefined,
241
+ inputs: [],
242
+ outputs: [{
243
+ basket: 'test-kv-context',
244
+ tags: ['myTestKey'],
245
+ lockingScript: testLockingScriptHex, // From the mock lock result
246
+ satoshis: 1,
247
+ outputDescription: 'Key-value token'
248
+ }],
249
+ options: {
250
+ acceptDelayedBroadcast: false,
251
+ randomizeOutputs: false
252
+ }
253
+ })
254
+ expect(mockWallet.signAction).not.toHaveBeenCalled()
255
+ expect(mockWallet.relinquishOutput).not.toHaveBeenCalled()
256
+ })
257
+
258
+ it('should create a new non-encrypted output if none exists and encrypt=false', async () => {
259
+ kvStore = new LocalKVStore(mockWallet, testContext, false) // encrypt=false
260
+ const valueArray = Array.from(testRawValueBuffer)
261
+ MockedUtils.toArray.mockReturnValue(valueArray)
262
+ mockWallet.listOutputs.mockResolvedValue({ outputs: [], totalOutputs: 0, BEEF: undefined })
263
+ mockWallet.createAction.mockResolvedValue({ txid: 'newTxIdNonEnc' } as CreateActionResult)
264
+
265
+ // Get the mock instance returned by the constructor
266
+ const mockPDInstance = new MockedPushDrop(mockWallet)
267
+
268
+ const result = await kvStore.set(testKey, testValue)
269
+
270
+ expect(result).toBe('newTxIdNonEnc.0')
271
+ expect(MockedUtils.toArray).toHaveBeenCalledWith(testValue, 'utf8')
272
+ expect(mockWallet.encrypt).not.toHaveBeenCalled()
273
+ // Check the mock instance's lock method
274
+ expect(mockPDInstance.lock).toHaveBeenCalledWith(
275
+ [(valueArray)], // Pass raw value buffer
276
+ [2, testContext],
277
+ testKey,
278
+ 'self'
279
+ )
280
+ //expect(mockWallet.listOutputs).toHaveBeenCalledWith({ basket: testContext, tags: [testKey], include: 'entire transactions' })
281
+ expect(mockWallet.createAction).toHaveBeenCalledWith({
282
+ description: `Update ${testKey} in ${testContext}`,
283
+ inputBEEF: undefined,
284
+ inputs: [],
285
+ outputs: [{
286
+ basket: "test-kv-context",
287
+ tags: ['myTestKey'],
288
+ lockingScript: testLockingScriptHex, // From mock lock
289
+ satoshis: 1,
290
+ outputDescription: 'Key-value token'
291
+ }],
292
+ options: {
293
+ acceptDelayedBroadcast: false,
294
+ randomizeOutputs: false
295
+ }
296
+ })
297
+ expect(mockWallet.signAction).not.toHaveBeenCalled()
298
+ expect(mockWallet.relinquishOutput).not.toHaveBeenCalled()
299
+ })
300
+
301
+ it('should update an existing output (spend and create)', async () => {
302
+ const existingOutpoint = 'oldTxId.0'
303
+ const existingOutput = { outpoint: existingOutpoint, txid: 'oldTxId', vout: 0, lockingScript: 'oldScriptHex' } // Added script
304
+ const mockBEEF = [1,2,3,4,5,6]
305
+ const signableRef = 'signableTxRef123'
306
+ const signableTx = []
307
+ const updatedTxId = 'updatedTxId'
308
+
309
+ const valueArray = Array.from(testRawValueBuffer)
310
+ const encryptedArray = Array.from(testEncryptedValue)
311
+
312
+ MockedUtils.toArray.mockReturnValue(valueArray)
313
+ mockWallet.encrypt.mockResolvedValue({ ciphertext: encryptedArray } as WalletEncryptResult)
314
+ mockWallet.listOutputs.mockResolvedValue({ outputs: [existingOutput], totalOutputs: 1, BEEF: mockBEEF } as any)
315
+
316
+ // Mock createAction to return a signable transaction structure
317
+ mockWallet.createAction.mockResolvedValue({
318
+ signableTransaction: { reference: signableRef, tx: signableTx }
319
+ } as CreateActionResult)
320
+
321
+ // Mock Transaction.fromAtomicBEEF to return a mock TX object
322
+ const mockTxObject = { /* Can add mock properties/methods if SUT uses them */ }
323
+ MockedTransaction.fromAtomicBEEF.mockReturnValue(mockTxObject as any)
324
+
325
+ mockWallet.signAction.mockResolvedValue({ txid: updatedTxId } as SignActionResult)
326
+
327
+ // Get the mock instance returned by the constructor
328
+ const mockPDInstance = new MockedPushDrop(mockWallet)
329
+
330
+ const mockedLor: ListOutputsResult = {
331
+ totalOutputs: 1,
332
+ outputs: [{
333
+ satoshis: 0,
334
+ spendable: true,
335
+ outpoint: existingOutpoint
336
+ }],
337
+ BEEF: mockBEEF
338
+ }
339
+
340
+ const lookupValueReal = kvStore['lookupValue']
341
+ kvStore['lookupValue'] = jest.fn().mockResolvedValue({
342
+ value: 'oldValue',
343
+ outpoint: existingOutpoint,
344
+ lor: mockedLor
345
+ })
346
+
347
+ /**
348
+ * set now starts by getting existing outputs, which are then checked for current value.
349
+ * The current value must be decodable.
350
+ */
351
+ const result = await kvStore.set(testKey, testValue)
352
+
353
+ kvStore['lookupValue'] = lookupValueReal
354
+
355
+ expect(result).toBe(`${updatedTxId}.0`) // Assuming output 0 is the new KV token
356
+ expect(mockWallet.encrypt).toHaveBeenCalled()
357
+ expect(mockPDInstance.lock).toHaveBeenCalledWith([(encryptedArray)], [2, testContext], testKey, 'self')
358
+
359
+ // Verify createAction for UPDATE
360
+ expect(mockWallet.createAction).toHaveBeenCalledWith(expect.objectContaining({ // Use objectContaining for flexibility
361
+ description: `Update ${testKey} in ${testContext}`,
362
+ inputBEEF: mockBEEF,
363
+ inputs: expect.arrayContaining([ // Check inputs array
364
+ expect.objectContaining({ outpoint: existingOutpoint }) // Check specific input
365
+ ]),
366
+ outputs: expect.arrayContaining([ // Check outputs array
367
+ expect.objectContaining({ lockingScript: testLockingScriptHex }) // Check the new output script
368
+ ])
369
+ }))
370
+
371
+ // Verify signing steps
372
+ expect(MockedTransaction.fromAtomicBEEF).toHaveBeenCalledWith(signableTx)
373
+ // Check unlock was called on the instance
374
+ expect(mockPDInstance.unlock).toHaveBeenCalledWith([2, testContext], testKey, 'self')
375
+
376
+ // Get the unlocker returned by the mock unlock method
377
+ const mockUnlocker = (mockPDInstance.unlock as jest.Mock).mock.results[0].value
378
+ expect(mockUnlocker.sign).toHaveBeenCalledWith(mockTxObject, 0) // Check sign args
379
+
380
+ // Verify signAction call
381
+ expect(mockWallet.signAction).toHaveBeenCalledWith({
382
+ reference: signableRef,
383
+ spends: {
384
+ 0: { unlockingScript: testUnlockingScriptHex } // Check unlocking script from mock sign result
385
+ }
386
+ })
387
+ expect(mockWallet.relinquishOutput).not.toHaveBeenCalled()
388
+ })
389
+
390
+ it('should collapse multiple existing outputs into one', async () => {
391
+ /**
392
+ * The mocked state doesn't include a valid BEEF from which the locking script of the current value.
393
+ */
394
+ const existingOutpoint1 = 'oldTxId1.0'
395
+ const existingOutpoint2 = 'oldTxId2.1'
396
+ const existingOutput1 = { outpoint: existingOutpoint1, txid: 'oldTxId1', vout: 0, lockingScript: 's1' }
397
+ const existingOutput2 = { outpoint: existingOutpoint2, txid: 'oldTxId2', vout: 1, lockingScript: 's2' }
398
+ const mockBEEF = [1,2,3,4,5,6]
399
+ const signableRef = 'signableTxRefMulti'
400
+ const signableTx = []
401
+ const updatedTxId = 'updatedTxIdMulti'
402
+ const mockTxObject = {} // Dummy TX object
403
+
404
+ const valueArray = Array.from(testRawValueBuffer)
405
+ const encryptedArray = Array.from(testEncryptedValue)
406
+
407
+ MockedUtils.toArray.mockReturnValue(valueArray)
408
+ mockWallet.encrypt.mockResolvedValue({ ciphertext: encryptedArray } as WalletEncryptResult)
409
+ mockWallet.listOutputs.mockResolvedValue({ outputs: [existingOutput1, existingOutput2], totalOutputs: 2, BEEF: mockBEEF } as any)
410
+ mockWallet.createAction.mockResolvedValue({
411
+ signableTransaction: { reference: signableRef, tx: signableTx }
412
+ } as CreateActionResult)
413
+ MockedTransaction.fromAtomicBEEF.mockReturnValue(mockTxObject as any)
414
+ mockWallet.signAction.mockResolvedValue({ txid: updatedTxId } as SignActionResult)
415
+
416
+ // Get the mock instance
417
+ const mockPDInstance = new MockedPushDrop(mockWallet)
418
+
419
+ const mockedLor: ListOutputsResult = {
420
+ totalOutputs: 1,
421
+ outputs: [
422
+ {
423
+ satoshis: 0,
424
+ spendable: true,
425
+ outpoint: existingOutpoint1
426
+ },
427
+ {
428
+ satoshis: 0,
429
+ spendable: true,
430
+ outpoint: existingOutpoint2
431
+ }
432
+ ],
433
+ BEEF: mockBEEF
434
+ }
435
+
436
+ const lookupValueReal = kvStore['lookupValue']
437
+ kvStore['lookupValue'] = jest.fn().mockResolvedValue({
438
+ value: 'oldValue',
439
+ outpoint: existingOutpoint2,
440
+ lor: mockedLor
441
+ })
442
+
443
+ const result = await kvStore.set(testKey, testValue)
444
+ kvStore['lookupValue'] = lookupValueReal
445
+
446
+ expect(result).toBe(`${updatedTxId}.0`)
447
+ expect(mockWallet.encrypt).toHaveBeenCalled()
448
+ expect(mockPDInstance.lock).toHaveBeenCalled()
449
+
450
+ // Verify createAction with multiple inputs
451
+ expect(mockWallet.createAction).toHaveBeenCalledWith(expect.objectContaining({
452
+ inputBEEF: mockBEEF,
453
+ inputs: expect.arrayContaining([
454
+ expect.objectContaining({ outpoint: existingOutpoint1 }),
455
+ expect.objectContaining({ outpoint: existingOutpoint2 })
456
+ ]),
457
+ outputs: expect.arrayContaining([
458
+ expect.objectContaining({ lockingScript: testLockingScriptHex })
459
+ ])
460
+ }))
461
+
462
+ // Verify signing loop
463
+ expect(MockedTransaction.fromAtomicBEEF).toHaveBeenCalledWith(signableTx)
464
+ expect(mockPDInstance.unlock).toHaveBeenCalledTimes(2) // Called for each input
465
+ expect(mockPDInstance.unlock).toHaveBeenNthCalledWith(1, [2, testContext], testKey, 'self')
466
+ expect(mockPDInstance.unlock).toHaveBeenNthCalledWith(2, [2, testContext], testKey, 'self')
467
+
468
+ // Get the *same* mock unlocker instance (since unlock is mocked to always return it)
469
+ const mockUnlocker = (mockPDInstance.unlock as jest.Mock).mock.results[0].value
470
+ expect(mockUnlocker.sign).toHaveBeenCalledTimes(2)
471
+ expect(mockUnlocker.sign).toHaveBeenNthCalledWith(1, mockTxObject, 0) // Input index 0
472
+ expect(mockUnlocker.sign).toHaveBeenNthCalledWith(2, mockTxObject, 1) // Input index 1
473
+
474
+ // Verify signAction call with multiple spends
475
+ expect(mockWallet.signAction).toHaveBeenCalledWith({
476
+ reference: signableRef,
477
+ spends: {
478
+ 0: { unlockingScript: testUnlockingScriptHex }, // Same mock script for both
479
+ 1: { unlockingScript: testUnlockingScriptHex }
480
+ }
481
+ })
482
+ expect(mockWallet.relinquishOutput).not.toHaveBeenCalled()
483
+ })
484
+ })
485
+
486
+ // --- Remove Method Tests ---
487
+ describe('remove', () => {
488
+ let pushDropInstance: PushDrop // To access the instance methods
489
+
490
+ beforeEach(() => {
491
+ // Get the mock instance that will be created by `new PushDrop()`
492
+ pushDropInstance = new (PushDrop as any)()
493
+ })
494
+
495
+ it('should do nothing and return void if key does not exist', async () => {
496
+ mockWallet.listOutputs.mockResolvedValue({ outputs: [], totalOutputs: 0, BEEF: undefined })
497
+
498
+ const result = await kvStore.remove(testKey)
499
+
500
+ expect(result).toEqual([])
501
+ /*
502
+ expect(mockWallet.listOutputs).toHaveBeenCalledWith({
503
+ basket: testContext,
504
+ tags: [testKey],
505
+ tagsQueryMode: 'all',
506
+ include: 'entire transactions', // remove checks for entire transactions
507
+ limit: undefined,
508
+ })
509
+ */
510
+ expect(mockWallet.createAction).not.toHaveBeenCalled()
511
+ expect(mockWallet.signAction).not.toHaveBeenCalled()
512
+ expect(mockWallet.relinquishOutput).not.toHaveBeenCalled()
513
+ })
514
+
515
+ it('should remove an existing key by spending its output(s)', async () => {
516
+ const existingOutpoint1 = 'removeTxId1.0'
517
+ const existingOutpoint2 = 'removeTxId2.1'
518
+ const existingOutput1 = { outpoint: existingOutpoint1, txid: 'removeTxId1', vout: 0, lockingScript: 's1' }
519
+ const existingOutput2 = { outpoint: existingOutpoint2, txid: 'removeTxId2', vout: 1, lockingScript: 's2' }
520
+ const mockBEEF = Buffer.from('mockBEEFRemove')
521
+ const signableRef = 'signableTxRefRemove'
522
+ const signableTx = []
523
+ const removalTxId = 'removalTxId'
524
+ const mockTxObject = {}
525
+
526
+ mockWallet.listOutputs.mockResolvedValue({ outputs: [existingOutput1, existingOutput2], totalOutputs: 2, BEEF: mockBEEF } as any)
527
+ mockWallet.createAction.mockResolvedValue({
528
+ signableTransaction: { reference: signableRef, tx: signableTx }
529
+ } as CreateActionResult) // Note: removal tx has NO outputs field in result
530
+ MockedTransaction.fromAtomicBEEF.mockReturnValue(mockTxObject as any)
531
+ mockWallet.signAction.mockResolvedValue({ txid: removalTxId } as SignActionResult)
532
+
533
+ // Get the mock instance
534
+ const mockPDInstance = new MockedPushDrop(mockWallet)
535
+
536
+ const result = await kvStore.remove(testKey)
537
+
538
+ expect(result).toEqual([removalTxId])
539
+ //expect(mockWallet.listOutputs).toHaveBeenCalledWith({ basket: testContext, tags: [testKey], include: 'entire transactions', limit: undefined, tagsQueryMode: 'all' })
540
+
541
+ // Verify createAction for REMOVE (no outputs in the action)
542
+ expect(mockWallet.createAction).toHaveBeenCalledWith({
543
+ // The description might still say "Update" depending on implementation reuse
544
+ // description: `Remove ${testKey} from ${testContext}`, // Ideal description
545
+ description: expect.stringContaining(testKey), // More general check
546
+ inputBEEF: mockBEEF,
547
+ inputs: expect.arrayContaining([
548
+ expect.objectContaining({ outpoint: existingOutpoint1 }),
549
+ expect.objectContaining({ outpoint: existingOutpoint2 })
550
+ ]),
551
+ // IMPORTANT: No 'outputs' key should be present for removal action
552
+ outputs: undefined, // Or check that the key is not present
553
+ options: {
554
+ acceptDelayedBroadcast: false
555
+ }
556
+ })
557
+ // Check that outputs key is absent
558
+ expect(mockWallet.createAction.mock.calls[0][0]).not.toHaveProperty('outputs')
559
+
560
+ // Verify signing
561
+ expect(MockedTransaction.fromAtomicBEEF).toHaveBeenCalledWith(signableTx)
562
+ expect(mockPDInstance.unlock).toHaveBeenCalledTimes(2)
563
+ expect(mockPDInstance.unlock).toHaveBeenNthCalledWith(1, [2, testContext], testKey, 'self')
564
+ expect(mockPDInstance.unlock).toHaveBeenNthCalledWith(2, [2, testContext], testKey, 'self')
565
+ const mockUnlocker = (mockPDInstance.unlock as jest.Mock).mock.results[0].value
566
+ expect(mockUnlocker.sign).toHaveBeenCalledTimes(2)
567
+ expect(mockUnlocker.sign).toHaveBeenNthCalledWith(1, mockTxObject, 0)
568
+ expect(mockUnlocker.sign).toHaveBeenNthCalledWith(2, mockTxObject, 1)
569
+
570
+ // Verify signAction call
571
+ expect(mockWallet.signAction).toHaveBeenCalledWith({
572
+ reference: signableRef,
573
+ spends: {
574
+ 0: { unlockingScript: testUnlockingScriptHex },
575
+ 1: { unlockingScript: testUnlockingScriptHex }
576
+ }
577
+ })
578
+ expect(mockWallet.relinquishOutput).not.toHaveBeenCalled()
579
+ })
580
+
581
+ it('should relinquish outputs if signing fails during removal', async () => {
582
+ const existingOutpoint1 = 'failRemoveTxId1.0'
583
+ const existingOutput1 = { outpoint: existingOutpoint1, txid: 'failRemoveTxId1', vout: 0, lockingScript: 's1' }
584
+ const mockBEEF = Buffer.from('mockBEEFFailRemove')
585
+ const signableRef = 'signableTxRefFailRemove'
586
+ const signableTx = []
587
+ const mockTxObject = {}
588
+
589
+ mockWallet.listOutputs.mockResolvedValue({ outputs: [existingOutput1], totalOutputs: 1, BEEF: mockBEEF } as any)
590
+ mockWallet.createAction.mockResolvedValue({
591
+ signableTransaction: { reference: signableRef, tx: signableTx }
592
+ } as CreateActionResult)
593
+ MockedTransaction.fromAtomicBEEF.mockReturnValue(mockTxObject as any)
594
+ mockWallet.signAction.mockRejectedValue(new Error('Signature failed remove')) // Make signAction fail
595
+ mockWallet.relinquishOutput.mockResolvedValue({ relinquished: true })
596
+
597
+ // Get the mock instance
598
+ const mockPDInstance = new MockedPushDrop(mockWallet)
599
+
600
+ // Expect the error to be caught, method completes returning undefined/void
601
+ await expect(kvStore.remove(testKey)).rejects.toThrow('There are')
602
+
603
+ // Verify setup calls
604
+ expect(mockWallet.listOutputs).toHaveBeenCalled()
605
+ expect(mockWallet.createAction).toHaveBeenCalled() // createAction called for removal attempt
606
+ expect(MockedTransaction.fromAtomicBEEF).toHaveBeenCalled()
607
+ //expect(mockPDInstance.unlock).toHaveBeenCalledTimes(1) // unlock was called
608
+ const mockUnlocker = (mockPDInstance.unlock as jest.Mock).mock.results[0].value
609
+ expect(mockUnlocker.sign).toHaveBeenCalledTimes(1) // sign was called
610
+ expect(mockWallet.signAction).toHaveBeenCalled() // Called but failed
611
+
612
+ })
613
+ })
614
+ })
@@ -0,0 +1 @@
1
+ export { default as LocalKVStore } from './LocalKVStore.js'
@@ -0,0 +1,30 @@
1
+ import { AtomicBEEF, OutpointString, ReviewActionResult, SendWithResult, TXIDHexString } from './Wallet.interfaces.js'
2
+
3
+ /**
4
+ * When a `createAction` or `signAction` is completed in undelayed mode (`acceptDelayedBroadcast`: false),
5
+ * any unsucccessful result will return the results by way of this exception to ensure attention is
6
+ * paid to processing errors.
7
+ */
8
+ export class WERR_REVIEW_ACTIONS extends Error {
9
+ code: number
10
+ isError: boolean = true
11
+
12
+ /**
13
+ * All parameters correspond to their comparable `createAction` or `signSction` results
14
+ * with the exception of `reviewActionResults`;
15
+ * which contains more details, particularly for double spend results.
16
+ */
17
+ constructor (
18
+ public reviewActionResults: ReviewActionResult[],
19
+ public sendWithResults: SendWithResult[],
20
+ public txid?: TXIDHexString,
21
+ public tx?: AtomicBEEF,
22
+ public noSendChange?: OutpointString[]
23
+ ) {
24
+ super('Undelayed createAction or signAction results require review.')
25
+ this.code = 5
26
+ this.name = this.constructor.name
27
+ }
28
+ }
29
+
30
+ export default WERR_REVIEW_ACTIONS
@@ -303,6 +303,30 @@ export interface SendWithResult {
303
303
  status: SendWithResultStatus
304
304
  }
305
305
 
306
+ /**
307
+ * Indicates status of a new Action following a `createAction` or `signAction` in immediate mode:
308
+ * When `acceptDelayedBroadcast` is falses.
309
+ *
310
+ * 'success': The action has been broadcast and accepted by the bitcoin processing network.
311
+ * 'doulbeSpend': The action has been confirmed to double spend one or more inputs, and by the "first-seen-rule" is the loosing transaction.
312
+ * 'invalidTx': The action was rejected by the processing network as an invalid bitcoin transaction.
313
+ * 'serviceError': The broadcast services are currently unable to reach the bitcoin network. The action is now queued for delayed retries.
314
+ */
315
+ export type ReviewActionResultStatus = 'success' | 'doubleSpend' | 'serviceError' | 'invalidTx'
316
+
317
+ export interface ReviewActionResult {
318
+ txid: TXIDHexString
319
+ status: ReviewActionResultStatus
320
+ /**
321
+ * Any competing txids reported for this txid, valid when status is 'doubleSpend'.
322
+ */
323
+ competingTxs?: string[]
324
+ /**
325
+ * Merged beef of competingTxs, valid when status is 'doubleSpend'.
326
+ */
327
+ competingBeef?: number[]
328
+ }
329
+
306
330
  export interface SignableTransaction {
307
331
  tx: AtomicBEEF
308
332
  reference: Base64String