Revert "feat: Multi EVM Chain Signers" (#4910)

Reverts #4869 because it breaks commands that require usage of the
`HYP_KEY` env variable or prompted for an owner address like `warp init`


![image](https://github.com/user-attachments/assets/c301c1ec-e69d-44e7-abab-714e215298db)
pull/4864/merge
xeno097 4 days ago committed by GitHub
parent 61157097bc
commit 28a824ad7e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 5
      .changeset/chilly-balloons-rule.md
  2. 5
      .changeset/spicy-gifts-hear.md
  3. 7
      typescript/cli/cli.ts
  4. 15
      typescript/cli/src/commands/config.ts
  5. 5
      typescript/cli/src/commands/options.ts
  6. 21
      typescript/cli/src/commands/signCommands.ts
  7. 70
      typescript/cli/src/commands/strategy.ts
  8. 186
      typescript/cli/src/config/strategy.ts
  9. 30
      typescript/cli/src/config/warp.ts
  10. 53
      typescript/cli/src/context/context.ts
  11. 34
      typescript/cli/src/context/strategies/chain/ChainResolverFactory.ts
  12. 200
      typescript/cli/src/context/strategies/chain/MultiChainResolver.ts
  13. 25
      typescript/cli/src/context/strategies/chain/SingleChainResolver.ts
  14. 10
      typescript/cli/src/context/strategies/chain/types.ts
  15. 22
      typescript/cli/src/context/strategies/signer/BaseMultiProtocolSigner.ts
  16. 79
      typescript/cli/src/context/strategies/signer/MultiProtocolSignerFactory.ts
  17. 153
      typescript/cli/src/context/strategies/signer/MultiProtocolSignerManager.ts
  18. 4
      typescript/cli/src/context/types.ts
  19. 1
      typescript/cli/src/deploy/agent.ts
  20. 6
      typescript/cli/src/deploy/core.ts
  21. 30
      typescript/cli/src/deploy/utils.ts
  22. 17
      typescript/cli/src/deploy/warp.ts
  23. 6
      typescript/cli/src/read/warp.ts
  24. 24
      typescript/cli/src/send/transfer.ts
  25. 7
      typescript/cli/src/tests/commands/helpers.ts
  26. 5
      typescript/cli/src/utils/balances.ts
  27. 33
      typescript/cli/src/utils/chains.ts
  28. 47
      typescript/cli/src/utils/output.ts
  29. 2
      typescript/sdk/src/providers/transactions/submitter/ethersV5/schemas.ts
  30. 10
      typescript/utils/src/addresses.ts
  31. 1
      typescript/utils/src/index.ts

@ -1,5 +0,0 @@
---
'@hyperlane-xyz/utils': minor
---
Added `isPrivateKeyEvm` function for validating EVM private keys

@ -1,5 +0,0 @@
---
'@hyperlane-xyz/cli': minor
---
Added strategy management CLI commands and MultiProtocolSigner implementation for flexible cross-chain signer configuration and management

@ -19,17 +19,15 @@ 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, signerMiddleware } from './src/context/context.js';
import { contextMiddleware } 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';
@ -51,14 +49,12 @@ 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)
@ -70,7 +66,6 @@ try {
.command(relayerCommand)
.command(sendCommand)
.command(statusCommand)
.command(strategyCommand)
.command(submitCommand)
.command(validatorCommand)
.command(warpCommand)

@ -3,7 +3,6 @@ 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';
@ -32,7 +31,6 @@ const validateCommand: CommandModule = {
.command(validateChainCommand)
.command(validateIsmCommand)
.command(validateIsmAdvancedCommand)
.command(validateStrategyCommand)
.command(validateWarpCommand)
.version(false)
.demandCommand(),
@ -78,19 +76,6 @@ 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',

@ -95,7 +95,6 @@ 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',
@ -197,8 +196,8 @@ export const transactionsCommandOption: Options = {
export const strategyCommandOption: Options = {
type: 'string',
description: 'The submission strategy input file path.',
alias: ['s', 'strategy'],
demandOption: false,
alias: 's',
demandOption: true,
};
export const addressCommandOption = (

@ -1,14 +1,7 @@
// 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 = [
'apply',
'deploy',
'send',
'status',
'submit',
'relayer',
];
export const SIGN_COMMANDS = ['deploy', 'send', 'status', 'submit', 'relayer'];
export function isSignCommand(argv: any): boolean {
return (
@ -16,15 +9,3 @@ export function isSignCommand(argv: any): boolean {
(argv._.length > 1 && SIGN_COMMANDS.includes(argv._[1]))
);
}
export enum CommandType {
WARP_DEPLOY = 'warp:deploy',
WARP_SEND = 'warp:send',
WARP_APPLY = 'warp:apply',
WARP_READ = 'warp:read',
SEND_MESSAGE = 'send:message',
AGENT_KURTOSIS = 'deploy:kurtosis-agents',
STATUS = 'status:',
SUBMIT = 'submit:',
RELAYER = 'relayer:',
}

@ -1,70 +0,0 @@
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);
},
};

@ -1,186 +0,0 @@
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<ChainSubmissionStrategy> {
log(`Reading submission strategy in ${filePath}`);
try {
const strategyConfig = readYamlOrJson<ChainSubmissionStrategy>(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<ChainSubmissionStrategy> {
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<string, any> = { 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.`,
);
}
}

@ -21,8 +21,6 @@ import {
promiseObjAll,
} from '@hyperlane-xyz/utils';
import { DEFAULT_STRATEGY_CONFIG_PATH } from '../commands/options.js';
import { MultiProtocolSignerManager } from '../context/strategies/signer/MultiProtocolSignerManager.js';
import { CommandContext } from '../context/types.js';
import { errorRed, log, logBlue, logGreen } from '../logger.js';
import { runMultiChainSelectionStep } from '../utils/chains.js';
@ -37,7 +35,6 @@ import {
} from '../utils/input.js';
import { createAdvancedIsmConfig } from './ism.js';
import { readChainSubmissionStrategyConfig } from './strategy.js';
const TYPE_DESCRIPTIONS: Record<TokenType, string> = {
[TokenType.synthetic]: 'A new ERC20 with remote transfer functionality',
@ -125,6 +122,13 @@ 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',
@ -134,31 +138,11 @@ export async function createWarpRouteDeployConfig({
requiresConfirmation: !context.skipConfirmation,
});
const strategyConfig = await readChainSubmissionStrategyConfig(
context.strategyPath ?? DEFAULT_STRATEGY_CONFIG_PATH,
);
const multiProtocolSigner = new MultiProtocolSignerManager(
strategyConfig,
warpChains,
context.multiProvider,
{ key: context.key },
);
const multiProviderWithSigners = await multiProtocolSigner.getMultiProvider();
const result: WarpRouteDeployConfig = {};
let typeChoices = TYPE_CHOICES;
for (const chain of warpChains) {
logBlue(`${chain}: Configuring warp route...`);
const owner = await detectAndConfirmOrPrompt(
async () => multiProviderWithSigners.getSigner(chain).getAddress(),
'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);

@ -16,18 +16,14 @@ 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';
import { runSingleChainSelectionStep } from '../utils/chains.js';
import { detectAndConfirmOrPrompt } from '../utils/input.js';
import { getImpersonatedSigner } from '../utils/keys.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,
@ -45,7 +41,6 @@ export async function contextMiddleware(argv: Record<string, any>) {
requiresKey,
disableProxy: argv.disableProxy,
skipConfirmation: argv.yes,
strategyPath: argv.strategy,
};
if (!isDryRun && settings.fromAddress)
throw new Error(
@ -57,44 +52,6 @@ export async function contextMiddleware(argv: Record<string, any>) {
argv.context = context;
}
export async function signerMiddleware(argv: Record<string, any>) {
const { key, requiresKey, multiProvider, strategyPath } = argv.context;
if (!requiresKey && !key) 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
@ -109,14 +66,18 @@ export async function getContext({
}: ContextSettings): Promise<CommandContext> {
const registry = getRegistry(registryUri, registryOverrideUri, !disableProxy);
const multiProvider = await getMultiProvider(registry);
let signer: ethers.Wallet | undefined = undefined;
if (key || requiresKey) {
({ key, signer } = await getSigner({ key, skipConfirmation }));
}
const multiProvider = await getMultiProvider(registry, signer);
return {
registry,
requiresKey,
chainMetadata: multiProvider.metadata,
multiProvider,
key,
signer,
skipConfirmation: !!skipConfirmation,
} as CommandContext;
}

@ -1,34 +0,0 @@
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<CommandType, () => ChainResolver> = new Map([
[CommandType.WARP_DEPLOY, () => MultiChainResolver.forWarpRouteConfig()],
[CommandType.WARP_SEND, () => MultiChainResolver.forOriginDestination()],
[CommandType.WARP_APPLY, () => MultiChainResolver.forWarpRouteConfig()],
[CommandType.WARP_READ, () => MultiChainResolver.forWarpCoreConfig()],
[CommandType.SEND_MESSAGE, () => MultiChainResolver.forOriginDestination()],
[CommandType.AGENT_KURTOSIS, () => MultiChainResolver.forAgentKurtosis()],
[CommandType.STATUS, () => MultiChainResolver.forOriginDestination()],
[CommandType.SUBMIT, () => MultiChainResolver.forStrategyConfig()],
[CommandType.RELAYER, () => MultiChainResolver.forRelayer()],
]);
/**
* @param argv - Command line arguments.
* @returns ChainResolver - The appropriate chain resolver strategy based on the command type.
*/
static getStrategy(argv: Record<string, any>): ChainResolver {
const commandKey = `${argv._[0]}:${argv._[1] || ''}`.trim() as CommandType;
const createStrategy =
this.strategyMap.get(commandKey) || (() => new SingleChainResolver());
return createStrategy();
}
}

@ -1,200 +0,0 @@
import { ChainMap, ChainName } from '@hyperlane-xyz/sdk';
import { assert } from '@hyperlane-xyz/utils';
import { DEFAULT_WARP_ROUTE_DEPLOYMENT_CONFIG_PATH } from '../../../commands/options.js';
import { readChainSubmissionStrategyConfig } from '../../../config/strategy.js';
import { logRed } 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,
}
// 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<any>): Promise<ChainName[]> {
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.ORIGIN_DESTINATION:
default:
return this.resolveOriginDestinationChains(argv);
}
}
private async resolveWarpRouteConfigChains(
argv: Record<string, any>,
): Promise<ChainName[]> {
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<string, any>,
): Promise<ChainName[]> {
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<string, any>,
): Promise<ChainName[]> {
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<string, any>,
): Promise<ChainName[]> {
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<string, any>,
): Promise<ChainName[]> {
const strategy = await readChainSubmissionStrategyConfig(argv.strategy);
return extractChainsFromObj(strategy);
}
private async resolveRelayerChains(
argv: Record<string, any>,
): Promise<ChainName[]> {
return argv.chains.split(',').map((item: string) => item.trim());
}
private async getWarpRouteConfigChains(
configPath: string,
skipConfirmation: boolean,
): Promise<ChainName[]> {
if (!configPath || !isFile(configPath)) {
assert(!skipConfirmation, 'Warp route deployment config is required');
configPath = await runFileSelectionStep(
'./configs',
'Warp route deployment config',
'warp',
);
} else {
logRed(`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;
}
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);
}
}

@ -1,25 +0,0 @@
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<any>): Promise<ChainName[]> {
argv.chain ||= await runSingleChainSelectionStep(
argv.context.chainMetadata,
'Select chain to connect:',
);
return [argv.chain]; // Explicitly return as single-item array
}
}

