@xswap-link/sdk 0.10.8 → 0.10.10

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 (69) hide show
  1. package/.eslintrc.json +10 -1
  2. package/CHANGELOG.md +12 -0
  3. package/dist/index.global.js +331 -331
  4. package/dist/index.js +9 -9
  5. package/dist/index.mjs +9 -9
  6. package/package.json +6 -3
  7. package/src/components/Swap/SwapView/ConfirmationView/TxOverview/index.tsx +106 -9
  8. package/src/components/Swap/SwapView/SwapButton/index.tsx +11 -11
  9. package/src/constants/index.ts +18 -0
  10. package/src/context/SwapProvider.tsx +7 -3
  11. package/src/services/api.ts +1 -0
  12. package/src/services/svm/README.md +483 -0
  13. package/src/services/svm/bindings/accounts/AllowedOfframp.ts +73 -0
  14. package/src/services/svm/bindings/accounts/Config.ts +153 -0
  15. package/src/services/svm/bindings/accounts/DestChain.ts +113 -0
  16. package/src/services/svm/bindings/accounts/Nonce.ts +97 -0
  17. package/src/services/svm/bindings/accounts/index.ts +15 -0
  18. package/src/services/svm/bindings/accounts/tokenAdminRegistry.ts +128 -0
  19. package/src/services/svm/bindings/errors/anchor.ts +773 -0
  20. package/src/services/svm/bindings/errors/custom.ts +375 -0
  21. package/src/services/svm/bindings/errors/index.ts +62 -0
  22. package/src/services/svm/bindings/instructions/ccipSend.ts +112 -0
  23. package/src/services/svm/bindings/instructions/getFee.ts +73 -0
  24. package/src/services/svm/bindings/instructions/index.ts +4 -0
  25. package/src/services/svm/bindings/programId.ts +6 -0
  26. package/src/services/svm/bindings/types/BaseChain.ts +92 -0
  27. package/src/services/svm/bindings/types/BaseConfig.ts +184 -0
  28. package/src/services/svm/bindings/types/CodeVersion.ts +88 -0
  29. package/src/services/svm/bindings/types/CrossChainAmount.ts +53 -0
  30. package/src/services/svm/bindings/types/DestChainConfig.ts +76 -0
  31. package/src/services/svm/bindings/types/DestChainState.ts +76 -0
  32. package/src/services/svm/bindings/types/GetFeeResult.ts +72 -0
  33. package/src/services/svm/bindings/types/LockOrBurnInV1.ts +102 -0
  34. package/src/services/svm/bindings/types/LockOrBurnOutV1.ts +79 -0
  35. package/src/services/svm/bindings/types/RampMessageHeader.ts +94 -0
  36. package/src/services/svm/bindings/types/RateLimitConfig.ts +72 -0
  37. package/src/services/svm/bindings/types/RateLimitTokenBucket.ts +76 -0
  38. package/src/services/svm/bindings/types/ReleaseOrMintInV1.ts +156 -0
  39. package/src/services/svm/bindings/types/ReleaseOrMintOutV1.ts +53 -0
  40. package/src/services/svm/bindings/types/RemoteAddress.ts +61 -0
  41. package/src/services/svm/bindings/types/RemoteConfig.ts +86 -0
  42. package/src/services/svm/bindings/types/RestoreOnAction.ts +120 -0
  43. package/src/services/svm/bindings/types/SVM2AnyMessage.ts +128 -0
  44. package/src/services/svm/bindings/types/SVM2AnyRampMessage.ts +166 -0
  45. package/src/services/svm/bindings/types/SVM2AnyTokenTransfer.ts +118 -0
  46. package/src/services/svm/bindings/types/SVMTokenAmount.ts +64 -0
  47. package/src/services/svm/bindings/types/index.ts +78 -0
  48. package/src/services/svm/core/client/accounts.ts +97 -0
  49. package/src/services/svm/core/client/events.ts +95 -0
  50. package/src/services/svm/core/client/fee.ts +279 -0
  51. package/src/services/svm/core/client/index.ts +150 -0
  52. package/src/services/svm/core/client/send.ts +607 -0
  53. package/src/services/svm/core/client/utils.ts +131 -0
  54. package/src/services/svm/core/models.ts +236 -0
  55. package/src/services/svm/index.ts +32 -0
  56. package/src/services/svm/utils/conversion.ts +62 -0
  57. package/src/services/svm/utils/errors.ts +51 -0
  58. package/src/services/svm/utils/keypair.ts +19 -0
  59. package/src/services/svm/utils/logger.ts +171 -0
  60. package/src/services/svm/utils/pdas/common.ts +15 -0
  61. package/src/services/svm/utils/pdas/feeQuoter.ts +68 -0
  62. package/src/services/svm/utils/pdas/index.ts +12 -0
  63. package/src/services/svm/utils/pdas/receiver.ts +58 -0
  64. package/src/services/svm/utils/pdas/rmnRemote.ts +23 -0
  65. package/src/services/svm/utils/pdas/router.ts +328 -0
  66. package/src/services/svm/utils/pdas/tokenpool.ts +161 -0
  67. package/src/services/svm/utils/transaction.ts +132 -0
  68. package/src/utils/validation.ts +7 -3
  69. package/tsconfig.json +2 -1
