diff --git a/src/arch/svm/SpokeUtils.ts b/src/arch/svm/SpokeUtils.ts index 39808d734..7cd3c1e9b 100644 --- a/src/arch/svm/SpokeUtils.ts +++ b/src/arch/svm/SpokeUtils.ts @@ -36,6 +36,7 @@ import { type WritableAccount, type ReadonlyAccount, type Commitment, + type CompilableTransactionMessage, } from "@solana/kit"; import assert from "assert"; import winston from "winston"; @@ -100,6 +101,8 @@ import { */ export const SLOT_DURATION_MS = 400; +export const SOLANA_TX_SIZE_LIMIT = 1232; + type ProtoFill = Omit & { destinationChainId: number; recipient: SvmAddress; @@ -693,6 +696,8 @@ export async function getIPFillRelayTx( getEventAuthority(program), ]); + const recipientAtaEncodedAccount = await fetchEncodedAccount(solanaClient, recipientAta); + // Add remaining accounts if the relayData has a non-empty message. // @dev ! since in the context of creating a `fillRelayTx`, `relayData` must be defined. const remainingAccounts: (WritableAccount | ReadonlyAccount)[] = []; @@ -736,7 +741,7 @@ export async function getIPFillRelayTx( fillInput, { outputAmount: relayData.outputAmount.toBigInt(), recipient: toAddress(relayData.recipient) }, mintInfo.data.decimals, - true, + !recipientAtaEncodedAccount.exists, remainingAccounts ); } @@ -1413,6 +1418,47 @@ export async function getCCTPDepositAccounts( }; } +/** + * Returns true if the input deposit's corresponding relay data would result in a transaction size + * that is larger than the Solana transaction size limit. + * @param fillRelayTx The compilable fill relay transaction to check. + * @returns Object containing a boolean if the input deposit requires a multipart fill, false otherwise and + * the number of bytes in the serialized transaction. + */ +export async function isSVMFillTooLarge(fillRelayTx: CompilableTransactionMessage): Promise<{ + tooLarge: boolean; + sizeBytes: number; +}> { + const sizeBytes = await calculateFillSizeBytes(fillRelayTx); + return { + tooLarge: sizeBytes > SOLANA_TX_SIZE_LIMIT, + sizeBytes, + }; +} + +/** + * Returns the byte size of a base64 transaction. + * @param base64TxString base64 serialized Solana transaction. + * @returns The number of bytes in the transaction. + */ +export function base64StrToByteSize(base64TxString: string): number { + // base64 string has 6 bits per character, so every 4 symbols represent 3 bytes + // However, we also need to account for padding: https://en.wikipedia.org/wiki/Base64#Padding + const paddingLen = base64TxString.endsWith("==") ? 2 : base64TxString.endsWith("=") ? 1 : 0; + return (base64TxString.length * 3) / 4 - paddingLen; +} + +/** + * Returns the size of the fill relay transaction using the input relayData. + * @param fillTx The compilable fill relay transaction. + * @returns The number of bytes in the serialized fillRelay transaction. + */ +export async function calculateFillSizeBytes(fillTx: CompilableTransactionMessage): Promise { + const signedTransaction = await signTransactionMessageWithSigners(fillTx); + const serializedTx = getBase64EncodedWireTransaction(signedTransaction); + return base64StrToByteSize(serializedTx); +} + /** * Returns the account metas for a deposit message. * @param message The CCTP message. diff --git a/src/arch/svm/encoders.ts b/src/arch/svm/encoders.ts index c5ff550eb..419eebc1b 100644 --- a/src/arch/svm/encoders.ts +++ b/src/arch/svm/encoders.ts @@ -62,6 +62,10 @@ export function getHandlerMessageEncoder(): Encoder> { return getArrayEncoder(getCompiledIxEncoder()); } +export function getHandlerMessageDecoder(): Decoder> { + return getArrayDecoder(getCompiledIxDecoder()); +} + export function getCompiledIxEncoder(): Encoder { return getStructEncoder([ ["program_id_index", getU8Encoder()], diff --git a/src/relayFeeCalculator/chain-queries/svmQuery.ts b/src/relayFeeCalculator/chain-queries/svmQuery.ts index 3ad6297a0..8cb23ee48 100644 --- a/src/relayFeeCalculator/chain-queries/svmQuery.ts +++ b/src/relayFeeCalculator/chain-queries/svmQuery.ts @@ -4,8 +4,16 @@ import { TransactionSigner, fetchEncodedAccount, isSome, + type CompilableTransactionMessage, } from "@solana/kit"; -import { SVMProvider, SolanaVoidSigner, getFillRelayTx, toAddress, getAssociatedTokenAddress } from "../../arch/svm"; +import { + SVMProvider, + SolanaVoidSigner, + getFillRelayTx, + toAddress, + getAssociatedTokenAddress, + isSVMFillTooLarge, +} from "../../arch/svm"; import { Coingecko } from "../../coingecko"; import { CHAIN_IDs } from "../../constants"; import { getGasPriceEstimate } from "../../gasPriceOracle"; @@ -85,7 +93,7 @@ export class SvmQuery implements QueryInterface { ); const [computeUnitsConsumed, gasPriceEstimate, tokenAccountInfo] = await Promise.all([ - toBN(await this.computeUnitEstimator(fillRelayTx)), + this.estimateComputeUnits(fillRelayTx), getGasPriceEstimate(this.provider, { unsignedTx: fillRelayTx, baseFeeMultiplier: options.baseFeeMultiplier, @@ -211,4 +219,14 @@ export class SvmQuery implements QueryInterface { if (!this.symbolMapping[tokenSymbol]) throw new Error(`${tokenSymbol} does not exist in mapping`); return this.symbolMapping[tokenSymbol].decimals; } + + async estimateComputeUnits(fillRelayTx: CompilableTransactionMessage): Promise { + const fillTooLarge = await isSVMFillTooLarge(fillRelayTx); + if (fillTooLarge.tooLarge) { + return toBN(await this.computeUnitEstimator(fillRelayTx)); + } + const totalComputeUnitAmount = 0; + // The fill is too large; we need to simulate the transaction in a bundle. + return toBN(totalComputeUnitAmount); + } }