@bsv/sdk 1.4.15 → 1.4.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/mod.js +1 -0
- package/dist/cjs/mod.js.map +1 -1
- package/dist/cjs/package.json +9 -9
- package/dist/cjs/src/kvstore/LocalKVStore.js +268 -0
- package/dist/cjs/src/kvstore/LocalKVStore.js.map +1 -0
- package/dist/cjs/src/kvstore/index.js +9 -0
- package/dist/cjs/src/kvstore/index.js.map +1 -0
- package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
- package/dist/esm/mod.js +1 -0
- package/dist/esm/mod.js.map +1 -1
- package/dist/esm/src/kvstore/LocalKVStore.js +263 -0
- package/dist/esm/src/kvstore/LocalKVStore.js.map +1 -0
- package/dist/esm/src/kvstore/index.js +2 -0
- package/dist/esm/src/kvstore/index.js.map +1 -0
- package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
- package/dist/types/mod.d.ts +1 -0
- package/dist/types/mod.d.ts.map +1 -1
- package/dist/types/src/kvstore/LocalKVStore.d.ts +79 -0
- package/dist/types/src/kvstore/LocalKVStore.d.ts.map +1 -0
- package/dist/types/src/kvstore/index.d.ts +2 -0
- package/dist/types/src/kvstore/index.d.ts.map +1 -0
- package/dist/types/tsconfig.types.tsbuildinfo +1 -1
- package/dist/umd/bundle.js +1 -1
- package/docs/identity.md +225 -0
- package/docs/kvstore.md +132 -0
- package/docs/registry.md +383 -0
- package/docs/transaction.md +3 -3
- package/mod.ts +2 -1
- package/package.json +19 -9
- package/src/kvstore/LocalKVStore.ts +282 -0
- package/src/kvstore/__tests/LocalKVStore.test.ts +703 -0
- package/src/kvstore/index.ts +1 -0
- package/docs/wallet-substrates.md +0 -1194
|
@@ -0,0 +1,703 @@
|
|
|
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
|
+
|
|
16
|
+
// --- Constants for Mock Values ---
|
|
17
|
+
const testLockingScriptHex = 'mockLockingScriptHex'
|
|
18
|
+
const testUnlockingScriptHex = 'mockUnlockingScriptHex'
|
|
19
|
+
const testEncryptedValue = Buffer.from('encryptedData') // Use Buffer for ciphertext
|
|
20
|
+
const testRawValue = 'myTestDataValue'
|
|
21
|
+
const testRawValueBuffer = Buffer.from(testRawValue) // Buffer for raw value
|
|
22
|
+
|
|
23
|
+
jest.mock('../../script/LockingScript.js', () => {
|
|
24
|
+
const mockLockingScriptInstance = {
|
|
25
|
+
toHex: jest.fn(() => testLockingScriptHex) // Default value
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
fromHex: jest.fn(() => mockLockingScriptInstance) // Static method returns mock instance
|
|
29
|
+
}
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
jest.mock('../../script/templates/PushDrop.js', () => {
|
|
33
|
+
const mockLockingScriptInstance = {
|
|
34
|
+
toHex: jest.fn(() => testLockingScriptHex) // Default value
|
|
35
|
+
}
|
|
36
|
+
const mockUnlockerInstance = {
|
|
37
|
+
// Default sign behavior returns an object with a toHex mock
|
|
38
|
+
sign: jest.fn().mockResolvedValue({ toHex: jest.fn(() => testUnlockingScriptHex) })
|
|
39
|
+
}
|
|
40
|
+
// --- Define the mock instance returned by the PushDrop constructor ---
|
|
41
|
+
const mockPushDropInstance = {
|
|
42
|
+
// Default lock behavior returns the mock script
|
|
43
|
+
lock: jest.fn().mockResolvedValue(mockLockingScriptInstance),
|
|
44
|
+
// Default unlock behavior returns the mock unlocker
|
|
45
|
+
unlock: jest.fn().mockReturnValue(mockUnlockerInstance)
|
|
46
|
+
// Add a mock for the static decode method directly here if needed,
|
|
47
|
+
// or manage it separately as done below.
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// --- Define the mock for the static decode method ---
|
|
51
|
+
// It needs to be separate because it's static, not on the instance.
|
|
52
|
+
const mockPushDropDecode = jest.fn()
|
|
53
|
+
return Object.assign(
|
|
54
|
+
jest.fn(() => mockPushDropInstance), // Constructor mock
|
|
55
|
+
{ decode: mockPushDropDecode } // Static method mock
|
|
56
|
+
)
|
|
57
|
+
})
|
|
58
|
+
jest.mock('../../transaction/Transaction.js', () => ({
|
|
59
|
+
// Static method returns a minimal mock object
|
|
60
|
+
fromAtomicBEEF: jest.fn(() => ({ /* mock tx object if needed */ }))
|
|
61
|
+
}))
|
|
62
|
+
|
|
63
|
+
jest.mock('../../primitives/utils.js', () => ({
|
|
64
|
+
// Ensure toArray returns Array<number> or Uint8Array
|
|
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 Mocks for SDK Components ---
|
|
72
|
+
const MockedLockingScript = LockingScript as jest.Mocked<typeof LockingScript>
|
|
73
|
+
// Use MockedClass for the constructor and add static methods separately
|
|
74
|
+
const MockedPushDrop = PushDrop as jest.MockedClass<typeof PushDrop> & {
|
|
75
|
+
decode: jest.Mock<any, any>
|
|
76
|
+
}
|
|
77
|
+
// Access the static mock assigned during jest.mock
|
|
78
|
+
const MockedPushDropDecode = MockedPushDrop.decode
|
|
79
|
+
const MockedUtils = Utils as jest.Mocked<typeof Utils>
|
|
80
|
+
const MockedTransaction = Transaction as jest.Mocked<typeof Transaction>
|
|
81
|
+
|
|
82
|
+
// --- Mock Wallet Setup ---
|
|
83
|
+
const createMockWallet = (): jest.Mocked<WalletInterface> => ({
|
|
84
|
+
listOutputs: jest.fn(),
|
|
85
|
+
encrypt: jest.fn(),
|
|
86
|
+
decrypt: jest.fn(),
|
|
87
|
+
createAction: jest.fn(),
|
|
88
|
+
signAction: jest.fn(),
|
|
89
|
+
relinquishOutput: jest.fn()
|
|
90
|
+
} as unknown as jest.Mocked<WalletInterface>)
|
|
91
|
+
|
|
92
|
+
describe('localKVStore', () => {
|
|
93
|
+
let mockWallet: jest.Mocked<WalletInterface>
|
|
94
|
+
let kvStore: LocalKVStore
|
|
95
|
+
const testContext = 'test-kv-context'
|
|
96
|
+
const testKey = 'myTestKey'
|
|
97
|
+
const testValue = 'myTestDataValue' // Raw value string used in tests
|
|
98
|
+
// Use the constants defined above for mock results
|
|
99
|
+
// const testEncryptedValue = Buffer.from('encryptedData'); // Defined above
|
|
100
|
+
const testOutpoint = 'txid123.0'
|
|
101
|
+
// const testLockingScriptHex = 'mockLockingScriptHex'; // Defined above
|
|
102
|
+
// const testUnlockingScriptHex = 'mockUnlockingScriptHex'; // Defined above
|
|
103
|
+
|
|
104
|
+
beforeEach(() => {
|
|
105
|
+
// Reset mocks before each test (clears calls and resets implementations)
|
|
106
|
+
jest.clearAllMocks()
|
|
107
|
+
|
|
108
|
+
// Create a fresh mock wallet for each test
|
|
109
|
+
mockWallet = createMockWallet()
|
|
110
|
+
|
|
111
|
+
// Create a kvStore instance with the mock wallet
|
|
112
|
+
// Default encrypt=true unless specified otherwise in a test block
|
|
113
|
+
kvStore = new LocalKVStore(mockWallet, testContext, true)
|
|
114
|
+
|
|
115
|
+
// Reset specific mock implementations if needed after clearAllMocks
|
|
116
|
+
// (e.g., if a test overrides a default implementation)
|
|
117
|
+
MockedPushDropDecode.mockClear() // Clear calls/results for static decode
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
// --- Constructor Tests ---
|
|
121
|
+
describe('constructor', () => {
|
|
122
|
+
it('should create an instance with default wallet and encrypt=true', () => {
|
|
123
|
+
// We need to mock the default WalletClient if the SUT uses it
|
|
124
|
+
const MockedWalletClient = require('../../../mod.js').WalletClient
|
|
125
|
+
const store = new LocalKVStore(undefined, 'default-context')
|
|
126
|
+
expect(store).toBeInstanceOf(LocalKVStore)
|
|
127
|
+
expect(MockedWalletClient).toHaveBeenCalledTimes(1) // Verify default was created
|
|
128
|
+
expect((store as any).context).toEqual('default-context')
|
|
129
|
+
expect((store as any).encrypt).toBe(true)
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
it('should create an instance with provided wallet, context, and encrypt=false', () => {
|
|
133
|
+
const customWallet = createMockWallet()
|
|
134
|
+
const store = new LocalKVStore(customWallet, 'custom-context', false)
|
|
135
|
+
expect(store).toBeInstanceOf(LocalKVStore)
|
|
136
|
+
expect((store as any).wallet).toBe(customWallet)
|
|
137
|
+
expect((store as any).context).toEqual('custom-context')
|
|
138
|
+
expect((store as any).encrypt).toBe(false)
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
it('should throw an error if context is missing or empty', () => {
|
|
142
|
+
expect(() => new LocalKVStore(mockWallet, '')).toThrow('A context in which to operate is required.')
|
|
143
|
+
expect(() => new LocalKVStore(mockWallet, null as any)).toThrow('A context in which to operate is required.')
|
|
144
|
+
})
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
// --- Get Method Tests ---
|
|
148
|
+
describe('get', () => {
|
|
149
|
+
it('should return defaultValue if no output is found', async () => {
|
|
150
|
+
mockWallet.listOutputs.mockResolvedValue({ outputs: [], totalOutputs: 0, BEEF: undefined })
|
|
151
|
+
const defaultValue = 'default'
|
|
152
|
+
const result = await kvStore.get(testKey, defaultValue)
|
|
153
|
+
|
|
154
|
+
expect(result).toBe(defaultValue)
|
|
155
|
+
expect(mockWallet.listOutputs).toHaveBeenCalledWith({
|
|
156
|
+
basket: testContext,
|
|
157
|
+
tags: [testKey],
|
|
158
|
+
include: 'locking scripts' // Check include value
|
|
159
|
+
})
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
it('should return undefined if no output is found and no defaultValue provided', async () => {
|
|
163
|
+
mockWallet.listOutputs.mockResolvedValue({ outputs: [], totalOutputs: 0, BEEF: undefined })
|
|
164
|
+
const result = await kvStore.get(testKey)
|
|
165
|
+
|
|
166
|
+
expect(result).toBeUndefined()
|
|
167
|
+
expect(mockWallet.listOutputs).toHaveBeenCalledWith({
|
|
168
|
+
basket: testContext,
|
|
169
|
+
tags: [testKey],
|
|
170
|
+
include: 'locking scripts' // Check include value
|
|
171
|
+
})
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
it('should throw an error if multiple outputs are found', async () => {
|
|
175
|
+
mockWallet.listOutputs.mockResolvedValue({
|
|
176
|
+
outputs: [
|
|
177
|
+
{ outpoint: 'txid1.0', lockingScript: 'script1' },
|
|
178
|
+
{ outpoint: 'txid2.0', lockingScript: 'script2' }
|
|
179
|
+
],
|
|
180
|
+
totalOutputs: 2,
|
|
181
|
+
BEEF: undefined
|
|
182
|
+
} as unknown as ListOutputsResult)
|
|
183
|
+
|
|
184
|
+
await expect(kvStore.get(testKey)).rejects.toThrow(
|
|
185
|
+
'Multiple tokens found for this key. You need to call set to collapse this ambiguous state before you can get this value again.'
|
|
186
|
+
)
|
|
187
|
+
expect(mockWallet.listOutputs).toHaveBeenCalledWith({
|
|
188
|
+
basket: testContext,
|
|
189
|
+
tags: [testKey],
|
|
190
|
+
include: 'locking scripts' // Check include value
|
|
191
|
+
})
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
it('should throw an error if PushDrop.decode fails', async () => {
|
|
195
|
+
const mockOutput = { outpoint: testOutpoint, lockingScript: testLockingScriptHex }
|
|
196
|
+
mockWallet.listOutputs.mockResolvedValue({ outputs: [mockOutput], totalOutputs: 1, BEEF: undefined } as any)
|
|
197
|
+
// LockingScript.fromHex is called internally by PushDrop.decode in the real implementation,
|
|
198
|
+
// but we mock decode directly here. We still need fromHex mocked if the SUT calls it elsewhere.
|
|
199
|
+
// MockedLockingScript.fromHex is already mocked globally to return mockLockingScriptInstance
|
|
200
|
+
|
|
201
|
+
// Make the *static* decode method throw
|
|
202
|
+
MockedPushDropDecode.mockImplementation(() => { throw new Error('Decode failed') })
|
|
203
|
+
|
|
204
|
+
await expect(kvStore.get(testKey)).rejects.toThrow(
|
|
205
|
+
// Match the error message precisely
|
|
206
|
+
`Invalid value found. You need to call set to collapse the corrupted state (or relinquish the corrupted ${testOutpoint} output from the ${testContext} basket) before you can get this value again.`
|
|
207
|
+
)
|
|
208
|
+
expect(MockedLockingScript.fromHex).toHaveBeenCalledWith(testLockingScriptHex)
|
|
209
|
+
expect(MockedPushDropDecode).toHaveBeenCalledWith(expect.objectContaining({ // Check arg for decode
|
|
210
|
+
toHex: expect.any(Function) // Check it got the script obj
|
|
211
|
+
}))
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
it('should throw an error if decoded fields length is not 1', async () => {
|
|
215
|
+
const mockOutput = { outpoint: testOutpoint, lockingScript: testLockingScriptHex }
|
|
216
|
+
mockWallet.listOutputs.mockResolvedValue({ outputs: [mockOutput], totalOutputs: 1, BEEF: undefined } as any)
|
|
217
|
+
// MockedLockingScript.fromHex is implicitly called by PushDrop.decode
|
|
218
|
+
|
|
219
|
+
// Mock the *static* decode to return multiple fields
|
|
220
|
+
MockedPushDropDecode.mockReturnValue({ fields: [Buffer.from([1, 2]), Buffer.from([3, 4])] })
|
|
221
|
+
|
|
222
|
+
await expect(kvStore.get(testKey)).rejects.toThrow('Invalid value found. You need to call set to collapse the corrupted state (or relinquish the corrupted txid123.0 output from the test-kv-context basket) before you can get this value again.')
|
|
223
|
+
expect(MockedLockingScript.fromHex).toHaveBeenCalledWith(testLockingScriptHex)
|
|
224
|
+
expect(MockedPushDropDecode).toHaveBeenCalled()
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
it('should get, decrypt and return the value when encrypt=true', async () => {
|
|
228
|
+
const mockOutput = { outpoint: testOutpoint, lockingScript: testLockingScriptHex }
|
|
229
|
+
mockWallet.listOutputs.mockResolvedValue({ outputs: [mockOutput], totalOutputs: 1, BEEF: undefined } as any)
|
|
230
|
+
// MockedLockingScript.fromHex is implicitly called by PushDrop.decode
|
|
231
|
+
|
|
232
|
+
// Mock the *static* decode to return the encrypted value buffer
|
|
233
|
+
MockedPushDropDecode.mockReturnValue({ fields: [testEncryptedValue] })
|
|
234
|
+
|
|
235
|
+
// Mock decrypt to return the plain text Array<number>
|
|
236
|
+
mockWallet.decrypt.mockResolvedValue({ plaintext: Array.from(testRawValueBuffer) } as WalletDecryptResult)
|
|
237
|
+
|
|
238
|
+
// Mock Utils.toUTF8 to perform the final conversion
|
|
239
|
+
MockedUtils.toUTF8.mockReturnValue(testValue) // Mock based on testValue string
|
|
240
|
+
|
|
241
|
+
const result = await kvStore.get(testKey)
|
|
242
|
+
|
|
243
|
+
expect(result).toBe(testValue)
|
|
244
|
+
expect(mockWallet.listOutputs).toHaveBeenCalledWith({ basket: testContext, tags: [testKey], include: 'locking scripts' })
|
|
245
|
+
expect(MockedLockingScript.fromHex).toHaveBeenCalledWith(testLockingScriptHex)
|
|
246
|
+
expect(MockedPushDropDecode).toHaveBeenCalled()
|
|
247
|
+
expect(mockWallet.decrypt).toHaveBeenCalledWith({
|
|
248
|
+
protocolID: [2, testContext],
|
|
249
|
+
keyID: testKey,
|
|
250
|
+
// Ensure ciphertext is passed as Uint8Array or Buffer (Buffer should work)
|
|
251
|
+
ciphertext: testEncryptedValue
|
|
252
|
+
})
|
|
253
|
+
// Ensure toUTF8 is called with the *decrypted* buffer data (as Array<number> or Uint8Array)
|
|
254
|
+
expect(MockedUtils.toUTF8).toHaveBeenCalledWith(Array.from(testRawValueBuffer))
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
it('should get and return the value without decryption when encrypt=false', async () => {
|
|
258
|
+
kvStore = new LocalKVStore(mockWallet, testContext, false) // Recreate store with encrypt=false
|
|
259
|
+
|
|
260
|
+
const mockOutput = { outpoint: testOutpoint, lockingScript: testLockingScriptHex }
|
|
261
|
+
mockWallet.listOutputs.mockResolvedValue({ outputs: [mockOutput], totalOutputs: 1, BEEF: undefined } as any)
|
|
262
|
+
// MockedLockingScript.fromHex implicitly called by PushDrop.decode
|
|
263
|
+
|
|
264
|
+
// Mock the *static* decode to return the raw value buffer
|
|
265
|
+
MockedPushDropDecode.mockReturnValue({ fields: [testRawValueBuffer] })
|
|
266
|
+
|
|
267
|
+
// Mock Utils.toUTF8 for final conversion
|
|
268
|
+
MockedUtils.toUTF8.mockReturnValue(testValue)
|
|
269
|
+
|
|
270
|
+
const result = await kvStore.get(testKey)
|
|
271
|
+
|
|
272
|
+
expect(result).toBe(testValue)
|
|
273
|
+
expect(mockWallet.listOutputs).toHaveBeenCalledWith({ basket: testContext, tags: [testKey], include: 'locking scripts' })
|
|
274
|
+
expect(MockedLockingScript.fromHex).toHaveBeenCalledWith(testLockingScriptHex)
|
|
275
|
+
expect(MockedPushDropDecode).toHaveBeenCalled()
|
|
276
|
+
expect(mockWallet.decrypt).not.toHaveBeenCalled() // Ensure decrypt was NOT called
|
|
277
|
+
expect(MockedUtils.toUTF8).toHaveBeenCalledWith(testRawValueBuffer) // Called with the raw buffer
|
|
278
|
+
})
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
// --- Set Method Tests ---
|
|
282
|
+
describe('set', () => {
|
|
283
|
+
let pushDropInstance: PushDrop // To access the instance methods
|
|
284
|
+
|
|
285
|
+
beforeEach(() => {
|
|
286
|
+
// Get the mock instance that will be created by `new PushDrop()`
|
|
287
|
+
pushDropInstance = new (PushDrop as any)()
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
it('should create a new encrypted output if none exists', async () => {
|
|
291
|
+
const valueArray = Array.from(testRawValueBuffer)
|
|
292
|
+
const encryptedArray = Array.from(testEncryptedValue)
|
|
293
|
+
MockedUtils.toArray.mockReturnValue(valueArray) // Mock toArray -> Array<number>
|
|
294
|
+
mockWallet.encrypt.mockResolvedValue({ ciphertext: encryptedArray } as WalletEncryptResult) // Encrypt returns Array<number>
|
|
295
|
+
mockWallet.listOutputs.mockResolvedValue({ outputs: [], totalOutputs: 0, BEEF: undefined })
|
|
296
|
+
mockWallet.createAction.mockResolvedValue({ txid: 'newTxId' } as CreateActionResult)
|
|
297
|
+
|
|
298
|
+
// Get the mock instance returned by the constructor
|
|
299
|
+
const mockPDInstance = new MockedPushDrop(mockWallet)
|
|
300
|
+
|
|
301
|
+
const result = await kvStore.set(testKey, testValue)
|
|
302
|
+
|
|
303
|
+
expect(result).toBe('newTxId.0')
|
|
304
|
+
expect(MockedUtils.toArray).toHaveBeenCalledWith(testValue, 'utf8')
|
|
305
|
+
expect(mockWallet.encrypt).toHaveBeenCalledWith({
|
|
306
|
+
plaintext: valueArray, // Should be Array<number>
|
|
307
|
+
protocolID: [2, testContext],
|
|
308
|
+
keyID: testKey
|
|
309
|
+
})
|
|
310
|
+
// Check the mock instance's lock method
|
|
311
|
+
expect(mockPDInstance.lock).toHaveBeenCalledWith(
|
|
312
|
+
// The lock function expects Array<number[] | Uint8Array>
|
|
313
|
+
// Ensure the encrypted value is passed correctly (as Uint8Array or Array<number>)
|
|
314
|
+
[(encryptedArray)], // Pass buffer derived from encrypted array
|
|
315
|
+
[2, testContext],
|
|
316
|
+
testKey,
|
|
317
|
+
'self'
|
|
318
|
+
)
|
|
319
|
+
expect(mockWallet.listOutputs).toHaveBeenCalledWith({
|
|
320
|
+
basket: testContext,
|
|
321
|
+
tags: [testKey],
|
|
322
|
+
include: 'entire transactions'
|
|
323
|
+
})
|
|
324
|
+
// Verify createAction for NEW output
|
|
325
|
+
expect(mockWallet.createAction).toHaveBeenCalledWith({
|
|
326
|
+
description: `Set ${testKey} in ${testContext}`,
|
|
327
|
+
outputs: [{
|
|
328
|
+
lockingScript: testLockingScriptHex, // From the mock lock result
|
|
329
|
+
satoshis: 1,
|
|
330
|
+
outputDescription: 'Key-value token'
|
|
331
|
+
}],
|
|
332
|
+
options: {
|
|
333
|
+
acceptDelayedBroadcast: false,
|
|
334
|
+
randomizeOutputs: false
|
|
335
|
+
}
|
|
336
|
+
})
|
|
337
|
+
expect(mockWallet.signAction).not.toHaveBeenCalled()
|
|
338
|
+
expect(mockWallet.relinquishOutput).not.toHaveBeenCalled()
|
|
339
|
+
})
|
|
340
|
+
|
|
341
|
+
it('should create a new non-encrypted output if none exists and encrypt=false', async () => {
|
|
342
|
+
kvStore = new LocalKVStore(mockWallet, testContext, false) // encrypt=false
|
|
343
|
+
const valueArray = Array.from(testRawValueBuffer)
|
|
344
|
+
MockedUtils.toArray.mockReturnValue(valueArray)
|
|
345
|
+
mockWallet.listOutputs.mockResolvedValue({ outputs: [], totalOutputs: 0, BEEF: undefined })
|
|
346
|
+
mockWallet.createAction.mockResolvedValue({ txid: 'newTxIdNonEnc' } as CreateActionResult)
|
|
347
|
+
|
|
348
|
+
// Get the mock instance returned by the constructor
|
|
349
|
+
const mockPDInstance = new MockedPushDrop(mockWallet)
|
|
350
|
+
|
|
351
|
+
const result = await kvStore.set(testKey, testValue)
|
|
352
|
+
|
|
353
|
+
expect(result).toBe('newTxIdNonEnc.0')
|
|
354
|
+
expect(MockedUtils.toArray).toHaveBeenCalledWith(testValue, 'utf8')
|
|
355
|
+
expect(mockWallet.encrypt).not.toHaveBeenCalled()
|
|
356
|
+
// Check the mock instance's lock method
|
|
357
|
+
expect(mockPDInstance.lock).toHaveBeenCalledWith(
|
|
358
|
+
[(valueArray)], // Pass raw value buffer
|
|
359
|
+
[2, testContext],
|
|
360
|
+
testKey,
|
|
361
|
+
'self'
|
|
362
|
+
)
|
|
363
|
+
expect(mockWallet.listOutputs).toHaveBeenCalledWith({
|
|
364
|
+
basket: testContext,
|
|
365
|
+
tags: [testKey],
|
|
366
|
+
include: 'entire transactions'
|
|
367
|
+
})
|
|
368
|
+
expect(mockWallet.createAction).toHaveBeenCalledWith({
|
|
369
|
+
description: `Set ${testKey} in ${testContext}`,
|
|
370
|
+
outputs: [{
|
|
371
|
+
lockingScript: testLockingScriptHex, // From mock lock
|
|
372
|
+
satoshis: 1,
|
|
373
|
+
outputDescription: 'Key-value token'
|
|
374
|
+
}],
|
|
375
|
+
options: {
|
|
376
|
+
acceptDelayedBroadcast: false,
|
|
377
|
+
randomizeOutputs: false
|
|
378
|
+
}
|
|
379
|
+
})
|
|
380
|
+
expect(mockWallet.signAction).not.toHaveBeenCalled()
|
|
381
|
+
expect(mockWallet.relinquishOutput).not.toHaveBeenCalled()
|
|
382
|
+
})
|
|
383
|
+
|
|
384
|
+
it('should update an existing output (spend and create)', async () => {
|
|
385
|
+
const existingOutpoint = 'oldTxId.0'
|
|
386
|
+
const existingOutput = { outpoint: existingOutpoint, txid: 'oldTxId', vout: 0, lockingScript: 'oldScriptHex' } // Added script
|
|
387
|
+
const mockBEEF = Array.from(Buffer.from('mockBEEFData'))
|
|
388
|
+
const signableRef = 'signableTxRef123'
|
|
389
|
+
const signableTx = []
|
|
390
|
+
const updatedTxId = 'updatedTxId'
|
|
391
|
+
|
|
392
|
+
const valueArray = Array.from(testRawValueBuffer)
|
|
393
|
+
const encryptedArray = Array.from(testEncryptedValue)
|
|
394
|
+
|
|
395
|
+
MockedUtils.toArray.mockReturnValue(valueArray)
|
|
396
|
+
mockWallet.encrypt.mockResolvedValue({ ciphertext: encryptedArray } as WalletEncryptResult)
|
|
397
|
+
mockWallet.listOutputs.mockResolvedValue({ outputs: [existingOutput], totalOutputs: 1, BEEF: mockBEEF } as any)
|
|
398
|
+
|
|
399
|
+
// Mock createAction to return a signable transaction structure
|
|
400
|
+
mockWallet.createAction.mockResolvedValue({
|
|
401
|
+
signableTransaction: { reference: signableRef, tx: signableTx }
|
|
402
|
+
} as CreateActionResult)
|
|
403
|
+
|
|
404
|
+
// Mock Transaction.fromAtomicBEEF to return a mock TX object
|
|
405
|
+
const mockTxObject = { /* Can add mock properties/methods if SUT uses them */ }
|
|
406
|
+
MockedTransaction.fromAtomicBEEF.mockReturnValue(mockTxObject as any)
|
|
407
|
+
|
|
408
|
+
mockWallet.signAction.mockResolvedValue({ txid: updatedTxId } as SignActionResult)
|
|
409
|
+
|
|
410
|
+
// Get the mock instance returned by the constructor
|
|
411
|
+
const mockPDInstance = new MockedPushDrop(mockWallet)
|
|
412
|
+
|
|
413
|
+
const result = await kvStore.set(testKey, testValue)
|
|
414
|
+
|
|
415
|
+
expect(result).toBe(`${updatedTxId}.0`) // Assuming output 0 is the new KV token
|
|
416
|
+
expect(mockWallet.encrypt).toHaveBeenCalled()
|
|
417
|
+
expect(mockPDInstance.lock).toHaveBeenCalledWith([(encryptedArray)], [2, testContext], testKey, 'self')
|
|
418
|
+
expect(mockWallet.listOutputs).toHaveBeenCalledWith({ basket: testContext, tags: [testKey], include: 'entire transactions' })
|
|
419
|
+
|
|
420
|
+
// Verify createAction for UPDATE
|
|
421
|
+
expect(mockWallet.createAction).toHaveBeenCalledWith(expect.objectContaining({ // Use objectContaining for flexibility
|
|
422
|
+
description: `Update ${testKey} in ${testContext}`,
|
|
423
|
+
inputBEEF: mockBEEF,
|
|
424
|
+
inputs: expect.arrayContaining([ // Check inputs array
|
|
425
|
+
expect.objectContaining({ outpoint: existingOutpoint }) // Check specific input
|
|
426
|
+
]),
|
|
427
|
+
outputs: expect.arrayContaining([ // Check outputs array
|
|
428
|
+
expect.objectContaining({ lockingScript: testLockingScriptHex }) // Check the new output script
|
|
429
|
+
])
|
|
430
|
+
}))
|
|
431
|
+
|
|
432
|
+
// Verify signing steps
|
|
433
|
+
expect(MockedTransaction.fromAtomicBEEF).toHaveBeenCalledWith(signableTx)
|
|
434
|
+
// Check unlock was called on the instance
|
|
435
|
+
expect(mockPDInstance.unlock).toHaveBeenCalledWith([2, testContext], testKey, 'self')
|
|
436
|
+
|
|
437
|
+
// Get the unlocker returned by the mock unlock method
|
|
438
|
+
const mockUnlocker = (mockPDInstance.unlock as jest.Mock).mock.results[0].value
|
|
439
|
+
expect(mockUnlocker.sign).toHaveBeenCalledWith(mockTxObject, 0) // Check sign args
|
|
440
|
+
|
|
441
|
+
// Verify signAction call
|
|
442
|
+
expect(mockWallet.signAction).toHaveBeenCalledWith({
|
|
443
|
+
reference: signableRef,
|
|
444
|
+
spends: {
|
|
445
|
+
0: { unlockingScript: testUnlockingScriptHex } // Check unlocking script from mock sign result
|
|
446
|
+
}
|
|
447
|
+
})
|
|
448
|
+
expect(mockWallet.relinquishOutput).not.toHaveBeenCalled()
|
|
449
|
+
})
|
|
450
|
+
|
|
451
|
+
it('should collapse multiple existing outputs into one', async () => {
|
|
452
|
+
const existingOutpoint1 = 'oldTxId1.0'
|
|
453
|
+
const existingOutpoint2 = 'oldTxId2.1'
|
|
454
|
+
const existingOutput1 = { outpoint: existingOutpoint1, txid: 'oldTxId1', vout: 0, lockingScript: 's1' }
|
|
455
|
+
const existingOutput2 = { outpoint: existingOutpoint2, txid: 'oldTxId2', vout: 1, lockingScript: 's2' }
|
|
456
|
+
const mockBEEF = Buffer.from('mockBEEFDataMulti')
|
|
457
|
+
const signableRef = 'signableTxRefMulti'
|
|
458
|
+
const signableTx = []
|
|
459
|
+
const updatedTxId = 'updatedTxIdMulti'
|
|
460
|
+
const mockTxObject = {} // Dummy TX object
|
|
461
|
+
|
|
462
|
+
const valueArray = Array.from(testRawValueBuffer)
|
|
463
|
+
const encryptedArray = Array.from(testEncryptedValue)
|
|
464
|
+
|
|
465
|
+
MockedUtils.toArray.mockReturnValue(valueArray)
|
|
466
|
+
mockWallet.encrypt.mockResolvedValue({ ciphertext: encryptedArray } as WalletEncryptResult)
|
|
467
|
+
mockWallet.listOutputs.mockResolvedValue({ outputs: [existingOutput1, existingOutput2], totalOutputs: 2, BEEF: mockBEEF } as any)
|
|
468
|
+
mockWallet.createAction.mockResolvedValue({
|
|
469
|
+
signableTransaction: { reference: signableRef, tx: signableTx }
|
|
470
|
+
} as CreateActionResult)
|
|
471
|
+
MockedTransaction.fromAtomicBEEF.mockReturnValue(mockTxObject as any)
|
|
472
|
+
mockWallet.signAction.mockResolvedValue({ txid: updatedTxId } as SignActionResult)
|
|
473
|
+
|
|
474
|
+
// Get the mock instance
|
|
475
|
+
const mockPDInstance = new MockedPushDrop(mockWallet)
|
|
476
|
+
|
|
477
|
+
const result = await kvStore.set(testKey, testValue)
|
|
478
|
+
|
|
479
|
+
expect(result).toBe(`${updatedTxId}.0`)
|
|
480
|
+
expect(mockWallet.encrypt).toHaveBeenCalled()
|
|
481
|
+
expect(mockPDInstance.lock).toHaveBeenCalled()
|
|
482
|
+
expect(mockWallet.listOutputs).toHaveBeenCalledWith({ basket: testContext, tags: [testKey], include: 'entire transactions' })
|
|
483
|
+
|
|
484
|
+
// Verify createAction with multiple inputs
|
|
485
|
+
expect(mockWallet.createAction).toHaveBeenCalledWith(expect.objectContaining({
|
|
486
|
+
inputBEEF: mockBEEF,
|
|
487
|
+
inputs: expect.arrayContaining([
|
|
488
|
+
expect.objectContaining({ outpoint: existingOutpoint1 }),
|
|
489
|
+
expect.objectContaining({ outpoint: existingOutpoint2 })
|
|
490
|
+
]),
|
|
491
|
+
outputs: expect.arrayContaining([
|
|
492
|
+
expect.objectContaining({ lockingScript: testLockingScriptHex })
|
|
493
|
+
])
|
|
494
|
+
}))
|
|
495
|
+
|
|
496
|
+
// Verify signing loop
|
|
497
|
+
expect(MockedTransaction.fromAtomicBEEF).toHaveBeenCalledWith(signableTx)
|
|
498
|
+
expect(mockPDInstance.unlock).toHaveBeenCalledTimes(2) // Called for each input
|
|
499
|
+
expect(mockPDInstance.unlock).toHaveBeenNthCalledWith(1, [2, testContext], testKey, 'self')
|
|
500
|
+
expect(mockPDInstance.unlock).toHaveBeenNthCalledWith(2, [2, testContext], testKey, 'self')
|
|
501
|
+
|
|
502
|
+
// Get the *same* mock unlocker instance (since unlock is mocked to always return it)
|
|
503
|
+
const mockUnlocker = (mockPDInstance.unlock as jest.Mock).mock.results[0].value
|
|
504
|
+
expect(mockUnlocker.sign).toHaveBeenCalledTimes(2)
|
|
505
|
+
expect(mockUnlocker.sign).toHaveBeenNthCalledWith(1, mockTxObject, 0) // Input index 0
|
|
506
|
+
expect(mockUnlocker.sign).toHaveBeenNthCalledWith(2, mockTxObject, 1) // Input index 1
|
|
507
|
+
|
|
508
|
+
// Verify signAction call with multiple spends
|
|
509
|
+
expect(mockWallet.signAction).toHaveBeenCalledWith({
|
|
510
|
+
reference: signableRef,
|
|
511
|
+
spends: {
|
|
512
|
+
0: { unlockingScript: testUnlockingScriptHex }, // Same mock script for both
|
|
513
|
+
1: { unlockingScript: testUnlockingScriptHex }
|
|
514
|
+
}
|
|
515
|
+
})
|
|
516
|
+
expect(mockWallet.relinquishOutput).not.toHaveBeenCalled()
|
|
517
|
+
})
|
|
518
|
+
|
|
519
|
+
it('should relinquish outputs if signing fails during update', async () => {
|
|
520
|
+
const existingOutpoint1 = 'failTxId1.0'
|
|
521
|
+
const existingOutpoint2 = 'failTxId2.1'
|
|
522
|
+
const existingOutput1 = { outpoint: existingOutpoint1, txid: 'failTxId1', vout: 0, lockingScript: 's1' }
|
|
523
|
+
const existingOutput2 = { outpoint: existingOutpoint2, txid: 'failTxId2', vout: 1, lockingScript: 's2' }
|
|
524
|
+
const mockBEEF = Buffer.from('mockBEEFFail')
|
|
525
|
+
const signableRef = 'signableTxRefFail'
|
|
526
|
+
const signableTx = []
|
|
527
|
+
const mockTxObject = {}
|
|
528
|
+
|
|
529
|
+
const valueArray = Array.from(testRawValueBuffer)
|
|
530
|
+
const encryptedArray = Array.from(testEncryptedValue)
|
|
531
|
+
|
|
532
|
+
MockedUtils.toArray.mockReturnValue(valueArray)
|
|
533
|
+
mockWallet.encrypt.mockResolvedValue({ ciphertext: encryptedArray } as WalletEncryptResult)
|
|
534
|
+
mockWallet.listOutputs.mockResolvedValue({ outputs: [existingOutput1, existingOutput2], totalOutputs: 2, BEEF: mockBEEF } as any)
|
|
535
|
+
mockWallet.createAction.mockResolvedValue({
|
|
536
|
+
signableTransaction: { reference: signableRef, tx: signableTx },
|
|
537
|
+
txid: 'fallback'
|
|
538
|
+
} as CreateActionResult)
|
|
539
|
+
MockedTransaction.fromAtomicBEEF.mockReturnValue(mockTxObject as any)
|
|
540
|
+
mockWallet.signAction.mockRejectedValue(new Error('Signature failed')) // Make signAction fail
|
|
541
|
+
mockWallet.relinquishOutput.mockResolvedValue({ relinquished: true }) // Mock relinquish success
|
|
542
|
+
|
|
543
|
+
// Get the mock instance
|
|
544
|
+
const mockPDInstance = new MockedPushDrop(mockWallet)
|
|
545
|
+
|
|
546
|
+
// Expect the error to be caught, and the method to complete and returns the fallback outpoint.
|
|
547
|
+
await expect(kvStore.set(testKey, testValue)).resolves.toEqual('fallback.0')
|
|
548
|
+
|
|
549
|
+
// Verify setup calls happened
|
|
550
|
+
expect(mockWallet.encrypt).toHaveBeenCalled()
|
|
551
|
+
expect(mockPDInstance.lock).toHaveBeenCalled()
|
|
552
|
+
expect(mockWallet.listOutputs).toHaveBeenCalled()
|
|
553
|
+
expect(mockWallet.createAction).toHaveBeenCalled()
|
|
554
|
+
expect(MockedTransaction.fromAtomicBEEF).toHaveBeenCalled()
|
|
555
|
+
expect(mockPDInstance.unlock).toHaveBeenCalledTimes(2) // Unlock was still called
|
|
556
|
+
const mockUnlocker = (mockPDInstance.unlock as jest.Mock).mock.results[0].value
|
|
557
|
+
expect(mockUnlocker.sign).toHaveBeenCalledTimes(2) // Sign was still called
|
|
558
|
+
expect(mockWallet.signAction).toHaveBeenCalled() // It was called, but failed
|
|
559
|
+
|
|
560
|
+
// Crucially, verify relinquish was called for each input
|
|
561
|
+
expect(mockWallet.relinquishOutput).toHaveBeenCalledTimes(2)
|
|
562
|
+
expect(mockWallet.relinquishOutput).toHaveBeenNthCalledWith(1, {
|
|
563
|
+
output: existingOutpoint1,
|
|
564
|
+
basket: testContext
|
|
565
|
+
})
|
|
566
|
+
expect(mockWallet.relinquishOutput).toHaveBeenNthCalledWith(2, {
|
|
567
|
+
output: existingOutpoint2,
|
|
568
|
+
basket: testContext
|
|
569
|
+
})
|
|
570
|
+
})
|
|
571
|
+
})
|
|
572
|
+
|
|
573
|
+
// --- Remove Method Tests ---
|
|
574
|
+
describe('remove', () => {
|
|
575
|
+
let pushDropInstance: PushDrop // To access the instance methods
|
|
576
|
+
|
|
577
|
+
beforeEach(() => {
|
|
578
|
+
// Get the mock instance that will be created by `new PushDrop()`
|
|
579
|
+
pushDropInstance = new (PushDrop as any)()
|
|
580
|
+
})
|
|
581
|
+
|
|
582
|
+
it('should do nothing and return void if key does not exist', async () => {
|
|
583
|
+
mockWallet.listOutputs.mockResolvedValue({ outputs: [], totalOutputs: 0, BEEF: undefined })
|
|
584
|
+
|
|
585
|
+
const result = await kvStore.remove(testKey)
|
|
586
|
+
|
|
587
|
+
expect(result).toBeUndefined()
|
|
588
|
+
expect(mockWallet.listOutputs).toHaveBeenCalledWith({
|
|
589
|
+
basket: testContext,
|
|
590
|
+
tags: [testKey],
|
|
591
|
+
include: 'entire transactions' // remove checks for entire transactions
|
|
592
|
+
})
|
|
593
|
+
expect(mockWallet.createAction).not.toHaveBeenCalled()
|
|
594
|
+
expect(mockWallet.signAction).not.toHaveBeenCalled()
|
|
595
|
+
expect(mockWallet.relinquishOutput).not.toHaveBeenCalled()
|
|
596
|
+
})
|
|
597
|
+
|
|
598
|
+
it('should remove an existing key by spending its output(s)', async () => {
|
|
599
|
+
const existingOutpoint1 = 'removeTxId1.0'
|
|
600
|
+
const existingOutpoint2 = 'removeTxId2.1'
|
|
601
|
+
const existingOutput1 = { outpoint: existingOutpoint1, txid: 'removeTxId1', vout: 0, lockingScript: 's1' }
|
|
602
|
+
const existingOutput2 = { outpoint: existingOutpoint2, txid: 'removeTxId2', vout: 1, lockingScript: 's2' }
|
|
603
|
+
const mockBEEF = Buffer.from('mockBEEFRemove')
|
|
604
|
+
const signableRef = 'signableTxRefRemove'
|
|
605
|
+
const signableTx = []
|
|
606
|
+
const removalTxId = 'removalTxId'
|
|
607
|
+
const mockTxObject = {}
|
|
608
|
+
|
|
609
|
+
mockWallet.listOutputs.mockResolvedValue({ outputs: [existingOutput1, existingOutput2], totalOutputs: 2, BEEF: mockBEEF } as any)
|
|
610
|
+
mockWallet.createAction.mockResolvedValue({
|
|
611
|
+
signableTransaction: { reference: signableRef, tx: signableTx }
|
|
612
|
+
} as CreateActionResult) // Note: removal tx has NO outputs field in result
|
|
613
|
+
MockedTransaction.fromAtomicBEEF.mockReturnValue(mockTxObject as any)
|
|
614
|
+
mockWallet.signAction.mockResolvedValue({ txid: removalTxId } as SignActionResult)
|
|
615
|
+
|
|
616
|
+
// Get the mock instance
|
|
617
|
+
const mockPDInstance = new MockedPushDrop(mockWallet)
|
|
618
|
+
|
|
619
|
+
const result = await kvStore.remove(testKey)
|
|
620
|
+
|
|
621
|
+
expect(result).toBe(removalTxId)
|
|
622
|
+
expect(mockWallet.listOutputs).toHaveBeenCalledWith({ basket: testContext, tags: [testKey], include: 'entire transactions' })
|
|
623
|
+
|
|
624
|
+
// Verify createAction for REMOVE (no outputs in the action)
|
|
625
|
+
expect(mockWallet.createAction).toHaveBeenCalledWith({
|
|
626
|
+
// The description might still say "Update" depending on implementation reuse
|
|
627
|
+
// description: `Remove ${testKey} from ${testContext}`, // Ideal description
|
|
628
|
+
description: expect.stringContaining(testKey), // More general check
|
|
629
|
+
inputBEEF: mockBEEF,
|
|
630
|
+
inputs: expect.arrayContaining([
|
|
631
|
+
expect.objectContaining({ outpoint: existingOutpoint1 }),
|
|
632
|
+
expect.objectContaining({ outpoint: existingOutpoint2 })
|
|
633
|
+
]),
|
|
634
|
+
// IMPORTANT: No 'outputs' key should be present for removal action
|
|
635
|
+
outputs: undefined, // Or check that the key is not present
|
|
636
|
+
options: {
|
|
637
|
+
acceptDelayedBroadcast: false
|
|
638
|
+
}
|
|
639
|
+
})
|
|
640
|
+
// Check that outputs key is absent
|
|
641
|
+
expect(mockWallet.createAction.mock.calls[0][0]).not.toHaveProperty('outputs')
|
|
642
|
+
|
|
643
|
+
// Verify signing
|
|
644
|
+
expect(MockedTransaction.fromAtomicBEEF).toHaveBeenCalledWith(signableTx)
|
|
645
|
+
expect(mockPDInstance.unlock).toHaveBeenCalledTimes(2)
|
|
646
|
+
expect(mockPDInstance.unlock).toHaveBeenNthCalledWith(1, [2, testContext], testKey, 'self')
|
|
647
|
+
expect(mockPDInstance.unlock).toHaveBeenNthCalledWith(2, [2, testContext], testKey, 'self')
|
|
648
|
+
const mockUnlocker = (mockPDInstance.unlock as jest.Mock).mock.results[0].value
|
|
649
|
+
expect(mockUnlocker.sign).toHaveBeenCalledTimes(2)
|
|
650
|
+
expect(mockUnlocker.sign).toHaveBeenNthCalledWith(1, mockTxObject, 0)
|
|
651
|
+
expect(mockUnlocker.sign).toHaveBeenNthCalledWith(2, mockTxObject, 1)
|
|
652
|
+
|
|
653
|
+
// Verify signAction call
|
|
654
|
+
expect(mockWallet.signAction).toHaveBeenCalledWith({
|
|
655
|
+
reference: signableRef,
|
|
656
|
+
spends: {
|
|
657
|
+
0: { unlockingScript: testUnlockingScriptHex },
|
|
658
|
+
1: { unlockingScript: testUnlockingScriptHex }
|
|
659
|
+
}
|
|
660
|
+
})
|
|
661
|
+
expect(mockWallet.relinquishOutput).not.toHaveBeenCalled()
|
|
662
|
+
})
|
|
663
|
+
|
|
664
|
+
it('should relinquish outputs if signing fails during removal', async () => {
|
|
665
|
+
const existingOutpoint1 = 'failRemoveTxId1.0'
|
|
666
|
+
const existingOutput1 = { outpoint: existingOutpoint1, txid: 'failRemoveTxId1', vout: 0, lockingScript: 's1' }
|
|
667
|
+
const mockBEEF = Buffer.from('mockBEEFFailRemove')
|
|
668
|
+
const signableRef = 'signableTxRefFailRemove'
|
|
669
|
+
const signableTx = []
|
|
670
|
+
const mockTxObject = {}
|
|
671
|
+
|
|
672
|
+
mockWallet.listOutputs.mockResolvedValue({ outputs: [existingOutput1], totalOutputs: 1, BEEF: mockBEEF } as any)
|
|
673
|
+
mockWallet.createAction.mockResolvedValue({
|
|
674
|
+
signableTransaction: { reference: signableRef, tx: signableTx }
|
|
675
|
+
} as CreateActionResult)
|
|
676
|
+
MockedTransaction.fromAtomicBEEF.mockReturnValue(mockTxObject as any)
|
|
677
|
+
mockWallet.signAction.mockRejectedValue(new Error('Signature failed remove')) // Make signAction fail
|
|
678
|
+
mockWallet.relinquishOutput.mockResolvedValue({ relinquished: true })
|
|
679
|
+
|
|
680
|
+
// Get the mock instance
|
|
681
|
+
const mockPDInstance = new MockedPushDrop(mockWallet)
|
|
682
|
+
|
|
683
|
+
// Expect the error to be caught, method completes returning undefined/void
|
|
684
|
+
await expect(kvStore.remove(testKey)).resolves.toBeUndefined()
|
|
685
|
+
|
|
686
|
+
// Verify setup calls
|
|
687
|
+
expect(mockWallet.listOutputs).toHaveBeenCalled()
|
|
688
|
+
expect(mockWallet.createAction).toHaveBeenCalled() // createAction called for removal attempt
|
|
689
|
+
expect(MockedTransaction.fromAtomicBEEF).toHaveBeenCalled()
|
|
690
|
+
expect(mockPDInstance.unlock).toHaveBeenCalledTimes(1) // unlock was called
|
|
691
|
+
const mockUnlocker = (mockPDInstance.unlock as jest.Mock).mock.results[0].value
|
|
692
|
+
expect(mockUnlocker.sign).toHaveBeenCalledTimes(1) // sign was called
|
|
693
|
+
expect(mockWallet.signAction).toHaveBeenCalled() // Called but failed
|
|
694
|
+
|
|
695
|
+
// Verify relinquish was called
|
|
696
|
+
expect(mockWallet.relinquishOutput).toHaveBeenCalledTimes(1)
|
|
697
|
+
expect(mockWallet.relinquishOutput).toHaveBeenCalledWith({
|
|
698
|
+
output: existingOutpoint1,
|
|
699
|
+
basket: testContext
|
|
700
|
+
})
|
|
701
|
+
})
|
|
702
|
+
})
|
|
703
|
+
})
|