diff --git a/.changeset/chilly-balloons-rule.md b/.changeset/chilly-balloons-rule.md new file mode 100644 index 0000000000..b339b75699 --- /dev/null +++ b/.changeset/chilly-balloons-rule.md @@ -0,0 +1,5 @@ +--- +'@hyperlane-xyz/utils': minor +--- + +Added `isPrivateKeyEvm` function for validating EVM private keys diff --git a/.changeset/smooth-rocks-hammer.md b/.changeset/smooth-rocks-hammer.md new file mode 100644 index 0000000000..8cc170aac9 --- /dev/null +++ b/.changeset/smooth-rocks-hammer.md @@ -0,0 +1,5 @@ +--- +'@hyperlane-xyz/sdk': patch +--- + +Update default validator sets for alephzeroevmmainnet, appchain, lisk, lumiaprism, swell, treasure, vana, zklink. diff --git a/.changeset/spicy-gifts-hear.md b/.changeset/spicy-gifts-hear.md new file mode 100644 index 0000000000..37d4efa28d --- /dev/null +++ b/.changeset/spicy-gifts-hear.md @@ -0,0 +1,5 @@ +--- +'@hyperlane-xyz/cli': minor +--- + +Added strategy management CLI commands and MultiProtocolSigner implementation for flexible cross-chain signer configuration and management diff --git a/rust/main/Cargo.lock b/rust/main/Cargo.lock index e8831b6029..110954d2f7 100644 --- a/rust/main/Cargo.lock +++ b/rust/main/Cargo.lock @@ -2901,7 +2901,7 @@ dependencies = [ [[package]] name = "ethers" version = "1.0.2" -source = "git+https://github.com/hyperlane-xyz/ethers-rs?tag=2024-12-03-3#edf703a6e515266245b88bb4f1daad24f7c6566c" +source = "git+https://github.com/hyperlane-xyz/ethers-rs?tag=2024-12-10#d9f822ef9dd3d63b88cae74973540ef9e6773015" dependencies = [ "ethers-addressbook", "ethers-contract", @@ -2915,7 +2915,7 @@ dependencies = [ [[package]] name = "ethers-addressbook" version = "1.0.2" -source = "git+https://github.com/hyperlane-xyz/ethers-rs?tag=2024-12-03-3#edf703a6e515266245b88bb4f1daad24f7c6566c" +source = "git+https://github.com/hyperlane-xyz/ethers-rs?tag=2024-12-10#d9f822ef9dd3d63b88cae74973540ef9e6773015" dependencies = [ "ethers-core", "once_cell", @@ -2926,7 +2926,7 @@ dependencies = [ [[package]] name = "ethers-contract" version = "1.0.2" -source = "git+https://github.com/hyperlane-xyz/ethers-rs?tag=2024-12-03-3#edf703a6e515266245b88bb4f1daad24f7c6566c" +source = "git+https://github.com/hyperlane-xyz/ethers-rs?tag=2024-12-10#d9f822ef9dd3d63b88cae74973540ef9e6773015" dependencies = [ "ethers-contract-abigen", "ethers-contract-derive", @@ -2944,7 +2944,7 @@ dependencies = [ [[package]] name = "ethers-contract-abigen" version = "1.0.2" -source = "git+https://github.com/hyperlane-xyz/ethers-rs?tag=2024-12-03-3#edf703a6e515266245b88bb4f1daad24f7c6566c" +source = "git+https://github.com/hyperlane-xyz/ethers-rs?tag=2024-12-10#d9f822ef9dd3d63b88cae74973540ef9e6773015" dependencies = [ "Inflector", "cfg-if", @@ -2968,7 +2968,7 @@ dependencies = [ [[package]] name = "ethers-contract-derive" version = "1.0.2" -source = "git+https://github.com/hyperlane-xyz/ethers-rs?tag=2024-12-03-3#edf703a6e515266245b88bb4f1daad24f7c6566c" +source = "git+https://github.com/hyperlane-xyz/ethers-rs?tag=2024-12-10#d9f822ef9dd3d63b88cae74973540ef9e6773015" dependencies = [ "ethers-contract-abigen", "ethers-core", @@ -2982,7 +2982,7 @@ dependencies = [ [[package]] name = "ethers-core" version = "1.0.2" -source = "git+https://github.com/hyperlane-xyz/ethers-rs?tag=2024-12-03-3#edf703a6e515266245b88bb4f1daad24f7c6566c" +source = "git+https://github.com/hyperlane-xyz/ethers-rs?tag=2024-12-10#d9f822ef9dd3d63b88cae74973540ef9e6773015" dependencies = [ "arrayvec", "bytes", @@ -3012,7 +3012,7 @@ dependencies = [ [[package]] name = "ethers-etherscan" version = "1.0.2" -source = "git+https://github.com/hyperlane-xyz/ethers-rs?tag=2024-12-03-3#edf703a6e515266245b88bb4f1daad24f7c6566c" +source = "git+https://github.com/hyperlane-xyz/ethers-rs?tag=2024-12-10#d9f822ef9dd3d63b88cae74973540ef9e6773015" dependencies = [ "ethers-core", "getrandom 0.2.15", @@ -3028,7 +3028,7 @@ dependencies = [ [[package]] name = "ethers-middleware" version = "1.0.2" -source = "git+https://github.com/hyperlane-xyz/ethers-rs?tag=2024-12-03-3#edf703a6e515266245b88bb4f1daad24f7c6566c" +source = "git+https://github.com/hyperlane-xyz/ethers-rs?tag=2024-12-10#d9f822ef9dd3d63b88cae74973540ef9e6773015" dependencies = [ "async-trait", "auto_impl 0.5.0", @@ -3076,7 +3076,7 @@ dependencies = [ [[package]] name = "ethers-providers" version = "1.0.2" -source = "git+https://github.com/hyperlane-xyz/ethers-rs?tag=2024-12-03-3#edf703a6e515266245b88bb4f1daad24f7c6566c" +source = "git+https://github.com/hyperlane-xyz/ethers-rs?tag=2024-12-10#d9f822ef9dd3d63b88cae74973540ef9e6773015" dependencies = [ "async-trait", "auto_impl 1.2.0", @@ -3112,7 +3112,7 @@ dependencies = [ [[package]] name = "ethers-signers" version = "1.0.2" -source = "git+https://github.com/hyperlane-xyz/ethers-rs?tag=2024-12-03-3#edf703a6e515266245b88bb4f1daad24f7c6566c" +source = "git+https://github.com/hyperlane-xyz/ethers-rs?tag=2024-12-10#d9f822ef9dd3d63b88cae74973540ef9e6773015" dependencies = [ "async-trait", "coins-bip32 0.7.0", diff --git a/rust/main/Cargo.toml b/rust/main/Cargo.toml index 3fcdfa294c..6a0b7cbb35 100644 --- a/rust/main/Cargo.toml +++ b/rust/main/Cargo.toml @@ -197,27 +197,27 @@ overflow-checks = true [workspace.dependencies.ethers] features = [] git = "https://github.com/hyperlane-xyz/ethers-rs" -tag = "2024-12-03-3" +tag = "2024-12-10" [workspace.dependencies.ethers-contract] features = ["legacy"] git = "https://github.com/hyperlane-xyz/ethers-rs" -tag = "2024-12-03-3" +tag = "2024-12-10" [workspace.dependencies.ethers-core] features = [] git = "https://github.com/hyperlane-xyz/ethers-rs" -tag = "2024-12-03-3" +tag = "2024-12-10" [workspace.dependencies.ethers-providers] features = [] git = "https://github.com/hyperlane-xyz/ethers-rs" -tag = "2024-12-03-3" +tag = "2024-12-10" [workspace.dependencies.ethers-signers] features = ["aws"] git = "https://github.com/hyperlane-xyz/ethers-rs" -tag = "2024-12-03-3" +tag = "2024-12-10" [patch.crates-io.curve25519-dalek] branch = "v3.2.2-relax-zeroize" diff --git a/rust/main/hyperlane-base/src/types/gcs_storage.rs b/rust/main/hyperlane-base/src/types/gcs_storage.rs index a413edb323..93219f8510 100644 --- a/rust/main/hyperlane-base/src/types/gcs_storage.rs +++ b/rust/main/hyperlane-base/src/types/gcs_storage.rs @@ -240,7 +240,8 @@ impl CheckpointSyncer for GcsStorageClient { &self, signed_checkpoint: &SignedCheckpointWithMessageId, ) -> Result<()> { - let object_name = Self::get_checkpoint_key(signed_checkpoint.value.index); + let object_key = Self::get_checkpoint_key(signed_checkpoint.value.index); + let object_name = self.object_path(&object_key); let data = serde_json::to_vec(signed_checkpoint)?; self.upload_and_log(&object_name, data).await } diff --git a/typescript/cli/cli.ts b/typescript/cli/cli.ts index 77be0b86f8..45cad33bb8 100644 --- a/typescript/cli/cli.ts +++ b/typescript/cli/cli.ts @@ -19,15 +19,17 @@ import { overrideRegistryUriCommandOption, registryUriCommandOption, skipConfirmationOption, + strategyCommandOption, } from './src/commands/options.js'; import { registryCommand } from './src/commands/registry.js'; import { relayerCommand } from './src/commands/relayer.js'; import { sendCommand } from './src/commands/send.js'; import { statusCommand } from './src/commands/status.js'; +import { strategyCommand } from './src/commands/strategy.js'; import { submitCommand } from './src/commands/submit.js'; import { validatorCommand } from './src/commands/validator.js'; import { warpCommand } from './src/commands/warp.js'; -import { contextMiddleware } from './src/context/context.js'; +import { contextMiddleware, signerMiddleware } from './src/context/context.js'; import { configureLogger, errorRed } from './src/logger.js'; import { checkVersion } from './src/utils/version-check.js'; import { VERSION } from './src/version.js'; @@ -49,12 +51,14 @@ try { .option('key', keyCommandOption) .option('disableProxy', disableProxyCommandOption) .option('yes', skipConfirmationOption) + .option('strategy', strategyCommandOption) .global(['log', 'verbosity', 'registry', 'overrides', 'yes']) .middleware([ (argv) => { configureLogger(argv.log as LogFormat, argv.verbosity as LogLevel); }, contextMiddleware, + signerMiddleware, ]) .command(avsCommand) .command(configCommand) @@ -66,6 +70,7 @@ try { .command(relayerCommand) .command(sendCommand) .command(statusCommand) + .command(strategyCommand) .command(submitCommand) .command(validatorCommand) .command(warpCommand) diff --git a/typescript/cli/src/commands/config.ts b/typescript/cli/src/commands/config.ts index e72b72452a..4a5c6b580a 100644 --- a/typescript/cli/src/commands/config.ts +++ b/typescript/cli/src/commands/config.ts @@ -3,6 +3,7 @@ import { CommandModule } from 'yargs'; import { readChainConfigs } from '../config/chain.js'; import { readIsmConfig } from '../config/ism.js'; import { readMultisigConfig } from '../config/multisig.js'; +import { readChainSubmissionStrategyConfig } from '../config/strategy.js'; import { readWarpRouteDeployConfig } from '../config/warp.js'; import { CommandModuleWithContext } from '../context/types.js'; import { log, logGreen } from '../logger.js'; @@ -31,6 +32,7 @@ const validateCommand: CommandModule = { .command(validateChainCommand) .command(validateIsmCommand) .command(validateIsmAdvancedCommand) + .command(validateStrategyCommand) .command(validateWarpCommand) .version(false) .demandCommand(), @@ -76,6 +78,19 @@ const validateIsmAdvancedCommand: CommandModuleWithContext<{ path: string }> = { }, }; +const validateStrategyCommand: CommandModuleWithContext<{ path: string }> = { + command: 'strategy', + describe: 'Validates a Strategy config file', + builder: { + path: inputFileCommandOption(), + }, + handler: async ({ path }) => { + await readChainSubmissionStrategyConfig(path); + logGreen('Config is valid'); + process.exit(0); + }, +}; + const validateWarpCommand: CommandModuleWithContext<{ path: string }> = { command: 'warp', describe: 'Validate a Warp Route deployment config file', diff --git a/typescript/cli/src/commands/options.ts b/typescript/cli/src/commands/options.ts index f23194c804..baf0fa8472 100644 --- a/typescript/cli/src/commands/options.ts +++ b/typescript/cli/src/commands/options.ts @@ -95,6 +95,7 @@ export const DEFAULT_WARP_ROUTE_DEPLOYMENT_CONFIG_PATH = './configs/warp-route-deployment.yaml'; export const DEFAULT_CORE_DEPLOYMENT_CONFIG_PATH = './configs/core-config.yaml'; +export const DEFAULT_STRATEGY_CONFIG_PATH = `${os.homedir()}/.hyperlane/strategies/default-strategy.yaml`; export const warpDeploymentConfigCommandOption: Options = { type: 'string', @@ -196,8 +197,8 @@ export const transactionsCommandOption: Options = { export const strategyCommandOption: Options = { type: 'string', description: 'The submission strategy input file path.', - alias: 's', - demandOption: true, + alias: ['s', 'strategy'], + demandOption: false, }; export const addressCommandOption = ( diff --git a/typescript/cli/src/commands/signCommands.ts b/typescript/cli/src/commands/signCommands.ts index 37d83096a0..8cfa6d4c1a 100644 --- a/typescript/cli/src/commands/signCommands.ts +++ b/typescript/cli/src/commands/signCommands.ts @@ -1,11 +1,36 @@ // Commands that send tx and require a key to sign. // It's useful to have this listed here so the context // middleware can request keys up front when required. -export const SIGN_COMMANDS = ['deploy', 'send', 'status', 'submit', 'relayer']; +export const SIGN_COMMANDS = [ + 'apply', + 'deploy', + 'send', + 'status', + 'submit', + 'relayer', +]; export function isSignCommand(argv: any): boolean { + //TODO: fix reading and checking warp without signer, and remove this + const temporarySignCommandsCheck = + argv._[0] === 'warp' && (argv._[1] === 'read' || argv._[1] === 'check'); return ( SIGN_COMMANDS.includes(argv._[0]) || - (argv._.length > 1 && SIGN_COMMANDS.includes(argv._[1])) + (argv._.length > 1 && SIGN_COMMANDS.includes(argv._[1])) || + temporarySignCommandsCheck ); } + +export enum CommandType { + WARP_DEPLOY = 'warp:deploy', + WARP_SEND = 'warp:send', + WARP_APPLY = 'warp:apply', + WARP_READ = 'warp:read', + WARP_CHECK = 'warp:check', + SEND_MESSAGE = 'send:message', + AGENT_KURTOSIS = 'deploy:kurtosis-agents', + STATUS = 'status:', + SUBMIT = 'submit:', + RELAYER = 'relayer:', + CORE_APPLY = 'core:apply', +} diff --git a/typescript/cli/src/commands/strategy.ts b/typescript/cli/src/commands/strategy.ts new file mode 100644 index 0000000000..414a3d48ee --- /dev/null +++ b/typescript/cli/src/commands/strategy.ts @@ -0,0 +1,70 @@ +import { stringify as yamlStringify } from 'yaml'; +import { CommandModule } from 'yargs'; + +import { + createStrategyConfig, + readChainSubmissionStrategyConfig, +} from '../config/strategy.js'; +import { CommandModuleWithWriteContext } from '../context/types.js'; +import { log, logCommandHeader } from '../logger.js'; +import { indentYamlOrJson } from '../utils/files.js'; +import { maskSensitiveData } from '../utils/output.js'; + +import { + DEFAULT_STRATEGY_CONFIG_PATH, + outputFileCommandOption, + strategyCommandOption, +} from './options.js'; + +/** + * Parent command + */ +export const strategyCommand: CommandModule = { + command: 'strategy', + describe: 'Manage Hyperlane deployment strategies', + builder: (yargs) => + yargs.command(init).command(read).version(false).demandCommand(), + handler: () => log('Command required'), +}; + +export const init: CommandModuleWithWriteContext<{ + out: string; +}> = { + command: 'init', + describe: 'Creates strategy configuration', + builder: { + out: outputFileCommandOption(DEFAULT_STRATEGY_CONFIG_PATH), + }, + handler: async ({ context, out }) => { + logCommandHeader(`Hyperlane Strategy Init`); + + await createStrategyConfig({ + context, + outPath: out, + }); + process.exit(0); + }, +}; + +export const read: CommandModuleWithWriteContext<{ + strategy: string; +}> = { + command: 'read', + describe: 'Reads strategy configuration', + builder: { + strategy: { + ...strategyCommandOption, + demandOption: true, + default: DEFAULT_STRATEGY_CONFIG_PATH, + }, + }, + handler: async ({ strategy: strategyUrl }) => { + logCommandHeader(`Hyperlane Strategy Read`); + + const strategy = await readChainSubmissionStrategyConfig(strategyUrl); + const maskedConfig = maskSensitiveData(strategy); + log(indentYamlOrJson(yamlStringify(maskedConfig, null, 2), 4)); + + process.exit(0); + }, +}; diff --git a/typescript/cli/src/config/core.ts b/typescript/cli/src/config/core.ts index c7161f6b4b..5b4395c951 100644 --- a/typescript/cli/src/config/core.ts +++ b/typescript/cli/src/config/core.ts @@ -39,7 +39,7 @@ export async function createCoreDeployConfig({ logBlue('Creating a new core deployment config...'); const owner = await detectAndConfirmOrPrompt( - async () => context.signer?.getAddress(), + async () => context.signerAddress, ENTER_DESIRED_VALUE_MSG, 'owner address', SIGNER_PROMPT_LABEL, @@ -64,7 +64,7 @@ export async function createCoreDeployConfig({ }); proxyAdmin = { owner: await detectAndConfirmOrPrompt( - async () => context.signer?.getAddress(), + async () => context.signerAddress, ENTER_DESIRED_VALUE_MSG, 'ProxyAdmin owner address', SIGNER_PROMPT_LABEL, diff --git a/typescript/cli/src/config/hooks.ts b/typescript/cli/src/config/hooks.ts index e8df64dc00..5ad005dc24 100644 --- a/typescript/cli/src/config/hooks.ts +++ b/typescript/cli/src/config/hooks.ts @@ -243,10 +243,10 @@ async function getOwnerAndBeneficiary( advanced: boolean, ) { const unnormalizedOwner = - !advanced && context.signer - ? await context.signer.getAddress() + !advanced && context.signerAddress + ? context.signerAddress : await detectAndConfirmOrPrompt( - async () => context.signer?.getAddress(), + async () => context.signerAddress, `For ${module}, enter`, 'owner address', 'signer', diff --git a/typescript/cli/src/config/ism.ts b/typescript/cli/src/config/ism.ts index f7f6bab9ad..81440e96eb 100644 --- a/typescript/cli/src/config/ism.ts +++ b/typescript/cli/src/config/ism.ts @@ -163,10 +163,10 @@ export const createTrustedRelayerConfig = callWithConfigCreationLogs( advanced: boolean = false, ): Promise => { const relayer = - !advanced && context.signer - ? await context.signer.getAddress() + !advanced && context.signerAddress + ? context.signerAddress : await detectAndConfirmOrPrompt( - async () => context.signer?.getAddress(), + async () => context.signerAddress, 'For trusted relayer ISM, enter', 'relayer address', 'signer', diff --git a/typescript/cli/src/config/strategy.ts b/typescript/cli/src/config/strategy.ts new file mode 100644 index 0000000000..f57c7d3378 --- /dev/null +++ b/typescript/cli/src/config/strategy.ts @@ -0,0 +1,186 @@ +import { confirm, input, password, select } from '@inquirer/prompts'; +import { Wallet } from 'ethers'; +import { stringify as yamlStringify } from 'yaml'; + +import { + ChainSubmissionStrategy, + ChainSubmissionStrategySchema, + TxSubmitterType, +} from '@hyperlane-xyz/sdk'; +import { + ProtocolType, + assert, + errorToString, + isAddress, + isPrivateKeyEvm, +} from '@hyperlane-xyz/utils'; + +import { CommandContext } from '../context/types.js'; +import { errorRed, log, logBlue, logGreen, logRed } from '../logger.js'; +import { runSingleChainSelectionStep } from '../utils/chains.js'; +import { + indentYamlOrJson, + isFile, + readYamlOrJson, + writeYamlOrJson, +} from '../utils/files.js'; +import { maskSensitiveData } from '../utils/output.js'; + +/** + * Reads and validates a chain submission strategy configuration from a file + */ +export async function readChainSubmissionStrategyConfig( + filePath: string, +): Promise { + log(`Reading submission strategy in ${filePath}`); + try { + const strategyConfig = readYamlOrJson(filePath); + + const parseResult = ChainSubmissionStrategySchema.parse(strategyConfig); + + return parseResult; + } catch (error) { + logRed(`⛔️ Error reading strategy config:`, errorToString(error)); + throw error; // Re-throw to let caller handle the error + } +} + +/** + * Safely reads chain submission strategy config, returns empty object if any errors occur + */ +export async function safeReadChainSubmissionStrategyConfig( + filePath: string, +): Promise { + try { + const trimmedFilePath = filePath.trim(); + if (!isFile(trimmedFilePath)) { + logBlue(`File ${trimmedFilePath} does not exist, returning empty config`); + return {}; + } + return await readChainSubmissionStrategyConfig(trimmedFilePath); + } catch (error) { + logRed( + `Failed to read strategy config, defaulting to empty config:`, + errorToString(error), + ); + return {}; + } +} + +export async function createStrategyConfig({ + context, + outPath, +}: { + context: CommandContext; + outPath: string; +}) { + let strategy: ChainSubmissionStrategy; + try { + const strategyObj = await readYamlOrJson(outPath); + strategy = ChainSubmissionStrategySchema.parse(strategyObj); + } catch { + strategy = writeYamlOrJson(outPath, {}, 'yaml'); + } + + const chain = await runSingleChainSelectionStep(context.chainMetadata); + const chainProtocol = context.chainMetadata[chain].protocol; + + if ( + !context.skipConfirmation && + strategy && + Object.prototype.hasOwnProperty.call(strategy, chain) + ) { + const isConfirmed = await confirm({ + message: `Default strategy for chain ${chain} already exists. Are you sure you want to overwrite existing strategy config?`, + default: false, + }); + + assert(isConfirmed, 'Strategy initialization cancelled by user.'); + } + + const isEthereum = chainProtocol === ProtocolType.Ethereum; + const submitterType = isEthereum + ? await select({ + message: 'Select the submitter type', + choices: Object.values(TxSubmitterType).map((value) => ({ + name: value, + value: value, + })), + }) + : TxSubmitterType.JSON_RPC; // Do other non-evm chains support gnosis and account impersonation? + + const submitter: Record = { type: submitterType }; + + switch (submitterType) { + case TxSubmitterType.JSON_RPC: + submitter.privateKey = await password({ + message: 'Enter the private key for JSON-RPC submission:', + validate: (pk) => (isEthereum ? isPrivateKeyEvm(pk) : true), + }); + + submitter.userAddress = isEthereum + ? await new Wallet(submitter.privateKey).getAddress() + : await input({ + message: 'Enter the user address for JSON-RPC submission:', + }); + + submitter.chain = chain; + break; + + case TxSubmitterType.IMPERSONATED_ACCOUNT: + submitter.userAddress = await input({ + message: 'Enter the user address to impersonate', + validate: (address) => + isAddress(address) ? true : 'Invalid Ethereum address', + }); + assert( + submitter.userAddress, + 'User address is required for impersonated account', + ); + break; + + case TxSubmitterType.GNOSIS_SAFE: + case TxSubmitterType.GNOSIS_TX_BUILDER: + submitter.safeAddress = await input({ + message: 'Enter the Safe address', + validate: (address) => + isAddress(address) ? true : 'Invalid Safe address', + }); + + submitter.chain = chain; + + if (submitterType === TxSubmitterType.GNOSIS_TX_BUILDER) { + submitter.version = await input({ + message: 'Enter the Safe version (default: 1.0)', + default: '1.0', + }); + } + break; + + default: + throw new Error(`Unsupported submitter type: ${submitterType}`); + } + + const strategyResult: ChainSubmissionStrategy = { + ...strategy, + [chain]: { + submitter: submitter as ChainSubmissionStrategy[string]['submitter'], + }, + }; + + try { + const strategyConfig = ChainSubmissionStrategySchema.parse(strategyResult); + logBlue(`Strategy configuration is valid. Writing to file ${outPath}:\n`); + + const maskedConfig = maskSensitiveData(strategyConfig); + log(indentYamlOrJson(yamlStringify(maskedConfig, null, 2), 4)); + + writeYamlOrJson(outPath, strategyConfig); + logGreen('✅ Successfully created a new strategy configuration.'); + } catch { + // don't log error since it may contain sensitive data + errorRed( + `The strategy configuration is invalid. Please review the submitter settings.`, + ); + } +} diff --git a/typescript/cli/src/config/warp.ts b/typescript/cli/src/config/warp.ts index 1174d0156b..f31069e091 100644 --- a/typescript/cli/src/config/warp.ts +++ b/typescript/cli/src/config/warp.ts @@ -82,7 +82,7 @@ async function fillDefaults( let owner = config.owner; if (!owner) { owner = - (await context.signer?.getAddress()) ?? + context.signerAddress ?? (await context.multiProvider.getSignerAddress(chain)); } return { @@ -122,13 +122,6 @@ export async function createWarpRouteDeployConfig({ }) { logBlue('Creating a new warp route deployment config...'); - const owner = await detectAndConfirmOrPrompt( - async () => context.signer?.getAddress(), - 'Enter the desired', - 'owner address', - 'signer', - ); - const warpChains = await runMultiChainSelectionStep({ chainMetadata: context.chainMetadata, message: 'Select chains to connect', @@ -142,6 +135,12 @@ export async function createWarpRouteDeployConfig({ let typeChoices = TYPE_CHOICES; for (const chain of warpChains) { logBlue(`${chain}: Configuring warp route...`); + const owner = await detectAndConfirmOrPrompt( + async () => context.signerAddress, + 'Enter the desired', + 'owner address', + 'signer', + ); // default to the mailbox from the registry and if not found ask to the user to submit one const chainAddresses = await context.registry.getChainAddresses(chain); diff --git a/typescript/cli/src/context/context.ts b/typescript/cli/src/context/context.ts index f9dfb34ab9..570b233cde 100644 --- a/typescript/cli/src/context/context.ts +++ b/typescript/cli/src/context/context.ts @@ -1,5 +1,5 @@ import { confirm } from '@inquirer/prompts'; -import { ethers } from 'ethers'; +import { Signer, ethers } from 'ethers'; import { DEFAULT_GITHUB_REGISTRY, @@ -16,7 +16,9 @@ import { } from '@hyperlane-xyz/sdk'; import { isHttpsUrl, isNullish, rootLogger } from '@hyperlane-xyz/utils'; +import { DEFAULT_STRATEGY_CONFIG_PATH } from '../commands/options.js'; import { isSignCommand } from '../commands/signCommands.js'; +import { safeReadChainSubmissionStrategyConfig } from '../config/strategy.js'; import { PROXY_DEPLOYED_URL } from '../consts.js'; import { forkNetworkToMultiProvider, verifyAnvil } from '../deploy/dry-run.js'; import { logBlue } from '../logger.js'; @@ -24,6 +26,8 @@ import { runSingleChainSelectionStep } from '../utils/chains.js'; import { detectAndConfirmOrPrompt } from '../utils/input.js'; import { getImpersonatedSigner, getSigner } from '../utils/keys.js'; +import { ChainResolverFactory } from './strategies/chain/ChainResolverFactory.js'; +import { MultiProtocolSignerManager } from './strategies/signer/MultiProtocolSignerManager.js'; import { CommandContext, ContextSettings, @@ -41,6 +45,7 @@ export async function contextMiddleware(argv: Record) { requiresKey, disableProxy: argv.disableProxy, skipConfirmation: argv.yes, + strategyPath: argv.strategy, }; if (!isDryRun && settings.fromAddress) throw new Error( @@ -52,6 +57,44 @@ export async function contextMiddleware(argv: Record) { argv.context = context; } +export async function signerMiddleware(argv: Record) { + const { key, requiresKey, multiProvider, strategyPath } = argv.context; + + if (!requiresKey) return argv; + + const strategyConfig = await safeReadChainSubmissionStrategyConfig( + strategyPath ?? DEFAULT_STRATEGY_CONFIG_PATH, + ); + + /** + * Intercepts Hyperlane command to determine chains. + */ + const chainStrategy = ChainResolverFactory.getStrategy(argv); + + /** + * Resolves chains based on the chain strategy. + */ + const chains = await chainStrategy.resolveChains(argv); + + /** + * Extracts signer config + */ + const multiProtocolSigner = new MultiProtocolSignerManager( + strategyConfig, + chains, + multiProvider, + { key }, + ); + + /** + * @notice Attaches signers to MultiProvider and assigns it to argv.multiProvider + */ + argv.multiProvider = await multiProtocolSigner.getMultiProvider(); + argv.multiProtocolSigner = multiProtocolSigner; + + return argv; +} + /** * Retrieves context for the user-selected command * @returns context for the current command @@ -66,19 +109,24 @@ export async function getContext({ }: ContextSettings): Promise { const registry = getRegistry(registryUri, registryOverrideUri, !disableProxy); - let signer: ethers.Wallet | undefined = undefined; - if (key || requiresKey) { + //Just for backward compatibility + let signerAddress: string | undefined = undefined; + if (key) { + let signer: Signer; ({ key, signer } = await getSigner({ key, skipConfirmation })); + signerAddress = await signer.getAddress(); } - const multiProvider = await getMultiProvider(registry, signer); + + const multiProvider = await getMultiProvider(registry); return { registry, + requiresKey, chainMetadata: multiProvider.metadata, multiProvider, key, - signer, skipConfirmation: !!skipConfirmation, + signerAddress, } as CommandContext; } diff --git a/typescript/cli/src/context/strategies/chain/ChainResolverFactory.ts b/typescript/cli/src/context/strategies/chain/ChainResolverFactory.ts new file mode 100644 index 0000000000..e417ba27b3 --- /dev/null +++ b/typescript/cli/src/context/strategies/chain/ChainResolverFactory.ts @@ -0,0 +1,36 @@ +import { CommandType } from '../../../commands/signCommands.js'; + +import { MultiChainResolver } from './MultiChainResolver.js'; +import { SingleChainResolver } from './SingleChainResolver.js'; +import { ChainResolver } from './types.js'; + +/** + * @class ChainResolverFactory + * @description Intercepts commands to determine the appropriate chain resolver strategy based on command type. + */ +export class ChainResolverFactory { + private static strategyMap: Map ChainResolver> = new Map([ + [CommandType.WARP_DEPLOY, () => MultiChainResolver.forWarpRouteConfig()], + [CommandType.WARP_SEND, () => MultiChainResolver.forOriginDestination()], + [CommandType.WARP_APPLY, () => MultiChainResolver.forWarpRouteConfig()], + [CommandType.WARP_READ, () => MultiChainResolver.forWarpCoreConfig()], + [CommandType.WARP_CHECK, () => MultiChainResolver.forWarpCoreConfig()], + [CommandType.SEND_MESSAGE, () => MultiChainResolver.forOriginDestination()], + [CommandType.AGENT_KURTOSIS, () => MultiChainResolver.forAgentKurtosis()], + [CommandType.STATUS, () => MultiChainResolver.forOriginDestination()], + [CommandType.SUBMIT, () => MultiChainResolver.forStrategyConfig()], + [CommandType.RELAYER, () => MultiChainResolver.forRelayer()], + [CommandType.CORE_APPLY, () => MultiChainResolver.forCoreApply()], + ]); + + /** + * @param argv - Command line arguments. + * @returns ChainResolver - The appropriate chain resolver strategy based on the command type. + */ + static getStrategy(argv: Record): ChainResolver { + const commandKey = `${argv._[0]}:${argv._[1] || ''}`.trim() as CommandType; + const createStrategy = + this.strategyMap.get(commandKey) || (() => new SingleChainResolver()); + return createStrategy(); + } +} diff --git a/typescript/cli/src/context/strategies/chain/MultiChainResolver.ts b/typescript/cli/src/context/strategies/chain/MultiChainResolver.ts new file mode 100644 index 0000000000..64f3257520 --- /dev/null +++ b/typescript/cli/src/context/strategies/chain/MultiChainResolver.ts @@ -0,0 +1,249 @@ +import { + ChainMap, + ChainName, + DeployedCoreAddresses, + DeployedCoreAddressesSchema, + EvmCoreModule, +} from '@hyperlane-xyz/sdk'; +import { assert } from '@hyperlane-xyz/utils'; + +import { DEFAULT_WARP_ROUTE_DEPLOYMENT_CONFIG_PATH } from '../../../commands/options.js'; +import { readCoreDeployConfigs } from '../../../config/core.js'; +import { readChainSubmissionStrategyConfig } from '../../../config/strategy.js'; +import { log } from '../../../logger.js'; +import { + extractChainsFromObj, + runMultiChainSelectionStep, + runSingleChainSelectionStep, +} from '../../../utils/chains.js'; +import { + isFile, + readYamlOrJson, + runFileSelectionStep, +} from '../../../utils/files.js'; +import { getWarpCoreConfigOrExit } from '../../../utils/warp.js'; + +import { ChainResolver } from './types.js'; + +enum ChainSelectionMode { + ORIGIN_DESTINATION, + AGENT_KURTOSIS, + WARP_CONFIG, + WARP_READ, + STRATEGY, + RELAYER, + CORE_APPLY, +} + +// This class could be broken down into multiple strategies + +/** + * @title MultiChainResolver + * @notice Resolves chains based on the specified selection mode. + */ +export class MultiChainResolver implements ChainResolver { + constructor(private mode: ChainSelectionMode) {} + + async resolveChains(argv: ChainMap): Promise { + switch (this.mode) { + case ChainSelectionMode.WARP_CONFIG: + return this.resolveWarpRouteConfigChains(argv); + case ChainSelectionMode.WARP_READ: + return this.resolveWarpCoreConfigChains(argv); + case ChainSelectionMode.AGENT_KURTOSIS: + return this.resolveAgentChains(argv); + case ChainSelectionMode.STRATEGY: + return this.resolveStrategyChains(argv); + case ChainSelectionMode.RELAYER: + return this.resolveRelayerChains(argv); + case ChainSelectionMode.CORE_APPLY: + return this.resolveCoreApplyChains(argv); + case ChainSelectionMode.ORIGIN_DESTINATION: + default: + return this.resolveOriginDestinationChains(argv); + } + } + + private async resolveWarpRouteConfigChains( + argv: Record, + ): Promise { + argv.config ||= DEFAULT_WARP_ROUTE_DEPLOYMENT_CONFIG_PATH; + argv.context.chains = await this.getWarpRouteConfigChains( + argv.config.trim(), + argv.skipConfirmation, + ); + return argv.context.chains; + } + + private async resolveWarpCoreConfigChains( + argv: Record, + ): Promise { + if (argv.symbol || argv.warp) { + const warpCoreConfig = await getWarpCoreConfigOrExit({ + context: argv.context, + warp: argv.warp, + symbol: argv.symbol, + }); + argv.context.warpCoreConfig = warpCoreConfig; + const chains = extractChainsFromObj(warpCoreConfig); + return chains; + } else if (argv.chain) { + return [argv.chain]; + } else { + throw new Error( + `Please specify either a symbol, chain and address or warp file`, + ); + } + } + + private async resolveAgentChains( + argv: Record, + ): Promise { + const { chainMetadata } = argv.context; + argv.origin = + argv.origin ?? + (await runSingleChainSelectionStep( + chainMetadata, + 'Select the origin chain', + )); + + if (!argv.targets) { + const selectedRelayChains = await runMultiChainSelectionStep({ + chainMetadata: chainMetadata, + message: 'Select chains to relay between', + requireNumber: 2, + }); + argv.targets = selectedRelayChains.join(','); + } + + return [argv.origin, ...argv.targets]; + } + + private async resolveOriginDestinationChains( + argv: Record, + ): Promise { + const { chainMetadata } = argv.context; + + argv.origin = + argv.origin ?? + (await runSingleChainSelectionStep( + chainMetadata, + 'Select the origin chain', + )); + + argv.destination = + argv.destination ?? + (await runSingleChainSelectionStep( + chainMetadata, + 'Select the destination chain', + )); + + return [argv.origin, argv.destination]; + } + + private async resolveStrategyChains( + argv: Record, + ): Promise { + const strategy = await readChainSubmissionStrategyConfig(argv.strategy); + return extractChainsFromObj(strategy); + } + + private async resolveRelayerChains( + argv: Record, + ): Promise { + return argv.chains.split(',').map((item: string) => item.trim()); + } + + private async getWarpRouteConfigChains( + configPath: string, + skipConfirmation: boolean, + ): Promise { + if (!configPath || !isFile(configPath)) { + assert(!skipConfirmation, 'Warp route deployment config is required'); + configPath = await runFileSelectionStep( + './configs', + 'Warp route deployment config', + 'warp', + ); + } else { + log(`Using warp route deployment config at ${configPath}`); + } + + // Alternative to readWarpRouteDeployConfig that doesn't use context for signer and zod validation + const warpRouteConfig = (await readYamlOrJson(configPath)) as Record< + string, + any + >; + + const chains = Object.keys(warpRouteConfig) as ChainName[]; + assert( + chains.length !== 0, + 'No chains found in warp route deployment config', + ); + + return chains; + } + + private async resolveCoreApplyChains( + argv: Record, + ): Promise { + try { + const config = readCoreDeployConfigs(argv.config); + + if (!config?.interchainAccountRouter) { + return [argv.chain]; + } + + const addresses = await argv.context.registry.getChainAddresses( + argv.chain, + ); + const coreAddresses = DeployedCoreAddressesSchema.parse( + addresses, + ) as DeployedCoreAddresses; + + const evmCoreModule = new EvmCoreModule(argv.context.multiProvider, { + chain: argv.chain, + config, + addresses: coreAddresses, + }); + + const transactions = await evmCoreModule.update(config); + + return Array.from(new Set(transactions.map((tx) => tx.chainId))).map( + (chainId) => argv.context.multiProvider.getChainName(chainId), + ); + } catch (error) { + throw new Error(`Failed to resolve core apply chains`, { + cause: error, + }); + } + } + + static forAgentKurtosis(): MultiChainResolver { + return new MultiChainResolver(ChainSelectionMode.AGENT_KURTOSIS); + } + + static forOriginDestination(): MultiChainResolver { + return new MultiChainResolver(ChainSelectionMode.ORIGIN_DESTINATION); + } + + static forRelayer(): MultiChainResolver { + return new MultiChainResolver(ChainSelectionMode.RELAYER); + } + + static forStrategyConfig(): MultiChainResolver { + return new MultiChainResolver(ChainSelectionMode.STRATEGY); + } + + static forWarpRouteConfig(): MultiChainResolver { + return new MultiChainResolver(ChainSelectionMode.WARP_CONFIG); + } + + static forWarpCoreConfig(): MultiChainResolver { + return new MultiChainResolver(ChainSelectionMode.WARP_READ); + } + + static forCoreApply(): MultiChainResolver { + return new MultiChainResolver(ChainSelectionMode.CORE_APPLY); + } +} diff --git a/typescript/cli/src/context/strategies/chain/SingleChainResolver.ts b/typescript/cli/src/context/strategies/chain/SingleChainResolver.ts new file mode 100644 index 0000000000..8dddaf3c4a --- /dev/null +++ b/typescript/cli/src/context/strategies/chain/SingleChainResolver.ts @@ -0,0 +1,25 @@ +import { ChainMap, ChainName } from '@hyperlane-xyz/sdk'; + +import { runSingleChainSelectionStep } from '../../../utils/chains.js'; + +import { ChainResolver } from './types.js'; + +/** + * @title SingleChainResolver + * @notice Strategy implementation for managing single-chain operations + * @dev Primarily used for operations like 'core:apply' and 'warp:read' + */ +export class SingleChainResolver implements ChainResolver { + /** + * @notice Determines the chain to be used for signing operations + * @dev Either uses the chain specified in argv or prompts for interactive selection + */ + async resolveChains(argv: ChainMap): Promise { + argv.chain ||= await runSingleChainSelectionStep( + argv.context.chainMetadata, + 'Select chain to connect:', + ); + + return [argv.chain]; // Explicitly return as single-item array + } +} diff --git a/typescript/cli/src/context/strategies/chain/types.ts b/typescript/cli/src/context/strategies/chain/types.ts new file mode 100644 index 0000000000..9318bed8c2 --- /dev/null +++ b/typescript/cli/src/context/strategies/chain/types.ts @@ -0,0 +1,10 @@ +import { ChainMap, ChainName } from '@hyperlane-xyz/sdk'; + +export interface ChainResolver { + /** + * Determines the chains to be used for signing + * @param argv Command arguments + * @returns Array of chain names + */ + resolveChains(argv: ChainMap): Promise; +} diff --git a/typescript/cli/src/context/strategies/signer/BaseMultiProtocolSigner.ts b/typescript/cli/src/context/strategies/signer/BaseMultiProtocolSigner.ts new file mode 100644 index 0000000000..b91242b42d --- /dev/null +++ b/typescript/cli/src/context/strategies/signer/BaseMultiProtocolSigner.ts @@ -0,0 +1,22 @@ +import { Signer } from 'ethers'; + +import { ChainName, ChainSubmissionStrategy } from '@hyperlane-xyz/sdk'; +import { Address } from '@hyperlane-xyz/utils'; + +export interface SignerConfig { + privateKey: string; + address?: Address; // For chains like StarkNet that require address + extraParams?: Record; // For any additional chain-specific params +} + +export interface IMultiProtocolSigner { + getSignerConfig(chain: ChainName): Promise | SignerConfig; + getSigner(config: SignerConfig): Signer; +} + +export abstract class BaseMultiProtocolSigner implements IMultiProtocolSigner { + constructor(protected config: ChainSubmissionStrategy) {} + + abstract getSignerConfig(chain: ChainName): Promise; + abstract getSigner(config: SignerConfig): Signer; +} diff --git a/typescript/cli/src/context/strategies/signer/MultiProtocolSignerFactory.ts b/typescript/cli/src/context/strategies/signer/MultiProtocolSignerFactory.ts new file mode 100644 index 0000000000..030f11b5f4 --- /dev/null +++ b/typescript/cli/src/context/strategies/signer/MultiProtocolSignerFactory.ts @@ -0,0 +1,79 @@ +import { password } from '@inquirer/prompts'; +import { Signer, Wallet } from 'ethers'; + +import { + ChainName, + ChainSubmissionStrategy, + ChainTechnicalStack, + MultiProvider, + TxSubmitterType, +} from '@hyperlane-xyz/sdk'; +import { ProtocolType } from '@hyperlane-xyz/utils'; + +import { + BaseMultiProtocolSigner, + IMultiProtocolSigner, + SignerConfig, +} from './BaseMultiProtocolSigner.js'; + +export class MultiProtocolSignerFactory { + static getSignerStrategy( + chain: ChainName, + strategyConfig: ChainSubmissionStrategy, + multiProvider: MultiProvider, + ): IMultiProtocolSigner { + const { protocol, technicalStack } = multiProvider.getChainMetadata(chain); + + switch (protocol) { + case ProtocolType.Ethereum: + if (technicalStack === ChainTechnicalStack.ZkSync) + return new ZKSyncSignerStrategy(strategyConfig); + return new EthereumSignerStrategy(strategyConfig); + default: + throw new Error(`Unsupported protocol: ${protocol}`); + } + } +} + +class EthereumSignerStrategy extends BaseMultiProtocolSigner { + async getSignerConfig(chain: ChainName): Promise { + const submitter = this.config[chain]?.submitter as { + type: TxSubmitterType.JSON_RPC; + privateKey?: string; + }; + + const privateKey = + submitter?.privateKey ?? + (await password({ + message: `Please enter the private key for chain ${chain}`, + })); + + return { privateKey }; + } + + getSigner(config: SignerConfig): Signer { + return new Wallet(config.privateKey); + } +} + +// 99% overlap with EthereumSignerStrategy for the sake of keeping MultiProtocolSignerFactory clean +// TODO: import ZKSync signer +class ZKSyncSignerStrategy extends BaseMultiProtocolSigner { + async getSignerConfig(chain: ChainName): Promise { + const submitter = this.config[chain]?.submitter as { + privateKey?: string; + }; + + const privateKey = + submitter?.privateKey ?? + (await password({ + message: `Please enter the private key for chain ${chain}`, + })); + + return { privateKey }; + } + + getSigner(config: SignerConfig): Signer { + return new Wallet(config.privateKey); + } +} diff --git a/typescript/cli/src/context/strategies/signer/MultiProtocolSignerManager.ts b/typescript/cli/src/context/strategies/signer/MultiProtocolSignerManager.ts new file mode 100644 index 0000000000..12f9c0f819 --- /dev/null +++ b/typescript/cli/src/context/strategies/signer/MultiProtocolSignerManager.ts @@ -0,0 +1,153 @@ +import { Signer } from 'ethers'; +import { Logger } from 'pino'; + +import { + ChainName, + ChainSubmissionStrategy, + MultiProvider, +} from '@hyperlane-xyz/sdk'; +import { assert, rootLogger } from '@hyperlane-xyz/utils'; + +import { ENV } from '../../../utils/env.js'; + +import { IMultiProtocolSigner } from './BaseMultiProtocolSigner.js'; +import { MultiProtocolSignerFactory } from './MultiProtocolSignerFactory.js'; + +export interface MultiProtocolSignerOptions { + logger?: Logger; + key?: string; +} + +/** + * @title MultiProtocolSignerManager + * @dev Context manager for signers across multiple protocols + */ +export class MultiProtocolSignerManager { + protected readonly signerStrategies: Map; + protected readonly signers: Map; + public readonly logger: Logger; + + constructor( + protected readonly submissionStrategy: ChainSubmissionStrategy, + protected readonly chains: ChainName[], + protected readonly multiProvider: MultiProvider, + protected readonly options: MultiProtocolSignerOptions = {}, + ) { + this.logger = + options?.logger || + rootLogger.child({ + module: 'MultiProtocolSignerManager', + }); + this.signerStrategies = new Map(); + this.signers = new Map(); + this.initializeStrategies(); + } + + /** + * @notice Sets up chain-specific signer strategies + */ + protected initializeStrategies(): void { + for (const chain of this.chains) { + const strategy = MultiProtocolSignerFactory.getSignerStrategy( + chain, + this.submissionStrategy, + this.multiProvider, + ); + this.signerStrategies.set(chain, strategy); + } + } + + /** + * @dev Configures signers for EVM chains in MultiProvider + */ + async getMultiProvider(): Promise { + for (const chain of this.chains) { + const signer = await this.initSigner(chain); + this.multiProvider.setSigner(chain, signer); + } + + return this.multiProvider; + } + + /** + * @notice Creates signer for specific chain + */ + async initSigner(chain: ChainName): Promise { + const { privateKey } = await this.resolveConfig(chain); + + const signerStrategy = this.signerStrategies.get(chain); + assert(signerStrategy, `No signer strategy found for chain ${chain}`); + + return signerStrategy.getSigner({ privateKey }); + } + + /** + * @notice Creates signers for all chains + */ + async initAllSigners(): Promise { + const signerConfigs = await this.resolveAllConfigs(); + + for (const { chain, privateKey } of signerConfigs) { + const signerStrategy = this.signerStrategies.get(chain); + if (signerStrategy) { + this.signers.set(chain, signerStrategy.getSigner({ privateKey })); + } + } + + return this.signers; + } + + /** + * @notice Resolves all chain configurations + */ + private async resolveAllConfigs(): Promise< + Array<{ chain: ChainName; privateKey: string }> + > { + return Promise.all(this.chains.map((chain) => this.resolveConfig(chain))); + } + + /** + * @notice Resolves single chain configuration + */ + private async resolveConfig( + chain: ChainName, + ): Promise<{ chain: ChainName; privateKey: string }> { + const signerStrategy = this.signerStrategies.get(chain); + assert(signerStrategy, `No signer strategy found for chain ${chain}`); + + let privateKey: string; + + if (this.options.key) { + this.logger.info( + `Using private key passed via CLI --key flag for chain ${chain}`, + ); + privateKey = this.options.key; + } else if (ENV.HYP_KEY) { + this.logger.info(`Using private key from .env for chain ${chain}`); + privateKey = ENV.HYP_KEY; + } else { + privateKey = await this.extractPrivateKey(chain, signerStrategy); + } + + return { chain, privateKey }; + } + + /** + * @notice Gets private key from strategy + */ + private async extractPrivateKey( + chain: ChainName, + signerStrategy: IMultiProtocolSigner, + ): Promise { + const strategyConfig = await signerStrategy.getSignerConfig(chain); + assert( + strategyConfig.privateKey, + `No private key found for chain ${chain}`, + ); + + this.logger.info( + `Extracting private key from strategy config/user prompt for chain ${chain}`, + ); + return strategyConfig.privateKey; + } +} diff --git a/typescript/cli/src/context/types.ts b/typescript/cli/src/context/types.ts index 6c3a17c5ff..c320ff3cac 100644 --- a/typescript/cli/src/context/types.ts +++ b/typescript/cli/src/context/types.ts @@ -6,6 +6,7 @@ import type { ChainMap, ChainMetadata, MultiProvider, + WarpCoreConfig, } from '@hyperlane-xyz/sdk'; export interface ContextSettings { @@ -16,6 +17,7 @@ export interface ContextSettings { requiresKey?: boolean; disableProxy?: boolean; skipConfirmation?: boolean; + strategyPath?: string; } export interface CommandContext { @@ -24,7 +26,10 @@ export interface CommandContext { multiProvider: MultiProvider; skipConfirmation: boolean; key?: string; - signer?: ethers.Signer; + // just for evm chains backward compatibility + signerAddress?: string; + warpCoreConfig?: WarpCoreConfig; + strategyPath?: string; } export interface WriteCommandContext extends CommandContext { diff --git a/typescript/cli/src/deploy/agent.ts b/typescript/cli/src/deploy/agent.ts index ca490fc5fb..a36955a3f3 100644 --- a/typescript/cli/src/deploy/agent.ts +++ b/typescript/cli/src/deploy/agent.ts @@ -21,6 +21,7 @@ export async function runKurtosisAgentDeploy({ relayChains?: string; agentConfigurationPath?: string; }) { + // Future works: decide what to do with this, since its handled in MultiChainResolver - AGENT_KURTOSIS mode if (!originChain) { originChain = await runSingleChainSelectionStep( context.chainMetadata, diff --git a/typescript/cli/src/deploy/core.ts b/typescript/cli/src/deploy/core.ts index 06d08b13cd..7ce8a0247c 100644 --- a/typescript/cli/src/deploy/core.ts +++ b/typescript/cli/src/deploy/core.ts @@ -43,7 +43,6 @@ export async function runCoreDeploy(params: DeployParams) { let chain = params.chain; const { - signer, isDryRun, chainMetadata, dryRunChain, @@ -62,13 +61,14 @@ export async function runCoreDeploy(params: DeployParams) { 'Select chain to connect:', ); } - let apiKeys: ChainMap = {}; if (!skipConfirmation) apiKeys = await requestAndSaveApiKeys([chain], chainMetadata, registry); + const signer = multiProvider.getSigner(chain); + const deploymentParams: DeployParams = { - context, + context: { ...context, signer }, chain, config, }; diff --git a/typescript/cli/src/deploy/utils.ts b/typescript/cli/src/deploy/utils.ts index 125e7b1e77..f5ac01a175 100644 --- a/typescript/cli/src/deploy/utils.ts +++ b/typescript/cli/src/deploy/utils.ts @@ -41,7 +41,7 @@ export async function runPreflightChecksForChains({ chainsToGasCheck?: ChainName[]; }) { log('Running pre-flight checks for chains...'); - const { signer, multiProvider } = context; + const { multiProvider } = context; if (!chains?.length) throw new Error('Empty chain selection'); for (const chain of chains) { @@ -49,15 +49,14 @@ export async function runPreflightChecksForChains({ if (!metadata) throw new Error(`No chain config found for ${chain}`); if (metadata.protocol !== ProtocolType.Ethereum) throw new Error('Only Ethereum chains are supported for now'); + const signer = multiProvider.getSigner(chain); + assertSigner(signer); + logGreen(`✅ ${chain} signer is valid`); } logGreen('✅ Chains are valid'); - assertSigner(signer); - logGreen('✅ Signer is valid'); - await nativeBalancesAreSufficient( multiProvider, - signer, chainsToGasCheck ?? chains, minGas, ); @@ -70,8 +69,13 @@ export async function runDeployPlanStep({ context: WriteCommandContext; chain: ChainName; }) { - const { signer, chainMetadata: chainMetadataMap, skipConfirmation } = context; - const address = await signer.getAddress(); + const { + chainMetadata: chainMetadataMap, + multiProvider, + skipConfirmation, + } = context; + + const address = await multiProvider.getSigner(chain).getAddress(); logBlue('\nDeployment plan'); logGray('==============='); @@ -124,7 +128,7 @@ export function isZODISMConfig(filepath: string): boolean { export async function prepareDeploy( context: WriteCommandContext, - userAddress: Address, + userAddress: Address | null, chains: ChainName[], ): Promise> { const { multiProvider, isDryRun } = context; @@ -134,7 +138,9 @@ export async function prepareDeploy( const provider = isDryRun ? getLocalProvider(ENV.ANVIL_IP_ADDR, ENV.ANVIL_PORT) : multiProvider.getProvider(chain); - const currentBalance = await provider.getBalance(userAddress); + const address = + userAddress ?? (await multiProvider.getSigner(chain).getAddress()); + const currentBalance = await provider.getBalance(address); initialBalances[chain] = currentBalance; }), ); @@ -145,7 +151,7 @@ export async function completeDeploy( context: WriteCommandContext, command: string, initialBalances: Record, - userAddress: Address, + userAddress: Address | null, chains: ChainName[], ) { const { multiProvider, isDryRun } = context; @@ -154,7 +160,9 @@ export async function completeDeploy( const provider = isDryRun ? getLocalProvider(ENV.ANVIL_IP_ADDR, ENV.ANVIL_PORT) : multiProvider.getProvider(chain); - const currentBalance = await provider.getBalance(userAddress); + const address = + userAddress ?? (await multiProvider.getSigner(chain).getAddress()); + const currentBalance = await provider.getBalance(address); const balanceDelta = initialBalances[chain].sub(currentBalance); if (isDryRun && balanceDelta.lt(0)) break; logPink( diff --git a/typescript/cli/src/deploy/warp.ts b/typescript/cli/src/deploy/warp.ts index 018244700a..e94bd709da 100644 --- a/typescript/cli/src/deploy/warp.ts +++ b/typescript/cli/src/deploy/warp.ts @@ -102,7 +102,7 @@ export async function runWarpRouteDeploy({ context: WriteCommandContext; warpRouteDeploymentConfigPath?: string; }) { - const { signer, skipConfirmation, chainMetadata, registry } = context; + const { skipConfirmation, chainMetadata, registry } = context; if ( !warpRouteDeploymentConfigPath || @@ -149,13 +149,8 @@ export async function runWarpRouteDeploy({ minGas: MINIMUM_WARP_DEPLOY_GAS, }); - const userAddress = await signer.getAddress(); + const initialBalances = await prepareDeploy(context, null, ethereumChains); - const initialBalances = await prepareDeploy( - context, - userAddress, - ethereumChains, - ); const deployedContracts = await executeDeploy(deploymentParams, apiKeys); const warpCoreConfig = await getWarpCoreConfig( @@ -165,13 +160,7 @@ export async function runWarpRouteDeploy({ await writeDeploymentArtifacts(warpCoreConfig, context); - await completeDeploy( - context, - 'warp', - initialBalances, - userAddress, - ethereumChains, - ); + await completeDeploy(context, 'warp', initialBalances, null, ethereumChains!); } async function runDeployPlanStep({ context, warpDeployConfig }: DeployParams) { diff --git a/typescript/cli/src/read/warp.ts b/typescript/cli/src/read/warp.ts index bd5d01e95e..169593c5e9 100644 --- a/typescript/cli/src/read/warp.ts +++ b/typescript/cli/src/read/warp.ts @@ -34,11 +34,13 @@ export async function runWarpRouteRead({ let addresses: ChainMap; if (symbol || warp) { - const warpCoreConfig = await getWarpCoreConfigOrExit({ - context, - warp, - symbol, - }); + const warpCoreConfig = + context.warpCoreConfig ?? // this case is be handled by MultiChainHandler.forWarpCoreConfig() interceptor + (await getWarpCoreConfigOrExit({ + context, + warp, + symbol, + })); // TODO: merge with XERC20TokenAdapter and WarpRouteReader const xerc20Limits = await Promise.all( diff --git a/typescript/cli/src/send/transfer.ts b/typescript/cli/src/send/transfer.ts index a89eb6aa99..2929b09c6e 100644 --- a/typescript/cli/src/send/transfer.ts +++ b/typescript/cli/src/send/transfer.ts @@ -40,8 +40,8 @@ export async function sendTestTransfer({ }: { context: WriteCommandContext; warpCoreConfig: WarpCoreConfig; - origin?: ChainName; - destination?: ChainName; + origin?: ChainName; // resolved in signerMiddleware + destination?: ChainName; // resolved in signerMiddleware amount: string; recipient?: string; timeoutSec: number; @@ -106,10 +106,15 @@ async function executeDelivery({ skipWaitForDelivery: boolean; selfRelay?: boolean; }) { - const { signer, multiProvider, registry } = context; + const { multiProvider, registry } = context; + const signer = multiProvider.getSigner(origin); + const recipientSigner = multiProvider.getSigner(destination); + + const recipientAddress = await recipientSigner.getAddress(); const signerAddress = await signer.getAddress(); - recipient ||= signerAddress; + + recipient ||= recipientAddress; const chainAddresses = await registry.getAddresses(); @@ -136,12 +141,11 @@ async function executeDelivery({ token = warpCore.findToken(origin, routerAddress)!; } - const senderAddress = await signer.getAddress(); const errors = await warpCore.validateTransfer({ originTokenAmount: token.amount(amount), destination, - recipient: recipient ?? senderAddress, - sender: senderAddress, + recipient, + sender: signerAddress, }); if (errors) { logRed('Error validating transfer', JSON.stringify(errors)); @@ -152,8 +156,8 @@ async function executeDelivery({ const transferTxs = await warpCore.getTransferRemoteTxs({ originTokenAmount: new TokenAmount(amount, token), destination, - sender: senderAddress, - recipient: recipient ?? senderAddress, + sender: signerAddress, + recipient, }); const txReceipts = []; @@ -172,7 +176,7 @@ async function executeDelivery({ const parsed = parseWarpRouteMessage(message.parsed.body); logBlue( - `Sent transfer from sender (${senderAddress}) on ${origin} to recipient (${recipient}) on ${destination}.`, + `Sent transfer from sender (${signerAddress}) on ${origin} to recipient (${recipient}) on ${destination}.`, ); logBlue(`Message ID: ${message.id}`); log(`Message:\n${indentYamlOrJson(yamlStringify(message, null, 2), 4)}`); diff --git a/typescript/cli/src/tests/commands/helpers.ts b/typescript/cli/src/tests/commands/helpers.ts index c6bfdc9a7c..7853815459 100644 --- a/typescript/cli/src/tests/commands/helpers.ts +++ b/typescript/cli/src/tests/commands/helpers.ts @@ -1,3 +1,4 @@ +import { ethers } from 'ethers'; import { $ } from 'zx'; import { ERC20Test__factory, ERC4626Test__factory } from '@hyperlane-xyz/core'; @@ -160,6 +161,9 @@ export async function deployToken(privateKey: string, chain: string) { key: privateKey, }); + // Future works: make signer compatible with protocol/chain stack + multiProvider.setSigner(chain, new ethers.Wallet(privateKey)); + const token = await new ERC20Test__factory( multiProvider.getSigner(chain), ).deploy('token', 'token', '100000000000000000000', 18); @@ -179,6 +183,9 @@ export async function deploy4626Vault( key: privateKey, }); + // Future works: make signer compatible with protocol/chain stack + multiProvider.setSigner(chain, new ethers.Wallet(privateKey)); + const vault = await new ERC4626Test__factory( multiProvider.getSigner(chain), ).deploy(tokenAddress, 'VAULT', 'VAULT'); diff --git a/typescript/cli/src/utils/balances.ts b/typescript/cli/src/utils/balances.ts index 4536353e57..2a6e6fcb8a 100644 --- a/typescript/cli/src/utils/balances.ts +++ b/typescript/cli/src/utils/balances.ts @@ -8,12 +8,9 @@ import { logGray, logGreen, logRed } from '../logger.js'; export async function nativeBalancesAreSufficient( multiProvider: MultiProvider, - signer: ethers.Signer, chains: ChainName[], minGas: string, ) { - const address = await signer.getAddress(); - const sufficientBalances: boolean[] = []; for (const chain of chains) { // Only Ethereum chains are supported @@ -21,7 +18,7 @@ export async function nativeBalancesAreSufficient( logGray(`Skipping balance check for non-EVM chain: ${chain}`); continue; } - + const address = multiProvider.getSigner(chain).getAddress(); const provider = multiProvider.getProvider(chain); const gasPrice = await provider.getGasPrice(); const minBalanceWei = gasPrice.mul(minGas).toString(); diff --git a/typescript/cli/src/utils/chains.ts b/typescript/cli/src/utils/chains.ts index add11203d0..7e2eaccd0a 100644 --- a/typescript/cli/src/utils/chains.ts +++ b/typescript/cli/src/utils/chains.ts @@ -171,3 +171,36 @@ function handleNewChain(chainNames: string[]) { process.exit(0); } } + +/** + * @notice Extracts chain names from a nested configuration object + * @param config Object to search for chain names + * @return Array of discovered chain names + */ +export function extractChainsFromObj(config: Record): string[] { + const chains: string[] = []; + + // Recursively search for chain/chainName fields + function findChainFields(obj: any) { + if (obj === null || typeof obj !== 'object') return; + + if (Array.isArray(obj)) { + obj.forEach((item) => findChainFields(item)); + return; + } + + if ('chain' in obj) { + chains.push(obj.chain); + } + + if ('chainName' in obj) { + chains.push(obj.chainName); + } + + // Recursively search in all nested values + Object.values(obj).forEach((value) => findChainFields(value)); + } + + findChainFields(config); + return chains; +} diff --git a/typescript/cli/src/utils/output.ts b/typescript/cli/src/utils/output.ts index 442b8a0906..2e1acfdf41 100644 --- a/typescript/cli/src/utils/output.ts +++ b/typescript/cli/src/utils/output.ts @@ -54,3 +54,50 @@ export function formatYamlViolationsOutput( return highlightedLines.join('\n'); } + +/** + * @notice Masks sensitive key with dots + * @param key Sensitive key to mask + * @return Masked key + */ +export function maskSensitiveKey(key: string): string { + if (!key) return key; + const middle = '•'.repeat(key.length); + return `${middle}`; +} + +const SENSITIVE_PATTERNS = [ + 'privatekey', + 'key', + 'secret', + 'secretkey', + 'password', +]; + +const isSensitiveKey = (key: string) => { + const lowerKey = key.toLowerCase(); + return SENSITIVE_PATTERNS.some((pattern) => lowerKey.includes(pattern)); +}; + +/** + * @notice Recursively masks sensitive data in objects + * @param obj Object with potential sensitive data + * @return Object with masked sensitive data + */ +export function maskSensitiveData(obj: any): any { + if (!obj) return obj; + + if (typeof obj === 'object') { + const masked = { ...obj }; + for (const [key, value] of Object.entries(masked)) { + if (isSensitiveKey(key) && typeof value === 'string') { + masked[key] = maskSensitiveKey(value); + } else if (typeof value === 'object') { + masked[key] = maskSensitiveData(value); + } + } + return masked; + } + + return obj; +} diff --git a/typescript/infra/config/environments/mainnet3/agent.ts b/typescript/infra/config/environments/mainnet3/agent.ts index b598a99d46..59d8386c49 100644 --- a/typescript/infra/config/environments/mainnet3/agent.ts +++ b/typescript/infra/config/environments/mainnet3/agent.ts @@ -282,8 +282,7 @@ export const hyperlaneContextAgentChainConfig: AgentChainConfig< degenchain: true, dogechain: true, duckchain: true, - // Cannot scrape Sealevel chains - eclipsemainnet: false, + eclipsemainnet: true, endurance: true, ethereum: true, everclear: true, @@ -334,8 +333,7 @@ export const hyperlaneContextAgentChainConfig: AgentChainConfig< sei: true, shibarium: true, snaxchain: true, - // Cannot scrape Sealevel chains - solanamainnet: false, + solanamainnet: true, stride: true, superseed: true, superpositionmainnet: true, diff --git a/typescript/infra/config/environments/mainnet3/chains.ts b/typescript/infra/config/environments/mainnet3/chains.ts index 66d12a8937..f0d7f66400 100644 --- a/typescript/infra/config/environments/mainnet3/chains.ts +++ b/typescript/infra/config/environments/mainnet3/chains.ts @@ -65,6 +65,16 @@ export const chainMetadataOverrides: ChainMap> = { // maxFeePerGas: 100000 * 10 ** 9, // 100,000 gwei // }, // }, + // taiko: { + // transactionOverrides: { + // gasPrice: 25 * 10 ** 7, // 0.25 gwei + // }, + // }, + // linea: { + // transactionOverrides: { + // gasPrice: 5 * 10 ** 8, // 0.5 gwei + // }, + // }, // zircuit: { // blocks: { // confirmations: 3, diff --git a/typescript/infra/config/environments/mainnet3/gasPrices.json b/typescript/infra/config/environments/mainnet3/gasPrices.json index 1009f5eee1..1c5f05b28a 100644 --- a/typescript/infra/config/environments/mainnet3/gasPrices.json +++ b/typescript/infra/config/environments/mainnet3/gasPrices.json @@ -28,7 +28,7 @@ "decimals": 9 }, "astarzkevm": { - "amount": "0.24", + "amount": "0.0988", "decimals": 9 }, "flame": { @@ -36,11 +36,11 @@ "decimals": 9 }, "avalanche": { - "amount": "25.0", + "amount": "27.735398516", "decimals": 9 }, "b3": { - "amount": "0.001000252", + "amount": "0.001000253", "decimals": 9 }, "base": { @@ -60,7 +60,7 @@ "decimals": 9 }, "boba": { - "amount": "0.001000047", + "amount": "0.001000059", "decimals": 9 }, "bsc": { @@ -140,7 +140,7 @@ "decimals": 9 }, "gnosis": { - "amount": "1.500000008", + "amount": "1.500000007", "decimals": 9 }, "gravity": { @@ -176,11 +176,11 @@ "decimals": 9 }, "lisk": { - "amount": "0.00100103", + "amount": "0.001001147", "decimals": 9 }, "lukso": { - "amount": "0.921815267", + "amount": "1.109955713", "decimals": 9 }, "lumia": { @@ -192,7 +192,7 @@ "decimals": 9 }, "mantapacific": { - "amount": "0.00300029", + "amount": "0.003000983", "decimals": 9 }, "mantle": { @@ -216,7 +216,7 @@ "decimals": 9 }, "mode": { - "amount": "0.001000252", + "amount": "0.001001363", "decimals": 9 }, "molten": { @@ -240,7 +240,7 @@ "decimals": 9 }, "optimism": { - "amount": "0.001065045", + "amount": "0.001000469", "decimals": 9 }, "orderly": { @@ -264,7 +264,7 @@ "decimals": 9 }, "prom": { - "amount": "546.0", + "amount": "51.9", "decimals": 9 }, "proofofplay": { @@ -276,7 +276,7 @@ "decimals": 9 }, "real": { - "amount": "0.04", + "amount": "0.022", "decimals": 9 }, "redstone": { @@ -296,7 +296,7 @@ "decimals": 9 }, "sei": { - "amount": "100.0", + "amount": "3.328028877", "decimals": 9 }, "shibarium": { @@ -336,7 +336,7 @@ "decimals": 9 }, "treasure": { - "amount": "10000.0", + "amount": "702.999550885", "decimals": 9 }, "unichain": { @@ -344,7 +344,7 @@ "decimals": 9 }, "vana": { - "amount": "0.002488334", + "amount": "0.002986", "decimals": 9 }, "viction": { @@ -352,7 +352,7 @@ "decimals": 9 }, "worldchain": { - "amount": "0.00100026", + "amount": "0.001000255", "decimals": 9 }, "xai": { diff --git a/typescript/infra/config/environments/mainnet3/owners.ts b/typescript/infra/config/environments/mainnet3/owners.ts index f37ac77af8..5b495d316f 100644 --- a/typescript/infra/config/environments/mainnet3/owners.ts +++ b/typescript/infra/config/environments/mainnet3/owners.ts @@ -56,6 +56,7 @@ export const safes: ChainMap
= { endurance: '0xaCD1865B262C89Fb0b50dcc8fB095330ae8F35b5', zircuit: '0x9e2fe7723b018d02cDE4f5cC1A9bC9C65b922Fc8', zeronetwork: '0xCB21F61A3c8139F18e635d45aD1e62A4A61d2c3D', + swell: '0x5F7771EA40546e2932754C263455Cb0023a55ca7', }; export const icaOwnerChain = 'ethereum'; @@ -166,6 +167,12 @@ export const icas: Partial< vana: '0x29dfa34765e29ea353FC8aB70A19e32a5578E603', bsquared: '0xd9564EaaA68A327933f758A54450D3A0531E60BB', superseed: '0x29dfa34765e29ea353FC8aB70A19e32a5578E603', + + // Dec 4, 2024 batch + // ---------------------------------------------------------- + // swell: '0xff8326468e7AaB51c53D3569cf7C45Dd54c11687', // already has a safe + lumiaprism: '0xAFfA863646D1bC74ecEC0dB1070f069Af065EBf5', + appchain: '0x4F25DFFd10A6D61C365E1a605d07B2ab0E82A7E6', } as const; export const DEPLOYER = '0xa7ECcdb9Be08178f896c26b7BbD8C3D4E844d9Ba'; diff --git a/typescript/infra/config/environments/mainnet3/tokenPrices.json b/typescript/infra/config/environments/mainnet3/tokenPrices.json index e340241632..9567a31775 100644 --- a/typescript/infra/config/environments/mainnet3/tokenPrices.json +++ b/typescript/infra/config/environments/mainnet3/tokenPrices.json @@ -1,99 +1,99 @@ { - "ancient8": "3628.85", - "alephzeroevmmainnet": "0.61841", - "apechain": "1.52", - "appchain": "3628.85", - "arbitrum": "3628.85", - "arbitrumnova": "3628.85", - "astar": "0.078025", - "astarzkevm": "3628.85", - "flame": "7.36", - "avalanche": "48.31", - "b3": "3628.85", - "base": "3628.85", - "bitlayer": "95794", - "blast": "3628.85", - "bob": "3628.85", - "boba": "3628.85", - "bsc": "639.49", - "bsquared": "95794", - "celo": "0.983257", - "cheesechain": "0.00180324", - "chilizmainnet": "0.104233", - "coredao": "1.43", - "cyber": "3628.85", - "degenchain": "0.01711559", - "dogechain": "0.421781", - "duckchain": "6.5", - "eclipsemainnet": "3628.85", - "endurance": "3.09", - "ethereum": "3628.85", - "everclear": "3628.85", - "fantom": "1.034", - "flare": "0.03388989", - "flowmainnet": "1.01", - "fraxtal": "3614.4", - "fusemainnet": "0.03486937", - "gnosis": "0.997956", - "gravity": "0.03730451", - "harmony": "0.02834153", - "immutablezkevmmainnet": "1.94", - "inevm": "30.08", - "injective": "30.08", - "kaia": "0.357047", - "kroma": "3628.85", - "linea": "3628.85", - "lisk": "3628.85", - "lukso": "3.33", - "lumia": "1.7", - "lumiaprism": "1.7", - "mantapacific": "3628.85", - "mantle": "0.888186", - "merlin": "95787", - "metal": "3628.85", - "metis": "60.01", - "mint": "3628.85", - "mode": "3628.85", - "molten": "0.284308", - "moonbeam": "0.313413", - "morph": "3628.85", - "neutron": "0.523303", - "oortmainnet": "0.255252", - "optimism": "3628.85", - "orderly": "3628.85", - "osmosis": "0.589662", - "polygon": "0.621246", - "polygonzkevm": "3628.85", - "polynomialfi": "3628.85", - "prom": "6.5", - "proofofplay": "3628.85", - "rarichain": "3628.85", + "ancient8": "3849.95", + "alephzeroevmmainnet": "0.563568", + "apechain": "1.66", + "appchain": "3849.95", + "arbitrum": "3849.95", + "arbitrumnova": "3849.95", + "astar": "0.078825", + "astarzkevm": "3849.95", + "flame": "7.62", + "avalanche": "49.4", + "b3": "3849.95", + "base": "3849.95", + "bitlayer": "98047", + "blast": "3849.95", + "bob": "3849.95", + "boba": "3849.95", + "bsc": "714.94", + "bsquared": "98047", + "celo": "0.916567", + "cheesechain": "0.0015485", + "chilizmainnet": "0.119182", + "coredao": "1.42", + "cyber": "3849.95", + "degenchain": "0.01880045", + "dogechain": "0.429424", + "duckchain": "6.4", + "eclipsemainnet": "3849.95", + "endurance": "3.21", + "ethereum": "3849.95", + "everclear": "3849.95", + "fantom": "1.23", + "flare": "0.02912373", + "flowmainnet": "1.048", + "fraxtal": "3847.8", + "fusemainnet": "0.04124996", + "gnosis": "1.001", + "gravity": "0.03951512", + "harmony": "0.03939191", + "immutablezkevmmainnet": "1.89", + "inevm": "30.43", + "injective": "30.43", + "kaia": "0.282109", + "kroma": "3849.95", + "linea": "3849.95", + "lisk": "3849.95", + "lukso": "2.81", + "lumia": "2.25", + "lumiaprism": "2.25", + "mantapacific": "3849.95", + "mantle": "1.13", + "merlin": "99320", + "metal": "3849.95", + "metis": "59.74", + "mint": "3849.95", + "mode": "3849.95", + "molten": "0.382952", + "moonbeam": "0.345164", + "morph": "3849.95", + "neutron": "0.571583", + "oortmainnet": "0.22645", + "optimism": "3849.95", + "orderly": "3849.95", + "osmosis": "0.699208", + "polygon": "0.633271", + "polygonzkevm": "3849.95", + "polynomialfi": "3849.95", + "prom": "7.16", + "proofofplay": "3849.95", + "rarichain": "3849.95", "real": "1", - "redstone": "3628.85", - "rootstockmainnet": "95652", - "sanko": "53.86", - "scroll": "3628.85", - "sei": "0.613723", - "shibarium": "0.59728", - "snaxchain": "3628.85", - "solanamainnet": "223.96", - "stride": "0.675504", - "superseed": "3628.85", - "superpositionmainnet": "3628.85", - "swell": "3628.85", - "taiko": "3628.85", + "redstone": "3849.95", + "rootstockmainnet": "98004", + "sanko": "58.27", + "scroll": "3849.95", + "sei": "0.625869", + "shibarium": "0.670964", + "snaxchain": "3849.95", + "solanamainnet": "226.39", + "stride": "0.779753", + "superseed": "3849.95", + "superpositionmainnet": "3849.95", + "swell": "3849.95", + "taiko": "3849.95", "tangle": "1", - "treasure": "0.64326", - "unichain": "3628.85", + "treasure": "0.638598", + "unichain": "3849.95", "vana": "1", - "viction": "0.479042", - "worldchain": "3628.85", - "xai": "0.37142", - "xlayer": "53.81", - "zeronetwork": "3628.85", - "zetachain": "0.819612", - "zircuit": "3628.85", - "zklink": "3628.85", - "zksync": "3628.85", - "zoramainnet": "3628.85" + "viction": "0.50166", + "worldchain": "3849.95", + "xai": "0.368066", + "xlayer": "56.38", + "zeronetwork": "3849.95", + "zetachain": "0.805386", + "zircuit": "3849.95", + "zklink": "3849.95", + "zksync": "3849.95", + "zoramainnet": "3849.95" } diff --git a/typescript/infra/config/environments/mainnet3/warp/configGetters/getEthereumVictionETHWarpConfig.ts b/typescript/infra/config/environments/mainnet3/warp/configGetters/getEthereumVictionETHWarpConfig.ts index 4617aec50e..f058edb64b 100644 --- a/typescript/infra/config/environments/mainnet3/warp/configGetters/getEthereumVictionETHWarpConfig.ts +++ b/typescript/infra/config/environments/mainnet3/warp/configGetters/getEthereumVictionETHWarpConfig.ts @@ -1,10 +1,10 @@ +import { ethers } from 'ethers'; + import { ChainMap, HypTokenRouterConfig, OwnableConfig, TokenType, - buildAggregationIsmConfigs, - defaultMultisigConfigs, } from '@hyperlane-xyz/sdk'; import { RouterConfigWithoutOwner } from '../../../../../src/config/warp.js'; @@ -13,12 +13,6 @@ export const getEthereumVictionETHWarpConfig = async ( routerConfig: ChainMap, abacusWorksEnvOwnerConfig: ChainMap, ): Promise> => { - const ismConfig = buildAggregationIsmConfigs( - 'ethereum', - ['viction'], - defaultMultisigConfigs, - ).viction; - const viction: HypTokenRouterConfig = { ...routerConfig.viction, ...abacusWorksEnvOwnerConfig.viction, @@ -28,6 +22,7 @@ export const getEthereumVictionETHWarpConfig = async ( decimals: 18, totalSupply: 0, gas: 50_000, + interchainSecurityModule: ethers.constants.AddressZero, }; const ethereum: HypTokenRouterConfig = { @@ -35,7 +30,7 @@ export const getEthereumVictionETHWarpConfig = async ( ...abacusWorksEnvOwnerConfig.ethereum, type: TokenType.native, gas: 65_000, - interchainSecurityModule: ismConfig, + interchainSecurityModule: ethers.constants.AddressZero, }; return { diff --git a/typescript/infra/config/environments/mainnet3/warp/configGetters/getEthereumVictionUSDCWarpConfig.ts b/typescript/infra/config/environments/mainnet3/warp/configGetters/getEthereumVictionUSDCWarpConfig.ts index 8ea4026b82..7bcf283573 100644 --- a/typescript/infra/config/environments/mainnet3/warp/configGetters/getEthereumVictionUSDCWarpConfig.ts +++ b/typescript/infra/config/environments/mainnet3/warp/configGetters/getEthereumVictionUSDCWarpConfig.ts @@ -1,10 +1,10 @@ +import { ethers } from 'ethers'; + import { ChainMap, HypTokenRouterConfig, OwnableConfig, TokenType, - buildAggregationIsmConfigs, - defaultMultisigConfigs, } from '@hyperlane-xyz/sdk'; import { @@ -16,13 +16,6 @@ export const getEthereumVictionUSDCWarpConfig = async ( routerConfig: ChainMap, abacusWorksEnvOwnerConfig: ChainMap, ): Promise> => { - // commit that the config was copied from https://github.com/hyperlane-xyz/hyperlane-monorepo/pull/3067/commits/7ed5b460034ea5e140c6ff86bcd6baf6ebb824c4#diff-fab5dd1a27c76e4310699c57ccf92ab6274ef0acf17e079b17270cedf4057775R109 - const ismConfig = buildAggregationIsmConfigs( - 'ethereum', - ['viction'], - defaultMultisigConfigs, - ).viction; - const viction: HypTokenRouterConfig = { ...routerConfig.viction, ...abacusWorksEnvOwnerConfig.viction, @@ -32,6 +25,7 @@ export const getEthereumVictionUSDCWarpConfig = async ( decimals: 6, totalSupply: 0, gas: 75_000, + interchainSecurityModule: ethers.constants.AddressZero, }; const ethereum: HypTokenRouterConfig = { @@ -40,7 +34,7 @@ export const getEthereumVictionUSDCWarpConfig = async ( type: TokenType.collateral, token: tokens.ethereum.USDC, gas: 65_000, - interchainSecurityModule: ismConfig, + interchainSecurityModule: ethers.constants.AddressZero, }; return { diff --git a/typescript/infra/config/environments/mainnet3/warp/configGetters/getEthereumVictionUSDTWarpConfig.ts b/typescript/infra/config/environments/mainnet3/warp/configGetters/getEthereumVictionUSDTWarpConfig.ts index 3b98d5debc..3001880aa3 100644 --- a/typescript/infra/config/environments/mainnet3/warp/configGetters/getEthereumVictionUSDTWarpConfig.ts +++ b/typescript/infra/config/environments/mainnet3/warp/configGetters/getEthereumVictionUSDTWarpConfig.ts @@ -1,10 +1,10 @@ +import { ethers } from 'ethers'; + import { ChainMap, HypTokenRouterConfig, OwnableConfig, TokenType, - buildAggregationIsmConfigs, - defaultMultisigConfigs, } from '@hyperlane-xyz/sdk'; import { @@ -16,12 +16,6 @@ export const getEthereumVictionUSDTWarpConfig = async ( routerConfig: ChainMap, abacusWorksEnvOwnerConfig: ChainMap, ): Promise> => { - const ismConfig = buildAggregationIsmConfigs( - 'ethereum', - ['viction'], - defaultMultisigConfigs, - ).viction; - const viction: HypTokenRouterConfig = { ...routerConfig.viction, ...abacusWorksEnvOwnerConfig.viction, @@ -31,6 +25,7 @@ export const getEthereumVictionUSDTWarpConfig = async ( decimals: 6, totalSupply: 0, gas: 75_000, + interchainSecurityModule: ethers.constants.AddressZero, }; const ethereum: HypTokenRouterConfig = { @@ -39,7 +34,7 @@ export const getEthereumVictionUSDTWarpConfig = async ( type: TokenType.collateral, token: tokens.ethereum.USDT, gas: 65_000, - interchainSecurityModule: ismConfig, + interchainSecurityModule: ethers.constants.AddressZero, }; return { diff --git a/typescript/infra/config/environments/testnet4/agent.ts b/typescript/infra/config/environments/testnet4/agent.ts index 4f984ebfe8..2bab1291ac 100644 --- a/typescript/infra/config/environments/testnet4/agent.ts +++ b/typescript/infra/config/environments/testnet4/agent.ts @@ -121,7 +121,6 @@ export const hyperlaneContextAgentChainConfig: AgentChainConfig< citreatestnet: true, connextsepolia: false, ecotestnet: true, - // Cannot scrape non-EVM chains eclipsetestnet: false, formtestnet: true, fuji: true, @@ -135,7 +134,6 @@ export const hyperlaneContextAgentChainConfig: AgentChainConfig< polygonamoy: true, scrollsepolia: true, sepolia: true, - // Cannot scrape non-EVM chains solanatestnet: false, soneiumtestnet: true, sonictestnet: true, diff --git a/typescript/infra/scripts/check/check-utils.ts b/typescript/infra/scripts/check/check-utils.ts index a8289671a0..ef7a2b1215 100644 --- a/typescript/infra/scripts/check/check-utils.ts +++ b/typescript/infra/scripts/check/check-utils.ts @@ -30,6 +30,7 @@ import { DeployEnvironment } from '../../src/config/environment.js'; import { HyperlaneAppGovernor } from '../../src/govern/HyperlaneAppGovernor.js'; import { HyperlaneCoreGovernor } from '../../src/govern/HyperlaneCoreGovernor.js'; import { HyperlaneHaasGovernor } from '../../src/govern/HyperlaneHaasGovernor.js'; +import { HyperlaneICAChecker } from '../../src/govern/HyperlaneICAChecker.js'; import { HyperlaneIgpGovernor } from '../../src/govern/HyperlaneIgpGovernor.js'; import { ProxiedRouterGovernor } from '../../src/govern/ProxiedRouterGovernor.js'; import { Role } from '../../src/roles.js'; @@ -148,7 +149,7 @@ export async function getGovernor( governor = new ProxiedRouterGovernor(checker); } else if (module === Modules.HAAS) { chainsToSkip.forEach((chain) => delete routerConfig[chain]); - const icaChecker = new InterchainAccountChecker( + const icaChecker = new HyperlaneICAChecker( multiProvider, ica, objFilter( diff --git a/typescript/infra/scripts/safes/parse-txs.ts b/typescript/infra/scripts/safes/parse-txs.ts index ac897cd279..21e8f87eed 100644 --- a/typescript/infra/scripts/safes/parse-txs.ts +++ b/typescript/infra/scripts/safes/parse-txs.ts @@ -1,7 +1,12 @@ import { BigNumber } from 'ethers'; import { AnnotatedEV5Transaction } from '@hyperlane-xyz/sdk'; -import { stringifyObject } from '@hyperlane-xyz/utils'; +import { + LogFormat, + LogLevel, + configureRootLogger, + stringifyObject, +} from '@hyperlane-xyz/utils'; import { GovernTransactionReader } from '../../src/tx/govern-transaction-reader.js'; import { getSafeTx } from '../../src/utils/safe.js'; @@ -13,6 +18,8 @@ async function main() { withChainsRequired(getArgs()), ).argv; + configureRootLogger(LogFormat.Pretty, LogLevel.Info); + const config = getEnvironmentConfig(environment); const multiProvider = await config.getMultiProvider(); const { chainAddresses } = await getHyperlaneCore(environment, multiProvider); diff --git a/typescript/infra/src/govern/HyperlaneAppGovernor.ts b/typescript/infra/src/govern/HyperlaneAppGovernor.ts index a704b3d9b8..1d7b5167e3 100644 --- a/typescript/infra/src/govern/HyperlaneAppGovernor.ts +++ b/typescript/infra/src/govern/HyperlaneAppGovernor.ts @@ -486,6 +486,20 @@ export abstract class HyperlaneAppGovernor< await this.checkSubmitterBalance(chain, submitterAddress, call.value); } + // Check if the submitter is the owner of the contract + try { + const ownable = Ownable__factory.connect(call.to, signer); + const owner = await ownable.owner(); + const isOwner = eqAddress(owner, submitterAddress); + + if (!isOwner) { + return false; + } + } catch { + // If the contract does not implement Ownable, just continue + // with the next check. + } + // Check if the transaction has additional success criteria if ( additionalTxSuccessCriteria && diff --git a/typescript/infra/src/govern/HyperlaneHaasGovernor.ts b/typescript/infra/src/govern/HyperlaneHaasGovernor.ts index 921649c5b9..6fa6ebcd18 100644 --- a/typescript/infra/src/govern/HyperlaneHaasGovernor.ts +++ b/typescript/infra/src/govern/HyperlaneHaasGovernor.ts @@ -5,7 +5,6 @@ import { HyperlaneCore, HyperlaneCoreChecker, InterchainAccount, - InterchainAccountChecker, } from '@hyperlane-xyz/sdk'; import { @@ -13,6 +12,7 @@ import { HyperlaneAppGovernor, } from './HyperlaneAppGovernor.js'; import { HyperlaneCoreGovernor } from './HyperlaneCoreGovernor.js'; +import { HyperlaneICAChecker } from './HyperlaneICAChecker.js'; import { ProxiedRouterGovernor } from './ProxiedRouterGovernor.js'; export class HyperlaneHaasGovernor extends HyperlaneAppGovernor< @@ -24,7 +24,7 @@ export class HyperlaneHaasGovernor extends HyperlaneAppGovernor< constructor( ica: InterchainAccount, - private readonly icaChecker: InterchainAccountChecker, + private readonly icaChecker: HyperlaneICAChecker, private readonly coreChecker: HyperlaneCoreChecker, ) { super(coreChecker, ica); diff --git a/typescript/infra/src/govern/HyperlaneICAChecker.ts b/typescript/infra/src/govern/HyperlaneICAChecker.ts new file mode 100644 index 0000000000..cb72bd73a6 --- /dev/null +++ b/typescript/infra/src/govern/HyperlaneICAChecker.ts @@ -0,0 +1,63 @@ +import { + ChainMap, + ChainName, + InterchainAccountChecker, + RouterViolation, + RouterViolationType, +} from '@hyperlane-xyz/sdk'; +import { AddressBytes32, addressToBytes32 } from '@hyperlane-xyz/utils'; + +export class HyperlaneICAChecker extends InterchainAccountChecker { + /* + * Check that the Ethereum router is enrolled correctly, + * and that remote chains have the correct router enrolled. + */ + async checkEthRouterEnrollment(chain: ChainName): Promise { + // If the chain is Ethereum, do the regular full check + if (chain === 'ethereum') { + return super.checkEnrolledRouters(chain); + } + + // Get the Ethereum router address and domain id + const ethereumRouterAddress = this.app.routerAddress('ethereum'); + const ethereumDomainId = this.multiProvider.getDomainId('ethereum'); + // Get the expected Ethereum router address (with padding) + const expectedRouter = addressToBytes32(ethereumRouterAddress); + + // Get the actual Ethereum router address + const router = this.app.router(this.app.getContracts(chain)); + const actualRouter = await router.routers(ethereumDomainId); + + // Check if the actual router address matches the expected router address + if (actualRouter !== expectedRouter) { + const currentRouters: ChainMap = { ethereum: actualRouter }; + const expectedRouters: ChainMap = { + ethereum: expectedRouter, + }; + const routerDiff: ChainMap<{ + actual: AddressBytes32; + expected: AddressBytes32; + }> = { + ethereum: { actual: actualRouter, expected: expectedRouter }, + }; + + const violation: RouterViolation = { + chain, + type: RouterViolationType.EnrolledRouter, + contract: router, + actual: currentRouters, + expected: expectedRouters, + routerDiff, + description: `Ethereum router is not enrolled correctly`, + }; + this.addViolation(violation); + } + } + + async checkChain(chain: ChainName): Promise { + await this.checkMailboxClient(chain); + await this.checkEthRouterEnrollment(chain); + await this.checkProxiedContracts(chain); + await this.checkOwnership(chain); + } +} diff --git a/typescript/infra/src/tx/govern-transaction-reader.ts b/typescript/infra/src/tx/govern-transaction-reader.ts index fadde0f87e..ef82ab1e1a 100644 --- a/typescript/infra/src/tx/govern-transaction-reader.ts +++ b/typescript/infra/src/tx/govern-transaction-reader.ts @@ -8,7 +8,7 @@ import assert from 'assert'; import chalk from 'chalk'; import { BigNumber, ethers } from 'ethers'; -import { TokenRouter__factory } from '@hyperlane-xyz/core'; +import { ProxyAdmin__factory, TokenRouter__factory } from '@hyperlane-xyz/core'; import { AnnotatedEV5Transaction, ChainMap, @@ -123,6 +123,11 @@ export class GovernTransactionReader { return this.readMailboxTransaction(chain, tx); } + // If it's to a Proxy Admin + if (this.isProxyAdminTransaction(chain, tx)) { + return this.readProxyAdminTransaction(chain, tx); + } + // If it's a Multisend if (await this.isMultisendTransaction(chain, tx)) { return this.readMultisendTransaction(chain, tx); @@ -154,6 +159,7 @@ export class GovernTransactionReader { ): boolean { return ( tx.to !== undefined && + this.warpRouteIndex[chain] !== undefined && this.warpRouteIndex[chain][tx.to.toLowerCase()] !== undefined ); } @@ -381,6 +387,43 @@ export class GovernTransactionReader { }; } + private async readProxyAdminTransaction( + chain: ChainName, + tx: AnnotatedEV5Transaction, + ): Promise { + if (!tx.data) { + throw new Error('⚠️ No data in proxyAdmin transaction'); + } + + const proxyAdminInterface = ProxyAdmin__factory.createInterface(); + const decoded = proxyAdminInterface.parseTransaction({ + data: tx.data, + value: tx.value, + }); + + const args = formatFunctionFragmentArgs( + decoded.args, + decoded.functionFragment, + ); + + let insight; + if ( + decoded.functionFragment.name === + proxyAdminInterface.functions['transferOwnership(address)'].name + ) { + const [newOwner] = decoded.args; + insight = `Transfer ownership to ${newOwner}`; + } + + return { + chain, + to: `Proxy Admin (${chain} ${this.chainAddresses[chain].proxyAdmin})`, + insight, + signature: decoded.signature, + args, + }; + } + private ismDerivationsInProgress: ChainMap = {}; private async deriveIsmConfig( @@ -600,6 +643,16 @@ export class GovernTransactionReader { ); } + isProxyAdminTransaction( + chain: ChainName, + tx: AnnotatedEV5Transaction, + ): boolean { + return ( + tx.to !== undefined && + eqAddress(tx.to, this.chainAddresses[chain].proxyAdmin) + ); + } + async isMultisendTransaction( chain: ChainName, tx: AnnotatedEV5Transaction, diff --git a/typescript/sdk/src/consts/multisigIsm.ts b/typescript/sdk/src/consts/multisigIsm.ts index eaa675b327..867059be1d 100644 --- a/typescript/sdk/src/consts/multisigIsm.ts +++ b/typescript/sdk/src/consts/multisigIsm.ts @@ -45,7 +45,7 @@ export const defaultMultisigConfigs: ChainMap = { }, alephzeroevmmainnet: { - threshold: 2, + threshold: 3, validators: [ { address: '0x33f20e6e775747d60301c6ea1c50e51f0389740c', @@ -53,6 +53,10 @@ export const defaultMultisigConfigs: ChainMap = { }, DEFAULT_MERKLY_VALIDATOR, DEFAULT_MITOSIS_VALIDATOR, + { + address: '0xCbf382214825F8c2f347dd4f23F0aDFaFad55dAa', + alias: 'Aleph Zero', + }, ], }, @@ -115,12 +119,14 @@ export const defaultMultisigConfigs: ChainMap = { }, appchain: { - threshold: 1, + threshold: 2, validators: [ { address: '0x0531251bbadc1f9f19ccce3ca6b3f79f08eae1be', alias: AW_VALIDATOR_ALIAS, }, + DEFAULT_MERKLY_VALIDATOR, + DEFAULT_MITOSIS_VALIDATOR, ], }, @@ -888,6 +894,10 @@ export const defaultMultisigConfigs: ChainMap = { address: '0xf0da628f3fb71652d48260bad4691054045832ce', alias: 'Luganodes', }, + { + address: '0xead4141b6ea149901ce4f4b556953f66d04b1d0c', + alias: 'Lisk', + }, ], }, @@ -919,12 +929,14 @@ export const defaultMultisigConfigs: ChainMap = { }, lumiaprism: { - threshold: 1, + threshold: 2, validators: [ { address: '0xb69731640ffd4338a2c9358a935b0274c6463f85', alias: AW_VALIDATOR_ALIAS, }, + DEFAULT_MERKLY_VALIDATOR, + DEFAULT_MITOSIS_VALIDATOR, ], }, @@ -1583,12 +1595,18 @@ export const defaultMultisigConfigs: ChainMap = { }, swell: { - threshold: 1, + threshold: 3, validators: [ { address: '0x4f51e4f4c7fb45d82f91568480a1a2cfb69216ed', alias: AW_VALIDATOR_ALIAS, }, + { + address: '0x9eadf9217be22d9878e0e464727a2176d5c69ff8', + alias: 'Luganodes', + }, + DEFAULT_MERKLY_VALIDATOR, + DEFAULT_MITOSIS_VALIDATOR, ], }, @@ -1624,12 +1642,21 @@ export const defaultMultisigConfigs: ChainMap = { }, treasure: { - threshold: 1, + threshold: 3, validators: [ { address: '0x6ad994819185553e8baa01533f0cd2c7cadfe6cc', alias: AW_VALIDATOR_ALIAS, }, + { + address: '0x278460fa51ff448eb53ffa62951b4b8e3e8f74e3', + alias: 'P2P', + }, + { + address: '0xe92ff70bb463e2aa93426fd2ba51afc39567d426', + alias: 'Treasure', + }, + DEFAULT_MITOSIS_VALIDATOR, ], }, @@ -1666,7 +1693,7 @@ export const defaultMultisigConfigs: ChainMap = { }, vana: { - threshold: 2, + threshold: 3, validators: [ { address: '0xfdf3b0dfd4b822d10cacb15c8ae945ea269e7534', @@ -1674,6 +1701,10 @@ export const defaultMultisigConfigs: ChainMap = { }, DEFAULT_MERKLY_VALIDATOR, DEFAULT_MITOSIS_VALIDATOR, + { + address: '0xba2f4f89cae6863d8b49e4ca0208ed48ad9ac354', + alias: 'P2P', + }, ], }, @@ -1782,12 +1813,14 @@ export const defaultMultisigConfigs: ChainMap = { }, zklink: { - threshold: 1, + threshold: 2, validators: [ { address: '0x217a8cb4789fc45abf56cb6e2ca96f251a5ac181', alias: AW_VALIDATOR_ALIAS, }, + DEFAULT_MERKLY_VALIDATOR, + DEFAULT_MITOSIS_VALIDATOR, ], }, diff --git a/typescript/sdk/src/providers/transactions/submitter/ethersV5/types.ts b/typescript/sdk/src/providers/transactions/submitter/ethersV5/types.ts index d1e1c7a90c..bf0d29d540 100644 --- a/typescript/sdk/src/providers/transactions/submitter/ethersV5/types.ts +++ b/typescript/sdk/src/providers/transactions/submitter/ethersV5/types.ts @@ -23,6 +23,8 @@ export type EV5GnosisSafeTxBuilderProps = z.infer< export const EV5JsonRpcTxSubmitterPropsSchema = z.object({ chain: ZChainName, + userAddress: ZHash.optional(), + privateKey: ZHash.optional(), }); export type EV5JsonRpcTxSubmitterProps = z.infer< diff --git a/typescript/utils/src/addresses.ts b/typescript/utils/src/addresses.ts index 29a35b6b88..a244c810ba 100644 --- a/typescript/utils/src/addresses.ts +++ b/typescript/utils/src/addresses.ts @@ -1,6 +1,6 @@ import { fromBech32, normalizeBech32, toBech32 } from '@cosmjs/encoding'; import { PublicKey } from '@solana/web3.js'; -import { utils as ethersUtils } from 'ethers'; +import { Wallet, utils as ethersUtils } from 'ethers'; import { isNullish } from './typeof.js'; import { Address, HexString, ProtocolType } from './types.js'; @@ -380,3 +380,11 @@ export function ensure0x(hexstr: string) { export function strip0x(hexstr: string) { return hexstr.startsWith('0x') ? hexstr.slice(2) : hexstr; } + +export function isPrivateKeyEvm(privateKey: string): boolean { + try { + return new Wallet(privateKey).privateKey === privateKey; + } catch { + throw new Error('Provided Private Key is not EVM compatible!'); + } +} diff --git a/typescript/utils/src/index.ts b/typescript/utils/src/index.ts index 8314418631..f4bd9779cb 100644 --- a/typescript/utils/src/index.ts +++ b/typescript/utils/src/index.ts @@ -26,6 +26,7 @@ export { isValidAddressCosmos, isValidAddressEvm, isValidAddressSealevel, + isPrivateKeyEvm, isValidTransactionHash, isValidTransactionHashCosmos, isValidTransactionHashEvm,