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