import { type AptosSettings, type Client, type ClientRequest, type ClientResponse, Aptos, AptosApiError, AptosConfig, Deserializer, Network, SimpleTransaction, TransactionResponseType, } from '@aptos-labs/ts-sdk' import { type BytesLike, concat, isBytesLike, isHexString } from 'ethers' import { memoize } from 'micro-memoize' import { type BlockInfo, type ChainContext, type GetBalanceOpts, type LogFilter, type TokenInfo, type TokenPoolRemote, type TokenPrice, type TokenTransferFeeOpts, Chain, } from '../chain.ts' import { createRateLimitedFetch, fetchProfileForUrl } from '../fetch.ts' import { generateUnsignedExecuteReport } from './exec.ts' import { getAptosLeafHasher } from './hasher.ts' import { getUserTxByVersion, getVersionTimestamp, streamAptosLogs } from './logs.ts' import { generateUnsignedCcipSend, getFee } from './send.ts' import { CCIPAptosExtraArgsV2RequiredError, CCIPAptosNetworkUnknownError, CCIPAptosRegistryTypeInvalidError, CCIPAptosTransactionInvalidError, CCIPAptosTransactionTypeInvalidError, CCIPError, CCIPExtraArgsEncodingUnsupportedError, CCIPLogDataInvalidError, CCIPTokenNotRegisteredError, CCIPTokenPoolChainConfigNotFoundError, CCIPWalletInvalidError, } from '../errors/index.ts' import { type EVMExtraArgsV2, type ExtraArgs, type SVMExtraArgsV1, EVMExtraArgsV2Tag, SVMExtraArgsV1Tag, } from '../extra-args.ts' import { type UnsignedAptosTx, isAptosAccount } from './types.ts' import type { LeafHasher } from '../hasher/common.ts' import { type NetworkInfo, ChainFamily, networkInfo } from '../networks.ts' import { BcsEVMExtraArgsV2Codec, BcsSVMExtraArgsV1Codec, decodeMoveExtraArgs, getMoveAddress, } from '../shared/bcs-codecs.ts' import { supportedChains } from '../supported-chains.ts' import type { CCIPExecution, CCIPMessage, CCIPRequest, ChainLog, ChainTransaction, CommitReport, ExecutionInput, ExecutionReceipt, Lane, LeanNumbers, WithLogger, } from '../types.ts' import { convertKeysToCamelCase, decodeAddress, decodeOnRampAddress, getAddressBytes, parseTypeAndVersion, util, } from '../utils.ts' import { getTokenInfo } from './token.ts' import type { CCIPMessage_V1_6_EVM } from '../evm/messages.ts' import { buildMessageForDest, decodeMessage, normalizeDeep } from '../requests.ts' export type { UnsignedAptosTx } /** * Creates an Aptos SDK `Client` that routes all HTTP calls through the supplied fetch function. * Non-2xx responses are returned (not thrown) so the SDK can build its own `AptosApiError`. */ function createAptosFetchClient(fetchFn: typeof fetch): Client { return { async provider(req: ClientRequest): Promise> { const url = new URL(req.url) if (req.params) { for (const [k, v] of Object.entries( req.params as Record, )) { if (v != null) url.searchParams.set(k, String(v)) } } const headers: Record = {} for (const [k, v] of Object.entries(req.headers ?? {})) { if (v != null) headers[k] = String(v) } // The SDK moves contentType into headers before calling provider(), so // req.contentType is undefined here. Check the header value directly. const resolvedCT = headers['content-type'] ?? req.contentType ?? 'application/json' type FetchBody = NonNullable[1]>['body'] let body: FetchBody if (req.body != null) { headers['content-type'] ??= resolvedCT body = resolvedCT.includes('json') ? JSON.stringify(req.body) : (req.body as FetchBody) } const resp = await fetchFn(url.toString(), { method: req.method, headers, body }) const text = await resp.text() let data: unknown = text const respCT = resp.headers.get('content-type') ?? '' if (respCT.includes('json') || (!respCT && text)) { try { data = text ? JSON.parse(text) : undefined } catch { // keep text as-is } } const respHeaders: Record = {} resp.headers.forEach((v, k) => { respHeaders[k] = v }) return { status: resp.status, statusText: resp.statusText, data: data as Res, headers: respHeaders, config: req, request: null, response: null, } }, } } /** * Aptos chain implementation supporting Aptos networks. */ export class AptosChain extends Chain { static { supportedChains[ChainFamily.Aptos] = AptosChain } /** Chain family identifier for Aptos networks. */ static readonly family = ChainFamily.Aptos /** Native token decimals (8 for APT). */ static readonly decimals = 8 /** The Aptos SDK provider for blockchain interactions. */ provider: Aptos /** Retrieves token information for a given token address. */ getTokenInfo: (token: string) => Promise /** @internal */ _getAccountModulesNames: (address: string) => Promise /** * Creates a new AptosChain instance. * @param provider - Aptos SDK provider instance. * @param network - Network information for this chain. */ constructor(provider: Aptos, network: NetworkInfo, ctx?: ChainContext) { super(network, ctx) this.provider = provider this.typeAndVersion = memoize(this.typeAndVersion.bind(this), { maxSize: 100, maxArgs: 1, expires: 60e3, // 1min }) this.getTransaction = memoize(this.getTransaction.bind(this), { async: true, maxSize: 100, maxArgs: 1, }) this.getTokenForTokenPool = memoize(this.getTokenForTokenPool.bind(this), { async: true, maxSize: 100, maxArgs: 1, }) this.getOnRampConfig = memoize(this.getOnRampConfig.bind(this), { async: true, maxSize: 100, maxArgs: 2, expires: 60e3, // 1min }) this.getOffRampConfig = memoize(this.getOffRampConfig.bind(this), { maxSize: 100, maxArgs: 2, async: true, expires: 60e3, // 1min }) this.getTokenInfo = memoize((token) => getTokenInfo(this.provider, token), { async: true, maxSize: 100, maxArgs: 1, }) this._getAccountModulesNames = memoize( (address) => this.provider .getAccountModules({ accountAddress: address }) .then((modules) => modules.map(({ abi }) => abi!.name)), { maxSize: 100, maxArgs: 1 }, ) this.provider.getTransactionByVersion = memoize( this.provider.getTransactionByVersion.bind(this.provider), { maxSize: 100, async: true, transformKey: ([arg]: [{ ledgerVersion: bigint | number }]) => [Number(arg.ledgerVersion)], }, ) } /** * Creates an AptosChain instance from an existing Aptos provider. * @param provider - Aptos SDK provider instance. * @param ctx - context containing logger. * @returns A new AptosChain instance. */ static async fromProvider(provider: Aptos, ctx?: WithLogger): Promise { return new AptosChain(provider, networkInfo(`aptos:${await provider.getChainId()}`), ctx) } /** * Creates an AptosChain instance from Aptos configuration settings. * * Installs a fetch-based `Client` shim so that all Aptos REST calls are routed * through `ctx.fetch` (when provided) or through the built-in rate-limited fetch. * If the settings already include an explicit `client`, that client is used as-is. * * Use {@link AptosChain.fromProvider} instead when you have a fully constructed * `Aptos` instance and want no shim to be installed. * * @param settings - Aptos configuration settings (AptosSettings or AptosConfig). * @param ctx - context containing logger and optional fetch override. * @returns A new AptosChain instance. */ static async fromAptosConfig( settings: AptosSettings | AptosConfig, ctx?: ChainContext, ): Promise { // Detect whether the caller explicitly set a custom HTTP client adapter: // - For raw AptosSettings: `client` is undefined unless explicitly set. // - For a pre-built AptosConfig: the SDK always fills in `config.client` with a default // whose `.provider` is named `"aptosClient"`. Any other name or identity indicates a // user-supplied adapter. const explicitClient = settings.client const hasExplicitClient = explicitClient != null && explicitClient.provider.name !== 'aptosClient' let effectiveConfig: AptosConfig if (hasExplicitClient) { effectiveConfig = settings instanceof AptosConfig ? settings : new AptosConfig(settings) } else { const fetchFn = ctx?.fetch ?? createRateLimitedFetch(fetchProfileForUrl(settings.fullnode ?? ''), ctx) effectiveConfig = new AptosConfig({ network: settings.network, fullnode: settings.fullnode, faucet: settings.faucet, indexer: settings.indexer, pepper: settings.pepper, prover: settings.prover, clientConfig: settings.clientConfig, fullnodeConfig: settings.fullnodeConfig, indexerConfig: settings.indexerConfig, faucetConfig: settings.faucetConfig, client: createAptosFetchClient(fetchFn), }) } const provider = new Aptos(effectiveConfig) return this.fromProvider(provider, ctx) } /** * Creates an AptosChain instance from a URL or network identifier. * @param url - RPC URL, Aptos Network enum value or [fullNodeUrl, Network] tuple. * @param ctx - context containing logger * @returns A new AptosChain instance. * @throws {@link CCIPAptosNetworkUnknownError} if network cannot be determined from URL */ static async fromUrl( url: string | Network | readonly [string, Network], ctx?: ChainContext, ): Promise { let network: Network if (Array.isArray(url)) { ;[url, network] = url } else if (Object.values(Network).includes(url as Network)) network = url as Network else if (url.includes('mainnet')) network = Network.MAINNET else if (url.includes('testnet')) network = Network.TESTNET else if (url.includes('local')) network = Network.LOCAL else throw new CCIPAptosNetworkUnknownError(util.inspect(url)) // Pass raw AptosSettings (not a pre-built AptosConfig) so fromAptosConfig can // detect the absence of an explicit `client` and install the fetch shim. const settings: AptosSettings = { network, fullnode: typeof url === 'string' && url.includes('://') ? url : undefined, // indexer: url.includes('://') ? `${url}/v1/graphql` : undefined, } return this.fromAptosConfig(settings, ctx) } /** {@inheritDoc Chain.getBlockInfo} */ async getBlockInfo(version: number | 'finalized' | 'latest'): Promise { let version_ = typeof version !== 'number' ? 0 : version if (version_ <= 0) version_ = +(await this.provider.getLedgerInfo()).ledger_version + version_ const timestamp = await getVersionTimestamp(this.provider, version_) return { number: version_, timestamp } } /** * {@inheritDoc Chain.getTransaction} * @throws {@link CCIPAptosTransactionInvalidError} if hash/version format is invalid * @throws {@link CCIPAptosTransactionTypeInvalidError} if transaction is not a user transaction */ async getTransaction(hashOrVersion: string | number): Promise { let tx if (isHexString(hashOrVersion, 32)) { tx = await this.provider.getTransactionByHash({ transactionHash: hashOrVersion, }) } else if (!isNaN(+hashOrVersion)) { tx = await getUserTxByVersion(this.provider, +hashOrVersion) } else { throw new CCIPAptosTransactionInvalidError(hashOrVersion) } if (tx.type !== TransactionResponseType.User) throw new CCIPAptosTransactionTypeInvalidError() const timestamp = +tx.timestamp / 1e6 return { hash: tx.hash, blockNumber: +tx.version, from: tx.sender, timestamp, logs: tx.events.map((event, index) => ({ address: event.type.slice(0, event.type.lastIndexOf('::')), transactionHash: tx.hash, index, blockNumber: +tx.version, // we use version as Aptos' blockNumber, as blockHeight isn't very useful blockTimestamp: timestamp, data: event.data as Record, topics: [event.type.slice(event.type.lastIndexOf('::') + 2)], })), } } /** {@inheritDoc Chain.getLogs} */ async *getLogs( opts: LeanNumbers & { versionAsHash?: boolean }, ): AsyncIterableIterator { if (opts.watch) { opts = { ...opts, watch: opts.watch instanceof AbortSignal ? AbortSignal.any([opts.watch, this.abort]) : this.abort, } } yield* streamAptosLogs(this, opts) } /** {@inheritDoc Chain.typeAndVersion} */ async typeAndVersion(address: string) { // requires address with `::` suffix const [typeAndVersion] = await this.provider.view<[string]>({ payload: { function: `${address}::type_and_version` as `${string}::${string}::type_and_version`, }, }) return parseTypeAndVersion(typeAndVersion) } /** {@inheritDoc Chain.getOnRampConfig} */ async getOnRampConfig(onRamp: string, destChainSelector: bigint) { const pkg = onRamp.split('::')[0]! const feeQuoter = `${pkg}::fee_quoter` const onRampModule = `${pkg}::onramp` const [, , typeAndVersion] = await this.typeAndVersion(onRampModule) const [sequenceNumber, allowlistEnabled, router, routerStateAddress] = await this.provider.view< [ sequence_number: string, allowlist_enabled: boolean, router: string, router_state_address: string, ] >({ payload: { function: `${onRampModule}::get_dest_chain_config_v2` as `${string}::${string}::get_dest_chain_config_v2`, functionArguments: [destChainSelector], }, }) const [feeQuoterConfig] = await this.provider.view<[Record]>({ payload: { function: `${feeQuoter}::get_static_config` as `${string}::${string}::get_static_config`, }, }) const [feeQuoterDestConfig] = await this.provider.view<[Record]>({ payload: { function: `${feeQuoter}::get_dest_chain_config` as `${string}::${string}::get_dest_chain_config`, functionArguments: [destChainSelector], }, }) return normalizeDeep({ feeQuoter, destChainSelector, sequenceNumber: +sequenceNumber, allowlistEnabled, router: router.includes('::') ? router : `${router}::router`, routerStateAddress, feeQuoterConfig: { ...feeQuoterConfig, ...feeQuoterDestConfig }, typeAndVersion, }) } /** {@inheritDoc Chain.getOffRampConfig} */ async getOffRampConfig(offRamp: string, sourceChainSelector: bigint) { const pkg = offRamp.split('::')[0]! const router = `${pkg}::router` const offRampModule = offRamp.includes('::') ? offRamp : `${pkg}::offramp` const [, , typeAndVersion] = await this.typeAndVersion(offRampModule) const [sourceChainConfig] = await this.provider.view< [Record & { on_ramp: string }] >({ payload: { function: `${offRampModule}::get_source_chain_config` as `${string}::${string}::get_source_chain_config`, functionArguments: [sourceChainSelector], }, }) const onRamp = decodeAddress(sourceChainConfig.on_ramp, networkInfo(sourceChainSelector).family) return normalizeDeep( { sourceChainSelector, ...sourceChainConfig, onRamps: [onRamp], router, typeAndVersion, }, { sourceFamily: networkInfo(sourceChainSelector).family, destFamily: (this.constructor as typeof AptosChain).family, }, ) } /** {@inheritDoc Chain.getNativeTokenForRouter} */ getNativeTokenForRouter(_router: string): Promise { return Promise.resolve('0xa') } /** {@inheritDoc Chain.getOffRampsForRouter} */ getOffRampsForRouter(router: string, _sourceChainSelector: bigint): Promise { return Promise.resolve([router.split('::')[0] + '::offramp']) } /** {@inheritDoc Chain.getOnRampForRouter} */ getOnRampForRouter(router: string, _destChainSelector: bigint): Promise { return Promise.resolve(router.split('::')[0] + '::onramp') } /** {@inheritDoc Chain.getBalance} */ async getBalance(opts: GetBalanceOpts): Promise { const { holder, token } = opts const asset = token ?? '0x1::aptos_coin::AptosCoin' const balance = await this.provider.getBalance({ accountAddress: holder, asset, }) return BigInt(balance) } /** * {@inheritDoc Chain.getTokenAdminRegistryFor} * @throws {@link CCIPAptosRegistryTypeInvalidError} if registry type is invalid */ async getTokenAdminRegistryFor(address: string): Promise { const registry = address.split('::')[0] + '::token_admin_registry' const [type] = await this.typeAndVersion(registry) if (type !== 'TokenAdminRegistry') { throw new CCIPAptosRegistryTypeInvalidError(registry, type) } return registry } /** * Decodes a CCIP message from an Aptos log event. * @param log - Log with data field. * @returns Decoded CCIPMessage or undefined if not valid. * @throws {@link CCIPAptosLogInvalidError} if log data format is invalid */ static decodeMessage(log: { data: BytesLike | Record }): CCIPMessage | undefined { const { data } = log if ( (typeof data !== 'string' || !data.startsWith('{')) && (typeof data !== 'object' || isBytesLike(data)) ) throw new CCIPLogDataInvalidError(util.inspect(log), { chain: ChainFamily.Aptos }) // offload massaging to generic decodeJsonMessage try { return decodeMessage(data) } catch (_) { // return undefined } } /** * Decodes extra arguments from Aptos CCIP messages. * @param extraArgs - Encoded extra arguments bytes. * @returns Decoded extra arguments or undefined if unknown format. */ static decodeExtraArgs( extraArgs: BytesLike, ): | (EVMExtraArgsV2 & { _tag: 'EVMExtraArgsV2' }) | (SVMExtraArgsV1 & { _tag: 'SVMExtraArgsV1' }) | undefined { return decodeMoveExtraArgs(extraArgs) } /** * Encodes extra arguments for Aptos CCIP messages. * @param extraArgs - Extra arguments to encode. * @returns Encoded extra arguments as hex string. * @throws {@link CCIPAptosExtraArgsEncodingError} if extra args format is not supported */ static encodeExtraArgs(extraArgs: ExtraArgs): string { if ('gasLimit' in extraArgs && 'allowOutOfOrderExecution' in extraArgs) return concat([EVMExtraArgsV2Tag, BcsEVMExtraArgsV2Codec.serialize(extraArgs).toBytes()]) else if ('computeUnits' in extraArgs) return concat([ SVMExtraArgsV1Tag, BcsSVMExtraArgsV1Codec.serialize({ ...extraArgs, computeUnits: Number(extraArgs.computeUnits), tokenReceiver: getAddressBytes(extraArgs.tokenReceiver), accounts: extraArgs.accounts.map(getAddressBytes), }).toBytes(), ]) throw new CCIPExtraArgsEncodingUnsupportedError( ChainFamily.Aptos, 'EVMExtraArgsV2 & SVMExtraArgsV1', ) } /** * Decodes commit reports from an Aptos log event. * @param log - Log with data field. * @param lane - Lane info for filtering. * @returns Array of CommitReport or undefined if not valid. * @throws {@link CCIPAptosLogInvalidError} if log data format is invalid */ static decodeCommits({ data }: Pick, lane?: Lane): CommitReport[] | undefined { if (!data || typeof data != 'object') throw new CCIPLogDataInvalidError(data, { chain: ChainFamily.Aptos }) const data_ = data as { blessed_merkle_roots: unknown[] | undefined unblessed_merkle_roots: unknown[] } if (!data_.blessed_merkle_roots) return let commits = ( convertKeysToCamelCase( data_.blessed_merkle_roots.concat(data_.unblessed_merkle_roots), (v) => (typeof v === 'string' && v.match(/^\d+$/) ? BigInt(v) : v), ) as CommitReport[] ).map((c) => ({ ...c, onRampAddress: decodeOnRampAddress( c.onRampAddress, networkInfo(c.sourceChainSelector).family, ), })) if (lane) { commits = commits.filter( (c) => c.sourceChainSelector === lane.sourceChainSelector && c.onRampAddress === lane.onRamp, ) } return commits } /** * Decodes an execution receipt from an Aptos log event. * @param log - Log with data field. * @returns ExecutionReceipt or undefined if not valid. * @throws {@link CCIPAptosLogInvalidError} if log data format is invalid */ static decodeReceipt({ data }: Pick): ExecutionReceipt | undefined { if (!data || typeof data != 'object') throw new CCIPLogDataInvalidError(data, { chain: ChainFamily.Aptos }) const data_ = data as { message_id: string; state: number } if (!data_.message_id || !data_.state) return return convertKeysToCamelCase(data_, (v) => typeof v === 'string' && v.match(/^\d+$/) ? BigInt(v) : v, ) as ExecutionReceipt } /** * Converts bytes to an Aptos address. * @param bytes - Bytes to convert. * @returns Aptos address (0x-prefixed hex, 32 bytes padded). * @throws {@link CCIPDataFormatUnsupportedError} if bytes length exceeds 32 */ static getAddress(bytes: BytesLike | readonly number[]): string { return getMoveAddress(bytes) } /** * Validates a transaction hash format for Aptos */ static isTxHash(v: unknown): v is `0x${string}` { return typeof v === 'string' && /^0x[0-9a-fA-F]{64}$/.test(v) } /** * Gets the leaf hasher for Aptos destination chains. * @param lane - Lane configuration. * @returns Leaf hasher function. */ static getDestLeafHasher(lane: Lane, _ctx?: WithLogger): LeafHasher { return getAptosLeafHasher(lane) } /** {@inheritDoc Chain.getFee} */ async getFee(opts: Parameters[0]): Promise { await this.checkSendMessage(opts) const { router, destChainSelector, message } = opts const populatedMessage = buildMessageForDest(message, networkInfo(destChainSelector).family) return getFee(this.provider, router, destChainSelector, populatedMessage) } /** {@inheritDoc Chain.generateUnsignedSendMessage} */ async generateUnsignedSendMessage( opts: Parameters[0], ): Promise { const { sender, router, destChainSelector } = opts const populatedMessage = buildMessageForDest( opts.message, networkInfo(destChainSelector).family, ) const message = { ...populatedMessage, fee: opts.message.fee ?? (await this.getFee({ ...opts, message: populatedMessage })), } const tx = await generateUnsignedCcipSend( this.provider, sender, router, destChainSelector, message, opts, ) return { family: ChainFamily.Aptos, transactions: [tx], } } /** * {@inheritDoc Chain.sendMessage} * @throws {@link CCIPAptosWalletInvalidError} if wallet is not a valid Aptos account */ async sendMessage(opts: Parameters[0]): Promise { const account = opts.wallet if (!isAptosAccount(account)) { throw new CCIPWalletInvalidError(opts.wallet, { className: this.constructor.name }) } const unsignedTx = await this.generateUnsignedSendMessage({ ...opts, sender: account.accountAddress.toString(), }) const unsigned = SimpleTransaction.deserialize(new Deserializer(unsignedTx.transactions[0])) // Sign and submit the transaction const signed = await account.signTransactionWithAuthenticator(unsigned) const pendingTxn = await this.provider.transaction.submit.simple({ transaction: unsigned, senderAuthenticator: signed, }) // Wait for the transaction to be confirmed const { hash } = await this.provider.waitForTransaction({ transactionHash: pendingTxn.hash, }) // Return the CCIPRequest by fetching it return (await this.getMessagesInTx(await this.getTransaction(hash)))[0]! } /** * {@inheritDoc Chain.generateUnsignedExecute} * @throws {@link CCIPAptosExtraArgsV2RequiredError} if message missing EVMExtraArgsV2 fields */ async generateUnsignedExecute({ payer, ...opts }: Parameters[0]): Promise { const resolved = await this.resolveExecuteOpts(opts) if ( !('message' in resolved.input) || !('allowOutOfOrderExecution' in resolved.input.message) || !('gasLimit' in resolved.input.message) ) { throw new CCIPAptosExtraArgsV2RequiredError() } const tx = await generateUnsignedExecuteReport( this.provider, payer, resolved.offRamp, resolved.input as ExecutionInput, resolved, ) return { family: ChainFamily.Aptos, transactions: [tx], } } /** * {@inheritDoc Chain.execute} * @throws {@link CCIPAptosWalletInvalidError} if wallet is not a valid Aptos account */ async execute(opts: Parameters[0]): Promise { const account = opts.wallet if (!isAptosAccount(account)) { throw new CCIPWalletInvalidError(opts.wallet, { className: this.constructor.name }) } const unsignedTx = await this.generateUnsignedExecute({ ...opts, payer: account.accountAddress.toString(), }) const unsigned = SimpleTransaction.deserialize(new Deserializer(unsignedTx.transactions[0])) // Sign and submit the transaction const signed = await account.signTransactionWithAuthenticator(unsigned) const pendingTxn = await this.provider.transaction.submit.simple({ transaction: unsigned, senderAuthenticator: signed, }) // Wait for the transaction to be confirmed const { hash } = await this.provider.waitForTransaction({ transactionHash: pendingTxn.hash, }) const tx = await this.getTransaction(hash) return this.getExecutionReceiptInTx(tx) } /** * Parses raw Aptos data into typed structures. * @param data - Raw data to parse. * @returns Parsed data or undefined. */ static parse(data: unknown) { try { if (isBytesLike(data)) { const parsedExtraArgs = this.decodeExtraArgs(data) if (parsedExtraArgs) return parsedExtraArgs } } catch { // ignore } } /** {@inheritDoc Chain.getSupportedTokens} */ async getSupportedTokens(address: string, opts?: Pick): Promise { const res = [] let page, nextKey = '0x0', hasMore do { ;[page, nextKey, hasMore] = await this.provider.view<[string[], string, boolean]>({ payload: { function: `${address.split('::')[0] + '::token_admin_registry'}::get_all_configured_tokens` as `${string}::${string}::get_all_configured_tokens`, functionArguments: [nextKey, (opts?.page ?? 1000) || Number.MAX_SAFE_INTEGER], }, }) res.push(...page) } while (hasMore) return page } /** * {@inheritDoc Chain.getRegistryTokenConfig} * @throws {@link CCIPAptosTokenNotRegisteredError} if token is not registered */ async getRegistryTokenConfig( registry: string, token: string, ): Promise<{ administrator: string pendingAdministrator?: string tokenPool?: string }> { const [tokenPool, administrator, pendingAdministrator] = await this.provider.view< [string, string, string] >({ payload: { function: `${registry.includes('::') ? registry : registry + '::token_admin_registry'}::get_token_config` as `${string}::${string}::get_token_config`, functionArguments: [token], }, }) if (administrator.match(/^0x0*$/)) throw new CCIPTokenNotRegisteredError(token, registry) return { administrator, ...(!pendingAdministrator.match(/^0x0*$/) && { pendingAdministrator }), ...(!tokenPool.match(/^0x0*$/) && { tokenPool }), } } /** {@inheritDoc Chain.getTokenPoolConfig} */ async getTokenPoolConfig( tokenPool: string, _feeOpts?: TokenTransferFeeOpts, ): Promise<{ token: string router: string typeAndVersion?: string }> { const modulesNames = (await this._getAccountModulesNames(tokenPool)) .reverse() .filter((name) => name.endsWith('token_pool')) let firstErr for (const name of modulesNames) { try { const [typeAndVersion, token, router] = await Promise.all([ this.typeAndVersion(`${tokenPool}::${name}`), this.provider.view<[string]>({ payload: { function: `${tokenPool}::${name}::get_token`, functionArguments: [], }, }), this.provider.view<[string]>({ payload: { function: `${tokenPool}::${name}::get_router`, functionArguments: [], }, }), ]) return { token: token[0], router: router[0], typeAndVersion: typeAndVersion[2], } } catch (err) { firstErr ??= err as Error } } throw CCIPError.from(firstErr ?? `Could not get tokenPool configs from ${tokenPool}`, 'UNKNOWN') } /** {@inheritDoc Chain.getTokenPoolRemotes} */ async getTokenPoolRemotes( tokenPool: string, remoteChainSelector?: bigint, ): Promise> { type RawRateLimiterState_ = { capacity: string is_enabled: boolean last_updated: string rate: string tokens: string } const modulesNames = (await this._getAccountModulesNames(tokenPool)) .filter((name) => name.endsWith('token_pool')) .sort((a, b) => b.length - a.length) let firstErr for (const name of modulesNames) { try { const [supportedChains] = remoteChainSelector ? [[remoteChainSelector]] : await this.provider.view<[string[]]>({ payload: { function: `${tokenPool}::${name}::get_supported_chains`, functionArguments: [], }, }) return Object.fromEntries( await Promise.all( supportedChains.map(networkInfo).map(async (chain) => { const remoteToken$ = this.provider.view<[BytesLike]>({ payload: { function: `${tokenPool}::${name}::get_remote_token`, functionArguments: [chain.chainSelector], }, }) const remotePools$ = this.provider.view<[BytesLike[]]>({ payload: { function: `${tokenPool}::${name}::get_remote_pools`, functionArguments: [chain.chainSelector], }, }) const inboundRateLimiterState$ = this.provider.view<[RawRateLimiterState_]>({ payload: { function: `${tokenPool}::${name}::get_current_inbound_rate_limiter_state`, functionArguments: [chain.chainSelector], }, }) const outboundRateLimiterState$ = this.provider.view<[RawRateLimiterState_]>({ payload: { function: `${tokenPool}::${name}::get_current_outbound_rate_limiter_state`, functionArguments: [chain.chainSelector], }, }) try { const [ [remoteToken], [remotePools], [inboundRateLimiterState], [outboundRateLimiterState], ] = await Promise.all([ remoteToken$, remotePools$, inboundRateLimiterState$, outboundRateLimiterState$, ]) return [ chain.name, { remoteToken: decodeAddress(remoteToken, chain.family), remotePools: remotePools.map((pool) => decodeAddress(pool, chain.family)), inboundRateLimiterState: inboundRateLimiterState.is_enabled ? { capacity: BigInt(inboundRateLimiterState.capacity), lastUpdated: Number(inboundRateLimiterState.last_updated), rate: BigInt(inboundRateLimiterState.rate), tokens: BigInt(inboundRateLimiterState.tokens), } : null, outboundRateLimiterState: outboundRateLimiterState.is_enabled ? { capacity: BigInt(outboundRateLimiterState.capacity), lastUpdated: Number(outboundRateLimiterState.last_updated), rate: BigInt(outboundRateLimiterState.rate), tokens: BigInt(outboundRateLimiterState.tokens), } : null, }, ] as const } catch (err) { if ( err instanceof AptosApiError && err.message.includes('Key not found in the smart table') ) throw new CCIPTokenPoolChainConfigNotFoundError(tokenPool, tokenPool, chain.name) throw err } }), ), ) } catch (err) { firstErr ??= err as Error } } throw CCIPError.from(firstErr ?? `Could not view 'get_remote_token' in ${tokenPool}`, 'UNKNOWN') } /** {@inheritDoc Chain.getTokenPrice} */ override async getTokenPrice(opts: { router: string token: string timestamp?: number }): Promise { if (opts.timestamp != null) { this.logger.warn( 'getTokenPrice: timestamp parameter not yet supported on Aptos, returning latest price', ) } const feeQuoterModule = `${opts.router.split('::')[0]}::fee_quoter` const [[prices], { decimals }] = await Promise.all([ this.provider.view<[{ value: string; timestamp: string }[]]>({ payload: { function: `${feeQuoterModule}::get_token_prices` as `${string}::${string}::get_token_prices`, functionArguments: [[opts.token]], }, }), this.getTokenInfo(opts.token), ]) const rawPrice = BigInt(prices[0]!.value) return { price: Number(rawPrice) * 10 ** (decimals - 36), } } /** {@inheritDoc Chain.getFeeTokens} */ async getFeeTokens(router: string): Promise> { const [feeTokens] = await this.provider.view<[string[]]>({ payload: { function: `${router.split('::')[0] + '::fee_quoter'}::get_fee_tokens` as `${string}::${string}::get_fee_tokens`, }, }) return Object.fromEntries( await Promise.all( feeTokens.map(async (token) => [token, await this.getTokenInfo(token)] as const), ), ) } }