feat(cli): add submit functionality support to warp apply (#4225)

### Description

- adds submit functionality support to warp apply
- enables dynamic submission of transactions to vanilla json rpc, gnosis
safe, and impersonated accounts while dry-running
- allows easy support of ICA tx submissions in the future

### Drive-by changes
- just updated `ApplyParams` to `WarpApplyParams`
- e2e tests written in ts

### Related issues

- https://github.com/hyperlane-xyz/hyperlane-monorepo/issues/4242

### Backward compatibility

- yes

### Testing
- [x] Single warp route transfer ownership from EOA to Safe
- [x] Single warp route transfer from Safe to EOA
- [x] 2 warp route transfer ownership from EOA to safe
- [x] 2 warp route transfer from Safe to EOA (sepolia and basesepolia)

Multichain enrollments through their respective Safes:
- [x] Deploy to sepolia with address to Signer
- [x] Transfer Owner to safe
- [x] Warp apply to extend a synthetic Route to base sepolia and set
owner to safe
- [x] Approve safe txs to enroll each other
- [x] Send a test message

E2e Testing

---------

Co-authored-by: Le Yu <6251863+ltyu@users.noreply.github.com>
pull/4412/head
Noah Bayindirli 🥂 3 months ago committed by GitHub
parent ef813b9810
commit 3c07ded5b7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 2
      .changeset/nice-deers-tan.md
  2. 6
      .changeset/wicked-knives-care.md
  3. 3
      mono.code-workspace
  4. 2
      typescript/cli/.mocharc.json
  5. 10
      typescript/cli/examples/submit/strategy/gnosis-chain-strategy.yaml
  6. 0
      typescript/cli/examples/submit/strategy/gnosis-strategy-avalanche.yaml
  7. 3
      typescript/cli/examples/submit/strategy/json-rpc-chain-strategy.yaml
  8. 10
      typescript/cli/examples/submit/transactions/anvil-transactions.yaml
  9. 11
      typescript/cli/examples/warp-route-deployment.yaml
  10. 3
      typescript/cli/package.json
  11. 25
      typescript/cli/scripts/all-test.sh
  12. 7
      typescript/cli/src/commands/warp.ts
  13. 189
      typescript/cli/src/deploy/warp.ts
  14. 2
      typescript/cli/src/submit/submit.ts
  15. 9
      typescript/cli/src/submit/types.ts
  16. 20
      typescript/cli/src/tests/commands/core.ts
  17. 116
      typescript/cli/src/tests/commands/helpers.ts
  18. 68
      typescript/cli/src/tests/commands/warp.ts
  19. 151
      typescript/cli/src/tests/warp-apply.e2e-test.ts
  20. 10
      typescript/cli/test-configs/anvil/chains/anvil1/metadata.yaml
  21. 15
      typescript/cli/test-configs/anvil/chains/anvil2/metadata.yaml
  22. 22
      typescript/cli/test-configs/anvil/chains/anvil3/metadata.yaml
  23. 2
      typescript/sdk/scripts/foundry-test.sh
  24. 6
      typescript/sdk/src/providers/transactions/submitter/builder/TxSubmitterBuilder.ts
  25. 61
      typescript/sdk/src/providers/transactions/submitter/ethersV5/EV5GnosisSafeTxSubmitter.ts
  26. 53
      yarn.lock

@ -1,5 +1,5 @@
--- ---
"@hyperlane-xyz/sdk": patch '@hyperlane-xyz/sdk': patch
--- ---
Support DefaultFallbackRoutingIsm in metadata builder Support DefaultFallbackRoutingIsm in metadata builder

@ -0,0 +1,6 @@
---
'@hyperlane-xyz/cli': minor
'@hyperlane-xyz/sdk': minor
---
Add Safe submit functionality to warp apply

@ -17,6 +17,9 @@
"**/.git/**": true, "**/.git/**": true,
"**/node_modules/*/**": true "**/node_modules/*/**": true
}, },
"cSpell.words": [
"hyperlane"
],
}, },
"folders": [ "folders": [
{ {

@ -5,4 +5,4 @@
"experimental-specifier-resolution=node", "experimental-specifier-resolution=node",
"loader=ts-node/esm" "loader=ts-node/esm"
] ]
} }

@ -0,0 +1,10 @@
sepolia:
submitter:
type: gnosisSafe
chain: sepolia
safeAddress: '0x7fd32493Ca3A38cDf78A4cb74F32f6292f822aBe'
basesepolia:
submitter:
type: gnosisSafe
chain: basesepolia
safeAddress: '0x7fd32493Ca3A38cDf78A4cb74F32f6292f822aBe'

@ -0,0 +1,10 @@
# Sends some eth
[
{
'data': '0x00',
'value': 1,
'to': '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266',
'from': '0x70997970C51812dc3A010C7d01b50e0d17dc79C8',
'chainId': 31337,
},
]

@ -19,14 +19,3 @@ anvil1:
# name: "MyCollateralToken" # name: "MyCollateralToken"
# symbol: "MCT" # symbol: "MCT"
# totalSupply: 10000000 # totalSupply: 10000000
anvil2:
type: synthetic
# token: "0x123" # Collateral/vault address. Required for collateral types
# owner: "0x123" # Optional owner address for synthetic token
# mailbox: "0x123" # mailbox address route
# interchainGasPaymaster: "0x123" # Optional interchainGasPaymaster address
# You can optionally set the token metadata
# name: "MySyntheticToken"
# symbol: "MST"
# totalSupply: 10000000

