@xswap-link/sdk
Version:
JavaScript SDK for XSwap platform
280 lines (248 loc) • 8.96 kB
text/typescript
// 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,
};
}