@xswap-link/sdk 0.10.7 → 0.10.9
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/.eslintrc.json +10 -1
- package/.github/workflows/publish.yml +10 -10
- package/CHANGELOG.md +12 -0
- package/dist/index.global.js +331 -331
- package/dist/index.js +9 -9
- package/dist/index.mjs +9 -9
- package/package.json +6 -3
- package/src/components/Swap/SwapView/ConfirmationView/TxOverview/index.tsx +106 -9
- package/src/components/Swap/SwapView/SwapButton/index.tsx +11 -11
- package/src/components/TxDataCard/TxDataCardUI/index.tsx +12 -18
- package/src/components/TxDataCard/index.tsx +6 -2
- package/src/constants/index.ts +19 -2
- package/src/context/SwapProvider.tsx +7 -3
- package/src/services/api.ts +1 -0
- package/src/services/solana/jupiter/extract-swap-data.ts +6 -7
- package/src/services/svm/README.md +483 -0
- package/src/services/svm/bindings/accounts/AllowedOfframp.ts +73 -0
- package/src/services/svm/bindings/accounts/Config.ts +153 -0
- package/src/services/svm/bindings/accounts/DestChain.ts +113 -0
- package/src/services/svm/bindings/accounts/Nonce.ts +97 -0
- package/src/services/svm/bindings/accounts/index.ts +15 -0
- package/src/services/svm/bindings/accounts/tokenAdminRegistry.ts +128 -0
- package/src/services/svm/bindings/errors/anchor.ts +773 -0
- package/src/services/svm/bindings/errors/custom.ts +375 -0
- package/src/services/svm/bindings/errors/index.ts +62 -0
- package/src/services/svm/bindings/instructions/ccipSend.ts +112 -0
- package/src/services/svm/bindings/instructions/getFee.ts +73 -0
- package/src/services/svm/bindings/instructions/index.ts +4 -0
- package/src/services/svm/bindings/programId.ts +6 -0
- package/src/services/svm/bindings/types/BaseChain.ts +92 -0
- package/src/services/svm/bindings/types/BaseConfig.ts +184 -0
- package/src/services/svm/bindings/types/CodeVersion.ts +88 -0
- package/src/services/svm/bindings/types/CrossChainAmount.ts +53 -0
- package/src/services/svm/bindings/types/DestChainConfig.ts +76 -0
- package/src/services/svm/bindings/types/DestChainState.ts +76 -0
- package/src/services/svm/bindings/types/GetFeeResult.ts +72 -0
- package/src/services/svm/bindings/types/LockOrBurnInV1.ts +102 -0
- package/src/services/svm/bindings/types/LockOrBurnOutV1.ts +79 -0
- package/src/services/svm/bindings/types/RampMessageHeader.ts +94 -0
- package/src/services/svm/bindings/types/RateLimitConfig.ts +72 -0
- package/src/services/svm/bindings/types/RateLimitTokenBucket.ts +76 -0
- package/src/services/svm/bindings/types/ReleaseOrMintInV1.ts +156 -0
- package/src/services/svm/bindings/types/ReleaseOrMintOutV1.ts +53 -0
- package/src/services/svm/bindings/types/RemoteAddress.ts +61 -0
- package/src/services/svm/bindings/types/RemoteConfig.ts +86 -0
- package/src/services/svm/bindings/types/RestoreOnAction.ts +120 -0
- package/src/services/svm/bindings/types/SVM2AnyMessage.ts +128 -0
- package/src/services/svm/bindings/types/SVM2AnyRampMessage.ts +166 -0
- package/src/services/svm/bindings/types/SVM2AnyTokenTransfer.ts +118 -0
- package/src/services/svm/bindings/types/SVMTokenAmount.ts +64 -0
- package/src/services/svm/bindings/types/index.ts +78 -0
- package/src/services/svm/core/client/accounts.ts +97 -0
- package/src/services/svm/core/client/events.ts +95 -0
- package/src/services/svm/core/client/fee.ts +279 -0
- package/src/services/svm/core/client/index.ts +150 -0
- package/src/services/svm/core/client/send.ts +607 -0
- package/src/services/svm/core/client/utils.ts +131 -0
- package/src/services/svm/core/models.ts +236 -0
- package/src/services/svm/index.ts +32 -0
- package/src/services/svm/utils/conversion.ts +62 -0
- package/src/services/svm/utils/errors.ts +51 -0
- package/src/services/svm/utils/keypair.ts +19 -0
- package/src/services/svm/utils/logger.ts +171 -0
- package/src/services/svm/utils/pdas/common.ts +15 -0
- package/src/services/svm/utils/pdas/feeQuoter.ts +68 -0
- package/src/services/svm/utils/pdas/index.ts +12 -0
- package/src/services/svm/utils/pdas/receiver.ts +58 -0
- package/src/services/svm/utils/pdas/rmnRemote.ts +23 -0
- package/src/services/svm/utils/pdas/router.ts +328 -0
- package/src/services/svm/utils/pdas/tokenpool.ts +161 -0
- package/src/services/svm/utils/transaction.ts +132 -0
- package/src/utils/validation.ts +7 -3
- 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
|
+
}
|