@ -19,7 +19,8 @@
"yaml": "^2.4.1", "yaml": "^2.4.1",
"yargs": "^17.7.2", "yargs": "^17.7.2",
"zod": "^3.21.2", "zod": "^3.21.2",
"zod-validation-error": "^3.3.0" "zod-validation-error": "^3.3.0",
"zx": "^8.1.4"
}, },
"devDependencies": { "devDependencies": {
"@ethersproject/abi": "*", "@ethersproject/abi": "*",

@ -0,0 +1,25 @@
#!/usr/bin/env bash
function cleanup() {
set +e
pkill -f anvil
rm -rf /tmp/anvil2
rm -rf /tmp/anvil3
rm -f ./test-configs/anvil/chains/anvil2/addresses.yaml
rm -f ./test-configs/anvil/chains/anvil3/addresses.yaml
set -e
}
# cleanup
echo "Starting anvil2 and anvil3 chain"
anvil --chain-id 31338 -p 8555 --state /tmp/anvil2/state --gas-price 1 > /dev/null &
anvil --chain-id 31347 -p 8600 --state /tmp/anvil3/state --gas-price 1 > /dev/null &
echo "Running all tests"
yarn mocha --config .mocharc.json
# cleanup
echo "Done all tests"

@ -37,6 +37,7 @@ import {
dryRunCommandOption, dryRunCommandOption,
fromAddressCommandOption, fromAddressCommandOption,
outputFileCommandOption, outputFileCommandOption,
strategyCommandOption,
symbolCommandOption, symbolCommandOption,
warpCoreConfigCommandOption, warpCoreConfigCommandOption,
warpDeploymentConfigCommandOption, warpDeploymentConfigCommandOption,
@ -66,6 +67,7 @@ export const apply: CommandModuleWithWriteContext<{
config: string; config: string;
symbol?: string; symbol?: string;
warp: string; warp: string;
strategy?: string;
}> = { }> = {
command: 'apply', command: 'apply',
describe: 'Update Warp Route contracts', describe: 'Update Warp Route contracts',
@ -79,8 +81,9 @@ export const apply: CommandModuleWithWriteContext<{
...warpCoreConfigCommandOption, ...warpCoreConfigCommandOption,
demandOption: false, demandOption: false,
}, },
strategy: { ...strategyCommandOption, demandOption: false },
}, },
handler: async ({ context, config, symbol, warp }) => { handler: async ({ context, config, symbol, warp, strategy: strategyUrl }) => {
logGray(`Hyperlane Warp Apply`); logGray(`Hyperlane Warp Apply`);
logGray('--------------------'); // @TODO consider creating a helper function for these dashes logGray('--------------------'); // @TODO consider creating a helper function for these dashes
let warpCoreConfig: WarpCoreConfig; let warpCoreConfig: WarpCoreConfig;
@ -93,10 +96,12 @@ export const apply: CommandModuleWithWriteContext<{
process.exit(0); process.exit(0);
} }
const warpDeployConfig = await readWarpRouteDeployConfig(config); const warpDeployConfig = await readWarpRouteDeployConfig(config);
await runWarpRouteApply({ await runWarpRouteApply({
context, context,
warpDeployConfig, warpDeployConfig,
warpCoreConfig, warpCoreConfig,
strategyUrl,
}); });
process.exit(0); process.exit(0);
}, },

