UNPKG

@hyperlane-xyz/sdk

Version:

The official SDK for the Hyperlane Network

184 lines 9.16 kB
import { BigNumber, utils } from 'ethers'; import { z } from 'zod'; import { InterchainAccountRouter__factory, } from '@hyperlane-xyz/core'; import { addBufferToGasLimit, addressToBytes32, arrayToObject, bytes32ToAddress, eqAddress, isZeroishAddress, objFilter, objMap, promiseObjAll, } from '@hyperlane-xyz/utils'; import { appFromAddressesMapHelper } from '../../contracts/contracts.js'; import { RouterApp } from '../../router/RouterApps.js'; import { interchainAccountFactories, } from './contracts.js'; export class InterchainAccount extends RouterApp { knownAccounts; constructor(contractsMap, multiProvider) { super(contractsMap, multiProvider); this.knownAccounts = {}; } async remoteChains(chainName) { return Object.keys(this.contractsMap).filter((chain) => chain !== chainName); } router(contracts) { return contracts.interchainAccountRouter; } static fromAddressesMap(addressesMap, multiProvider) { const helper = appFromAddressesMapHelper(addressesMap, interchainAccountFactories, multiProvider); return new InterchainAccount(helper.contractsMap, helper.multiProvider); } async getAccount(destinationChain, config) { return this.getOrDeployAccount(false, destinationChain, config); } async deployAccount(destinationChain, config) { return this.getOrDeployAccount(true, destinationChain, config); } async getOrDeployAccount(deployIfNotExists, destinationChain, config) { const originDomain = this.multiProvider.tryGetDomainId(config.origin); if (!originDomain) { throw new Error(`Origin chain (${config.origin}) metadata needed for deploying ICAs ...`); } const destinationRouter = this.router(this.contractsMap[destinationChain]); const originRouterAddress = config.localRouter ? bytes32ToAddress(config.localRouter) : bytes32ToAddress(await destinationRouter.routers(originDomain)); if (isZeroishAddress(originRouterAddress)) { throw new Error(`Origin router address is zero for ${config.origin} on ${destinationChain}`); } const destinationIsmAddress = bytes32ToAddress(addressToBytes32(config.ismOverride ?? (await destinationRouter.isms(originDomain)))); const destinationAccount = await destinationRouter['getLocalInterchainAccount(uint32,address,address,address)'](originDomain, config.owner, originRouterAddress, destinationIsmAddress); // If not deploying anything, return the account address. if (!deployIfNotExists) { return destinationAccount; } // If the account does not exist, deploy it. if ((await this.multiProvider .getProvider(destinationChain) .getCode(destinationAccount)) === '0x') { const txOverrides = this.multiProvider.getTransactionOverrides(destinationChain); // Estimate gas for deployment const gasEstimate = await destinationRouter.estimateGas['getDeployedInterchainAccount(uint32,address,address,address)'](originDomain, config.owner, originRouterAddress, destinationIsmAddress); // Add buffer to gas estimate const gasWithBuffer = addBufferToGasLimit(gasEstimate); // Execute deployment with buffered gas estimate await this.multiProvider.handleTx(destinationChain, destinationRouter['getDeployedInterchainAccount(uint32,address,address,address)'](originDomain, config.owner, originRouterAddress, destinationIsmAddress, { ...txOverrides, gasLimit: gasWithBuffer, })); this.logger.debug(`Interchain account deployed at ${destinationAccount}`); } else { this.logger.debug(`Interchain account recovered at ${destinationAccount}`); } this.knownAccounts[destinationAccount] = config; return destinationAccount; } // meant for ICA governance to return the populatedTx async getCallRemote({ chain, destination, innerCalls, config, hookMetadata, }) { const localRouter = config.localRouter ? InterchainAccountRouter__factory.connect(config.localRouter, this.multiProvider.getSigner(chain)) : this.router(this.contractsMap[chain]); const remoteDomain = this.multiProvider.getDomainId(destination); const quote = await localRouter['quoteGasPayment(uint32)'](remoteDomain); const remoteRouter = addressToBytes32(config.routerOverride ?? this.routerAddress(destination)); const remoteIsm = addressToBytes32(config.ismOverride ?? (await this.router(this.contractsMap[destination]).isms(remoteDomain))); const callEncoded = await localRouter.populateTransaction['callRemoteWithOverrides(uint32,bytes32,bytes32,(bytes32,uint256,bytes)[],bytes)'](remoteDomain, remoteRouter, remoteIsm, innerCalls.map((call) => ({ to: addressToBytes32(call.to), value: call.value ?? BigNumber.from('0'), data: call.data, })), hookMetadata ?? '0x', { value: quote }); return callEncoded; } // general helper for different overloaded callRemote functions // can override the gasLimit by StandardHookMetadata.overrideGasLimit for optional hookMetadata here async callRemote({ chain, destination, innerCalls, config, hookMetadata, }) { await this.multiProvider.sendTransaction(chain, this.getCallRemote({ chain, destination, innerCalls, config, hookMetadata, })); } } export async function buildInterchainAccountApp(multiProvider, chain, config, coreAddressesByChain) { if (!config.localRouter) { throw new Error('localRouter is required for account deployment'); } let remoteIcaAddresses; const localChainAddresses = coreAddressesByChain[chain]; // if the user specified a custom router address we need to retrieve the remote ica addresses // configured on the user provided router, otherwise we use the ones defined in the registry if (localChainAddresses?.interchainAccountRouter && eqAddress(config.localRouter, localChainAddresses.interchainAccountRouter)) { remoteIcaAddresses = objMap(coreAddressesByChain, (_, chainAddresses) => ({ interchainAccountRouter: chainAddresses.interchainAccountRouter, })); } else { const currentIca = InterchainAccountRouter__factory.connect(config.localRouter, multiProvider.getSigner(chain)); const knownDomains = await currentIca.domains(); remoteIcaAddresses = await promiseObjAll(objMap(arrayToObject(knownDomains.map(String)), async (domainId) => { const routerAddress = await currentIca.routers(domainId); return { interchainAccountRouter: bytes32ToAddress(routerAddress) }; })); } // remove the undefined or 0 addresses values remoteIcaAddresses = objFilter(remoteIcaAddresses, (_chainId, chainAddresses) => !!chainAddresses.interchainAccountRouter && !isZeroishAddress(chainAddresses.interchainAccountRouter)); const addressesMap = { [chain]: { interchainAccountRouter: config.localRouter, }, ...remoteIcaAddresses, }; return InterchainAccount.fromAddressesMap(addressesMap, multiProvider); } export async function deployInterchainAccount(multiProvider, chain, config, coreAddressesByChain) { const interchainAccountApp = await buildInterchainAccountApp(multiProvider, chain, config, coreAddressesByChain); return interchainAccountApp.deployAccount(chain, config); } export function encodeIcaCalls(calls, salt) { return (salt + utils.defaultAbiCoder .encode(['tuple(bytes32 to,uint256 value,bytes data)[]'], [ calls.map((c) => ({ to: addressToBytes32(c.to), value: c.value || 0, data: c.data, })), ]) .slice(2)); } export function normalizeCalls(calls) { return calls.map((call) => ({ to: addressToBytes32(call.to), value: BigNumber.from(call.value || 0), data: call.data, })); } export function commitmentFromIcaCalls(calls, salt) { return utils.keccak256(encodeIcaCalls(calls, salt)); } export const PostCallsSchema = z.object({ calls: z .array(z.object({ to: z.string(), data: z.string(), value: z.string().optional(), })) .min(1), relayers: z.array(z.string()), salt: z.string(), commitmentDispatchTx: z.string(), originDomain: z.number(), }); export async function shareCallsWithPrivateRelayer(serverUrl, payload) { const resp = await fetch(serverUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }); if (!resp.ok) { // Read body const body = await resp.text(); throw new Error(`Failed to share calls with relayer: ${resp.status} ${body}`); } return resp; } //# sourceMappingURL=InterchainAccount.js.map