UNPKG

@xswap-link/sdk

Version:
280 lines (248 loc) 8.96 kB
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-nocheck import { PublicKey, TransactionMessage, VersionedTransaction, } from "@solana/web3.js"; import { NATIVE_MINT } from "@solana/spl-token"; import { AccountMeta } from "@solana/web3.js"; import { createErrorEnhancer } from "../../utils/errors"; import { CCIPFeeRequest, CCIPContext, CCIPCoreConfig } from "../models"; import * as types from "../../bindings/types"; import { GetFeeResult } from "../../bindings/types/GetFeeResult"; import { findFqConfigPDA, findFqDestChainPDA, findFqBillingTokenConfigPDA, findFqPerChainPerTokenConfigPDA, findConfigPDA, findDestChainStatePDA, } from "../../utils/pdas"; import { getFee, GetFeeAccounts, GetFeeArgs, } from "../../bindings/instructions/getFee"; /** * Calculates the fee for a CCIP message * * @param context SDK context with provider, config and logger * @param request Fee request parameters * @returns Fee result */ export async function calculateFee( context: CCIPContext, request: CCIPFeeRequest, ): Promise<types.GetFeeResult> { const logger = context.logger; const config = context.config; const connection = context.provider.connection; const signerPublicKey = context.provider.getAddress(); if (!logger) { throw new Error("Logger is required for calculateFee"); } const enhanceError = createErrorEnhancer(logger); const selectorBigInt = BigInt(request.destChainSelector.toString()); logger.info( `Calculating fee for destination chain ${request.destChainSelector.toString()}`, ); const feeTokenMint = request.message.feeToken.equals(PublicKey.default) ? NATIVE_MINT : request.message.feeToken; logger.debug( `Using fee token: ${feeTokenMint.toString()} (${ request.message.feeToken.equals(PublicKey.default) ? "Native SOL" : "SPL Token" })`, ); // Build the accounts needed for the getFee instruction logger.debug(`Building accounts for getFee instruction`); const accounts = await buildGetFeeAccounts( config, selectorBigInt, feeTokenMint, ); logger.trace("Fee accounts:", { config: accounts.config.toString(), destChainState: accounts.destChainState.toString(), feeQuoter: accounts.feeQuoter.toString(), feeQuoterConfig: accounts.feeQuoterConfig.toString(), feeQuoterDestChain: accounts.feeQuoterDestChain.toString(), feeQuoterBillingTokenConfig: accounts.feeQuoterBillingTokenConfig.toString(), feeQuoterLinkTokenConfig: accounts.feeQuoterLinkTokenConfig.toString(), }); // Create the getFee instruction arguments logger.debug(`Creating getFee instruction arguments`); const args: GetFeeArgs = { destChainSelector: request.destChainSelector, message: { receiver: request.message.receiver, data: request.message.data, tokenAmounts: request.message.tokenAmounts, feeToken: request.message.feeToken, extraArgs: request.message.extraArgs, }, }; // Create instruction logger.debug(`Creating getFee instruction`); const instruction = getFee(args, accounts, config.ccipRouterProgramId); // Build and add token-specific remaining accounts for each token in tokenAmounts const remainingAccounts: AccountMeta[] = []; // Process each token in tokenAmounts logger.debug( `Processing ${request.message.tokenAmounts.length} token amounts for remaining accounts`, ); for (const tokenAmount of request.message.tokenAmounts) { try { logger.trace( `Processing token: ${tokenAmount.token.toString()}, amount: ${tokenAmount.amount.toString()}`, ); // Find the token billing config PDA const [tokenBillingConfig] = findFqBillingTokenConfigPDA( tokenAmount.token, config.feeQuoterProgramId, ); // Find the per chain per token config PDA const [perChainPerTokenConfig] = findFqPerChainPerTokenConfigPDA( selectorBigInt, tokenAmount.token, config.feeQuoterProgramId, ); logger.trace(`Found token configs:`, { tokenBillingConfig: tokenBillingConfig.toString(), perChainPerTokenConfig: perChainPerTokenConfig.toString(), }); // Add these accounts to the remaining accounts remainingAccounts.push( { pubkey: tokenBillingConfig, isWritable: false, isSigner: false }, { pubkey: perChainPerTokenConfig, isWritable: false, isSigner: false }, ); } catch (error) { // Log the error with context but continue with other tokens enhanceError(error, { operation: "getFee:processToken", token: tokenAmount.token.toString(), amount: tokenAmount.amount.toString(), destChainSelector: selectorBigInt.toString(), }); // Continue with other tokens if one fails } } // Add remaining accounts to the instruction if (remainingAccounts.length > 0) { logger.debug( `Adding ${remainingAccounts.length} remaining accounts to the instruction`, ); instruction.keys.push(...remainingAccounts); } // Log complete instruction accounts in TRACE mode logger.trace( "Complete instruction accounts:", instruction.keys.map((key, index) => ({ index, pubkey: key.pubkey.toString(), isSigner: key.isSigner, isWritable: key.isWritable, })), ); // Get recent blockhash logger.debug(`Getting recent blockhash for transaction`); const { blockhash } = await connection.getLatestBlockhash("confirmed"); // Create transaction logger.debug(`Creating versioned transaction for simulation`); const messageV0 = new TransactionMessage({ payerKey: signerPublicKey, recentBlockhash: blockhash, instructions: [instruction], }).compileToV0Message(); const tx = new VersionedTransaction(messageV0); await context.provider.signTransaction(tx); // Simulate transaction to get the return data logger.debug(`Simulating transaction to get fee result`); const simulation = await connection.simulateTransaction(tx, { commitment: "confirmed", sigVerify: false, }); // Parse the return data if (simulation.value.logs) { logger.trace(`Simulation logs:`, simulation.value.logs); const ccipReturnLog = simulation.value.logs.find((log) => log.includes(`Program return: ${config.ccipRouterProgramId.toString()}`), ); if (ccipReturnLog) { logger.debug(`Found CCIP program return log`); const parts = ccipReturnLog.split( `Program return: ${config.ccipRouterProgramId.toString()} `, ); if (parts.length > 1) { const base64Data = parts[1].trim(); const buffer = Buffer.from(base64Data, "base64"); // Use the proper bindings to decode the result logger.debug(`Decoding fee result data`); const feeResultData = GetFeeResult.layout().decode(buffer); const result = GetFeeResult.fromDecoded(feeResultData); logger.info( `Fee calculation complete: ${result.amount.toString()} tokens`, ); return result; } } logger.error(`Could not find CCIP program return log in simulation logs`); } else { logger.error(`Simulation did not return any logs`); } throw enhanceError( new Error("Could not parse fee from transaction return data"), { operation: "getFee", destChainSelector: request.destChainSelector.toString(), feeToken: request.message.feeToken.toString(), simulationStatus: simulation?.value?.err || "No specific error", hasLogs: !!simulation?.value?.logs, logCount: simulation?.value?.logs?.length || 0, }, ); } /** * Build accounts required for the getFee instruction * @param config SDK configuration * @param selectorBigInt Chain selector as BigInt * @param feeTokenMint Fee token mint address * @returns GetFeeAccounts object with all required accounts */ async function buildGetFeeAccounts( config: CCIPCoreConfig, selectorBigInt: bigint, feeTokenMint: PublicKey, ): Promise<GetFeeAccounts> { const [configPDA] = findConfigPDA(config.ccipRouterProgramId); const [destChainState] = findDestChainStatePDA( selectorBigInt, config.ccipRouterProgramId, ); const [feeQuoterConfig] = findFqConfigPDA(config.feeQuoterProgramId); const [fqDestChain] = findFqDestChainPDA( selectorBigInt, config.feeQuoterProgramId, ); const [fqBillingTokenConfig] = findFqBillingTokenConfigPDA( feeTokenMint, config.feeQuoterProgramId, ); const [fqLinkBillingTokenConfig] = findFqBillingTokenConfigPDA( config.linkTokenMint, config.feeQuoterProgramId, ); return { config: configPDA, destChainState: destChainState, feeQuoter: config.feeQuoterProgramId, feeQuoterConfig: feeQuoterConfig, feeQuoterDestChain: fqDestChain, feeQuoterBillingTokenConfig: fqBillingTokenConfig, feeQuoterLinkTokenConfig: fqLinkBillingTokenConfig, }; }