UNPKG

@hyperlane-xyz/sdk

Version:

The official SDK for the Hyperlane Network

482 lines 22.2 kB
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