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,425 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for Sol Trade SDK - TypeScript
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
6
|
+
import { PublicKey, TransactionInstruction } from '@solana/web3.js';
|
|
7
|
+
import {
|
|
8
|
+
GasFeeStrategy,
|
|
9
|
+
GasFeeStrategyType,
|
|
10
|
+
createGasFeeStrategy,
|
|
11
|
+
SwqosType,
|
|
12
|
+
TradeType,
|
|
13
|
+
TradeConfigBuilder,
|
|
14
|
+
MiddlewareManager,
|
|
15
|
+
InstructionProcessor,
|
|
16
|
+
MAX_INSTRUCTIONS_WARN,
|
|
17
|
+
ExecutionPath,
|
|
18
|
+
createTradeConfig,
|
|
19
|
+
RUST_PARITY_SIMULATE_CONFIG,
|
|
20
|
+
commitmentToGetTxFinality,
|
|
21
|
+
type InstructionMiddleware,
|
|
22
|
+
} from '../index';
|
|
23
|
+
import {
|
|
24
|
+
confirmAnyTransactionSignature,
|
|
25
|
+
extractHintsFromLogs,
|
|
26
|
+
instructionErrorCodeFromMetaErr,
|
|
27
|
+
} from '../common/confirm-any-signature';
|
|
28
|
+
import { mapWithConcurrencyLimit } from '../common/map-pool';
|
|
29
|
+
import { Connection } from '@solana/web3.js';
|
|
30
|
+
import { SOL_TOKEN_ACCOUNT, WSOL_TOKEN_ACCOUNT } from '../constants';
|
|
31
|
+
import {
|
|
32
|
+
computeFee,
|
|
33
|
+
ceilDiv,
|
|
34
|
+
calculateWithSlippageBuy,
|
|
35
|
+
calculateWithSlippageSell,
|
|
36
|
+
getBuyTokenAmountFromSolAmount,
|
|
37
|
+
getSellSolAmountFromTokenAmount,
|
|
38
|
+
} from '../calc';
|
|
39
|
+
|
|
40
|
+
describe('GasFeeStrategy', () => {
|
|
41
|
+
let strategy: GasFeeStrategy;
|
|
42
|
+
|
|
43
|
+
beforeEach(() => {
|
|
44
|
+
strategy = new GasFeeStrategy();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should create a gas fee strategy', () => {
|
|
48
|
+
expect(strategy).toBeDefined();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should set and get a strategy', () => {
|
|
52
|
+
strategy.set(
|
|
53
|
+
SwqosType.Jito,
|
|
54
|
+
TradeType.Buy,
|
|
55
|
+
GasFeeStrategyType.Normal,
|
|
56
|
+
200000,
|
|
57
|
+
100000,
|
|
58
|
+
0.001
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
const value = strategy.get(SwqosType.Jito, TradeType.Buy, GasFeeStrategyType.Normal);
|
|
62
|
+
expect(value).toBeDefined();
|
|
63
|
+
expect(value?.cuLimit).toBe(200000);
|
|
64
|
+
expect(value?.cuPrice).toBe(100000);
|
|
65
|
+
expect(value?.tip).toBe(0.001);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('should set global fee strategy', () => {
|
|
69
|
+
const globalStrategy = createGasFeeStrategy();
|
|
70
|
+
|
|
71
|
+
// Set global fee strategy first
|
|
72
|
+
globalStrategy.setGlobalFeeStrategy(
|
|
73
|
+
200000, 200000, // buy/sell CU limit
|
|
74
|
+
100000, 100000, // buy/sell CU price
|
|
75
|
+
100000, 100000 // buy/sell tip
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
// Check that strategies are set for common SWQOS types
|
|
79
|
+
const jitoValue = globalStrategy.get(SwqosType.Jito, TradeType.Buy, GasFeeStrategyType.Normal);
|
|
80
|
+
expect(jitoValue).toBeDefined();
|
|
81
|
+
expect(jitoValue?.cuLimit).toBe(200000);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('should update buy tip for all strategies', () => {
|
|
85
|
+
strategy.set(SwqosType.Jito, TradeType.Buy, GasFeeStrategyType.Normal, 200000, 100000, 0.001);
|
|
86
|
+
strategy.set(SwqosType.Jito, TradeType.Sell, GasFeeStrategyType.Normal, 200000, 100000, 0.002);
|
|
87
|
+
|
|
88
|
+
strategy.updateBuyTip(0.005);
|
|
89
|
+
|
|
90
|
+
const buyValue = strategy.get(SwqosType.Jito, TradeType.Buy, GasFeeStrategyType.Normal);
|
|
91
|
+
const sellValue = strategy.get(SwqosType.Jito, TradeType.Sell, GasFeeStrategyType.Normal);
|
|
92
|
+
|
|
93
|
+
expect(buyValue?.tip).toBe(0.005);
|
|
94
|
+
expect(sellValue?.tip).toBe(0.002);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('should delete a strategy', () => {
|
|
98
|
+
strategy.set(SwqosType.Jito, TradeType.Buy, GasFeeStrategyType.Normal, 200000, 100000, 0.001);
|
|
99
|
+
strategy.delete(SwqosType.Jito, TradeType.Buy, GasFeeStrategyType.Normal);
|
|
100
|
+
|
|
101
|
+
const value = strategy.get(SwqosType.Jito, TradeType.Buy, GasFeeStrategyType.Normal);
|
|
102
|
+
expect(value).toBeUndefined();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('should resolve conflicts when setting Normal strategy', () => {
|
|
106
|
+
// Set high/low strategies first
|
|
107
|
+
strategy.set(
|
|
108
|
+
SwqosType.Jito, TradeType.Buy, GasFeeStrategyType.LowTipHighCuPrice,
|
|
109
|
+
200000, 100000, 0.0005
|
|
110
|
+
);
|
|
111
|
+
strategy.set(
|
|
112
|
+
SwqosType.Jito, TradeType.Buy, GasFeeStrategyType.HighTipLowCuPrice,
|
|
113
|
+
200000, 100000, 0.002
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
// Set Normal strategy (should remove high/low)
|
|
117
|
+
strategy.set(
|
|
118
|
+
SwqosType.Jito, TradeType.Buy, GasFeeStrategyType.Normal,
|
|
119
|
+
200000, 100000, 0.001
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
const low = strategy.get(SwqosType.Jito, TradeType.Buy, GasFeeStrategyType.LowTipHighCuPrice);
|
|
123
|
+
const high = strategy.get(SwqosType.Jito, TradeType.Buy, GasFeeStrategyType.HighTipLowCuPrice);
|
|
124
|
+
const normal = strategy.get(SwqosType.Jito, TradeType.Buy, GasFeeStrategyType.Normal);
|
|
125
|
+
|
|
126
|
+
expect(low).toBeUndefined();
|
|
127
|
+
expect(high).toBeUndefined();
|
|
128
|
+
expect(normal).toBeDefined();
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('setHighLowFeeStrategies exposes two strategy rows per SWQOS (Rust parity)', () => {
|
|
132
|
+
strategy.setHighLowFeeStrategies(
|
|
133
|
+
[SwqosType.Jito],
|
|
134
|
+
TradeType.Buy,
|
|
135
|
+
200000,
|
|
136
|
+
1000,
|
|
137
|
+
500000,
|
|
138
|
+
0.0001,
|
|
139
|
+
0.0005
|
|
140
|
+
);
|
|
141
|
+
const rows = strategy
|
|
142
|
+
.getStrategies(TradeType.Buy)
|
|
143
|
+
.filter((r) => r.swqosType === SwqosType.Jito);
|
|
144
|
+
expect(rows.length).toBe(2);
|
|
145
|
+
const st = new Set(rows.map((r) => r.strategyType));
|
|
146
|
+
expect(st.has(GasFeeStrategyType.LowTipHighCuPrice)).toBe(true);
|
|
147
|
+
expect(st.has(GasFeeStrategyType.HighTipLowCuPrice)).toBe(true);
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
describe('Calculations', () => {
|
|
152
|
+
it('should compute fee correctly', () => {
|
|
153
|
+
const fee = computeFee(1000000n, 100n, 10000n); // 1%
|
|
154
|
+
expect(fee).toBe(10000n);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('should perform ceiling division', () => {
|
|
158
|
+
expect(ceilDiv(10n, 3n)).toBe(4n);
|
|
159
|
+
expect(ceilDiv(9n, 3n)).toBe(3n);
|
|
160
|
+
expect(ceilDiv(11n, 3n)).toBe(4n);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('should calculate with slippage for buy', () => {
|
|
164
|
+
const result = calculateWithSlippageBuy(1000n, 100n); // 1% slippage
|
|
165
|
+
expect(result).toBe(1010n);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('should calculate with slippage for sell', () => {
|
|
169
|
+
const result = calculateWithSlippageSell(1000n, 100n); // 1% slippage
|
|
170
|
+
expect(result).toBe(990n);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('should calculate PumpFun buy output', () => {
|
|
174
|
+
const tokens = getBuyTokenAmountFromSolAmount(
|
|
175
|
+
1000000n, // 0.001 SOL
|
|
176
|
+
30000000000n,
|
|
177
|
+
1073000000000000n,
|
|
178
|
+
false,
|
|
179
|
+
793000000000000n
|
|
180
|
+
);
|
|
181
|
+
expect(tokens > 0n).toBe(true);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('should calculate PumpFun sell output', () => {
|
|
185
|
+
const sol = getSellSolAmountFromTokenAmount(
|
|
186
|
+
1000000000n, // 1 million tokens
|
|
187
|
+
30000000000n,
|
|
188
|
+
1073000000000000n,
|
|
189
|
+
1000000000n
|
|
190
|
+
);
|
|
191
|
+
expect(sol > 0n).toBe(true);
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
describe('createTradeConfig', () => {
|
|
196
|
+
it('accepts optional commitment and flags', () => {
|
|
197
|
+
const cfg = createTradeConfig('https://x', [], {
|
|
198
|
+
commitment: 'finalized',
|
|
199
|
+
checkMinTip: true,
|
|
200
|
+
});
|
|
201
|
+
expect(cfg.commitment).toBe('finalized');
|
|
202
|
+
expect(cfg.checkMinTip).toBe(true);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('passes through builder-style fields', () => {
|
|
206
|
+
const cfg = createTradeConfig('https://x', [], {
|
|
207
|
+
maxSwqosSubmitConcurrency: 4,
|
|
208
|
+
mevProtection: true,
|
|
209
|
+
});
|
|
210
|
+
expect(cfg.maxSwqosSubmitConcurrency).toBe(4);
|
|
211
|
+
expect(cfg.mevProtection).toBe(true);
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
describe('commitmentToGetTxFinality', () => {
|
|
216
|
+
it('maps non-finality commitments to confirmed', () => {
|
|
217
|
+
expect(commitmentToGetTxFinality('recent')).toBe('confirmed');
|
|
218
|
+
expect(commitmentToGetTxFinality('processed')).toBe('confirmed');
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('preserves finalized', () => {
|
|
222
|
+
expect(commitmentToGetTxFinality('finalized')).toBe('finalized');
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
describe('RUST_PARITY_SIMULATE_CONFIG', () => {
|
|
227
|
+
it('matches Rust simulate_transaction RPC flags', () => {
|
|
228
|
+
expect(RUST_PARITY_SIMULATE_CONFIG.sigVerify).toBe(false);
|
|
229
|
+
expect(RUST_PARITY_SIMULATE_CONFIG.replaceRecentBlockhash).toBe(false);
|
|
230
|
+
expect(RUST_PARITY_SIMULATE_CONFIG.commitment).toBe('processed');
|
|
231
|
+
expect(RUST_PARITY_SIMULATE_CONFIG.innerInstructions).toBe(true);
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
describe('mapWithConcurrencyLimit', () => {
|
|
236
|
+
it('caps concurrent workers', async () => {
|
|
237
|
+
let active = 0;
|
|
238
|
+
let peak = 0;
|
|
239
|
+
const out = await mapWithConcurrencyLimit([1, 2, 3, 4, 5], 2, async (n) => {
|
|
240
|
+
active++;
|
|
241
|
+
peak = Math.max(peak, active);
|
|
242
|
+
await new Promise((r) => setTimeout(r, 5));
|
|
243
|
+
active--;
|
|
244
|
+
return n * 2;
|
|
245
|
+
});
|
|
246
|
+
expect(peak).toBeLessThanOrEqual(2);
|
|
247
|
+
expect(out).toEqual([2, 4, 6, 8, 10]);
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
describe('extractHintsFromLogs', () => {
|
|
252
|
+
it('parses Rust log patterns', () => {
|
|
253
|
+
const h = extractHintsFromLogs([
|
|
254
|
+
'Program log: Error: slippage.',
|
|
255
|
+
'x Error Message: user rejected.',
|
|
256
|
+
]);
|
|
257
|
+
expect(h).toContain('slippage');
|
|
258
|
+
expect(h).toContain('user rejected');
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
describe('instructionErrorCodeFromMetaErr', () => {
|
|
263
|
+
it('returns Custom instruction code', () => {
|
|
264
|
+
expect(
|
|
265
|
+
instructionErrorCodeFromMetaErr({
|
|
266
|
+
InstructionError: [2, { Custom: 6001 }],
|
|
267
|
+
})
|
|
268
|
+
).toEqual({ code: 6001, instructionIndex: 2 });
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
describe('confirmAnyTransactionSignature', () => {
|
|
273
|
+
it('throws TradeError 106 when signatures empty', async () => {
|
|
274
|
+
const c = new Connection('https://api.mainnet-beta.solana.com');
|
|
275
|
+
await expect(confirmAnyTransactionSignature(c, [])).rejects.toMatchObject({
|
|
276
|
+
code: 106,
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it('after poll threshold, uses getTransaction meta.err like Rust (Custom code)', async () => {
|
|
281
|
+
const connection = {
|
|
282
|
+
getSignatureStatuses: vi.fn().mockResolvedValue({
|
|
283
|
+
context: { slot: 1 },
|
|
284
|
+
value: [
|
|
285
|
+
{
|
|
286
|
+
err: null,
|
|
287
|
+
confirmationStatus: 'processed' as const,
|
|
288
|
+
slot: 1,
|
|
289
|
+
confirmations: 0,
|
|
290
|
+
},
|
|
291
|
+
null,
|
|
292
|
+
],
|
|
293
|
+
}),
|
|
294
|
+
getTransaction: vi.fn().mockResolvedValue({
|
|
295
|
+
slot: 1,
|
|
296
|
+
transaction: { signatures: [], message: {} },
|
|
297
|
+
meta: {
|
|
298
|
+
err: { InstructionError: [0, { Custom: 6001 }] },
|
|
299
|
+
fee: 5000,
|
|
300
|
+
preBalances: [0],
|
|
301
|
+
postBalances: [0],
|
|
302
|
+
logMessages: ['Program log: Error: slippage exceeded.'],
|
|
303
|
+
},
|
|
304
|
+
}),
|
|
305
|
+
} as unknown as Connection;
|
|
306
|
+
await expect(
|
|
307
|
+
confirmAnyTransactionSignature(connection, ['sigA', 'sigB'], {
|
|
308
|
+
pollsBeforeGetTransaction: 1,
|
|
309
|
+
pollIntervalMs: 0,
|
|
310
|
+
})
|
|
311
|
+
).rejects.toMatchObject({ code: 6001 });
|
|
312
|
+
expect(connection.getTransaction).toHaveBeenCalled();
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
describe('ExecutionPath (Rust parity)', () => {
|
|
317
|
+
it('isBuy is true for SOL/WSOL quote mints', () => {
|
|
318
|
+
expect(ExecutionPath.isBuy(SOL_TOKEN_ACCOUNT)).toBe(true);
|
|
319
|
+
expect(ExecutionPath.isBuy(WSOL_TOKEN_ACCOUNT)).toBe(true);
|
|
320
|
+
});
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
describe('InstructionProcessor (Rust parity)', () => {
|
|
324
|
+
it('throws when instructions empty', () => {
|
|
325
|
+
expect(() => InstructionProcessor.preprocess([])).toThrow('Instructions empty');
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it('calculateSize counts web3 TransactionInstruction keys', () => {
|
|
329
|
+
const ix = new TransactionInstruction({
|
|
330
|
+
keys: [{ pubkey: PublicKey.default, isSigner: false, isWritable: true }],
|
|
331
|
+
programId: PublicKey.default,
|
|
332
|
+
data: Buffer.from([1, 2]),
|
|
333
|
+
});
|
|
334
|
+
const size = InstructionProcessor.calculateSize([ix]);
|
|
335
|
+
expect(size).toBe(2 + 32);
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it('warns when instruction count exceeds MAX_INSTRUCTIONS_WARN', () => {
|
|
339
|
+
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
340
|
+
const stub = new TransactionInstruction({
|
|
341
|
+
keys: [],
|
|
342
|
+
programId: PublicKey.default,
|
|
343
|
+
data: Buffer.alloc(0),
|
|
344
|
+
});
|
|
345
|
+
const many = Array.from({ length: MAX_INSTRUCTIONS_WARN + 1 }, () => stub);
|
|
346
|
+
InstructionProcessor.preprocess(many);
|
|
347
|
+
expect(warn).toHaveBeenCalled();
|
|
348
|
+
warn.mockRestore();
|
|
349
|
+
});
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
describe('MiddlewareManager (Rust parity)', () => {
|
|
353
|
+
class RecordingMiddleware implements InstructionMiddleware {
|
|
354
|
+
protocolCalls: { len: number; protocol: string; isBuy: boolean }[] = [];
|
|
355
|
+
fullCalls: { len: number; protocol: string; isBuy: boolean }[] = [];
|
|
356
|
+
|
|
357
|
+
name(): string {
|
|
358
|
+
return 'RecordingMiddleware';
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
processProtocolInstructions(
|
|
362
|
+
ixs: TransactionInstruction[],
|
|
363
|
+
protocolName: string,
|
|
364
|
+
isBuy: boolean
|
|
365
|
+
): TransactionInstruction[] {
|
|
366
|
+
this.protocolCalls.push({ len: ixs.length, protocol: protocolName, isBuy });
|
|
367
|
+
return ixs;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
processFullInstructions(
|
|
371
|
+
ixs: TransactionInstruction[],
|
|
372
|
+
protocolName: string,
|
|
373
|
+
isBuy: boolean
|
|
374
|
+
): TransactionInstruction[] {
|
|
375
|
+
this.fullCalls.push({ len: ixs.length, protocol: protocolName, isBuy });
|
|
376
|
+
return ixs;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
clone(): InstructionMiddleware {
|
|
380
|
+
return new RecordingMiddleware();
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
it('TradeConfigBuilder passes middlewareManager to TradeConfig', () => {
|
|
385
|
+
const mgr = new MiddlewareManager();
|
|
386
|
+
const cfg = TradeConfigBuilder.create('https://api.mainnet-beta.solana.com')
|
|
387
|
+
.middlewareManager(mgr)
|
|
388
|
+
.build();
|
|
389
|
+
expect(cfg.middlewareManager).toBe(mgr);
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
it('TradeConfigBuilder passes maxSwqosSubmitConcurrency', () => {
|
|
393
|
+
const cfg = TradeConfigBuilder.create('https://api.mainnet-beta.solana.com')
|
|
394
|
+
.maxSwqosSubmitConcurrency(8)
|
|
395
|
+
.build();
|
|
396
|
+
expect(cfg.maxSwqosSubmitConcurrency).toBe(8);
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
it('protocol pass then full pass match Rust executor + transaction_builder ordering', () => {
|
|
400
|
+
const rec = new RecordingMiddleware();
|
|
401
|
+
const mgr = new MiddlewareManager().addMiddleware(rec);
|
|
402
|
+
const proto = [
|
|
403
|
+
new TransactionInstruction({
|
|
404
|
+
keys: [],
|
|
405
|
+
programId: PublicKey.default,
|
|
406
|
+
data: Buffer.alloc(0),
|
|
407
|
+
}),
|
|
408
|
+
];
|
|
409
|
+
mgr.applyMiddlewaresProcessProtocolInstructions(proto, 'PumpFun', true);
|
|
410
|
+
expect(rec.protocolCalls).toEqual([
|
|
411
|
+
{ len: 1, protocol: 'PumpFun', isBuy: true },
|
|
412
|
+
]);
|
|
413
|
+
|
|
414
|
+
const wired = [
|
|
415
|
+
new TransactionInstruction({
|
|
416
|
+
keys: [],
|
|
417
|
+
programId: PublicKey.default,
|
|
418
|
+
data: Buffer.from([1]),
|
|
419
|
+
}),
|
|
420
|
+
...proto,
|
|
421
|
+
];
|
|
422
|
+
mgr.applyMiddlewaresProcessFullInstructions(wired, 'PumpFun', true);
|
|
423
|
+
expect(rec.fullCalls).toEqual([{ len: 2, protocol: 'PumpFun', isBuy: true }]);
|
|
424
|
+
});
|
|
425
|
+
});
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Address Lookup Table support for Solana transactions.
|
|
3
|
+
*
|
|
4
|
+
* This module provides functionality to fetch and use Address Lookup Tables (ALT)
|
|
5
|
+
* to reduce transaction size by storing frequently used addresses in a lookup table.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { PublicKey, Connection, AccountInfo } from '@solana/web3.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Represents an address lookup table account.
|
|
12
|
+
*/
|
|
13
|
+
export interface AddressLookupTableAccount {
|
|
14
|
+
key: PublicKey;
|
|
15
|
+
addresses: PublicKey[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Header structure for Address Lookup Table (56 bytes)
|
|
20
|
+
* - authority: 32 bytes
|
|
21
|
+
* - deactivationSlot: 8 bytes
|
|
22
|
+
* - lastExtendedSlot: 8 bytes
|
|
23
|
+
* - lastExtendedSlotStartIndex: 1 byte
|
|
24
|
+
* - padding: 7 bytes
|
|
25
|
+
*/
|
|
26
|
+
const ALT_HEADER_SIZE = 56;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Fetch an address lookup table account from the blockchain.
|
|
30
|
+
*
|
|
31
|
+
* @param connection - Solana RPC connection
|
|
32
|
+
* @param lookupTableAddress - The address of the lookup table
|
|
33
|
+
* @param commitment - Commitment level for the query
|
|
34
|
+
* @returns AddressLookupTableAccount if found, null otherwise
|
|
35
|
+
*/
|
|
36
|
+
export async function fetchAddressLookupTableAccount(
|
|
37
|
+
connection: Connection,
|
|
38
|
+
lookupTableAddress: PublicKey,
|
|
39
|
+
commitment?: 'processed' | 'confirmed' | 'finalized'
|
|
40
|
+
): Promise<AddressLookupTableAccount | null> {
|
|
41
|
+
try {
|
|
42
|
+
const info = await connection.getAccountInfo(
|
|
43
|
+
lookupTableAddress,
|
|
44
|
+
commitment ?? 'confirmed'
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
if (info === null) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return parseAddressLookupTable(lookupTableAddress, info);
|
|
52
|
+
} catch (error) {
|
|
53
|
+
throw new Error(`Failed to fetch address lookup table: ${error}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Parse an address lookup table from account data.
|
|
59
|
+
*
|
|
60
|
+
* @param key - The lookup table public key
|
|
61
|
+
* @param accountInfo - The account info containing the lookup table data
|
|
62
|
+
* @returns AddressLookupTableAccount
|
|
63
|
+
*/
|
|
64
|
+
export function parseAddressLookupTable(
|
|
65
|
+
key: PublicKey,
|
|
66
|
+
accountInfo: AccountInfo<Buffer>
|
|
67
|
+
): AddressLookupTableAccount | null {
|
|
68
|
+
const data = accountInfo.data;
|
|
69
|
+
|
|
70
|
+
if (data.length < ALT_HEADER_SIZE) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Skip header and parse addresses
|
|
75
|
+
const addressesData = data.slice(ALT_HEADER_SIZE);
|
|
76
|
+
const addresses: PublicKey[] = [];
|
|
77
|
+
|
|
78
|
+
// Each address is 32 bytes
|
|
79
|
+
for (let i = 0; i + 32 <= addressesData.length; i += 32) {
|
|
80
|
+
const addrBytes = addressesData.slice(i, i + 32);
|
|
81
|
+
addresses.push(new PublicKey(addrBytes));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
key,
|
|
86
|
+
addresses,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Cache for address lookup tables to avoid repeated RPC calls.
|
|
92
|
+
*/
|
|
93
|
+
export class AddressLookupTableCache {
|
|
94
|
+
private cache: Map<string, AddressLookupTableAccount> = new Map();
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Get lookup table from cache or fetch from RPC.
|
|
98
|
+
*
|
|
99
|
+
* @param connection - Solana RPC connection
|
|
100
|
+
* @param lookupTableAddress - The lookup table address
|
|
101
|
+
* @returns AddressLookupTableAccount if found, null otherwise
|
|
102
|
+
*/
|
|
103
|
+
async getLookupTable(
|
|
104
|
+
connection: Connection,
|
|
105
|
+
lookupTableAddress: PublicKey
|
|
106
|
+
): Promise<AddressLookupTableAccount | null> {
|
|
107
|
+
const key = lookupTableAddress.toBase58();
|
|
108
|
+
|
|
109
|
+
if (this.cache.has(key)) {
|
|
110
|
+
return this.cache.get(key)!;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const lookupTable = await fetchAddressLookupTableAccount(connection, lookupTableAddress);
|
|
114
|
+
|
|
115
|
+
if (lookupTable) {
|
|
116
|
+
this.cache.set(key, lookupTable);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return lookupTable;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Get multiple lookup tables from cache or fetch from RPC.
|
|
124
|
+
*
|
|
125
|
+
* @param connection - Solana RPC connection
|
|
126
|
+
* @param lookupTableAddresses - Array of lookup table addresses
|
|
127
|
+
* @returns Array of AddressLookupTableAccount (null for not found)
|
|
128
|
+
*/
|
|
129
|
+
async getLookupTables(
|
|
130
|
+
connection: Connection,
|
|
131
|
+
lookupTableAddresses: PublicKey[]
|
|
132
|
+
): Promise<(AddressLookupTableAccount | null)[]> {
|
|
133
|
+
const results: (AddressLookupTableAccount | null)[] = [];
|
|
134
|
+
const toFetch: { index: number; address: PublicKey }[] = [];
|
|
135
|
+
|
|
136
|
+
// Check cache first
|
|
137
|
+
for (let i = 0; i < lookupTableAddresses.length; i++) {
|
|
138
|
+
const addr = lookupTableAddresses[i];
|
|
139
|
+
if (!addr) continue;
|
|
140
|
+
const key = addr.toBase58();
|
|
141
|
+
const cached = this.cache.get(key);
|
|
142
|
+
if (cached) {
|
|
143
|
+
results[i] = cached;
|
|
144
|
+
} else {
|
|
145
|
+
results[i] = null;
|
|
146
|
+
toFetch.push({ index: i, address: addr });
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Fetch missing tables
|
|
151
|
+
if (toFetch.length > 0) {
|
|
152
|
+
const fetchPromises = toFetch.map(async ({ index, address }) => {
|
|
153
|
+
const lookupTable = await fetchAddressLookupTableAccount(connection, address);
|
|
154
|
+
if (lookupTable) {
|
|
155
|
+
this.cache.set(address.toBase58(), lookupTable);
|
|
156
|
+
}
|
|
157
|
+
return { index, lookupTable };
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
const fetched = await Promise.all(fetchPromises);
|
|
161
|
+
for (const { index, lookupTable } of fetched) {
|
|
162
|
+
results[index] = lookupTable;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return results;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Clear the cache.
|
|
171
|
+
*/
|
|
172
|
+
clear(): void {
|
|
173
|
+
this.cache.clear();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Remove a specific lookup table from cache.
|
|
178
|
+
*
|
|
179
|
+
* @param lookupTableAddress - The lookup table address to remove
|
|
180
|
+
*/
|
|
181
|
+
remove(lookupTableAddress: PublicKey): void {
|
|
182
|
+
const key = lookupTableAddress.toBase58();
|
|
183
|
+
this.cache.delete(key);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Get cache size.
|
|
188
|
+
*/
|
|
189
|
+
get size(): number {
|
|
190
|
+
return this.cache.size;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Default global cache instance.
|
|
196
|
+
*/
|
|
197
|
+
export const addressLookupTableCache = new AddressLookupTableCache();
|