@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,193 @@
|
|
|
1
|
+
import WhatsOnChain from '../../../transaction/chaintrackers/WhatsOnChain'
|
|
2
|
+
import { FetchHttpClient } from '../../../transaction/http/FetchHttpClient'
|
|
3
|
+
|
|
4
|
+
// These tests cover the branches that are missing from WhatsOnChainChainTracker.test.ts:
|
|
5
|
+
// Line 80-85 — currentHeight() non-ok response branch (throws)
|
|
6
|
+
// Line 84 — currentHeight() catch block re-throws with formatted message
|
|
7
|
+
// Line 97 — getHttpHeaders() sets Authorization header when apiKey is non-empty
|
|
8
|
+
|
|
9
|
+
describe('WhatsOnChain — additional coverage', () => {
|
|
10
|
+
afterEach(() => {
|
|
11
|
+
jest.clearAllMocks()
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
// -------------------------------------------------------------------------
|
|
15
|
+
// currentHeight() — non-ok response (lines 80-83)
|
|
16
|
+
// -------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
it('throws when currentHeight receives a non-ok HTTP response', async () => {
|
|
19
|
+
const mockFetch = mockedFetch({ status: 503, data: { error: 'Service Unavailable' } })
|
|
20
|
+
|
|
21
|
+
const tracker = new WhatsOnChain('main', {
|
|
22
|
+
httpClient: new FetchHttpClient(mockFetch)
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
await expect(tracker.currentHeight()).rejects.toThrow(
|
|
26
|
+
/Failed to get current height because of an error:/
|
|
27
|
+
)
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('includes the serialised response data in the thrown message for non-ok currentHeight', async () => {
|
|
31
|
+
const mockFetch = mockedFetch({ status: 429, data: { code: 'RATE_LIMITED' } })
|
|
32
|
+
|
|
33
|
+
const tracker = new WhatsOnChain('main', {
|
|
34
|
+
httpClient: new FetchHttpClient(mockFetch)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
await expect(tracker.currentHeight()).rejects.toThrow(
|
|
38
|
+
/RATE_LIMITED/
|
|
39
|
+
)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
// -------------------------------------------------------------------------
|
|
43
|
+
// currentHeight() — catch block (lines 84-87)
|
|
44
|
+
// The outer try/catch in currentHeight() wraps everything, including the
|
|
45
|
+
// non-ok branch. A network-level rejection also exercises lines 84-87.
|
|
46
|
+
// -------------------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
it('wraps network errors in a formatted message via the catch block', async () => {
|
|
49
|
+
const mockFetch = jest.fn().mockRejectedValue(new Error('connection refused'))
|
|
50
|
+
|
|
51
|
+
const tracker = new WhatsOnChain('main', {
|
|
52
|
+
httpClient: new FetchHttpClient(mockFetch)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
await expect(tracker.currentHeight()).rejects.toThrow(
|
|
56
|
+
'Failed to get current height because of an error: connection refused'
|
|
57
|
+
)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('handles non-Error thrown values in the catch block', async () => {
|
|
61
|
+
const mockHttpClient = {
|
|
62
|
+
request: jest.fn().mockRejectedValue('raw string rejection')
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const tracker = new WhatsOnChain('main', { httpClient: mockHttpClient })
|
|
66
|
+
|
|
67
|
+
await expect(tracker.currentHeight()).rejects.toThrow(
|
|
68
|
+
'Failed to get current height because of an error: raw string rejection'
|
|
69
|
+
)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
// -------------------------------------------------------------------------
|
|
73
|
+
// getHttpHeaders() — Authorization header when apiKey is set (line 97)
|
|
74
|
+
// -------------------------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
it('includes Authorization header in requests when apiKey is provided', async () => {
|
|
77
|
+
const apiKey = 'my-test-api-key'
|
|
78
|
+
const mockFetch = mockedFetch({ status: 200, data: { merkleroot: 'root123' } })
|
|
79
|
+
|
|
80
|
+
const tracker = new WhatsOnChain('main', {
|
|
81
|
+
apiKey,
|
|
82
|
+
httpClient: new FetchHttpClient(mockFetch)
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
await tracker.isValidRootForHeight('root123', 100)
|
|
86
|
+
|
|
87
|
+
expect(mockFetch).toHaveBeenCalledTimes(1)
|
|
88
|
+
const [, fetchOptions] = mockFetch.mock.calls[0]
|
|
89
|
+
expect(fetchOptions.headers?.Authorization).toBe(apiKey)
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('does NOT include Authorization header when apiKey is an empty string', async () => {
|
|
93
|
+
const mockFetch = mockedFetch({ status: 200, data: { merkleroot: 'root456' } })
|
|
94
|
+
|
|
95
|
+
const tracker = new WhatsOnChain('main', {
|
|
96
|
+
apiKey: '',
|
|
97
|
+
httpClient: new FetchHttpClient(mockFetch)
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
await tracker.isValidRootForHeight('root456', 200)
|
|
101
|
+
|
|
102
|
+
const [, fetchOptions] = mockFetch.mock.calls[0]
|
|
103
|
+
expect(fetchOptions.headers?.Authorization).toBeUndefined()
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('does NOT include Authorization header when apiKey is only whitespace', async () => {
|
|
107
|
+
const mockFetch = mockedFetch({ status: 200, data: { merkleroot: 'root789' } })
|
|
108
|
+
|
|
109
|
+
const tracker = new WhatsOnChain('main', {
|
|
110
|
+
apiKey: ' ',
|
|
111
|
+
httpClient: new FetchHttpClient(mockFetch)
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
await tracker.isValidRootForHeight('root789', 300)
|
|
115
|
+
|
|
116
|
+
const [, fetchOptions] = mockFetch.mock.calls[0]
|
|
117
|
+
expect(fetchOptions.headers?.Authorization).toBeUndefined()
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it('always includes Accept: application/json regardless of apiKey', async () => {
|
|
121
|
+
const mockFetch = mockedFetch({ status: 200, data: { merkleroot: 'rAny' } })
|
|
122
|
+
|
|
123
|
+
const tracker = new WhatsOnChain('main', {
|
|
124
|
+
httpClient: new FetchHttpClient(mockFetch)
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
await tracker.isValidRootForHeight('rAny', 1)
|
|
128
|
+
|
|
129
|
+
const [, fetchOptions] = mockFetch.mock.calls[0]
|
|
130
|
+
expect(fetchOptions.headers?.Accept).toBe('application/json')
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
it('sends Authorization header to currentHeight endpoint when apiKey is set', async () => {
|
|
134
|
+
const apiKey = 'another-key'
|
|
135
|
+
const mockFetch = mockedFetch({
|
|
136
|
+
status: 200,
|
|
137
|
+
data: [{ height: 999999 }]
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
const tracker = new WhatsOnChain('test', {
|
|
141
|
+
apiKey,
|
|
142
|
+
httpClient: new FetchHttpClient(mockFetch)
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
await tracker.currentHeight()
|
|
146
|
+
|
|
147
|
+
const [, fetchOptions] = mockFetch.mock.calls[0]
|
|
148
|
+
expect(fetchOptions.headers?.Authorization).toBe(apiKey)
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
// -------------------------------------------------------------------------
|
|
152
|
+
// Constructor — network variants
|
|
153
|
+
// -------------------------------------------------------------------------
|
|
154
|
+
|
|
155
|
+
it('builds the correct URL for the "test" network', async () => {
|
|
156
|
+
const mockFetch = mockedFetch({ status: 200, data: { merkleroot: 'testroot' } })
|
|
157
|
+
|
|
158
|
+
const tracker = new WhatsOnChain('test', { httpClient: new FetchHttpClient(mockFetch) })
|
|
159
|
+
await tracker.isValidRootForHeight('testroot', 10)
|
|
160
|
+
|
|
161
|
+
const [calledUrl] = mockFetch.mock.calls[0]
|
|
162
|
+
expect(calledUrl).toContain('bsv/test')
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
it('builds the correct URL for the "stn" network', async () => {
|
|
166
|
+
const mockFetch = mockedFetch({ status: 200, data: { merkleroot: 'stnroot' } })
|
|
167
|
+
|
|
168
|
+
const tracker = new WhatsOnChain('stn', { httpClient: new FetchHttpClient(mockFetch) })
|
|
169
|
+
await tracker.isValidRootForHeight('stnroot', 20)
|
|
170
|
+
|
|
171
|
+
const [calledUrl] = mockFetch.mock.calls[0]
|
|
172
|
+
expect(calledUrl).toContain('bsv/stn')
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
// -------------------------------------------------------------------------
|
|
176
|
+
// Helper
|
|
177
|
+
// -------------------------------------------------------------------------
|
|
178
|
+
|
|
179
|
+
function mockedFetch (response: { status: number, data: any }): jest.Mock {
|
|
180
|
+
return jest.fn().mockResolvedValue({
|
|
181
|
+
ok: response.status >= 200 && response.status < 300,
|
|
182
|
+
status: response.status,
|
|
183
|
+
statusText: response.status === 200 ? 'OK' : 'Error',
|
|
184
|
+
headers: {
|
|
185
|
+
get (key: string): string | undefined {
|
|
186
|
+
if (key === 'Content-Type') return 'application/json'
|
|
187
|
+
return undefined
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
json: async () => response.data
|
|
191
|
+
})
|
|
192
|
+
}
|
|
193
|
+
})
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import SatoshisPerKilobyte from '../SatoshisPerKilobyte'
|
|
2
|
+
import Transaction from '../../Transaction'
|
|
3
|
+
import Script from '../../../script/Script'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Tests for SatoshisPerKilobyte fee model.
|
|
7
|
+
*
|
|
8
|
+
* SatoshisPerKilobyte.computeFee() calculates transaction size from:
|
|
9
|
+
* - 4 bytes version
|
|
10
|
+
* - varint number of inputs
|
|
11
|
+
* - per input: 40 bytes (fixed) + varint script length + script bytes
|
|
12
|
+
* - varint number of outputs
|
|
13
|
+
* - per output: 8 bytes (satoshis) + varint script length + script bytes
|
|
14
|
+
* - 4 bytes lock time
|
|
15
|
+
*
|
|
16
|
+
* Fee = ceil(size / 1000 * value)
|
|
17
|
+
*
|
|
18
|
+
* The unlocking-script source can be:
|
|
19
|
+
* a) an actual UnlockingScript object (.unlockingScript present)
|
|
20
|
+
* b) a template (.unlockingScriptTemplate present with .estimateLength())
|
|
21
|
+
* c) neither → throws
|
|
22
|
+
*
|
|
23
|
+
* getVarIntSize thresholds:
|
|
24
|
+
* - <= 253 → 1 byte
|
|
25
|
+
* - 254..65535 → 3 bytes (> 253)
|
|
26
|
+
* - 65536..2^32 → 5 bytes (> 2^16)
|
|
27
|
+
* - > 2^32 → 9 bytes
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// Helpers
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
/** Build the simplest possible mock input with an already-compiled unlocking script. */
|
|
35
|
+
function makeScriptInput (scriptBytes: number[]): any {
|
|
36
|
+
return {
|
|
37
|
+
unlockingScript: Script.fromBinary(scriptBytes),
|
|
38
|
+
unlockingScriptTemplate: undefined
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Build an input that uses an unlockingScriptTemplate instead. */
|
|
43
|
+
function makeTemplateInput (estimatedLength: number): any {
|
|
44
|
+
return {
|
|
45
|
+
unlockingScript: undefined,
|
|
46
|
+
unlockingScriptTemplate: {
|
|
47
|
+
estimateLength: jest.fn().mockResolvedValue(estimatedLength)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Build a simple output with a locking script of the given byte length. */
|
|
53
|
+
function makeOutput (scriptBytes: number[], satoshis = 1000): any {
|
|
54
|
+
return {
|
|
55
|
+
lockingScript: Script.fromBinary(scriptBytes),
|
|
56
|
+
satoshis
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Create a minimal Transaction-like object with the given inputs and outputs. */
|
|
61
|
+
function makeTx (inputs: any[], outputs: any[]): Transaction {
|
|
62
|
+
const tx = new Transaction()
|
|
63
|
+
tx.inputs = inputs
|
|
64
|
+
tx.outputs = outputs
|
|
65
|
+
return tx
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
// Tests
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
describe('SatoshisPerKilobyte', () => {
|
|
73
|
+
// -------------------------------------------------------------------------
|
|
74
|
+
// Constructor
|
|
75
|
+
// -------------------------------------------------------------------------
|
|
76
|
+
describe('constructor', () => {
|
|
77
|
+
it('stores the satoshis-per-kilobyte value', () => {
|
|
78
|
+
const model = new SatoshisPerKilobyte(50)
|
|
79
|
+
expect(model.value).toBe(50)
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('accepts value of 0', () => {
|
|
83
|
+
const model = new SatoshisPerKilobyte(0)
|
|
84
|
+
expect(model.value).toBe(0)
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('accepts fractional values', () => {
|
|
88
|
+
const model = new SatoshisPerKilobyte(0.5)
|
|
89
|
+
expect(model.value).toBe(0.5)
|
|
90
|
+
})
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
// -------------------------------------------------------------------------
|
|
94
|
+
// computeFee – happy path
|
|
95
|
+
// -------------------------------------------------------------------------
|
|
96
|
+
describe('computeFee', () => {
|
|
97
|
+
it('returns 0 for empty transaction with value 0', async () => {
|
|
98
|
+
const model = new SatoshisPerKilobyte(0)
|
|
99
|
+
const tx = makeTx([], [])
|
|
100
|
+
// size = 4 (version) + 1 (input count varint) + 1 (output count varint) + 4 (locktime) = 10
|
|
101
|
+
// fee = ceil(10/1000 * 0) = 0
|
|
102
|
+
const fee = await model.computeFee(tx)
|
|
103
|
+
expect(fee).toBe(0)
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('computes correct fee for an empty transaction (no inputs, no outputs)', async () => {
|
|
107
|
+
const model = new SatoshisPerKilobyte(1000) // 1 sat/byte
|
|
108
|
+
const tx = makeTx([], [])
|
|
109
|
+
// size = 4 + 1 + 1 + 4 = 10 bytes
|
|
110
|
+
// fee = ceil(10/1000 * 1000) = ceil(10) = 10
|
|
111
|
+
const fee = await model.computeFee(tx)
|
|
112
|
+
expect(fee).toBe(10)
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it('computes fee for one input with an unlocking script', async () => {
|
|
116
|
+
const model = new SatoshisPerKilobyte(1000)
|
|
117
|
+
const scriptData = new Array(107).fill(0x00) // P2PKH-ish unlock ~107 bytes
|
|
118
|
+
const tx = makeTx([makeScriptInput(scriptData)], [])
|
|
119
|
+
// size = 4 (ver) + 1 (input count) + [40 + 1 (script varint) + 107 (script)] + 1 (output count) + 4 (locktime)
|
|
120
|
+
// = 4 + 1 + 148 + 1 + 4 = 158 bytes
|
|
121
|
+
const fee = await model.computeFee(tx)
|
|
122
|
+
expect(fee).toBe(Math.ceil((158 / 1000) * 1000))
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it('uses unlockingScriptTemplate.estimateLength when no script is compiled', async () => {
|
|
126
|
+
const model = new SatoshisPerKilobyte(1000)
|
|
127
|
+
const templateInput = makeTemplateInput(107)
|
|
128
|
+
const tx = makeTx([templateInput], [])
|
|
129
|
+
const fee = await model.computeFee(tx)
|
|
130
|
+
expect(templateInput.unlockingScriptTemplate.estimateLength).toHaveBeenCalledTimes(1)
|
|
131
|
+
expect(fee).toBe(Math.ceil((158 / 1000) * 1000))
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it('computes fee including outputs', async () => {
|
|
135
|
+
const model = new SatoshisPerKilobyte(1000)
|
|
136
|
+
const lockScript = new Array(25).fill(0x00) // P2PKH locking script = 25 bytes
|
|
137
|
+
const tx = makeTx([], [makeOutput(lockScript)])
|
|
138
|
+
// size = 4 + 1 + 1 + [8 + 1 + 25] + 4 = 44 bytes
|
|
139
|
+
const fee = await model.computeFee(tx)
|
|
140
|
+
expect(fee).toBe(Math.ceil((44 / 1000) * 1000))
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
it('computes fee for one input and one output', async () => {
|
|
144
|
+
const model = new SatoshisPerKilobyte(1000)
|
|
145
|
+
const unlockScript = new Array(107).fill(0x00)
|
|
146
|
+
const lockScript = new Array(25).fill(0x00)
|
|
147
|
+
const tx = makeTx([makeScriptInput(unlockScript)], [makeOutput(lockScript)])
|
|
148
|
+
// size = 4 + 1 + (40 + 1 + 107) + 1 + (8 + 1 + 25) + 4
|
|
149
|
+
// = 4 + 1 + 148 + 1 + 34 + 4 = 192 bytes
|
|
150
|
+
const fee = await model.computeFee(tx)
|
|
151
|
+
expect(fee).toBe(Math.ceil((192 / 1000) * 1000))
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
it('uses Math.ceil to round up fractional fees', async () => {
|
|
155
|
+
// Choose a value where the result is not a whole satoshi
|
|
156
|
+
const model = new SatoshisPerKilobyte(1) // very small rate
|
|
157
|
+
const tx = makeTx([], [])
|
|
158
|
+
// size = 10 bytes → fee = ceil(10/1000 * 1) = ceil(0.01) = 1
|
|
159
|
+
const fee = await model.computeFee(tx)
|
|
160
|
+
expect(fee).toBe(1)
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
it('fee scales proportionally with sat/kb rate', async () => {
|
|
164
|
+
const tx = makeTx([], [])
|
|
165
|
+
const fee100 = await new SatoshisPerKilobyte(100).computeFee(tx)
|
|
166
|
+
const fee200 = await new SatoshisPerKilobyte(200).computeFee(tx)
|
|
167
|
+
expect(fee200).toBeGreaterThanOrEqual(fee100)
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
it('throws when input has neither unlockingScript nor unlockingScriptTemplate', async () => {
|
|
171
|
+
const model = new SatoshisPerKilobyte(1000)
|
|
172
|
+
const badInput: any = {} // no unlockingScript, no unlockingScriptTemplate
|
|
173
|
+
const tx = makeTx([badInput], [])
|
|
174
|
+
await expect(model.computeFee(tx)).rejects.toThrow(
|
|
175
|
+
'All inputs must have an unlocking script or an unlocking script template for sat/kb fee computation.'
|
|
176
|
+
)
|
|
177
|
+
})
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
// -------------------------------------------------------------------------
|
|
181
|
+
// getVarIntSize thresholds (tested indirectly through computeFee)
|
|
182
|
+
// -------------------------------------------------------------------------
|
|
183
|
+
describe('getVarIntSize thresholds', () => {
|
|
184
|
+
it('uses 1-byte varint for script length <= 253', async () => {
|
|
185
|
+
const model = new SatoshisPerKilobyte(1000)
|
|
186
|
+
const script253 = new Array(253).fill(0x00)
|
|
187
|
+
const tx = makeTx([makeScriptInput(script253)], [])
|
|
188
|
+
const fee = await model.computeFee(tx)
|
|
189
|
+
// script len 253 ≤ 253, so varint is 1 byte
|
|
190
|
+
// size = 4 + 1 + 40 + 1 + 253 + 1 + 4 = 304
|
|
191
|
+
expect(fee).toBe(Math.ceil((304 / 1000) * 1000))
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
it('uses 3-byte varint for script length 254 (> 253)', async () => {
|
|
195
|
+
const model = new SatoshisPerKilobyte(1000)
|
|
196
|
+
const script254 = new Array(254).fill(0x00)
|
|
197
|
+
const tx = makeTx([makeScriptInput(script254)], [])
|
|
198
|
+
const fee = await model.computeFee(tx)
|
|
199
|
+
// script len 254 > 253, so varint is 3 bytes
|
|
200
|
+
// size = 4 + 1 + 40 + 3 + 254 + 1 + 4 = 307
|
|
201
|
+
expect(fee).toBe(Math.ceil((307 / 1000) * 1000))
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
it('uses 5-byte varint for script length > 2^16', async () => {
|
|
205
|
+
const model = new SatoshisPerKilobyte(1000)
|
|
206
|
+
// The condition in getVarIntSize is `i > 2**16` (i.e. strictly greater than 65536).
|
|
207
|
+
// A script of 65537 bytes is the smallest value that triggers the 5-byte varint path.
|
|
208
|
+
const bigScript = new Array(65537).fill(0x00)
|
|
209
|
+
const tx = makeTx([makeScriptInput(bigScript)], [])
|
|
210
|
+
const fee = await model.computeFee(tx)
|
|
211
|
+
// varint = 5 bytes (65537 > 2^16)
|
|
212
|
+
// size = 4 + 1 (input count) + 40 + 5 (script len varint) + 65537 + 1 (output count) + 4 = 65592
|
|
213
|
+
expect(fee).toBe(Math.ceil((65592 / 1000) * 1000))
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
it('uses multiple inputs and outputs correctly', async () => {
|
|
217
|
+
const model = new SatoshisPerKilobyte(1000)
|
|
218
|
+
const unlockScript = new Array(107).fill(0x00)
|
|
219
|
+
const lockScript = new Array(25).fill(0x00)
|
|
220
|
+
const inputs = [
|
|
221
|
+
makeScriptInput(unlockScript),
|
|
222
|
+
makeScriptInput(unlockScript),
|
|
223
|
+
makeScriptInput(unlockScript)
|
|
224
|
+
]
|
|
225
|
+
const outputs = [
|
|
226
|
+
makeOutput(lockScript),
|
|
227
|
+
makeOutput(lockScript)
|
|
228
|
+
]
|
|
229
|
+
const tx = makeTx(inputs, outputs)
|
|
230
|
+
|
|
231
|
+
const fee = await model.computeFee(tx)
|
|
232
|
+
// size = 4 + 1 + 3*(40+1+107) + 1 + 2*(8+1+25) + 4
|
|
233
|
+
// = 4 + 1 + 444 + 1 + 68 + 4 = 522
|
|
234
|
+
expect(fee).toBe(Math.ceil((522 / 1000) * 1000))
|
|
235
|
+
})
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
// -------------------------------------------------------------------------
|
|
239
|
+
// Template vs. compiled script
|
|
240
|
+
// -------------------------------------------------------------------------
|
|
241
|
+
describe('unlockingScriptTemplate path', () => {
|
|
242
|
+
it('calls estimateLength with (tx, inputIndex)', async () => {
|
|
243
|
+
const model = new SatoshisPerKilobyte(1000)
|
|
244
|
+
const templateInput = makeTemplateInput(50)
|
|
245
|
+
const tx = makeTx([templateInput], [])
|
|
246
|
+
await model.computeFee(tx)
|
|
247
|
+
expect(templateInput.unlockingScriptTemplate.estimateLength).toHaveBeenCalledWith(tx, 0)
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
it('correctly handles multiple template inputs, each with different estimated lengths', async () => {
|
|
251
|
+
const model = new SatoshisPerKilobyte(1000)
|
|
252
|
+
const input0 = makeTemplateInput(107)
|
|
253
|
+
const input1 = makeTemplateInput(50)
|
|
254
|
+
const tx = makeTx([input0, input1], [])
|
|
255
|
+
const fee = await model.computeFee(tx)
|
|
256
|
+
// size = 4 + 1 + (40+1+107) + (40+1+50) + 1 + 4 = 249
|
|
257
|
+
expect(fee).toBe(Math.ceil((249 / 1000) * 1000))
|
|
258
|
+
expect(input0.unlockingScriptTemplate.estimateLength).toHaveBeenCalledWith(tx, 0)
|
|
259
|
+
expect(input1.unlockingScriptTemplate.estimateLength).toHaveBeenCalledWith(tx, 1)
|
|
260
|
+
})
|
|
261
|
+
})
|
|
262
|
+
})
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BinaryFetchClient,
|
|
3
|
+
BinaryNodejsHttpClient,
|
|
4
|
+
binaryHttpClient
|
|
5
|
+
} from '../../../transaction/http/BinaryFetchClient'
|
|
6
|
+
import { executeNodejsRequest } from '../../../transaction/http/NodejsHttpRequestUtils'
|
|
7
|
+
|
|
8
|
+
jest.mock('../../../transaction/http/NodejsHttpRequestUtils', () => ({
|
|
9
|
+
executeNodejsRequest: jest.fn()
|
|
10
|
+
}))
|
|
11
|
+
|
|
12
|
+
const mockedExecuteNodejsRequest = executeNodejsRequest as jest.MockedFunction<typeof executeNodejsRequest>
|
|
13
|
+
|
|
14
|
+
describe('BinaryFetchClient', () => {
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
jest.clearAllMocks()
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
function makeMockFetch (opts: {
|
|
20
|
+
ok: boolean
|
|
21
|
+
status: number
|
|
22
|
+
statusText: string
|
|
23
|
+
body: string
|
|
24
|
+
}): jest.Mock {
|
|
25
|
+
return jest.fn().mockResolvedValue({
|
|
26
|
+
ok: opts.ok,
|
|
27
|
+
status: opts.status,
|
|
28
|
+
statusText: opts.statusText,
|
|
29
|
+
text: async () => opts.body
|
|
30
|
+
})
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
it('returns correct HttpClientResponse structure on successful response', async () => {
|
|
34
|
+
const mockFetch = makeMockFetch({
|
|
35
|
+
ok: true,
|
|
36
|
+
status: 200,
|
|
37
|
+
statusText: 'OK',
|
|
38
|
+
body: 'response text'
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
const client = new BinaryFetchClient(mockFetch)
|
|
42
|
+
const result = await client.request('https://example.com', { method: 'GET' })
|
|
43
|
+
|
|
44
|
+
expect(result.ok).toBe(true)
|
|
45
|
+
expect(result.status).toBe(200)
|
|
46
|
+
expect(result.statusText).toBe('OK')
|
|
47
|
+
expect(result.data).toBe('response text')
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('sends correct method to fetch', async () => {
|
|
51
|
+
const mockFetch = makeMockFetch({ ok: true, status: 200, statusText: 'OK', body: '' })
|
|
52
|
+
|
|
53
|
+
const client = new BinaryFetchClient(mockFetch)
|
|
54
|
+
await client.request('https://example.com/endpoint', { method: 'POST' })
|
|
55
|
+
|
|
56
|
+
const [, fetchOptions] = mockFetch.mock.calls[0]
|
|
57
|
+
expect(fetchOptions.method).toBe('POST')
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('sends correct headers to fetch', async () => {
|
|
61
|
+
const mockFetch = makeMockFetch({ ok: true, status: 200, statusText: 'OK', body: '' })
|
|
62
|
+
const headers = { 'Content-Type': 'application/octet-stream', Authorization: 'Bearer token' }
|
|
63
|
+
|
|
64
|
+
const client = new BinaryFetchClient(mockFetch)
|
|
65
|
+
await client.request('https://example.com/endpoint', { method: 'PUT', headers })
|
|
66
|
+
|
|
67
|
+
const [, fetchOptions] = mockFetch.mock.calls[0]
|
|
68
|
+
expect(fetchOptions.headers).toEqual(headers)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('sends data as body to fetch', async () => {
|
|
72
|
+
const mockFetch = makeMockFetch({ ok: true, status: 200, statusText: 'OK', body: '' })
|
|
73
|
+
const data = Buffer.from('binary data')
|
|
74
|
+
|
|
75
|
+
const client = new BinaryFetchClient(mockFetch)
|
|
76
|
+
await client.request('https://example.com/endpoint', { method: 'POST', data })
|
|
77
|
+
|
|
78
|
+
const [, fetchOptions] = mockFetch.mock.calls[0]
|
|
79
|
+
expect(fetchOptions.body).toBe(data)
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('sends correct URL to fetch', async () => {
|
|
83
|
+
const mockFetch = makeMockFetch({ ok: true, status: 200, statusText: 'OK', body: '' })
|
|
84
|
+
const url = 'https://example.com/api/v1/resource'
|
|
85
|
+
|
|
86
|
+
const client = new BinaryFetchClient(mockFetch)
|
|
87
|
+
await client.request(url, { method: 'GET' })
|
|
88
|
+
|
|
89
|
+
const [calledUrl] = mockFetch.mock.calls[0]
|
|
90
|
+
expect(calledUrl).toBe(url)
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('handles response.text() correctly and stores result as data', async () => {
|
|
94
|
+
const responseBody = 'raw binary string content'
|
|
95
|
+
const mockFetch = makeMockFetch({ ok: true, status: 200, statusText: 'OK', body: responseBody })
|
|
96
|
+
|
|
97
|
+
const client = new BinaryFetchClient(mockFetch)
|
|
98
|
+
const result = await client.request<string>('https://example.com', { method: 'GET' })
|
|
99
|
+
|
|
100
|
+
expect(result.data).toBe(responseBody)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('reflects non-ok response correctly', async () => {
|
|
104
|
+
const mockFetch = makeMockFetch({
|
|
105
|
+
ok: false,
|
|
106
|
+
status: 404,
|
|
107
|
+
statusText: 'Not Found',
|
|
108
|
+
body: 'not found'
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
const client = new BinaryFetchClient(mockFetch)
|
|
112
|
+
const result = await client.request('https://example.com/missing', { method: 'GET' })
|
|
113
|
+
|
|
114
|
+
expect(result.ok).toBe(false)
|
|
115
|
+
expect(result.status).toBe(404)
|
|
116
|
+
expect(result.statusText).toBe('Not Found')
|
|
117
|
+
expect(result.data).toBe('not found')
|
|
118
|
+
})
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
describe('BinaryNodejsHttpClient', () => {
|
|
122
|
+
afterEach(() => {
|
|
123
|
+
jest.clearAllMocks()
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it('calls executeNodejsRequest with https module, url, options, and Buffer.from serializer', async () => {
|
|
127
|
+
const mockHttps = {
|
|
128
|
+
request: jest.fn()
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const expectedResponse = { ok: true, status: 200, statusText: 'OK', data: Buffer.from('ok') }
|
|
132
|
+
mockedExecuteNodejsRequest.mockResolvedValue(expectedResponse)
|
|
133
|
+
|
|
134
|
+
const client = new BinaryNodejsHttpClient(mockHttps as any)
|
|
135
|
+
const url = 'https://example.com/binary'
|
|
136
|
+
const options = { method: 'POST', data: Buffer.from('payload') }
|
|
137
|
+
|
|
138
|
+
const result = await client.request(url, options)
|
|
139
|
+
|
|
140
|
+
expect(mockedExecuteNodejsRequest).toHaveBeenCalledTimes(1)
|
|
141
|
+
const [httpsArg, urlArg, optionsArg, serializerArg] = mockedExecuteNodejsRequest.mock.calls[0]
|
|
142
|
+
expect(httpsArg).toBe(mockHttps)
|
|
143
|
+
expect(urlArg).toBe(url)
|
|
144
|
+
expect(optionsArg).toBe(options)
|
|
145
|
+
expect(typeof serializerArg).toBe('function')
|
|
146
|
+
expect(result).toBe(expectedResponse)
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it('serializer passed to executeNodejsRequest wraps data with Buffer.from', async () => {
|
|
150
|
+
const mockHttps = { request: jest.fn() }
|
|
151
|
+
mockedExecuteNodejsRequest.mockResolvedValue({
|
|
152
|
+
ok: true, status: 200, statusText: 'OK', data: ''
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
const client = new BinaryNodejsHttpClient(mockHttps as any)
|
|
156
|
+
await client.request('https://example.com', { method: 'GET' })
|
|
157
|
+
|
|
158
|
+
const serializer = mockedExecuteNodejsRequest.mock.calls[0][3]
|
|
159
|
+
const input = 'test input'
|
|
160
|
+
const result = serializer(input)
|
|
161
|
+
expect(result).toEqual(Buffer.from(input))
|
|
162
|
+
})
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
describe('binaryHttpClient', () => {
|
|
166
|
+
afterEach(() => {
|
|
167
|
+
if ('window' in globalThis) {
|
|
168
|
+
delete (globalThis as any).window
|
|
169
|
+
}
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
it('returns a BinaryFetchClient when window.fetch is available', async () => {
|
|
173
|
+
const mockFetch = jest.fn().mockResolvedValue({
|
|
174
|
+
ok: true,
|
|
175
|
+
status: 200,
|
|
176
|
+
statusText: 'OK',
|
|
177
|
+
text: async () => 'binary content'
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
global.window = { fetch: mockFetch } as unknown as Window & typeof globalThis
|
|
181
|
+
|
|
182
|
+
const client = binaryHttpClient()
|
|
183
|
+
expect(client).toBeDefined()
|
|
184
|
+
expect(client).toBeInstanceOf(BinaryFetchClient)
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
it('BinaryFetchClient returned by binaryHttpClient uses window.fetch', async () => {
|
|
188
|
+
const mockFetch = jest.fn().mockResolvedValue({
|
|
189
|
+
ok: true,
|
|
190
|
+
status: 200,
|
|
191
|
+
statusText: 'OK',
|
|
192
|
+
text: async () => 'content'
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
global.window = { fetch: mockFetch } as unknown as Window & typeof globalThis
|
|
196
|
+
|
|
197
|
+
const client = binaryHttpClient()
|
|
198
|
+
await client.request('https://example.com', { method: 'GET' })
|
|
199
|
+
|
|
200
|
+
expect(mockFetch).toHaveBeenCalledTimes(1)
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
it('returns a BinaryNodejsHttpClient when window is absent but require is available (Node.js path)', async () => {
|
|
204
|
+
if ('window' in globalThis) {
|
|
205
|
+
delete (globalThis as any).window
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const client = binaryHttpClient()
|
|
209
|
+
expect(client).toBeDefined()
|
|
210
|
+
expect(typeof client.request).toBe('function')
|
|
211
|
+
})
|
|
212
|
+
})
|