UNPKG

@hyperlane-xyz/sdk

Version:

The official SDK for the Hyperlane Network

558 lines 24.1 kB
import { compareVersions } from 'compare-versions'; import { z } from 'zod'; import { CONTRACTS_PACKAGE_VERSION } from '@hyperlane-xyz/core'; import { isAddressEvm, objMap } from '@hyperlane-xyz/utils'; import { TokenFeeType } from '../fee/types.js'; import { HookType } from '../hook/types.js'; import { IsmType, OffchainLookupIsmConfigSchema, } from '../ism/types.js'; import { ZBigNumberish, ZHash } from '../metadata/customZodTypes.js'; import { GasRouterConfigSchema, RemoteRouterDomainOrChainNameSchema, } from '../router/types.js'; import { isCompliant } from '../utils/schemas.js'; import { TokenType } from './config.js'; export const WarpRouteDeployConfigSchemaErrors = { ONLY_SYNTHETIC_REBASE: `Config with ${TokenType.collateralVaultRebase} must be deployed with ${TokenType.syntheticRebase}`, NO_SYNTHETIC_ONLY: `Config must include Native or Collateral OR all synthetics must define token metadata`, }; export const contractVersionMatchesDependency = (version) => { return compareVersions(version, CONTRACTS_PACKAGE_VERSION) === 0; }; export const VERSION_ERROR_MESSAGE = `Contract version must match the @hyperlane-xyz/core dependency version (${CONTRACTS_PACKAGE_VERSION})`; /** * Coerces string to bigint at runtime with positivity check. * Needed because bigints are serialized as strings in JSON/YAML * and must be converted back on parse. */ const PositiveZBigNumberish = ZBigNumberish.refine((n) => n > 0n, { message: 'Must be positive', }); export const TokenMetadataSchema = z.object({ name: z.string(), symbol: z.string(), decimals: z.number().gt(0).optional(), scale: z .union([ z.number().int().gt(0), z.object({ numerator: z.number().int().gt(0), denominator: z.number().int().gt(0), }), z.object({ numerator: PositiveZBigNumberish, denominator: PositiveZBigNumberish, }), ]) .optional(), isNft: z.boolean().optional(), contractVersion: z.string().optional(), }); export const isTokenMetadata = isCompliant(TokenMetadataSchema); const MovableTokenRebalancingBridgeConfigSchema = z.object({ bridge: ZHash, approvedTokens: z .array(ZHash) .transform((rawRebalancers) => Array.from(new Set(rawRebalancers))) .optional(), }); const BaseEverclearTokenBridgeConfigSchema = z.object({ everclearBridgeAddress: ZHash, outputAssets: z.record(RemoteRouterDomainOrChainNameSchema, ZHash), everclearFeeParams: z.record(RemoteRouterDomainOrChainNameSchema, z.object({ fee: z.number().int(), deadline: z.number().int(), signature: z.string(), })), }); export const isEverclearTokenBridgeConfig = isCompliant(BaseEverclearTokenBridgeConfigSchema); export const BaseMovableTokenConfigSchema = z.object({ allowedRebalancingBridges: z .record(RemoteRouterDomainOrChainNameSchema, z.array(MovableTokenRebalancingBridgeConfigSchema)) .optional(), allowedRebalancers: z .array(ZHash) .transform((rawRebalancers) => Array.from(new Set(rawRebalancers))) .optional(), }); // Predicate wrapper configuration for compliance-gated transfers export const PredicateWrapperConfigSchema = z.object({ predicateRegistry: ZHash.describe('Predicate registry contract address'), policyId: z .string() .min(1) .describe('Predicate policy ID for attestation validation'), owner: ZHash.describe('Owner of the predicate wrapper contract (controls setPolicyID, setRegistry, withdrawETH)'), }); export const isPredicateWrapperConfig = isCompliant(PredicateWrapperConfigSchema); export const NativeTokenConfigSchema = TokenMetadataSchema.partial().extend({ type: z.enum([TokenType.native, TokenType.nativeScaled]), ...BaseMovableTokenConfigSchema.shape, predicateWrapper: PredicateWrapperConfigSchema.optional(), }); export const isNativeTokenConfig = isCompliant(NativeTokenConfigSchema); export const OpL2TokenConfigSchema = NativeTokenConfigSchema.omit({ type: true, }).extend({ type: z.literal(TokenType.nativeOpL2), l2Bridge: z.string(), }); export const OpL1TokenConfigSchema = NativeTokenConfigSchema.omit({ type: true, }) .extend({ type: z.literal(TokenType.nativeOpL1), portal: z.string(), version: z.number(), }) .merge(OffchainLookupIsmConfigSchema.omit({ type: true, owner: true })); export const isOpL1TokenConfig = isCompliant(OpL1TokenConfigSchema); export const isOpL2TokenConfig = isCompliant(OpL2TokenConfigSchema); export const CollateralTokenConfigSchema = TokenMetadataSchema.partial().extend({ type: z.enum([ TokenType.collateral, TokenType.collateralVault, TokenType.collateralVaultRebase, TokenType.collateralFiat, TokenType.collateralUri, ]), token: z .string() .describe('Existing token address to extend with Warp Route functionality'), ...BaseMovableTokenConfigSchema.shape, predicateWrapper: PredicateWrapperConfigSchema.optional(), }); export const isCollateralTokenConfig = isCompliant(CollateralTokenConfigSchema); export var XERC20Type; (function (XERC20Type) { XERC20Type["Velo"] = "velo"; XERC20Type["Standard"] = "standard"; })(XERC20Type || (XERC20Type = {})); // Velo variant const XERC20VSLimitConfigSchema = z.object({ type: z.literal(XERC20Type.Velo), bufferCap: z.string().optional(), rateLimitPerSecond: z.string().optional(), }); const XERC20StandardLimitConfigSchema = z.object({ type: z.literal(XERC20Type.Standard), mint: z.string().optional(), burn: z.string().optional(), }); const xERC20Limits = z.discriminatedUnion('type', [ XERC20VSLimitConfigSchema, XERC20StandardLimitConfigSchema, ]); const xERC20ExtraBridgesLimitConfigsSchema = z.object({ lockbox: z.string(), limits: xERC20Limits, }); const xERC20TokenMetadataSchema = z.object({ xERC20: z .object({ extraBridges: z.array(xERC20ExtraBridgesLimitConfigsSchema).optional(), warpRouteLimits: xERC20Limits, }) .optional(), }); export const XERC20TokenConfigSchema = CollateralTokenConfigSchema.omit({ type: true, }) .extend({ type: z.enum([TokenType.XERC20, TokenType.XERC20Lockbox]), }) .merge(xERC20TokenMetadataSchema); export const isXERC20TokenConfig = isCompliant(XERC20TokenConfigSchema); export const CctpTokenConfigSchema = TokenMetadataSchema.partial() .extend({ type: z.literal(TokenType.collateralCctp), token: z.string().describe('CCTP enabled token'), messageTransmitter: z .string() .describe('CCTP Message Transmitter contract address'), tokenMessenger: z .string() .describe('CCTP Token Messenger contract address'), cctpVersion: z.enum(['V1', 'V2']), minFinalityThreshold: z.number().optional(), maxFeeBps: z .number() .min(0) .max(9_999.99) .optional() .describe('Maximum fee in basis points (bps), supports decimals for fractional bps. 1 bps = 0.01%. Examples: 1.3 bps for Circle Optimism/Arbitrum/Base fee, 1.5 bps for Circle Unichain fee. Internally converted to ppm (parts per million) for contract precision.'), predicateWrapper: PredicateWrapperConfigSchema.optional(), }) .merge(OffchainLookupIsmConfigSchema.omit({ type: true, owner: true })); export const isCctpTokenConfig = isCompliant(CctpTokenConfigSchema); export const DepositAddressRecipientConfigSchema = z.object({ depositAddress: z .string() .refine(isAddressEvm, 'depositAddress must be a valid EVM address'), feeBps: z .string() .or(z.number()) .optional() .refine((value) => { if (value === undefined) return true; if (typeof value === 'string' && value.trim() === '') return false; try { const feeBps = BigInt(value); return feeBps >= 0n && feeBps < 10000n; } catch { return false; } }, { message: 'feeBps must be a valid number >= 0 and < 10000', }) .describe('Bridge fee in basis points for this destination'), }); export const DepositAddressDestinationConfigSchema = z.record(ZHash.describe('Expected destination recipient as bytes32'), DepositAddressRecipientConfigSchema); export const DepositAddressTokenConfigSchema = TokenMetadataSchema.partial().extend({ type: z.literal(TokenType.collateralDepositAddress), token: z.string().describe('Underlying ERC20 token address'), destinationConfigs: z.record(RemoteRouterDomainOrChainNameSchema, DepositAddressDestinationConfigSchema), predicateWrapper: PredicateWrapperConfigSchema.optional(), }); export const isDepositAddressTokenConfig = isCompliant(DepositAddressTokenConfigSchema); export const OftTokenConfigSchema = TokenMetadataSchema.partial().extend({ type: z.literal(TokenType.collateralOft), token: z .string() .describe('Underlying ERC20 token address (resolved from OFT)'), oft: z.string().describe('OFT / OFTAdapter / OFTWrapper contract address'), domainMappings: z .record(RemoteRouterDomainOrChainNameSchema, z.number().int().nonnegative().max(4294967295)) .describe('Mapping of Hyperlane domain (or chain name) to LayerZero endpoint ID'), extraOptions: z.string().optional().describe('LayerZero extra options (hex)'), predicateWrapper: PredicateWrapperConfigSchema.optional(), }); export const isOftTokenConfig = isCompliant(OftTokenConfigSchema); export const CollateralRebaseTokenConfigSchema = TokenMetadataSchema.partial().extend({ type: z.literal(TokenType.collateralVaultRebase), }); export const isCollateralRebaseTokenConfig = isCompliant(CollateralRebaseTokenConfigSchema); export const SyntheticTokenConfigSchema = TokenMetadataSchema.partial().extend({ type: z.enum([TokenType.synthetic, TokenType.syntheticUri]), initialSupply: z.string().or(z.number()).optional(), predicateWrapper: PredicateWrapperConfigSchema.optional(), metadataUri: z.string().url().optional(), }); export const isSyntheticTokenConfig = isCompliant(SyntheticTokenConfigSchema); export const SyntheticRebaseTokenConfigSchema = TokenMetadataSchema.partial().extend({ type: z.literal(TokenType.syntheticRebase), collateralChainName: z.string(), predicateWrapper: PredicateWrapperConfigSchema.optional(), }); export const isSyntheticRebaseTokenConfig = isCompliant(SyntheticRebaseTokenConfigSchema); /** * Configuration for CrossCollateralRouter (multi-router collateral routing). * Direct 1-message atomic transfers between collateral routers. */ export const CrossCollateralTokenConfigSchema = TokenMetadataSchema.partial().extend({ type: z.literal(TokenType.crossCollateral), token: z.string().describe('Collateral token address'), /** Map of domain → router addresses to enroll */ crossCollateralRouters: z .record(RemoteRouterDomainOrChainNameSchema, z.array(ZHash)) .optional(), ...BaseMovableTokenConfigSchema.shape, predicateWrapper: PredicateWrapperConfigSchema.optional(), }); export const isCrossCollateralTokenConfig = isCompliant(CrossCollateralTokenConfigSchema); export const EverclearCollateralTokenConfigSchema = z.object({ type: z.literal(TokenType.collateralEverclear), ...CollateralTokenConfigSchema.omit({ type: true }).shape, ...BaseEverclearTokenBridgeConfigSchema.shape, }); export const isEverclearCollateralTokenConfig = isCompliant(EverclearCollateralTokenConfigSchema); export const EverclearEthBridgeTokenConfigSchema = z.object({ type: z.literal(TokenType.ethEverclear), wethAddress: ZHash, ...NativeTokenConfigSchema.omit({ type: true }).shape, ...BaseEverclearTokenBridgeConfigSchema.shape, }); export const isEverclearEthBridgeTokenConfig = isCompliant(EverclearEthBridgeTokenConfigSchema); export var ContractVerificationStatus; (function (ContractVerificationStatus) { ContractVerificationStatus["Verified"] = "verified"; ContractVerificationStatus["Unverified"] = "unverified"; ContractVerificationStatus["Error"] = "error"; ContractVerificationStatus["Skipped"] = "skipped"; })(ContractVerificationStatus || (ContractVerificationStatus = {})); export var OwnerStatus; (function (OwnerStatus) { OwnerStatus["Active"] = "active"; OwnerStatus["Inactive"] = "inactive"; OwnerStatus["GnosisSafe"] = "gnosisSafe"; OwnerStatus["Error"] = "error"; OwnerStatus["Skipped"] = "skipped"; })(OwnerStatus || (OwnerStatus = {})); export const HypTokenRouterVirtualConfigSchema = z.object({ contractVerificationStatus: z.record(z.enum([ ContractVerificationStatus.Error, ContractVerificationStatus.Skipped, ContractVerificationStatus.Verified, ContractVerificationStatus.Unverified, ])), ownerStatus: z.record(z.enum([ OwnerStatus.Error, OwnerStatus.Skipped, OwnerStatus.Active, OwnerStatus.Inactive, OwnerStatus.GnosisSafe, ])), }); export const UnknownTokenConfigSchema = TokenMetadataSchema.partial() .extend({ type: z.literal(TokenType.unknown), predicateWrapper: PredicateWrapperConfigSchema.optional(), }) .passthrough(); export const isUnknownTokenConfig = isCompliant(UnknownTokenConfigSchema); const KnownTokenTypes = Object.values(TokenType).filter((t) => t !== TokenType.unknown); const AllHypTokenConfigSchema = z.discriminatedUnion('type', [ NativeTokenConfigSchema, OpL2TokenConfigSchema, OpL1TokenConfigSchema, CollateralTokenConfigSchema, XERC20TokenConfigSchema, SyntheticTokenConfigSchema, SyntheticRebaseTokenConfigSchema, CctpTokenConfigSchema, OftTokenConfigSchema, EverclearCollateralTokenConfigSchema, EverclearEthBridgeTokenConfigSchema, DepositAddressTokenConfigSchema, CrossCollateralTokenConfigSchema, UnknownTokenConfigSchema, ]); /** * @remarks * The discriminatedUnion is basically a switch statement for zod schemas * It uses the 'type' key to pick from the array of schemas to validate */ export const HypTokenConfigSchema = z.preprocess((val) => { if (typeof val === 'object' && val !== null && 'type' in val) { const obj = val; if (typeof obj.type === 'string' && !KnownTokenTypes.includes(obj.type)) { return { ...obj, type: TokenType.unknown }; } } return val; }, AllHypTokenConfigSchema); export const HypTokenRouterConfigSchema = z.preprocess(preprocessWarpRouteDeployConfig, HypTokenConfigSchema.and(GasRouterConfigSchema).and(HypTokenRouterVirtualConfigSchema.partial())); export function derivedHookAddress(config) { return typeof config.hook === 'string' ? config.hook : config.hook.address; } export function derivedIsmAddress(config) { return typeof config.interchainSecurityModule === 'string' ? config.interchainSecurityModule : config.interchainSecurityModule.address; } export const HypTokenRouterConfigMailboxOptionalBaseSchema = HypTokenConfigSchema.and(GasRouterConfigSchema.extend({ mailbox: z.string().optional(), })).and(HypTokenRouterVirtualConfigSchema.partial()); export const HypTokenRouterConfigMailboxOptionalSchema = z.preprocess(preprocessWarpRouteDeployConfig, HypTokenRouterConfigMailboxOptionalBaseSchema); function preprocessWarpRouteDeployConfig(value) { const mutatedConfig = value; return populateFeeOwner({ tokenConfig: mutatedConfig, feeConfig: mutatedConfig.tokenFee, }); } function populateFeeOwner(params) { const { tokenConfig, feeConfig, inheritedOwner } = params; if (!feeConfig) return tokenConfig; const resolvedOwner = feeConfig.owner ?? inheritedOwner ?? tokenConfig.owner; feeConfig.owner = resolvedOwner; if (feeConfig.type === TokenFeeType.RoutingFee) { objMap(feeConfig.feeContracts, (_, innerConfig) => { populateFeeOwner({ tokenConfig, feeConfig: innerConfig, inheritedOwner: resolvedOwner, }); }); } if (feeConfig.type === TokenFeeType.CrossCollateralRoutingFee) { objMap(feeConfig.feeContracts, (_, destinationConfig) => { objMap(destinationConfig, (_, innerConfig) => { populateFeeOwner({ tokenConfig, feeConfig: innerConfig, inheritedOwner: resolvedOwner, }); }); }); } return tokenConfig; } export const WarpRouteDeployConfigSchema = z .record(HypTokenRouterConfigMailboxOptionalSchema) .refine((configMap) => { const entries = Object.entries(configMap); return (entries.some(([_, config]) => isCollateralTokenConfig(config) || isCollateralRebaseTokenConfig(config) || isCctpTokenConfig(config) || isXERC20TokenConfig(config) || isNativeTokenConfig(config) || isEverclearTokenBridgeConfig(config) || isDepositAddressTokenConfig(config) || isCrossCollateralTokenConfig(config) || isOftTokenConfig(config)) || entries.every(([_, config]) => isTokenMetadata(config))); }, WarpRouteDeployConfigSchemaErrors.NO_SYNTHETIC_ONLY) // Verify synthetic rebase tokens config .transform((warpRouteDeployConfig, ctx) => { const collateralRebaseEntry = Object.entries(warpRouteDeployConfig).find(([_, config]) => isCollateralRebaseTokenConfig(config)); const syntheticRebaseEntry = Object.entries(warpRouteDeployConfig).find(([_, config]) => isSyntheticRebaseTokenConfig(config)); // Require both collateral rebase and synthetic rebase to be present in the config if (!collateralRebaseEntry && !syntheticRebaseEntry) { // Pass through for other token types return warpRouteDeployConfig; } if (collateralRebaseEntry && isCollateralRebasePairedCorrectly(warpRouteDeployConfig)) { const collateralChainName = collateralRebaseEntry[0]; return objMap(warpRouteDeployConfig, (_, config) => { if (config.type === TokenType.syntheticRebase) config.collateralChainName = collateralChainName; return config; }); } ctx.addIssue({ code: z.ZodIssueCode.custom, message: WarpRouteDeployConfigSchemaErrors.ONLY_SYNTHETIC_REBASE, }); return z.NEVER; // Causes schema validation to throw with above issue }) // Verify that CCIP hooks are paired with CCIP ISMs .transform((warpRouteDeployConfig, ctx) => { const { ccipHookMap, ccipIsmMap } = getCCIPConfigMaps(warpRouteDeployConfig); // Check hooks have corresponding ISMs const hookConfigHasMissingIsms = Object.entries(ccipHookMap).some(([originChain, destinationChains]) => Array.from(destinationChains).some((chain) => { if (!ccipIsmMap[originChain]?.has(chain)) { ctx.addIssue({ code: z.ZodIssueCode.custom, path: [chain, 'interchainSecurityModule', '...'], message: `Required CCIP ISM not found in config for CCIP Hook with origin chain ${originChain} and destination chain ${chain}`, }); return true; } return false; })); // Check ISMs have corresponding hooks const ismConfigHasMissingHooks = Object.entries(ccipIsmMap).some(([originChain, destinationChains]) => Array.from(destinationChains).some((chain) => { if (!ccipHookMap[originChain]?.has(chain)) { ctx.addIssue({ code: z.ZodIssueCode.custom, path: [originChain, 'hook', '...'], message: `Required CCIP Hook not found in config for CCIP ISM with origin chain ${originChain} and destination chain ${chain}`, }); return true; } return false; })); return hookConfigHasMissingIsms || ismConfigHasMissingHooks ? z.NEVER : warpRouteDeployConfig; }) // Verify that xERC20 are only with xERC20s or collateral .transform((warpRouteDeployConfig, ctx) => { const isXERC20Route = Object.values(warpRouteDeployConfig).some(isXERC20TokenConfig); if (!isXERC20Route) { return warpRouteDeployConfig; } const isAllXERC20sOrCollateral = Object.values(warpRouteDeployConfig).every((config) => isXERC20TokenConfig(config) || isCollateralTokenConfig(config)); if (!isAllXERC20sOrCollateral) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: `xERC20 warp routes must only contain xERC20 or collateral token types`, }); return z.NEVER; } return warpRouteDeployConfig; }); const _RequiredMailboxSchema = z.record(z.object({ mailbox: z.string(), })); export const WarpRouteDeployConfigMailboxRequiredSchema = WarpRouteDeployConfigSchema.and(_RequiredMailboxSchema); function isCollateralRebasePairedCorrectly(warpRouteDeployConfig) { // Filter out all the non-collateral rebase configs to check if they are only synthetic rebase tokens const otherConfigs = Object.entries(warpRouteDeployConfig).filter(([_, config]) => !isCollateralRebaseTokenConfig(config)); if (otherConfigs.length === 0) return false; // The other configs MUST be synthetic rebase const allOthersSynthetic = otherConfigs.every(([_, config], _index) => isSyntheticRebaseTokenConfig(config)); return allOthersSynthetic; } function getCCIPConfigMaps(warpRouteDeployConfig) { const ccipHookMap = {}; const ccipIsmMap = {}; Object.entries(warpRouteDeployConfig).forEach(([chainName, config]) => { extractCCIPHookMap(chainName, config.hook, ccipHookMap); extractCCIPIsmMap(chainName, config.interchainSecurityModule, ccipIsmMap); }); return { ccipHookMap, ccipIsmMap }; } function extractCCIPHookMap(currentChain, hookConfig, existsCCIPHookMap) { if (!hookConfig || typeof hookConfig === 'string') { return; } switch (hookConfig.type) { case HookType.AGGREGATION: hookConfig.hooks.forEach((hook) => extractCCIPHookMap(currentChain, hook, existsCCIPHookMap)); break; case HookType.ARB_L2_TO_L1: extractCCIPHookMap(currentChain, hookConfig.childHook, existsCCIPHookMap); break; case HookType.CCIP: if (!existsCCIPHookMap[currentChain]) { existsCCIPHookMap[currentChain] = new Set(); } existsCCIPHookMap[currentChain].add(hookConfig.destinationChain); break; case HookType.FALLBACK_ROUTING: case HookType.ROUTING: Object.entries(hookConfig.domains).forEach(([_, hook]) => { extractCCIPHookMap(currentChain, hook, existsCCIPHookMap); }); break; default: break; } } function extractCCIPIsmMap(currentChain, ismConfig, existsCCIPIsmMap) { if (!ismConfig || typeof ismConfig === 'string') { return; } switch (ismConfig.type) { case IsmType.AGGREGATION: case IsmType.STORAGE_AGGREGATION: ismConfig.modules.forEach((hook) => extractCCIPIsmMap(currentChain, hook, existsCCIPIsmMap)); break; case IsmType.CCIP: if (!existsCCIPIsmMap[ismConfig.originChain]) { existsCCIPIsmMap[ismConfig.originChain] = new Set(); } existsCCIPIsmMap[ismConfig.originChain].add(currentChain); break; case IsmType.FALLBACK_ROUTING: case IsmType.ROUTING: Object.entries(ismConfig.domains).forEach(([_, hook]) => { extractCCIPIsmMap(currentChain, hook, existsCCIPIsmMap); }); break; default: break; } } const MovableTokenSchema = z.discriminatedUnion('type', [ CollateralTokenConfigSchema, CrossCollateralTokenConfigSchema, NativeTokenConfigSchema, ]); export const isMovableCollateralTokenConfig = isCompliant(MovableTokenSchema); //# sourceMappingURL=types.js.map