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