@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,383 @@
1
+ import PublicKey from '../../primitives/PublicKey'
2
+ import PrivateKey from '../../primitives/PrivateKey'
3
+ import BigNumber from '../../primitives/BigNumber'
4
+ import Signature from '../../primitives/Signature'
5
+ import { sha256 } from '../../primitives/Hash'
6
+
7
+ /**
8
+ * Additional tests for PublicKey.ts
9
+ *
10
+ * The existing PublicKey.test.ts covers:
11
+ * - fromPrivateKey
12
+ * - fromString / fromDER round-trip
13
+ * - constructor DER-string guard
14
+ * - deriveSharedSecret (valid and invalid)
15
+ * - verify (valid signature)
16
+ * - toDER returning number[] and hex string
17
+ * - deriveChild (BRC-42 vectors)
18
+ *
19
+ * The methods/branches below are NOT yet exercised:
20
+ * - toDER with enc='hex' explicitly
21
+ * - toHash returning number[] and hex string
22
+ * - toAddress with all prefix variants ('mainnet', 'main', 'testnet', 'test', array, invalid)
23
+ * - fromMsgHashAndCompactSignature (happy-path and all error paths)
24
+ * - constructor with a Point instance (x instanceof Point branch)
25
+ * - constructor with explicit isRed = false (skips the DER-string guard)
26
+ * - verify with 'hex' encoding
27
+ * - deriveChild with cache functions (both retrieve-hit and retrieve-miss paths)
28
+ */
29
+
30
+ // ---------------------------------------------------------------------------
31
+ // Fixed deterministic key pair used throughout these tests
32
+ // ---------------------------------------------------------------------------
33
+ const PRIV_HEX = 'f97c89aaacf0cd2e47ddbacc97dae1f88bec49106ac37716c451dcdd008a581b'
34
+ const privateKey = PrivateKey.fromString(PRIV_HEX, 'hex')
35
+ const publicKey = PublicKey.fromPrivateKey(privateKey)
36
+
37
+ describe('PublicKey – additional coverage', () => {
38
+ // -------------------------------------------------------------------------
39
+ // toDER
40
+ // -------------------------------------------------------------------------
41
+ describe('toDER', () => {
42
+ it('returns a 66-char hex string when enc is "hex"', () => {
43
+ const hex = publicKey.toDER('hex')
44
+ expect(typeof hex).toBe('string')
45
+ expect((hex as string).length).toBe(66)
46
+ // Compressed keys start with 02 or 03
47
+ expect((hex as string)).toMatch(/^0[23][0-9a-f]{64}$/)
48
+ })
49
+
50
+ it('returns a 33-byte number array when enc is undefined', () => {
51
+ const bytes = publicKey.toDER()
52
+ expect(Array.isArray(bytes)).toBe(true)
53
+ expect((bytes as number[]).length).toBe(33)
54
+ })
55
+
56
+ it('toDER hex and toDER array encode the same key', () => {
57
+ const hex = publicKey.toDER('hex') as string
58
+ const arr = publicKey.toDER() as number[]
59
+ const arrFromHex = Buffer.from(hex, 'hex')
60
+ expect(Array.from(arrFromHex)).toEqual(arr)
61
+ })
62
+ })
63
+
64
+ // -------------------------------------------------------------------------
65
+ // fromDER (number array → PublicKey)
66
+ // -------------------------------------------------------------------------
67
+ describe('fromDER', () => {
68
+ it('creates a PublicKey from a DER byte array', () => {
69
+ const derBytes = publicKey.toDER() as number[]
70
+ const recovered = PublicKey.fromDER(derBytes)
71
+ expect(recovered).toBeInstanceOf(PublicKey)
72
+ expect(recovered.toString()).toBe(publicKey.toString())
73
+ })
74
+
75
+ it('round-trips through toDER → fromDER correctly', () => {
76
+ const original = PublicKey.fromPrivateKey(PrivateKey.fromRandom())
77
+ const bytes = original.toDER() as number[]
78
+ const restored = PublicKey.fromDER(bytes)
79
+ expect(restored.toString()).toBe(original.toString())
80
+ })
81
+ })
82
+
83
+ // -------------------------------------------------------------------------
84
+ // toHash
85
+ // -------------------------------------------------------------------------
86
+ describe('toHash', () => {
87
+ it('returns a non-empty number array by default', () => {
88
+ const hash = publicKey.toHash()
89
+ expect(Array.isArray(hash)).toBe(true)
90
+ expect((hash as number[]).length).toBeGreaterThan(0)
91
+ })
92
+
93
+ it('returns a hex string when enc is "hex"', () => {
94
+ const hex = publicKey.toHash('hex')
95
+ expect(typeof hex).toBe('string')
96
+ // hash160 = 20 bytes = 40 hex chars
97
+ expect((hex as string).length).toBe(40)
98
+ expect((hex as string)).toMatch(/^[0-9a-f]{40}$/)
99
+ })
100
+
101
+ it('toHash() and toHash("hex") represent the same bytes', () => {
102
+ const arr = publicKey.toHash() as number[]
103
+ const hex = publicKey.toHash('hex') as string
104
+ expect(Buffer.from(arr).toString('hex')).toBe(hex)
105
+ })
106
+
107
+ it('different keys produce different hashes', () => {
108
+ const other = PublicKey.fromPrivateKey(PrivateKey.fromRandom())
109
+ expect(publicKey.toHash('hex')).not.toBe(other.toHash('hex'))
110
+ })
111
+ })
112
+
113
+ // -------------------------------------------------------------------------
114
+ // toAddress
115
+ // -------------------------------------------------------------------------
116
+ describe('toAddress', () => {
117
+ it('returns a string when called with no arguments (mainnet default)', () => {
118
+ const addr = publicKey.toAddress()
119
+ expect(typeof addr).toBe('string')
120
+ expect(addr.length).toBeGreaterThan(0)
121
+ })
122
+
123
+ it('accepts "mainnet" prefix string', () => {
124
+ const addr = publicKey.toAddress('mainnet')
125
+ expect(addr).toBe(publicKey.toAddress([0x00]))
126
+ })
127
+
128
+ it('accepts "main" prefix string (alias for mainnet)', () => {
129
+ const addr = publicKey.toAddress('main')
130
+ expect(addr).toBe(publicKey.toAddress([0x00]))
131
+ })
132
+
133
+ it('accepts "testnet" prefix string', () => {
134
+ const addr = publicKey.toAddress('testnet')
135
+ expect(addr).toBe(publicKey.toAddress([0x6f]))
136
+ })
137
+
138
+ it('accepts "test" prefix string (alias for testnet)', () => {
139
+ const addr = publicKey.toAddress('test')
140
+ expect(addr).toBe(publicKey.toAddress([0x6f]))
141
+ })
142
+
143
+ it('accepts an explicit byte-array prefix', () => {
144
+ // P2PKH mainnet prefix
145
+ const addr = publicKey.toAddress([0x00])
146
+ expect(typeof addr).toBe('string')
147
+ })
148
+
149
+ it('mainnet and testnet addresses differ for the same key', () => {
150
+ expect(publicKey.toAddress('mainnet')).not.toBe(publicKey.toAddress('testnet'))
151
+ })
152
+
153
+ it('throws on an unrecognised string prefix', () => {
154
+ expect(() => publicKey.toAddress('regtest')).toThrow('Invalid prefix regtest')
155
+ })
156
+ })
157
+
158
+ // -------------------------------------------------------------------------
159
+ // Constructor – Point overload
160
+ // -------------------------------------------------------------------------
161
+ describe('constructor with Point argument', () => {
162
+ it('builds a PublicKey from an existing Point (uses x/y of point)', () => {
163
+ // The point from the existing key IS a Point; passing it should succeed
164
+ // without going through the string-length guard.
165
+ const copy = new PublicKey(publicKey)
166
+ expect(copy).toBeInstanceOf(PublicKey)
167
+ expect(copy.toString()).toBe(publicKey.toString())
168
+ })
169
+ })
170
+
171
+ // -------------------------------------------------------------------------
172
+ // Constructor – isRed = false (skips the DER string guard)
173
+ // -------------------------------------------------------------------------
174
+ describe('constructor with isRed = false', () => {
175
+ it('does not throw for a 66-char string when isRed is false', () => {
176
+ // With isRed=false the guard is bypassed (y is still null, but isRed is
177
+ // false, so the condition is not met).
178
+ // We test that the guard is only active when isRed is true.
179
+ const derHex = publicKey.toString() // 66 hex chars
180
+ expect(() => new PublicKey(derHex, null, false)).not.toThrow()
181
+ })
182
+ })
183
+
184
+ // -------------------------------------------------------------------------
185
+ // verify – hex-encoded message
186
+ // -------------------------------------------------------------------------
187
+ describe('verify with hex encoding', () => {
188
+ it('verifies a signature against a hex-encoded message', () => {
189
+ const msgHex = 'deadbeef'
190
+ const sig = privateKey.sign(msgHex, 'hex')
191
+ expect(publicKey.verify(msgHex, sig, 'hex')).toBe(true)
192
+ })
193
+
194
+ it('returns false for a wrong signature with hex encoding', () => {
195
+ const msgHex = 'deadbeef'
196
+ const otherKey = PrivateKey.fromRandom()
197
+ const wrongSig = otherKey.sign(msgHex, 'hex')
198
+ expect(publicKey.verify(msgHex, wrongSig, 'hex')).toBe(false)
199
+ })
200
+ })
201
+
202
+ // -------------------------------------------------------------------------
203
+ // fromMsgHashAndCompactSignature
204
+ // -------------------------------------------------------------------------
205
+ describe('fromMsgHashAndCompactSignature', () => {
206
+ /**
207
+ * Build a compact signature manually from a Signature object using the
208
+ * compact-signature convention used by the BSV SDK:
209
+ * byte 0 = 27 + recovery_param (uncompressed variants use 27-30)
210
+ * or 31 + recovery_param (compressed variants use 31-34)
211
+ * bytes 1-32 = r (big-endian, 32 bytes)
212
+ * bytes 33-64 = s (big-endian, 32 bytes)
213
+ */
214
+ function makeCompactBytes (sig: Signature, recoveryParam: number, compressed: boolean): number[] {
215
+ const compactByte = (compressed ? 31 : 27) + recoveryParam
216
+ const rBytes = sig.r.toArray('be', 32)
217
+ const sBytes = sig.s.toArray('be', 32)
218
+ return [compactByte, ...rBytes, ...sBytes]
219
+ }
220
+
221
+ it('recovers the correct public key from a compact signature', () => {
222
+ // privateKey.sign(msg, 'hex') internally does: msgHash = SHA256(msg as hex bytes)
223
+ // so we must use that same SHA256 hash as the msgHash for recovery
224
+ const msgHex = 'deadbeef'
225
+ const sig = privateKey.sign(msgHex, 'hex')
226
+ const msgHash = new BigNumber(sha256(msgHex, 'hex'), 16)
227
+ // Try all valid recovery params until we get one that produces our key
228
+ let recovered: PublicKey | null = null
229
+ for (let r = 0; r <= 3; r++) {
230
+ try {
231
+ const compact = makeCompactBytes(sig, r, true)
232
+ const candidate = PublicKey.fromMsgHashAndCompactSignature(msgHash, compact)
233
+ if (candidate.toString() === publicKey.toString()) {
234
+ recovered = candidate
235
+ break
236
+ }
237
+ } catch {
238
+ // This recovery param didn't work, try next
239
+ }
240
+ }
241
+ expect(recovered).not.toBeNull()
242
+ expect(recovered!.toString()).toBe(publicKey.toString())
243
+ })
244
+
245
+ it('throws for a signature array that is not 65 bytes', () => {
246
+ const msgHash = new BigNumber(1)
247
+ expect(() =>
248
+ PublicKey.fromMsgHashAndCompactSignature(msgHash, new Array(64).fill(0))
249
+ ).toThrow('Invalid Compact Signature')
250
+ })
251
+
252
+ it('throws for a signature array that is 66 bytes', () => {
253
+ const msgHash = new BigNumber(1)
254
+ expect(() =>
255
+ PublicKey.fromMsgHashAndCompactSignature(msgHash, new Array(66).fill(0))
256
+ ).toThrow('Invalid Compact Signature')
257
+ })
258
+
259
+ it('throws for a compact byte below the valid range (< 27)', () => {
260
+ const msgHash = new BigNumber(1)
261
+ const compact = new Array(65).fill(0)
262
+ compact[0] = 26 // just below 27
263
+ expect(() =>
264
+ PublicKey.fromMsgHashAndCompactSignature(msgHash, compact)
265
+ ).toThrow('Invalid Compact Byte')
266
+ })
267
+
268
+ it('throws for a compact byte at or above 35', () => {
269
+ const msgHash = new BigNumber(1)
270
+ const compact = new Array(65).fill(0)
271
+ compact[0] = 35 // >= 35
272
+ expect(() =>
273
+ PublicKey.fromMsgHashAndCompactSignature(msgHash, compact)
274
+ ).toThrow('Invalid Compact Byte')
275
+ })
276
+
277
+ it('handles hex-encoded compact signature string', () => {
278
+ const msgHex = 'cafebabe'
279
+ const sig = privateKey.sign(msgHex, 'hex')
280
+ const msgHash = new BigNumber(sha256(msgHex, 'hex'), 16)
281
+
282
+ for (let r = 0; r <= 3; r++) {
283
+ try {
284
+ const compactBytes = makeCompactBytes(sig, r, true)
285
+ const hexStr = Buffer.from(compactBytes).toString('hex')
286
+ const candidate = PublicKey.fromMsgHashAndCompactSignature(msgHash, hexStr, 'hex')
287
+ if (candidate.toString() === publicKey.toString()) {
288
+ expect(candidate).toBeInstanceOf(PublicKey)
289
+ return // test passed
290
+ }
291
+ } catch {
292
+ // continue trying recovery params
293
+ }
294
+ }
295
+ // If we reach here all recovery params failed to match – that's a test data issue
296
+ throw new Error('Could not find a valid recovery param for the test key pair')
297
+ })
298
+
299
+ it('handles uncompressed compact byte (27-30 range)', () => {
300
+ const msgHex = 'aabbccdd'
301
+ const sig = privateKey.sign(msgHex, 'hex')
302
+ const msgHash = new BigNumber(sha256(msgHex, 'hex'), 16)
303
+
304
+ // Try uncompressed recovery params (byte 27-30, i.e. r=0..3 without the +4)
305
+ let passed = false
306
+ for (let r = 0; r <= 3; r++) {
307
+ try {
308
+ const compact = makeCompactBytes(sig, r, false) // uncompressed variant (byte 27-30)
309
+ const candidate = PublicKey.fromMsgHashAndCompactSignature(msgHash, compact)
310
+ if (candidate.toString() === publicKey.toString()) {
311
+ passed = true
312
+ break
313
+ }
314
+ } catch {
315
+ // next
316
+ }
317
+ }
318
+ // At least one recovery param should work
319
+ expect(passed).toBe(true)
320
+ })
321
+ })
322
+
323
+ // -------------------------------------------------------------------------
324
+ // deriveChild – cache callbacks
325
+ // -------------------------------------------------------------------------
326
+ describe('deriveChild cache callbacks', () => {
327
+ const invoiceNumber = 'test-invoice-001'
328
+
329
+ it('calls cacheSharedSecret when retrieveCachedSharedSecret returns undefined', () => {
330
+ const cacheSharedSecret = jest.fn()
331
+ const retrieveCachedSharedSecret = jest.fn().mockReturnValue(undefined)
332
+
333
+ const derived = publicKey.deriveChild(
334
+ privateKey,
335
+ invoiceNumber,
336
+ cacheSharedSecret,
337
+ retrieveCachedSharedSecret
338
+ )
339
+
340
+ expect(derived).toBeInstanceOf(PublicKey)
341
+ expect(retrieveCachedSharedSecret).toHaveBeenCalledTimes(1)
342
+ expect(cacheSharedSecret).toHaveBeenCalledTimes(1)
343
+ })
344
+
345
+ it('uses cached shared secret when retrieveCachedSharedSecret returns a Point', () => {
346
+ // First derive without cache to capture the real shared secret
347
+ const realSharedSecret = publicKey.deriveSharedSecret(privateKey)
348
+
349
+ const cacheSharedSecret = jest.fn()
350
+ const retrieveCachedSharedSecret = jest.fn().mockReturnValue(realSharedSecret)
351
+
352
+ const derivedWithCache = publicKey.deriveChild(
353
+ privateKey,
354
+ invoiceNumber,
355
+ cacheSharedSecret,
356
+ retrieveCachedSharedSecret
357
+ )
358
+ const derivedWithout = publicKey.deriveChild(privateKey, invoiceNumber)
359
+
360
+ // Both derivations must produce the same child key
361
+ expect(derivedWithCache.toString()).toBe(derivedWithout.toString())
362
+ expect(retrieveCachedSharedSecret).toHaveBeenCalledTimes(1)
363
+ // cacheSharedSecret should NOT be called when a cached value is found
364
+ expect(cacheSharedSecret).not.toHaveBeenCalled()
365
+ })
366
+
367
+ it('does not call cacheSharedSecret when it is not provided (cache miss path)', () => {
368
+ // retrieveCachedSharedSecret returns undefined but no cacheSharedSecret provided
369
+ const retrieveCachedSharedSecret = jest.fn().mockReturnValue(undefined)
370
+
371
+ expect(() =>
372
+ publicKey.deriveChild(privateKey, invoiceNumber, undefined, retrieveCachedSharedSecret)
373
+ ).not.toThrow()
374
+
375
+ expect(retrieveCachedSharedSecret).toHaveBeenCalledTimes(1)
376
+ })
377
+
378
+ it('derives without any cache callbacks (direct path)', () => {
379
+ const derived = publicKey.deriveChild(privateKey, invoiceNumber)
380
+ expect(derived).toBeInstanceOf(PublicKey)
381
+ })
382
+ })
383
+ })
@@ -0,0 +1,262 @@
1
+ /* eslint-env jest */
2
+ /**
3
+ * Additional tests for src/primitives/Random.ts
4
+ *
5
+ * The existing Random.test.ts covers the happy-path via the real Node.js 18+
6
+ * globalThis.crypto path. These tests exercise the remaining branches by
7
+ * isolating the module in different synthetic environments so that the Rand
8
+ * constructor walks a different code path each time.
9
+ *
10
+ * Because the module caches its `ayn` singleton at the module level we must
11
+ * re-require a fresh copy via jest.isolateModules() for every environment
12
+ * variant.
13
+ *
14
+ * NOTE: In Node 18+, globalThis.crypto is a getter on the prototype, not an
15
+ * own property. To shadow it, we use Object.defineProperty to install an own
16
+ * property with configurable:true, then delete it in afterEach to restore.
17
+ */
18
+
19
+ /** Shadow globalThis.crypto with the given value (or undefined). */
20
+ function shadowCrypto (value: any): void {
21
+ Object.defineProperty(globalThis, 'crypto', {
22
+ value,
23
+ configurable: true,
24
+ writable: true
25
+ })
26
+ }
27
+
28
+ /** Remove the own-property shadow so the prototype getter is visible again. */
29
+ function restoreCrypto (): void {
30
+ // Only delete if we installed an own property
31
+ if (Object.prototype.hasOwnProperty.call(globalThis, 'crypto')) {
32
+ delete (globalThis as any).crypto
33
+ }
34
+ }
35
+
36
+ describe('Random – environment branches', () => {
37
+ // -------------------------------------------------------------------------
38
+ // Helpers
39
+ // -------------------------------------------------------------------------
40
+
41
+ /** Load a fresh copy of the Random module inside the current environment. */
42
+ function loadRandom (): (len: number) => number[] {
43
+ let Random: (len: number) => number[]
44
+ // isolateModules executes synchronously
45
+ jest.isolateModules(() => {
46
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
47
+ Random = require('../../primitives/Random').default
48
+ })
49
+ return Random!
50
+ }
51
+
52
+ // -------------------------------------------------------------------------
53
+ // 1. globalThis.crypto path (already covered by the main test suite but
54
+ // explicitly validated here to confirm isolateModules works).
55
+ // -------------------------------------------------------------------------
56
+ describe('globalThis.crypto path', () => {
57
+ it('produces bytes when globalThis.crypto is available', () => {
58
+ // Node 18+ exposes globalThis.crypto – do not mock, just verify the
59
+ // isolated module still works.
60
+ const Random = loadRandom()
61
+ const bytes = Random(16)
62
+ expect(bytes).toHaveLength(16)
63
+ bytes.forEach(b => {
64
+ expect(b).toBeGreaterThanOrEqual(0)
65
+ expect(b).toBeLessThanOrEqual(255)
66
+ })
67
+ })
68
+ })
69
+
70
+ // -------------------------------------------------------------------------
71
+ // 2. self.crypto path (Web Worker / Service Worker environment)
72
+ // -------------------------------------------------------------------------
73
+ describe('self.crypto path', () => {
74
+ const hadSelf = typeof self !== 'undefined'
75
+ let originalSelf: any
76
+ let originalProcess: any
77
+
78
+ beforeEach(() => {
79
+ // Shadow globalThis.crypto with undefined so the first branch is skipped
80
+ shadowCrypto(undefined)
81
+ originalSelf = (globalThis as any).self
82
+ // Remove process so the Node.js < 18 branch (which precedes self.crypto) is also skipped
83
+ originalProcess = (globalThis as any).process
84
+ delete (globalThis as any).process
85
+ })
86
+
87
+ afterEach(() => {
88
+ restoreCrypto()
89
+ if (hadSelf) {
90
+ (globalThis as any).self = originalSelf
91
+ } else {
92
+ delete (globalThis as any).self
93
+ }
94
+ ;(globalThis as any).process = originalProcess
95
+ })
96
+
97
+ it('uses self.crypto.getRandomValues when globalThis.crypto is absent', () => {
98
+ const mockGetRandomValues = jest.fn((arr: Uint8Array) => {
99
+ for (let i = 0; i < arr.length; i++) arr[i] = 42
100
+ return arr
101
+ })
102
+
103
+ // Install a synthetic `self` with a working crypto object.
104
+ ;(globalThis as any).self = {
105
+ crypto: { getRandomValues: mockGetRandomValues }
106
+ }
107
+
108
+ const Random = loadRandom()
109
+ const bytes = Random(4)
110
+
111
+ expect(bytes).toHaveLength(4)
112
+ expect(bytes).toEqual([42, 42, 42, 42])
113
+ expect(mockGetRandomValues).toHaveBeenCalledTimes(1)
114
+ })
115
+ })
116
+
117
+ // -------------------------------------------------------------------------
118
+ // 3. window.crypto path (browser main-thread environment)
119
+ // -------------------------------------------------------------------------
120
+ describe('window.crypto path', () => {
121
+ const hadSelf = typeof self !== 'undefined'
122
+ const hadWindow = typeof window !== 'undefined'
123
+ let originalSelf: any
124
+ let originalWindow: any
125
+ let originalProcess: any
126
+
127
+ beforeEach(() => {
128
+ // Shadow globalThis.crypto with undefined
129
+ shadowCrypto(undefined)
130
+ originalSelf = (globalThis as any).self
131
+ originalWindow = (globalThis as any).window
132
+ // Remove process and self so the Node.js and self.crypto branches are skipped
133
+ originalProcess = (globalThis as any).process
134
+ delete (globalThis as any).process
135
+ delete (globalThis as any).self
136
+ })
137
+
138
+ afterEach(() => {
139
+ restoreCrypto()
140
+ if (hadSelf) {
141
+ (globalThis as any).self = originalSelf
142
+ } else {
143
+ delete (globalThis as any).self
144
+ }
145
+ if (hadWindow) {
146
+ (globalThis as any).window = originalWindow
147
+ } else {
148
+ delete (globalThis as any).window
149
+ }
150
+ ;(globalThis as any).process = originalProcess
151
+ })
152
+
153
+ it('uses window.crypto.getRandomValues when globalThis/self.crypto are absent', () => {
154
+ const mockGetRandomValues = jest.fn((arr: Uint8Array) => {
155
+ for (let i = 0; i < arr.length; i++) arr[i] = 99
156
+ return arr
157
+ })
158
+
159
+ ;(globalThis as any).window = {
160
+ crypto: { getRandomValues: mockGetRandomValues }
161
+ }
162
+
163
+ const Random = loadRandom()
164
+ const bytes = Random(3)
165
+
166
+ expect(bytes).toHaveLength(3)
167
+ expect(bytes).toEqual([99, 99, 99])
168
+ expect(mockGetRandomValues).toHaveBeenCalledTimes(1)
169
+ })
170
+ })
171
+
172
+ // -------------------------------------------------------------------------
173
+ // 4. noRand path – throws when no crypto is available anywhere
174
+ // -------------------------------------------------------------------------
175
+ describe('noRand path', () => {
176
+ const hadSelf = typeof self !== 'undefined'
177
+ const hadWindow = typeof window !== 'undefined'
178
+ let originalSelf: any
179
+ let originalWindow: any
180
+ let originalProcess: any
181
+
182
+ beforeEach(() => {
183
+ // Shadow globalThis.crypto with undefined
184
+ shadowCrypto(undefined)
185
+ originalSelf = (globalThis as any).self
186
+ delete (globalThis as any).self
187
+ originalWindow = (globalThis as any).window
188
+ delete (globalThis as any).window
189
+ // Remove process entirely so that the Node < 18 require('crypto') branch is skipped
190
+ originalProcess = (globalThis as any).process
191
+ delete (globalThis as any).process
192
+ })
193
+
194
+ afterEach(() => {
195
+ restoreCrypto()
196
+ if (hadSelf) {
197
+ (globalThis as any).self = originalSelf
198
+ } else {
199
+ delete (globalThis as any).self
200
+ }
201
+ if (hadWindow) {
202
+ (globalThis as any).window = originalWindow
203
+ } else {
204
+ delete (globalThis as any).window
205
+ }
206
+ ;(globalThis as any).process = originalProcess
207
+ })
208
+
209
+ it('throws an error when no crypto source is available', () => {
210
+ const Random = loadRandom()
211
+ expect(() => Random(16)).toThrow(
212
+ 'No secure random number generator is available in this environment.'
213
+ )
214
+ })
215
+ })
216
+
217
+ // -------------------------------------------------------------------------
218
+ // 5. Node.js < 18 require('crypto') fallback path
219
+ // -------------------------------------------------------------------------
220
+ describe('Node.js < 18 require(\'crypto\') fallback path', () => {
221
+ beforeEach(() => {
222
+ // Shadow globalThis.crypto with undefined so the first branch is skipped
223
+ // and the constructor falls through to the process.release.name === 'node' check
224
+ shadowCrypto(undefined)
225
+ })
226
+
227
+ afterEach(() => {
228
+ restoreCrypto()
229
+ })
230
+
231
+ it('falls through to require("crypto").randomBytes when globalThis.crypto is absent', () => {
232
+ // process.release.name === 'node' is already true in Jest / Node.
233
+ // With globalThis.crypto shadowed as undefined the module should attempt require('crypto')
234
+ // and use randomBytes successfully.
235
+ const Random = loadRandom()
236
+ const bytes = Random(8)
237
+ expect(bytes).toHaveLength(8)
238
+ bytes.forEach(b => {
239
+ expect(b).toBeGreaterThanOrEqual(0)
240
+ expect(b).toBeLessThanOrEqual(255)
241
+ })
242
+ })
243
+ })
244
+
245
+ // -------------------------------------------------------------------------
246
+ // 6. Singleton caching – ayn is reused across calls
247
+ // -------------------------------------------------------------------------
248
+ describe('singleton caching', () => {
249
+ it('caches the Rand instance across multiple calls within the same module scope', () => {
250
+ // We cannot access `ayn` directly, but we can verify that calling the
251
+ // exported function multiple times without resetting the module still
252
+ // produces valid results (the cached instance is reused without error).
253
+ jest.isolateModules(() => {
254
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
255
+ const Random = require('../../primitives/Random').default as (len: number) => number[]
256
+ expect(Random(4)).toHaveLength(4)
257
+ expect(Random(4)).toHaveLength(4)
258
+ expect(Random(4)).toHaveLength(4)
259
+ })
260
+ })
261
+ })
262
+ })