@ -1,5 +1,4 @@
import { confirm } from '@inquirer/prompts'; import { confirm } from '@inquirer/prompts';
import { ContractReceipt } from 'ethers';
import { stringify as yamlStringify } from 'yaml'; import { stringify as yamlStringify } from 'yaml';
import { buildArtifact as coreBuildArtifact } from '@hyperlane-xyz/core/buildArtifact.js'; import { buildArtifact as coreBuildArtifact } from '@hyperlane-xyz/core/buildArtifact.js';
@ -9,6 +8,8 @@ import {
AnnotatedEV5Transaction, AnnotatedEV5Transaction,
ChainMap, ChainMap,
ChainName, ChainName,
ChainSubmissionStrategy,
ChainSubmissionStrategySchema,
ContractVerifier, ContractVerifier,
EvmERC20WarpModule, EvmERC20WarpModule,
EvmERC20WarpRouteReader, EvmERC20WarpRouteReader,
@ -30,9 +31,12 @@ import {
ProxyFactoryFactoriesAddresses, ProxyFactoryFactoriesAddresses,
RemoteRouters, RemoteRouters,
RoutingIsmConfig, RoutingIsmConfig,
SubmissionStrategy,
TOKEN_TYPE_TO_STANDARD, TOKEN_TYPE_TO_STANDARD,
TokenFactories, TokenFactories,
TrustedRelayerIsmConfig, TrustedRelayerIsmConfig,
TxSubmitterBuilder,
TxSubmitterType,
WarpCoreConfig, WarpCoreConfig,
WarpCoreConfigSchema, WarpCoreConfigSchema,
WarpRouteDeployConfig, WarpRouteDeployConfig,
@ -67,9 +71,11 @@ import {
logRed, logRed,
logTable, logTable,
} from '../logger.js'; } from '../logger.js';
import { getSubmitterBuilder } from '../submit/submit.js';
import { import {
indentYamlOrJson, indentYamlOrJson,
isFile, isFile,
readYamlOrJson,
runFileSelectionStep, runFileSelectionStep,
} from '../utils/files.js'; } from '../utils/files.js';
@ -84,8 +90,9 @@ interface DeployParams {
warpDeployConfig: WarpRouteDeployConfig; warpDeployConfig: WarpRouteDeployConfig;
} }
interface ApplyParams extends DeployParams { interface WarpApplyParams extends DeployParams {
warpCoreConfig: WarpCoreConfig; warpCoreConfig: WarpCoreConfig;
strategyUrl?: string;
} }
export async function runWarpRouteDeploy({ export async function runWarpRouteDeploy({
@ -424,9 +431,12 @@ function fullyConnectTokens(warpCoreConfig: WarpCoreConfig): void {
} }
} }
export async function runWarpRouteApply(params: ApplyParams): Promise<void> { export async function runWarpRouteApply(
const { warpDeployConfig, warpCoreConfig, context } = params; params: WarpApplyParams,
): Promise<void> {
const { warpDeployConfig, warpCoreConfig, context, strategyUrl } = params;
const { registry, multiProvider, chainMetadata, skipConfirmation } = context; const { registry, multiProvider, chainMetadata, skipConfirmation } = context;
WarpRouteDeployConfigSchema.parse(warpDeployConfig); WarpRouteDeployConfigSchema.parse(warpDeployConfig);
WarpCoreConfigSchema.parse(warpCoreConfig); WarpCoreConfigSchema.parse(warpCoreConfig);
const addresses = await registry.getAddresses(); const addresses = await registry.getAddresses();
@ -475,16 +485,23 @@ export async function runWarpRouteApply(params: ApplyParams): Promise<void> {
); );
const transactions = await evmERC20WarpModule.update(config); const transactions = await evmERC20WarpModule.update(config);
if (transactions.length) { if (transactions.length == 0)
for (const transaction of transactions) { return logGreen(
await multiProvider.sendTransaction(chain, transaction);
}
logGreen(`Warp config updated on ${chain}.`);
} else {
logGreen(
`Warp config on ${chain} is the same as target. No updates needed.`, `Warp config on ${chain} is the same as target. No updates needed.`,
); );
const submitter: TxSubmitterBuilder<ProtocolType> =
await getWarpApplySubmitter({
chain,
context,
strategyUrl,
});
const transactionReceipts = await submitter.submit(...transactions);
if (transactionReceipts && transactionReceipts.length > 0) {
return logGreen(
`✅ Warp config update successfully submitted with ${submitter.txSubmitterType} on ${chain}:\n\n`,
indentYamlOrJson(yamlStringify(transactionReceipts, null, 2), 4),
);
} }
} catch (e) { } catch (e) {
logRed(`Warp config on ${chain} failed to update.`, e); logRed(`Warp config on ${chain} failed to update.`, e);
@ -505,18 +522,13 @@ export async function runWarpRouteApply(params: ApplyParams): Promise<void> {
(chain, _config): _config is any => !warpCoreChains.includes(chain), (chain, _config): _config is any => !warpCoreChains.includes(chain),
); );
const existingTokenMetadata = await HypERC20Deployer.deriveTokenMetadata( extendedConfigs = await deriveMetadataFromExisting(
multiProvider, multiProvider,
existingConfigs, existingConfigs,
extendedConfigs,
); );
extendedConfigs = objMap(extendedConfigs, (_chain, extendedConfig) => {
return {
...extendedConfig,
...existingTokenMetadata,
};
});
const newExtensionContracts = await executeDeploy( const newDeployedContracts = await executeDeploy(
{ {
// TODO: use EvmERC20WarpModule when it's ready // TODO: use EvmERC20WarpModule when it's ready
context, context,
@ -525,21 +537,14 @@ export async function runWarpRouteApply(params: ApplyParams): Promise<void> {
apiKeys, apiKeys,
); );
const existingContractAddresses = objMap( const mergedRouters = mergeAllRouters(
multiProvider,
existingConfigs, existingConfigs,
(chain, config) => ({ newDeployedContracts,
[config.type]: warpCoreConfigByChain[chain].addressOrDenom!, warpCoreConfigByChain,
}),
); );
const mergedRouters = {
...connectContractsMap(
attachContractsMap(existingContractAddresses, hypERC20factories),
multiProvider,
),
...newExtensionContracts,
} as HyperlaneContractsMap<HypERC20Factories>;
await enrollRemoteRouters(mergedRouters, multiProvider); await enrollRemoteRouters(context, mergedRouters, strategyUrl);
const updatedWarpCoreConfig = await getWarpCoreConfig( const updatedWarpCoreConfig = await getWarpCoreConfig(
params, params,
@ -552,6 +557,67 @@ export async function runWarpRouteApply(params: ApplyParams): Promise<void> {
} }
} }
/**
* Retrieves a chain submission strategy from the provided filepath.
* @param submissionStrategyFilepath a filepath to the submission strategy file
* @returns a formatted submission strategy
*/
export function readChainSubmissionStrategy(
submissionStrategyFilepath: string,
): ChainSubmissionStrategy {
const submissionStrategyFileContent = readYamlOrJson(
submissionStrategyFilepath.trim(),
);
return ChainSubmissionStrategySchema.parse(submissionStrategyFileContent);
}
/**
* Derives token metadata from existing config and merges it with extended config.
* @returns The merged Warp route deployment config with token metadata.
*/
async function deriveMetadataFromExisting(
multiProvider: MultiProvider,
existingConfigs: WarpRouteDeployConfig,
extendedConfigs: WarpRouteDeployConfig,
): Promise<WarpRouteDeployConfig> {
const existingTokenMetadata = await HypERC20Deployer.deriveTokenMetadata(
multiProvider,
existingConfigs,
);
return objMap(extendedConfigs, (_chain, extendedConfig) => {
return {
...existingTokenMetadata,
...extendedConfig,
};
});
}
/**
* Merges existing router configs with newly deployed router contracts.
*/
function mergeAllRouters(
multiProvider: MultiProvider,
existingConfigs: WarpRouteDeployConfig,
deployedContractsMap: HyperlaneContractsMap<
HypERC20Factories | HypERC721Factories
>,
warpCoreConfigByChain: ChainMap<WarpCoreConfig['tokens'][number]>,
) {
const existingContractAddresses = objMap(
existingConfigs,
(chain, config) => ({
[config.type]: warpCoreConfigByChain[chain].addressOrDenom!,
}),
);
return {
...connectContractsMap(
attachContractsMap(existingContractAddresses, hypERC20factories),
multiProvider,
),
...deployedContractsMap,
} as HyperlaneContractsMap<HypERC20Factories>;
}
/** /**
* Enroll all deployed routers with each other. * Enroll all deployed routers with each other.
* *
@ -559,10 +625,12 @@ export async function runWarpRouteApply(params: ApplyParams): Promise<void> {
* @param multiProvider - A MultiProvider instance to interact with multiple chains. * @param multiProvider - A MultiProvider instance to interact with multiple chains.
*/ */
async function enrollRemoteRouters( async function enrollRemoteRouters(
context: WriteCommandContext,
deployedContractsMap: HyperlaneContractsMap<HypERC20Factories>, deployedContractsMap: HyperlaneContractsMap<HypERC20Factories>,
multiProvider: MultiProvider, strategyUrl?: string,
): Promise<void> { ): Promise<void> {
logBlue(`Enrolling deployed routers with each other (if not already)...`); logBlue(`Enrolling deployed routers with each other (if not already)...`);
const { multiProvider } = context;
const deployedRouters: ChainMap<Address> = objMap( const deployedRouters: ChainMap<Address> = objMap(
deployedContractsMap, deployedContractsMap,
(_, contracts) => getRouter(contracts).address, (_, contracts) => getRouter(contracts).address,
@ -597,15 +665,23 @@ async function enrollRemoteRouters(
); );
const mutatedConfigTxs: AnnotatedEV5Transaction[] = const mutatedConfigTxs: AnnotatedEV5Transaction[] =
await evmERC20WarpModule.update(mutatedWarpRouteConfig); await evmERC20WarpModule.update(mutatedWarpRouteConfig);
for (const transaction of mutatedConfigTxs) {
const receipt: ContractReceipt = await multiProvider.sendTransaction( if (mutatedConfigTxs.length == 0)
chain, return logGreen(
transaction, `Mutated warp config on ${chain} is the same as target. No updates needed.`,
);
logGreen(
`Successfully enrolled routers on ${chain}: ${receipt.transactionHash}`,
); );
} const submitter: TxSubmitterBuilder<ProtocolType> =
await getWarpApplySubmitter({
chain,
context,
strategyUrl,
});
const transactionReceipts = await submitter.submit(...mutatedConfigTxs);
return logGreen(
`✅ Router enrollment update successfully submitted with ${submitter.txSubmitterType} on ${chain}:\n\n`,
indentYamlOrJson(yamlStringify(transactionReceipts, null, 2), 4),
);
}), }),
); );
} }
@ -744,3 +820,34 @@ function transformIsmConfigForDisplay(ismConfig: IsmConfig): any[] {
return [ismConfig]; return [ismConfig];
} }
} }
/**
* Helper function to get warp apply specific submitter.
*
* @returns the warp apply submitter
*/
async function getWarpApplySubmitter({
chain,
context,
strategyUrl,
}: {
chain: ChainName;
context: WriteCommandContext;
strategyUrl?: string;
}): Promise<TxSubmitterBuilder<ProtocolType>> {
const { chainMetadata, multiProvider } = context;
const submissionStrategy: SubmissionStrategy = strategyUrl
? readChainSubmissionStrategy(strategyUrl)[chain]
: {
submitter: {
type: TxSubmitterType.JSON_RPC,
},
};
const protocol = chainMetadata[chain].protocol;
return getSubmitterBuilder<typeof protocol>({
submissionStrategy,
multiProvider,
});
}

