UNPKG

@hyperlane-xyz/sdk

Version:

The official SDK for the Hyperlane Network

207 lines 9.04 kB
import { COSMOS_MODULE_MESSAGE_REGISTRY as R, } from '@hyperlane-xyz/cosmos-sdk'; import { assert, deepEquals, intersection, rootLogger, } from '@hyperlane-xyz/utils'; import { HyperlaneModule, } from '../core/AbstractHyperlaneModule.js'; import { normalizeConfig } from '../utils/ism.js'; import { CosmosNativeIsmReader } from './CosmosNativeIsmReader.js'; import { IsmConfigSchema, IsmType, STATIC_ISM_TYPES, } from './types.js'; import { calculateDomainRoutingDelta } from './utils.js'; export class CosmosNativeIsmModule extends HyperlaneModule { metadataManager; signer; logger = rootLogger.child({ module: 'CosmosNativeIsmModule', }); reader; mailbox; // Adding these to reduce how often we need to grab from MetadataManager. chain; chainId; domainId; constructor(metadataManager, params, signer) { params.config = IsmConfigSchema.parse(params.config); super(params); this.metadataManager = metadataManager; this.signer = signer; this.mailbox = params.addresses.mailbox; this.chain = metadataManager.getChainName(this.args.chain); this.chainId = metadataManager.getChainId(this.chain); this.domainId = metadataManager.getDomainId(this.chain); this.reader = new CosmosNativeIsmReader(this.metadataManager, this.signer); } async read() { return this.reader.deriveIsmConfig(this.args.addresses.deployedIsm); } // whoever calls update() needs to ensure that targetConfig has a valid owner async update(expectedConfig) { expectedConfig = IsmConfigSchema.parse(expectedConfig); // Do not support updating to a custom ISM address if (typeof expectedConfig === 'string') { throw new Error('Invalid targetConfig: Updating to a custom ISM address is not supported. Please provide a valid ISM configuration.'); } // save current config for comparison // normalize the config to ensure it's in a consistent format for comparison const actualConfig = normalizeConfig(await this.read()); expectedConfig = normalizeConfig(expectedConfig); assert(typeof expectedConfig === 'object', 'normalized expectedConfig should be an object'); // Update the config this.args.config = expectedConfig; // If configs match, no updates needed if (deepEquals(actualConfig, expectedConfig)) { return []; } // if the ISM is a static ISM we can not update it, instead // it needs to be recreated with the expected config if (STATIC_ISM_TYPES.includes(expectedConfig.type)) { this.args.addresses.deployedIsm = await this.deploy({ config: expectedConfig, }); return []; } let updateTxs = []; if (expectedConfig.type === IsmType.ROUTING) { const logger = this.logger.child({ destination: this.chain, ismType: expectedConfig.type, }); logger.debug(`Updating ${expectedConfig.type} on ${this.chain}`); updateTxs = await this.updateRoutingIsm({ actual: actualConfig, expected: expectedConfig, logger, }); } return updateTxs; } // manually write static create function static async create({ chain, config, addresses, multiProvider, signer, }) { const module = new CosmosNativeIsmModule(multiProvider, { addresses: { ...addresses, deployedIsm: '', }, chain, config, }, signer); module.args.addresses.deployedIsm = await module.deploy({ config }); return module; } async deploy({ config }) { if (typeof config === 'string') { return config; } const ismType = config.type; this.logger.info(`Deploying ${ismType} to ${this.chain}`); switch (ismType) { case IsmType.MERKLE_ROOT_MULTISIG: { return this.deployMerkleRootMultisigIsm(config); } case IsmType.MESSAGE_ID_MULTISIG: { return this.deployMessageIdMultisigIsm(config); } case IsmType.ROUTING: { return this.deployRoutingIsm(config); } case IsmType.TEST_ISM: { return this.deployNoopIsm(); } default: throw new Error(`ISM type ${ismType} is not supported on Cosmos Native`); } } async deployMerkleRootMultisigIsm(config) { assert(config.threshold <= config.validators.length, `threshold (${config.threshold}) for merkle root multisig ISM is greater than number of validators (${config.validators.length})`); const { response } = await this.signer.createMerkleRootMultisigIsm({ validators: config.validators, threshold: config.threshold, }); return response.id; } async deployMessageIdMultisigIsm(config) { assert(config.threshold <= config.validators.length, `threshold (${config.threshold}) for message id multisig ISM is greater than number of validators (${config.validators.length})`); const { response } = await this.signer.createMessageIdMultisigIsm({ validators: config.validators, threshold: config.threshold, }); return response.id; } async deployRoutingIsm(config) { const routes = []; // deploy ISMs for each domain for (const chainName of Object.keys(config.domains)) { const domainId = this.metadataManager.tryGetDomainId(chainName); if (!domainId) { this.logger.warn(`Unknown chain ${chainName}, skipping ISM configuration`); continue; } const address = await this.deploy({ config: config.domains[chainName] }); routes.push({ ism: address, domain: domainId, }); } const { response } = await this.signer.createRoutingIsm({ routes, }); return response.id; } async updateRoutingIsm({ actual, expected, logger, }) { const updateTxs = []; const knownChains = new Set(this.metadataManager.getKnownChainNames()); const { domainsToEnroll, domainsToUnenroll } = calculateDomainRoutingDelta(actual, expected); const knownEnrolls = intersection(knownChains, new Set(domainsToEnroll)); // Enroll domains for (const origin of knownEnrolls) { logger.debug(`Reconfiguring preexisting routing ISM for origin ${origin}...`); const ism = await this.deploy({ config: expected.domains[origin], }); const domain = this.metadataManager.getDomainId(origin); updateTxs.push({ annotation: `Setting new ISM for origin ${origin}...`, typeUrl: R.MsgSetRoutingIsmDomain.proto.type, value: R.MsgSetRoutingIsmDomain.proto.converter.create({ owner: actual.owner, ism_id: this.args.addresses.deployedIsm, route: { ism, domain, }, }), }); } const knownUnenrolls = intersection(knownChains, new Set(domainsToUnenroll)); // Unenroll domains for (const origin of knownUnenrolls) { const domain = this.metadataManager.getDomainId(origin); updateTxs.push({ annotation: `Unenrolling originDomain ${domain} from preexisting routing ISM at ${this.args.addresses.deployedIsm}...`, typeUrl: R.MsgRemoveRoutingIsmDomain.proto.type, value: R.MsgRemoveRoutingIsmDomain.proto.converter.create({ owner: actual.owner, ism_id: this.args.addresses.deployedIsm, domain, }), }); } // Update ownership if (actual.owner !== expected.owner) { updateTxs.push({ annotation: `Transferring ownership of ISM from ${actual.owner} to ${expected.owner}`, typeUrl: R.MsgUpdateRoutingIsmOwner.proto.type, value: R.MsgUpdateRoutingIsmOwner.proto.converter.create({ owner: actual.owner, ism_id: this.args.addresses.deployedIsm, new_owner: expected.owner, // if the new owner is empty we renounce the ownership renounce_ownership: !expected.owner, }), }); } return updateTxs; } async deployNoopIsm() { const { response } = await this.signer.createNoopIsm({}); return response.id; } } //# sourceMappingURL=CosmosNativeIsmModule.js.map