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/4892/merge
parent
61157097bc
commit
28a824ad7e
@ -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 |
@ -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,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; |
||||
} |
||||
} |
Loading…
Reference in new issue