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.
Files changed (87) hide show
  1. package/README.md +390 -0
  2. package/dist/chunk-MMQAMIKR.mjs +3735 -0
  3. package/dist/chunk-NEZDFAYA.mjs +7744 -0
  4. package/dist/clients-VITWK7B6.mjs +1370 -0
  5. package/dist/index-1BK_FXsW.d.mts +2327 -0
  6. package/dist/index-1BK_FXsW.d.ts +2327 -0
  7. package/dist/index.d.mts +2659 -0
  8. package/dist/index.d.ts +2659 -0
  9. package/dist/index.js +13265 -0
  10. package/dist/index.mjs +562 -0
  11. package/dist/perf/index.d.mts +2 -0
  12. package/dist/perf/index.d.ts +2 -0
  13. package/dist/perf/index.js +3742 -0
  14. package/dist/perf/index.mjs +214 -0
  15. package/package.json +101 -0
  16. package/src/__tests__/complete_sdk.test.ts +354 -0
  17. package/src/__tests__/hotpath.test.ts +486 -0
  18. package/src/__tests__/nonce.test.ts +45 -0
  19. package/src/__tests__/sdk.test.ts +425 -0
  20. package/src/address-lookup/index.ts +197 -0
  21. package/src/cache/cache.ts +308 -0
  22. package/src/calc/index.ts +1058 -0
  23. package/src/calc/pumpfun.ts +124 -0
  24. package/src/common/bonding_curve.ts +272 -0
  25. package/src/common/compute-budget.ts +148 -0
  26. package/src/common/confirm-any-signature.ts +184 -0
  27. package/src/common/fast-timing.ts +481 -0
  28. package/src/common/fast_fn.ts +150 -0
  29. package/src/common/gas-fee-strategy.ts +253 -0
  30. package/src/common/map-pool.ts +23 -0
  31. package/src/common/nonce.ts +40 -0
  32. package/src/common/sdk-log.ts +460 -0
  33. package/src/common/seed.ts +381 -0
  34. package/src/common/spl-token.ts +578 -0
  35. package/src/common/subscription-handle.ts +644 -0
  36. package/src/common/trading-utils.ts +239 -0
  37. package/src/common/wsol-manager.ts +325 -0
  38. package/src/compute/compute_budget_manager.ts +187 -0
  39. package/src/compute/index.ts +21 -0
  40. package/src/constants/index.ts +96 -0
  41. package/src/execution/execution.ts +532 -0
  42. package/src/execution/index.ts +42 -0
  43. package/src/hotpath/executor.ts +464 -0
  44. package/src/hotpath/index.ts +64 -0
  45. package/src/hotpath/state.ts +435 -0
  46. package/src/index.ts +2117 -0
  47. package/src/instruction/bonk_builder.ts +730 -0
  48. package/src/instruction/index.ts +24 -0
  49. package/src/instruction/meteora_damm_v2_builder.ts +509 -0
  50. package/src/instruction/pumpfun_builder.ts +1183 -0
  51. package/src/instruction/pumpswap.ts +1123 -0
  52. package/src/instruction/raydium_amm_v4_builder.ts +692 -0
  53. package/src/instruction/raydium_cpmm_builder.ts +795 -0
  54. package/src/middleware/traits.ts +407 -0
  55. package/src/params/index.ts +483 -0
  56. package/src/perf/compiler-optimization.ts +529 -0
  57. package/src/perf/hardware.ts +631 -0
  58. package/src/perf/index.ts +9 -0
  59. package/src/perf/kernel-bypass.ts +656 -0
  60. package/src/perf/protocol.ts +682 -0
  61. package/src/perf/realtime.ts +592 -0
  62. package/src/perf/simd.ts +668 -0
  63. package/src/perf/syscall-bypass.ts +331 -0
  64. package/src/perf/ultra-low-latency.ts +505 -0
  65. package/src/perf/zero-copy.ts +589 -0
  66. package/src/pool/pool.ts +294 -0
  67. package/src/rpc/client.ts +345 -0
  68. package/src/sdk-errors.ts +13 -0
  69. package/src/security/index.ts +26 -0
  70. package/src/security/secure-key.ts +303 -0
  71. package/src/security/validators.ts +281 -0
  72. package/src/seed/pda.ts +262 -0
  73. package/src/serialization/index.ts +28 -0
  74. package/src/serialization/serialization.ts +288 -0
  75. package/src/swqos/clients.ts +1754 -0
  76. package/src/swqos/index.ts +50 -0
  77. package/src/swqos/providers.ts +1707 -0
  78. package/src/trading/core/async-executor.ts +702 -0
  79. package/src/trading/core/confirmation-monitor.ts +711 -0
  80. package/src/trading/core/index.ts +82 -0
  81. package/src/trading/core/retry-handler.ts +683 -0
  82. package/src/trading/core/transaction-pool.ts +780 -0
  83. package/src/trading/executor.ts +385 -0
  84. package/src/trading/factory.ts +282 -0
  85. package/src/trading/index.ts +30 -0
  86. package/src/types.ts +8 -0
  87. 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();