@bsv/sdk 2.0.12 → 2.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/dist/cjs/package.json +1 -1
  2. package/dist/cjs/src/auth/clients/__tests__/AuthFetch.additional.test.js +827 -0
  3. package/dist/cjs/src/auth/clients/__tests__/AuthFetch.additional.test.js.map +1 -0
  4. package/dist/cjs/src/auth/transports/__tests__/SimplifiedFetchTransport.additional.test.js +654 -0
  5. package/dist/cjs/src/auth/transports/__tests__/SimplifiedFetchTransport.additional.test.js.map +1 -0
  6. package/dist/cjs/src/transaction/MerklePath.js +132 -0
  7. package/dist/cjs/src/transaction/MerklePath.js.map +1 -1
  8. package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
  9. package/dist/esm/src/auth/clients/__tests__/AuthFetch.additional.test.js +825 -0
  10. package/dist/esm/src/auth/clients/__tests__/AuthFetch.additional.test.js.map +1 -0
  11. package/dist/esm/src/auth/transports/__tests__/SimplifiedFetchTransport.additional.test.js +619 -0
  12. package/dist/esm/src/auth/transports/__tests__/SimplifiedFetchTransport.additional.test.js.map +1 -0
  13. package/dist/esm/src/transaction/MerklePath.js +132 -0
  14. package/dist/esm/src/transaction/MerklePath.js.map +1 -1
  15. package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
  16. package/dist/types/src/auth/clients/__tests__/AuthFetch.additional.test.d.ts +21 -0
  17. package/dist/types/src/auth/clients/__tests__/AuthFetch.additional.test.d.ts.map +1 -0
  18. package/dist/types/src/auth/transports/__tests__/SimplifiedFetchTransport.additional.test.d.ts +2 -0
  19. package/dist/types/src/auth/transports/__tests__/SimplifiedFetchTransport.additional.test.d.ts.map +1 -0
  20. package/dist/types/src/transaction/MerklePath.d.ts +27 -0
  21. package/dist/types/src/transaction/MerklePath.d.ts.map +1 -1
  22. package/dist/types/tsconfig.types.tsbuildinfo +1 -1
  23. package/dist/umd/bundle.js +1 -1
  24. package/dist/umd/bundle.js.map +1 -1
  25. package/docs/reference/storage.md +1 -1
  26. package/docs/reference/transaction.md +40 -0
  27. package/package.json +1 -1
  28. package/src/auth/clients/__tests__/AuthFetch.additional.test.ts +1131 -0
  29. package/src/auth/transports/__tests__/SimplifiedFetchTransport.additional.test.ts +770 -0
  30. package/src/compat/__tests/Mnemonic.additional.test.ts +64 -0
  31. package/src/identity/__tests/IdentityClient.additional.test.ts +767 -0
  32. package/src/kvstore/__tests/LocalKVStore.additional.test.ts +611 -0
  33. package/src/kvstore/__tests/kvStoreInterpreter.test.ts +327 -0
  34. package/src/overlay-tools/__tests/HostReputationTracker.additional.test.ts +561 -0
  35. package/src/overlay-tools/__tests/LookupResolver.additional.test.ts +612 -0
  36. package/src/overlay-tools/__tests/withDoubleSpendRetry.test.ts +278 -0
  37. package/src/primitives/__tests/BigNumber.additional.test.ts +79 -0
  38. package/src/primitives/__tests/Curve.additional.test.ts +208 -0
  39. package/src/primitives/__tests/ECDSA.additional.test.ts +122 -0
  40. package/src/primitives/__tests/Hash.additional.test.ts +59 -0
  41. package/src/primitives/__tests/JacobianPoint.test.ts +308 -0
  42. package/src/primitives/__tests/Point.additional.test.ts +503 -0
  43. package/src/primitives/__tests/PublicKey.additional.test.ts +383 -0
  44. package/src/primitives/__tests/Random.additional.test.ts +262 -0
  45. package/src/primitives/__tests/Signature.test.ts +333 -0
  46. package/src/primitives/__tests/TransactionSignature.additional.test.ts +241 -0
  47. package/src/registry/__tests/RegistryClient.additional.test.ts +750 -0
  48. package/src/remittance/__tests/BasicBRC29.additional.test.ts +657 -0
  49. package/src/remittance/__tests/RemittanceManager.additional.test.ts +1272 -0
  50. package/src/script/__tests/LockingUnlockingScript.test.ts +79 -0
  51. package/src/script/__tests/Script.additional.test.ts +100 -0
  52. package/src/script/__tests/ScriptEvaluationError.test.ts +98 -0
  53. package/src/script/__tests/Spend.additional.test.ts +837 -0
  54. package/src/script/templates/__tests/RPuzzle.test.ts +134 -0
  55. package/src/transaction/MerklePath.ts +155 -0
  56. package/src/transaction/__tests/BeefParty.additional.test.ts +22 -0
  57. package/src/transaction/__tests/Broadcaster.test.ts +159 -0
  58. package/src/transaction/__tests/MerklePath.bench.test.ts +105 -0
  59. package/src/transaction/__tests/MerklePath.test.ts +80 -0
  60. package/src/transaction/__tests/Transaction.additional.test.ts +225 -0
  61. package/src/transaction/broadcasters/__tests/ARC.additional.test.ts +585 -0
  62. package/src/transaction/broadcasters/__tests/Teranode.test.ts +349 -0
  63. package/src/transaction/chaintrackers/__tests/BlockHeadersService.test.ts +253 -0
  64. package/src/transaction/chaintrackers/__tests/DefaultChainTracker.test.ts +44 -0
  65. package/src/transaction/chaintrackers/__tests/WhatsOnChain.additional.test.ts +193 -0
  66. package/src/transaction/fee-models/__tests/SatoshisPerKilobyte.test.ts +262 -0
  67. package/src/transaction/http/__tests/BinaryFetchClient.test.ts +212 -0
  68. package/src/transaction/http/__tests/DefaultHttpClient.additional.test.ts +192 -0
  69. package/src/transaction/http/__tests/DefaultHttpClient.test.ts +71 -0
  70. package/src/wallet/__tests/ProtoWallet.additional.test.ts +134 -0
  71. package/src/wallet/__tests/WERR.test.ts +212 -0
  72. package/src/wallet/__tests/WalletClient.additional.test.ts +699 -0
  73. package/src/wallet/__tests/WalletClient.substrate.test.ts +759 -0
  74. package/src/wallet/__tests/WalletError.test.ts +290 -0
  75. package/src/wallet/__tests/validationHelpers.test.ts +1218 -0
  76. package/src/wallet/substrates/__tests/HTTPWalletJSON.test.ts +496 -0
  77. package/src/wallet/substrates/__tests/HTTPWalletWire.test.ts +273 -0
