feat: Add ChainSubmissionStrategy and update hyperlane submit (#4380)

### Description
- This PR is a prerequisite for
https://github.com/hyperlane-xyz/hyperlane-monorepo/pull/4225
- Adds `ChainSubmissionStrategy` which is a ChainMap of
SubmissionStrategy
- Moves the submissionStrategy logic out of the context to allow
`--strategy` to be used for both cases (mostly to not having to parse
and validate 2 schemas in `getSubmissionStrategy()`)
- Adds logic to assume that all `--transactions` are of the same chainId
with explicit validation

### Backward compatibility
Yes

### Testing
Manual
pull/4391/head
Lee 3 months ago committed by GitHub
parent 38a52deac2
commit f2783c03bb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 6
      .changeset/tricky-horses-repair.md
  2. 1
      typescript/cli/examples/submit/strategy/gnosis-ica-strategy.yaml
  3. 1
      typescript/cli/examples/submit/strategy/gnosis-strategy.yaml
  4. 1
      typescript/cli/examples/submit/strategy/impersonated-account-strategy.yaml
  5. 1
      typescript/cli/examples/submit/strategy/json-rpc-strategy.yaml
  6. 29
      typescript/cli/src/commands/submit.ts
  7. 32
      typescript/cli/src/config/submit.ts
  8. 43
      typescript/cli/src/context/context.ts
  9. 3
      typescript/cli/src/context/types.ts
  10. 10
      typescript/sdk/src/index.ts
  11. 5
      typescript/sdk/src/providers/transactions/schemas.ts
  12. 6
      typescript/sdk/src/providers/transactions/submitter/builder/schemas.ts
  13. 8
      typescript/sdk/src/providers/transactions/submitter/builder/types.ts

@ -0,0 +1,6 @@
---
'@hyperlane-xyz/cli': minor
'@hyperlane-xyz/sdk': minor
---
Add ChainSubmissionStrategySchema

@ -1,4 +1,3 @@
chain: avalanche
submitter:
type: gnosisSafe
chain: avalanche

@ -1,4 +1,3 @@
chain: avalanche
submitter:
type: gnosisSafe
chain: avalanche

@ -1,4 +1,3 @@
chain: alfajores
submitter:
type: impersonatedAccount
userAddress: '0x16F4898F47c085C41d7Cc6b1dc72B91EA617dcBb'

@ -1,3 +1,2 @@
chain: alfajores
submitter:
type: jsonRpc

@ -1,6 +1,12 @@
import {
SubmissionStrategy,
SubmissionStrategySchema,
} from '@hyperlane-xyz/sdk';
import { runSubmit } from '../config/submit.js';
import { CommandModuleWithWriteContext } from '../context/types.js';
import { logBlue, logGray } from '../logger.js';
import { readYamlOrJson } from '../utils/files.js';
import {
dryRunCommandOption,
@ -26,17 +32,38 @@ export const submitCommand: CommandModuleWithWriteContext<{
'dry-run': dryRunCommandOption,
receipts: outputFileCommandOption('./generated/transactions/receipts.yaml'),
},
handler: async ({ context, transactions, receipts }) => {
handler: async ({
context,
transactions,
strategy: strategyUrl,
receipts,
}) => {
logGray(`Hyperlane Submit`);
logGray(`----------------`);
const submissionStrategy = readSubmissionStrategy(strategyUrl);
await runSubmit({
context,
transactionsFilepath: transactions,
receiptsFilepath: receipts,
submissionStrategy,
});
logBlue(`✅ Submission complete`);
process.exit(0);
},
};
/**
* Retrieves a submission strategy from the provided filepath.
* @param submissionStrategyFilepath a filepath to the submission strategy file
* @returns a formatted submission strategy
*/
export function readSubmissionStrategy(
submissionStrategyFilepath: string,
): SubmissionStrategy {
const submissionStrategyFileContent = readYamlOrJson(
submissionStrategyFilepath.trim(),
);
return SubmissionStrategySchema.parse(submissionStrategyFileContent);
}

@ -3,7 +3,10 @@ import { stringify as yamlStringify } from 'yaml';
import {
PopulatedTransactions,
PopulatedTransactionsSchema,
SubmissionStrategy,
} from '@hyperlane-xyz/sdk';
import { PopulatedTransaction } from '@hyperlane-xyz/sdk';
import { MultiProvider } from '@hyperlane-xyz/sdk';
import { assert, errorToString } from '@hyperlane-xyz/utils';
import { WriteCommandContext } from '../context/types.js';
@ -19,25 +22,27 @@ export async function runSubmit({
context,
transactionsFilepath,
receiptsFilepath,
submissionStrategy,
}: {
context: WriteCommandContext;
transactionsFilepath: string;
receiptsFilepath: string;
submissionStrategy: SubmissionStrategy;
}) {
const { submissionStrategy, chainMetadata, multiProvider } = context;
const { chainMetadata, multiProvider } = context;
assert(
submissionStrategy,
'Submission strategy required to submit transactions.\nPlease create a submission strategy. See examples in cli/examples/submit/strategy/*.',
);
const transactions = getTransactions(transactionsFilepath);
const chain = getChainFromTxs(multiProvider, transactions);
const chain = submissionStrategy.chain;
const protocol = chainMetadata[chain].protocol;
const submitterBuilder = await getSubmitterBuilder<typeof protocol>({
submissionStrategy,
multiProvider,
});
const transactions = getTransactions(transactionsFilepath);
try {
const transactionReceipts = await submitterBuilder.submit(...transactions);
@ -57,6 +62,27 @@ export async function runSubmit({
}
}
/**
* Retrieves the chain name from transactions[0].
*
* @param multiProvider - The MultiProvider instance to use for chain name lookup.
* @param transactions - The list of populated transactions.
* @returns The name of the chain that the transactions are submitted on.
* @throws If the transactions are not all on the same chain or chain is not found
*/
function getChainFromTxs(
multiProvider: MultiProvider,
transactions: PopulatedTransactions,
) {
const firstTransaction = transactions[0];
const sameChainIds = transactions.every(
(t: PopulatedTransaction) => t.chainId === firstTransaction.chainId,
);
assert(sameChainIds, 'Transactions must be submitted on the same chains');
return multiProvider.getChainName(firstTransaction.chainId);
}
function getTransactions(transactionsFilepath: string): PopulatedTransactions {
const transactionsFileContent = readYamlOrJson<any[]>(
transactionsFilepath.trim(),

@ -12,9 +12,6 @@ import {
ChainMetadata,
ChainName,
MultiProvider,
SubmissionStrategy,
SubmissionStrategySchema,
TxSubmitterType,
} from '@hyperlane-xyz/sdk';
import { isHttpsUrl, isNullish, rootLogger } from '@hyperlane-xyz/utils';
@ -22,7 +19,6 @@ import { isSignCommand } from '../commands/signCommands.js';
import { forkNetworkToMultiProvider, verifyAnvil } from '../deploy/dry-run.js';
import { logBlue } from '../logger.js';
import { runSingleChainSelectionStep } from '../utils/chains.js';
import { readYamlOrJson } from '../utils/files.js';
import { detectAndConfirmOrPrompt } from '../utils/input.js';
import { getImpersonatedSigner, getSigner } from '../utils/keys.js';
@ -33,7 +29,7 @@ import {
} from './types.js';
export async function contextMiddleware(argv: Record<string, any>) {
let isDryRun = !isNullish(argv.dryRun);
const isDryRun = !isNullish(argv.dryRun);
const requiresKey = isSignCommand(argv);
const settings: ContextSettings = {
registryUri: argv.registry,
@ -47,15 +43,6 @@ export async function contextMiddleware(argv: Record<string, any>) {
throw new Error(
"'--from-address' or '-f' should only be used for dry-runs",
);
if (argv.strategy) {
settings.submissionStrategy = getSubmissionStrategy(argv.strategy);
if (
settings.submissionStrategy.submitter.type ===
TxSubmitterType.IMPERSONATED_ACCOUNT
) {
isDryRun = true;
}
}
const context = isDryRun
? await getDryRunContext(settings, argv.dryRun)
: await getContext(settings);
@ -72,7 +59,6 @@ export async function getContext({
key,
requiresKey,
skipConfirmation,
submissionStrategy,
}: ContextSettings): Promise<CommandContext> {
const registry = getRegistry(registryUri, registryOverrideUri);
@ -89,7 +75,6 @@ export async function getContext({
key,
signer,
skipConfirmation: !!skipConfirmation,
submissionStrategy,
} as CommandContext;
}
@ -104,7 +89,6 @@ export async function getDryRunContext(
key,
fromAddress,
skipConfirmation,
submissionStrategy,
}: ContextSettings,
chain?: ChainName,
): Promise<CommandContext> {
@ -113,12 +97,10 @@ export async function getDryRunContext(
if (!chain) {
if (skipConfirmation) throw new Error('No chains provided');
chain = submissionStrategy
? submissionStrategy.chain
: await runSingleChainSelectionStep(
chainMetadata,
'Select chain to dry-run against:',
);
chain = await runSingleChainSelectionStep(
chainMetadata,
'Select chain to dry-run against:',
);
}
logBlue(`Dry-running against chain: ${chain}`);
@ -142,7 +124,6 @@ export async function getDryRunContext(
skipConfirmation: !!skipConfirmation,
isDryRun: true,
dryRunChain: chain,
submissionStrategy,
} as WriteCommandContext;
}
@ -190,20 +171,6 @@ async function getMultiProvider(registry: IRegistry, signer?: ethers.Signer) {
return multiProvider;
}
/**
* Retrieves a submission strategy from the provided filepath.
* @param submissionStrategyFilepath a filepath to the submission strategy file
* @returns a formatted submission strategy
*/
function getSubmissionStrategy(
submissionStrategyFilepath: string,
): SubmissionStrategy {
const submissionStrategyFileContent = readYamlOrJson(
submissionStrategyFilepath.trim(),
);
return SubmissionStrategySchema.parse(submissionStrategyFileContent);
}
export async function getOrRequestApiKeys(
chains: ChainName[],
chainMetadata: ChainMap<ChainMetadata>,

@ -6,7 +6,6 @@ import type {
ChainMap,
ChainMetadata,
MultiProvider,
SubmissionStrategy,
} from '@hyperlane-xyz/sdk';
export interface ContextSettings {
@ -16,7 +15,6 @@ export interface ContextSettings {
fromAddress?: string;
requiresKey?: boolean;
skipConfirmation?: boolean;
submissionStrategy?: SubmissionStrategy;
}
export interface CommandContext {
@ -24,7 +22,6 @@ export interface CommandContext {
chainMetadata: ChainMap<ChainMetadata>;
multiProvider: MultiProvider;
skipConfirmation: boolean;
submissionStrategy?: SubmissionStrategy;
key?: string;
signer?: ethers.Signer;
}

@ -336,9 +336,15 @@ export {
EV5ImpersonatedAccountTxSubmitterProps,
} from './providers/transactions/submitter/ethersV5/types.js';
export { SubmissionStrategySchema } from './providers/transactions/submitter/builder/schemas.js';
export {
SubmissionStrategySchema,
ChainSubmissionStrategySchema,
} from './providers/transactions/submitter/builder/schemas.js';
export { TxSubmitterBuilder } from './providers/transactions/submitter/builder/TxSubmitterBuilder.js';
export { SubmissionStrategy } from './providers/transactions/submitter/builder/types.js';
export {
SubmissionStrategy,
ChainSubmissionStrategy,
} from './providers/transactions/submitter/builder/types.js';
export { EV5GnosisSafeTxSubmitter } from './providers/transactions/submitter/ethersV5/EV5GnosisSafeTxSubmitter.js';
export { EV5ImpersonatedAccountTxSubmitter } from './providers/transactions/submitter/ethersV5/EV5ImpersonatedAccountTxSubmitter.js';

@ -10,7 +10,10 @@ export const PopulatedTransactionSchema = z.object({
chainId: z.number(),
});
export const PopulatedTransactionsSchema = PopulatedTransactionSchema.array();
export const PopulatedTransactionsSchema =
PopulatedTransactionSchema.array().refine((txs) => txs.length > 0, {
message: 'Populated Transactions cannot be empty',
});
export const CallDataSchema = z.object({
to: ZHash,

@ -5,7 +5,11 @@ import { TransformerMetadataSchema } from '../../transformer/schemas.js';
import { SubmitterMetadataSchema } from '../schemas.js';
export const SubmissionStrategySchema = z.object({
chain: ZChainName,
submitter: SubmitterMetadataSchema,
transforms: z.array(TransformerMetadataSchema).optional(),
});
export const ChainSubmissionStrategySchema = z.record(
ZChainName,
SubmissionStrategySchema,
);

@ -1,5 +1,11 @@
import { z } from 'zod';
import { SubmissionStrategySchema } from './schemas.js';
import {
ChainSubmissionStrategySchema,
SubmissionStrategySchema,
} from './schemas.js';
export type SubmissionStrategy = z.infer<typeof SubmissionStrategySchema>;
export type ChainSubmissionStrategy = z.infer<
typeof ChainSubmissionStrategySchema
>;

Loading…
Cancel
Save