@ -44,7 +44,7 @@ async function getSubmitter<TProtocol extends ProtocolType>(
...submitterMetadata, ...submitterMetadata,
}); });
case TxSubmitterType.GNOSIS_SAFE: case TxSubmitterType.GNOSIS_SAFE:
return new EV5GnosisSafeTxSubmitter(multiProvider, { return EV5GnosisSafeTxSubmitter.create(multiProvider, {
...submitterMetadata, ...submitterMetadata,
}); });
default: default:

@ -1,11 +1,6 @@
import { z } from 'zod'; import type { MultiProvider, SubmissionStrategy } from '@hyperlane-xyz/sdk';
import type {
MultiProvider,
SubmissionStrategySchema,
} from '@hyperlane-xyz/sdk';
export type SubmitterBuilderSettings = { export type SubmitterBuilderSettings = {
submissionStrategy: z.infer<typeof SubmissionStrategySchema>; submissionStrategy: SubmissionStrategy;
multiProvider: MultiProvider; multiProvider: MultiProvider;
}; };

@ -0,0 +1,20 @@
import { $ } from 'zx';
import { ANVIL_KEY, REGISTRY_PATH } from './helpers.js';
/**
* Deploys the Hyperlane core contracts to the specified chain using the provided config.
*/
export async function hyperlaneCoreDeploy(
chain: string,
coreInputPath: string,
) {
return $`yarn workspace @hyperlane-xyz/cli run hyperlane core deploy \
--registry ${REGISTRY_PATH} \
--overrides " " \
--config ${coreInputPath} \
--chain ${chain} \
--key ${ANVIL_KEY} \
--verbosity debug \
--yes`;
}

