@hyperlane-xyz/sdk
Version:
The official SDK for the Hyperlane Network
482 lines • 22.2 kB
JavaScript
import { constants } from 'ethers';
import { zeroAddress } from 'viem';
import { ProtocolType } from '@hyperlane-xyz/provider-sdk';
import { addressToBytes32, assert, deepCopy, intersection, isAddressEvm, isCosmosIbcDenomAddress, isEVMLike, isObjEmpty, objFilter, objMap, promiseObjAll, sortArraysInObject, transformObj, } from '@hyperlane-xyz/utils';
import { isProxy } from '../deploy/proxy.js';
import { TokenFeeType, } from '../fee/types.js';
import { EvmHookReader } from '../hook/EvmHookReader.js';
import { EvmIsmReader } from '../ism/EvmIsmReader.js';
import { resolveRouterMapConfig, } from '../router/types.js';
import { normalizeScale } from '../utils/decimals.js';
import { EvmWarpRouteReader } from './EvmWarpRouteReader.js';
import { gasOverhead } from './config.js';
import { deriveTokenMetadata } from './tokenMetadataUtils.js';
import { ContractVerificationStatus, OwnerStatus, isCollateralTokenConfig, isCrossCollateralTokenConfig, isDepositAddressTokenConfig, isMovableCollateralTokenConfig, isNativeTokenConfig, isOftTokenConfig, isSyntheticRebaseTokenConfig, isSyntheticTokenConfig, } 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]));
}
/**
* Gets the chain names from a WarpCoreConfig
*/
export function getChainsFromWarpCoreConfig(warpCoreConfig) {
return warpCoreConfig.tokens.map((token) => token.chainName);
}
/**
* Checks if a WarpCoreConfig includes all specified chains
* @param config - The warp core config to check
* @param chains - Array of chain names that must all be present
* @returns true if the config spans all specified chains
*/
export function warpCoreConfigMatchesChains(config, chains) {
const configChains = new Set(getChainsFromWarpCoreConfig(config));
return chains.every((chain) => configChains.has(chain));
}
/**
* Filters a map of WarpCoreConfigs to only include routes that span all specified chains
* @param configMap - Record of route IDs to WarpCoreConfig
* @param chains - Array of chain names that must all be present in each route
* @returns Filtered record containing only routes that span all specified chains.
* If `chains` is empty, returns `configMap` unchanged (treated as no filter).
*/
export function filterWarpCoreConfigMapByChains(configMap, chains) {
if (chains.length === 0) {
return configMap;
}
return objFilter(configMap, (_, config) => warpCoreConfigMatchesChains(config, chains));
}
/**
* 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, warpDeployConfig, deployedRoutersAddresses, expandedOnChainWarpConfig, validateScale = true, } = params;
const derivedTokenMetadata = await deriveTokenMetadata(multiProvider, warpDeployConfig, { validateScale });
// 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 (!isEVMLike(multiProvider.getProtocol(chain))) {
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,
};
if (chainConfig.proxyAdmin) {
chainConfig.proxyAdmin = {
...chainConfig.proxyAdmin,
owner: config.ownerOverrides?.proxyAdmin ??
chainConfig.proxyAdmin.owner ??
config.owner,
};
}
// 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 selfDomain = multiProvider.getDomainId(chain).toString();
const ccrGasDomains = isCrossCollateralTokenConfig(chainConfig)
? Object.keys(chainConfig.crossCollateralRouters ?? {}).filter((domain) => domain !== selfDomain)
: [];
const gasDomainsToKeep = new Set([
...Object.keys(formattedRemoteRouters),
...ccrGasDomains,
]);
// CrossCollateralRouter may require destination gas for CCR-only domains
// that are not present in Router._routers.
if (isCrossCollateralTokenConfig(chainConfig)) {
for (const domain of ccrGasDomains) {
gasDomainsToKeep.add(domain);
// Ensure CCR-only destinations get destinationGas defaults so
// warp check/apply can enforce gas config on enrolled CCR domains.
if (!chainConfig.destinationGas?.[domain]) {
chainConfig.destinationGas = {
...chainConfig.destinationGas,
[domain]: chainConfig.gas?.toString() ||
gasOverhead(chainConfig.type).toString(),
};
}
}
}
const remoteGasDomainsToKeep = intersection(new Set(Object.keys(chainConfig.destinationGas ?? {})), gasDomainsToKeep);
// 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 = isEVMLike(protocol);
// 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
// (not just an address string for EVM - those are left as-is to avoid deriving)
if (chainConfig.hook && typeof chainConfig.hook !== 'string') {
switch (protocol) {
case ProtocolType.Tron:
case ProtocolType.Ethereum: {
const reader = new EvmHookReader(multiProvider, chain);
chainConfig.hook = await reader.deriveHookConfig(chainConfig.hook);
break;
}
default: {
// For non-EVM chains: config objects are kept as-is (no recursive expansion support)
// TODO: Handle HookConfig objects (nested config expansion) when Artifact API adds support
break;
}
}
}
// Expand the ism config only if we have an explicit config in the deploy config
// (not just an address string for EVM - those are left as-is to avoid deriving)
if (chainConfig.interchainSecurityModule &&
typeof chainConfig.interchainSecurityModule !== 'string') {
switch (protocol) {
case ProtocolType.Tron:
case ProtocolType.Ethereum: {
const reader = new EvmIsmReader(multiProvider, chain);
chainConfig.interchainSecurityModule = await reader.deriveIsmConfig(chainConfig.interchainSecurityModule);
break;
}
default: {
// For non-EVM chains: config objects are kept as-is (no recursive expansion support)
// TODO: Handle IsmConfig objects (nested config expansion) when Artifact API adds support
break;
}
}
}
if (chainConfig.tokenFee) {
const routerAddress = deployedRoutersAddresses[chain];
assert(routerAddress, `Missing deployed router address for ${chain}`);
chainConfig.tokenFee = resolveTokenFeeAddress(chainConfig.tokenFee, routerAddress, chainConfig);
}
return chainConfig;
}));
}
export function normalizeWarpDeployConfigForCheck(params) {
const { multiProvider, warpDeployConfig } = params;
return objMap(warpDeployConfig, (_chain, config) => {
if (isDepositAddressTokenConfig(config)) {
return {
...config,
mailbox: constants.AddressZero,
interchainSecurityModule: constants.AddressZero,
remoteRouters: {},
destinationGas: undefined,
};
}
if (!isOftTokenConfig(config)) {
return config;
}
return {
...config,
mailbox: constants.AddressZero,
hook: constants.AddressZero,
interchainSecurityModule: constants.AddressZero,
remoteRouters: {},
destinationGas: undefined,
domainMappings: resolveRouterMapConfig(multiProvider, config.domainMappings),
extraOptions: config.extraOptions === '0x' ? undefined : config.extraOptions,
};
});
}
/**
* Resolves the fee token address based on the warp route token type.
* - Native tokens: fee token is AddressZero
* - Collateral tokens: fee token is the collateral token address
* - Synthetic tokens: fee token is the router address (the HypERC20 itself)
*/
function getFeeTokenAddress(routerAddress, tokenConfig) {
if (isNativeTokenConfig(tokenConfig)) {
return constants.AddressZero;
}
if (isCollateralTokenConfig(tokenConfig) ||
isCrossCollateralTokenConfig(tokenConfig)) {
return tokenConfig.token;
}
if (isSyntheticTokenConfig(tokenConfig) ||
isSyntheticRebaseTokenConfig(tokenConfig)) {
return routerAddress;
}
throw new Error(`Unsupported token type for fee resolution`);
}
function resolveCrossCollateralFeeContracts(destinationConfig, routerAddress, tokenConfig) {
return Object.fromEntries(Object.entries(destinationConfig).map(([router, subFee]) => [
router,
resolveTokenFeeAddress(subFee, routerAddress, tokenConfig),
]));
}
export function resolveTokenFeeAddress(feeConfig, routerAddress, tokenConfig) {
const feeToken = getFeeTokenAddress(routerAddress, tokenConfig);
if (feeConfig.type === TokenFeeType.RoutingFee) {
return {
...feeConfig,
token: feeToken,
feeContracts: Object.fromEntries(Object.entries(feeConfig.feeContracts).map(([chain, subFee]) => [
chain,
resolveTokenFeeAddress(subFee, routerAddress, tokenConfig),
])),
};
}
if (feeConfig.type === TokenFeeType.CrossCollateralRoutingFee) {
return {
...feeConfig,
feeContracts: Object.fromEntries(Object.keys(feeConfig.feeContracts).map((chain) => [
chain,
resolveCrossCollateralFeeContracts(feeConfig.feeContracts[chain], routerAddress, tokenConfig),
])),
};
}
return {
...feeConfig,
token: feeToken,
};
}
export async function expandVirtualWarpDeployConfig(params) {
const { multiProvider, onChainWarpConfig, deployedRoutersAddresses } = params;
return promiseObjAll(objMap(onChainWarpConfig, async (chain, config) => {
const warpReader = new EvmWarpRouteReader(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 parentObjectKey = propPath[propPath.length - 2];
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' &&
parentObjectKey !== 'proxyAdmin') ||
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;
}
// Sort allowedRebalancingBridges by bridge address
if (a.bridge && b.bridge) {
if (a.bridge < b.bridge)
return -1;
if (a.bridge > b.bridge)
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',
]);
function normalizeCrossCollateralFeeContractsForCheck(destinationConfig) {
return Object.fromEntries(Object.entries(destinationConfig).map(([router, nestedFee]) => [
router,
normalizeTokenFeeForCheck(nestedFee),
]));
}
function normalizeTokenFeeForCheck(feeConfig) {
if (!feeConfig)
return feeConfig;
const tokenConfig = 'token' in feeConfig && feeConfig.token ? { token: feeConfig.token } : {};
if (feeConfig.type === TokenFeeType.RoutingFee) {
const normalizedFeeContracts = Object.fromEntries(Object.entries(feeConfig.feeContracts).map(([chain, nestedFee]) => [
chain,
normalizeTokenFeeForCheck(nestedFee),
]));
return {
type: TokenFeeType.RoutingFee,
owner: feeConfig.owner,
...tokenConfig,
feeContracts: normalizedFeeContracts,
};
}
if (feeConfig.type === TokenFeeType.CrossCollateralRoutingFee) {
const normalizedFeeContracts = Object.fromEntries(Object.keys(feeConfig.feeContracts).map((chain) => [
chain,
normalizeCrossCollateralFeeContractsForCheck(feeConfig.feeContracts[chain]),
]));
return {
type: TokenFeeType.CrossCollateralRoutingFee,
owner: feeConfig.owner,
feeContracts: normalizedFeeContracts,
};
}
if (feeConfig.type === TokenFeeType.OffchainQuotedLinearFee) {
return {
type: feeConfig.type,
owner: feeConfig.owner,
bps: feeConfig.bps,
...tokenConfig,
quoteSigners: feeConfig.quoteSigners,
};
}
if (feeConfig.type === TokenFeeType.LinearFee) {
return {
type: feeConfig.type,
owner: feeConfig.owner,
bps: feeConfig.bps,
...tokenConfig,
};
}
return feeConfig;
}
/**
* 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;
}
if (clonedTokenConfig.tokenFee) {
clonedTokenConfig.tokenFee = normalizeTokenFeeForCheck(clonedTokenConfig.tokenFee);
}
// normalizeScale(undefined) -> {1n,1n}, matching EvmWarpRouteReader.fetchScale's
// identity-collapse so both sides of the diff agree symmetrically.
clonedTokenConfig.scale = normalizeScale(clonedTokenConfig.scale);
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