@bsv/sdk 2.0.11 → 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/overlay-tools/HostReputationTracker.js +21 -13
- package/dist/cjs/src/overlay-tools/HostReputationTracker.js.map +1 -1
- package/dist/cjs/src/primitives/PrivateKey.js +3 -3
- package/dist/cjs/src/primitives/PrivateKey.js.map +1 -1
- package/dist/cjs/src/script/Spend.js +17 -9
- package/dist/cjs/src/script/Spend.js.map +1 -1
- package/dist/cjs/src/storage/StorageDownloader.js +6 -6
- package/dist/cjs/src/storage/StorageDownloader.js.map +1 -1
- package/dist/cjs/src/storage/StorageUtils.js +1 -1
- package/dist/cjs/src/storage/StorageUtils.js.map +1 -1
- package/dist/cjs/src/transaction/MerklePath.js +168 -27
- 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/overlay-tools/HostReputationTracker.js +21 -13
- package/dist/esm/src/overlay-tools/HostReputationTracker.js.map +1 -1
- package/dist/esm/src/primitives/PrivateKey.js +3 -3
- package/dist/esm/src/primitives/PrivateKey.js.map +1 -1
- package/dist/esm/src/script/Spend.js +17 -9
- package/dist/esm/src/script/Spend.js.map +1 -1
- package/dist/esm/src/storage/StorageDownloader.js +6 -6
- package/dist/esm/src/storage/StorageDownloader.js.map +1 -1
- package/dist/esm/src/storage/StorageUtils.js +1 -1
- package/dist/esm/src/storage/StorageUtils.js.map +1 -1
- package/dist/esm/src/transaction/MerklePath.js +168 -27
- 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/overlay-tools/HostReputationTracker.d.ts.map +1 -1
- package/dist/types/src/script/Spend.d.ts.map +1 -1
- 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 +3 -3
- 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/auth/utils/__tests/validateCertificates.test.ts +12 -9
- 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/LocalKVStore.test.ts +4 -6
- package/src/kvstore/__tests/kvStoreInterpreter.test.ts +327 -0
- package/src/overlay-tools/HostReputationTracker.ts +17 -14
- 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/PrivateKey.ts +3 -3
- 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/Spend.ts +19 -11
- 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/storage/StorageDownloader.ts +6 -6
- package/src/storage/StorageUtils.ts +1 -1
- package/src/transaction/MerklePath.ts +196 -36
- 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 +232 -21
- 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,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
|
+
})
|
|
@@ -55,7 +55,7 @@ export class KeyShares {
|
|
|
55
55
|
const [x, y, t, i] = shareParts
|
|
56
56
|
if (t === undefined) throw new Error('Threshold not found in share ' + idx.toString())
|
|
57
57
|
if (i === undefined) throw new Error('Integrity not found in share ' + idx.toString())
|
|
58
|
-
const tInt = parseInt(t)
|
|
58
|
+
const tInt = Number.parseInt(t, 10)
|
|
59
59
|
if (idx !== 0 && threshold !== tInt) { throw new Error('Threshold mismatch in share ' + idx.toString()) }
|
|
60
60
|
if (idx !== 0 && integrity !== i) { throw new Error('Integrity mismatch in share ' + idx.toString()) }
|
|
61
61
|
threshold = tInt
|
|
@@ -399,7 +399,7 @@ export default class PrivateKey extends BigNumber {
|
|
|
399
399
|
let sharedSecret: Point
|
|
400
400
|
if (typeof retrieveCachedSharedSecret === 'function') {
|
|
401
401
|
const retrieved = retrieveCachedSharedSecret(this, publicKey)
|
|
402
|
-
if (
|
|
402
|
+
if (retrieved !== undefined) {
|
|
403
403
|
sharedSecret = retrieved
|
|
404
404
|
} else {
|
|
405
405
|
sharedSecret = this.deriveSharedSecret(publicKey)
|
|
@@ -429,7 +429,7 @@ export default class PrivateKey extends BigNumber {
|
|
|
429
429
|
* const shares = key.toKeyShares(2, 5)
|
|
430
430
|
*/
|
|
431
431
|
toKeyShares (threshold: number, totalShares: number): KeyShares {
|
|
432
|
-
if (typeof threshold !== 'number' || typeof totalShares !== 'number') { throw new
|
|
432
|
+
if (typeof threshold !== 'number' || typeof totalShares !== 'number') { throw new TypeError('threshold and totalShares must be numbers') }
|
|
433
433
|
if (threshold < 2) throw new Error('threshold must be at least 2')
|
|
434
434
|
if (totalShares < 2) throw new Error('totalShares must be at least 2')
|
|
435
435
|
if (threshold > totalShares) { throw new Error('threshold should be less than or equal to totalShares') }
|
|
@@ -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
|
+
})
|