@ -0,0 +1,116 @@
import { ChainAddresses } from '@hyperlane-xyz/registry';
import {
TokenRouterConfig,
WarpCoreConfig,
WarpCoreConfigSchema,
} from '@hyperlane-xyz/sdk';
import { Address } from '@hyperlane-xyz/utils';
import { getContext } from '../../context/context.js';
import { readYamlOrJson, writeYamlOrJson } from '../../utils/files.js';
import { hyperlaneCoreDeploy } from './core.js';
import { hyperlaneWarpApply, readWarpConfig } from './warp.js';
export const TEST_CONFIGS_PATH = './test-configs';
export const REGISTRY_PATH = `${TEST_CONFIGS_PATH}/anvil`;
export const ANVIL_KEY =
'0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80';
/**
* Retrieves the deployed Warp address from the Warp core config.
*/
export function getDeployedWarpAddress(chain: string, warpCorePath: string) {
const warpCoreConfig: WarpCoreConfig = readYamlOrJson(warpCorePath);
WarpCoreConfigSchema.parse(warpCoreConfig);
return warpCoreConfig.tokens.find((t) => t.chainName === chain)!
.addressOrDenom;
}
/**
* Updates the owner of the Warp route deployment config, and then output to a file
*/
export async function updateWarpOwnerConfig(
chain: string,
owner: Address,
warpCorePath: string,
warpDeployPath: string,
): Promise<string> {
const warpDeployConfig = await readWarpConfig(
chain,
warpCorePath,
warpDeployPath,
);
warpDeployConfig[chain].owner = owner;
writeYamlOrJson(warpDeployPath, warpDeployConfig);
return warpDeployPath;
}
/**
* Updates the Warp route deployment configuration with a new owner, and then applies the changes.
*/
export async function updateOwner(
owner: Address,
chain: string,
warpConfigPath: string,
warpCoreConfigPath: string,
) {
await updateWarpOwnerConfig(chain, owner, warpCoreConfigPath, warpConfigPath);
return hyperlaneWarpApply(warpConfigPath, warpCoreConfigPath);
}
/**
* Extends the Warp route deployment with a new warp config
*/
export async function extendWarpConfig(
chain: string,
chainToExtend: string,
extendedConfig: TokenRouterConfig,
warpCorePath: string,
warpDeployPath: string,
): Promise<string> {
const warpDeployConfig = await readWarpConfig(
chain,
warpCorePath,
warpDeployPath,
);
warpDeployConfig[chainToExtend] = extendedConfig;
writeYamlOrJson(warpDeployPath, warpDeployConfig);
await hyperlaneWarpApply(warpDeployPath, warpCorePath);
return warpDeployPath;
}
/**
* Deploys new core contracts on the specified chain if it doesn't already exist, and returns the chain addresses.
*/
export async function deployOrUseExistingCore(
chain: string,
coreInputPath: string,
key: string,
) {
const { registry } = await getContext({
registryUri: REGISTRY_PATH,
registryOverrideUri: '',
key,
});
const addresses = (await registry.getChainAddresses(chain)) as ChainAddresses;
if (!addresses) {
await hyperlaneCoreDeploy(chain, coreInputPath);
return deployOrUseExistingCore(chain, coreInputPath, key);
}
return addresses;
}
export async function getChainId(chainName: string, key: string) {
const { registry } = await getContext({
registryUri: REGISTRY_PATH,
registryOverrideUri: '',
key,
});
const chainMetadata = await registry.getChainMetadata(chainName);
return String(chainMetadata?.chainId);
}

@ -0,0 +1,68 @@
import { $ } from 'zx';
import { WarpRouteDeployConfig } from '@hyperlane-xyz/sdk';
import { readYamlOrJson } from '../../utils/files.js';
import { ANVIL_KEY, REGISTRY_PATH, getDeployedWarpAddress } from './helpers.js';
$.verbose = true;
/**
* Deploys the Warp route to the specified chain using the provided config.
*/
export async function hyperlaneWarpDeploy(warpCorePath: string) {
return $`yarn workspace @hyperlane-xyz/cli run hyperlane warp deploy \
--registry ${REGISTRY_PATH} \
--overrides " " \
--config ${warpCorePath} \
--key ${ANVIL_KEY} \
--verbosity debug \
--yes`;
}
/**
* Applies updates to the Warp route config.
*/
export async function hyperlaneWarpApply(
warpDeployPath: string,
warpCorePath: string,
) {
return $`yarn workspace @hyperlane-xyz/cli run hyperlane warp apply \
--registry ${REGISTRY_PATH} \
--overrides " " \
--config ${warpDeployPath} \
--warp ${warpCorePath} \
--key ${ANVIL_KEY} \
--verbosity debug \
--yes`;
}
export async function hyperlaneWarpRead(
chain: string,
warpAddress: string,
warpDeployPath: string,
) {
return $`yarn workspace @hyperlane-xyz/cli run hyperlane warp read \
--registry ${REGISTRY_PATH} \
--overrides " " \
--address ${warpAddress} \
--chain ${chain} \
--config ${warpDeployPath}`;
}
/**
* Reads the Warp route deployment config to specified output path.
* @param warpCorePath path to warp core
* @param warpDeployPath path to output the resulting read
* @returns The Warp route deployment config.
*/
export async function readWarpConfig(
chain: string,
warpCorePath: string,
warpDeployPath: string,
): Promise<WarpRouteDeployConfig> {
const warpAddress = getDeployedWarpAddress(chain, warpCorePath);
await hyperlaneWarpRead(chain, warpAddress!, warpDeployPath);
return readYamlOrJson(warpDeployPath);
}

