UNPKG

@hyperlane-xyz/sdk

Version:

The official SDK for the Hyperlane Network

375 lines 14 kB
import { DirectSecp256k1Wallet } from '@cosmjs/proto-signing'; import { Keypair } from '@solana/web3.js'; import { BigNumber, ethers } from 'ethers'; import { bytesToHex } from 'viem'; import { exclude, objMap, randomElement } from '@hyperlane-xyz/utils'; import { testChains } from '../consts/testChains.js'; import { HookType } from '../hook/types.js'; import { IsmType, ModuleType, ismTypeToModuleType, } from '../ism/types.js'; export function randomInt(max, min = 0) { return Math.floor(Math.random() * (max - min)) + min; } export function randomAddress() { return ethers.utils.hexlify(ethers.utils.randomBytes(20)).toLowerCase(); } export function randomSvmAddress() { return Keypair.generate().publicKey.toBase58(); } export function randomStarknetAddress() { return bytesToHex(ethers.utils.randomBytes(32)); } export async function randomCosmosAddress(prefix) { const wallet = await DirectSecp256k1Wallet.fromKey(ethers.utils.randomBytes(32), prefix); const accounts = await wallet.getAccounts(); return accounts[0].address; } export function createRouterConfigMap(owner, coreContracts, igpContracts) { return objMap(coreContracts, (chain, contracts) => { return { owner, mailbox: contracts.mailbox.address, interchainGasPaymaster: igpContracts[chain].interchainGasPaymaster.address, }; }); } const nonZeroAddress = ethers.constants.AddressZero.replace('00', '01'); // dummy config as TestInbox and TestOutbox do not use deployed ISM export function testCoreConfig(chains, owner = nonZeroAddress) { const chainConfig = { owner, defaultIsm: { type: IsmType.TEST_ISM, }, defaultHook: { type: HookType.MERKLE_TREE, }, requiredHook: { type: HookType.PROTOCOL_FEE, maxProtocolFee: ethers.utils.parseUnits('1', 'gwei').toString(), // 1 gwei of native token protocolFee: BigNumber.from(1).toString(), // 1 wei beneficiary: nonZeroAddress, owner, }, }; return Object.fromEntries(chains.map((local) => [local, chainConfig])); } const TEST_ORACLE_CONFIG = { gasPrice: ethers.utils.parseUnits('1', 'gwei').toString(), tokenExchangeRate: ethers.utils.parseUnits('1', 10).toString(), tokenDecimals: 18, }; const TEST_OVERHEAD_COST = 60000; export function testIgpConfig(chains, owner = nonZeroAddress) { return Object.fromEntries(chains.map((local) => { const overhead = {}; const oracleConfig = {}; exclude(local, chains).map((remote) => { overhead[remote] = TEST_OVERHEAD_COST; oracleConfig[remote] = TEST_ORACLE_CONFIG; }); return [ local, { type: HookType.INTERCHAIN_GAS_PAYMASTER, owner, oracleKey: owner, beneficiary: owner, overhead, oracleConfig, }, ]; })); } export const hookTypes = Object.values(HookType); export const hookTypesToFilter = [ HookType.OP_STACK, HookType.ARB_L2_TO_L1, HookType.CUSTOM, HookType.PREDICATE, HookType.CCIP, HookType.CCTP, HookType.UNKNOWN, ]; export const DEFAULT_TOKEN_DECIMALS = 18; function randomHookType() { // OP_STACK filtering is temporary until we have a way to deploy the required contracts // ARB_L2_TO_L1 filtered out until we have a way to deploy the required contracts (arbL2ToL1.hardhat-test.ts has the same test for checking deployment) const filteredHookTypes = hookTypes.filter((type) => !hookTypesToFilter.includes(type)); return filteredHookTypes[Math.floor(Math.random() * filteredHookTypes.length)]; } function randomProtocolFee() { const maxProtocolFee = Math.random() * 100000000000000; const protocolFee = (Math.random() * maxProtocolFee) / 1000; return { maxProtocolFee: Math.floor(maxProtocolFee).toString(), protocolFee: Math.floor(protocolFee).toString(), }; } export function randomHookConfig(depth = 0, maxDepth = 2, providedHookType) { const hookType = providedHookType ?? randomHookType(); if (depth >= maxDepth) { if (hookType === HookType.AGGREGATION || hookType === HookType.ROUTING || hookType === HookType.FALLBACK_ROUTING) { return { type: HookType.MERKLE_TREE }; } } switch (hookType) { case HookType.MERKLE_TREE: case HookType.MAILBOX_DEFAULT: return { type: hookType }; case HookType.AGGREGATION: return { type: hookType, hooks: [ randomHookConfig(depth + 1, maxDepth), randomHookConfig(depth + 1, maxDepth), ], }; case HookType.INTERCHAIN_GAS_PAYMASTER: { const owner = randomAddress(); return { owner, type: hookType, beneficiary: randomAddress(), oracleKey: owner, overhead: Object.fromEntries(testChains.map((c) => [c, Math.floor(Math.random() * 100)])), oracleConfig: Object.fromEntries(testChains.map((c) => [ c, { tokenExchangeRate: randomInt(1234567891234).toString(), gasPrice: randomInt(1234567891234).toString(), tokenDecimals: DEFAULT_TOKEN_DECIMALS, }, ])), }; } case HookType.PROTOCOL_FEE: { const { maxProtocolFee, protocolFee } = randomProtocolFee(); return { owner: randomAddress(), type: hookType, maxProtocolFee, protocolFee, beneficiary: randomAddress(), }; } case HookType.OP_STACK: return { owner: randomAddress(), type: hookType, nativeBridge: randomAddress(), destinationChain: 'testChain', }; case HookType.ARB_L2_TO_L1: return { type: hookType, arbSys: randomAddress(), bridge: randomAddress(), destinationChain: 'testChain', }; case HookType.ROUTING: return { owner: randomAddress(), type: hookType, domains: Object.fromEntries(testChains.map((c) => [c, randomHookConfig(depth + 1, maxDepth)])), }; case HookType.FALLBACK_ROUTING: return { owner: randomAddress(), type: hookType, fallback: randomHookConfig(depth + 1, maxDepth), domains: Object.fromEntries(testChains.map((c) => [c, randomHookConfig(depth + 1, maxDepth)])), }; case HookType.PAUSABLE: return { owner: randomAddress(), type: hookType, paused: false, }; case HookType.AMOUNT_ROUTING: return { type: hookType, threshold: Math.floor(Math.random() * 100), lowerHook: randomHookConfig(depth + 1, maxDepth), upperHook: randomHookConfig(depth + 1, maxDepth), }; case HookType.RATE_LIMITED: return { owner: randomAddress(), type: hookType, maxCapacity: ((1 + Math.floor(Math.random() * 100)) * 86400).toString(), }; default: throw new Error(`Unsupported Hook type: ${hookType}`); } } export const randomMultisigIsmConfig = (m, n, addresses) => { const validators = Array.from({ length: n }, () => addresses ? randomElement(addresses) : randomAddress()).sort(); return { type: IsmType.MERKLE_ROOT_MULTISIG, validators, threshold: m, }; }; export const randomWeightedMultisigIsmConfig = (n, addresses) => { const totalWeight = 1e10; const validators = Array.from({ length: n }, () => ({ signingAddress: addresses ? randomElement(addresses) : randomAddress(), weight: 0, })); let remainingWeight = totalWeight; for (let i = 0; i < n; i++) { if (i === n - 1) { validators[i].weight = remainingWeight; } else { const weight = Math.floor(Math.random() * (remainingWeight + 1)); validators[i].weight = weight; remainingWeight -= weight; } } const thresholdWeight = Math.floor(Math.random() * totalWeight); return { type: IsmType.WEIGHTED_MESSAGE_ID_MULTISIG, validators, thresholdWeight, }; }; const ModuleTypes = [ ModuleType.AGGREGATION, ModuleType.MERKLE_ROOT_MULTISIG, ModuleType.ROUTING, ModuleType.NULL, ]; const NonNestedModuleTypes = [ModuleType.MERKLE_ROOT_MULTISIG, ModuleType.NULL]; function randomModuleType() { return ModuleTypes[randomInt(ModuleTypes.length)]; } function randomNonNestedModuleType() { return NonNestedModuleTypes[randomInt(NonNestedModuleTypes.length)]; } export const randomIsmConfig = (depth = 0, maxDepth = 2, providedIsmType) => { // Use input IsmType, otherwise randomize a config based on depth const moduleType = providedIsmType ? ismTypeToModuleType(providedIsmType) : depth === maxDepth ? randomNonNestedModuleType() : randomModuleType(); switch (moduleType) { case ModuleType.MERKLE_ROOT_MULTISIG: { const n = randomInt(5, 1); return randomMultisigIsmConfig(randomInt(n, 1), n); } case ModuleType.ROUTING: { const config = { type: IsmType.ROUTING, owner: randomAddress(), domains: Object.fromEntries(testChains.map((c) => [c, randomIsmConfig(depth + 1)])), }; return config; } case ModuleType.AGGREGATION: { const n = randomInt(2, 1); const moduleTypes = new Set(); const modules = Array.from({ length: n }, () => { let moduleConfig; let moduleType; // Ensure that we do not add the same module type more than once per level do { moduleConfig = randomIsmConfig(depth + 1, maxDepth); moduleType = moduleConfig.type; } while (moduleTypes.has(moduleType)); moduleTypes.add(moduleType); return moduleConfig; }); const config = { type: IsmType.AGGREGATION, threshold: randomInt(n, 1), modules, }; return config; } case ModuleType.NULL: { if (providedIsmType === IsmType.RATE_LIMITED) { const config = { type: IsmType.RATE_LIMITED, maxCapacity: '86400', recipient: randomAddress(), owner: randomAddress(), }; return config; } const config = { type: IsmType.TRUSTED_RELAYER, relayer: randomAddress(), }; return config; } default: throw new Error(`Unsupported ISM type: ${moduleType}`); } }; /** * Generate a random ISM config suitable for deployment with specific validators/relayer. * Unlike randomIsmConfig, this version allows specifying validator addresses and relayer * for use in hardhat tests where real signatures are needed. */ export const randomDeployableIsmConfig = (maxDepth = 5, validatorAddresses, relayerAddress) => { const DeployableModuleTypes = [ ModuleType.AGGREGATION, ModuleType.MESSAGE_ID_MULTISIG, ModuleType.WEIGHTED_MESSAGE_ID_MULTISIG, ModuleType.ROUTING, ModuleType.NULL, ]; const moduleType = maxDepth === 0 ? ModuleType.MESSAGE_ID_MULTISIG : DeployableModuleTypes[randomInt(DeployableModuleTypes.length)]; if (moduleType === ModuleType.MESSAGE_ID_MULTISIG) { const n = randomInt(validatorAddresses?.length ?? 5, 1); const m = randomInt(n, 1); const validators = Array.from({ length: n }, () => validatorAddresses ? randomElement(validatorAddresses) : randomAddress()).sort(); return { type: IsmType.MESSAGE_ID_MULTISIG, validators, threshold: m, }; } else if (moduleType === ModuleType.WEIGHTED_MESSAGE_ID_MULTISIG) { const n = randomInt(validatorAddresses?.length ?? 5, 1); return randomWeightedMultisigIsmConfig(n, validatorAddresses); } else if (moduleType === ModuleType.ROUTING) { const config = { type: IsmType.ROUTING, owner: randomAddress(), domains: Object.fromEntries(testChains.map((c) => [ c, randomDeployableIsmConfig(maxDepth - 1, validatorAddresses, relayerAddress), ])), }; return config; } else if (moduleType === ModuleType.AGGREGATION) { const n = randomInt(5, 2); const modules = Array.from({ length: n }, () => randomDeployableIsmConfig(maxDepth - 1, validatorAddresses, relayerAddress)); const config = { type: IsmType.AGGREGATION, threshold: randomInt(n, 1), modules, }; return config; } else if (moduleType === ModuleType.NULL) { const config = { type: IsmType.TRUSTED_RELAYER, relayer: relayerAddress ?? randomAddress(), }; return config; } else { throw new Error(`Unsupported ISM type: ${moduleType}`); } }; //# sourceMappingURL=testUtils.js.map