commit
db31f2b63d
@ -1,7 +0,0 @@ |
||||
--- |
||||
'@hyperlane-xyz/helloworld': minor |
||||
'@hyperlane-xyz/infra': minor |
||||
'@hyperlane-xyz/cli': minor |
||||
--- |
||||
|
||||
Upgrade registry to 2.1.1 |
@ -0,0 +1,19 @@ |
||||
# A config to define the core contract deployments |
||||
owner: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266' |
||||
defaultIsm: |
||||
type: 'testIsm' |
||||
threshold: 1 # Number: Signatures required to approve a message |
||||
validators: # Array: List of validator addresses |
||||
- '0xa0ee7a142d267c1f36714e4a8f75612f20a79720' |
||||
defaultHook: |
||||
type: protocolFee |
||||
maxProtocolFee: '1000000000000000000' # in wei (string) |
||||
protocolFee: '200000000000000' # in wei (string) |
||||
beneficiary: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266' |
||||
owner: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266' |
||||
requiredHook: |
||||
type: protocolFee |
||||
maxProtocolFee: '1000000000000000000' # in wei (string) |
||||
protocolFee: '200000000000000' # in wei (string) |
||||
beneficiary: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266' |
||||
owner: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266' |
@ -0,0 +1,155 @@ |
||||
import { stringify as yamlStringify } from 'yaml'; |
||||
import { CommandModule } from 'yargs'; |
||||
|
||||
import { EvmCoreReader } from '@hyperlane-xyz/sdk'; |
||||
|
||||
import { createCoreDeployConfig } from '../config/core.js'; |
||||
import { |
||||
CommandModuleWithContext, |
||||
CommandModuleWithWriteContext, |
||||
} from '../context/types.js'; |
||||
import { runCoreDeploy } from '../deploy/core.js'; |
||||
import { evaluateIfDryRunFailure } from '../deploy/dry-run.js'; |
||||
import { log, logGray, logGreen } from '../logger.js'; |
||||
import { |
||||
indentYamlOrJson, |
||||
readYamlOrJson, |
||||
writeYamlOrJson, |
||||
} from '../utils/files.js'; |
||||
|
||||
import { |
||||
chainCommandOption, |
||||
dryRunCommandOption, |
||||
fromAddressCommandOption, |
||||
outputFileCommandOption, |
||||
} from './options.js'; |
||||
|
||||
/** |
||||
* Parent command |
||||
*/ |
||||
export const coreCommand: CommandModule = { |
||||
command: 'core', |
||||
describe: 'Manage core Hyperlane contracts & configs', |
||||
builder: (yargs) => |
||||
yargs |
||||
.command(deploy) |
||||
.command(init) |
||||
.command(read) |
||||
.version(false) |
||||
.demandCommand(), |
||||
handler: () => log('Command required'), |
||||
}; |
||||
|
||||
/** |
||||
* Generates a command module for deploying Hyperlane contracts, given a command |
||||
* |
||||
* @param commandName - the deploy command key used to look up the deployFunction |
||||
* @returns A command module used to deploy Hyperlane contracts. |
||||
*/ |
||||
export const deploy: CommandModuleWithWriteContext<{ |
||||
chain: string; |
||||
config: string; |
||||
dryRun: string; |
||||
fromAddress: string; |
||||
}> = { |
||||
command: 'deploy', |
||||
describe: 'Deploy Hyperlane contracts', |
||||
builder: { |
||||
chain: chainCommandOption, |
||||
config: outputFileCommandOption( |
||||
'./configs/core-config.yaml', |
||||
false, |
||||
'The path to a JSON or YAML file with a core deployment config.', |
||||
), |
||||
'dry-run': dryRunCommandOption, |
||||
'from-address': fromAddressCommandOption, |
||||
}, |
||||
handler: async ({ context, chain, config: configFilePath, dryRun }) => { |
||||
logGray(`Hyperlane permissionless deployment${dryRun ? ' dry-run' : ''}`); |
||||
logGray(`------------------------------------------------`); |
||||
|
||||
try { |
||||
await runCoreDeploy({ |
||||
context, |
||||
chain, |
||||
config: readYamlOrJson(configFilePath), |
||||
}); |
||||
} catch (error: any) { |
||||
evaluateIfDryRunFailure(error, dryRun); |
||||
throw error; |
||||
} |
||||
process.exit(0); |
||||
}, |
||||
}; |
||||
|
||||
export const init: CommandModuleWithContext<{ |
||||
advanced: boolean; |
||||
config: string; |
||||
}> = { |
||||
command: 'init', |
||||
describe: 'Create a core configuration, including ISMs and hooks.', |
||||
builder: { |
||||
advanced: { |
||||
type: 'boolean', |
||||
describe: 'Create an advanced ISM & hook configuration', |
||||
default: false, |
||||
}, |
||||
config: outputFileCommandOption( |
||||
'./configs/core-config.yaml', |
||||
false, |
||||
'The path to output a Core Config JSON or YAML file.', |
||||
), |
||||
}, |
||||
handler: async ({ context, advanced, config: configFilePath }) => { |
||||
logGray('Hyperlane Core Configure'); |
||||
logGray('------------------------'); |
||||
|
||||
await createCoreDeployConfig({ |
||||
context, |
||||
configFilePath, |
||||
advanced, |
||||
}); |
||||
|
||||
process.exit(0); |
||||
}, |
||||
}; |
||||
|
||||
export const read: CommandModuleWithContext<{ |
||||
chain: string; |
||||
mailbox: string; |
||||
config: string; |
||||
}> = { |
||||
command: 'read', |
||||
describe: 'Reads onchain Core configuration for a given mailbox address', |
||||
builder: { |
||||
chain: { |
||||
...chainCommandOption, |
||||
demandOption: true, |
||||
}, |
||||
mailbox: { |
||||
type: 'string', |
||||
description: 'Mailbox address used to derive the core config', |
||||
demandOption: true, |
||||
}, |
||||
config: outputFileCommandOption( |
||||
'./configs/core-config.yaml', |
||||
false, |
||||
'The path to output a Core Config JSON or YAML file.', |
||||
), |
||||
}, |
||||
handler: async ({ context, chain, mailbox, config: configFilePath }) => { |
||||
logGray('Hyperlane Core Read'); |
||||
logGray('-------------------'); |
||||
|
||||
const evmCoreReader = new EvmCoreReader(context.multiProvider, chain); |
||||
const coreConfig = await evmCoreReader.deriveCoreConfig(mailbox); |
||||
|
||||
writeYamlOrJson(configFilePath, coreConfig, 'yaml'); |
||||
logGreen( |
||||
`✅ Warp route config written successfully to ${configFilePath}:\n`, |
||||
); |
||||
log(indentYamlOrJson(yamlStringify(coreConfig, null, 2), 4)); |
||||
|
||||
process.exit(0); |
||||
}, |
||||
}; |
@ -0,0 +1,2 @@ |
||||
export const ChainTypes = ['mainnet', 'testnet']; |
||||
export type ChainType = (typeof ChainTypes)[number]; |
@ -0,0 +1,290 @@ |
||||
import { ethers } from 'ethers'; |
||||
import { stringify as yamlStringify } from 'yaml'; |
||||
import { CommandModule } from 'yargs'; |
||||
|
||||
import { |
||||
HypXERC20Lockbox__factory, |
||||
HypXERC20__factory, |
||||
IXERC20__factory, |
||||
} from '@hyperlane-xyz/core'; |
||||
import { |
||||
ChainMap, |
||||
EvmERC20WarpRouteReader, |
||||
TokenStandard, |
||||
WarpCoreConfig, |
||||
} from '@hyperlane-xyz/sdk'; |
||||
import { objMap, promiseObjAll } from '@hyperlane-xyz/utils'; |
||||
|
||||
import { |
||||
createWarpRouteDeployConfig, |
||||
readWarpCoreConfig, |
||||
} from '../config/warp.js'; |
||||
import { |
||||
CommandModuleWithContext, |
||||
CommandModuleWithWriteContext, |
||||
} from '../context/types.js'; |
||||
import { evaluateIfDryRunFailure } from '../deploy/dry-run.js'; |
||||
import { runWarpRouteDeploy } from '../deploy/warp.js'; |
||||
import { log, logGray, logGreen, logRed, logTable } from '../logger.js'; |
||||
import { sendTestTransfer } from '../send/transfer.js'; |
||||
import { indentYamlOrJson, writeYamlOrJson } from '../utils/files.js'; |
||||
import { selectRegistryWarpRoute } from '../utils/tokens.js'; |
||||
|
||||
import { |
||||
addressCommandOption, |
||||
chainCommandOption, |
||||
dryRunCommandOption, |
||||
fromAddressCommandOption, |
||||
outputFileCommandOption, |
||||
symbolCommandOption, |
||||
warpCoreConfigCommandOption, |
||||
warpDeploymentConfigCommandOption, |
||||
} from './options.js'; |
||||
import { MessageOptionsArgTypes, messageOptions } from './send.js'; |
||||
|
||||
/** |
||||
* Parent command |
||||
*/ |
||||
export const warpCommand: CommandModule = { |
||||
command: 'warp', |
||||
describe: 'Manage Hyperlane warp routes', |
||||
builder: (yargs) => |
||||
yargs |
||||
.command(deploy) |
||||
.command(init) |
||||
.command(read) |
||||
.command(send) |
||||
.version(false) |
||||
.demandCommand(), |
||||
|
||||
handler: () => log('Command required'), |
||||
}; |
||||
|
||||
export const deploy: CommandModuleWithWriteContext<{ |
||||
config: string; |
||||
'dry-run': string; |
||||
'from-address': string; |
||||
}> = { |
||||
command: 'deploy', |
||||
describe: 'Deploy Warp Route contracts', |
||||
builder: { |
||||
config: warpDeploymentConfigCommandOption, |
||||
'dry-run': dryRunCommandOption, |
||||
'from-address': fromAddressCommandOption, |
||||
}, |
||||
handler: async ({ context, config, dryRun }) => { |
||||
logGray(`Hyperlane warp route deployment${dryRun ? ' dry-run' : ''}`); |
||||
logGray('------------------------------------------------'); |
||||
|
||||
try { |
||||
await runWarpRouteDeploy({ |
||||
context, |
||||
warpRouteDeploymentConfigPath: config, |
||||
}); |
||||
} catch (error: any) { |
||||
evaluateIfDryRunFailure(error, dryRun); |
||||
throw error; |
||||
} |
||||
process.exit(0); |
||||
}, |
||||
}; |
||||
|
||||
export const init: CommandModuleWithContext<{ |
||||
advanced: boolean; |
||||
out: string; |
||||
}> = { |
||||
command: 'init', |
||||
describe: 'Create a warp route configuration.', |
||||
builder: { |
||||
advanced: { |
||||
type: 'boolean', |
||||
describe: 'Create an advanced ISM', |
||||
default: false, |
||||
}, |
||||
out: outputFileCommandOption('./configs/warp-route-deployment.yaml'), |
||||
}, |
||||
handler: async ({ context, advanced, out }) => { |
||||
logGray('Hyperlane Warp Configure'); |
||||
logGray('------------------------'); |
||||
|
||||
await createWarpRouteDeployConfig({ |
||||
context, |
||||
outPath: out, |
||||
advanced, |
||||
}); |
||||
process.exit(0); |
||||
}, |
||||
}; |
||||
|
||||
export const read: CommandModuleWithContext<{ |
||||
chain?: string; |
||||
address?: string; |
||||
out?: string; |
||||
symbol?: string; |
||||
}> = { |
||||
command: 'read', |
||||
describe: 'Derive the warp route config from onchain artifacts', |
||||
builder: { |
||||
symbol: { |
||||
...symbolCommandOption, |
||||
demandOption: false, |
||||
}, |
||||
chain: { |
||||
...chainCommandOption, |
||||
demandOption: false, |
||||
}, |
||||
address: addressCommandOption( |
||||
'Address of the router contract to read.', |
||||
false, |
||||
), |
||||
out: outputFileCommandOption(), |
||||
}, |
||||
handler: async ({ context, chain, address, out, symbol }) => { |
||||
logGray('Hyperlane Warp Reader'); |
||||
logGray('---------------------'); |
||||
|
||||
const { multiProvider } = context; |
||||
|
||||
let addresses: ChainMap<string>; |
||||
if (symbol) { |
||||
const warpCoreConfig = await selectRegistryWarpRoute( |
||||
context.registry, |
||||
symbol, |
||||
); |
||||
|
||||
// TODO: merge with XERC20TokenAdapter and WarpRouteReader
|
||||
const xerc20Limits = await Promise.all( |
||||
warpCoreConfig.tokens |
||||
.filter( |
||||
(t) => |
||||
t.standard === TokenStandard.EvmHypXERC20 || |
||||
t.standard === TokenStandard.EvmHypXERC20Lockbox, |
||||
) |
||||
.map(async (t) => { |
||||
const provider = multiProvider.getProvider(t.chainName); |
||||
const router = t.addressOrDenom!; |
||||
const xerc20Address = |
||||
t.standard === TokenStandard.EvmHypXERC20Lockbox |
||||
? await HypXERC20Lockbox__factory.connect( |
||||
router, |
||||
provider, |
||||
).xERC20() |
||||
: await HypXERC20__factory.connect( |
||||
router, |
||||
provider, |
||||
).wrappedToken(); |
||||
|
||||
const xerc20 = IXERC20__factory.connect(xerc20Address, provider); |
||||
const mint = await xerc20.mintingCurrentLimitOf(router); |
||||
const burn = await xerc20.burningCurrentLimitOf(router); |
||||
|
||||
const formattedLimits = objMap({ mint, burn }, (_, v) => |
||||
ethers.utils.formatUnits(v, t.decimals), |
||||
); |
||||
|
||||
return [t.chainName, formattedLimits]; |
||||
}), |
||||
); |
||||
if (xerc20Limits.length > 0) { |
||||
logGray('xERC20 Limits:'); |
||||
logTable(Object.fromEntries(xerc20Limits)); |
||||
} |
||||
|
||||
addresses = Object.fromEntries( |
||||
warpCoreConfig.tokens.map((t) => [t.chainName, t.addressOrDenom!]), |
||||
); |
||||
} else if (chain && address) { |
||||
addresses = { |
||||
[chain]: address, |
||||
}; |
||||
} else { |
||||
logGreen(`Please specify either a symbol or chain and address`); |
||||
process.exit(0); |
||||
} |
||||
|
||||
const config = await promiseObjAll( |
||||
objMap(addresses, async (chain, address) => |
||||
new EvmERC20WarpRouteReader(multiProvider, chain).deriveWarpRouteConfig( |
||||
address, |
||||
), |
||||
), |
||||
); |
||||
|
||||
if (out) { |
||||
writeYamlOrJson(out, config, 'yaml'); |
||||
logGreen(`✅ Warp route config written successfully to ${out}:\n`); |
||||
} else { |
||||
logGreen(`✅ Warp route config read successfully:\n`); |
||||
} |
||||
log(indentYamlOrJson(yamlStringify(config, null, 2), 4)); |
||||
process.exit(0); |
||||
}, |
||||
}; |
||||
|
||||
const send: CommandModuleWithWriteContext< |
||||
MessageOptionsArgTypes & { |
||||
warp?: string; |
||||
symbol?: string; |
||||
router?: string; |
||||
amount: string; |
||||
recipient?: string; |
||||
} |
||||
> = { |
||||
command: 'send', |
||||
describe: 'Send a test token transfer on a warp route', |
||||
builder: { |
||||
...messageOptions, |
||||
symbol: { |
||||
...symbolCommandOption, |
||||
demandOption: false, |
||||
}, |
||||
warp: { |
||||
...warpCoreConfigCommandOption, |
||||
demandOption: false, |
||||
}, |
||||
amount: { |
||||
type: 'string', |
||||
description: 'Amount to send (in smallest unit)', |
||||
default: 1, |
||||
}, |
||||
recipient: { |
||||
type: 'string', |
||||
description: 'Token recipient address (defaults to sender)', |
||||
}, |
||||
}, |
||||
handler: async ({ |
||||
context, |
||||
origin, |
||||
destination, |
||||
timeout, |
||||
quick, |
||||
relay, |
||||
symbol, |
||||
warp, |
||||
amount, |
||||
recipient, |
||||
}) => { |
||||
let warpCoreConfig: WarpCoreConfig; |
||||
if (symbol) { |
||||
warpCoreConfig = await selectRegistryWarpRoute(context.registry, symbol); |
||||
} else if (warp) { |
||||
warpCoreConfig = readWarpCoreConfig(warp); |
||||
} else { |
||||
logRed(`Please specify either a symbol or warp config`); |
||||
process.exit(0); |
||||
} |
||||
|
||||
await sendTestTransfer({ |
||||
context, |
||||
warpCoreConfig, |
||||
origin, |
||||
destination, |
||||
amount, |
||||
recipient, |
||||
timeoutSec: timeout, |
||||
skipWaitForDelivery: quick, |
||||
selfRelay: relay, |
||||
}); |
||||
process.exit(0); |
||||
}, |
||||
}; |
@ -0,0 +1,75 @@ |
||||
import { fromError } from 'zod-validation-error'; |
||||
|
||||
import { |
||||
AgentConfigSchema, |
||||
ChainMap, |
||||
HyperlaneCore, |
||||
HyperlaneDeploymentArtifacts, |
||||
buildAgentConfig, |
||||
} from '@hyperlane-xyz/sdk'; |
||||
import { objMap, promiseObjAll } from '@hyperlane-xyz/utils'; |
||||
|
||||
import { CommandContext } from '../context/types.js'; |
||||
import { errorRed, logBlue, logGreen, logRed } from '../logger.js'; |
||||
import { writeYamlOrJson } from '../utils/files.js'; |
||||
|
||||
export async function createAgentConfig({ |
||||
context, |
||||
chains, |
||||
out, |
||||
}: { |
||||
context: CommandContext; |
||||
chains: string[]; |
||||
out: string; |
||||
}) { |
||||
logBlue('\nCreating agent config...'); |
||||
|
||||
const { registry, multiProvider, chainMetadata } = context; |
||||
const addresses = await registry.getAddresses(); |
||||
|
||||
const core = HyperlaneCore.fromAddressesMap(addresses, multiProvider); |
||||
|
||||
const startBlocks = await promiseObjAll( |
||||
objMap(addresses, async (chain, _) => { |
||||
// If the index.from is specified in the chain metadata, use that.
|
||||
const indexFrom = chainMetadata[chain].index?.from; |
||||
if (indexFrom !== undefined) { |
||||
return indexFrom; |
||||
} |
||||
|
||||
const mailbox = core.getContracts(chain).mailbox; |
||||
try { |
||||
const deployedBlock = await mailbox.deployedBlock(); |
||||
return deployedBlock.toNumber(); |
||||
} catch (err) { |
||||
logRed( |
||||
`Failed to get deployed block to set an index for ${chain}, this is potentially an issue with rpc provider or a misconfiguration`, |
||||
); |
||||
process.exit(1); |
||||
} |
||||
}), |
||||
); |
||||
|
||||
// @TODO: consider adding additional config used to pass in gas prices for Cosmos chains
|
||||
const agentConfig = buildAgentConfig( |
||||
chains, |
||||
multiProvider, |
||||
addresses as ChainMap<HyperlaneDeploymentArtifacts>, |
||||
startBlocks, |
||||
); |
||||
|
||||
try { |
||||
AgentConfigSchema.parse(agentConfig); |
||||
} catch (e) { |
||||
errorRed( |
||||
`Agent config is invalid, this is possibly due to required contracts not being deployed. See details below:\n${fromError( |
||||
e, |
||||
).toString()}`,
|
||||
); |
||||
process.exit(1); |
||||
} |
||||
|
||||
logBlue(`Agent config is valid, writing to file ${out}`); |
||||
writeYamlOrJson(out, agentConfig, 'json'); |
||||
logGreen(`✅ Agent config successfully written to ${out}`); |
||||
} |
@ -0,0 +1,71 @@ |
||||
import { stringify as yamlStringify } from 'yaml'; |
||||
|
||||
import { CoreConfigSchema, HookConfig, IsmConfig } from '@hyperlane-xyz/sdk'; |
||||
|
||||
import { CommandContext } from '../context/types.js'; |
||||
import { errorRed, log, logBlue, logGreen } from '../logger.js'; |
||||
import { indentYamlOrJson, writeYamlOrJson } from '../utils/files.js'; |
||||
import { detectAndConfirmOrPrompt } from '../utils/input.js'; |
||||
|
||||
import { |
||||
createHookConfig, |
||||
createMerkleTreeConfig, |
||||
createProtocolFeeConfig, |
||||
} from './hooks.js'; |
||||
import { createAdvancedIsmConfig, createTrustedRelayerConfig } from './ism.js'; |
||||
|
||||
export async function createCoreDeployConfig({ |
||||
context, |
||||
configFilePath, |
||||
advanced = false, |
||||
}: { |
||||
context: CommandContext; |
||||
configFilePath: string; |
||||
advanced: boolean; |
||||
}) { |
||||
logBlue('Creating a new core deployment config...'); |
||||
|
||||
const owner = await detectAndConfirmOrPrompt( |
||||
async () => context.signer?.getAddress(), |
||||
'Enter the desired', |
||||
'owner address', |
||||
'signer', |
||||
); |
||||
|
||||
const defaultIsm: IsmConfig = advanced |
||||
? await createAdvancedIsmConfig(context) |
||||
: await createTrustedRelayerConfig(context, advanced); |
||||
|
||||
let defaultHook: HookConfig, requiredHook: HookConfig; |
||||
if (advanced) { |
||||
defaultHook = await createHookConfig({ |
||||
context, |
||||
selectMessage: 'Select default hook type', |
||||
advanced, |
||||
}); |
||||
requiredHook = await createHookConfig({ |
||||
context, |
||||
selectMessage: 'Select required hook type', |
||||
advanced, |
||||
}); |
||||
} else { |
||||
defaultHook = await createMerkleTreeConfig(); |
||||
requiredHook = await createProtocolFeeConfig(context, advanced); |
||||
} |
||||
|
||||
try { |
||||
const coreConfig = CoreConfigSchema.parse({ |
||||
owner, |
||||
defaultIsm, |
||||
defaultHook, |
||||
requiredHook, |
||||
}); |
||||
logBlue(`Core config is valid, writing to file ${configFilePath}:\n`); |
||||
log(indentYamlOrJson(yamlStringify(coreConfig, null, 2), 4)); |
||||
writeYamlOrJson(configFilePath, coreConfig, 'yaml'); |
||||
logGreen('✅ Successfully created new core deployment config.'); |
||||
} catch (e) { |
||||
errorRed(`Core deployment config is invalid.`); |
||||
throw e; |
||||
} |
||||
} |
@ -0,0 +1,18 @@ |
||||
import { HookConfig, HookType, IsmConfig, IsmType } from '@hyperlane-xyz/sdk'; |
||||
|
||||
import { logGray } from '../logger.js'; |
||||
|
||||
export function callWithConfigCreationLogs<T extends IsmConfig | HookConfig>( |
||||
fn: (...args: any[]) => Promise<T>, |
||||
type: IsmType | HookType, |
||||
) { |
||||
return async (...args: any[]): Promise<T> => { |
||||
logGray(`Creating ${type}...`); |
||||
try { |
||||
const result = await fn(...args); |
||||
return result; |
||||
} finally { |
||||
logGray(`Created ${type}!`); |
||||
} |
||||
}; |
||||
} |
@ -1,461 +1,90 @@ |
||||
import { confirm } from '@inquirer/prompts'; |
||||
import { ethers } from 'ethers'; |
||||
import { stringify as yamlStringify } from 'yaml'; |
||||
|
||||
import { ChainAddresses, IRegistry } from '@hyperlane-xyz/registry'; |
||||
import { |
||||
ChainMap, |
||||
ChainName, |
||||
CoreConfig, |
||||
GasOracleContractType, |
||||
HooksConfig, |
||||
HyperlaneAddressesMap, |
||||
HyperlaneContractsMap, |
||||
HyperlaneCore, |
||||
HyperlaneCoreDeployer, |
||||
HyperlaneIsmFactory, |
||||
HyperlaneProxyFactoryDeployer, |
||||
IgpConfig, |
||||
IsmConfig, |
||||
IsmType, |
||||
MultisigConfig, |
||||
RoutingIsmConfig, |
||||
buildAgentConfig, |
||||
buildAggregationIsmConfigs, |
||||
defaultMultisigConfigs, |
||||
multisigIsmVerificationCost, |
||||
serializeContractsMap, |
||||
} from '@hyperlane-xyz/sdk'; |
||||
import { Address, objFilter, objMap, objMerge } from '@hyperlane-xyz/utils'; |
||||
import { ChainName, CoreConfig, EvmCoreModule } from '@hyperlane-xyz/sdk'; |
||||
|
||||
import { presetHookConfigs, readHooksConfigMap } from '../config/hooks.js'; |
||||
import { readIsmConfig } from '../config/ism.js'; |
||||
import { readMultisigConfig } from '../config/multisig.js'; |
||||
import { MINIMUM_CORE_DEPLOY_GAS } from '../consts.js'; |
||||
import { WriteCommandContext } from '../context/types.js'; |
||||
import { |
||||
log, |
||||
logBlue, |
||||
logBoldUnderlinedRed, |
||||
logGray, |
||||
logGreen, |
||||
logRed, |
||||
} from '../logger.js'; |
||||
import { runMultiChainSelectionStep } from '../utils/chains.js'; |
||||
import { runFileSelectionStep, writeJson } from '../utils/files.js'; |
||||
import { log, logBlue, logGreen } from '../logger.js'; |
||||
import { runSingleChainSelectionStep } from '../utils/chains.js'; |
||||
import { indentYamlOrJson } from '../utils/files.js'; |
||||
|
||||
import { |
||||
completeDeploy, |
||||
isISMConfig, |
||||
isZODISMConfig, |
||||
prepareDeploy, |
||||
runDeployPlanStep, |
||||
runPreflightChecksForChains, |
||||
} from './utils.js'; |
||||
|
||||
const CONTRACT_CACHE_EXCLUSIONS = ['interchainGasPaymaster']; |
||||
|
||||
interface DeployParams { |
||||
context: WriteCommandContext; |
||||
chain: ChainName; |
||||
config: CoreConfig; |
||||
} |
||||
/** |
||||
* Executes the core deploy command. |
||||
*/ |
||||
export async function runCoreDeploy({ |
||||
context, |
||||
chains, |
||||
ismConfigPath, |
||||
hookConfigPath, |
||||
agentOutPath, |
||||
chain, |
||||
config, |
||||
}: { |
||||
context: WriteCommandContext; |
||||
chains?: ChainName[]; |
||||
ismConfigPath?: string; |
||||
hookConfigPath?: string; |
||||
agentOutPath: string; |
||||
chain: ChainName; |
||||
config: CoreConfig; |
||||
}) { |
||||
const { chainMetadata, signer, dryRunChain, skipConfirmation } = context; |
||||
|
||||
if (dryRunChain) chains = [dryRunChain]; |
||||
else if (!chains?.length) { |
||||
if (skipConfirmation) throw new Error('No chains provided'); |
||||
chains = await runMultiChainSelectionStep( |
||||
const { |
||||
signer, |
||||
isDryRun, |
||||
chainMetadata, |
||||
dryRunChain, |
||||
registry, |
||||
skipConfirmation, |
||||
} = context; |
||||
|
||||
// Select a dry-run chain if it's not supplied
|
||||
if (dryRunChain) { |
||||
chain = dryRunChain; |
||||
} else if (!chain) { |
||||
if (skipConfirmation) throw new Error('No chain provided'); |
||||
chain = await runSingleChainSelectionStep( |
||||
chainMetadata, |
||||
'Select chains to connect:', |
||||
true, |
||||
'Select chain to connect:', |
||||
); |
||||
} |
||||
|
||||
const result = await runIsmStep(chains, skipConfirmation, ismConfigPath); |
||||
// we can either specify the full ISM config or just the multisig config
|
||||
const isIsmConfig = isISMConfig(result); |
||||
const ismConfigs = isIsmConfig ? (result as ChainMap<IsmConfig>) : undefined; |
||||
const multisigConfigs = isIsmConfig |
||||
? defaultMultisigConfigs |
||||
: (result as ChainMap<MultisigConfig>); |
||||
const hooksConfig = await runHookStep(chains, hookConfigPath); |
||||
|
||||
const deploymentParams: DeployParams = { |
||||
context, |
||||
chains, |
||||
ismConfigs, |
||||
multisigConfigs, |
||||
hooksConfig, |
||||
agentOutPath, |
||||
chain, |
||||
config, |
||||
}; |
||||
|
||||
await runDeployPlanStep(deploymentParams); |
||||
await runPreflightChecksForChains({ |
||||
...deploymentParams, |
||||
chains: [chain], |
||||
minGas: MINIMUM_CORE_DEPLOY_GAS, |
||||
}); |
||||
|
||||
const userAddress = await signer.getAddress(); |
||||
|
||||
const initialBalances = await prepareDeploy(context, userAddress, chains); |
||||
|
||||
await executeDeploy(deploymentParams); |
||||
const initialBalances = await prepareDeploy(context, userAddress, [chain]); |
||||
|
||||
await completeDeploy(context, 'core', initialBalances, userAddress, chains); |
||||
} |
||||
|
||||
async function runIsmStep( |
||||
selectedChains: ChainName[], |
||||
skipConfirmation: boolean, |
||||
ismConfigPath?: string, |
||||
) { |
||||
if (!ismConfigPath) { |
||||
logBlue( |
||||
'\n', |
||||
'Hyperlane instances requires an Interchain Security Module (ISM).', |
||||
); |
||||
logGray( |
||||
'Example config: https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/main/cli/typescript/cli/examples/ism.yaml', |
||||
); |
||||
if (skipConfirmation) throw new Error('ISM config required'); |
||||
ismConfigPath = await runFileSelectionStep( |
||||
'./configs', |
||||
'ISM config', |
||||
'ism', |
||||
); |
||||
} |
||||
|
||||
const isAdvancedIsm = isZODISMConfig(ismConfigPath); |
||||
// separate flow for 'ism' and 'ism-advanced' options
|
||||
if (isAdvancedIsm) { |
||||
logBoldUnderlinedRed( |
||||
'WARNING: YOU ARE DEPLOYING WITH AN ADVANCED ISM CONFIG', |
||||
); |
||||
logRed( |
||||
'Advanced ISM configs require knowledge of different ISM types and how they work together topologically. If possible, use the basic ISM configs are recommended.', |
||||
); |
||||
const ismConfig = readIsmConfig(ismConfigPath); |
||||
const requiredIsms = objFilter( |
||||
ismConfig, |
||||
(chain, config): config is IsmConfig => selectedChains.includes(chain), |
||||
); |
||||
// selected chains - (user configs + default configs) = missing config
|
||||
const missingConfigs = selectedChains.filter( |
||||
(c) => !Object.keys(ismConfig).includes(c), |
||||
); |
||||
if (missingConfigs.length > 0) { |
||||
throw new Error( |
||||
`Missing advanced ISM config for one or more chains: ${missingConfigs.join( |
||||
', ', |
||||
)}`,
|
||||
); |
||||
} |
||||
|
||||
log(`Found configs for chains: ${selectedChains.join(', ')}`); |
||||
return requiredIsms as ChainMap<IsmConfig>; |
||||
} else { |
||||
const multisigConfigs = { |
||||
...defaultMultisigConfigs, |
||||
...readMultisigConfig(ismConfigPath), |
||||
} as ChainMap<MultisigConfig>; |
||||
const requiredMultisigs = objFilter( |
||||
multisigConfigs, |
||||
(chain, config): config is MultisigConfig => |
||||
selectedChains.includes(chain), |
||||
); |
||||
// selected chains - (user configs + default configs) = missing config
|
||||
const missingConfigs = selectedChains.filter( |
||||
(c) => !Object.keys(requiredMultisigs).includes(c), |
||||
); |
||||
if (missingConfigs.length > 0) { |
||||
throw new Error( |
||||
`Missing ISM config for one or more chains: ${missingConfigs.join( |
||||
', ', |
||||
)}`,
|
||||
); |
||||
} |
||||
|
||||
log(`Found configs for chains: ${selectedChains.join(', ')}`); |
||||
return requiredMultisigs as ChainMap<MultisigConfig>; |
||||
} |
||||
} |
||||
|
||||
async function runHookStep( |
||||
_selectedChains: ChainName[], |
||||
hookConfigPath?: string, |
||||
) { |
||||
if (!hookConfigPath) return {}; |
||||
return readHooksConfigMap(hookConfigPath); |
||||
} |
||||
|
||||
interface DeployParams { |
||||
context: WriteCommandContext; |
||||
chains: ChainName[]; |
||||
ismConfigs?: ChainMap<IsmConfig>; |
||||
multisigConfigs?: ChainMap<MultisigConfig>; |
||||
hooksConfig?: ChainMap<HooksConfig>; |
||||
agentOutPath: string; |
||||
} |
||||
|
||||
async function runDeployPlanStep({ context, chains }: DeployParams) { |
||||
const { signer, skipConfirmation } = context; |
||||
const address = await signer.getAddress(); |
||||
|
||||
logBlue('\nDeployment plan'); |
||||
logGray('==============='); |
||||
log(`Transaction signer and owner of new contracts will be ${address}`); |
||||
log(`Deploying to ${chains.join(', ')}`); |
||||
log( |
||||
`There are several contracts required for each chain but contracts in your provided registries will be skipped`, |
||||
); |
||||
|
||||
if (skipConfirmation) return; |
||||
const isConfirmed = await confirm({ |
||||
message: 'Is this deployment plan correct?', |
||||
}); |
||||
if (!isConfirmed) throw new Error('Deployment cancelled'); |
||||
} |
||||
|
||||
async function executeDeploy({ |
||||
context, |
||||
chains, |
||||
ismConfigs = {}, |
||||
multisigConfigs = {}, |
||||
hooksConfig = {}, |
||||
agentOutPath, |
||||
}: DeployParams) { |
||||
logBlue('All systems ready, captain! Beginning deployment...'); |
||||
const { signer, multiProvider, registry } = context; |
||||
|
||||
let chainAddresses = await registry.getAddresses(); |
||||
chainAddresses = filterAddressesToCache(chainAddresses); |
||||
|
||||
const owner = await signer.getAddress(); |
||||
let artifacts: HyperlaneAddressesMap<any> = {}; |
||||
|
||||
// 1. Deploy ISM factories to all deployable chains that don't have them.
|
||||
logBlue('Deploying ISM factory contracts'); |
||||
const ismFactoryDeployer = new HyperlaneProxyFactoryDeployer(multiProvider); |
||||
ismFactoryDeployer.cacheAddressesMap(chainAddresses); |
||||
|
||||
const ismFactoryConfig = chains.reduce((chainMap, curr) => { |
||||
chainMap[curr] = {}; |
||||
return chainMap; |
||||
}, {} as ChainMap<{}>); |
||||
const ismFactoryContracts = await ismFactoryDeployer.deploy(ismFactoryConfig); |
||||
|
||||
artifacts = await updateChainAddresses( |
||||
registry, |
||||
ismFactoryContracts, |
||||
artifacts, |
||||
context.isDryRun, |
||||
); |
||||
|
||||
logGreen('ISM factory contracts deployed'); |
||||
|
||||
// Build an IsmFactory that covers all chains so that we can
|
||||
// use it to deploy ISMs to remote chains.
|
||||
const ismFactory = HyperlaneIsmFactory.fromAddressesMap( |
||||
chainAddresses, |
||||
multiProvider, |
||||
); |
||||
// 3. Construct ISM configs for all deployable chains
|
||||
const defaultIsms: ChainMap<IsmConfig> = {}; |
||||
for (const ismOrigin of chains) { |
||||
defaultIsms[ismOrigin] = |
||||
ismConfigs[ismOrigin] ?? |
||||
buildIsmConfig(owner, ismOrigin, chains, multisigConfigs); |
||||
} |
||||
|
||||
// 4. Deploy core contracts to chains
|
||||
logBlue(`Deploying core contracts to ${chains.join(', ')}`); |
||||
const coreDeployer = new HyperlaneCoreDeployer(multiProvider, ismFactory); |
||||
coreDeployer.cacheAddressesMap(chainAddresses as any); |
||||
const coreConfigs = buildCoreConfigMap( |
||||
owner, |
||||
chains, |
||||
defaultIsms, |
||||
hooksConfig, |
||||
); |
||||
const coreContracts = await coreDeployer.deploy(coreConfigs); |
||||
|
||||
// 4.5 recover the toplevel ISM address
|
||||
const isms: HyperlaneAddressesMap<any> = {}; |
||||
for (const chain of chains) { |
||||
isms[chain] = { |
||||
interchainSecurityModule: |
||||
coreDeployer.cachedAddresses[chain].interchainSecurityModule, |
||||
}; |
||||
} |
||||
artifacts = objMerge(artifacts, isms); |
||||
artifacts = await updateChainAddresses( |
||||
registry, |
||||
coreContracts, |
||||
artifacts, |
||||
context.isDryRun, |
||||
); |
||||
logGreen('✅ Core contracts deployed'); |
||||
log(JSON.stringify(artifacts, null, 2)); |
||||
|
||||
await writeAgentConfig(context, artifacts, chains, agentOutPath); |
||||
|
||||
logBlue('Deployment is complete!'); |
||||
} |
||||
|
||||
function filterAddressesToCache(addressesMap: ChainMap<ChainAddresses>) { |
||||
// Filter out the certain addresses that must always be
|
||||
// deployed when deploying to a PI chain.
|
||||
// See https://github.com/hyperlane-xyz/hyperlane-monorepo/pull/2983
|
||||
// And https://github.com/hyperlane-xyz/hyperlane-monorepo/pull/3183
|
||||
return objMap(addressesMap, (_chain, addresses) => |
||||
objFilter( |
||||
addresses, |
||||
(contract, _address): _address is string => |
||||
!CONTRACT_CACHE_EXCLUSIONS.includes(contract), |
||||
), |
||||
); |
||||
} |
||||
|
||||
function buildIsmConfig( |
||||
owner: Address, |
||||
local: ChainName, |
||||
chains: ChainName[], |
||||
multisigIsmConfigs: ChainMap<MultisigConfig>, |
||||
): RoutingIsmConfig { |
||||
const aggregationIsmConfigs = buildAggregationIsmConfigs( |
||||
local, |
||||
chains, |
||||
multisigIsmConfigs, |
||||
); |
||||
return { |
||||
owner, |
||||
type: IsmType.ROUTING, |
||||
domains: aggregationIsmConfigs, |
||||
}; |
||||
} |
||||
|
||||
function buildCoreConfigMap( |
||||
owner: Address, |
||||
chains: ChainName[], |
||||
defaultIsms: ChainMap<IsmConfig>, |
||||
hooksConfig: ChainMap<HooksConfig>, |
||||
): ChainMap<CoreConfig> { |
||||
return chains.reduce<ChainMap<CoreConfig>>((config, chain) => { |
||||
const hooks = hooksConfig[chain] ?? presetHookConfigs(owner); |
||||
config[chain] = { |
||||
owner, |
||||
defaultIsm: defaultIsms[chain], |
||||
defaultHook: hooks.default, |
||||
requiredHook: hooks.required, |
||||
}; |
||||
return config; |
||||
}, {}); |
||||
} |
||||
|
||||
export function buildIgpConfigMap( |
||||
owner: Address, |
||||
chains: ChainName[], |
||||
multisigConfigs: ChainMap<MultisigConfig>, |
||||
): ChainMap<IgpConfig> { |
||||
const configMap: ChainMap<IgpConfig> = {}; |
||||
for (const chain of chains) { |
||||
const overhead: ChainMap<number> = {}; |
||||
const gasOracleType: ChainMap<GasOracleContractType> = {}; |
||||
for (const remote of chains) { |
||||
if (chain === remote) continue; |
||||
// TODO: accurate estimate of gas from ChainMap<ISMConfig>
|
||||
const threshold = multisigConfigs[remote] |
||||
? multisigConfigs[remote].threshold |
||||
: 2; |
||||
const validatorsLength = multisigConfigs[remote] |
||||
? multisigConfigs[remote].validators.length |
||||
: 3; |
||||
overhead[remote] = multisigIsmVerificationCost( |
||||
threshold, |
||||
validatorsLength, |
||||
); |
||||
gasOracleType[remote] = GasOracleContractType.StorageGasOracle; |
||||
} |
||||
configMap[chain] = { |
||||
owner, |
||||
beneficiary: owner, |
||||
gasOracleType, |
||||
overhead, |
||||
oracleKey: owner, |
||||
}; |
||||
} |
||||
return configMap; |
||||
} |
||||
|
||||
async function updateChainAddresses( |
||||
registry: IRegistry, |
||||
newContracts: HyperlaneContractsMap<any>, |
||||
otherAddresses: HyperlaneAddressesMap<any>, |
||||
isDryRun?: boolean, |
||||
) { |
||||
let newAddresses = serializeContractsMap(newContracts); |
||||
// The HyperlaneCoreDeployer is returning a nested object with ISM addresses
|
||||
// from other chains, which don't need to be in the artifacts atm.
|
||||
newAddresses = objMap(newAddresses, (_, newChainAddresses) => { |
||||
// For each chain in the addresses chainmap, filter the values to those that are just strings
|
||||
return objFilter( |
||||
newChainAddresses, |
||||
(_, value): value is string => typeof value === 'string', |
||||
); |
||||
const evmCoreModule = await EvmCoreModule.create({ |
||||
chain, |
||||
config, |
||||
multiProvider: context.multiProvider, |
||||
}); |
||||
const mergedAddresses = objMerge(otherAddresses, newAddresses); |
||||
|
||||
if (isDryRun) return mergedAddresses; |
||||
await completeDeploy(context, 'core', initialBalances, userAddress, [chain]); |
||||
const deployedAddresses = evmCoreModule.serialize(); |
||||
|
||||
for (const chainName of Object.keys(newContracts)) { |
||||
if (!isDryRun) { |
||||
await registry.updateChain({ |
||||
chainName, |
||||
addresses: mergedAddresses[chainName], |
||||
chainName: chain, |
||||
addresses: deployedAddresses, |
||||
}); |
||||
} |
||||
return mergedAddresses; |
||||
} |
||||
|
||||
async function writeAgentConfig( |
||||
context: WriteCommandContext, |
||||
artifacts: HyperlaneAddressesMap<any>, |
||||
chains: ChainName[], |
||||
outPath: string, |
||||
) { |
||||
if (context.isDryRun) return; |
||||
log('Writing agent configs'); |
||||
const { multiProvider, registry } = context; |
||||
const startBlocks: ChainMap<number> = {}; |
||||
const core = HyperlaneCore.fromAddressesMap(artifacts, multiProvider); |
||||
|
||||
for (const chain of chains) { |
||||
const mailbox = core.getContracts(chain).mailbox; |
||||
startBlocks[chain] = (await mailbox.deployedBlock()).toNumber(); |
||||
} |
||||
|
||||
const chainAddresses = await registry.getAddresses(); |
||||
for (const chain of chains) { |
||||
if (!chainAddresses[chain].interchainGasPaymaster) { |
||||
chainAddresses[chain].interchainGasPaymaster = |
||||
ethers.constants.AddressZero; |
||||
} |
||||
} |
||||
const agentConfig = buildAgentConfig( |
||||
chains, // Use only the chains that were deployed to
|
||||
multiProvider, |
||||
chainAddresses as any, |
||||
startBlocks, |
||||
); |
||||
writeJson(outPath, agentConfig); |
||||
logGreen('Agent configs written'); |
||||
logGreen('✅ Core contract deployments complete:\n'); |
||||
log(indentYamlOrJson(yamlStringify(deployedAddresses, null, 2), 4)); |
||||
} |
||||
|
@ -0,0 +1,17 @@ |
||||
// Functions used to manipulate CLI specific options
|
||||
|
||||
/** |
||||
* Calculates the page size for a CLI Terminal output, taking into account the number of lines to skip and a default page size. |
||||
* |
||||
* @param skipSize - The number of lines to skip, which can be used to skip previous prompts. |
||||
* @param defaultPageSize - The default page size to use if the terminal height is too small. |
||||
* @returns The calculated pageSize, which is the terminal height minus the skip size, or the default page size if the terminal height is too small. |
||||
*/ |
||||
export function calculatePageSize( |
||||
skipSize: number = 0, |
||||
defaultPageSize: number = 15, |
||||
) { |
||||
return process.stdout.rows > skipSize |
||||
? process.stdout.rows - skipSize |
||||
: defaultPageSize; |
||||
} |
@ -0,0 +1,55 @@ |
||||
import { confirm, input } from '@inquirer/prompts'; |
||||
|
||||
import { logGray } from '../logger.js'; |
||||
|
||||
import { indentYamlOrJson } from './files.js'; |
||||
|
||||
export async function detectAndConfirmOrPrompt( |
||||
detect: () => Promise<string | undefined>, |
||||
prompt: string, |
||||
label: string, |
||||
source?: string, |
||||
): Promise<string> { |
||||
let detectedValue: string | undefined; |
||||
try { |
||||
detectedValue = await detect(); |
||||
if (detectedValue) { |
||||
const confirmed = await confirm({ |
||||
message: `Detected ${label} as ${detectedValue}${ |
||||
source ? ` from ${source}` : '' |
||||
}, is this correct?`,
|
||||
}); |
||||
if (confirmed) { |
||||
return detectedValue; |
||||
} |
||||
} |
||||
// eslint-disable-next-line no-empty
|
||||
} catch (e) {} |
||||
return input({ message: `${prompt} ${label}:`, default: detectedValue }); |
||||
} |
||||
|
||||
const INFO_COMMAND: string = 'i'; |
||||
const DOCS_NOTICE: string = |
||||
'For more information, please visit https://docs.hyperlane.xyz.'; |
||||
|
||||
export async function inputWithInfo({ |
||||
message, |
||||
info = 'No additional information available.', |
||||
defaultAnswer, |
||||
}: { |
||||
message: string; |
||||
info?: string; |
||||
defaultAnswer?: string; |
||||
}): Promise<string> { |
||||
let answer: string = ''; |
||||
do { |
||||
answer = await input({ |
||||
message: message.concat(` [enter '${INFO_COMMAND}' for more info]`), |
||||
default: defaultAnswer, |
||||
}); |
||||
answer = answer.trim().toLowerCase(); |
||||
const indentedInfo = indentYamlOrJson(`${info}\n${DOCS_NOTICE}\n`, 4); |
||||
if (answer === INFO_COMMAND) logGray(indentedInfo); |
||||
} while (answer === INFO_COMMAND); |
||||
return answer; |
||||
} |
@ -0,0 +1,69 @@ |
||||
import { MerkleTreeHook, ValidatorAnnounce } from '@hyperlane-xyz/core'; |
||||
import { S3Validator } from '@hyperlane-xyz/sdk'; |
||||
|
||||
import { logDebug } from '../logger.js'; |
||||
|
||||
export const getLatestMerkleTreeCheckpointIndex = async ( |
||||
merkleTreeHook: MerkleTreeHook, |
||||
chainName?: string, |
||||
): Promise<number | undefined> => { |
||||
try { |
||||
const [_, latestCheckpointIndex] = await merkleTreeHook.latestCheckpoint(); |
||||
return latestCheckpointIndex; |
||||
} catch (err) { |
||||
const debugMessage = `Failed to get latest checkpoint index from merkleTreeHook contract ${ |
||||
chainName ? `on ${chainName}` : '' |
||||
} : ${err}`;
|
||||
logDebug(debugMessage); |
||||
return undefined; |
||||
} |
||||
}; |
||||
|
||||
export const getValidatorStorageLocations = async ( |
||||
validatorAnnounce: ValidatorAnnounce, |
||||
validators: string[], |
||||
chainName?: string, |
||||
): Promise<string[][] | undefined> => { |
||||
try { |
||||
return await validatorAnnounce.getAnnouncedStorageLocations(validators); |
||||
} catch (err) { |
||||
const debugMessage = `Failed to get announced storage locations from validatorAnnounce contract ${ |
||||
chainName ? `on ${chainName}` : '' |
||||
} : ${err}`;
|
||||
logDebug(debugMessage); |
||||
return undefined; |
||||
} |
||||
}; |
||||
|
||||
export const getLatestValidatorCheckpointIndexAndUrl = async ( |
||||
s3StorageLocation: string, |
||||
): Promise<[number, string] | undefined> => { |
||||
let s3Validator: S3Validator; |
||||
try { |
||||
s3Validator = await S3Validator.fromStorageLocation(s3StorageLocation); |
||||
} catch (err) { |
||||
logDebug( |
||||
`Failed to instantiate S3Validator at location ${s3StorageLocation}: ${err}`, |
||||
); |
||||
return undefined; |
||||
} |
||||
try { |
||||
const latestCheckpointIndex = await s3Validator.getLatestCheckpointIndex(); |
||||
return [latestCheckpointIndex, s3Validator.getLatestCheckpointUrl()]; |
||||
} catch (err) { |
||||
logDebug( |
||||
`Failed to get latest checkpoint index from S3Validator at location ${s3StorageLocation}: ${err}`, |
||||
); |
||||
return undefined; |
||||
} |
||||
}; |
||||
|
||||
export const isValidatorSigningLatestCheckpoint = ( |
||||
latestValidatorCheckpointIndex: number, |
||||
latestMerkleTreeCheckpointIndex: number, |
||||
): boolean => { |
||||
const diff = Math.abs( |
||||
latestValidatorCheckpointIndex - latestMerkleTreeCheckpointIndex, |
||||
); |
||||
return diff < latestMerkleTreeCheckpointIndex / 100; |
||||
}; |
@ -1 +1 @@ |
||||
export const VERSION = '3.16.0'; |
||||
export const VERSION = '4.0.0'; |
||||
|
@ -0,0 +1,150 @@ |
||||
import { writeFileSync } from 'fs'; |
||||
import { stringify as yamlStringify } from 'yaml'; |
||||
|
||||
import { GithubRegistry } from '@hyperlane-xyz/registry'; |
||||
import { |
||||
IsmType, |
||||
TokenRouterConfig, |
||||
TokenType, |
||||
WarpRouteDeployConfig, |
||||
WarpRouteDeployConfigSchema, |
||||
buildAggregationIsmConfigs, |
||||
} from '@hyperlane-xyz/sdk'; |
||||
|
||||
const lockbox = '0xC8140dA31E6bCa19b287cC35531c2212763C2059'; |
||||
const xERC20 = '0x2416092f143378750bb29b79eD961ab195CcEea5'; |
||||
const lockboxChain = 'ethereum'; |
||||
|
||||
const chainsToDeploy = [ |
||||
'arbitrum', |
||||
'optimism', |
||||
'base', |
||||
'blast', |
||||
'bsc', |
||||
'mode', |
||||
'linea', |
||||
'ethereum', |
||||
]; |
||||
|
||||
const ezEthValidators = { |
||||
arbitrum: { |
||||
threshold: 1, |
||||
validators: [ |
||||
'0xc27032c6bbd48c20005f552af3aaa0dbf14260f3', // Renzo
|
||||
'0x9bCcFAd3BD12Ef0Ee8aE839dD9ED7835BcCaDc9D', // Everclear
|
||||
], |
||||
}, |
||||
optimism: { |
||||
threshold: 1, |
||||
validators: [ |
||||
'0xe2593D205F5E7F74A50fA900824501084E092eBd', // Renzo
|
||||
'0x6f4cb8e96db5d44422a4495faa73fffb9d30e9e2', // Everclear
|
||||
], |
||||
}, |
||||
base: { |
||||
threshold: 1, |
||||
validators: [ |
||||
'0x25BA4eE5268CbfB8D69BAc531Aa10368778702BD', // Renzo
|
||||
'0x9ec803b503e9c7d2611e231521ef3fde73f7a21c', // Everclear
|
||||
], |
||||
}, |
||||
blast: { |
||||
threshold: 1, |
||||
validators: [ |
||||
'0x54Bb0036F777202371429e062FE6AEE0d59442F9', // Renzo
|
||||
'0x1652d8ba766821cf01aeea34306dfc1cab964a32', // Everclear
|
||||
], |
||||
}, |
||||
bsc: { |
||||
threshold: 1, |
||||
validators: [ |
||||
'0x3156Db97a3B3e2dcc3D69FdDfD3e12dc7c937b6D', // Renzo
|
||||
'0x9a0326c43e4713ae2477f09e0f28ffedc24d8266', // Everclear
|
||||
], |
||||
}, |
||||
mode: { |
||||
threshold: 1, |
||||
validators: [ |
||||
'0x7e29608C6E5792bBf9128599ca309Be0728af7B4', // Renzo
|
||||
'0x456fbbe05484fc9f2f38ea09648424f54d6872be', // Everclear
|
||||
], |
||||
}, |
||||
linea: { |
||||
threshold: 1, |
||||
validators: [ |
||||
'0xcb3e44EdD2229860bDBaA58Ba2c3817D111bEE9A', // Renzo
|
||||
'0x06a5a2a429560034d38bf62ca6d470942535947e', // Everclear
|
||||
], |
||||
}, |
||||
ethereum: { |
||||
threshold: 1, |
||||
validators: [ |
||||
'0xc7f7b94a6BaF2FFFa54DfE1dDE6E5Fcbb749e04f', // Renzo
|
||||
'0x1fd889337F60986aa57166bc5AC121eFD13e4fdd', // Everclear
|
||||
], |
||||
}, |
||||
}; |
||||
const zeroAddress = '0x0000000000000000000000000000000000000001'; |
||||
|
||||
async function main() { |
||||
const registry = new GithubRegistry(); |
||||
|
||||
const tokenConfig: WarpRouteDeployConfig = |
||||
Object.fromEntries<TokenRouterConfig>( |
||||
await Promise.all( |
||||
chainsToDeploy.map( |
||||
async (chain): Promise<[string, TokenRouterConfig]> => { |
||||
const ret: [string, TokenRouterConfig] = [ |
||||
chain, |
||||
{ |
||||
isNft: false, |
||||
type: |
||||
chain === lockboxChain |
||||
? TokenType.XERC20Lockbox |
||||
: TokenType.XERC20, |
||||
token: chain === lockboxChain ? lockbox : xERC20, |
||||
owner: zeroAddress, |
||||
mailbox: (await registry.getChainAddresses(chain))!.mailbox, |
||||
interchainSecurityModule: { |
||||
type: IsmType.AGGREGATION, |
||||
threshold: 2, |
||||
modules: [ |
||||
{ |
||||
type: IsmType.ROUTING, |
||||
owner: zeroAddress, |
||||
domains: buildAggregationIsmConfigs( |
||||
chain, |
||||
chainsToDeploy, |
||||
ezEthValidators, |
||||
), |
||||
}, |
||||
{ |
||||
type: IsmType.FALLBACK_ROUTING, |
||||
domains: {}, |
||||
owner: zeroAddress, |
||||
}, |
||||
], |
||||
}, |
||||
}, |
||||
]; |
||||
|
||||
return ret; |
||||
}, |
||||
), |
||||
), |
||||
); |
||||
|
||||
const parsed = WarpRouteDeployConfigSchema.safeParse(tokenConfig); |
||||
|
||||
if (!parsed.success) { |
||||
console.dir(parsed.error.format(), { depth: null }); |
||||
return; |
||||
} |
||||
|
||||
writeFileSync( |
||||
'renzo-warp-route-config.yaml', |
||||
yamlStringify(parsed.data, null, 2), |
||||
); |
||||
} |
||||
|
||||
main().catch(console.error).then(console.log); |
@ -0,0 +1,164 @@ |
||||
import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers.js'; |
||||
import { expect } from 'chai'; |
||||
import { constants } from 'ethers'; |
||||
import hre from 'hardhat'; |
||||
|
||||
import { |
||||
Mailbox__factory, |
||||
ProxyAdmin__factory, |
||||
TestRecipient__factory, |
||||
TimelockController__factory, |
||||
ValidatorAnnounce__factory, |
||||
} from '@hyperlane-xyz/core'; |
||||
import { objMap } from '@hyperlane-xyz/utils'; |
||||
|
||||
import { TestChainName } from '../consts/testChains.js'; |
||||
import { MultiProvider } from '../providers/MultiProvider.js'; |
||||
import { testCoreConfig } from '../test/testUtils.js'; |
||||
|
||||
import { EvmCoreModule } from './EvmCoreModule.js'; |
||||
import { CoreConfig } from './types.js'; |
||||
|
||||
describe('EvmCoreModule', async () => { |
||||
const CHAIN = TestChainName.test1; |
||||
const DELAY = 1892391283182; |
||||
let config: CoreConfig; |
||||
let signer: SignerWithAddress; |
||||
let multiProvider: MultiProvider; |
||||
let evmCoreModule: EvmCoreModule; |
||||
let proxyAdminContract: any; |
||||
let mailboxContract: any; |
||||
let validatorAnnounceContract: any; |
||||
let testRecipientContract: any; |
||||
let timelockControllerContract: any; |
||||
|
||||
before(async () => { |
||||
[signer] = await hre.ethers.getSigners(); |
||||
multiProvider = MultiProvider.createTestMultiProvider({ signer }); |
||||
config = { |
||||
...testCoreConfig([CHAIN])[CHAIN], |
||||
upgrade: { |
||||
timelock: { |
||||
delay: DELAY, |
||||
roles: { |
||||
executor: signer.address, |
||||
proposer: signer.address, |
||||
}, |
||||
}, |
||||
}, |
||||
}; |
||||
|
||||
evmCoreModule = await EvmCoreModule.create({ |
||||
chain: CHAIN, |
||||
config, |
||||
multiProvider, |
||||
}); |
||||
|
||||
const { |
||||
proxyAdmin, |
||||
mailbox, |
||||
validatorAnnounce, |
||||
testRecipient, |
||||
timelockController, |
||||
} = evmCoreModule.serialize(); |
||||
|
||||
proxyAdminContract = ProxyAdmin__factory.connect( |
||||
proxyAdmin!, |
||||
multiProvider.getProvider(CHAIN), |
||||
); |
||||
|
||||
mailboxContract = Mailbox__factory.connect( |
||||
mailbox!, |
||||
multiProvider.getProvider(CHAIN), |
||||
); |
||||
|
||||
validatorAnnounceContract = ValidatorAnnounce__factory.connect( |
||||
validatorAnnounce!, |
||||
multiProvider.getProvider(CHAIN), |
||||
); |
||||
|
||||
testRecipientContract = TestRecipient__factory.connect( |
||||
testRecipient!, |
||||
multiProvider.getProvider(CHAIN), |
||||
); |
||||
|
||||
timelockControllerContract = TimelockController__factory.connect( |
||||
timelockController!, |
||||
multiProvider.getProvider(CHAIN), |
||||
); |
||||
}); |
||||
|
||||
describe('Create', async () => { |
||||
it('should create deploy an ICA', () => { |
||||
const { interchainAccountRouter, interchainAccountIsm } = |
||||
evmCoreModule.serialize(); |
||||
expect(interchainAccountIsm).to.exist; |
||||
expect(interchainAccountRouter).to.exist; |
||||
}); |
||||
|
||||
it('should deploy ISM factories', () => { |
||||
// Each ISM factory
|
||||
const deployedContracts = evmCoreModule.serialize(); |
||||
|
||||
objMap(deployedContracts as any, (_, address) => { |
||||
expect(address).to.exist; |
||||
expect(address).to.not.equal(constants.AddressZero); |
||||
}); |
||||
}); |
||||
|
||||
it('should deploy proxyAdmin', () => { |
||||
expect(evmCoreModule.serialize().proxyAdmin).to.exist; |
||||
}); |
||||
|
||||
it('should set proxyAdmin owner to deployer', async () => { |
||||
expect(await proxyAdminContract.owner()).to.equal(signer.address); |
||||
}); |
||||
|
||||
it('should deploy mailbox', async () => { |
||||
const mailboxAddress = evmCoreModule.serialize().mailbox; |
||||
expect(mailboxAddress).to.exist; |
||||
|
||||
// Check that it's actually a mailbox by calling one of it's methods
|
||||
expect(await mailboxContract.localDomain()).to.equal( |
||||
multiProvider.getChainId(CHAIN), |
||||
); |
||||
}); |
||||
|
||||
it('should set mailbox owner to config owner', async () => { |
||||
expect(await mailboxContract.owner()).to.equal(config.owner); |
||||
}); |
||||
|
||||
it('should deploy mailbox default Ism', async () => { |
||||
expect(await mailboxContract.defaultIsm()).to.not.equal( |
||||
constants.AddressZero, |
||||
); |
||||
}); |
||||
|
||||
it('should deploy mailbox default hook', async () => { |
||||
expect(await mailboxContract.defaultHook()).to.not.equal( |
||||
constants.AddressZero, |
||||
); |
||||
}); |
||||
|
||||
it('should deploy mailbox required hook', async () => { |
||||
expect(await mailboxContract.requiredHook()).to.not.equal( |
||||
constants.AddressZero, |
||||
); |
||||
}); |
||||
|
||||
it('should deploy validatorAnnounce', async () => { |
||||
expect(evmCoreModule.serialize().validatorAnnounce).to.exist; |
||||
expect(await validatorAnnounceContract.owner()).to.equal(signer.address); |
||||
}); |
||||
|
||||
it('should deploy testRecipient', async () => { |
||||
expect(evmCoreModule.serialize().testRecipient).to.exist; |
||||
expect(await testRecipientContract.owner()).to.equal(signer.address); |
||||
}); |
||||
|
||||
it('should deploy timelock if upgrade is set', async () => { |
||||
expect(evmCoreModule.serialize().timelockController).to.exist; |
||||
expect(await timelockControllerContract.getMinDelay()).to.equal(DELAY); |
||||
}); |
||||
}); |
||||
}); |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue