@hyperlane-xyz/sdk
Version:
The official SDK for the Hyperlane Network
375 lines • 14 kB
JavaScript
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