@@ -0,0 +1,1218 @@
1
+ /**
2
+ * Tests for src/wallet/validationHelpers.ts
3
+ *
4
+ * validationHelpers.ts is at ~20% coverage (215 missed lines).
5
+ * All exported functions are covered here. Private helpers are exercised
6
+ * indirectly through the exported functions that call them.
7
+ */
8
+
9
+ import WERR_INVALID_PARAMETER from '../WERR_INVALID_PARAMETER'
10
+ import {
11
+ parseWalletOutpoint,
12
+ validateSatoshis,
13
+ validateOptionalInteger,
14
+ validateInteger,
15
+ validatePositiveIntegerOrZero,
16
+ validateStringLength,
17
+ validateBase64String,
18
+ isHexString,
19
+ validateCreateActionInput,
20
+ validateCreateActionOutput,
21
+ validateCreateActionOptions,
22
+ validateCreateActionArgs,
23
+ validateSignActionOptions,
24
+ validateSignActionArgs,
25
+ validateAbortActionArgs,
26
+ validateWalletPayment,
27
+ validateBasketInsertion,
28
+ validateInternalizeOutput,
29
+ validateOriginator,
30
+ validateOptionalOutpointString,
31
+ validateOutpointString,
32
+ validateRelinquishOutputArgs,
33
+ validateRelinquishCertificateArgs,
34
+ validateListCertificatesArgs,
35
+ validateAcquireIssuanceCertificateArgs,
36
+ validateAcquireDirectCertificateArgs,
37
+ validateProveCertificateArgs,
38
+ validateDiscoverByIdentityKeyArgs,
39
+ validateDiscoverByAttributesArgs,
40
+ validateListOutputsArgs,
41
+ validateListActionsArgs,
42
+ specOpThrowReviewActions
43
+ } from '../validationHelpers'
44
+
45
+ // ---------------------------------------------------------------------------
46
+ // Shared test data
47
+ // ---------------------------------------------------------------------------
48
+
49
+ // Valid 64-char hex txid
50
+ const VALID_TXID = 'a'.repeat(64)
51
+ // Valid outpoint string
52
+ const VALID_OUTPOINT = `${VALID_TXID}.0`
53
+ // Valid compressed pubkey hex (66 chars)
54
+ const VALID_PUBKEY_HEX = '02' + 'ab'.repeat(32)
55
+ // Valid base64 strings
56
+ const VALID_BASE64 = 'SGVsbG8=' // "Hello" in base64
57
+ const VALID_BASE64_NOPAD = 'SGVsbG8' // without padding (valid 4n+3)
58
+
59
+ // ============================================================================
60
+ // parseWalletOutpoint
61
+ // ============================================================================
62
+
63
+ describe('parseWalletOutpoint', () => {
64
+ it('splits "txid.vout" into txid string and numeric vout', () => {
65
+ const result = parseWalletOutpoint(`${VALID_TXID}.3`)
66
+ expect(result.txid).toBe(VALID_TXID)
67
+ expect(result.vout).toBe(3)
68
+ })
69
+
70
+ it('handles vout 0', () => {
71
+ expect(parseWalletOutpoint(`${VALID_TXID}.0`).vout).toBe(0)
72
+ })
73
+ })
74
+
75
+ // ============================================================================
76
+ // validateSatoshis
77
+ // ============================================================================
78
+
79
+ describe('validateSatoshis', () => {
80
+ it('accepts 0 satoshis', () => {
81
+ expect(validateSatoshis(0, 'amount')).toBe(0)
82
+ })
83
+
84
+ it('accepts maximum satoshis (21e14)', () => {
85
+ expect(validateSatoshis(21e14, 'amount')).toBe(21e14)
86
+ })
87
+
88
+ it('throws for undefined', () => {
89
+ expect(() => validateSatoshis(undefined, 'amount')).toThrow(WERR_INVALID_PARAMETER)
90
+ })
91
+
92
+ it('throws for a float', () => {
93
+ expect(() => validateSatoshis(1.5, 'amount')).toThrow(WERR_INVALID_PARAMETER)
94
+ })
95
+
96
+ it('throws for a negative value', () => {
97
+ expect(() => validateSatoshis(-1, 'amount')).toThrow(WERR_INVALID_PARAMETER)
98
+ })
99
+
100
+ it('throws when value exceeds 21e14', () => {
101
+ expect(() => validateSatoshis(21e14 + 1, 'amount')).toThrow(WERR_INVALID_PARAMETER)
102
+ })
103
+
104
+ it('throws when below optional min', () => {
105
+ expect(() => validateSatoshis(5, 'amount', 10)).toThrow(WERR_INVALID_PARAMETER)
106
+ })
107
+
108
+ it('accepts when exactly at optional min', () => {
109
+ expect(validateSatoshis(10, 'amount', 10)).toBe(10)
110
+ })
111
+ })
112
+
113
+ // ============================================================================
114
+ // validateInteger
115
+ // ============================================================================
116
+
117
+ describe('validateInteger', () => {
118
+ it('returns the value when valid', () => {
119
+ expect(validateInteger(5, 'n')).toBe(5)
120
+ })
121
+
122
+ it('returns defaultValue when v is undefined', () => {
123
+ expect(validateInteger(undefined, 'n', 42)).toBe(42)
124
+ })
125
+
126
+ it('throws when undefined and no defaultValue', () => {
127
+ expect(() => validateInteger(undefined, 'n')).toThrow(WERR_INVALID_PARAMETER)
128
+ })
129
+
130
+ it('throws for a non-integer', () => {
131
+ expect(() => validateInteger(1.5, 'n')).toThrow(WERR_INVALID_PARAMETER)
132
+ })
133
+
134
+ it('throws when below min', () => {
135
+ expect(() => validateInteger(0, 'n', undefined, 1)).toThrow(WERR_INVALID_PARAMETER)
136
+ })
137
+
138
+ it('throws when above max', () => {
139
+ expect(() => validateInteger(11, 'n', undefined, undefined, 10)).toThrow(WERR_INVALID_PARAMETER)
140
+ })
141
+
142
+ it('accepts value at min boundary', () => {
143
+ expect(validateInteger(1, 'n', undefined, 1)).toBe(1)
144
+ })
145
+
146
+ it('accepts value at max boundary', () => {
147
+ expect(validateInteger(10, 'n', undefined, undefined, 10)).toBe(10)
148
+ })
149
+ })
150
+
151
+ // ============================================================================
152
+ // validateOptionalInteger
153
+ // ============================================================================
154
+
155
+ describe('validateOptionalInteger', () => {
156
+ it('returns undefined when v is undefined', () => {
157
+ expect(validateOptionalInteger(undefined, 'n')).toBeUndefined()
158
+ })
159
+
160
+ it('returns the value when valid', () => {
161
+ expect(validateOptionalInteger(5, 'n')).toBe(5)
162
+ })
163
+
164
+ it('throws for an invalid value', () => {
165
+ expect(() => validateOptionalInteger(1.5, 'n')).toThrow(WERR_INVALID_PARAMETER)
166
+ })
167
+ })
168
+
169
+ // ============================================================================
170
+ // validatePositiveIntegerOrZero
171
+ // ============================================================================
172
+
173
+ describe('validatePositiveIntegerOrZero', () => {
174
+ it('accepts 0', () => {
175
+ expect(validatePositiveIntegerOrZero(0, 'n')).toBe(0)
176
+ })
177
+
178
+ it('accepts positive integers', () => {
179
+ expect(validatePositiveIntegerOrZero(100, 'n')).toBe(100)
180
+ })
181
+
182
+ it('throws for negative integers', () => {
183
+ expect(() => validatePositiveIntegerOrZero(-1, 'n')).toThrow(WERR_INVALID_PARAMETER)
184
+ })
185
+ })
186
+
187
+ // ============================================================================
188
+ // validateStringLength
189
+ // ============================================================================
190
+
191
+ describe('validateStringLength', () => {
192
+ it('returns the string when within bounds', () => {
193
+ expect(validateStringLength('hello', 's', 1, 10)).toBe('hello')
194
+ })
195
+
196
+ it('throws when string is too short', () => {
197
+ expect(() => validateStringLength('hi', 's', 5)).toThrow(WERR_INVALID_PARAMETER)
198
+ })
199
+
200
+ it('throws when string is too long', () => {
201
+ expect(() => validateStringLength('hello world', 's', 1, 5)).toThrow(WERR_INVALID_PARAMETER)
202
+ })
203
+
204
+ it('accepts when no bounds provided', () => {
205
+ expect(validateStringLength('any string at all', 's')).toBe('any string at all')
206
+ })
207
+
208
+ it('handles multi-byte UTF-8 characters correctly', () => {
209
+ // '€' is 3 UTF-8 bytes
210
+ const euro = '€'
211
+ expect(() => validateStringLength(euro, 's', 1, 2)).toThrow(WERR_INVALID_PARAMETER)
212
+ expect(validateStringLength(euro, 's', 1, 3)).toBe(euro)
213
+ })
214
+ })
215
+
216
+ // ============================================================================
217
+ // validateBase64String
218
+ // ============================================================================
219
+
220
+ describe('validateBase64String', () => {
221
+ it('accepts a valid padded base64 string', () => {
222
+ expect(validateBase64String(VALID_BASE64, 's')).toBe(VALID_BASE64)
223
+ })
224
+
225
+ it('trims whitespace before validation', () => {
226
+ expect(validateBase64String(` ${VALID_BASE64} `, 's')).toBe(VALID_BASE64)
227
+ })
228
+
229
+ it('throws for an empty string', () => {
230
+ expect(() => validateBase64String('', 's')).toThrow(WERR_INVALID_PARAMETER)
231
+ })
232
+
233
+ it('throws for a string with invalid characters', () => {
234
+ expect(() => validateBase64String('abc!', 's')).toThrow(WERR_INVALID_PARAMETER)
235
+ })
236
+
237
+ it('throws when padding appears within the last 2 chars boundary', () => {
238
+ // '=' at position i is only valid when i >= length - 2
239
+ // 'a=bc' has '=' at i=1, length=4, so i < length-2 (1 < 2) → throws
240
+ expect(() => validateBase64String('a=bc', 's')).toThrow(WERR_INVALID_PARAMETER)
241
+ })
242
+
243
+ it('throws for more than 2 padding characters', () => {
244
+ expect(() => validateBase64String('a===', 's')).toThrow(WERR_INVALID_PARAMETER)
245
+ })
246
+
247
+ it('throws when bytes are below min', () => {
248
+ // VALID_BASE64 = "SGVsbG8=" → 5 decoded bytes
249
+ expect(() => validateBase64String(VALID_BASE64, 's', 6)).toThrow(WERR_INVALID_PARAMETER)
250
+ })
251
+
252
+ it('throws when bytes exceed max', () => {
253
+ expect(() => validateBase64String(VALID_BASE64, 's', undefined, 3)).toThrow(WERR_INVALID_PARAMETER)
254
+ })
255
+
256
+ it('throws for unpadded base64 where length % 4 == 3 (not accepted by this validator)', () => {
257
+ // This validator requires explicit padding or length % 4 == 0.
258
+ // 'SGVsbG8' has 7 chars, 7 % 4 == 3. Since paddingCount=0, mod(3) != 4-0(4), so it throws.
259
+ expect(() => validateBase64String(VALID_BASE64_NOPAD, 's')).toThrow(WERR_INVALID_PARAMETER)
260
+ })
261
+
262
+ it('accepts all valid base64 characters (A-Z, a-z, 0-9, +, /)', () => {
263
+ const valid = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
264
+ // length 64 → multiple of 4, no padding needed
265
+ expect(() => validateBase64String(valid, 's')).not.toThrow()
266
+ })
267
+ })
268
+
269
+ // ============================================================================
270
+ // isHexString
271
+ // ============================================================================
272
+
273
+ describe('isHexString', () => {
274
+ it('returns true for a valid lowercase hex string', () => {
275
+ expect(isHexString('deadbeef')).toBe(true)
276
+ })
277
+
278
+ it('returns true for a valid uppercase hex string', () => {
279
+ expect(isHexString('DEADBEEF')).toBe(true)
280
+ })
281
+
282
+ it('returns false for an odd-length string', () => {
283
+ expect(isHexString('abc')).toBe(false)
284
+ })
285
+
286
+ it('returns false for a string with non-hex characters', () => {
287
+ expect(isHexString('gg')).toBe(false)
288
+ })
289
+
290
+ it('trims whitespace before checking', () => {
291
+ expect(isHexString(' deadbeef ')).toBe(true)
292
+ })
293
+ })
294
+
295
+ // ============================================================================
296
+ // validateOutpointString / validateOptionalOutpointString
297
+ // ============================================================================
298
+
299
+ describe('validateOutpointString', () => {
300
+ it('returns "txid.vout" for a valid outpoint', () => {
301
+ const result = validateOutpointString(VALID_OUTPOINT, 'output')
302
+ expect(result).toContain('.')
303
+ const [txid, vout] = result.split('.')
304
+ expect(txid).toHaveLength(64)
305
+ expect(Number(vout)).toBe(0)
306
+ })
307
+
308
+ it('throws when there is no dot separator', () => {
309
+ expect(() => validateOutpointString('notanoutpoint', 'output')).toThrow(WERR_INVALID_PARAMETER)
310
+ })
311
+
312
+ it('throws when vout is not numeric', () => {
313
+ expect(() => validateOutpointString(`${VALID_TXID}.abc`, 'output')).toThrow(WERR_INVALID_PARAMETER)
314
+ })
315
+
316
+ it('throws when txid is not valid hex', () => {
317
+ expect(() => validateOutpointString('zzzzzzzz.0', 'output')).toThrow(WERR_INVALID_PARAMETER)
318
+ })
319
+
320
+ it('throws when vout is negative', () => {
321
+ expect(() => validateOutpointString(`${VALID_TXID}.-1`, 'output')).toThrow(WERR_INVALID_PARAMETER)
322
+ })
323
+ })
324
+
325
+ describe('validateOptionalOutpointString', () => {
326
+ it('returns undefined for undefined input', () => {
327
+ expect(validateOptionalOutpointString(undefined, 'output')).toBeUndefined()
328
+ })
329
+
330
+ it('validates when a value is provided', () => {
331
+ expect(validateOptionalOutpointString(VALID_OUTPOINT, 'output')).toBeDefined()
332
+ })
333
+ })
334
+
335
+ // ============================================================================
336
+ // validateCreateActionInput
337
+ // ============================================================================
338
+
339
+ describe('validateCreateActionInput', () => {
340
+ const validBase = {
341
+ outpoint: VALID_OUTPOINT,
342
+ inputDescription: 'A valid input description',
343
+ sequenceNumber: 0xffffffff
344
+ }
345
+
346
+ it('accepts input with unlockingScript only', () => {
347
+ const result = validateCreateActionInput({
348
+ ...validBase,
349
+ unlockingScript: 'aabb' // 2 bytes (4 hex chars / 2)
350
+ })
351
+ expect(result.unlockingScriptLength).toBe(2)
352
+ expect(result.unlockingScript).toBe('aabb')
353
+ })
354
+
355
+ it('accepts input with unlockingScriptLength only', () => {
356
+ const result = validateCreateActionInput({
357
+ ...validBase,
358
+ unlockingScriptLength: 107
359
+ })
360
+ expect(result.unlockingScriptLength).toBe(107)
361
+ expect(result.unlockingScript).toBeUndefined()
362
+ })
363
+
364
+ it('throws when neither unlockingScript nor unlockingScriptLength is provided', () => {
365
+ expect(() =>
366
+ validateCreateActionInput({ ...validBase })
367
+ ).toThrow(WERR_INVALID_PARAMETER)
368
+ })
369
+
370
+ it('throws when unlockingScriptLength does not match actual script length', () => {
371
+ expect(() =>
372
+ validateCreateActionInput({
373
+ ...validBase,
374
+ unlockingScript: 'aabb', // 1 byte
375
+ unlockingScriptLength: 5 // mismatch
376
+ })
377
+ ).toThrow(WERR_INVALID_PARAMETER)
378
+ })
379
+
380
+ it('uses default sequenceNumber 0xffffffff when not provided', () => {
381
+ const result = validateCreateActionInput({
382
+ ...validBase,
383
+ sequenceNumber: undefined as any,
384
+ unlockingScriptLength: 10
385
+ })
386
+ expect(result.sequenceNumber).toBe(0xffffffff)
387
+ })
388
+
389
+ it('throws for too-short inputDescription', () => {
390
+ expect(() =>
391
+ validateCreateActionInput({
392
+ ...validBase,
393
+ inputDescription: 'ab',
394
+ unlockingScriptLength: 10
395
+ })
396
+ ).toThrow(WERR_INVALID_PARAMETER)
397
+ })
398
+ })
399
+
400
+ // ============================================================================
401
+ // validateCreateActionOutput
402
+ // ============================================================================
403
+
404
+ describe('validateCreateActionOutput', () => {
405
+ const validBase = {
406
+ lockingScript: 'aabbcc',
407
+ satoshis: 1000,
408
+ outputDescription: 'A valid output description'
409
+ }
410
+
411
+ it('accepts a minimal valid output', () => {
412
+ const result = validateCreateActionOutput(validBase)
413
+ expect(result.satoshis).toBe(1000)
414
+ expect(result.lockingScript).toBe('aabbcc')
415
+ })
416
+
417
+ it('normalises tags via validateTag (trim + lowercase)', () => {
418
+ const result = validateCreateActionOutput({
419
+ ...validBase,
420
+ tags: [' MyTag ', 'ANOTHER']
421
+ })
422
+ expect(result.tags).toEqual(['mytag', 'another'])
423
+ })
424
+
425
+ it('accepts optional basket', () => {
426
+ const result = validateCreateActionOutput({
427
+ ...validBase,
428
+ basket: 'my-basket'
429
+ })
430
+ expect(result.basket).toBe('my-basket')
431
+ })
432
+
433
+ it('throws for invalid satoshis', () => {
434
+ expect(() =>
435
+ validateCreateActionOutput({ ...validBase, satoshis: -1 })
436
+ ).toThrow(WERR_INVALID_PARAMETER)
437
+ })
438
+
439
+ it('throws for invalid locking script (odd hex length)', () => {
440
+ expect(() =>
441
+ validateCreateActionOutput({ ...validBase, lockingScript: 'abc' })
442
+ ).toThrow(WERR_INVALID_PARAMETER)
443
+ })
444
+ })
445
+
446
+ // ============================================================================
447
+ // validateCreateActionOptions
448
+ // ============================================================================
449
+
450
+ describe('validateCreateActionOptions', () => {
451
+ it('applies all defaults when options is undefined', () => {
452
+ const v = validateCreateActionOptions(undefined)
453
+ expect(v.signAndProcess).toBe(true)
454
+ expect(v.acceptDelayedBroadcast).toBe(true)
455
+ expect(v.returnTXIDOnly).toBe(false)
456
+ expect(v.noSend).toBe(false)
457
+ expect(v.randomizeOutputs).toBe(true)
458
+ expect(v.knownTxids).toEqual([])
459
+ expect(v.sendWith).toEqual([])
460
+ expect(v.noSendChange).toEqual([])
461
+ })
462
+
463
+ it('applies all defaults when options is an empty object', () => {
464
+ const v = validateCreateActionOptions({})
465
+ expect(v.signAndProcess).toBe(true)
466
+ expect(v.randomizeOutputs).toBe(true)
467
+ })
468
+
469
+ it('preserves explicit boolean overrides', () => {
470
+ const v = validateCreateActionOptions({
471
+ signAndProcess: false,
472
+ returnTXIDOnly: true,
473
+ noSend: true,
474
+ randomizeOutputs: false
475
+ })
476
+ expect(v.signAndProcess).toBe(false)
477
+ expect(v.returnTXIDOnly).toBe(true)
478
+ expect(v.noSend).toBe(true)
479
+ expect(v.randomizeOutputs).toBe(false)
480
+ })
481
+
482
+ it('parses noSendChange outpoint strings', () => {
483
+ const v = validateCreateActionOptions({
484
+ noSendChange: [VALID_OUTPOINT]
485
+ })
486
+ expect(v.noSendChange).toHaveLength(1)
487
+ expect(v.noSendChange[0].txid).toHaveLength(64)
488
+ })
489
+ })
490
+
491
+ // ============================================================================
492
+ // validateCreateActionArgs
493
+ // ============================================================================
494
+
495
+ describe('validateCreateActionArgs', () => {
496
+ const minimalArgs = {
497
+ description: 'A valid action description'
498
+ }
499
+
500
+ it('validates a minimal args object with just a description', () => {
501
+ const v = validateCreateActionArgs(minimalArgs as any)
502
+ expect(v.description).toBe('A valid action description')
503
+ expect(v.inputs).toEqual([])
504
+ expect(v.outputs).toEqual([])
505
+ })
506
+
507
+ it('sets isRemixChange = true when no inputs and no outputs and no sendWith', () => {
508
+ const v = validateCreateActionArgs(minimalArgs as any)
509
+ expect(v.isRemixChange).toBe(true)
510
+ expect(v.isNewTx).toBe(true)
511
+ })
512
+
513
+ it('sets isSignAction = true when input lacks an unlockingScript', () => {
514
+ const v = validateCreateActionArgs({
515
+ ...minimalArgs,
516
+ inputs: [
517
+ {
518
+ outpoint: VALID_OUTPOINT,
519
+ inputDescription: 'An input with no unlock',
520
+ unlockingScriptLength: 107
521
+ }
522
+ ]
523
+ } as any)
524
+ expect(v.isSignAction).toBe(true)
525
+ })
526
+
527
+ it('sets isSignAction = false when all inputs have compiled unlocking scripts and signAndProcess = true', () => {
528
+ const v = validateCreateActionArgs({
529
+ ...minimalArgs,
530
+ inputs: [
531
+ {
532
+ outpoint: VALID_OUTPOINT,
533
+ inputDescription: 'An input with unlock',
534
+ unlockingScript: 'aabb',
535
+ unlockingScriptLength: 2
536
+ }
537
+ ],
538
+ options: { signAndProcess: true }
539
+ } as any)
540
+ expect(v.isSignAction).toBe(false)
541
+ })
542
+
543
+ it('sets isTestWerrReviewActions when the specOp label is present', () => {
544
+ const v = validateCreateActionArgs({
545
+ ...minimalArgs,
546
+ labels: [specOpThrowReviewActions]
547
+ } as any)
548
+ expect(v.isTestWerrReviewActions).toBe(true)
549
+ })
550
+
551
+ it('throws for a description that is too short', () => {
552
+ expect(() =>
553
+ validateCreateActionArgs({ description: 'hi' } as any)
554
+ ).toThrow(WERR_INVALID_PARAMETER)
555
+ })
556
+
557
+ it('validates labels via validateLabel (trim + lowercase)', () => {
558
+ const v = validateCreateActionArgs({
559
+ ...minimalArgs,
560
+ labels: [' MyLabel ']
561
+ } as any)
562
+ expect(v.labels).toEqual(['mylabel'])
563
+ })
564
+ })
565
+
566
+ // ============================================================================
567
+ // validateSignActionOptions
568
+ // ============================================================================
569
+
570
+ describe('validateSignActionOptions', () => {
571
+ it('applies defaults when options is undefined', () => {
572
+ const v = validateSignActionOptions(undefined)
573
+ expect(v.acceptDelayedBroadcast).toBe(true)
574
+ expect(v.returnTXIDOnly).toBe(false)
575
+ expect(v.noSend).toBe(false)
576
+ expect(v.sendWith).toEqual([])
577
+ })
578
+
579
+ it('preserves explicit values', () => {
580
+ const v = validateSignActionOptions({ noSend: true, returnTXIDOnly: true })
581
+ expect(v.noSend).toBe(true)
582
+ expect(v.returnTXIDOnly).toBe(true)
583
+ })
584
+ })
585
+
586
+ // ============================================================================
587
+ // validateSignActionArgs
588
+ // ============================================================================
589
+
590
+ describe('validateSignActionArgs', () => {
591
+ const minimalSignArgs = {
592
+ spends: {},
593
+ reference: VALID_BASE64
594
+ }
595
+
596
+ it('returns a valid object with flags set', () => {
597
+ const v = validateSignActionArgs(minimalSignArgs as any)
598
+ expect(v.isNewTx).toBe(true)
599
+ expect(v.isSendWith).toBe(false)
600
+ })
601
+
602
+ it('sets isSendWith when sendWith is non-empty', () => {
603
+ const v = validateSignActionArgs({
604
+ ...minimalSignArgs,
605
+ options: { sendWith: [VALID_TXID] }
606
+ } as any)
607
+ expect(v.isSendWith).toBe(true)
608
+ })
609
+ })
610
+
611
+ // ============================================================================
612
+ // validateAbortActionArgs
613
+ // ============================================================================
614
+
615
+ describe('validateAbortActionArgs', () => {
616
+ it('accepts a valid base64 reference', () => {
617
+ const v = validateAbortActionArgs({ reference: VALID_BASE64 })
618
+ expect(v.reference).toBe(VALID_BASE64)
619
+ })
620
+
621
+ it('throws for an invalid reference', () => {
622
+ expect(() =>
623
+ validateAbortActionArgs({ reference: '!invalid!' })
624
+ ).toThrow(WERR_INVALID_PARAMETER)
625
+ })
626
+ })
627
+
628
+ // ============================================================================
629
+ // validateWalletPayment
630
+ // ============================================================================
631
+
632
+ describe('validateWalletPayment', () => {
633
+ it('returns undefined when called with undefined', () => {
634
+ expect(validateWalletPayment(undefined)).toBeUndefined()
635
+ })
636
+
637
+ it('validates a complete wallet payment structure', () => {
638
+ const v = validateWalletPayment({
639
+ derivationPrefix: VALID_BASE64,
640
+ derivationSuffix: VALID_BASE64,
641
+ senderIdentityKey: VALID_PUBKEY_HEX
642
+ })
643
+ expect(v).toBeDefined()
644
+ expect(v!.senderIdentityKey).toBe(VALID_PUBKEY_HEX.toLowerCase())
645
+ })
646
+
647
+ it('throws for an invalid derivationPrefix', () => {
648
+ expect(() =>
649
+ validateWalletPayment({
650
+ derivationPrefix: '!!!',
651
+ derivationSuffix: VALID_BASE64,
652
+ senderIdentityKey: VALID_PUBKEY_HEX
653
+ })
654
+ ).toThrow(WERR_INVALID_PARAMETER)
655
+ })
656
+ })
657
+
658
+ // ============================================================================
659
+ // validateBasketInsertion
660
+ // ============================================================================
661
+
662
+ describe('validateBasketInsertion', () => {
663
+ it('returns undefined when called with undefined', () => {
664
+ expect(validateBasketInsertion(undefined)).toBeUndefined()
665
+ })
666
+
667
+ it('validates a basket insertion with basket and tags', () => {
668
+ const v = validateBasketInsertion({
669
+ basket: 'my-basket',
670
+ tags: ['tag1', 'TAG2']
671
+ })
672
+ expect(v).toBeDefined()
673
+ expect(v!.basket).toBe('my-basket')
674
+ expect(v!.tags).toEqual(['tag1', 'tag2'])
675
+ })
676
+
677
+ it('throws for an empty basket name', () => {
678
+ expect(() =>
679
+ validateBasketInsertion({ basket: '' })
680
+ ).toThrow(WERR_INVALID_PARAMETER)
681
+ })
682
+ })
683
+
684
+ // ============================================================================
685
+ // validateInternalizeOutput
686
+ // ============================================================================
687
+
688
+ describe('validateInternalizeOutput', () => {
689
+ it('accepts a "wallet payment" output', () => {
690
+ const v = validateInternalizeOutput({
691
+ outputIndex: 0,
692
+ protocol: 'wallet payment',
693
+ paymentRemittance: {
694
+ derivationPrefix: VALID_BASE64,
695
+ derivationSuffix: VALID_BASE64,
696
+ senderIdentityKey: VALID_PUBKEY_HEX
697
+ }
698
+ })
699
+ expect(v.protocol).toBe('wallet payment')
700
+ expect(v.outputIndex).toBe(0)
701
+ })
702
+
703
+ it('accepts a "basket insertion" output', () => {
704
+ const v = validateInternalizeOutput({
705
+ outputIndex: 1,
706
+ protocol: 'basket insertion',
707
+ insertionRemittance: { basket: 'default' }
708
+ })
709
+ expect(v.protocol).toBe('basket insertion')
710
+ })
711
+
712
+ it('throws for an unknown protocol', () => {
713
+ expect(() =>
714
+ validateInternalizeOutput({
715
+ outputIndex: 0,
716
+ protocol: 'unknown' as any
717
+ })
718
+ ).toThrow(WERR_INVALID_PARAMETER)
719
+ })
720
+
721
+ it('throws for a negative outputIndex', () => {
722
+ expect(() =>
723
+ validateInternalizeOutput({
724
+ outputIndex: -1,
725
+ protocol: 'basket insertion'
726
+ })
727
+ ).toThrow(WERR_INVALID_PARAMETER)
728
+ })
729
+ })
730
+
731
+ // ============================================================================
732
+ // validateOriginator
733
+ // ============================================================================
734
+
735
+ describe('validateOriginator', () => {
736
+ it('returns undefined for undefined input', () => {
737
+ expect(validateOriginator(undefined)).toBeUndefined()
738
+ })
739
+
740
+ it('normalises to lowercase and trims whitespace', () => {
741
+ expect(validateOriginator(' Example.COM ')).toBe('example.com')
742
+ })
743
+
744
+ it('accepts a simple domain name', () => {
745
+ expect(validateOriginator('example.com')).toBe('example.com')
746
+ })
747
+
748
+ it('throws for an empty originator after trimming', () => {
749
+ expect(() => validateOriginator(' ')).toThrow(WERR_INVALID_PARAMETER)
750
+ })
751
+
752
+ it('throws for a domain part exceeding 63 bytes', () => {
753
+ const longPart = 'a'.repeat(64)
754
+ expect(() => validateOriginator(`${longPart}.com`)).toThrow(WERR_INVALID_PARAMETER)
755
+ })
756
+
757
+ it('throws for an originator exceeding 250 total bytes', () => {
758
+ const longOriginator = 'a'.repeat(251)
759
+ expect(() => validateOriginator(longOriginator)).toThrow(WERR_INVALID_PARAMETER)
760
+ })
761
+ })
762
+
763
+ // ============================================================================
764
+ // validateRelinquishOutputArgs
765
+ // ============================================================================
766
+
767
+ describe('validateRelinquishOutputArgs', () => {
768
+ it('validates a complete set of args', () => {
769
+ const v = validateRelinquishOutputArgs({
770
+ basket: 'default',
771
+ output: VALID_OUTPOINT
772
+ })
773
+ expect(v.basket).toBe('default')
774
+ expect(v.output).toContain('.')
775
+ })
776
+
777
+ it('throws for invalid basket', () => {
778
+ expect(() =>
779
+ validateRelinquishOutputArgs({ basket: '', output: VALID_OUTPOINT })
780
+ ).toThrow(WERR_INVALID_PARAMETER)
781
+ })
782
+ })
783
+
784
+ // ============================================================================
785
+ // validateRelinquishCertificateArgs
786
+ // ============================================================================
787
+
788
+ describe('validateRelinquishCertificateArgs', () => {
789
+ it('validates a valid certificate reference', () => {
790
+ const v = validateRelinquishCertificateArgs({
791
+ type: VALID_BASE64,
792
+ serialNumber: VALID_BASE64,
793
+ certifier: VALID_PUBKEY_HEX
794
+ })
795
+ expect(v.type).toBe(VALID_BASE64)
796
+ })
797
+
798
+ it('throws for an invalid type', () => {
799
+ expect(() =>
800
+ validateRelinquishCertificateArgs({
801
+ type: '!!!',
802
+ serialNumber: VALID_BASE64,
803
+ certifier: VALID_PUBKEY_HEX
804
+ })
805
+ ).toThrow(WERR_INVALID_PARAMETER)
806
+ })
807
+ })
808
+
809
+ // ============================================================================
810
+ // validateListCertificatesArgs
811
+ // ============================================================================
812
+
813
+ describe('validateListCertificatesArgs', () => {
814
+ const validArgs = {
815
+ certifiers: [VALID_PUBKEY_HEX],
816
+ types: [VALID_BASE64],
817
+ limit: 10,
818
+ offset: 0
819
+ }
820
+
821
+ it('validates a minimal set of args with defaults', () => {
822
+ const v = validateListCertificatesArgs(validArgs as any)
823
+ expect(v.limit).toBe(10)
824
+ expect(v.offset).toBe(0)
825
+ expect(v.privileged).toBe(false)
826
+ })
827
+
828
+ it('applies default limit of 10 when limit is undefined', () => {
829
+ const v = validateListCertificatesArgs({ ...validArgs, limit: undefined } as any)
830
+ expect(v.limit).toBe(10)
831
+ })
832
+
833
+ it('throws when limit exceeds 10000', () => {
834
+ expect(() =>
835
+ validateListCertificatesArgs({ ...validArgs, limit: 10001 } as any)
836
+ ).toThrow(WERR_INVALID_PARAMETER)
837
+ })
838
+
839
+ it('throws when limit is below 1', () => {
840
+ expect(() =>
841
+ validateListCertificatesArgs({ ...validArgs, limit: 0 } as any)
842
+ ).toThrow(WERR_INVALID_PARAMETER)
843
+ })
844
+ })
845
+
846
+ // ============================================================================
847
+ // validateAcquireIssuanceCertificateArgs
848
+ // ============================================================================
849
+
850
+ describe('validateAcquireIssuanceCertificateArgs', () => {
851
+ const validIssuanceArgs: any = {
852
+ acquisitionProtocol: 'issuance',
853
+ type: VALID_BASE64,
854
+ certifier: VALID_PUBKEY_HEX,
855
+ certifierUrl: 'https://example.com/certify',
856
+ fields: { name: 'Alice' },
857
+ privileged: false
858
+ }
859
+
860
+ it('validates a valid issuance request', () => {
861
+ const v = validateAcquireIssuanceCertificateArgs(validIssuanceArgs)
862
+ expect(v.certifierUrl).toBe('https://example.com/certify')
863
+ expect(v.subject).toBe('')
864
+ })
865
+
866
+ it('throws when acquisitionProtocol is not "issuance"', () => {
867
+ expect(() =>
868
+ validateAcquireIssuanceCertificateArgs({ ...validIssuanceArgs, acquisitionProtocol: 'direct' })
869
+ ).toThrow('Only acquire certificate via issuance requests allowed here.')
870
+ })
871
+
872
+ it('throws when serialNumber is present (not valid for issuance)', () => {
873
+ expect(() =>
874
+ validateAcquireIssuanceCertificateArgs({ ...validIssuanceArgs, serialNumber: VALID_BASE64 })
875
+ ).toThrow(WERR_INVALID_PARAMETER)
876
+ })
877
+
878
+ it('throws when signature is present', () => {
879
+ expect(() =>
880
+ validateAcquireIssuanceCertificateArgs({ ...validIssuanceArgs, signature: VALID_PUBKEY_HEX })
881
+ ).toThrow(WERR_INVALID_PARAMETER)
882
+ })
883
+
884
+ it('throws when revocationOutpoint is present', () => {
885
+ expect(() =>
886
+ validateAcquireIssuanceCertificateArgs({ ...validIssuanceArgs, revocationOutpoint: VALID_OUTPOINT })
887
+ ).toThrow(WERR_INVALID_PARAMETER)
888
+ })
889
+
890
+ it('throws when keyringRevealer is present', () => {
891
+ expect(() =>
892
+ validateAcquireIssuanceCertificateArgs({ ...validIssuanceArgs, keyringRevealer: 'certifier' })
893
+ ).toThrow(WERR_INVALID_PARAMETER)
894
+ })
895
+
896
+ it('throws when keyringForSubject is present', () => {
897
+ expect(() =>
898
+ validateAcquireIssuanceCertificateArgs({ ...validIssuanceArgs, keyringForSubject: {} })
899
+ ).toThrow(WERR_INVALID_PARAMETER)
900
+ })
901
+
902
+ it('throws when certifierUrl is missing', () => {
903
+ expect(() =>
904
+ validateAcquireIssuanceCertificateArgs({ ...validIssuanceArgs, certifierUrl: undefined })
905
+ ).toThrow(WERR_INVALID_PARAMETER)
906
+ })
907
+
908
+ it('throws when privileged is true but privilegedReason is absent', () => {
909
+ expect(() =>
910
+ validateAcquireIssuanceCertificateArgs({ ...validIssuanceArgs, privileged: true })
911
+ ).toThrow(WERR_INVALID_PARAMETER)
912
+ })
913
+
914
+ it('accepts privileged=true with a valid privilegedReason', () => {
915
+ const v = validateAcquireIssuanceCertificateArgs({
916
+ ...validIssuanceArgs,
917
+ privileged: true,
918
+ privilegedReason: 'A valid reason'
919
+ })
920
+ expect(v.privileged).toBe(true)
921
+ })
922
+ })
923
+
924
+ // ============================================================================
925
+ // validateAcquireDirectCertificateArgs
926
+ // ============================================================================
927
+
928
+ describe('validateAcquireDirectCertificateArgs', () => {
929
+ const validDirectArgs: any = {
930
+ acquisitionProtocol: 'direct',
931
+ type: VALID_BASE64,
932
+ serialNumber: VALID_BASE64,
933
+ certifier: VALID_PUBKEY_HEX,
934
+ revocationOutpoint: VALID_OUTPOINT,
935
+ fields: { name: 'Bob' },
936
+ signature: VALID_PUBKEY_HEX,
937
+ keyringRevealer: 'certifier',
938
+ keyringForSubject: { fieldA: VALID_BASE64 },
939
+ privileged: false
940
+ }
941
+
942
+ it('validates a valid direct acquisition request', () => {
943
+ const v = validateAcquireDirectCertificateArgs(validDirectArgs)
944
+ expect(v.subject).toBe('')
945
+ expect(v.keyringRevealer).toBe('certifier')
946
+ })
947
+
948
+ it('throws when acquisitionProtocol is not "direct"', () => {
949
+ expect(() =>
950
+ validateAcquireDirectCertificateArgs({ ...validDirectArgs, acquisitionProtocol: 'issuance' })
951
+ ).toThrow('Only acquire direct certificate requests allowed here.')
952
+ })
953
+
954
+ it('throws when serialNumber is missing', () => {
955
+ expect(() =>
956
+ validateAcquireDirectCertificateArgs({ ...validDirectArgs, serialNumber: undefined })
957
+ ).toThrow(WERR_INVALID_PARAMETER)
958
+ })
959
+
960
+ it('throws when signature is missing', () => {
961
+ expect(() =>
962
+ validateAcquireDirectCertificateArgs({ ...validDirectArgs, signature: undefined })
963
+ ).toThrow(WERR_INVALID_PARAMETER)
964
+ })
965
+
966
+ it('throws when revocationOutpoint is missing', () => {
967
+ expect(() =>
968
+ validateAcquireDirectCertificateArgs({ ...validDirectArgs, revocationOutpoint: undefined })
969
+ ).toThrow(WERR_INVALID_PARAMETER)
970
+ })
971
+
972
+ it('throws when keyringRevealer is missing', () => {
973
+ expect(() =>
974
+ validateAcquireDirectCertificateArgs({ ...validDirectArgs, keyringRevealer: undefined })
975
+ ).toThrow(WERR_INVALID_PARAMETER)
976
+ })
977
+
978
+ it('throws when keyringForSubject is null/undefined', () => {
979
+ expect(() =>
980
+ validateAcquireDirectCertificateArgs({ ...validDirectArgs, keyringForSubject: undefined })
981
+ ).toThrow(WERR_INVALID_PARAMETER)
982
+ })
983
+
984
+ it('throws when privileged is true but privilegedReason is absent', () => {
985
+ expect(() =>
986
+ validateAcquireDirectCertificateArgs({ ...validDirectArgs, privileged: true, privilegedReason: undefined })
987
+ ).toThrow(WERR_INVALID_PARAMETER)
988
+ })
989
+
990
+ it('validates a keyringRevealer hex string (non-"certifier")', () => {
991
+ const v = validateAcquireDirectCertificateArgs({
992
+ ...validDirectArgs,
993
+ keyringRevealer: VALID_PUBKEY_HEX
994
+ })
995
+ expect(v.keyringRevealer).toBe(VALID_PUBKEY_HEX.toLowerCase())
996
+ })
997
+ })
998
+
999
+ // ============================================================================
1000
+ // validateProveCertificateArgs
1001
+ // ============================================================================
1002
+
1003
+ describe('validateProveCertificateArgs', () => {
1004
+ const validArgs: any = {
1005
+ certificate: {
1006
+ type: VALID_BASE64,
1007
+ serialNumber: VALID_BASE64,
1008
+ certifier: VALID_PUBKEY_HEX,
1009
+ subject: VALID_PUBKEY_HEX
1010
+ },
1011
+ fieldsToReveal: ['fieldA', 'fieldB'],
1012
+ verifier: VALID_PUBKEY_HEX,
1013
+ privileged: false
1014
+ }
1015
+
1016
+ it('validates a complete prove certificate request', () => {
1017
+ const v = validateProveCertificateArgs(validArgs)
1018
+ expect(v.verifier).toBeDefined()
1019
+ expect(v.fieldsToReveal).toEqual(['fieldA', 'fieldB'])
1020
+ expect(v.privileged).toBe(false)
1021
+ })
1022
+
1023
+ it('throws when privileged is true but privilegedReason is absent', () => {
1024
+ expect(() =>
1025
+ validateProveCertificateArgs({ ...validArgs, privileged: true })
1026
+ ).toThrow(WERR_INVALID_PARAMETER)
1027
+ })
1028
+
1029
+ it('accepts privileged=true with a valid reason', () => {
1030
+ const v = validateProveCertificateArgs({
1031
+ ...validArgs,
1032
+ privileged: true,
1033
+ privilegedReason: 'A good reason'
1034
+ })
1035
+ expect(v.privileged).toBe(true)
1036
+ })
1037
+
1038
+ it('handles undefined optional certificate fields', () => {
1039
+ const v = validateProveCertificateArgs({
1040
+ ...validArgs,
1041
+ certificate: {}
1042
+ })
1043
+ expect(v.type).toBeUndefined()
1044
+ expect(v.certifier).toBeUndefined()
1045
+ })
1046
+ })
1047
+
1048
+ // ============================================================================
1049
+ // validateDiscoverByIdentityKeyArgs
1050
+ // ============================================================================
1051
+
1052
+ describe('validateDiscoverByIdentityKeyArgs', () => {
1053
+ const validArgs: any = {
1054
+ identityKey: VALID_PUBKEY_HEX,
1055
+ limit: 10,
1056
+ offset: 0
1057
+ }
1058
+
1059
+ it('validates a valid request', () => {
1060
+ const v = validateDiscoverByIdentityKeyArgs(validArgs)
1061
+ expect(v.identityKey).toBe(VALID_PUBKEY_HEX.toLowerCase())
1062
+ expect(v.seekPermission).toBe(false)
1063
+ })
1064
+
1065
+ it('applies default limit of 10', () => {
1066
+ const v = validateDiscoverByIdentityKeyArgs({ ...validArgs, limit: undefined })
1067
+ expect(v.limit).toBe(10)
1068
+ })
1069
+
1070
+ it('throws for identity key that is not 66 chars', () => {
1071
+ expect(() =>
1072
+ validateDiscoverByIdentityKeyArgs({ ...validArgs, identityKey: '0234' })
1073
+ ).toThrow(WERR_INVALID_PARAMETER)
1074
+ })
1075
+ })
1076
+
1077
+ // ============================================================================
1078
+ // validateDiscoverByAttributesArgs
1079
+ // ============================================================================
1080
+
1081
+ describe('validateDiscoverByAttributesArgs', () => {
1082
+ const validArgs: any = {
1083
+ attributes: { name: 'Alice' },
1084
+ limit: 10,
1085
+ offset: 0
1086
+ }
1087
+
1088
+ it('validates a valid request', () => {
1089
+ const v = validateDiscoverByAttributesArgs(validArgs)
1090
+ expect(v.attributes).toEqual({ name: 'Alice' })
1091
+ expect(v.seekPermission).toBe(false)
1092
+ })
1093
+
1094
+ it('applies default limit of 10', () => {
1095
+ const v = validateDiscoverByAttributesArgs({ ...validArgs, limit: undefined })
1096
+ expect(v.limit).toBe(10)
1097
+ })
1098
+
1099
+ it('throws for a field name that is too long', () => {
1100
+ const longName = 'a'.repeat(51)
1101
+ expect(() =>
1102
+ validateDiscoverByAttributesArgs({ ...validArgs, attributes: { [longName]: 'value' } })
1103
+ ).toThrow(WERR_INVALID_PARAMETER)
1104
+ })
1105
+ })
1106
+
1107
+ // ============================================================================
1108
+ // validateListOutputsArgs
1109
+ // ============================================================================
1110
+
1111
+ describe('validateListOutputsArgs', () => {
1112
+ const validArgs: any = {
1113
+ basket: 'default',
1114
+ limit: 10,
1115
+ offset: 0
1116
+ }
1117
+
1118
+ it('validates minimal args', () => {
1119
+ const v = validateListOutputsArgs(validArgs)
1120
+ expect(v.basket).toBe('default')
1121
+ expect(v.tagQueryMode).toBe('any')
1122
+ expect(v.includeLockingScripts).toBe(false)
1123
+ expect(v.includeTransactions).toBe(false)
1124
+ })
1125
+
1126
+ it('sets includeLockingScripts = true when include = "locking scripts"', () => {
1127
+ const v = validateListOutputsArgs({ ...validArgs, include: 'locking scripts' })
1128
+ expect(v.includeLockingScripts).toBe(true)
1129
+ expect(v.includeTransactions).toBe(false)
1130
+ })
1131
+
1132
+ it('sets includeTransactions = true when include = "entire transactions"', () => {
1133
+ const v = validateListOutputsArgs({ ...validArgs, include: 'entire transactions' })
1134
+ expect(v.includeTransactions).toBe(true)
1135
+ expect(v.includeLockingScripts).toBe(false)
1136
+ })
1137
+
1138
+ it('accepts tagQueryMode "all"', () => {
1139
+ const v = validateListOutputsArgs({ ...validArgs, tagQueryMode: 'all' })
1140
+ expect(v.tagQueryMode).toBe('all')
1141
+ })
1142
+
1143
+ it('throws for invalid tagQueryMode', () => {
1144
+ expect(() =>
1145
+ validateListOutputsArgs({ ...validArgs, tagQueryMode: 'none' })
1146
+ ).toThrow(WERR_INVALID_PARAMETER)
1147
+ })
1148
+
1149
+ it('applies default limit', () => {
1150
+ const v = validateListOutputsArgs({ ...validArgs, limit: undefined })
1151
+ expect(v.limit).toBe(10)
1152
+ })
1153
+ })
1154
+
1155
+ // ============================================================================
1156
+ // validateListActionsArgs
1157
+ // ============================================================================
1158
+
1159
+ describe('validateListActionsArgs', () => {
1160
+ const validArgs: any = {
1161
+ labels: ['my-label'],
1162
+ limit: 10,
1163
+ offset: 0
1164
+ }
1165
+
1166
+ it('validates minimal args', () => {
1167
+ const v = validateListActionsArgs(validArgs)
1168
+ expect(v.labels).toEqual(['my-label'])
1169
+ expect(v.labelQueryMode).toBe('any')
1170
+ expect(v.includeInputs).toBe(false)
1171
+ expect(v.includeOutputs).toBe(false)
1172
+ })
1173
+
1174
+ it('accepts labelQueryMode "all"', () => {
1175
+ const v = validateListActionsArgs({ ...validArgs, labelQueryMode: 'all' })
1176
+ expect(v.labelQueryMode).toBe('all')
1177
+ })
1178
+
1179
+ it('throws for invalid labelQueryMode', () => {
1180
+ expect(() =>
1181
+ validateListActionsArgs({ ...validArgs, labelQueryMode: 'none' })
1182
+ ).toThrow(WERR_INVALID_PARAMETER)
1183
+ })
1184
+
1185
+ it('applies default limit of 10', () => {
1186
+ const v = validateListActionsArgs({ ...validArgs, limit: undefined })
1187
+ expect(v.limit).toBe(10)
1188
+ })
1189
+
1190
+ it('applies boolean include flags', () => {
1191
+ const v = validateListActionsArgs({
1192
+ ...validArgs,
1193
+ includeInputs: true,
1194
+ includeOutputs: true,
1195
+ includeLabels: true,
1196
+ includeInputSourceLockingScripts: true,
1197
+ includeInputUnlockingScripts: true,
1198
+ includeOutputLockingScripts: true
1199
+ })
1200
+ expect(v.includeInputs).toBe(true)
1201
+ expect(v.includeOutputs).toBe(true)
1202
+ expect(v.includeLabels).toBe(true)
1203
+ expect(v.includeInputSourceLockingScripts).toBe(true)
1204
+ expect(v.includeInputUnlockingScripts).toBe(true)
1205
+ expect(v.includeOutputLockingScripts).toBe(true)
1206
+ })
1207
+ })
1208
+
1209
+ // ============================================================================
1210
+ // specOpThrowReviewActions constant
1211
+ // ============================================================================
1212
+
1213
+ describe('specOpThrowReviewActions', () => {
1214
+ it('is a non-empty string constant', () => {
1215
+ expect(typeof specOpThrowReviewActions).toBe('string')
1216
+ expect(specOpThrowReviewActions.length).toBeGreaterThan(0)
1217
+ })
1218
+ })