@@ -0,0 +1,607 @@
1
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
2
+ // @ts-nocheck
3
+ import {
4
+ PublicKey,
5
+ VersionedTransaction,
6
+ Connection,
7
+ AccountMeta,
8
+ SystemProgram,
9
+ TransactionInstruction,
10
+ TransactionMessage,
11
+ AddressLookupTableAccount,
12
+ } from "@solana/web3.js";
13
+ import {
14
+ getAssociatedTokenAddress,
15
+ TOKEN_PROGRAM_ID,
16
+ NATIVE_MINT,
17
+ TOKEN_2022_PROGRAM_ID,
18
+ ASSOCIATED_TOKEN_PROGRAM_ID,
19
+ } from "@solana/spl-token";
20
+ import { BN } from "@coral-xyz/anchor";
21
+ import { Logger } from "../../utils/logger";
22
+ import { createErrorEnhancer } from "../../utils/errors";
23
+ import {
24
+ CCIPContext,
25
+ CCIPSendRequest,
26
+ CCIPSendOptions,
27
+ CCIPCoreConfig,
28
+ } from "../models";
29
+ import { CCIPAccountReader } from "./accounts";
30
+ import {
31
+ ccipSend,
32
+ CcipSendAccounts,
33
+ CcipSendArgs,
34
+ } from "../../bindings/instructions/ccipSend";
35
+ import {
36
+ findConfigPDA,
37
+ findDestChainStatePDA,
38
+ findNoncePDA,
39
+ findFeeBillingSignerPDA,
40
+ findFqConfigPDA,
41
+ findFqDestChainPDA,
42
+ findFqBillingTokenConfigPDA,
43
+ findFqPerChainPerTokenConfigPDA,
44
+ findRMNRemoteConfigPDA,
45
+ findRMNRemoteCursesPDA,
46
+ findTokenPoolChainConfigPDA,
47
+ } from "../../utils/pdas";
48
+
49
+ /**
50
+ * Sends a CCIP message
51
+ *
52
+ * @param context SDK context with provider, config and logger
53
+ * @param request Send request parameters
54
+ * @param accountReader Account reader instance
55
+ * @param computeBudgetInstruction Optional compute budget instruction
56
+ * @param sendOptions Optional send options (skipPreflight, etc.)
57
+ * @returns Transaction signature
58
+ */
59
+ export async function sendCCIPMessage(
60
+ context: CCIPContext,
61
+ request: CCIPSendRequest,
62
+ accountReader: CCIPAccountReader,
63
+ computeBudgetInstruction?: TransactionInstruction,
64
+ sendOptions?: CCIPSendOptions,
65
+ ): Promise<string> {
66
+ if (!context.logger) {
67
+ throw new Error("Logger is required for sendCCIPMessage");
68
+ }
69
+
70
+ const logger = context.logger;
71
+ const config = context.config;
72
+ const connection = context.provider.connection;
73
+ const enhanceError = createErrorEnhancer(logger);
74
+
75
+ logger.info(
76
+ `Sending CCIP message to destination chain ${request.destChainSelector.toString()}`,
77
+ );
78
+
79
+ // Determine if we're using native SOL
80
+ const isNativeSol = request.feeToken.equals(PublicKey.default);
81
+
82
+ // For native SOL, we use NATIVE_MINT as the token mint
83
+ const feeTokenMint = isNativeSol ? NATIVE_MINT : request.feeToken;
84
+
85
+ logger.debug(
86
+ `Using fee token: ${feeTokenMint.toString()} (${
87
+ isNativeSol ? "Native SOL" : "SPL Token"
88
+ })`,
89
+ );
90
+
91
+ // Determine the correct fee token program ID
92
+ let feeTokenProgramId = TOKEN_PROGRAM_ID;
93
+ if (!isNativeSol) {
94
+ try {
95
+ const feeTokenMintInfo = await connection.getAccountInfo(feeTokenMint);
96
+ if (feeTokenMintInfo) {
97
+ feeTokenProgramId = feeTokenMintInfo.owner;
98
+ } else {
99
+ feeTokenProgramId = TOKEN_2022_PROGRAM_ID;
100
+ }
101
+ } catch (error) {
102
+ feeTokenProgramId = TOKEN_2022_PROGRAM_ID;
103
+ }
104
+ }
105
+
106
+ const selectorBigInt = BigInt(request.destChainSelector.toString());
107
+ const signerPublicKey = context.provider.getAddress();
108
+
109
+ // Build the accounts for the ccipSend instruction
110
+ const accounts = await buildCCIPSendAccounts(
111
+ config,
112
+ selectorBigInt,
113
+ request,
114
+ feeTokenMint,
115
+ feeTokenProgramId,
116
+ isNativeSol,
117
+ signerPublicKey,
118
+ logger,
119
+ );
120
+
121
+ // Build token indexes and accounts
122
+ const { tokenIndexes, remainingAccounts, lookupTableList } =
123
+ await buildTokenAccountsForSend(
124
+ request,
125
+ connection,
126
+ feeTokenProgramId,
127
+ accountReader,
128
+ logger,
129
+ config,
130
+ signerPublicKey,
131
+ );
132
+
133
+ // Create the args for the ccipSend instruction
134
+ const args: CcipSendArgs = {
135
+ destChainSelector: request.destChainSelector,
136
+ message: {
137
+ receiver: request.receiver,
138
+ data: request.data,
139
+ tokenAmounts: request.tokenAmounts,
140
+ feeToken: request.feeToken,
141
+ extraArgs: request.extraArgs,
142
+ },
143
+ tokenIndexes: new Uint8Array(tokenIndexes),
144
+ };
145
+
146
+ // Create the ccipSend instruction
147
+ const instruction = ccipSend(args, accounts, config.ccipRouterProgramId);
148
+
149
+ // Add remaining accounts to the instruction
150
+ if (remainingAccounts.length > 0) {
151
+ instruction.keys.push(...remainingAccounts);
152
+ }
153
+
154
+ // Log complete instruction accounts in TRACE mode
155
+ logger.trace(
156
+ "Complete instruction accounts:",
157
+ instruction.keys.map((key, index) => ({
158
+ index,
159
+ pubkey: key.pubkey.toString(),
160
+ isSigner: key.isSigner,
161
+ isWritable: key.isWritable,
162
+ })),
163
+ );
164
+
165
+ // Get recent blockhash with longer validity
166
+ const { blockhash, lastValidBlockHeight } =
167
+ await connection.getLatestBlockhash({
168
+ commitment: "finalized", // Using finalized for longer validity
169
+ });
170
+
171
+ // Create the transaction instructions array
172
+ const instructions: TransactionInstruction[] = [];
173
+
174
+ // Add compute budget instruction if provided
175
+ if (computeBudgetInstruction) {
176
+ instructions.push(computeBudgetInstruction);
177
+ }
178
+
179
+ // Add the ccipSend instruction
180
+ instructions.push(instruction);
181
+
182
+ // Create the transaction
183
+ const messageV0 = new TransactionMessage({
184
+ payerKey: signerPublicKey,
185
+ recentBlockhash: blockhash,
186
+ instructions,
187
+ }).compileToV0Message(lookupTableList);
188
+
189
+ const tx = new VersionedTransaction(messageV0);
190
+ await context.provider.signTransaction(tx);
191
+
192
+ // Send the transaction with improved options
193
+ const signature = await connection.sendTransaction(tx, {
194
+ skipPreflight: sendOptions?.skipPreflight ?? false,
195
+ preflightCommitment: "processed", // Faster preflight check
196
+ maxRetries: 5, // Increased retries
197
+ });
198
+
199
+ // Wait for transaction confirmation with improved options
200
+ await connection.confirmTransaction(
201
+ {
202
+ signature,
203
+ blockhash,
204
+ lastValidBlockHeight,
205
+ },
206
+ "finalized",
207
+ ); // Using finalized
208
+
209
+ logger.info(`CCIP message sent successfully: ${signature}`);
210
+
211
+ return signature;
212
+ }
213
+
214
+ /**
215
+ * Build accounts required for the ccipSend instruction
216
+ */
217
+ async function buildCCIPSendAccounts(
218
+ config: CCIPCoreConfig,
219
+ selectorBigInt: bigint,
220
+ request: CCIPSendRequest,
221
+ feeTokenMint: PublicKey,
222
+ feeTokenProgramId: PublicKey,
223
+ isNativeSol: boolean,
224
+ signerPublicKey: PublicKey,
225
+ logger: Logger,
226
+ ): Promise<CcipSendAccounts> {
227
+ const enhanceError = createErrorEnhancer(logger);
228
+
229
+ try {
230
+ logger.info(
231
+ `Building accounts for CCIP send to chain ${selectorBigInt.toString()}`,
232
+ );
233
+ logger.debug(
234
+ `Fee token: ${feeTokenMint.toString()} (${
235
+ isNativeSol ? "Native SOL" : "SPL Token"
236
+ })`,
237
+ );
238
+
239
+ // Find all the PDAs needed for the ccipSend instruction
240
+ const [configPDA] = findConfigPDA(config.ccipRouterProgramId);
241
+ const [destChainState] = findDestChainStatePDA(
242
+ selectorBigInt,
243
+ config.ccipRouterProgramId,
244
+ );
245
+ const [nonce] = findNoncePDA(
246
+ selectorBigInt,
247
+ signerPublicKey,
248
+ config.ccipRouterProgramId,
249
+ );
250
+ const [feeBillingSigner] = findFeeBillingSignerPDA(
251
+ config.ccipRouterProgramId,
252
+ );
253
+ const [feeQuoterConfig] = findFqConfigPDA(config.feeQuoterProgramId);
254
+ const [fqDestChain] = findFqDestChainPDA(
255
+ selectorBigInt,
256
+ config.feeQuoterProgramId,
257
+ );
258
+ const [fqBillingTokenConfig] = findFqBillingTokenConfigPDA(
259
+ feeTokenMint,
260
+ config.feeQuoterProgramId,
261
+ );
262
+ const [fqLinkBillingTokenConfig] = findFqBillingTokenConfigPDA(
263
+ config.linkTokenMint,
264
+ config.feeQuoterProgramId,
265
+ );
266
+ const [rmnRemoteCurses] = findRMNRemoteCursesPDA(config.rmnRemoteProgramId);
267
+ const [rmnRemoteConfig] = findRMNRemoteConfigPDA(config.rmnRemoteProgramId);
268
+
269
+ // Get the associated token accounts for the user and fee billing signer
270
+ logger.debug(
271
+ `Deriving token accounts for fee token: ${feeTokenMint.toString()}`,
272
+ );
273
+
274
+ const userFeeTokenAccount = isNativeSol
275
+ ? PublicKey.default // For native SOL we use the default public key
276
+ : await getAssociatedTokenAddress(
277
+ feeTokenMint,
278
+ signerPublicKey,
279
+ true,
280
+ feeTokenProgramId,
281
+ ASSOCIATED_TOKEN_PROGRAM_ID,
282
+ );
283
+
284
+ const feeBillingSignerFeeTokenAccount = await getAssociatedTokenAddress(
285
+ feeTokenMint,
286
+ feeBillingSigner,
287
+ true,
288
+ feeTokenProgramId,
289
+ ASSOCIATED_TOKEN_PROGRAM_ID,
290
+ );
291
+
292
+ return {
293
+ authority: signerPublicKey,
294
+ config: configPDA,
295
+ destChainState: destChainState,
296
+ nonce: nonce,
297
+ systemProgram: SystemProgram.programId,
298
+ feeTokenProgram: feeTokenProgramId,
299
+ feeTokenMint: feeTokenMint,
300
+ feeTokenUserAssociatedAccount: userFeeTokenAccount,
301
+ feeTokenReceiver: feeBillingSignerFeeTokenAccount,
302
+ feeBillingSigner: feeBillingSigner,
303
+ feeQuoter: config.feeQuoterProgramId,
304
+ feeQuoterConfig: feeQuoterConfig,
305
+ feeQuoterDestChain: fqDestChain,
306
+ feeQuoterBillingTokenConfig: fqBillingTokenConfig,
307
+ feeQuoterLinkTokenConfig: fqLinkBillingTokenConfig,
308
+ rmnRemote: config.rmnRemoteProgramId,
309
+ rmnRemoteCurses: rmnRemoteCurses,
310
+ rmnRemoteConfig: rmnRemoteConfig,
311
+ };
312
+ } catch (error) {
313
+ // Use enhanceError to add context and properly log the error
314
+ throw enhanceError(error, {
315
+ operation: "buildCCIPSendAccounts",
316
+ destChainSelector: selectorBigInt.toString(),
317
+ feeToken: feeTokenMint.toString(),
318
+ isNativeSol: isNativeSol,
319
+ });
320
+ }
321
+ }
322
+
323
+ /**
324
+ * Build token accounts and indexes for CCIP send
325
+ */
326
+ async function buildTokenAccountsForSend(
327
+ request: CCIPSendRequest,
328
+ connection: Connection,
329
+ feeTokenProgramId: PublicKey,
330
+ accountReader: CCIPAccountReader,
331
+ logger: Logger,
332
+ config: CCIPCoreConfig,
333
+ signerPublicKey: PublicKey,
334
+ ): Promise<{
335
+ tokenIndexes: number[];
336
+ remainingAccounts: AccountMeta[];
337
+ lookupTableList: AddressLookupTableAccount[];
338
+ }> {
339
+ const enhanceError = createErrorEnhancer(logger);
340
+ logger.debug(
341
+ `Building token accounts for ${request.tokenAmounts.length} tokens`,
342
+ );
343
+
344
+ // Setup token accounts
345
+ const tokenIndexes: number[] = [];
346
+ const remainingAccounts: AccountMeta[] = [];
347
+ const lookupTableList: AddressLookupTableAccount[] = [];
348
+ let lastIndex = 0;
349
+
350
+ // Process each token amount
351
+ for (const tokenAmount of request.tokenAmounts) {
352
+ try {
353
+ const tokenMint = tokenAmount.token;
354
+ logger.debug(
355
+ `Processing token: ${tokenMint.toString()}, amount: ${tokenAmount.amount.toString()}`,
356
+ );
357
+
358
+ // Determine token program from token mint
359
+ let tokenProgram = feeTokenProgramId;
360
+ try {
361
+ const tokenMintInfo = await connection.getAccountInfo(tokenMint);
362
+ if (tokenMintInfo) {
363
+ tokenProgram = tokenMintInfo.owner;
364
+ logger.debug(
365
+ `Auto-detected token program: ${tokenProgram.toString()} for token: ${tokenMint.toString()}`,
366
+ );
367
+ } else {
368
+ logger.warn(
369
+ `Token mint info not found for ${tokenMint.toString()}, using fallback token program`,
370
+ );
371
+ }
372
+ } catch (error) {
373
+ logger.warn(
374
+ `Error determining token program, using fallback: ${
375
+ error instanceof Error ? error.message : String(error)
376
+ }`,
377
+ );
378
+ }
379
+
380
+ // Get token admin registry for this token to access lookup table
381
+ const tokenAdminRegistry = await accountReader.getTokenAdminRegistry(
382
+ tokenMint,
383
+ );
384
+ logger.debug(
385
+ `Retrieved token admin registry for ${tokenMint.toString()}`,
386
+ );
387
+
388
+ // Get lookup table for this token
389
+ const lookupTable = await getLookupTableAccount(
390
+ connection,
391
+ tokenAdminRegistry.lookupTable,
392
+ logger,
393
+ );
394
+ lookupTableList.push(lookupTable);
395
+
396
+ // Get the lookup table addresses
397
+ const lookupTableAddresses = lookupTable.state.addresses;
398
+
399
+ // Extract pool program from lookup table
400
+ const poolProgram = getPoolProgram(lookupTableAddresses, logger);
401
+
402
+ // Get user token account - use the signer public key
403
+ const userTokenAccount = await getAssociatedTokenAddress(
404
+ tokenMint,
405
+ signerPublicKey,
406
+ true,
407
+ tokenProgram,
408
+ ASSOCIATED_TOKEN_PROGRAM_ID,
409
+ );
410
+
411
+ // Get token chain config
412
+ const [tokenBillingConfig] = findFqPerChainPerTokenConfigPDA(
413
+ BigInt(request.destChainSelector.toString()),
414
+ tokenMint,
415
+ config.feeQuoterProgramId,
416
+ );
417
+
418
+ // Get pool chain config
419
+ const [poolChainConfig] = findTokenPoolChainConfigPDA(
420
+ BigInt(request.destChainSelector.toString()),
421
+ tokenMint,
422
+ poolProgram,
423
+ );
424
+
425
+ // Build token accounts using lookup table
426
+ const tokenAccounts = buildTokenLookupAccounts(
427
+ userTokenAccount,
428
+ tokenBillingConfig,
429
+ poolChainConfig,
430
+ lookupTableAddresses,
431
+ tokenAdminRegistry.writableIndexes,
432
+ logger,
433
+ );
434
+
435
+ tokenIndexes.push(lastIndex);
436
+ const currentLen = tokenAccounts.length;
437
+ lastIndex += currentLen;
438
+ remainingAccounts.push(...tokenAccounts);
439
+
440
+ logger.debug(
441
+ `Added ${currentLen} token-specific accounts for ${tokenMint.toString()}`,
442
+ );
443
+ } catch (error) {
444
+ throw enhanceError(error, {
445
+ operation: "buildTokenAccountsForSend",
446
+ token: tokenAmount.token.toString(),
447
+ amount: tokenAmount.amount.toString(),
448
+ });
449
+ }
450
+ }
451
+
452
+ return { tokenIndexes, remainingAccounts, lookupTableList };
453
+ }
454
+
455
+ /**
456
+ * Gets an address lookup table account
457
+ */
458
+ async function getLookupTableAccount(
459
+ connection: Connection,
460
+ lookupTableAddress: PublicKey,
461
+ logger: Logger,
462
+ ): Promise<AddressLookupTableAccount> {
463
+ const enhanceError = createErrorEnhancer(logger);
464
+ logger.debug(`Fetching lookup table: ${lookupTableAddress.toString()}`);
465
+
466
+ const { value: lookupTableAccount } = await connection.getAddressLookupTable(
467
+ lookupTableAddress,
468
+ );
469
+
470
+ if (!lookupTableAccount) {
471
+ throw enhanceError(
472
+ new Error(`Lookup table not found: ${lookupTableAddress.toString()}`),
473
+ {
474
+ operation: "getLookupTableAccount",
475
+ lookupTableAddress: lookupTableAddress.toString(),
476
+ },
477
+ );
478
+ }
479
+
480
+ if (lookupTableAccount.state.addresses.length < 7) {
481
+ throw enhanceError(
482
+ new Error(
483
+ `Lookup table has insufficient accounts: ${lookupTableAccount.state.addresses.length} (needs at least 7)`,
484
+ ),
485
+ {
486
+ operation: "getLookupTableAccount",
487
+ lookupTableAddress: lookupTableAddress.toString(),
488
+ addressCount: lookupTableAccount.state.addresses.length,
489
+ },
490
+ );
491
+ }
492
+
493
+ logger.trace(
494
+ `Lookup table fetched with ${lookupTableAccount.state.addresses.length} addresses`,
495
+ );
496
+ return lookupTableAccount;
497
+ }
498
+
499
+ /**
500
+ * Extracts the pool program from lookup table addresses
501
+ */
502
+ function getPoolProgram(
503
+ lookupTableAddresses: PublicKey[],
504
+ logger: Logger,
505
+ ): PublicKey {
506
+ const enhanceError = createErrorEnhancer(logger);
507
+ // The pool program is at index 2 in the lookup table
508
+ if (lookupTableAddresses.length <= 2) {
509
+ throw enhanceError(
510
+ new Error(
511
+ "Lookup table doesn't have enough entries to determine pool program",
512
+ ),
513
+ {
514
+ operation: "getPoolProgram",
515
+ addressCount: lookupTableAddresses.length,
516
+ },
517
+ );
518
+ }
519
+
520
+ const poolProgram = lookupTableAddresses[2];
521
+ logger.debug(
522
+ `Using pool program: ${poolProgram.toString()} (index 2 in lookup table)`,
523
+ );
524
+
525
+ return poolProgram;
526
+ }
527
+
528
+ /**
529
+ * Build token accounts using lookup table
530
+ */
531
+ function buildTokenLookupAccounts(
532
+ userTokenAccount: PublicKey,
533
+ tokenBillingConfig: PublicKey,
534
+ poolChainConfig: PublicKey,
535
+ lookupTableEntries: Array<PublicKey>,
536
+ writableIndexes: BN[],
537
+ logger: Logger,
538
+ ): Array<AccountMeta> {
539
+ // First entry is the lookup table itself
540
+ const lookupTable = lookupTableEntries[0];
541
+
542
+ logger.trace("Building token lookup accounts", {
543
+ userTokenAccount: userTokenAccount.toString(),
544
+ tokenBillingConfig: tokenBillingConfig.toString(),
545
+ poolChainConfig: poolChainConfig.toString(),
546
+ lookupTableAddress: lookupTable.toString(),
547
+ entriesCount: lookupTableEntries.length,
548
+ });
549
+
550
+ // Build the token accounts with the correct writable flags
551
+ const accounts = [
552
+ { pubkey: userTokenAccount, isSigner: false, isWritable: true },
553
+ { pubkey: tokenBillingConfig, isSigner: false, isWritable: false },
554
+ { pubkey: poolChainConfig, isSigner: false, isWritable: true },
555
+
556
+ // First account is the lookup table - must be non-writable
557
+ { pubkey: lookupTable, isSigner: false, isWritable: false },
558
+ ];
559
+
560
+ // Add the remaining lookup table entries with correct writable flags
561
+ const remainingAccounts = lookupTableEntries.slice(1).map((pubkey, index) => {
562
+ const isWrit = isWritable(index + 1, writableIndexes, logger);
563
+ return {
564
+ pubkey,
565
+ isSigner: false,
566
+ isWritable: isWrit,
567
+ };
568
+ });
569
+
570
+ return [...accounts, ...remainingAccounts];
571
+ }
572
+
573
+ /**
574
+ * Checks if an account should be writable based on writable indexes bitmap
575
+ */
576
+ function isWritable(
577
+ index: number,
578
+ writableIndexes: BN[],
579
+ logger?: Logger,
580
+ ): boolean {
581
+ // For the lookup table access, index 0 is determined by the program requirements
582
+ // The lookup table itself must be NON-writable
583
+ if (index === 0) {
584
+ return false;
585
+ }
586
+
587
+ // For other accounts, check the writable indexes bitmap
588
+ // Each BN in writableIndexes represents a 256-bit mask
589
+ const bnIndex = Math.floor(index / 128);
590
+
591
+ // In the Rust code, bits are set from left to right
592
+ const bitPosition = bnIndex === 0 ? 127 - (index % 128) : 255 - (index % 128);
593
+
594
+ if (bnIndex < writableIndexes.length) {
595
+ // Create a BN with the bit at the position we want to check
596
+ const mask = new BN(1).shln(bitPosition);
597
+
598
+ // Check if the bit is set using bitwise AND
599
+ const result = writableIndexes[bnIndex].and(mask);
600
+
601
+ // If the result is not zero, the bit is set
602
+ return !result.isZero();
603
+ }
604
+
605
+ // Default to non-writable if index is out of bounds
606
+ return false;
607
+ }