@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,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
|
+
})
|