sol-trade-sdk 0.1.0
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/README.md +390 -0
- package/dist/chunk-MMQAMIKR.mjs +3735 -0
- package/dist/chunk-NEZDFAYA.mjs +7744 -0
- package/dist/clients-VITWK7B6.mjs +1370 -0
- package/dist/index-1BK_FXsW.d.mts +2327 -0
- package/dist/index-1BK_FXsW.d.ts +2327 -0
- package/dist/index.d.mts +2659 -0
- package/dist/index.d.ts +2659 -0
- package/dist/index.js +13265 -0
- package/dist/index.mjs +562 -0
- package/dist/perf/index.d.mts +2 -0
- package/dist/perf/index.d.ts +2 -0
- package/dist/perf/index.js +3742 -0
- package/dist/perf/index.mjs +214 -0
- package/package.json +101 -0
- package/src/__tests__/complete_sdk.test.ts +354 -0
- package/src/__tests__/hotpath.test.ts +486 -0
- package/src/__tests__/nonce.test.ts +45 -0
- package/src/__tests__/sdk.test.ts +425 -0
- package/src/address-lookup/index.ts +197 -0
- package/src/cache/cache.ts +308 -0
- package/src/calc/index.ts +1058 -0
- package/src/calc/pumpfun.ts +124 -0
- package/src/common/bonding_curve.ts +272 -0
- package/src/common/compute-budget.ts +148 -0
- package/src/common/confirm-any-signature.ts +184 -0
- package/src/common/fast-timing.ts +481 -0
- package/src/common/fast_fn.ts +150 -0
- package/src/common/gas-fee-strategy.ts +253 -0
- package/src/common/map-pool.ts +23 -0
- package/src/common/nonce.ts +40 -0
- package/src/common/sdk-log.ts +460 -0
- package/src/common/seed.ts +381 -0
- package/src/common/spl-token.ts +578 -0
- package/src/common/subscription-handle.ts +644 -0
- package/src/common/trading-utils.ts +239 -0
- package/src/common/wsol-manager.ts +325 -0
- package/src/compute/compute_budget_manager.ts +187 -0
- package/src/compute/index.ts +21 -0
- package/src/constants/index.ts +96 -0
- package/src/execution/execution.ts +532 -0
- package/src/execution/index.ts +42 -0
- package/src/hotpath/executor.ts +464 -0
- package/src/hotpath/index.ts +64 -0
- package/src/hotpath/state.ts +435 -0
- package/src/index.ts +2117 -0
- package/src/instruction/bonk_builder.ts +730 -0
- package/src/instruction/index.ts +24 -0
- package/src/instruction/meteora_damm_v2_builder.ts +509 -0
- package/src/instruction/pumpfun_builder.ts +1183 -0
- package/src/instruction/pumpswap.ts +1123 -0
- package/src/instruction/raydium_amm_v4_builder.ts +692 -0
- package/src/instruction/raydium_cpmm_builder.ts +795 -0
- package/src/middleware/traits.ts +407 -0
- package/src/params/index.ts +483 -0
- package/src/perf/compiler-optimization.ts +529 -0
- package/src/perf/hardware.ts +631 -0
- package/src/perf/index.ts +9 -0
- package/src/perf/kernel-bypass.ts +656 -0
- package/src/perf/protocol.ts +682 -0
- package/src/perf/realtime.ts +592 -0
- package/src/perf/simd.ts +668 -0
- package/src/perf/syscall-bypass.ts +331 -0
- package/src/perf/ultra-low-latency.ts +505 -0
- package/src/perf/zero-copy.ts +589 -0
- package/src/pool/pool.ts +294 -0
- package/src/rpc/client.ts +345 -0
- package/src/sdk-errors.ts +13 -0
- package/src/security/index.ts +26 -0
- package/src/security/secure-key.ts +303 -0
- package/src/security/validators.ts +281 -0
- package/src/seed/pda.ts +262 -0
- package/src/serialization/index.ts +28 -0
- package/src/serialization/serialization.ts +288 -0
- package/src/swqos/clients.ts +1754 -0
- package/src/swqos/index.ts +50 -0
- package/src/swqos/providers.ts +1707 -0
- package/src/trading/core/async-executor.ts +702 -0
- package/src/trading/core/confirmation-monitor.ts +711 -0
- package/src/trading/core/index.ts +82 -0
- package/src/trading/core/retry-handler.ts +683 -0
- package/src/trading/core/transaction-pool.ts +780 -0
- package/src/trading/executor.ts +385 -0
- package/src/trading/factory.ts +282 -0
- package/src/trading/index.ts +30 -0
- package/src/types.ts +8 -0
- package/src/utils/index.ts +155 -0
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Comprehensive tests for Hot Path modules
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
6
|
+
import {
|
|
7
|
+
Connection,
|
|
8
|
+
PublicKey,
|
|
9
|
+
Keypair,
|
|
10
|
+
} from '@solana/web3.js';
|
|
11
|
+
import {
|
|
12
|
+
HotPathConfig,
|
|
13
|
+
HotPathState,
|
|
14
|
+
HotPathExecutor,
|
|
15
|
+
HotPathMetrics,
|
|
16
|
+
TradingContext,
|
|
17
|
+
defaultHotPathConfig,
|
|
18
|
+
StaleBlockhashError,
|
|
19
|
+
MissingAccountError,
|
|
20
|
+
createHotPathExecutor,
|
|
21
|
+
} from '../hotpath';
|
|
22
|
+
|
|
23
|
+
// ===== Mocks =====
|
|
24
|
+
|
|
25
|
+
const mockConnection = {
|
|
26
|
+
getLatestBlockhash: vi.fn(),
|
|
27
|
+
getMultipleAccountsInfo: vi.fn(),
|
|
28
|
+
getSignatureStatus: vi.fn(),
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
vi.mock('@solana/web3.js', () => ({
|
|
32
|
+
Connection: vi.fn(() => mockConnection),
|
|
33
|
+
PublicKey: vi.fn((key: string) => ({ toBase58: () => key })),
|
|
34
|
+
Keypair: vi.fn(),
|
|
35
|
+
}));
|
|
36
|
+
|
|
37
|
+
// ===== HotPathConfig Tests =====
|
|
38
|
+
|
|
39
|
+
describe('HotPathConfig', () => {
|
|
40
|
+
it('should have default values', () => {
|
|
41
|
+
const config = defaultHotPathConfig();
|
|
42
|
+
expect(config.blockhashRefreshIntervalMs).toBe(2000);
|
|
43
|
+
expect(config.cacheTtlMs).toBe(5000);
|
|
44
|
+
expect(config.enablePrefetch).toBe(true);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should allow custom values', () => {
|
|
48
|
+
const config: HotPathConfig = {
|
|
49
|
+
blockhashRefreshIntervalMs: 1000,
|
|
50
|
+
cacheTtlMs: 3000,
|
|
51
|
+
enablePrefetch: false,
|
|
52
|
+
prefetchTimeoutMs: 3000,
|
|
53
|
+
};
|
|
54
|
+
expect(config.blockhashRefreshIntervalMs).toBe(1000);
|
|
55
|
+
expect(config.cacheTtlMs).toBe(3000);
|
|
56
|
+
expect(config.enablePrefetch).toBe(false);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// ===== HotPathState Tests =====
|
|
61
|
+
|
|
62
|
+
describe('HotPathState', () => {
|
|
63
|
+
let state: HotPathState;
|
|
64
|
+
const connection = new Connection('http://localhost');
|
|
65
|
+
|
|
66
|
+
beforeEach(() => {
|
|
67
|
+
vi.clearAllMocks();
|
|
68
|
+
state = new HotPathState(connection, {
|
|
69
|
+
enablePrefetch: false,
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
afterEach(() => {
|
|
74
|
+
state.stop();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('should return null when no blockhash is cached', () => {
|
|
78
|
+
const result = state.getBlockhash();
|
|
79
|
+
expect(result).toBeNull();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should return blockhash when cached', async () => {
|
|
83
|
+
mockConnection.getLatestBlockhash.mockResolvedValueOnce({
|
|
84
|
+
blockhash: 'test_blockhash',
|
|
85
|
+
lastValidBlockHeight: 100,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Create state with prefetch enabled
|
|
89
|
+
const stateWithPrefetch = new HotPathState(connection, {
|
|
90
|
+
enablePrefetch: true,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
await stateWithPrefetch.start();
|
|
94
|
+
|
|
95
|
+
// Wait for initial prefetch
|
|
96
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
97
|
+
|
|
98
|
+
const result = stateWithPrefetch.getBlockhash();
|
|
99
|
+
expect(result).not.toBeNull();
|
|
100
|
+
expect(result?.blockhash).toBe('test_blockhash');
|
|
101
|
+
|
|
102
|
+
stateWithPrefetch.stop();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('should detect stale data', () => {
|
|
106
|
+
// Data is not fresh when nothing has been fetched
|
|
107
|
+
expect(state.isDataFresh()).toBe(false);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('should prefetch accounts', async () => {
|
|
111
|
+
mockConnection.getMultipleAccountsInfo.mockResolvedValueOnce([
|
|
112
|
+
{
|
|
113
|
+
data: Buffer.from('test_data'),
|
|
114
|
+
lamports: 1000000n,
|
|
115
|
+
owner: { toBase58: () => 'owner' },
|
|
116
|
+
executable: false,
|
|
117
|
+
rentEpoch: 0,
|
|
118
|
+
},
|
|
119
|
+
]);
|
|
120
|
+
|
|
121
|
+
await state.prefetchAccounts(['pubkey1']);
|
|
122
|
+
|
|
123
|
+
const account = state.getAccount('pubkey1');
|
|
124
|
+
expect(account).not.toBeNull();
|
|
125
|
+
expect(account?.data.toString()).toBe('test_data');
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('should update and retrieve account state', () => {
|
|
129
|
+
const accountState = {
|
|
130
|
+
pubkey: 'test_pubkey',
|
|
131
|
+
data: Buffer.from('test_data'),
|
|
132
|
+
lamports: 1000000n,
|
|
133
|
+
owner: 'owner',
|
|
134
|
+
executable: false,
|
|
135
|
+
rentEpoch: 0,
|
|
136
|
+
slot: 100,
|
|
137
|
+
fetchedAt: Date.now(),
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
state.updateAccount('test_pubkey', accountState);
|
|
141
|
+
|
|
142
|
+
const retrieved = state.getAccount('test_pubkey');
|
|
143
|
+
expect(retrieved).not.toBeNull();
|
|
144
|
+
expect(retrieved?.lamports).toBe(1000000n);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('should return null for stale account', () => {
|
|
148
|
+
const accountState = {
|
|
149
|
+
pubkey: 'stale_pubkey',
|
|
150
|
+
data: Buffer.from('data'),
|
|
151
|
+
lamports: 0n,
|
|
152
|
+
owner: '',
|
|
153
|
+
executable: false,
|
|
154
|
+
rentEpoch: 0,
|
|
155
|
+
slot: 0,
|
|
156
|
+
fetchedAt: Date.now() - 10000, // 10 seconds ago
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
state.updateAccount('stale_pubkey', accountState);
|
|
160
|
+
|
|
161
|
+
const retrieved = state.getAccount('stale_pubkey');
|
|
162
|
+
expect(retrieved).toBeNull();
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('should return metrics', () => {
|
|
166
|
+
const metrics = state.getMetrics();
|
|
167
|
+
expect(metrics).toHaveProperty('prefetchCount');
|
|
168
|
+
expect(metrics).toHaveProperty('prefetchErrors');
|
|
169
|
+
expect(metrics).toHaveProperty('accountsCached');
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// ===== TradingContext Tests =====
|
|
174
|
+
|
|
175
|
+
describe('TradingContext', () => {
|
|
176
|
+
let state: HotPathState;
|
|
177
|
+
const connection = new Connection('http://localhost');
|
|
178
|
+
|
|
179
|
+
beforeEach(() => {
|
|
180
|
+
vi.clearAllMocks();
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
afterEach(() => {
|
|
184
|
+
state?.stop();
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('should create context with cached blockhash', async () => {
|
|
188
|
+
// Set up cached blockhash
|
|
189
|
+
mockConnection.getLatestBlockhash.mockResolvedValueOnce({
|
|
190
|
+
blockhash: 'test_blockhash',
|
|
191
|
+
lastValidBlockHeight: 100,
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// Create state with prefetch enabled
|
|
195
|
+
state = new HotPathState(connection, { enablePrefetch: true });
|
|
196
|
+
await state.start();
|
|
197
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
198
|
+
|
|
199
|
+
const context = new TradingContext(state, 'payer_pubkey');
|
|
200
|
+
expect(context.blockhash).toBe('test_blockhash');
|
|
201
|
+
expect(context.lastValidBlockHeight).toBe(100);
|
|
202
|
+
expect(context.payer).toBe('payer_pubkey');
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('should throw error for stale blockhash', () => {
|
|
206
|
+
// Create state without prefetch - no blockhash available
|
|
207
|
+
const stateNoPrefetch = new HotPathState(connection, { enablePrefetch: false });
|
|
208
|
+
expect(() => {
|
|
209
|
+
new TradingContext(stateNoPrefetch, 'payer_pubkey');
|
|
210
|
+
}).toThrow(StaleBlockhashError);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('should add account to context', async () => {
|
|
214
|
+
mockConnection.getLatestBlockhash.mockResolvedValueOnce({
|
|
215
|
+
blockhash: 'test_blockhash',
|
|
216
|
+
lastValidBlockHeight: 100,
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
// Create state with prefetch enabled
|
|
220
|
+
state = new HotPathState(connection, { enablePrefetch: true });
|
|
221
|
+
await state.start();
|
|
222
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
223
|
+
|
|
224
|
+
const context = new TradingContext(state, 'payer');
|
|
225
|
+
|
|
226
|
+
// Add account to state
|
|
227
|
+
state.updateAccount('token_account', {
|
|
228
|
+
pubkey: 'token_account',
|
|
229
|
+
data: Buffer.from('data'),
|
|
230
|
+
lamports: 1000n,
|
|
231
|
+
owner: 'owner',
|
|
232
|
+
executable: false,
|
|
233
|
+
rentEpoch: 0,
|
|
234
|
+
slot: 0,
|
|
235
|
+
fetchedAt: Date.now(),
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
const added = context.addAccount('token_account', state);
|
|
239
|
+
expect(added).toBe(true);
|
|
240
|
+
expect(context.accountStates.has('token_account')).toBe(true);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it('should calculate age correctly', async () => {
|
|
244
|
+
mockConnection.getLatestBlockhash.mockResolvedValueOnce({
|
|
245
|
+
blockhash: 'test_blockhash',
|
|
246
|
+
lastValidBlockHeight: 100,
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
// Create state with prefetch enabled
|
|
250
|
+
state = new HotPathState(connection, { enablePrefetch: true });
|
|
251
|
+
await state.start();
|
|
252
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
253
|
+
|
|
254
|
+
const context = new TradingContext(state, 'payer');
|
|
255
|
+
|
|
256
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
257
|
+
|
|
258
|
+
expect(context.age()).toBeGreaterThanOrEqual(100);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it('should check validity', async () => {
|
|
262
|
+
mockConnection.getLatestBlockhash.mockResolvedValueOnce({
|
|
263
|
+
blockhash: 'test_blockhash',
|
|
264
|
+
lastValidBlockHeight: 100,
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
// Create state with prefetch enabled
|
|
268
|
+
state = new HotPathState(connection, { enablePrefetch: true });
|
|
269
|
+
await state.start();
|
|
270
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
271
|
+
|
|
272
|
+
const context = new TradingContext(state, 'payer');
|
|
273
|
+
|
|
274
|
+
expect(context.isValid(5000)).toBe(true);
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
// ===== HotPathMetrics Tests =====
|
|
279
|
+
|
|
280
|
+
describe('HotPathMetrics', () => {
|
|
281
|
+
let metrics: HotPathMetrics;
|
|
282
|
+
|
|
283
|
+
beforeEach(() => {
|
|
284
|
+
metrics = new HotPathMetrics();
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it('should record successful trades', () => {
|
|
288
|
+
metrics.record(true, 100);
|
|
289
|
+
|
|
290
|
+
const stats = metrics.getStats();
|
|
291
|
+
expect(stats.totalTrades).toBe(1);
|
|
292
|
+
expect(stats.successTrades).toBe(1);
|
|
293
|
+
expect(stats.failedTrades).toBe(0);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it('should record failed trades', () => {
|
|
297
|
+
metrics.record(false, 50);
|
|
298
|
+
|
|
299
|
+
const stats = metrics.getStats();
|
|
300
|
+
expect(stats.totalTrades).toBe(1);
|
|
301
|
+
expect(stats.failedTrades).toBe(1);
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it('should calculate average latency', () => {
|
|
305
|
+
metrics.record(true, 100);
|
|
306
|
+
metrics.record(true, 200);
|
|
307
|
+
|
|
308
|
+
const stats = metrics.getStats();
|
|
309
|
+
expect(stats.avgLatencyMs).toBe(150);
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it('should return zero average for no trades', () => {
|
|
313
|
+
const stats = metrics.getStats();
|
|
314
|
+
expect(stats.avgLatencyMs).toBe(0);
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
// ===== HotPathExecutor Tests =====
|
|
319
|
+
|
|
320
|
+
describe('HotPathExecutor', () => {
|
|
321
|
+
let executor: HotPathExecutor;
|
|
322
|
+
const connection = new Connection('http://localhost');
|
|
323
|
+
|
|
324
|
+
const mockSwqosClient = {
|
|
325
|
+
getSwqosType: () => 'jito' as const,
|
|
326
|
+
sendTransaction: vi.fn(),
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
beforeEach(() => {
|
|
330
|
+
vi.clearAllMocks();
|
|
331
|
+
executor = new HotPathExecutor(connection, {
|
|
332
|
+
enablePrefetch: false,
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
afterEach(() => {
|
|
337
|
+
executor.stop();
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
it('should add SWQoS client', () => {
|
|
341
|
+
executor.addSwqosClient(mockSwqosClient as any);
|
|
342
|
+
expect(executor.getSwqosClient('jito')).toBeDefined();
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
it('should remove SWQoS client', () => {
|
|
346
|
+
executor.addSwqosClient(mockSwqosClient as any);
|
|
347
|
+
executor.removeSwqosClient('jito');
|
|
348
|
+
expect(executor.getSwqosClient('jito')).toBeUndefined();
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
it('should check readiness', () => {
|
|
352
|
+
expect(executor.isReady()).toBe(false); // No blockhash, no clients
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
it('should return metrics', () => {
|
|
356
|
+
const metrics = executor.getMetrics();
|
|
357
|
+
expect(metrics).toHaveProperty('totalTrades');
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
it('should fail execution with no clients', async () => {
|
|
361
|
+
mockConnection.getLatestBlockhash.mockResolvedValueOnce({
|
|
362
|
+
blockhash: 'test_blockhash',
|
|
363
|
+
lastValidBlockHeight: 100,
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
// Create executor with prefetch enabled to get blockhash
|
|
367
|
+
const executorWithPrefetch = new HotPathExecutor(connection, {
|
|
368
|
+
enablePrefetch: true,
|
|
369
|
+
});
|
|
370
|
+
await executorWithPrefetch.start();
|
|
371
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
372
|
+
|
|
373
|
+
const result = await executorWithPrefetch.execute('buy' as any, Buffer.from('tx'));
|
|
374
|
+
|
|
375
|
+
expect(result.success).toBe(false);
|
|
376
|
+
expect(result.error).toContain('No SWQoS clients');
|
|
377
|
+
|
|
378
|
+
executorWithPrefetch.stop();
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
it('should fail execution with stale blockhash', async () => {
|
|
382
|
+
executor.addSwqosClient(mockSwqosClient as any);
|
|
383
|
+
|
|
384
|
+
const result = await executor.execute('buy' as any, Buffer.from('tx'));
|
|
385
|
+
|
|
386
|
+
expect(result.success).toBe(false);
|
|
387
|
+
expect(result.error).toContain('Stale blockhash');
|
|
388
|
+
});
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
// ===== createHotPathExecutor Tests =====
|
|
392
|
+
|
|
393
|
+
describe('createHotPathExecutor', () => {
|
|
394
|
+
it('should create executor with default config', () => {
|
|
395
|
+
const connection = new Connection('http://localhost');
|
|
396
|
+
const executor = createHotPathExecutor(connection);
|
|
397
|
+
expect(executor).toBeDefined();
|
|
398
|
+
expect(executor).toBeInstanceOf(HotPathExecutor);
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
it('should create executor with custom config', () => {
|
|
402
|
+
const connection = new Connection('http://localhost');
|
|
403
|
+
const executor = createHotPathExecutor(connection, [], {
|
|
404
|
+
blockhashRefreshIntervalMs: 1000,
|
|
405
|
+
});
|
|
406
|
+
expect(executor).toBeDefined();
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
it('should create executor with SWQoS clients', () => {
|
|
410
|
+
const connection = new Connection('http://localhost');
|
|
411
|
+
const mockClient = {
|
|
412
|
+
getSwqosType: () => 'jito' as const,
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
const executor = createHotPathExecutor(connection, [mockClient as any]);
|
|
416
|
+
expect(executor.getSwqosClient('jito')).toBeDefined();
|
|
417
|
+
});
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
// ===== Error Tests =====
|
|
421
|
+
|
|
422
|
+
describe('Errors', () => {
|
|
423
|
+
it('should create StaleBlockhashError', () => {
|
|
424
|
+
const error = new StaleBlockhashError('test message');
|
|
425
|
+
expect(error).toBeInstanceOf(Error);
|
|
426
|
+
expect(error.name).toBe('StaleBlockhashError');
|
|
427
|
+
expect(error.message).toBe('test message');
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
it('should create MissingAccountError', () => {
|
|
431
|
+
const error = new MissingAccountError('account not found');
|
|
432
|
+
expect(error).toBeInstanceOf(Error);
|
|
433
|
+
expect(error.name).toBe('MissingAccountError');
|
|
434
|
+
});
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
// ===== Concurrent Access Tests =====
|
|
438
|
+
|
|
439
|
+
describe('Concurrent Access', () => {
|
|
440
|
+
it('should handle concurrent account updates', async () => {
|
|
441
|
+
const connection = new Connection('http://localhost');
|
|
442
|
+
const state = new HotPathState(connection, { enablePrefetch: false });
|
|
443
|
+
|
|
444
|
+
const promises = Array.from({ length: 100 }, (_, i) => {
|
|
445
|
+
return Promise.resolve().then(() => {
|
|
446
|
+
state.updateAccount(`pubkey_${i}`, {
|
|
447
|
+
pubkey: `pubkey_${i}`,
|
|
448
|
+
data: Buffer.from(`data_${i}`),
|
|
449
|
+
lamports: BigInt(i),
|
|
450
|
+
owner: 'owner',
|
|
451
|
+
executable: false,
|
|
452
|
+
rentEpoch: 0,
|
|
453
|
+
slot: 0,
|
|
454
|
+
fetchedAt: Date.now(),
|
|
455
|
+
});
|
|
456
|
+
});
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
await Promise.all(promises);
|
|
460
|
+
|
|
461
|
+
const metrics = state.getMetrics();
|
|
462
|
+
expect(metrics.accountsCached).toBe(100);
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
it('should handle concurrent context creation', async () => {
|
|
466
|
+
const connection = new Connection('http://localhost');
|
|
467
|
+
const state = new HotPathState(connection, { enablePrefetch: true });
|
|
468
|
+
|
|
469
|
+
mockConnection.getLatestBlockhash.mockResolvedValue({
|
|
470
|
+
blockhash: 'test_blockhash',
|
|
471
|
+
lastValidBlockHeight: 100,
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
await state.start();
|
|
475
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
476
|
+
|
|
477
|
+
const promises = Array.from({ length: 10 }, () => {
|
|
478
|
+
return new TradingContext(state, 'payer');
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
expect(promises.length).toBe(10);
|
|
482
|
+
for (const ctx of promises) {
|
|
483
|
+
expect(ctx.blockhash).toBe('test_blockhash');
|
|
484
|
+
}
|
|
485
|
+
});
|
|
486
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rust `nonce_cache::fetch_nonce_info` parity
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect } from 'vitest';
|
|
6
|
+
import { PublicKey, type Connection } from '@solana/web3.js';
|
|
7
|
+
import bs58 from 'bs58';
|
|
8
|
+
import { fetchDurableNonceInfo } from '../common/nonce';
|
|
9
|
+
|
|
10
|
+
describe('fetchDurableNonceInfo', () => {
|
|
11
|
+
it('parses authority at 8..40 and blockhash at 40..72', async () => {
|
|
12
|
+
const auth = new PublicKey(Buffer.alloc(32, 7));
|
|
13
|
+
const hashBytes = Buffer.alloc(32);
|
|
14
|
+
hashBytes[0] = 9;
|
|
15
|
+
const data = Buffer.alloc(80);
|
|
16
|
+
auth.toBuffer().copy(data, 8);
|
|
17
|
+
hashBytes.copy(data, 40);
|
|
18
|
+
const conn = {
|
|
19
|
+
getAccountInfo: async () => ({ data, owner: PublicKey.default }),
|
|
20
|
+
} as Pick<Connection, 'getAccountInfo'>;
|
|
21
|
+
const noncePk = new PublicKey(Buffer.alloc(32, 3));
|
|
22
|
+
const got = await fetchDurableNonceInfo(conn, noncePk);
|
|
23
|
+
expect(got).not.toBeNull();
|
|
24
|
+
expect(got!.authority.equals(auth)).toBe(true);
|
|
25
|
+
expect(got!.nonceHash).toBe(bs58.encode(hashBytes));
|
|
26
|
+
expect(got!.recentBlockhash).toBe(got!.nonceHash);
|
|
27
|
+
expect(got!.nonceAccount.equals(noncePk)).toBe(true);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('returns null when account data too short', async () => {
|
|
31
|
+
const conn = {
|
|
32
|
+
getAccountInfo: async () => ({ data: Buffer.alloc(10) }),
|
|
33
|
+
} as Pick<Connection, 'getAccountInfo'>;
|
|
34
|
+
const got = await fetchDurableNonceInfo(conn, PublicKey.default);
|
|
35
|
+
expect(got).toBeNull();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('returns null when account missing', async () => {
|
|
39
|
+
const conn = {
|
|
40
|
+
getAccountInfo: async () => null,
|
|
41
|
+
} as Pick<Connection, 'getAccountInfo'>;
|
|
42
|
+
const got = await fetchDurableNonceInfo(conn, PublicKey.default);
|
|
43
|
+
expect(got).toBeNull();
|
|
44
|
+
});
|
|
45
|
+
});
|