@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.
- 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 +279 -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/src/wallet/WERR_REVIEW_ACTIONS.js +29 -0
- package/dist/cjs/src/wallet/WERR_REVIEW_ACTIONS.js.map +1 -0
- package/dist/cjs/src/wallet/WalletError.js +4 -3
- package/dist/cjs/src/wallet/WalletError.js.map +1 -1
- package/dist/cjs/src/wallet/index.js +4 -1
- package/dist/cjs/src/wallet/index.js.map +1 -1
- package/dist/cjs/src/wallet/substrates/HTTPWalletJSON.js +13 -6
- package/dist/cjs/src/wallet/substrates/HTTPWalletJSON.js.map +1 -1
- 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 +273 -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/src/wallet/WERR_REVIEW_ACTIONS.js +31 -0
- package/dist/esm/src/wallet/WERR_REVIEW_ACTIONS.js.map +1 -0
- package/dist/esm/src/wallet/WalletError.js +3 -2
- package/dist/esm/src/wallet/WalletError.js.map +1 -1
- package/dist/esm/src/wallet/index.js +2 -0
- package/dist/esm/src/wallet/index.js.map +1 -1
- package/dist/esm/src/wallet/substrates/HTTPWalletJSON.js +13 -6
- package/dist/esm/src/wallet/substrates/HTTPWalletJSON.js.map +1 -1
- 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 +85 -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/src/wallet/WERR_REVIEW_ACTIONS.d.ts +23 -0
- package/dist/types/src/wallet/WERR_REVIEW_ACTIONS.d.ts.map +1 -0
- package/dist/types/src/wallet/Wallet.interfaces.d.ts +22 -0
- package/dist/types/src/wallet/Wallet.interfaces.d.ts.map +1 -1
- package/dist/types/src/wallet/WalletError.d.ts +4 -3
- package/dist/types/src/wallet/WalletError.d.ts.map +1 -1
- package/dist/types/src/wallet/index.d.ts +1 -0
- package/dist/types/src/wallet/index.d.ts.map +1 -1
- package/dist/types/src/wallet/substrates/HTTPWalletJSON.d.ts.map +1 -1
- 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 +133 -0
- package/docs/registry.md +383 -0
- package/docs/transaction.md +3 -3
- package/docs/wallet.md +146 -38
- package/mod.ts +2 -1
- package/package.json +19 -9
- package/src/kvstore/LocalKVStore.ts +287 -0
- package/src/kvstore/__tests/LocalKVStore.test.ts +614 -0
- package/src/kvstore/index.ts +1 -0
- package/src/wallet/WERR_REVIEW_ACTIONS.ts +30 -0
- package/src/wallet/Wallet.interfaces.ts +24 -0
- package/src/wallet/WalletError.ts +4 -2
- package/src/wallet/index.ts +2 -0
- package/src/wallet/substrates/HTTPWalletJSON.ts +12 -6
- 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
|