@hyperlane-xyz/sdk
Version:
The official SDK for the Hyperlane Network
268 lines • 13.5 kB
JavaScript
import { zeroAddress } from 'viem';
import { AltVMHookReader, AltVMIsmReader } from '@hyperlane-xyz/deploy-sdk';
import { ProtocolType } from '@hyperlane-xyz/provider-sdk';
import { addressToBytes32, assert, deepCopy, intersection, isAddressEvm, isCosmosIbcDenomAddress, isObjEmpty, mustGet, objFilter, objMap, promiseObjAll, sortArraysInObject, transformObj, } from '@hyperlane-xyz/utils';
import { isProxy } from '../deploy/proxy.js';
import { EvmHookReader } from '../hook/EvmHookReader.js';
import { EvmIsmReader } from '../ism/EvmIsmReader.js';
import { EvmERC20WarpRouteReader } from './EvmERC20WarpRouteReader.js';
import { gasOverhead } from './config.js';
import { HypERC20Deployer } from './deploy.js';
import { ContractVerificationStatus, OwnerStatus, isMovableCollateralTokenConfig, } from './types.js';
/**
* Gets gas configuration for a chain
*/
const getGasConfig = (warpDeployConfig, chain) => {
const chainDeployConfig = warpDeployConfig[chain];
assert(chainDeployConfig, `Deploy config not found for chain ${chain}. Unable to get gas config`);
return (chainDeployConfig.gas?.toString() ||
gasOverhead(chainDeployConfig.type).toString());
};
/**
* Returns default router addresses and gas values for cross-chain communication.
* For each remote chain:
* - Sets up router addresses for message routing
* - Configures gas values for message processing
*/
export function getDefaultRemoteRouterAndDestinationGasConfig(multiProvider, chain, deployedRoutersAddresses, warpDeployConfig) {
const remoteRouters = {};
const destinationGas = {};
const otherChains = multiProvider.getRemoteChains(chain).filter((remoteChain) =>
// Include chains that specify foreignDeployment so that they can be enrolled
// in the current deployment/update
Object.keys(deployedRoutersAddresses).includes(remoteChain) ||
warpDeployConfig[remoteChain]?.foreignDeployment);
for (const otherChain of otherChains) {
const domainId = multiProvider.getDomainId(otherChain);
remoteRouters[domainId] = {
address:
// Include chains that specify foreignDeployment so that the gas configuration
// can be in the current deployment/update
deployedRoutersAddresses[otherChain] ??
warpDeployConfig[otherChain].foreignDeployment,
};
destinationGas[domainId] = getGasConfig(warpDeployConfig, otherChain);
}
return [remoteRouters, destinationGas];
}
export function getRouterAddressesFromWarpCoreConfig(warpCoreConfig) {
return Object.fromEntries(warpCoreConfig.tokens
// Removing IBC denom addresses because they are on the same
// chain as the actual warp token but they are only used
// used to pay the IGP hook
.filter((token) => token.addressOrDenom &&
!isCosmosIbcDenomAddress(token.addressOrDenom))
.map((token) => [token.chainName, token.addressOrDenom]));
}
/**
* Expands a Warp deploy config with additional data
*
* @param multiProvider
* @param warpDeployConfig - The warp deployment config
* @param deployedRoutersAddresses - Addresses of deployed routers for each chain
* @param virtualConfig - Optional virtual config to include in the warpDeployConfig
* @returns A promise resolving to an expanded Warp deploy config with derived and virtual metadata
*/
export async function expandWarpDeployConfig(params) {
const { multiProvider, altVmProviders, warpDeployConfig, deployedRoutersAddresses, expandedOnChainWarpConfig, } = params;
const derivedTokenMetadata = await HypERC20Deployer.deriveTokenMetadata(multiProvider, warpDeployConfig);
// If the token is on an EVM chain check if it is deployed as a proxy
// to expand the proxy config too
const isDeployedAsProxyByChain = await promiseObjAll(objMap(deployedRoutersAddresses, async (chain, address) => {
if (!(multiProvider.getProtocol(chain) === ProtocolType.Ethereum)) {
return false;
}
return isProxy(multiProvider.getProvider(chain), address);
}));
return promiseObjAll(objMap(warpDeployConfig, async (chain, config) => {
const [remoteRouters, destinationGas] = getDefaultRemoteRouterAndDestinationGasConfig(multiProvider, chain, deployedRoutersAddresses, warpDeployConfig);
const chainConfig = {
// Default Expansion
name: derivedTokenMetadata.getName(chain),
symbol: derivedTokenMetadata.getSymbol(chain),
decimals: derivedTokenMetadata.getDecimals(chain),
scale: derivedTokenMetadata.getScale(chain),
remoteRouters,
destinationGas,
hook: zeroAddress,
interchainSecurityModule: zeroAddress,
proxyAdmin: isDeployedAsProxyByChain[chain]
? { owner: config.owner }
: undefined,
isNft: false,
// User-specified config takes precedence
...config,
};
// Properly set the remote routers addresses to their 32 bytes representation
// as that is how they are set on chain
const formattedRemoteRouters = objMap(chainConfig.remoteRouters ?? {}, (_domainId, { address }) => ({
address: addressToBytes32(address),
}));
chainConfig.remoteRouters = formattedRemoteRouters;
const remoteGasDomainsToKeep = intersection(new Set(Object.keys(chainConfig.destinationGas ?? {})), new Set(Object.keys(formattedRemoteRouters)));
// If the deploy config specified a custom config for remote routers
// we should not have all the gas settings set
const formattedDestinationGas = objFilter(chainConfig.destinationGas ?? {}, (domainId, _gasSetting) => remoteGasDomainsToKeep.has(domainId));
chainConfig.destinationGas = formattedDestinationGas;
const protocol = multiProvider.getProtocol(chain);
const isEVMChain = protocol === ProtocolType.Ethereum;
// Expand EVM warpDeployConfig virtual to the control states (states that we expect)
// For contractVerificationStatus, all values should be 'verified'
// For ownerStatus, all values should be 'active or 'gnosisSafe'
if (isEVMChain &&
expandedOnChainWarpConfig?.[chain]?.contractVerificationStatus) {
// For most cases, we set to Verified
chainConfig.contractVerificationStatus = objMap(expandedOnChainWarpConfig[chain].contractVerificationStatus ?? {}, (_, status) => {
switch (status) {
case ContractVerificationStatus.Skipped:
case ContractVerificationStatus.Verified:
return status; // Pass through the status so diffs will be shown
case ContractVerificationStatus.Unverified:
case ContractVerificationStatus.Error:
return ContractVerificationStatus.Verified;
}
});
}
if (isEVMChain && expandedOnChainWarpConfig?.[chain]?.ownerStatus) {
// For 'active' or 'gnosis-safe', we set their actual state as the control because they are both acceptable.
// For other cases, we expect 'active'
chainConfig.ownerStatus = objMap(expandedOnChainWarpConfig[chain].ownerStatus ?? {}, (_, status) => {
switch (status) {
// Skipped for local e2e testing
case OwnerStatus.Skipped:
case OwnerStatus.Active:
case OwnerStatus.GnosisSafe:
return status; // Pass through the status so diffs will be shown
case OwnerStatus.Error:
case OwnerStatus.Inactive:
return OwnerStatus.Active;
}
});
}
// Expand the hook config only if we have an explicit config in the deploy config
// and the current chain is an EVM one.
// if we have an address we leave it like that to avoid deriving
if (chainConfig.hook && typeof chainConfig.hook !== 'string') {
switch (protocol) {
case ProtocolType.Ethereum: {
const reader = new EvmHookReader(multiProvider, chain);
chainConfig.hook = await reader.deriveHookConfig(chainConfig.hook);
break;
}
default: {
const provider = mustGet(altVmProviders, chain);
const reader = new AltVMHookReader((chain) => multiProvider.getChainMetadata(chain), provider);
chainConfig.hook = await reader.deriveHookConfig(
// FIXME: not all hook types are supported yet
chainConfig.hook);
}
}
}
// Expand the ism config only if we have an explicit config in the deploy config
// if we have an address we leave it like that to avoid deriving
if (chainConfig.interchainSecurityModule &&
typeof chainConfig.interchainSecurityModule !== 'string') {
switch (protocol) {
case ProtocolType.Ethereum: {
const reader = new EvmIsmReader(multiProvider, chain);
chainConfig.interchainSecurityModule = await reader.deriveIsmConfig(chainConfig.interchainSecurityModule);
break;
}
default: {
const provider = mustGet(altVmProviders, chain);
const reader = new AltVMIsmReader((chain) => multiProvider.tryGetChainName(chain), provider);
chainConfig.interchainSecurityModule = await reader.deriveIsmConfig(
// FIXME: not all ISM types are supported yet
chainConfig.interchainSecurityModule);
}
}
}
return chainConfig;
}));
}
export async function expandVirtualWarpDeployConfig(params) {
const { multiProvider, onChainWarpConfig, deployedRoutersAddresses } = params;
return promiseObjAll(objMap(onChainWarpConfig, async (chain, config) => {
const warpReader = new EvmERC20WarpRouteReader(multiProvider, chain);
const warpVirtualConfig = await warpReader.deriveWarpRouteVirtualConfig(chain, deployedRoutersAddresses[chain]);
return {
...warpVirtualConfig,
...config,
hook: config.hook ?? zeroAddress,
};
}));
}
const transformWarpDeployConfigToCheck = (obj, propPath) => {
// Needed to check if we are currently inside the remoteRouters object
const maybeRemoteRoutersKey = propPath[propPath.length - 3];
const parentKey = propPath[propPath.length - 1];
// Remove the address and ownerOverrides fields if we are not inside the
// remoteRouters property
if ((parentKey === 'address' && maybeRemoteRoutersKey !== 'remoteRouters') ||
parentKey === 'ownerOverrides') {
return undefined;
}
if (typeof obj === 'string' && parentKey !== 'type' && isAddressEvm(obj)) {
return obj.toLowerCase();
}
return obj;
};
const sortArraysInConfigToCheck = (a, b) => {
if (a.type && b.type) {
if (a.type < b.type)
return -1;
if (a.type > b.type)
return 1;
return 0;
}
if (a < b)
return -1;
if (a > b)
return 1;
return 0;
};
const FIELDS_TO_IGNORE = new Set([
// gas is removed because the destinationGas is the result of
// expanding the config based on the gas value for each chain
// see `expandWarpDeployConfig` function
'gas',
// Removing symbol and token metadata as they are not critical for
// checking, even if they are set "incorrectly" they do not affect how
// the warp route works
'symbol',
'name',
]);
/**
* transforms the provided {@link HypTokenRouterConfig}, removing the address, totalSupply and ownerOverrides
* field where they are not required for the config comparison
*/
export function transformConfigToCheck(obj) {
const filteredObj = Object.fromEntries(Object.entries(obj).filter(([key, _value]) => !FIELDS_TO_IGNORE.has(key)));
const clonedTokenConfig = deepCopy(filteredObj);
if (isMovableCollateralTokenConfig(clonedTokenConfig)) {
clonedTokenConfig.allowedRebalancers = clonedTokenConfig.allowedRebalancers
?.length
? clonedTokenConfig.allowedRebalancers
: undefined;
clonedTokenConfig.allowedRebalancingBridges = !isObjEmpty(clonedTokenConfig.allowedRebalancingBridges ?? {})
? clonedTokenConfig.allowedRebalancingBridges
: undefined;
}
return sortArraysInObject(transformObj(clonedTokenConfig, transformWarpDeployConfigToCheck), sortArraysInConfigToCheck);
}
/**
* Splits warp deploy config into existing and extended configurations based on warp core chains
* for the warp apply process.
*/
export function splitWarpCoreAndExtendedConfigs(warpDeployConfig, warpCoreChains) {
return Object.entries(warpDeployConfig).reduce(([existing, extended], [chain, config]) => {
if (warpCoreChains.includes(chain)) {
existing[chain] = config;
}
else {
extended[chain] = config;
}
return [existing, extended];
}, [{}, {}]);
}
//# sourceMappingURL=configUtils.js.map