@ -0,0 +1,151 @@
import { expect } from 'chai';
import { Wallet } from 'ethers';
import { ChainAddresses } from '@hyperlane-xyz/registry';
import {
TokenRouterConfig,
TokenType,
WarpRouteDeployConfig,
} from '@hyperlane-xyz/sdk';
import { readYamlOrJson, writeYamlOrJson } from '../utils/files.js';
import {
ANVIL_KEY,
REGISTRY_PATH,
deployOrUseExistingCore,
extendWarpConfig,
getChainId,
updateOwner,
} from './commands/helpers.js';
import { hyperlaneWarpDeploy, readWarpConfig } from './commands/warp.js';
/// To run: 1) start 2 anvils, 2) yarn run tsx tests/warp.zs-test.ts inside of cli/
const CHAIN_NAME_2 = 'anvil2';
const CHAIN_NAME_3 = 'anvil3';
const BURN_ADDRESS = '0x0000000000000000000000000000000000000001';
const EXAMPLES_PATH = './examples';
const CORE_CONFIG_PATH = `${EXAMPLES_PATH}/core-config.yaml`;
const WARP_CONFIG_PATH_EXAMPLE = `${EXAMPLES_PATH}/warp-route-deployment.yaml`;
const TEMP_PATH = '/tmp'; // /temp gets removed at the end of all-test.sh
const WARP_CONFIG_PATH_2 = `${TEMP_PATH}/anvil2/warp-route-deployment-anvil2.yaml`;
const WARP_CORE_CONFIG_PATH_2 = `${REGISTRY_PATH}/deployments/warp_routes/ETH/anvil2-config.yaml`;
describe('WarpApply e2e tests', async function () {
let chain2Addresses: ChainAddresses = {};
this.timeout(0); // No limit timeout since these tests can take a while
before(async function () {
await deployOrUseExistingCore(CHAIN_NAME_2, CORE_CONFIG_PATH, ANVIL_KEY);
chain2Addresses = await deployOrUseExistingCore(
CHAIN_NAME_3,
CORE_CONFIG_PATH,
ANVIL_KEY,
);
// Create a new warp config using the example
const warpConfig: WarpRouteDeployConfig = readYamlOrJson(
WARP_CONFIG_PATH_EXAMPLE,
);
const anvil2Config = { anvil2: { ...warpConfig.anvil1 } };
writeYamlOrJson(WARP_CONFIG_PATH_2, anvil2Config);
});
after(async function () {
this.timeout(2500);
});
beforeEach(async function () {
await hyperlaneWarpDeploy(WARP_CONFIG_PATH_2);
});
it('should burn owner address', async function () {
const warpConfigPath = `${TEMP_PATH}/warp-route-deployment-2.yaml`;
await updateOwner(
BURN_ADDRESS,
CHAIN_NAME_2,
warpConfigPath,
WARP_CORE_CONFIG_PATH_2,
);
const updatedWarpDeployConfig = await readWarpConfig(
CHAIN_NAME_2,
WARP_CORE_CONFIG_PATH_2,
warpConfigPath,
);
expect(updatedWarpDeployConfig.anvil2.owner).to.equal(BURN_ADDRESS);
});
it('should not update the same owner', async () => {
const warpConfigPath = `${TEMP_PATH}/warp-route-deployment-2.yaml`;
await updateOwner(
BURN_ADDRESS,
CHAIN_NAME_2,
warpConfigPath,
WARP_CORE_CONFIG_PATH_2,
);
const { stdout } = await updateOwner(
BURN_ADDRESS,
CHAIN_NAME_2,
warpConfigPath,
WARP_CORE_CONFIG_PATH_2,
);
expect(stdout).to.include(
'Warp config on anvil2 is the same as target. No updates needed.',
);
});
it('should extend an existing warp route', async () => {
// Read existing config into a file
const warpConfigPath = `${TEMP_PATH}/warp-route-deployment-2.yaml`;
await readWarpConfig(CHAIN_NAME_2, WARP_CORE_CONFIG_PATH_2, warpConfigPath);
// Extend with new config
const config: TokenRouterConfig = {
decimals: 18,
mailbox: chain2Addresses!.mailbox,
name: 'Ether',
owner: new Wallet(ANVIL_KEY).address,
symbol: 'ETH',
totalSupply: 0,
type: TokenType.native,
};
await extendWarpConfig(
CHAIN_NAME_2,
CHAIN_NAME_3,
config,
WARP_CORE_CONFIG_PATH_2,
warpConfigPath,
);
const COMBINED_WARP_CORE_CONFIG_PATH = `${REGISTRY_PATH}/deployments/warp_routes/ETH/anvil2-anvil3-config.yaml`;
// Check that chain2 is enrolled in chain1
const updatedWarpDeployConfig1 = await readWarpConfig(
CHAIN_NAME_2,
COMBINED_WARP_CORE_CONFIG_PATH,
warpConfigPath,
);
const chain2Id = await getChainId(CHAIN_NAME_3, ANVIL_KEY);
const remoteRouterKeys1 = Object.keys(
updatedWarpDeployConfig1[CHAIN_NAME_2].remoteRouters!,
);
expect(remoteRouterKeys1).to.include(chain2Id);
// Check that chain1 is enrolled in chain2
const updatedWarpDeployConfig2 = await readWarpConfig(
CHAIN_NAME_3,
COMBINED_WARP_CORE_CONFIG_PATH,
warpConfigPath,
);
const chain1Id = await getChainId(CHAIN_NAME_2, ANVIL_KEY);
const remoteRouterKeys2 = Object.keys(
updatedWarpDeployConfig2[CHAIN_NAME_3].remoteRouters!,
);
expect(remoteRouterKeys2).to.include(chain1Id);
});
});

@ -8,7 +8,15 @@ name: anvil1
protocol: ethereum protocol: ethereum
rpcUrls: rpcUrls:
- http: http://127.0.0.1:8545 - http: http://127.0.0.1:8545
blockExplorers: # Array: List of BlockExplorer configs
# Required fields:
- name: My Chain Explorer # String: Human-readable name for the explorer
url: https://mychain.com/explorer # String: Base URL for the explorer
apiUrl: https://mychain.com/api # String: Base URL for the explorer API
# Optional fields:
apiKey: myapikey # String: API key for the explorer (optional)
family: etherscan # ExplorerFamily: See ExplorerFamily for valid values
nativeToken: nativeToken:
name: Ether name: Ether
symbol: ETH symbol: ETH
decimals: 18 decimals: 18

@ -1,3 +1,6 @@
# Configs for describing chain metadata for use in Hyperlane deployments or apps
# Consists of a map of chain names to metadata
# Schema here: https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/main/typescript/sdk/src/metadata/chainMetadataTypes.ts
--- ---
chainId: 31338 chainId: 31338
domainId: 31338 domainId: 31338
@ -5,3 +8,15 @@ name: anvil2
protocol: ethereum protocol: ethereum
rpcUrls: rpcUrls:
- http: http://127.0.0.1:8555 - http: http://127.0.0.1:8555
blockExplorers: # Array: List of BlockExplorer configs
# Required fields:
- name: My Chain Explorer # String: Human-readable name for the explorer
url: https://mychain.com/explorer # String: Base URL for the explorer
apiUrl: https://mychain.com/api # String: Base URL for the explorer API
# Optional fields:
apiKey: myapikey # String: API key for the explorer (optional)
family: etherscan # ExplorerFamily: See ExplorerFamily for valid values
nativeToken:
name: Ether
symbol: ETH
decimals: 18

