@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,278 @@
1
+ /** eslint-env jest */
2
+ import { withDoubleSpendRetry } from '../withDoubleSpendRetry'
3
+ import { WERR_REVIEW_ACTIONS } from '../../wallet/WERR_REVIEW_ACTIONS'
4
+ import Transaction from '../../transaction/Transaction'
5
+ import { ReviewActionResult } from '../../wallet/Wallet.interfaces'
6
+ import TopicBroadcaster from '../SHIPBroadcaster'
7
+
8
+ // --- Module mocks -----------------------------------------------------------
9
+
10
+ jest.mock('../../transaction/Transaction.js', () => ({
11
+ fromBEEF: jest.fn()
12
+ }))
13
+
14
+ jest.mock('../SHIPBroadcaster.js', () => {
15
+ return jest.fn().mockImplementation(() => ({
16
+ broadcast: jest.fn()
17
+ }))
18
+ })
19
+
20
+ // --- Typed mock refs --------------------------------------------------------
21
+
22
+ const MockedTransaction = Transaction as jest.Mocked<typeof Transaction>
23
+
24
+ // --- Helpers ----------------------------------------------------------------
25
+
26
+ const MAX_DOUBLE_SPEND_RETRIES = 5
27
+
28
+ function makeMockBroadcaster (): jest.Mocked<TopicBroadcaster> {
29
+ return {
30
+ broadcast: jest.fn()
31
+ } as unknown as jest.Mocked<TopicBroadcaster>
32
+ }
33
+
34
+ function makeDoubleSpendError (
35
+ competingBeef: number[] | null = [0x01, 0x02],
36
+ competingTxs: string[] | null = ['competingtxid111111111111111111111111111111111111111111111111111111']
37
+ ): WERR_REVIEW_ACTIONS {
38
+ const result: ReviewActionResult = {
39
+ txid: 'originaltxid1111111111111111111111111111111111111111111111111111111',
40
+ status: 'doubleSpend',
41
+ ...(competingBeef != null && { competingBeef }),
42
+ ...(competingTxs != null && { competingTxs })
43
+ }
44
+ return new WERR_REVIEW_ACTIONS([result], [])
45
+ }
46
+
47
+ function makeNonDoubleSpendError (name: string = 'WERR_REVIEW_ACTIONS'): WERR_REVIEW_ACTIONS {
48
+ const result: ReviewActionResult = {
49
+ txid: 'originaltxid1111111111111111111111111111111111111111111111111111111',
50
+ status: 'serviceError'
51
+ }
52
+ const err = new WERR_REVIEW_ACTIONS([result], [])
53
+ err.name = name
54
+ return err
55
+ }
56
+
57
+ // --- Tests ------------------------------------------------------------------
58
+
59
+ describe('withDoubleSpendRetry', () => {
60
+ let broadcaster: jest.Mocked<TopicBroadcaster>
61
+ let mockCompetingTx: Partial<Transaction>
62
+
63
+ beforeEach(() => {
64
+ jest.clearAllMocks()
65
+ broadcaster = makeMockBroadcaster()
66
+ mockCompetingTx = {}
67
+ ;(MockedTransaction.fromBEEF as jest.Mock).mockReturnValue(mockCompetingTx as Transaction)
68
+ })
69
+
70
+ // --- Happy path -----------------------------------------------------------
71
+
72
+ describe('succeeds without retry', () => {
73
+ it('returns operation result immediately on first successful attempt', async () => {
74
+ const expectedResult = { success: true }
75
+ const operation = jest.fn().mockResolvedValue(expectedResult)
76
+
77
+ const result = await withDoubleSpendRetry(operation, broadcaster)
78
+
79
+ expect(result).toBe(expectedResult)
80
+ expect(operation).toHaveBeenCalledTimes(1)
81
+ expect(broadcaster.broadcast).not.toHaveBeenCalled()
82
+ })
83
+
84
+ it('returns operation result for non-object results (string)', async () => {
85
+ const operation = jest.fn().mockResolvedValue('done')
86
+
87
+ const result = await withDoubleSpendRetry(operation, broadcaster)
88
+
89
+ expect(result).toBe('done')
90
+ expect(operation).toHaveBeenCalledTimes(1)
91
+ })
92
+
93
+ it('returns operation result for undefined', async () => {
94
+ const operation = jest.fn().mockResolvedValue(undefined)
95
+
96
+ const result = await withDoubleSpendRetry(operation, broadcaster)
97
+
98
+ expect(result).toBeUndefined()
99
+ expect(operation).toHaveBeenCalledTimes(1)
100
+ })
101
+ })
102
+
103
+ // --- Non-double-spend errors rethrown immediately -------------------------
104
+
105
+ describe('rethrows non-WERR_REVIEW_ACTIONS errors immediately', () => {
106
+ it('rethrows a plain Error without retrying', async () => {
107
+ const plainError = new Error('Network error')
108
+ const operation = jest.fn().mockRejectedValue(plainError)
109
+
110
+ await expect(withDoubleSpendRetry(operation, broadcaster)).rejects.toThrow('Network error')
111
+ expect(operation).toHaveBeenCalledTimes(1)
112
+ expect(broadcaster.broadcast).not.toHaveBeenCalled()
113
+ })
114
+
115
+ it('rethrows errors with other error names without retrying', async () => {
116
+ const otherError = new Error('other error')
117
+ otherError.name = 'SOME_OTHER_ERROR'
118
+ const operation = jest.fn().mockRejectedValue(otherError)
119
+
120
+ await expect(withDoubleSpendRetry(operation, broadcaster)).rejects.toThrow('other error')
121
+ expect(operation).toHaveBeenCalledTimes(1)
122
+ expect(broadcaster.broadcast).not.toHaveBeenCalled()
123
+ })
124
+ })
125
+
126
+ // --- WERR_REVIEW_ACTIONS without doubleSpend rethrown immediately ---------
127
+
128
+ describe('rethrows WERR_REVIEW_ACTIONS that do not represent a valid doubleSpend', () => {
129
+ it('rethrows WERR_REVIEW_ACTIONS with no doubleSpend result in reviewActionResults', async () => {
130
+ const error = makeNonDoubleSpendError()
131
+ const operation = jest.fn().mockRejectedValue(error)
132
+
133
+ await expect(withDoubleSpendRetry(operation, broadcaster)).rejects.toThrow(error)
134
+ expect(operation).toHaveBeenCalledTimes(1)
135
+ expect(broadcaster.broadcast).not.toHaveBeenCalled()
136
+ })
137
+
138
+ it('rethrows WERR_REVIEW_ACTIONS where doubleSpend result has no competingBeef', async () => {
139
+ const error = makeDoubleSpendError(null, ['competingtxid'])
140
+ const operation = jest.fn().mockRejectedValue(error)
141
+
142
+ await expect(withDoubleSpendRetry(operation, broadcaster)).rejects.toThrow(error)
143
+ expect(operation).toHaveBeenCalledTimes(1)
144
+ expect(broadcaster.broadcast).not.toHaveBeenCalled()
145
+ })
146
+
147
+ it('rethrows WERR_REVIEW_ACTIONS where doubleSpend result has no competingTxs', async () => {
148
+ const error = makeDoubleSpendError([0x01, 0x02], null)
149
+ const operation = jest.fn().mockRejectedValue(error)
150
+
151
+ await expect(withDoubleSpendRetry(operation, broadcaster)).rejects.toThrow(error)
152
+ expect(operation).toHaveBeenCalledTimes(1)
153
+ expect(broadcaster.broadcast).not.toHaveBeenCalled()
154
+ })
155
+
156
+ it('rethrows WERR_REVIEW_ACTIONS where competingTxs is an empty array', async () => {
157
+ const error = makeDoubleSpendError([0x01, 0x02], [])
158
+ const operation = jest.fn().mockRejectedValue(error)
159
+
160
+ await expect(withDoubleSpendRetry(operation, broadcaster)).rejects.toThrow(error)
161
+ expect(operation).toHaveBeenCalledTimes(1)
162
+ expect(broadcaster.broadcast).not.toHaveBeenCalled()
163
+ })
164
+ })
165
+
166
+ // --- Retry on doubleSpend -------------------------------------------------
167
+
168
+ describe('retries after broadcasting the competing transaction', () => {
169
+ it('broadcasts the competing tx and retries the operation when doubleSpend is detected', async () => {
170
+ const competingBeef = [0xbe, 0xef]
171
+ const competingTxId = 'competingtxid111111111111111111111111111111111111111111111111111111'
172
+ const doubleSpendError = makeDoubleSpendError(competingBeef, [competingTxId])
173
+
174
+ broadcaster.broadcast.mockResolvedValue({ status: 'success', txid: competingTxId } as any)
175
+
176
+ const expectedResult = { done: true }
177
+ const operation = jest.fn()
178
+ .mockRejectedValueOnce(doubleSpendError) // first attempt: double-spend
179
+ .mockResolvedValueOnce(expectedResult) // second attempt: success
180
+
181
+ const result = await withDoubleSpendRetry(operation, broadcaster)
182
+
183
+ expect(result).toBe(expectedResult)
184
+ expect(operation).toHaveBeenCalledTimes(2)
185
+ expect(MockedTransaction.fromBEEF).toHaveBeenCalledWith(competingBeef, competingTxId)
186
+ expect(broadcaster.broadcast).toHaveBeenCalledTimes(1)
187
+ expect(broadcaster.broadcast).toHaveBeenCalledWith(mockCompetingTx)
188
+ })
189
+
190
+ it('calls Transaction.fromBEEF with competingBeef and the first competingTx', async () => {
191
+ const competingBeef = [0x01, 0x02, 0x03]
192
+ const firstTxId = 'firstcompetingtxid1111111111111111111111111111111111111111111111111'
193
+ const secondTxId = 'secondcompetingtxid111111111111111111111111111111111111111111111111'
194
+ const doubleSpendError = makeDoubleSpendError(competingBeef, [firstTxId, secondTxId])
195
+
196
+ broadcaster.broadcast.mockResolvedValue({ status: 'success', txid: firstTxId } as any)
197
+ const operation = jest.fn()
198
+ .mockRejectedValueOnce(doubleSpendError)
199
+ .mockResolvedValueOnce('ok')
200
+
201
+ await withDoubleSpendRetry(operation, broadcaster)
202
+
203
+ // Only the first competingTx should be used
204
+ expect(MockedTransaction.fromBEEF).toHaveBeenCalledWith(competingBeef, firstTxId)
205
+ })
206
+
207
+ it('retries multiple times until success', async () => {
208
+ const doubleSpendError = makeDoubleSpendError()
209
+ broadcaster.broadcast.mockResolvedValue({ status: 'success' } as any)
210
+
211
+ const operation = jest.fn()
212
+ .mockRejectedValueOnce(doubleSpendError) // attempt 1
213
+ .mockRejectedValueOnce(doubleSpendError) // attempt 2
214
+ .mockRejectedValueOnce(doubleSpendError) // attempt 3
215
+ .mockResolvedValueOnce('finally succeeded') // attempt 4
216
+
217
+ const result = await withDoubleSpendRetry(operation, broadcaster)
218
+
219
+ expect(result).toBe('finally succeeded')
220
+ expect(operation).toHaveBeenCalledTimes(4)
221
+ expect(broadcaster.broadcast).toHaveBeenCalledTimes(3)
222
+ })
223
+ })
224
+
225
+ // --- MAX_DOUBLE_SPEND_RETRIES enforcement ----------------------------------
226
+
227
+ describe('throws after MAX_DOUBLE_SPEND_RETRIES is exceeded', () => {
228
+ it('throws the error after MAX_DOUBLE_SPEND_RETRIES (5) failed attempts', async () => {
229
+ const doubleSpendError = makeDoubleSpendError()
230
+ broadcaster.broadcast.mockResolvedValue({ status: 'success' } as any)
231
+
232
+ // Operation always double-spends — should fail after maxRetries
233
+ const operation = jest.fn().mockRejectedValue(doubleSpendError)
234
+
235
+ await expect(
236
+ withDoubleSpendRetry(operation, broadcaster, MAX_DOUBLE_SPEND_RETRIES)
237
+ ).rejects.toThrow(doubleSpendError)
238
+
239
+ // Called maxRetries times; the last attempt's error is rethrown without broadcasting
240
+ expect(operation).toHaveBeenCalledTimes(MAX_DOUBLE_SPEND_RETRIES)
241
+ // Broadcast is called for all but the final attempt (last error is rethrown directly)
242
+ expect(broadcaster.broadcast).toHaveBeenCalledTimes(MAX_DOUBLE_SPEND_RETRIES - 1)
243
+ })
244
+
245
+ it('throws after custom maxRetries value is exceeded', async () => {
246
+ const doubleSpendError = makeDoubleSpendError()
247
+ broadcaster.broadcast.mockResolvedValue({ status: 'success' } as any)
248
+ const operation = jest.fn().mockRejectedValue(doubleSpendError)
249
+
250
+ await expect(
251
+ withDoubleSpendRetry(operation, broadcaster, 2)
252
+ ).rejects.toThrow(doubleSpendError)
253
+
254
+ expect(operation).toHaveBeenCalledTimes(2)
255
+ expect(broadcaster.broadcast).toHaveBeenCalledTimes(1)
256
+ })
257
+ })
258
+
259
+ // --- Broadcaster interaction -----------------------------------------------
260
+
261
+ describe('broadcaster.broadcast is called with the correct transaction', () => {
262
+ it('passes the Transaction.fromBEEF result to broadcaster.broadcast', async () => {
263
+ const competingTxMock = { id: jest.fn().mockReturnValue('abc') }
264
+ ;(MockedTransaction.fromBEEF as jest.Mock).mockReturnValue(competingTxMock)
265
+
266
+ const doubleSpendError = makeDoubleSpendError([0xaa, 0xbb], ['txid'])
267
+ broadcaster.broadcast.mockResolvedValue({ status: 'success' } as any)
268
+
269
+ const operation = jest.fn()
270
+ .mockRejectedValueOnce(doubleSpendError)
271
+ .mockResolvedValueOnce('done')
272
+
273
+ await withDoubleSpendRetry(operation, broadcaster)
274
+
275
+ expect(broadcaster.broadcast).toHaveBeenCalledWith(competingTxMock)
276
+ })
277
+ })
278
+ })
@@ -0,0 +1,79 @@
1
+ import BigNumber from '../BigNumber'
2
+
3
+ describe('BigNumber – additional coverage', () => {
4
+ describe('negative setter', () => {
5
+ it('sets sign to 0 when magnitude is zero (setting val=1 on zero BN)', () => {
6
+ const bn = new BigNumber(0)
7
+ bn.negative = 1
8
+ expect(bn.negative).toBe(0) // magnitude is 0 so sign stays 0
9
+ })
10
+
11
+ it('sets sign to 1 on a non-zero BigNumber', () => {
12
+ const bn = new BigNumber(5)
13
+ bn.negative = 1
14
+ expect(bn.negative).toBe(1)
15
+ })
16
+
17
+ it('sets sign to 0 on a non-zero BigNumber', () => {
18
+ const bn = new BigNumber(5)
19
+ bn.negative = 1
20
+ bn.negative = 0
21
+ expect(bn.negative).toBe(0)
22
+ })
23
+ })
24
+
25
+ describe('inspect', () => {
26
+ it('returns inspection string for a positive BigNumber', () => {
27
+ const bn = new BigNumber(255)
28
+ const s = bn.inspect()
29
+ expect(s).toContain('ff')
30
+ expect(s).toContain('BN')
31
+ })
32
+ })
33
+
34
+ describe('toBitArray', () => {
35
+ it('returns empty array for zero (static)', () => {
36
+ expect(BigNumber.toBitArray(new BigNumber(0))).toEqual([])
37
+ })
38
+
39
+ it('instance method returns same as static', () => {
40
+ const bn = new BigNumber(5) // binary: 101
41
+ expect(bn.toBitArray()).toEqual([1, 0, 1])
42
+ })
43
+ })
44
+
45
+ describe('toString with non-standard base', () => {
46
+ it('converts to base-3 string', () => {
47
+ const bn = new BigNumber(9)
48
+ expect(bn.toString(3)).toBe('100') // 9 in base 3 = 100
49
+ })
50
+ })
51
+
52
+ describe('fromBits / toBits edge cases', () => {
53
+ it('fromBits(0) returns zero BigNumber', () => {
54
+ const bn = BigNumber.fromBits(0)
55
+ expect(bn.toNumber()).toBe(0)
56
+ })
57
+
58
+ it('toBits for zero returns 0', () => {
59
+ expect(new BigNumber(0).toBits()).toBe(0)
60
+ })
61
+
62
+ it('toBits for a 3-byte number with MSB set in mantissa (triggers shift)', () => {
63
+ // mB[0] >= 0x80 → (nWordNum & 0x00800000) !== 0 → shift branch
64
+ const bn = BigNumber.fromHex('800001')
65
+ const bits = bn.toBits()
66
+ expect(bits).toBeGreaterThan(0)
67
+ })
68
+ })
69
+
70
+ describe('toSm', () => {
71
+ it('returns [0x80] for negative zero (magnitude 0, sign 1)', () => {
72
+ const bn = new BigNumber(0)
73
+ bn.negative = 1
74
+ const result = bn.toSm()
75
+ // magnitude is 0 so sign gets normalized to 0; returns []
76
+ expect(Array.isArray(result)).toBe(true)
77
+ })
78
+ })
79
+ })
@@ -0,0 +1,208 @@
1
+ import Curve from '../../primitives/Curve'
2
+ import Point from '../../primitives/Point'
3
+ import BigNumber from '../../primitives/BigNumber'
4
+
5
+ describe('Curve – additional coverage', () => {
6
+ const curve = new Curve()
7
+ const G = curve.g as Point
8
+
9
+ // --------------------------------------------------------------------------
10
+ // assert
11
+ // --------------------------------------------------------------------------
12
+ describe('Curve.assert', () => {
13
+ it('does not throw when expression is truthy', () => {
14
+ expect(() => Curve.assert(true)).not.toThrow()
15
+ expect(() => Curve.assert(1)).not.toThrow()
16
+ expect(() => Curve.assert('hello')).not.toThrow()
17
+ })
18
+
19
+ it('throws default message when expression is falsy', () => {
20
+ expect(() => Curve.assert(false)).toThrow('Elliptic curve assertion failed')
21
+ })
22
+
23
+ it('throws custom message', () => {
24
+ expect(() => Curve.assert(false, 'custom error')).toThrow('custom error')
25
+ })
26
+ })
27
+
28
+ // --------------------------------------------------------------------------
29
+ // getNAF
30
+ // --------------------------------------------------------------------------
31
+ describe('getNAF', () => {
32
+ it('returns non-empty array for a positive number', () => {
33
+ const naf = curve.getNAF(new BigNumber(7), 2, 256)
34
+ expect(Array.isArray(naf)).toBe(true)
35
+ expect(naf.length).toBeGreaterThan(0)
36
+ })
37
+
38
+ it('returns all zeros for BigNumber(0)', () => {
39
+ const naf = curve.getNAF(new BigNumber(0), 2, 256)
40
+ // For zero, all entries should be 0
41
+ expect(naf.every(x => x === 0)).toBe(true)
42
+ })
43
+
44
+ it('handles odd number', () => {
45
+ const naf = curve.getNAF(new BigNumber(15), 2, 256)
46
+ expect(Array.isArray(naf)).toBe(true)
47
+ })
48
+ })
49
+
50
+ // --------------------------------------------------------------------------
51
+ // getJSF
52
+ // --------------------------------------------------------------------------
53
+ describe('getJSF', () => {
54
+ it('returns two arrays (JSF of k1 and k2)', () => {
55
+ const k1 = new BigNumber(7)
56
+ const k2 = new BigNumber(11)
57
+ const jsf = curve.getJSF(k1, k2)
58
+ expect(Array.isArray(jsf)).toBe(true)
59
+ expect(jsf.length).toBe(2)
60
+ expect(Array.isArray(jsf[0])).toBe(true)
61
+ expect(Array.isArray(jsf[1])).toBe(true)
62
+ })
63
+
64
+ it('handles large numbers', () => {
65
+ const k1 = new BigNumber('deadbeef', 16)
66
+ const k2 = new BigNumber('cafebabe', 16)
67
+ const jsf = curve.getJSF(k1, k2)
68
+ expect(jsf[0].length).toBeGreaterThan(0)
69
+ })
70
+ })
71
+
72
+ // --------------------------------------------------------------------------
73
+ // parseBytes
74
+ // --------------------------------------------------------------------------
75
+ describe('Curve.parseBytes', () => {
76
+ it('converts hex string to byte array', () => {
77
+ const bytes = Curve.parseBytes('deadbeef')
78
+ expect(bytes).toEqual([0xde, 0xad, 0xbe, 0xef])
79
+ })
80
+
81
+ it('passes byte array through unchanged', () => {
82
+ const arr = [0x01, 0x02, 0x03]
83
+ const result = Curve.parseBytes(arr)
84
+ expect(result).toEqual(arr)
85
+ })
86
+ })
87
+
88
+ // --------------------------------------------------------------------------
89
+ // intFromLE
90
+ // --------------------------------------------------------------------------
91
+ describe('Curve.intFromLE', () => {
92
+ it('converts little-endian bytes to BigNumber', () => {
93
+ const bn = Curve.intFromLE([0x01, 0x00])
94
+ // 0x01 in LE means 0x0001 = 1
95
+ expect(bn.toNumber()).toBe(1)
96
+ })
97
+
98
+ it('converts multi-byte LE number', () => {
99
+ const bn = Curve.intFromLE([0x02, 0x01])
100
+ // 0x0102 = 258
101
+ expect(bn.toNumber()).toBe(0x0102)
102
+ })
103
+ })
104
+
105
+ // --------------------------------------------------------------------------
106
+ // cachedProperty
107
+ // --------------------------------------------------------------------------
108
+ describe('Curve.cachedProperty', () => {
109
+ it('caches the result after first call', () => {
110
+ let computeCount = 0
111
+ // Create a class to attach the property to
112
+ class TestClass {
113
+ _myProp: any = undefined
114
+ }
115
+ Curve.cachedProperty(TestClass, 'myProp', function () {
116
+ computeCount++
117
+ return 42
118
+ })
119
+ const instance = new TestClass()
120
+ // Calling twice — should compute once
121
+ const val1 = (instance as any).myProp()
122
+ const val2 = (instance as any).myProp()
123
+ expect(val1).toBe(42)
124
+ expect(val2).toBe(42)
125
+ expect(computeCount).toBe(1)
126
+ })
127
+ })
128
+
129
+ // --------------------------------------------------------------------------
130
+ // validate
131
+ // --------------------------------------------------------------------------
132
+ describe('validate', () => {
133
+ it('returns true for point at infinity', () => {
134
+ const inf = new Point(null, null)
135
+ expect(curve.validate(inf)).toBe(true)
136
+ })
137
+
138
+ it('returns true for a valid curve point', () => {
139
+ const p = Point.fromString('0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798')
140
+ expect(curve.validate(p)).toBe(true)
141
+ })
142
+
143
+ it('returns false for an off-curve point', () => {
144
+ // Create point with modified y to be off-curve
145
+ const p = Point.fromString('0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798')
146
+ const yModified = (p.y as BigNumber).clone().redIAdd(curve.one)
147
+ // Access internal: just test via validate
148
+ // Use a different approach to create an off-curve point
149
+ const offCurve = new Point(p.x, yModified, false)
150
+ // We need to bypass the isRed check since the modified point isn't on the curve
151
+ // Just call curve.validate
152
+ expect(curve.validate(offCurve)).toBe(false)
153
+ })
154
+ })
155
+
156
+ // --------------------------------------------------------------------------
157
+ // _endoSplit
158
+ // --------------------------------------------------------------------------
159
+ describe('_endoSplit', () => {
160
+ it('splits a scalar into balanced k1 and k2', () => {
161
+ const k = new BigNumber('deadbeefcafe', 16)
162
+ const split = curve._endoSplit(k)
163
+ expect(split).toHaveProperty('k1')
164
+ expect(split).toHaveProperty('k2')
165
+ // k1 + lambda * k2 ≡ k (mod n)
166
+ // This is an endomorphism property; just verify the result is plausible
167
+ expect(BigNumber.isBN(split.k1)).toBe(true)
168
+ expect(BigNumber.isBN(split.k2)).toBe(true)
169
+ })
170
+ })
171
+
172
+ // --------------------------------------------------------------------------
173
+ // _getEndoRoots
174
+ // --------------------------------------------------------------------------
175
+ describe('_getEndoRoots', () => {
176
+ it('computes two roots for the curve order n', () => {
177
+ const roots = curve._getEndoRoots(curve.n)
178
+ expect(Array.isArray(roots)).toBe(true)
179
+ expect(roots.length).toBe(2)
180
+ expect(BigNumber.isBN(roots[0])).toBe(true)
181
+ expect(BigNumber.isBN(roots[1])).toBe(true)
182
+ })
183
+
184
+ it('computes two roots for the curve field prime p', () => {
185
+ const roots = curve._getEndoRoots(curve.p)
186
+ expect(Array.isArray(roots)).toBe(true)
187
+ expect(roots.length).toBe(2)
188
+ })
189
+ })
190
+
191
+ // --------------------------------------------------------------------------
192
+ // _getEndoBasis
193
+ // --------------------------------------------------------------------------
194
+ describe('_getEndoBasis', () => {
195
+ it('returns a basis of two vectors', () => {
196
+ // Use the curve's lambda if available
197
+ if (curve.endo != null) {
198
+ const basis = curve._getEndoBasis(curve.endo.lambda)
199
+ expect(Array.isArray(basis)).toBe(true)
200
+ expect(basis.length).toBe(2)
201
+ expect(basis[0]).toHaveProperty('a')
202
+ expect(basis[0]).toHaveProperty('b')
203
+ expect(basis[1]).toHaveProperty('a')
204
+ expect(basis[1]).toHaveProperty('b')
205
+ }
206
+ })
207
+ })
208
+ })
@@ -0,0 +1,122 @@
1
+ import * as ECDSA from '../../primitives/ECDSA'
2
+ import BigNumber from '../../primitives/BigNumber'
3
+ import Curve from '../../primitives/Curve'
4
+ import Signature from '../../primitives/Signature'
5
+ import Point from '../../primitives/Point'
6
+
7
+ const curve = new Curve()
8
+ const key = new BigNumber(
9
+ '1e5edd45de6d22deebef4596b80444ffcc29143839c1dce18db470e25b4be7b5',
10
+ 16
11
+ )
12
+ const pub = curve.g.mul(key)
13
+ const msg = new BigNumber('deadbeef', 16)
14
+
15
+ describe('ECDSA – additional coverage', () => {
16
+ // --------------------------------------------------------------------------
17
+ // truncateToN paths
18
+ // --------------------------------------------------------------------------
19
+ describe('truncateToN paths', () => {
20
+ it('sign handles msg equal to n (cmp >= 0 path in truncateToN)', () => {
21
+ // msg == n: after truncation msg.cmp(n) = 0, so msg.sub(n) = 0
22
+ // But then the loop would handle the edge case
23
+ // Use msg = n - 1, which is safe
24
+ const nMinus1 = curve.n.subn(1)
25
+ const sig = ECDSA.sign(nMinus1, key)
26
+ expect(ECDSA.verify(nMinus1, sig, pub)).toBe(true)
27
+ })
28
+ })
29
+
30
+ // --------------------------------------------------------------------------
31
+ // verify – out-of-range signature components
32
+ // --------------------------------------------------------------------------
33
+ describe('verify out-of-range signature', () => {
34
+ it('returns false when r = 0', () => {
35
+ const r = new BigNumber(0)
36
+ const s = new BigNumber(1)
37
+ const sig = new Signature(r, s)
38
+ expect(ECDSA.verify(msg, sig, pub)).toBe(false)
39
+ })
40
+
41
+ it('returns false when s = 0', () => {
42
+ const r = new BigNumber(1)
43
+ const s = new BigNumber(0)
44
+ const sig = new Signature(r, s)
45
+ expect(ECDSA.verify(msg, sig, pub)).toBe(false)
46
+ })
47
+
48
+ it('returns false when r >= n', () => {
49
+ const r = curve.n.clone() // r == n
50
+ const s = new BigNumber(1)
51
+ const sig = new Signature(r, s)
52
+ expect(ECDSA.verify(msg, sig, pub)).toBe(false)
53
+ })
54
+
55
+ it('returns false when s >= n', () => {
56
+ const r = new BigNumber(1)
57
+ const s = curve.n.clone() // s == n
58
+ const sig = new Signature(r, s)
59
+ expect(ECDSA.verify(msg, sig, pub)).toBe(false)
60
+ })
61
+ })
62
+
63
+ // --------------------------------------------------------------------------
64
+ // forceLowS - line 163
65
+ // --------------------------------------------------------------------------
66
+ describe('forceLowS', () => {
67
+ it('produces s <= n/2 with forceLowS=true across multiple messages', () => {
68
+ const halfN = curve.n.ushrn(1)
69
+ // Try a few messages to ensure forceLowS is exercised
70
+ for (let i = 1; i <= 10; i++) {
71
+ const testMsg = new BigNumber(i * 0xdeadbeef)
72
+ const sig = ECDSA.sign(testMsg, key, true)
73
+ expect(sig.s.cmp(halfN) <= 0).toBe(true)
74
+ }
75
+ })
76
+ })
77
+
78
+ // --------------------------------------------------------------------------
79
+ // sign with function customK
80
+ // --------------------------------------------------------------------------
81
+ describe('sign with function customK', () => {
82
+ it('accepts a function as customK', () => {
83
+ let callCount = 0
84
+ const customK = (iter: number): BigNumber => {
85
+ callCount++
86
+ return new BigNumber(1358)
87
+ }
88
+ const sig = ECDSA.sign(msg, key, false, customK)
89
+ expect(ECDSA.verify(msg, sig, pub)).toBe(true)
90
+ expect(callCount).toBeGreaterThan(0)
91
+ })
92
+ })
93
+
94
+ // --------------------------------------------------------------------------
95
+ // verify - invalid public key path (key with null x or y)
96
+ // --------------------------------------------------------------------------
97
+ describe('verify invalid public key', () => {
98
+ it('throws when public key coordinates are null', () => {
99
+ const sig = ECDSA.sign(msg, key)
100
+ const infinityKey = new Point(null, null)
101
+ expect(() => ECDSA.verify(msg, sig, infinityKey)).toThrow('Invalid public key')
102
+ })
103
+ })
104
+
105
+ // --------------------------------------------------------------------------
106
+ // sign – k function returning invalid k causes iteration
107
+ // --------------------------------------------------------------------------
108
+ describe('sign – iterative k generation', () => {
109
+ it('eventually succeeds when k function returns invalid k on first iter', () => {
110
+ let callCount = 0
111
+ const customK = (iter: number): BigNumber => {
112
+ callCount++
113
+ // Return valid k on second call
114
+ if (iter === 0) return new BigNumber(0) // invalid k
115
+ return new BigNumber(1358) // valid k
116
+ }
117
+ const sig = ECDSA.sign(msg, key, false, customK)
118
+ expect(ECDSA.verify(msg, sig, pub)).toBe(true)
119
+ expect(callCount).toBeGreaterThanOrEqual(2)
120
+ })
121
+ })
122
+ })