diff --git a/typescript/cli/cli.ts b/typescript/cli/cli.ts index 77be0b86f8..672d84247d 100644 --- a/typescript/cli/cli.ts +++ b/typescript/cli/cli.ts @@ -24,10 +24,11 @@ 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'; @@ -55,6 +56,7 @@ try { configureLogger(argv.log as LogFormat, argv.verbosity as LogLevel); }, contextMiddleware, + signerMiddleware, ]) .command(avsCommand) .command(configCommand) @@ -69,6 +71,7 @@ try { .command(submitCommand) .command(validatorCommand) .command(warpCommand) + .command(strategyCommand) .version(VERSION) .demandCommand() .strict() diff --git a/typescript/cli/src/commands/options.ts b/typescript/cli/src/commands/options.ts index f23194c804..3ffffa410f 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 = './configs/default-strategy.yaml'; export const warpDeploymentConfigCommandOption: Options = { type: 'string', diff --git a/typescript/cli/src/commands/strategy.ts b/typescript/cli/src/commands/strategy.ts new file mode 100644 index 0000000000..2425389129 --- /dev/null +++ b/typescript/cli/src/commands/strategy.ts @@ -0,0 +1,207 @@ +// import { input, select } from '@inquirer/prompts'; +import { input, select } from '@inquirer/prompts'; +import { ethers } from 'ethers'; +import { stringify as yamlStringify } from 'yaml'; +import { CommandModule } from 'yargs'; + +import { + ChainSubmissionStrategy, + ChainSubmissionStrategySchema, + TxSubmitterType, +} from '@hyperlane-xyz/sdk'; +import { ProtocolType, assert } from '@hyperlane-xyz/utils'; + +import { CommandModuleWithWriteContext } from '../context/types.js'; +import { + errorRed, + log, + logBlue, + logCommandHeader, + logGreen, +} from '../logger.js'; +import { runSingleChainSelectionStep } from '../utils/chains.js'; +import { + indentYamlOrJson, + mergeYamlOrJson, + readYamlOrJson, + writeYamlOrJson, +} from '../utils/files.js'; + +import { + DEFAULT_STRATEGY_CONFIG_PATH, + outputFileCommandOption, +} from './options.js'; + +/** + * Parent command + */ +export const strategyCommand: CommandModule = { + command: 'strategy', + describe: 'Manage Hyperlane deployment strategies', + builder: (yargs) => yargs.command(init).version(false).demandCommand(), + handler: () => log('Command required'), +}; + +export const init: CommandModuleWithWriteContext<{ + chain: string; + config: string; +}> = { + command: 'init', + describe: 'Initiates strategy', + builder: { + config: outputFileCommandOption( + DEFAULT_STRATEGY_CONFIG_PATH, + false, + 'The path to output a Key Config JSON or YAML file.', + ), + type: { + type: 'string', + description: + 'Type of submitter (jsonRpc, impersonatedAccount, gnosisSafe, gnosisSafeTxBuilder)', + }, + safeAddress: { + type: 'string', + description: + 'Safe address (required for gnosisSafe and gnosisSafeTxBuilder types)', + }, + userAddress: { + type: 'string', + description: 'User address (required for impersonatedAccount type)', + }, + }, + handler: async ({ + context, + type: inputType, + safeAddress: inputSafeAddress, + userAddress: inputUserAddress, + }) => { + logCommandHeader(`Hyperlane Key Init`); + + try { + await readYamlOrJson(DEFAULT_STRATEGY_CONFIG_PATH); + } catch (e) { + writeYamlOrJson(DEFAULT_STRATEGY_CONFIG_PATH, {}, 'yaml'); + } + + const chain = await runSingleChainSelectionStep(context.chainMetadata); + const chainProtocol = context.chainMetadata[chain].protocol; + assert(chainProtocol === ProtocolType.Ethereum, 'Incompatible protocol'); + + // If type wasn't provided via command line, prompt for it + const type = + inputType || + (await select({ + message: 'Enter the type of submitter', + choices: Object.values(TxSubmitterType).map((value) => ({ + name: value, + value: value, + })), + })); + + let submitter: any = { + type: type, + }; + + // Configure submitter based on type + switch (type) { + case TxSubmitterType.JSON_RPC: + const privateKey = await input({ + message: 'Enter your private key', + validate: (pk) => isValidPrivateKey(pk), + }); + submitter.privateKey = privateKey; + break; + + case TxSubmitterType.IMPERSONATED_ACCOUNT: + const userAddress = + inputUserAddress || + (await input({ + message: 'Enter the user address to impersonate', + validate: (address) => { + try { + return ethers.utils.isAddress(address) + ? true + : 'Invalid Ethereum address'; + } catch { + return 'Invalid Ethereum address'; + } + }, + })); + assert( + userAddress, + 'User address is required for impersonated account', + ); + submitter.userAddress = userAddress; + break; + + case TxSubmitterType.GNOSIS_SAFE: + case TxSubmitterType.GNOSIS_TX_BUILDER: + const safeAddress = + inputSafeAddress || + (await input({ + message: 'Enter the Safe address', + validate: (address) => { + try { + return ethers.utils.isAddress(address) + ? true + : 'Invalid Safe address'; + } catch { + return 'Invalid Safe address'; + } + }, + })); + assert(safeAddress, 'Safe address is required for Gnosis Safe'); + + submitter = { + type: type, + chain: chain, + safeAddress: safeAddress, + }; + + if (type === TxSubmitterType.GNOSIS_TX_BUILDER) { + const version = await input({ + message: 'Enter the Safe version (default: 1.0)', + default: '1.0', + }); + submitter.version = version; + } + break; + + default: + throw new Error(`Unsupported submitter type: ${type}`); + } + + let result: ChainSubmissionStrategy = { + [chain]: { + submitter: submitter, + }, + }; + + try { + const strategyConfig = ChainSubmissionStrategySchema.parse(result); + logBlue( + `Strategy config is valid, writing to file ${DEFAULT_STRATEGY_CONFIG_PATH}:\n`, + ); + log(indentYamlOrJson(yamlStringify(strategyConfig, null, 2), 4)); + + mergeYamlOrJson(DEFAULT_STRATEGY_CONFIG_PATH, strategyConfig); + logGreen('✅ Successfully created new key config.'); + } catch (e) { + errorRed( + `Key config is invalid, please check the submitter configuration.`, + ); + throw e; + } + process.exit(0); + }, +}; + +function isValidPrivateKey(privateKey: string): boolean { + try { + // Attempt to create a Wallet instance with the private key + const wallet = new ethers.Wallet(privateKey); + return wallet.privateKey === privateKey; + } catch (error) { + return false; + } +} diff --git a/typescript/cli/src/commands/warp.ts b/typescript/cli/src/commands/warp.ts index b7fb456243..388c478b03 100644 --- a/typescript/cli/src/commands/warp.ts +++ b/typescript/cli/src/commands/warp.ts @@ -138,7 +138,6 @@ export const deploy: CommandModuleWithWriteContext<{ try { await runWarpRouteDeploy({ context, - warpRouteDeploymentConfigPath: config, }); } catch (error: any) { evaluateIfDryRunFailure(error, dryRun); diff --git a/typescript/cli/src/config/strategy.ts b/typescript/cli/src/config/strategy.ts new file mode 100644 index 0000000000..36925250a6 --- /dev/null +++ b/typescript/cli/src/config/strategy.ts @@ -0,0 +1,16 @@ +import { + ChainSubmissionStrategy, + ChainSubmissionStrategySchema, +} from '@hyperlane-xyz/sdk'; + +import { readYamlOrJson } from '../utils/files.js'; + +export async function readDefaultStrategyConfig( + filePath: string, +): Promise { + let config = readYamlOrJson(filePath); + if (!config) + throw new Error(`No default strategy config found at ${filePath}`); + + return ChainSubmissionStrategySchema.parse(config); +} diff --git a/typescript/cli/src/context/context.ts b/typescript/cli/src/context/context.ts index 30390f4b40..f641157cf3 100644 --- a/typescript/cli/src/context/context.ts +++ b/typescript/cli/src/context/context.ts @@ -16,14 +16,20 @@ import { } from '@hyperlane-xyz/sdk'; import { isHttpsUrl, isNullish, rootLogger } from '@hyperlane-xyz/utils'; +import { + DEFAULT_STRATEGY_CONFIG_PATH, // DEFAULT_WARP_ROUTE_DEPLOYMENT_CONFIG_PATH, +} from '../commands/options.js'; import { isSignCommand } from '../commands/signCommands.js'; +import { readDefaultStrategyConfig } from '../config/strategy.js'; +// import { readWarpRouteDeployConfig } from '../config/warp.js'; import { PROXY_DEPLOYED_URL } from '../consts.js'; import { forkNetworkToMultiProvider, verifyAnvil } from '../deploy/dry-run.js'; import { logBlue } from '../logger.js'; import { runSingleChainSelectionStep } from '../utils/chains.js'; import { detectAndConfirmOrPrompt } from '../utils/input.js'; -import { getImpersonatedSigner, getSigner } from '../utils/keys.js'; +import { getImpersonatedSigner } from '../utils/keys.js'; +import { SignerStrategyFactory } from './strategies/signer/signer.js'; import { CommandContext, ContextSettings, @@ -32,6 +38,7 @@ import { export async function contextMiddleware(argv: Record) { const isDryRun = !isNullish(argv.dryRun); + const requiresKey = isSignCommand(argv); const settings: ContextSettings = { registryUri: argv.registry, @@ -42,16 +49,44 @@ export async function contextMiddleware(argv: Record) { disableProxy: argv.disableProxy, skipConfirmation: argv.yes, }; + if (!isDryRun && settings.fromAddress) throw new Error( "'--from-address' or '-f' should only be used for dry-runs", ); + const context = isDryRun ? await getDryRunContext(settings, argv.dryRun) : await getContext(settings); argv.context = context; } +export async function signerMiddleware(argv: Record) { + const { requiresKey, multiProvider } = argv.context; + + if (!requiresKey) return argv; + + const defaultStrategy = await readDefaultStrategyConfig( + argv.strategy || DEFAULT_STRATEGY_CONFIG_PATH, + ); + + // Select appropriate strategy + const strategy = SignerStrategyFactory.createStrategy(argv); + + // Determine chains + const chains = await strategy.determineChains(argv); + + // Create context manager + const contextManager = strategy.createContextManager(chains, defaultStrategy); + + // Figure out if a command requires --origin and --destination + + // Configure signers + await strategy.configureSigners(argv, multiProvider, contextManager); + + return argv; +} + /** * Retrieves context for the user-selected command * @returns context for the current command @@ -63,21 +98,18 @@ export async function getContext({ requiresKey, skipConfirmation, disableProxy = false, + signers, }: ContextSettings): Promise { const registry = getRegistry(registryUri, registryOverrideUri, !disableProxy); - - let signer: ethers.Wallet | undefined = undefined; - if (key || requiresKey) { - ({ key, signer } = await getSigner({ key, skipConfirmation })); - } - const multiProvider = await getMultiProvider(registry, signer); + const multiProvider = await getMultiProvider(registry); return { registry, + requiresKey, chainMetadata: multiProvider.metadata, multiProvider, key, - signer, + signers, skipConfirmation: !!skipConfirmation, } as CommandContext; } diff --git a/typescript/cli/src/context/manager/ContextManager.ts b/typescript/cli/src/context/manager/ContextManager.ts new file mode 100644 index 0000000000..baa68e5ee7 --- /dev/null +++ b/typescript/cli/src/context/manager/ContextManager.ts @@ -0,0 +1,44 @@ +import { Signer } from 'ethers'; + +import { ChainName, TxSubmitterType } from '@hyperlane-xyz/sdk'; + +import { ISubmitterStrategy } from '../strategies/submitter/SubmitterStrategy.js'; +import { SubmitterStrategyFactory } from '../strategies/submitter/SubmitterStrategyFactory.js'; + +export class ContextManager { + private strategy: ISubmitterStrategy; + + constructor( + defaultStrategy: any, + private chains: ChainName[], + submitterType: TxSubmitterType, + ) { + this.strategy = SubmitterStrategyFactory.createStrategy( + submitterType, + defaultStrategy, + ); + } + + async getChainKeys(): Promise< + Array<{ chainName: ChainName; privateKey: string }> + > { + const chainKeys = await Promise.all( + this.chains.map(async (chain) => ({ + chainName: chain, + privateKey: await this.strategy.getPrivateKey(chain), + })), + ); + + return chainKeys; + } + + async getSigners(): Promise> { + const chainKeys = await this.getChainKeys(); + return Object.fromEntries( + chainKeys.map(({ chainName, privateKey }) => [ + chainName, + this.strategy.getSigner(privateKey), + ]), + ); + } +} diff --git a/typescript/cli/src/context/strategies/signer/signer.ts b/typescript/cli/src/context/strategies/signer/signer.ts new file mode 100644 index 0000000000..3da8bf9c70 --- /dev/null +++ b/typescript/cli/src/context/strategies/signer/signer.ts @@ -0,0 +1,192 @@ +import { confirm } from '@inquirer/prompts'; + +import { ChainName, MultiProvider, TxSubmitterType } from '@hyperlane-xyz/sdk'; + +import { DEFAULT_WARP_ROUTE_DEPLOYMENT_CONFIG_PATH } from '../../../commands/options.js'; +import { readWarpRouteDeployConfig } from '../../../config/warp.js'; +import { runSingleChainSelectionStep } from '../../../utils/chains.js'; +import { isFile, runFileSelectionStep } from '../../../utils/files.js'; +import { ContextManager } from '../../manager/ContextManager.js'; + +export interface WarpDeployContextResult { + warpRouteConfig: Record; + chains: ChainName[]; +} + +export interface SignerStrategy { + /** + * Determines the chains to be used for signing + * @param argv Command arguments + * @returns Array of chain names + */ + determineChains(argv: Record): Promise; + + /** + * Creates a context manager for the selected chains + * @param chains Selected chains + * @param defaultStrategy Default strategy configuration + * @returns ContextManager instance + */ + createContextManager( + chains: ChainName[], + defaultStrategy: any, + ): ContextManager; + + /** + * Configures signers for the multi-provider + * @param argv Command arguments + * @param multiProvider MultiProvider instance + * @param contextManager ContextManager instance + */ + configureSigners( + argv: Record, + multiProvider: MultiProvider, + contextManager: ContextManager, + ): Promise; +} + +export class SingleChainSignerStrategy implements SignerStrategy { + async determineChains(argv: Record): Promise { + const chain: ChainName = + argv.chain || + (await runSingleChainSelectionStep( + argv.context.chainMetadata, + 'Select chain to connect:', + )); + + argv.chain = chain; + return [chain]; // Explicitly return as single-item array + } + + createContextManager( + chains: ChainName[], + defaultStrategy: any, + ): ContextManager { + return new ContextManager( + defaultStrategy, + chains, + TxSubmitterType.JSON_RPC, + ); + } + + async configureSigners( + argv: Record, + multiProvider: MultiProvider, + contextManager: ContextManager, + ): Promise { + const signers = await contextManager.getSigners(); + multiProvider.setSigners(signers); + argv.context.multiProvider = multiProvider; + argv.contextManager = contextManager; + } +} + +export class WarpDeploySignerStrategy implements SignerStrategy { + async determineChains(argv: Record): Promise { + const { warpRouteConfig, chains } = await getWarpDeployContext({ + configPath: argv.wd || DEFAULT_WARP_ROUTE_DEPLOYMENT_CONFIG_PATH, + skipConfirmation: argv.skipConfirmation, + context: argv.context, + }); + + argv.context.warpRouteConfig = warpRouteConfig; + argv.context.chains = chains; + return chains; + } + + createContextManager( + chains: ChainName[], + defaultStrategy: any, + ): ContextManager { + return new ContextManager( + defaultStrategy, + chains, + TxSubmitterType.JSON_RPC, + ); + } + + async configureSigners( + argv: Record, + multiProvider: MultiProvider, + contextManager: ContextManager, + ): Promise { + const signers = await contextManager.getSigners(); + multiProvider.setSigners(signers); + argv.context.multiProvider = multiProvider; + argv.contextManager = contextManager; + } +} + +export class SignerStrategyFactory { + static createStrategy(argv: Record): SignerStrategy { + if ( + argv._[0] === 'warp' && + (argv._[1] === 'deploy' || argv._[1] === 'send') + ) { + return new WarpDeploySignerStrategy(); + } + + if (argv._[0] === 'send') { + // You might want to create a specific multi-chain send strategy + return new WarpDeploySignerStrategy(); + } + + return new SingleChainSignerStrategy(); + } +} + +export async function getWarpDeployContext({ + configPath = DEFAULT_WARP_ROUTE_DEPLOYMENT_CONFIG_PATH, + skipConfirmation = false, + context, +}: { + configPath?: string; + skipConfirmation?: boolean; + context: any; +}): Promise { + // Validate config path + if (!configPath || !isFile(configPath)) { + if (skipConfirmation) { + throw new Error('Warp route deployment config is required'); + } + + // Interactive file selection if no path provided + configPath = await runFileSelectionStep( + './configs', + 'Warp route deployment config', + 'warp', + ); + } else { + console.log(`Using warp route deployment config at ${configPath}`); + } + + // Read warp route deployment configuration + const warpRouteConfig = await readWarpRouteDeployConfig(configPath, context); + + // Extract chains from configuration + const chains = Object.keys(warpRouteConfig) as ChainName[]; + + // Validate chains + if (chains.length === 0) { + throw new Error('No chains found in warp route deployment config'); + } + + // Optional: Confirm multi-chain deployment + if (!skipConfirmation && chains.length > 1) { + const confirmMultiChain = await confirm({ + message: `Deploy warp route across ${chains.length} chains: ${chains.join( + ', ', + )}?`, + default: true, + }); + + if (!confirmMultiChain) { + throw new Error('Deployment cancelled by user'); + } + } + + return { + warpRouteConfig, + chains, + }; +} diff --git a/typescript/cli/src/context/strategies/submitter/GnosisSafeStrategy.ts b/typescript/cli/src/context/strategies/submitter/GnosisSafeStrategy.ts new file mode 100644 index 0000000000..8369be9b00 --- /dev/null +++ b/typescript/cli/src/context/strategies/submitter/GnosisSafeStrategy.ts @@ -0,0 +1,15 @@ +import { TxSubmitterType } from '@hyperlane-xyz/sdk'; +import { ChainName } from '@hyperlane-xyz/sdk'; + +import { BaseSubmitterStrategy } from './SubmitterStrategy.js'; + +export class GnosisSafeStrategy extends BaseSubmitterStrategy { + async getPrivateKey(chain: ChainName): Promise { + // Implement Gnosis Safe specific logic + throw new Error('Not implemented'); + } + + getType(): TxSubmitterType { + return TxSubmitterType.GNOSIS_SAFE; + } +} diff --git a/typescript/cli/src/context/strategies/submitter/JsonRpcStrategy.ts b/typescript/cli/src/context/strategies/submitter/JsonRpcStrategy.ts new file mode 100644 index 0000000000..55ca636931 --- /dev/null +++ b/typescript/cli/src/context/strategies/submitter/JsonRpcStrategy.ts @@ -0,0 +1,24 @@ +import { password } from '@inquirer/prompts'; + +import { TxSubmitterType } from '@hyperlane-xyz/sdk'; +import { ChainName } from '@hyperlane-xyz/sdk'; + +import { BaseSubmitterStrategy } from './SubmitterStrategy.js'; + +export class JsonRpcStrategy extends BaseSubmitterStrategy { + async getPrivateKey(chain: ChainName): Promise { + let pk = this.config[chain]?.submitter?.privateKey; + + if (!pk) { + pk = await password({ + message: `Please enter the private key for chain ${chain}`, + }); + } + + return pk; + } + + getType(): TxSubmitterType { + return TxSubmitterType.JSON_RPC; + } +} diff --git a/typescript/cli/src/context/strategies/submitter/SubmitterStrategy.ts b/typescript/cli/src/context/strategies/submitter/SubmitterStrategy.ts new file mode 100644 index 0000000000..10bc3f3123 --- /dev/null +++ b/typescript/cli/src/context/strategies/submitter/SubmitterStrategy.ts @@ -0,0 +1,22 @@ +import { ethers } from 'ethers'; + +import { TxSubmitterType } from '@hyperlane-xyz/sdk'; +import { ChainName } from '@hyperlane-xyz/sdk'; + +export interface ISubmitterStrategy { + getPrivateKey(chain: ChainName): Promise; + getSigner(privateKey: string): ethers.Signer; + getType(): TxSubmitterType; +} + +export abstract class BaseSubmitterStrategy implements ISubmitterStrategy { + constructor(protected config: any) {} + + abstract getPrivateKey(chain: ChainName): Promise; + + getSigner(privateKey: string): ethers.Signer { + return new ethers.Wallet(privateKey); + } + + abstract getType(): TxSubmitterType; +} diff --git a/typescript/cli/src/context/strategies/submitter/SubmitterStrategyFactory.ts b/typescript/cli/src/context/strategies/submitter/SubmitterStrategyFactory.ts new file mode 100644 index 0000000000..9c32f992c7 --- /dev/null +++ b/typescript/cli/src/context/strategies/submitter/SubmitterStrategyFactory.ts @@ -0,0 +1,21 @@ +import { TxSubmitterType } from '@hyperlane-xyz/sdk'; + +import { GnosisSafeStrategy } from './GnosisSafeStrategy.js'; +import { JsonRpcStrategy } from './JsonRpcStrategy.js'; +import { ISubmitterStrategy } from './SubmitterStrategy.js'; + +export class SubmitterStrategyFactory { + static createStrategy( + type: TxSubmitterType, + config: any, + ): ISubmitterStrategy { + switch (type) { + case TxSubmitterType.JSON_RPC: + return new JsonRpcStrategy(config); + case TxSubmitterType.GNOSIS_SAFE: + return new GnosisSafeStrategy(config); + default: + throw new Error(`Unsupported submitter type: ${type}`); + } + } +} diff --git a/typescript/cli/src/context/types.ts b/typescript/cli/src/context/types.ts index 6c3a17c5ff..e3ada01b2b 100644 --- a/typescript/cli/src/context/types.ts +++ b/typescript/cli/src/context/types.ts @@ -5,9 +5,13 @@ import type { IRegistry } from '@hyperlane-xyz/registry'; import type { ChainMap, ChainMetadata, + ChainName, MultiProvider, + WarpRouteDeployConfig, } from '@hyperlane-xyz/sdk'; +// TODO: revisit ContextSettings & CommandContext for improvements + export interface ContextSettings { registryUri: string; registryOverrideUri: string; @@ -16,6 +20,7 @@ export interface ContextSettings { requiresKey?: boolean; disableProxy?: boolean; skipConfirmation?: boolean; + signers?: any; } export interface CommandContext { @@ -25,6 +30,10 @@ export interface CommandContext { skipConfirmation: boolean; key?: string; signer?: ethers.Signer; + signers?: ethers.Signer[]; + chain?: ChainName; + chains?: ChainName[]; + warpRouteConfig?: WarpRouteDeployConfig; } export interface WriteCommandContext extends CommandContext { diff --git a/typescript/cli/src/deploy/core.ts b/typescript/cli/src/deploy/core.ts index f0848458fc..6eaed956f8 100644 --- a/typescript/cli/src/deploy/core.ts +++ b/typescript/cli/src/deploy/core.ts @@ -42,7 +42,6 @@ export async function runCoreDeploy(params: DeployParams) { let chain = params.chain; const { - signer, isDryRun, chainMetadata, dryRunChain, @@ -61,13 +60,14 @@ export async function runCoreDeploy(params: DeployParams) { 'Select chain to connect:', ); } - let apiKeys: ChainMap = {}; if (!skipConfirmation) apiKeys = await getOrRequestApiKeys([chain], chainMetadata); + 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 d8ced32dc7..71ea6cbb65 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,15 @@ 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, + null, chainsToGasCheck ?? chains, minGas, ); @@ -70,8 +70,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 +129,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,6 +139,7 @@ export async function prepareDeploy( const provider = isDryRun ? getLocalProvider(ENV.ANVIL_IP_ADDR, ENV.ANVIL_PORT) : multiProvider.getProvider(chain); + const userAddress = await multiProvider.getSigner(chain).getAddress(); const currentBalance = await provider.getBalance(userAddress); 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,6 +160,7 @@ export async function completeDeploy( const provider = isDryRun ? getLocalProvider(ENV.ANVIL_IP_ADDR, ENV.ANVIL_PORT) : multiProvider.getProvider(chain); + const userAddress = await multiProvider.getSigner(chain).getAddress(); const currentBalance = await provider.getBalance(userAddress); const balanceDelta = initialBalances[chain].sub(currentBalance); if (isDryRun && balanceDelta.lt(0)) break; diff --git a/typescript/cli/src/deploy/warp.ts b/typescript/cli/src/deploy/warp.ts index 36bbc2ad8f..71eed010c5 100644 --- a/typescript/cli/src/deploy/warp.ts +++ b/typescript/cli/src/deploy/warp.ts @@ -62,7 +62,6 @@ import { retryAsync, } from '@hyperlane-xyz/utils'; -import { readWarpRouteDeployConfig } from '../config/warp.js'; import { MINIMUM_WARP_DEPLOY_GAS } from '../consts.js'; import { getOrRequestApiKeys } from '../context/context.js'; import { WriteCommandContext } from '../context/types.js'; @@ -70,9 +69,7 @@ import { log, logBlue, logGray, logGreen, logTable } from '../logger.js'; import { getSubmitterBuilder } from '../submit/submit.js'; import { indentYamlOrJson, - isFile, readYamlOrJson, - runFileSelectionStep, writeYamlOrJson, } from '../utils/files.js'; @@ -95,43 +92,24 @@ interface WarpApplyParams extends DeployParams { export async function runWarpRouteDeploy({ context, - warpRouteDeploymentConfigPath, }: { context: WriteCommandContext; - warpRouteDeploymentConfigPath?: string; }) { - const { signer, skipConfirmation, chainMetadata } = context; - - if ( - !warpRouteDeploymentConfigPath || - !isFile(warpRouteDeploymentConfigPath) - ) { - if (skipConfirmation) - throw new Error('Warp route deployment config required'); - warpRouteDeploymentConfigPath = await runFileSelectionStep( - './configs', - 'Warp route deployment config', - 'warp', - ); - } else { - log( - `Using warp route deployment config at ${warpRouteDeploymentConfigPath}`, - ); - } - const warpRouteConfig = await readWarpRouteDeployConfig( - warpRouteDeploymentConfigPath, - context, - ); - - const chains = Object.keys(warpRouteConfig); + const { + skipConfirmation, + chainMetadata, + warpRouteConfig, + chains: contextChains, + } = context; + const chains = contextChains!; let apiKeys: ChainMap = {}; if (!skipConfirmation) apiKeys = await getOrRequestApiKeys(chains, chainMetadata); const deploymentParams = { context, - warpDeployConfig: warpRouteConfig, + warpDeployConfig: warpRouteConfig!, }; await runDeployPlanStep(deploymentParams); @@ -142,9 +120,7 @@ export async function runWarpRouteDeploy({ minGas: MINIMUM_WARP_DEPLOY_GAS, }); - const userAddress = await signer.getAddress(); - - const initialBalances = await prepareDeploy(context, userAddress, chains); + const initialBalances = await prepareDeploy(context, null, chains); const deployedContracts = await executeDeploy(deploymentParams, apiKeys); @@ -155,7 +131,7 @@ export async function runWarpRouteDeploy({ await writeDeploymentArtifacts(warpCoreConfig, context); - await completeDeploy(context, 'warp', initialBalances, userAddress, chains); + await completeDeploy(context, 'warp', initialBalances, null, chains!); } async function runDeployPlanStep({ context, warpDeployConfig }: DeployParams) { diff --git a/typescript/cli/src/send/transfer.ts b/typescript/cli/src/send/transfer.ts index a89eb6aa99..2e94fdd53f 100644 --- a/typescript/cli/src/send/transfer.ts +++ b/typescript/cli/src/send/transfer.ts @@ -106,8 +106,9 @@ async function executeDelivery({ skipWaitForDelivery: boolean; selfRelay?: boolean; }) { - const { signer, multiProvider, registry } = context; + const { multiProvider, registry } = context; + const signer = multiProvider.getSigner(origin); const signerAddress = await signer.getAddress(); recipient ||= signerAddress; @@ -136,12 +137,12 @@ async function executeDelivery({ token = warpCore.findToken(origin, routerAddress)!; } - const senderAddress = await signer.getAddress(); + // const senderAddress = await multiProvider.getSigner(origin).getAddress(); const errors = await warpCore.validateTransfer({ originTokenAmount: token.amount(amount), destination, - recipient: recipient ?? senderAddress, - sender: senderAddress, + recipient: recipient ?? signerAddress, + sender: signerAddress, }); if (errors) { logRed('Error validating transfer', JSON.stringify(errors)); @@ -152,8 +153,8 @@ async function executeDelivery({ const transferTxs = await warpCore.getTransferRemoteTxs({ originTokenAmount: new TokenAmount(amount, token), destination, - sender: senderAddress, - recipient: recipient ?? senderAddress, + sender: signerAddress, + recipient: recipient ?? signerAddress, }); const txReceipts = []; @@ -172,7 +173,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/utils/balances.ts b/typescript/cli/src/utils/balances.ts index 5cf8019771..ef497e6261 100644 --- a/typescript/cli/src/utils/balances.ts +++ b/typescript/cli/src/utils/balances.ts @@ -7,14 +7,13 @@ import { logGreen, logRed } from '../logger.js'; export async function nativeBalancesAreSufficient( multiProvider: MultiProvider, - signer: ethers.Signer, + signer: ethers.Signer | null, chains: ChainName[], minGas: string, ) { - const address = await signer.getAddress(); - const sufficientBalances: boolean[] = []; for (const chain of chains) { + 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/sdk/src/providers/transactions/submitter/schemas.ts b/typescript/sdk/src/providers/transactions/submitter/schemas.ts index 89c891c09c..81dfba7e45 100644 --- a/typescript/sdk/src/providers/transactions/submitter/schemas.ts +++ b/typescript/sdk/src/providers/transactions/submitter/schemas.ts @@ -10,6 +10,7 @@ import { export const SubmitterMetadataSchema = z.discriminatedUnion('type', [ z.object({ type: z.literal(TxSubmitterType.JSON_RPC), + privateKey: z.string().optional(), }), z.object({ type: z.literal(TxSubmitterType.IMPERSONATED_ACCOUNT),