import { formatUnits, hexlify, randomBytes, toBigInt } from 'ethers' import type { Simplify } from 'type-fest' import type { Chain } from './chain.ts' import { CCIPContractTypeInvalidError, CCIPMethodUnsupportedError, CCIPOnRampRequiredError, CCIPTokenDecimalsInsufficientError, } from './errors/index.ts' import type { CCIPMessage_V2_0 } from './evm/messages.ts' import { discoverOffRamp } from './execution.ts' import { sourceToDestTokenAddresses } from './requests.ts' import type { CCIPMessage_V1_6_Solana } from './solana/types.ts' import type { CCIPMessage, MessageInput } from './types.ts' import { getDataBytes } from './utils.ts' /** * A subset of {@link MessageInput} for estimating receive execution gas. */ export type EstimateMessageInput = Simplify< Pick & Partial> & Partial> & Partial< Pick > & { /** * optional tokenAmounts; `amount` with either source `token` (as in MessageInput) or * `{ sourceTokenAddress?, sourcePoolAddress, destTokenAddress }` (as in v1.5..v2.0 tokenAmounts) * can be provided */ tokenAmounts?: readonly ({ amount: bigint } & ( | { token: string } | { sourceTokenAddress?: string sourcePoolAddress: string destTokenAddress: string extraData?: string } ))[] } > function getSourceDecimalsFromExtraData(extraData?: string): bigint | undefined { if (!extraData) return undefined try { const bytes = getDataBytes(extraData) if (bytes.length !== 32) return undefined const decimals = toBigInt(bytes) return 0 < decimals && decimals <= 36 ? decimals : undefined } catch { return undefined } } /** * Options for {@link estimateReceiveExecution} function. */ export type EstimateReceiveExecutionOpts = { /** Source chain instance (for token data retrieval) */ source: Chain /** Dest chain instance (for token and execution simulation) */ dest: Chain /** source router or onRamp, or dest offRamp contract address */ routerOrRamp: string /** message to be simulated */ message: Omit } /** * Estimate CCIP gasLimit needed to execute a request on a contract receiver. * * @param opts - {@link EstimateReceiveExecutionOpts} for estimation * @returns Estimated execution gas (base transaction cost subtracted) * * @throws {@link CCIPMethodUnsupportedError} if dest chain doesn't support estimation * @throws {@link CCIPContractTypeInvalidError} if routerOrRamp is not a valid contract type * @throws {@link CCIPTokenDecimalsInsufficientError} if dest token has insufficient decimals * @throws {@link CCIPOnRampRequiredError} if no OnRamp found for the given OffRamp and source chain * * @example * ```typescript * import { estimateReceiveExecution, EVMChain } from '@chainlink/ccip-sdk' * * const source = await EVMChain.fromUrl('https://rpc.sepolia.org') * const dest = await EVMChain.fromUrl('https://rpc.fuji.avax.network') * * const gasLimit = await estimateReceiveExecution({ * source, * dest, * routerOrRamp: '0xRouter...', * message: { * sender: '0x...', * receiver: '0x...', * data: '0x...', * tokenAmounts: [], * }, * }) * console.log('Estimated gas:', gasLimit) * ``` */ export async function estimateReceiveExecution({ source, dest, routerOrRamp, message, }: EstimateReceiveExecutionOpts) { if (!dest.estimateReceiveExecution) throw new CCIPMethodUnsupportedError(dest.constructor.name, 'estimateReceiveExecution') let onRamp: string, offRamp: string if (message.onRampAddress) onRamp = message.onRampAddress if (message.offRampAddress) offRamp = message.offRampAddress if (!onRamp! || !offRamp!) try { const tnv = await source.typeAndVersion(routerOrRamp) if (!tnv[0].includes('OnRamp')) onRamp = await source.getOnRampForRouter(routerOrRamp, dest.network.chainSelector) else onRamp = routerOrRamp offRamp = await discoverOffRamp(source, dest, onRamp, source) } catch (sourceErr) { try { const tnv = await dest.typeAndVersion(routerOrRamp) if (!tnv[0].includes('OffRamp')) throw new CCIPContractTypeInvalidError(routerOrRamp, tnv[2], ['OffRamp']) offRamp = routerOrRamp const onRamps = await dest.getOnRampsForOffRamp(offRamp, source.network.chainSelector) if (!onRamps.length) throw new CCIPOnRampRequiredError() onRamp = onRamps[onRamps.length - 1]! } catch { throw sourceErr // re-throw original error } } const destTokenAmounts = await Promise.all( (message.tokenAmounts ?? []).map(async (ta) => { const tokenAmount = 'destTokenAddress' in ta ? ta : await sourceToDestTokenAddresses({ source, onRamp, destChainSelector: dest.network.chainSelector, sourceTokenAmount: ta, }) const sourceDecimalsFromExtraData = 'extraData' in tokenAmount ? getSourceDecimalsFromExtraData(tokenAmount.extraData) : undefined const { decimals: destDecimals } = await dest.getTokenInfo(tokenAmount.destTokenAddress) const sourceDecimals = sourceDecimalsFromExtraData ?? ( await source.getTokenInfo( 'token' in ta ? ta.token : ta.sourceTokenAddress ? ta.sourceTokenAddress : await source.getTokenForTokenPool(tokenAmount.sourcePoolAddress), ) ).decimals const destAmount = (tokenAmount.amount * BigInt(10) ** BigInt(destDecimals)) / BigInt(10) ** BigInt(sourceDecimals) if (destAmount === 0n) throw new CCIPTokenDecimalsInsufficientError( tokenAmount.destTokenAddress, destDecimals, dest.network.name, formatUnits(tokenAmount.amount, sourceDecimals), ) return { ...tokenAmount, token: tokenAmount.destTokenAddress, amount: destAmount } }), ) return dest.estimateReceiveExecution({ offRamp, message: { messageId: message.messageId ?? hexlify(randomBytes(32)), receiver: message.receiver, sender: message.sender, data: message.data, sourceChainSelector: source.network.chainSelector, tokenAmounts: destTokenAmounts, ...(!!message.tokenReceiver && { tokenReceiver: message.tokenReceiver }), ...(!!message.accounts?.length && { accounts: message.accounts, accountIsWritableBitmap: message.accountIsWritableBitmap, }), }, }) }