@ -0,0 +1,22 @@
# Configs for describing chain metadata for use in Hyperlane deployments or apps
# Consists of a map of chain names to metadata
# Schema here: https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/main/typescript/sdk/src/metadata/chainMetadataTypes.ts
---
chainId: 31347
domainId: 31347
name: anvil3
protocol: ethereum
rpcUrls:
- http: http://127.0.0.1:8600
blockExplorers: # Array: List of BlockExplorer configs
# Required fields:
- name: My Chain Explorer # String: Human-readable name for the explorer
url: https://mychain.com/explorer # String: Base URL for the explorer
apiUrl: https://mychain.com/api # String: Base URL for the explorer API
# Optional fields:
apiKey: myapikey # String: API key for the explorer (optional)
family: etherscan # ExplorerFamily: See ExplorerFamily for valid values
nativeToken:
name: Ether
symbol: ETH
decimals: 18

@ -3,7 +3,7 @@
function cleanup() { function cleanup() {
set +e set +e
pkill -f anvil pkill -f anvil
rm -rf /tmp/anvil* rm -rf /tmp/anvil1
set -e set -e
} }

@ -75,20 +75,20 @@ export class TxSubmitterBuilder<TProtocol extends ProtocolType>
public async submit( public async submit(
...txs: ProtocolTypedTransaction<TProtocol>['transaction'][] ...txs: ProtocolTypedTransaction<TProtocol>['transaction'][]
): Promise<ProtocolTypedReceipt<TProtocol>['receipt'][] | void> { ): Promise<ProtocolTypedReceipt<TProtocol>['receipt'][] | void> {
this.logger.info( this.logger.debug(
`Submitting ${txs.length} transactions to the ${this.currentSubmitter.txSubmitterType} submitter...`, `Submitting ${txs.length} transactions to the ${this.currentSubmitter.txSubmitterType} submitter...`,
); );
let transformedTxs = txs; let transformedTxs = txs;
for (const currentTransformer of this.currentTransformers) { for (const currentTransformer of this.currentTransformers) {
transformedTxs = await currentTransformer.transform(...transformedTxs); transformedTxs = await currentTransformer.transform(...transformedTxs);
this.logger.info( this.logger.debug(
`🔄 Transformed ${transformedTxs.length} transactions with the ${currentTransformer.txTransformerType} transformer...`, `🔄 Transformed ${transformedTxs.length} transactions with the ${currentTransformer.txTransformerType} transformer...`,
); );
} }
const txReceipts = await this.currentSubmitter.submit(...transformedTxs); const txReceipts = await this.currentSubmitter.submit(...transformedTxs);
this.logger.info( this.logger.debug(
`✅ Successfully submitted ${transformedTxs.length} transactions to the ${this.currentSubmitter.txSubmitterType} submitter.`, `✅ Successfully submitted ${transformedTxs.length} transactions to the ${this.currentSubmitter.txSubmitterType} submitter.`,
); );

@ -2,8 +2,9 @@ import { Logger } from 'pino';
import { Address, assert, rootLogger } from '@hyperlane-xyz/utils'; import { Address, assert, rootLogger } from '@hyperlane-xyz/utils';
// prettier-ignore
// @ts-ignore // @ts-ignore
import { getSafe, getSafeService } from '../../../../utils/gnosisSafe.js'; import { canProposeSafeTransactions, getSafe, getSafeService } from '../../../../utils/gnosisSafe.js';
import { MultiProvider } from '../../../MultiProvider.js'; import { MultiProvider } from '../../../MultiProvider.js';
import { PopulatedTransaction, PopulatedTransactions } from '../../types.js'; import { PopulatedTransaction, PopulatedTransactions } from '../../types.js';
import { TxSubmitterType } from '../TxSubmitterTypes.js'; import { TxSubmitterType } from '../TxSubmitterTypes.js';
@ -22,19 +23,47 @@ export class EV5GnosisSafeTxSubmitter implements EV5TxSubmitterInterface {
constructor( constructor(
public readonly multiProvider: MultiProvider, public readonly multiProvider: MultiProvider,
public readonly props: EV5GnosisSafeTxSubmitterProps, public readonly props: EV5GnosisSafeTxSubmitterProps,
private safe: any,
private safeService: any,
) {} ) {}
public async submit(...txs: PopulatedTransactions): Promise<void> { static async create(
const safe = await getSafe( multiProvider: MultiProvider,
this.props.chain, props: EV5GnosisSafeTxSubmitterProps,
this.multiProvider, ): Promise<EV5GnosisSafeTxSubmitter> {
this.props.safeAddress, const { chain, safeAddress } = props;
const { gnosisSafeTransactionServiceUrl } =
multiProvider.getChainMetadata(chain);
assert(
gnosisSafeTransactionServiceUrl,
`Must set gnosisSafeTransactionServiceUrl in the Registry metadata for ${chain}`,
); );
const safeService = await getSafeService(
this.props.chain, const signerAddress = await multiProvider.getSigner(chain).getAddress();
this.multiProvider, const authorized = await canProposeSafeTransactions(
signerAddress,
chain,
multiProvider,
safeAddress,
);
assert(
authorized,
`Signer ${signerAddress} is not an authorized Safe Proposer for ${safeAddress}`,
); );
const nextNonce: number = await safeService.getNextNonce(
const safe = await getSafe(chain, multiProvider, safeAddress);
const safeService = await getSafeService(chain, multiProvider);
return new EV5GnosisSafeTxSubmitter(
multiProvider,
props,
safe,
safeService,
);
}
public async submit(...txs: PopulatedTransactions): Promise<void> {
const nextNonce: number = await this.safeService.getNextNonce(
this.props.safeAddress, this.props.safeAddress,
); );
const safeTransactionBatch: any[] = txs.map( const safeTransactionBatch: any[] = txs.map(
@ -47,23 +76,25 @@ export class EV5GnosisSafeTxSubmitter implements EV5TxSubmitterInterface {
return { to, data, value: value?.toString() ?? '0' }; return { to, data, value: value?.toString() ?? '0' };
}, },
); );
const safeTransaction = await safe.createTransaction({ const safeTransaction = await this.safe.createTransaction({
safeTransactionData: safeTransactionBatch, safeTransactionData: safeTransactionBatch,
options: { nonce: nextNonce }, options: { nonce: nextNonce },
}); });
const safeTransactionData: any = safeTransaction.data; const safeTransactionData: any = safeTransaction.data;
const safeTxHash: string = await safe.getTransactionHash(safeTransaction); const safeTxHash: string = await this.safe.getTransactionHash(
safeTransaction,
);
const senderAddress: Address = await this.multiProvider.getSignerAddress( const senderAddress: Address = await this.multiProvider.getSignerAddress(
this.props.chain, this.props.chain,
); );
const safeSignature: any = await safe.signTransactionHash(safeTxHash); const safeSignature: any = await this.safe.signTransactionHash(safeTxHash);
const senderSignature: string = safeSignature.data; const senderSignature: string = safeSignature.data;
this.logger.debug( this.logger.info(
`Submitting transaction proposal to ${this.props.safeAddress} on ${this.props.chain}: ${safeTxHash}`, `Submitting transaction proposal to ${this.props.safeAddress} on ${this.props.chain}: ${safeTxHash}`,
); );
return safeService.proposeTransaction({ return this.safeService.proposeTransaction({
safeAddress: this.props.safeAddress, safeAddress: this.props.safeAddress,
safeTransactionData, safeTransactionData,
safeTxHash, safeTxHash,

@ -7363,6 +7363,7 @@ __metadata:
yargs: "npm:^17.7.2" yargs: "npm:^17.7.2"
zod: "npm:^3.21.2" zod: "npm:^3.21.2"
zod-validation-error: "npm:^3.3.0" zod-validation-error: "npm:^3.3.0"
zx: "npm:^8.1.4"
bin: bin:
hyperlane: ./dist/cli.js hyperlane: ./dist/cli.js
languageName: unknown languageName: unknown
@ -12397,6 +12398,16 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@types/fs-extra@npm:>=11":
version: 11.0.4
resolution: "@types/fs-extra@npm:11.0.4"
dependencies:
"@types/jsonfile": "npm:*"
"@types/node": "npm:*"
checksum: acc4c1eb0cde7b1f23f3fe6eb080a14832d8fa9dc1761aa444c5e2f0f6b6fa657ed46ebae32fb580a6700fc921b6165ce8ac3e3ba030c3dd15f10ad4dd4cae98
languageName: node
linkType: hard
"@types/glob@npm:^7.1.1, @types/glob@npm:^7.1.3": "@types/glob@npm:^7.1.1, @types/glob@npm:^7.1.3":
version: 7.2.0 version: 7.2.0
resolution: "@types/glob@npm:7.2.0" resolution: "@types/glob@npm:7.2.0"
@ -12485,6 +12496,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@types/jsonfile@npm:*":
version: 6.1.4
resolution: "@types/jsonfile@npm:6.1.4"
dependencies:
"@types/node": "npm:*"
checksum: 309fda20eb5f1cf68f2df28931afdf189c5e7e6bec64ac783ce737bb98908d57f6f58757ad5da9be37b815645a6f914e2d4f3ac66c574b8fe1ba6616284d0e97
languageName: node
linkType: hard
"@types/keyv@npm:^3.1.1, @types/keyv@npm:^3.1.4": "@types/keyv@npm:^3.1.1, @types/keyv@npm:^3.1.4":
version: 3.1.4 version: 3.1.4
resolution: "@types/keyv@npm:3.1.4" resolution: "@types/keyv@npm:3.1.4"
@ -12652,6 +12672,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@types/node@npm:>=20":
version: 22.5.0
resolution: "@types/node@npm:22.5.0"
dependencies:
undici-types: "npm:~6.19.2"
checksum: 89af3bd217b1559b645a9ed16d4ae3add75749814cbd8eefddd1b96003d1973afb1c8a2b23d69f3a8cc6c532e3aa185eaf5cc29a6e7c42c311a2aad4c99430ae
languageName: node
linkType: hard
"@types/node@npm:^10.0.3": "@types/node@npm:^10.0.3":
version: 10.17.60 version: 10.17.60
resolution: "@types/node@npm:10.17.60" resolution: "@types/node@npm:10.17.60"
@ -29137,6 +29166,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"undici-types@npm:~6.19.2":
version: 6.19.8
resolution: "undici-types@npm:6.19.8"
checksum: cf0b48ed4fc99baf56584afa91aaffa5010c268b8842f62e02f752df209e3dea138b372a60a963b3b2576ed932f32329ce7ddb9cb5f27a6c83040d8cd74b7a70
languageName: node
linkType: hard
"undici@npm:^5.11": "undici@npm:^5.11":
version: 5.11.0 version: 5.11.0
resolution: "undici@npm:5.11.0" resolution: "undici@npm:5.11.0"
@ -30753,3 +30789,20 @@ __metadata:
checksum: 1c67216871808c3beaeaf2439adfc589055502665e8fc4267abf36dc4f673018cd15575e8f38a3eb9b8edb43356d91a809fc6ded3fab4b7f5d6a3982d0b97c77 checksum: 1c67216871808c3beaeaf2439adfc589055502665e8fc4267abf36dc4f673018cd15575e8f38a3eb9b8edb43356d91a809fc6ded3fab4b7f5d6a3982d0b97c77
languageName: node languageName: node
linkType: hard linkType: hard
"zx@npm:^8.1.4":
version: 8.1.4
resolution: "zx@npm:8.1.4"
dependencies:
"@types/fs-extra": "npm:>=11"
"@types/node": "npm:>=20"
dependenciesMeta:
"@types/fs-extra":
optional: true
"@types/node":
optional: true
bin:
zx: build/cli.js
checksum: 1ffa4c51a1edad25de0729d09667b3d1b7b4f9c8f6b4300e34d85f8f18c2e768f7e297b9bfad4d3b8a24792b3f14085f229933d0a224febba49ac2588ed155b1
languageName: node
linkType: hard

Loading…
Cancel
Save