@ -1,10 +0,0 @@
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<any>): Promise<ChainName[]>;
}

@ -1,22 +0,0 @@
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<string, any>; // For any additional chain-specific params
}
export interface IMultiProtocolSigner {
getSignerConfig(chain: ChainName): Promise<SignerConfig> | SignerConfig;
getSigner(config: SignerConfig): Signer;
}
export abstract class BaseMultiProtocolSigner implements IMultiProtocolSigner {
constructor(protected config: ChainSubmissionStrategy) {}
abstract getSignerConfig(chain: ChainName): Promise<SignerConfig>;
abstract getSigner(config: SignerConfig): Signer;
}

@ -1,79 +0,0 @@
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<SignerConfig> {
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<SignerConfig> {
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);
}
}

@ -1,153 +0,0 @@
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<ChainName, IMultiProtocolSigner>;
protected readonly signers: Map<ChainName, Signer>;
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<MultiProvider> {
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<Signer> {
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<typeof this.signers> {
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<string> {
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;
}
}

@ -6,7 +6,6 @@ import type {
ChainMap,
ChainMetadata,
MultiProvider,
WarpCoreConfig,
} from '@hyperlane-xyz/sdk';
export interface ContextSettings {
@ -17,7 +16,6 @@ export interface ContextSettings {
requiresKey?: boolean;
disableProxy?: boolean;
skipConfirmation?: boolean;
strategyPath?: string;
}
export interface CommandContext {
@ -27,8 +25,6 @@ export interface CommandContext {
skipConfirmation: boolean;
key?: string;
signer?: ethers.Signer;
warpCoreConfig?: WarpCoreConfig;
strategyPath?: string;
}
export interface WriteCommandContext extends CommandContext {

@ -21,7 +21,6 @@ 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,

@ -43,6 +43,7 @@ export async function runCoreDeploy(params: DeployParams) {
let chain = params.chain;
const {
signer,
isDryRun,
chainMetadata,
dryRunChain,
@ -61,14 +62,13 @@ export async function runCoreDeploy(params: DeployParams) {
'Select chain to connect:',
);
}
let apiKeys: ChainMap<string> = {};
if (!skipConfirmation)
apiKeys = await requestAndSaveApiKeys([chain], chainMetadata, registry);
const signer = multiProvider.getSigner(chain);
const deploymentParams: DeployParams = {
context: { ...context, signer },
context,
chain,
config,
};

@ -41,7 +41,7 @@ export async function runPreflightChecksForChains({
chainsToGasCheck?: ChainName[];
}) {
log('Running pre-flight checks for chains...');
const { multiProvider } = context;
const { signer, multiProvider } = context;
if (!chains?.length) throw new Error('Empty chain selection');
for (const chain of chains) {
@ -49,14 +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,
chainsToGasCheck ?? chains,
minGas,
);
@ -69,13 +70,8 @@ export async function runDeployPlanStep({
context: WriteCommandContext;
chain: ChainName;
}) {
const {
chainMetadata: chainMetadataMap,
multiProvider,
skipConfirmation,
} = context;
const address = await multiProvider.getSigner(chain).getAddress();
const { signer, chainMetadata: chainMetadataMap, skipConfirmation } = context;
const address = await signer.getAddress();
logBlue('\nDeployment plan');
logGray('===============');
@ -128,7 +124,7 @@ export function isZODISMConfig(filepath: string): boolean {
export async function prepareDeploy(
context: WriteCommandContext,
userAddress: Address | null,
userAddress: Address,
chains: ChainName[],
): Promise<Record<string, BigNumber>> {
const { multiProvider, isDryRun } = context;
@ -138,9 +134,7 @@ export async function prepareDeploy(
const provider = isDryRun
? getLocalProvider(ENV.ANVIL_IP_ADDR, ENV.ANVIL_PORT)
: multiProvider.getProvider(chain);
const address =
userAddress ?? (await multiProvider.getSigner(chain).getAddress());
const currentBalance = await provider.getBalance(address);
const currentBalance = await provider.getBalance(userAddress);
initialBalances[chain] = currentBalance;
}),
);
@ -151,7 +145,7 @@ export async function completeDeploy(
context: WriteCommandContext,
command: string,
initialBalances: Record<string, BigNumber>,
userAddress: Address | null,
userAddress: Address,
chains: ChainName[],
) {
const { multiProvider, isDryRun } = context;
@ -160,9 +154,7 @@ export async function completeDeploy(
const provider = isDryRun
? getLocalProvider(ENV.ANVIL_IP_ADDR, ENV.ANVIL_PORT)
: multiProvider.getProvider(chain);
const address =
userAddress ?? (await multiProvider.getSigner(chain).getAddress());
const currentBalance = await provider.getBalance(address);
const currentBalance = await provider.getBalance(userAddress);
const balanceDelta = initialBalances[chain].sub(currentBalance);
if (isDryRun && balanceDelta.lt(0)) break;
logPink(

@ -100,7 +100,7 @@ export async function runWarpRouteDeploy({
context: WriteCommandContext;
warpRouteDeploymentConfigPath?: string;
}) {
const { skipConfirmation, chainMetadata, registry } = context;
const { signer, skipConfirmation, chainMetadata, registry } = context;
if (
!warpRouteDeploymentConfigPath ||
@ -147,8 +147,13 @@ export async function runWarpRouteDeploy({
minGas: MINIMUM_WARP_DEPLOY_GAS,
});
const initialBalances = await prepareDeploy(context, null, ethereumChains);
const userAddress = await signer.getAddress();
const initialBalances = await prepareDeploy(
context,
userAddress,
ethereumChains,
);
const deployedContracts = await executeDeploy(deploymentParams, apiKeys);
const warpCoreConfig = await getWarpCoreConfig(
@ -158,7 +163,13 @@ export async function runWarpRouteDeploy({
await writeDeploymentArtifacts(warpCoreConfig, context);
await completeDeploy(context, 'warp', initialBalances, null, ethereumChains!);
await completeDeploy(
context,
'warp',
initialBalances,
userAddress,
ethereumChains,
);
}
async function runDeployPlanStep({ context, warpDeployConfig }: DeployParams) {

@ -34,13 +34,11 @@ export async function runWarpRouteRead({
let addresses: ChainMap<string>;
if (symbol || warp) {
const warpCoreConfig =
context.warpCoreConfig ?? // this case is be handled by MultiChainHandler.forWarpCoreConfig() interceptor
(await getWarpCoreConfigOrExit({
const warpCoreConfig = await getWarpCoreConfigOrExit({
context,
warp,
symbol,
}));
});
// TODO: merge with XERC20TokenAdapter and WarpRouteReader
const xerc20Limits = await Promise.all(

@ -40,8 +40,8 @@ export async function sendTestTransfer({
}: {
context: WriteCommandContext;
warpCoreConfig: WarpCoreConfig;
origin?: ChainName; // resolved in signerMiddleware
destination?: ChainName; // resolved in signerMiddleware
origin?: ChainName;
destination?: ChainName;
amount: string;
recipient?: string;
timeoutSec: number;
@ -106,15 +106,10 @@ async function executeDelivery({
skipWaitForDelivery: boolean;
selfRelay?: boolean;
}) {
const { multiProvider, registry } = context;
const { signer, multiProvider, registry } = context;
const signer = multiProvider.getSigner(origin);
const recipientSigner = multiProvider.getSigner(destination);
const recipientAddress = await recipientSigner.getAddress();
const signerAddress = await signer.getAddress();
recipient ||= recipientAddress;
recipient ||= signerAddress;
const chainAddresses = await registry.getAddresses();
@ -141,11 +136,12 @@ async function executeDelivery({
token = warpCore.findToken(origin, routerAddress)!;
}
const senderAddress = await signer.getAddress();
const errors = await warpCore.validateTransfer({
originTokenAmount: token.amount(amount),
destination,
recipient,
sender: signerAddress,
recipient: recipient ?? senderAddress,
sender: senderAddress,
});
if (errors) {
logRed('Error validating transfer', JSON.stringify(errors));
@ -156,8 +152,8 @@ async function executeDelivery({
const transferTxs = await warpCore.getTransferRemoteTxs({
originTokenAmount: new TokenAmount(amount, token),
destination,
sender: signerAddress,
recipient,
sender: senderAddress,
recipient: recipient ?? senderAddress,
});
const txReceipts = [];
@ -176,7 +172,7 @@ async function executeDelivery({
const parsed = parseWarpRouteMessage(message.parsed.body);
logBlue(
`Sent transfer from sender (${signerAddress}) on ${origin} to recipient (${recipient}) on ${destination}.`,
`Sent transfer from sender (${senderAddress}) on ${origin} to recipient (${recipient}) on ${destination}.`,
);
logBlue(`Message ID: ${message.id}`);
log(`Message:\n${indentYamlOrJson(yamlStringify(message, null, 2), 4)}`);

@ -1,4 +1,3 @@
import { ethers } from 'ethers';
import { $ } from 'zx';
import { ERC20Test__factory, ERC4626Test__factory } from '@hyperlane-xyz/core';
@ -143,9 +142,6 @@ 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);
@ -165,9 +161,6 @@ 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');

@ -8,9 +8,12 @@ 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
@ -18,7 +21,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();

@ -171,36 +171,3 @@ 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, any>): 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;
}

@ -54,50 +54,3 @@ 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;
}

@ -15,8 +15,6 @@ export const EV5GnosisSafeTxBuilderPropsSchema = z.object({
export const EV5JsonRpcTxSubmitterPropsSchema = z.object({
chain: ZChainName,
userAddress: ZHash.optional(),
privateKey: ZHash.optional(),
});
export const EV5ImpersonatedAccountTxSubmitterPropsSchema =

@ -1,6 +1,6 @@
import { fromBech32, normalizeBech32, toBech32 } from '@cosmjs/encoding';
import { PublicKey } from '@solana/web3.js';
import { Wallet, utils as ethersUtils } from 'ethers';
import { utils as ethersUtils } from 'ethers';
import { isNullish } from './typeof.js';
import { Address, HexString, ProtocolType } from './types.js';
@ -380,11 +380,3 @@ 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!');
}
}

@ -26,7 +26,6 @@ export {
isValidAddressCosmos,
isValidAddressEvm,
isValidAddressSealevel,
isPrivateKeyEvm,
isValidTransactionHash,
isValidTransactionHashCosmos,
isValidTransactionHashEvm,

Loading…
Cancel
Save