@hyperlane-xyz/sdk
Version:
The official SDK for the Hyperlane Network
184 lines • 9.16 kB
JavaScript
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