UNPKG

@hyperlane-xyz/sdk

Version:

The official SDK for the Hyperlane Network

296 lines 14.8 kB
import { zeroAddress } from 'viem'; import { COSMOS_MODULE_MESSAGE_REGISTRY as R, } from '@hyperlane-xyz/cosmos-sdk'; import { addressToBytes32, assert, deepEquals, difference, eqAddress, objMap, rootLogger, } from '@hyperlane-xyz/utils'; import { HyperlaneModule, } from '../core/AbstractHyperlaneModule.js'; import { CosmosNativeIsmModule } from '../ism/CosmosNativeIsmModule.js'; import { CosmosNativeWarpRouteReader } from './CosmosNativeWarpRouteReader.js'; import { CosmosNativeDeployer } from './cosmosnativeDeploy.js'; import { HypTokenRouterConfigSchema, } from './types.js'; export class CosmosNativeWarpModule extends HyperlaneModule { metadataManager; signer; logger = rootLogger.child({ module: 'CosmosNativeWarpModule', }); reader; chainName; chainId; domainId; constructor(metadataManager, args, signer) { super(args); this.metadataManager = metadataManager; this.signer = signer; this.reader = new CosmosNativeWarpRouteReader(metadataManager, args.chain, signer); this.chainName = this.metadataManager.getChainName(args.chain); this.chainId = metadataManager.getChainId(args.chain).toString(); this.domainId = metadataManager.getDomainId(args.chain); } /** * Retrieves the token router configuration for the specified address. * * @param address - The address to derive the token router configuration from. * @returns A promise that resolves to the token router configuration. */ async read() { return this.reader.deriveWarpRouteConfig(this.args.addresses.deployedTokenRoute); } /** * Updates the Warp Route contract with the provided configuration. * * @param expectedConfig - The configuration for the token router to be updated. * @returns An array of Cosmos transactions that were executed to update the contract, or an error if the update failed. */ async update(expectedConfig) { HypTokenRouterConfigSchema.parse(expectedConfig); const actualConfig = await this.read(); const transactions = []; /** * @remark * The order of operations matter * 1. createOwnershipUpdateTxs() must always be LAST because no updates possible after ownership transferred * 2. createRemoteRoutersUpdateTxs() must always be BEFORE createSetDestinationGasUpdateTxs() because gas enumeration depends on domains */ transactions.push(...(await this.createIsmUpdateTxs(actualConfig, expectedConfig)), ...this.createEnrollRemoteRoutersUpdateTxs(actualConfig, expectedConfig), ...this.createUnenrollRemoteRoutersUpdateTxs(actualConfig, expectedConfig), ...(await this.createSetDestinationGasUpdateTxs(actualConfig, expectedConfig)), ...this.createOwnershipUpdateTxs(actualConfig, expectedConfig)); return transactions; } /** * Create a transaction to update the remote routers for the Warp Route contract. * * @param actualConfig - The on-chain router configuration, including the remoteRouters array. * @param expectedConfig - The expected token router configuration. * @returns An array with Cosmos Native transactions that need to be executed to enroll the routers */ createEnrollRemoteRoutersUpdateTxs(actualConfig, expectedConfig) { const updateTransactions = []; if (!expectedConfig.remoteRouters) { return []; } assert(actualConfig.remoteRouters, 'actualRemoteRouters is undefined'); assert(expectedConfig.remoteRouters, 'expectedRemoteRouters is undefined'); const { remoteRouters: actualRemoteRouters } = actualConfig; const { remoteRouters: expectedRemoteRouters } = expectedConfig; const routesToEnroll = Object.entries(expectedRemoteRouters) .filter(([domain, expectedRouter]) => { const actualRouter = actualRemoteRouters[domain]; // Enroll if router doesn't exist for domain or has different address return (!actualRouter || !eqAddress(actualRouter.address, expectedRouter.address)); }) .map(([domain]) => domain); if (routesToEnroll.length === 0) { return updateTransactions; } // in cosmos the gas is attached to the remote router. we set // it to zero for now and set the real value later during the // createSetDestinationGasUpdateTxs step routesToEnroll.forEach((domainId) => { updateTransactions.push({ annotation: `Enrolling Router ${this.args.addresses.deployedTokenRoute} on ${this.args.chain}`, typeUrl: R.MsgEnrollRemoteRouter.proto.type, value: R.MsgEnrollRemoteRouter.proto.converter.create({ owner: actualConfig.owner, token_id: this.args.addresses.deployedTokenRoute, remote_router: { receiver_domain: parseInt(domainId), receiver_contract: addressToBytes32(expectedRemoteRouters[domainId].address), gas: '0', }, }), }); }); return updateTransactions; } createUnenrollRemoteRoutersUpdateTxs(actualConfig, expectedConfig) { const updateTransactions = []; if (!expectedConfig.remoteRouters) { return []; } assert(actualConfig.remoteRouters, 'actualRemoteRouters is undefined'); assert(expectedConfig.remoteRouters, 'expectedRemoteRouters is undefined'); const { remoteRouters: actualRemoteRouters } = actualConfig; const { remoteRouters: expectedRemoteRouters } = expectedConfig; const routesToUnenroll = Array.from(difference(new Set(Object.keys(actualRemoteRouters)), new Set(Object.keys(expectedRemoteRouters)))); if (routesToUnenroll.length === 0) { return updateTransactions; } routesToUnenroll.forEach((domainId) => { updateTransactions.push({ annotation: `Unenrolling Router ${this.args.addresses.deployedTokenRoute} on ${this.args.chain}`, typeUrl: R.MsgUnrollRemoteRouter.proto.type, value: R.MsgUnrollRemoteRouter.proto.converter.create({ owner: actualConfig.owner, token_id: this.args.addresses.deployedTokenRoute, receiver_domain: parseInt(domainId), }), }); }); return updateTransactions; } /** * Create a transaction to update the remote routers for the Warp Route contract. * * @param actualConfig - The on-chain router configuration, including the remoteRouters array. * @param expectedConfig - The expected token router configuration. * @returns A array with Cosmos transactions that need to be executed to update the destination gas */ async createSetDestinationGasUpdateTxs(actualConfig, expectedConfig) { const updateTransactions = []; if (!expectedConfig.destinationGas) { return []; } assert(actualConfig.destinationGas, 'actualDestinationGas is undefined'); assert(expectedConfig.destinationGas, 'expectedDestinationGas is undefined'); assert(expectedConfig.remoteRouters, 'expectedRemoteRouters is undefined'); const { destinationGas: actualDestinationGas } = actualConfig; const { destinationGas: expectedDestinationGas } = expectedConfig; const { remoteRouters: expectedRemoteRouters } = expectedConfig; // refetch after routes have been previously enrolled without the "actualConfig" // updating const { remote_routers: actualRemoteRouters } = await this.signer.query.warp.RemoteRouters({ id: this.args.addresses.deployedTokenRoute, }); const alreadyEnrolledDomains = actualRemoteRouters.map((router) => router.receiver_domain); if (!deepEquals(actualDestinationGas, expectedDestinationGas)) { // Convert { 1: 2, 2: 3, ... } to [{ 1: 2 }, { 2: 3 }] const gasRouterConfigs = []; objMap(expectedDestinationGas, (domain, gas) => { gasRouterConfigs.push({ domain, gas, }); }); // in cosmos updating the gas config is done by unenrolling the router and then // enrolling it with the updating value again gasRouterConfigs.forEach(({ domain, gas }) => { if (alreadyEnrolledDomains.includes(parseInt(domain))) { updateTransactions.push({ annotation: `Unenrolling ${this.args.addresses.deployedTokenRoute} on ${this.args.chain}`, typeUrl: R.MsgUnrollRemoteRouter.proto.type, value: R.MsgUnrollRemoteRouter.proto.converter.create({ owner: actualConfig.owner, token_id: this.args.addresses.deployedTokenRoute, receiver_domain: parseInt(domain), }), }); } updateTransactions.push({ annotation: `Setting destination gas for ${this.args.addresses.deployedTokenRoute} on ${this.args.chain}`, typeUrl: R.MsgEnrollRemoteRouter.proto.type, value: R.MsgEnrollRemoteRouter.proto.converter.create({ owner: actualConfig.owner, token_id: this.args.addresses.deployedTokenRoute, remote_router: { receiver_domain: parseInt(domain), receiver_contract: addressToBytes32(expectedRemoteRouters[domain].address), gas, }, }), }); }); } return updateTransactions; } /** * Create transactions to update an existing ISM config, or deploy a new ISM and return a tx to setInterchainSecurityModule * * @param actualConfig - The on-chain router configuration, including the ISM configuration, and address. * @param expectedConfig - The expected token router configuration, including the ISM configuration. * @returns Cosmos transaction that need to be executed to update the ISM configuration. */ async createIsmUpdateTxs(actualConfig, expectedConfig) { const updateTransactions = []; if (!expectedConfig.interchainSecurityModule || expectedConfig.interchainSecurityModule === zeroAddress) { return []; } const actualDeployedIsm = actualConfig.interchainSecurityModule.address; // Try to update (may also deploy) Ism with the expected config const { deployedIsm: expectedDeployedIsm, updateTransactions: ismUpdateTransactions, } = await this.deployOrUpdateIsm(actualConfig, expectedConfig); // If an ISM is updated in-place, push the update txs updateTransactions.push(...ismUpdateTransactions); // If a new ISM is deployed, push the setInterchainSecurityModule tx if (actualDeployedIsm !== expectedDeployedIsm) { updateTransactions.push({ annotation: `Setting ISM for Warp Route to ${expectedDeployedIsm}`, typeUrl: R.MsgSetToken.proto.type, value: R.MsgSetToken.proto.converter.create({ owner: actualConfig.owner, token_id: this.args.addresses.deployedTokenRoute, ism_id: expectedDeployedIsm, }), }); } return updateTransactions; } /** * Transfer ownership of an existing Warp route with a given config. * * @param actualConfig - The on-chain router configuration. * @param expectedConfig - The expected token router configuration. * @returns Cosmos transaction that need to be executed to update the owner. */ createOwnershipUpdateTxs(actualConfig, expectedConfig) { if (eqAddress(actualConfig.owner, expectedConfig.owner)) { return []; } return [ { annotation: `Transferring ownership of ${this.args.addresses.deployedTokenRoute} from ${actualConfig.owner} to ${expectedConfig.owner}`, typeUrl: R.MsgSetToken.proto.type, value: R.MsgSetToken.proto.converter.create({ owner: actualConfig.owner, token_id: this.args.addresses.deployedTokenRoute, new_owner: expectedConfig.owner, }), }, ]; } /** * Updates or deploys the ISM using the provided configuration. * * @returns Object with deployedIsm address, and update Transactions */ async deployOrUpdateIsm(actualConfig, expectedConfig) { assert(expectedConfig.interchainSecurityModule, 'Ism derived incorrectly'); const ismModule = new CosmosNativeIsmModule(this.metadataManager, { chain: this.args.chain, config: expectedConfig.interchainSecurityModule, addresses: { ...this.args.addresses, mailbox: expectedConfig.mailbox, deployedIsm: actualConfig.interchainSecurityModule.address, }, }, this.signer); this.logger.info(`Comparing target ISM config with ${this.args.chain} chain`); const updateTransactions = await ismModule.update(expectedConfig.interchainSecurityModule); const { deployedIsm } = ismModule.serialize(); return { deployedIsm, updateTransactions }; } /** * Deploys the Warp Route. * * @param chain - The chain to deploy the module on. * @param config - The configuration for the token router. * @param multiProvider - The multi-provider instance to use. * @param signer - The Cosmos signing client * @returns A new instance of the CosmosNativeWarpModule. */ static async create(params) { const { chain, config, multiProvider, signer } = params; const deployer = new CosmosNativeDeployer(multiProvider, { [chain]: signer, }); const { [chain]: deployedTokenRoute } = await deployer.deploy({ [chain]: config, }); const warpModule = new CosmosNativeWarpModule(multiProvider, { addresses: { deployedTokenRoute, }, chain, config, }, signer); return warpModule; } } //# sourceMappingURL=CosmosNativeWarpModule.js.map