Skip to content

Commit

Permalink
feat: Singe/MultiVM signer strategy
Browse files Browse the repository at this point in the history
  • Loading branch information
ljankovic-txfusion committed Nov 7, 2024
1 parent 36e2b00 commit ba0d6ea
Show file tree
Hide file tree
Showing 19 changed files with 635 additions and 66 deletions.
5 changes: 4 additions & 1 deletion typescript/cli/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -55,6 +56,7 @@ try {
configureLogger(argv.log as LogFormat, argv.verbosity as LogLevel);
},
contextMiddleware,
signerMiddleware,
])
.command(avsCommand)
.command(configCommand)
Expand All @@ -69,6 +71,7 @@ try {
.command(submitCommand)
.command(validatorCommand)
.command(warpCommand)
.command(strategyCommand)
.version(VERSION)
.demandCommand()
.strict()
Expand Down
1 change: 1 addition & 0 deletions typescript/cli/src/commands/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
207 changes: 207 additions & 0 deletions typescript/cli/src/commands/strategy.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
1 change: 0 additions & 1 deletion typescript/cli/src/commands/warp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,6 @@ export const deploy: CommandModuleWithWriteContext<{
try {
await runWarpRouteDeploy({
context,
warpRouteDeploymentConfigPath: config,
});
} catch (error: any) {
evaluateIfDryRunFailure(error, dryRun);
Expand Down
16 changes: 16 additions & 0 deletions typescript/cli/src/config/strategy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import {
ChainSubmissionStrategy,
ChainSubmissionStrategySchema,
} from '@hyperlane-xyz/sdk';

import { readYamlOrJson } from '../utils/files.js';

export async function readDefaultStrategyConfig(
filePath: string,
): Promise<ChainSubmissionStrategy> {
let config = readYamlOrJson(filePath);
if (!config)
throw new Error(`No default strategy config found at ${filePath}`);

return ChainSubmissionStrategySchema.parse(config);
}
48 changes: 40 additions & 8 deletions typescript/cli/src/context/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -32,6 +38,7 @@ import {

export async function contextMiddleware(argv: Record<string, any>) {
const isDryRun = !isNullish(argv.dryRun);

const requiresKey = isSignCommand(argv);
const settings: ContextSettings = {
registryUri: argv.registry,
Expand All @@ -42,16 +49,44 @@ export async function contextMiddleware(argv: Record<string, any>) {
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<string, any>) {
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
Expand All @@ -63,21 +98,18 @@ export async function getContext({
requiresKey,
skipConfirmation,
disableProxy = false,
signers,
}: ContextSettings): Promise<CommandContext> {
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;
}
Expand Down
Loading

0 comments on commit ba0d6ea

Please sign in to comment.