feat: add dry-run support for warp deploy + gas stats (#3586)

### Description

* New feature to allow users to dry-run the warp deploy script against a
forked (base/origin) network of their choice
* To run: `yarn build && yarn hyperlane deploy warp --dry-run` || `yarn
build && yarn hyperlane deploy warp -d`
* Externally enables: `hyperlane deploy warp --dry-run` || `hyperlane
deploy warp -d`
* Also adds gas usage util for both warp & core deployments in all
contexts, e.g.
* When running a vanilla core deploy, for example, between alfajores and
fuji, you will now see:
```
️ Gas Usage Statistics
        - Gas required for core deploy on alfajores: 0.0058686745 CELO
        - Gas required for core deploy on fuji: 0.0239308515 AVAX
```

### Drive-by changes

* None

### Related issues

* Fixes https://github.com/hyperlane-xyz/issues/issues/819

### Backward compatibility

* Yes

### Testing

* Note: `hl` == `yarn build && yarn hyperlane`. The below tests are only
a sample and are not inclusive.

#### Manual Testing
  * With `anvil` NOT running in separate instance:
    * `hl deploy warp -d`
      * Throws:
```
Error: No active anvil node detected.
        Please run `anvil` in a separate instance.
```
  * With `anvil` running in separate instance:
* `hl deploy warp -d -k
c0052e22df5d1f4ae7c51e254Xx00Xx0eb833453eaed6301xXxxx8a30d92d10a` (any
private key)
* Throws `Error: Invalid address length. Please ensure you are passing
an address and not a private key.`
* `hl deploy warp -d -k 0x16F4898F47c085C41d7Cc6b1dc0xX0xXX017dcBb` (any
public address)
       * Output:
```
🔎 Verifying anvil node is running...
 Successfully verified anvil node is running
Using warp route deployment config at ./configs/warp-route-deployment.yaml
No chain config file provided
? Do you want to use some core deployment address artifacts? This is required for PI chains (non-core chains). no
Forking alfajores for dry-run...
 Successfully forked alfajores for dry-run
Impersonating account (0x16F4898F47c085C41d7Cc6b1dc72B91EA617dcBb)...
 Successfully impersonated account (0x16F4898F47c085C41d7Cc6b1dc72B91EA617dcBb)
...
 Hyp token deployments complete
Writing deployment artifacts
Deployment is complete!
Contract address artifacts are in artifacts/dry-run_warp-route-deployment-2024-04-16-12-27-02.json
Warp config is in artifacts/dry-run_warp-config-2024-04-16-12-27-02.json
️ Gas Usage Statistics
        - Gas required for warp dry-run on alfajores: 0.013310162514578124 CELO
Resetting forked network...
 Successfully reset forked network
 Warp dry-run completed successfully
```

#### CI Testing
  * Successful CI-backed integration/regression testing via `ci-test.sh`
pull/3620/head
Noah Bayindirli 🥂 7 months ago committed by GitHub
parent 2b3f75836a
commit aea79c686e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 5
      .changeset/early-crabs-float.md
  2. 1
      rust/.gitignore
  3. 35
      typescript/cli/ci-test.sh
  4. 17
      typescript/cli/examples/dry-run/anvil-chains.yaml
  5. 8
      typescript/cli/examples/dry-run/ism.yaml
  6. 5
      typescript/cli/examples/dry-run/warp-route-deployment.yaml
  7. 45
      typescript/cli/src/commands/deploy.ts
  8. 3
      typescript/cli/src/commands/options.ts
  9. 52
      typescript/cli/src/deploy/core.ts
  10. 20
      typescript/cli/src/deploy/dry-run.ts
  11. 71
      typescript/cli/src/deploy/utils.ts
  12. 81
      typescript/cli/src/deploy/warp.ts
  13. 16
      typescript/cli/src/utils/files.ts
  14. 8
      typescript/cli/src/utils/fork.ts

@ -0,0 +1,5 @@
---
'@hyperlane-xyz/cli': minor
---
Adds single-chain dry-run support for deploying warp routes & gas estimation for core and warp route dry-run deployments.

1
rust/.gitignore vendored

@ -4,3 +4,4 @@ relayerdb
kathydb
hyperlane_db
config/test_config.json
validator_db_anvil*

@ -29,6 +29,7 @@ _main() {
DEPLOYER=$(cast rpc eth_accounts | jq -r '.[0]');
run_hyperlane_deploy_core_dry_run;
run_hyperlane_deploy_warp_dry_run;
reset_anvil;
@ -124,12 +125,16 @@ kill_anvil() {
}
run_hyperlane_deploy_core_dry_run() {
if [ "$TEST_TYPE" == $TEST_TYPE_PI_CORE ]; then
return;
fi
BEFORE_CORE_DRY_RUN=$(cast balance $DEPLOYER --rpc-url http://127.0.0.1:${CHAIN1_PORT});
echo -e "\nDry-running contract deployments to Alfajores"
yarn workspace @hyperlane-xyz/cli run hyperlane deploy core --dry-run \
--targets alfajores \
--chains ${EXAMPLES_PATH}/anvil-chains.yaml \
--chains ${EXAMPLES_PATH}/dry-run/anvil-chains.yaml \
--artifacts /tmp/empty-artifacts.json \
$(if [ "$HOOK_FLAG" == "true" ]; then echo "--hook ${EXAMPLES_PATH}/hooks.yaml"; fi) \
--ism ${EXAMPLES_PATH}/ism.yaml \
@ -150,6 +155,33 @@ run_hyperlane_deploy_core_dry_run() {
AGENT_CONFIG_FILENAME=`ls -t1 /tmp | grep agent-config | head -1`
}
run_hyperlane_deploy_warp_dry_run() {
if [ "$TEST_TYPE" == $TEST_TYPE_PI_CORE ]; then
return;
fi
BEFORE_WARP_DRY_RUN=$(cast balance $DEPLOYER --rpc-url http://127.0.0.1:${CHAIN1_PORT});
echo -e "\nDry-running warp route deployments to Alfajores"
yarn workspace @hyperlane-xyz/cli run hyperlane deploy warp --dry-run \
--chains ${EXAMPLES_PATH}/dry-run/anvil-chains.yaml \
--core $CORE_ARTIFACTS_PATH \
--config ${EXAMPLES_PATH}/dry-run/warp-route-deployment.yaml \
--out /tmp \
--key 0xfaD1C94469700833717Fa8a3017278BC1cA8031C \
--yes
AFTER_WARP_DRY_RUN=$(cast balance $DEPLOYER --rpc-url http://127.0.0.1:${CHAIN1_PORT})
GAS_PRICE=$(cast gas-price --rpc-url http://127.0.0.1:${CHAIN1_PORT})
WARP_MIN_GAS=$(bc <<< "($BEFORE_WARP_DRY_RUN - $AFTER_WARP_DRY_RUN) / $GAS_PRICE")
echo "Gas used: $WARP_MIN_GAS"
WARP_ARTIFACTS_PATH=`find /tmp/dry-run_warp-route-deployment* -type f -exec ls -t1 {} + | head -1`
echo "Warp dry-run artifacts:"
echo $WARP_ARTIFACTS_PATH
cat $WARP_ARTIFACTS_PATH
}
run_hyperlane_deploy_core() {
BEFORE_CORE=$(cast balance $DEPLOYER --rpc-url http://127.0.0.1:${CHAIN1_PORT});
@ -346,4 +378,5 @@ run_hyperlane_status() {
}
_main "$@";
exit;

@ -0,0 +1,17 @@
anvil:
chainId: 31337
domainId: 31337
name: anvil
protocol: ethereum
rpcUrls:
- http: http://127.0.0.1:8545
nativeToken:
name: Ether
symbol: ETH
decimals: 18
alfajores:
rpcUrls:
- http: https://alfajores-forno.celo-testnet.org
blocks:
confirmations: 1
estimateBlockTime: 1

@ -0,0 +1,8 @@
anvil:
threshold: 1
validators:
- '0xa0ee7a142d267c1f36714e4a8f75612f20a79720'
alfajores:
threshold: 1
validators:
- '0xa0ee7a142d267c1f36714e4a8f75612f20a79720'

@ -0,0 +1,5 @@
base:
chainName: alfajores
type: native
synthetics:
- chainName: fuji

@ -25,11 +25,18 @@ import {
warpConfigCommandOption,
} from './options.js';
export enum Command {
DEPLOY = 'deploy',
KURTOSIS_AGENTS = 'kurtosis-agents',
CORE = 'core',
WARP = 'warp',
}
/**
* Parent command
*/
export const deployCommand: CommandModule = {
command: 'deploy',
command: Command.DEPLOY,
describe: 'Permissionlessly deploy a Hyperlane contracts or extensions',
builder: (yargs) =>
yargs
@ -45,7 +52,7 @@ export const deployCommand: CommandModule = {
* Agent command
*/
const agentCommand: CommandModule = {
command: 'kurtosis-agents',
command: Command.KURTOSIS_AGENTS,
describe: 'Deploy Hyperlane agents with Kurtosis',
builder: (yargs) =>
yargs.options<AgentCommandOptions>({
@ -75,7 +82,7 @@ const agentCommand: CommandModule = {
* Core command
*/
const coreCommand: CommandModule = {
command: 'core',
command: Command.CORE,
describe: 'Deploy core Hyperlane contracts',
builder: (yargs) =>
yargs.options<CoreCommandOptions>({
@ -133,7 +140,7 @@ const coreCommand: CommandModule = {
* Warp command
*/
const warpCommand: CommandModule = {
command: 'warp',
command: Command.WARP,
describe: 'Deploy Warp Route contracts',
builder: (yargs) =>
yargs.options<WarpCommandOptions>({
@ -143,6 +150,7 @@ const warpCommand: CommandModule = {
out: outDirCommandOption,
key: keyCommandOption,
yes: skipConfirmationOption,
'dry-run': dryRunOption,
}),
handler: async (argv: any) => {
const key: string | undefined = argv.key;
@ -151,14 +159,27 @@ const warpCommand: CommandModule = {
const coreArtifactsPath: string | undefined = argv.core;
const outPath: string = argv.out;
const skipConfirmation: boolean = argv.yes;
await runWarpRouteDeploy({
key,
chainConfigPath,
warpRouteDeploymentConfigPath,
coreArtifactsPath,
outPath,
skipConfirmation,
});
const dryRun: boolean = argv.dryRun;
logGray(`Hyperlane warp route deployment${dryRun ? ' dry-run' : ''}`);
logGray('------------------------------------------------');
if (dryRun) await verifyAnvil();
try {
await runWarpRouteDeploy({
key,
chainConfigPath,
warpRouteDeploymentConfigPath,
coreArtifactsPath,
outPath,
skipConfirmation,
dryRun,
});
} catch (error: any) {
evaluateIfDryRunFailure(error, dryRun);
throw error;
}
process.exit(0);
},
};

@ -40,6 +40,7 @@ export type WarpCommandOptions = CommandOptions & {
out: Options;
key: Options;
yes: Options;
'dry-run': Options;
};
export const coreTargetsCommandOption: Options = {
@ -144,5 +145,5 @@ export const dryRunOption: Options = {
description:
'Simulate deployment on forked network. Please ensure an anvil node instance is running during execution via `anvil`.',
default: false,
alias: 'd',
alias: ['d', 'dr'],
};

@ -29,6 +29,7 @@ import {
} from '@hyperlane-xyz/sdk';
import { Address, objFilter, objMerge } from '@hyperlane-xyz/utils';
import { Command } from '../commands/deploy.js';
import { runDeploymentArtifactStep } from '../config/artifacts.js';
import { presetHookConfigs, readHooksConfigMap } from '../config/hooks.js';
import { readIsmConfig } from '../config/ism.js';
@ -50,16 +51,17 @@ import {
} from '../logger.js';
import { runMultiChainSelectionStep } from '../utils/chains.js';
import {
ArtifactsFile,
getArtifactsFiles,
prepNewArtifactsFiles,
runFileSelectionStep,
writeJson,
} from '../utils/files.js';
import { resetFork } from '../utils/fork.js';
import {
completeDeploy,
isISMConfig,
isZODISMConfig,
prepareDeploy,
runPreflightChecksForChains,
} from './utils.js';
@ -146,9 +148,25 @@ export async function runCoreDeploy({
...deploymentParams,
minGas: MINIMUM_CORE_DEPLOY_GAS,
});
const userAddress = dryRun ? key! : await signer.getAddress();
const initialBalances = await prepareDeploy(
multiProvider,
userAddress,
chains,
);
await executeDeploy(deploymentParams);
if (dryRun) await resetFork();
await completeDeploy(
Command.CORE,
initialBalances,
multiProvider,
userAddress,
chains,
dryRun,
);
}
function runArtifactStep(
@ -305,7 +323,13 @@ async function executeDeploy({
const [contractsFilePath, agentFilePath] = prepNewArtifactsFiles(
outPath,
getArtifactsFiles(dryRun),
getArtifactsFiles(
[
{ filename: 'core-deployment', description: 'Contract addresses' },
{ filename: 'agent-config', description: 'Agent configs' },
],
dryRun,
),
);
const owner = await signer.getAddress();
@ -367,7 +391,7 @@ async function executeDeploy({
}
artifacts = objMerge(artifacts, isms);
artifacts = writeMergedAddresses(contractsFilePath, artifacts, coreContracts);
logGreen('Core contracts deployed');
logGreen('Core contracts deployed');
log('Writing agent configs');
await writeAgentConfig(agentFilePath, artifacts, chains, multiProvider);
@ -378,24 +402,6 @@ async function executeDeploy({
logBlue(`Agent configs are in ${agentFilePath}`);
}
/**
* Retrieves artifacts file metadata for the current command.
* @param dryRun whether or not the current command is being dry-run
* @returns the artifacts files
*/
function getArtifactsFiles(dryRun: boolean): Array<ArtifactsFile> {
const coreDeploymentFile = {
filename: dryRun ? 'dry-run_core-deployment' : 'core-deployment',
description: 'Contract addresses',
};
const agentConfigFile = {
filename: dryRun ? 'dry-run_agent-config' : 'agent-config',
description: 'Agent configs',
};
return [coreDeploymentFile, agentConfigFile];
}
function buildIsmConfig(
owner: Address,
local: ChainName,

@ -1,7 +1,15 @@
import { MultiProvider } from '@hyperlane-xyz/sdk';
import { Command } from '../commands/deploy.js';
import { logGray, logGreen, warnYellow } from '../logger.js';
import { ANVIL_RPC_METHODS, getLocalProvider, setFork } from '../utils/fork.js';
import {
ANVIL_RPC_METHODS,
getLocalProvider,
resetFork,
setFork,
} from '../utils/fork.js';
import { toUpperCamelCase } from './utils.js';
/**
* Forks a provided network onto MultiProvider
@ -23,7 +31,7 @@ export async function forkNetworkToMultiProvider(
* Ensures an anvil node is running locally.
*/
export async function verifyAnvil() {
logGray('Verifying anvil node is running...');
logGray('🔎 Verifying anvil node is running...');
const provider = getLocalProvider();
try {
@ -34,7 +42,7 @@ export async function verifyAnvil() {
\tPlease run \`anvil\` in a separate instance.`);
}
logGreen('Successfully verified anvil node is running');
logGreen('Successfully verified anvil node is running');
}
/**
@ -48,3 +56,9 @@ export function evaluateIfDryRunFailure(error: any, dryRun: boolean) {
'⛔ [dry-run] The current RPC may not support forking. Please consider using a different RPC provider.',
);
}
export async function completeDryRun(command: Command) {
await resetFork();
logGreen(`${toUpperCamelCase(command)} dry-run completed successfully`);
}

@ -1,4 +1,4 @@
import { ethers } from 'ethers';
import { BigNumber, ethers } from 'ethers';
import {
ChainMap,
@ -7,13 +7,17 @@ import {
MultiProvider,
MultisigConfig,
} from '@hyperlane-xyz/sdk';
import { ProtocolType } from '@hyperlane-xyz/utils';
import { Address, ProtocolType } from '@hyperlane-xyz/utils';
import { Command } from '../commands/deploy.js';
import { parseIsmConfig } from '../config/ism.js';
import { log, logGreen } from '../logger.js';
import { log, logGreen, logPink } from '../logger.js';
import { assertGasBalances } from '../utils/balances.js';
import { getLocalProvider } from '../utils/fork.js';
import { assertSigner } from '../utils/keys.js';
import { completeDryRun } from './dry-run.js';
export async function runPreflightChecks({
origin,
remotes,
@ -32,11 +36,11 @@ export async function runPreflightChecks({
log('Running pre-flight checks...');
if (!origin || !remotes?.length) throw new Error('Invalid chain selection');
logGreen('Chain selections are valid');
logGreen('Chain selections are valid');
if (remotes.includes(origin))
throw new Error('Origin and remotes must be distinct');
logGreen('Origin and remote are distinct');
logGreen('Origin and remote are distinct');
return runPreflightChecksForChains({
chains: [origin, ...remotes],
@ -71,10 +75,10 @@ export async function runPreflightChecksForChains({
if (metadata.protocol !== ProtocolType.Ethereum)
throw new Error('Only Ethereum chains are supported for now');
}
logGreen('Chains are valid');
logGreen('Chains are valid');
assertSigner(signer);
logGreen('Signer is valid');
logGreen('Signer is valid');
await assertGasBalances(
multiProvider,
@ -82,7 +86,7 @@ export async function runPreflightChecksForChains({
chainsToGasCheck ?? chains,
minGas,
);
logGreen('Balances are sufficient');
logGreen('Balances are sufficient');
}
// from parsed types
@ -96,3 +100,54 @@ export function isISMConfig(
export function isZODISMConfig(filepath: string): boolean {
return parseIsmConfig(filepath).success;
}
export async function prepareDeploy(
multiProvider: MultiProvider,
userAddress: Address,
chains: ChainName[],
dryRun: boolean = false,
): Promise<Record<string, BigNumber>> {
const initialBalances: Record<string, BigNumber> = {};
await Promise.all(
chains.map(async (chain: ChainName) => {
const provider = dryRun
? getLocalProvider()
: multiProvider.getProvider(chain);
const currentBalance = await provider.getBalance(userAddress);
initialBalances[chain] = currentBalance;
}),
);
return initialBalances;
}
export async function completeDeploy(
command: Command,
initialBalances: Record<string, BigNumber>,
multiProvider: MultiProvider,
userAddress: Address,
chains: ChainName[],
dryRun: boolean = false,
) {
if (chains.length > 0) logPink(` Gas Usage Statistics`);
for (const chain of chains) {
const provider = dryRun
? getLocalProvider()
: multiProvider.getProvider(chain);
const currentBalance = await provider.getBalance(userAddress);
const balanceDelta = initialBalances[chain].sub(currentBalance);
if (dryRun && balanceDelta.lt(0)) break;
logPink(
`\t- Gas required for ${command} ${
dryRun ? 'dry-run' : 'deploy'
} on ${chain}: ${ethers.utils.formatEther(balanceDelta)} ${
multiProvider.getChainMetadata(chain).nativeToken?.symbol
}`,
);
}
if (dryRun) await completeDryRun(command);
}
export function toUpperCamelCase(string: string) {
return string.charAt(0).toUpperCase() + string.slice(1);
}

@ -22,21 +22,27 @@ import {
} from '@hyperlane-xyz/sdk';
import { Address, ProtocolType, objMap } from '@hyperlane-xyz/utils';
import { Command } from '../commands/deploy.js';
import {
WarpRouteDeployConfig,
readWarpRouteDeployConfig,
} from '../config/warp.js';
import { MINIMUM_WARP_DEPLOY_GAS } from '../consts.js';
import { getContext, getMergedContractAddresses } from '../context.js';
import {
getContext,
getDryRunContext,
getMergedContractAddresses,
} from '../context.js';
import { log, logBlue, logGray, logGreen } from '../logger.js';
import {
getArtifactsFiles,
isFile,
prepNewArtifactsFiles,
runFileSelectionStep,
writeJson,
} from '../utils/files.js';
import { runPreflightChecks } from './utils.js';
import { completeDeploy, prepareDeploy, runPreflightChecks } from './utils.js';
export async function runWarpRouteDeploy({
key,
@ -45,6 +51,7 @@ export async function runWarpRouteDeploy({
coreArtifactsPath,
outPath,
skipConfirmation,
dryRun,
}: {
key?: string;
chainConfigPath: string;
@ -52,14 +59,8 @@ export async function runWarpRouteDeploy({
coreArtifactsPath?: string;
outPath: string;
skipConfirmation: boolean;
dryRun: boolean;
}) {
const { multiProvider, signer, coreArtifacts } = await getContext({
chainConfigPath,
coreConfig: { coreArtifactsPath },
keyConfig: { key },
skipConfirmation,
});
if (
!warpRouteDeploymentConfigPath ||
!isFile(warpRouteDeploymentConfigPath)
@ -80,6 +81,21 @@ export async function runWarpRouteDeploy({
warpRouteDeploymentConfigPath,
);
const { multiProvider, signer, coreArtifacts } = dryRun
? await getDryRunContext({
chainConfigPath,
chains: [warpRouteConfig.base.chainName],
coreConfig: { coreArtifactsPath },
keyConfig: { key },
skipConfirmation,
})
: await getContext({
chainConfigPath,
coreConfig: { coreArtifactsPath },
keyConfig: { key },
skipConfirmation,
});
const configs = await runBuildConfigStep({
warpRouteConfig,
coreArtifacts,
@ -94,6 +110,7 @@ export async function runWarpRouteDeploy({
multiProvider,
outPath,
skipConfirmation,
dryRun,
};
logBlue('Warp route deployment plan');
@ -103,7 +120,26 @@ export async function runWarpRouteDeploy({
...deploymentParams,
minGas: MINIMUM_WARP_DEPLOY_GAS,
});
const userAddress = dryRun ? key! : await signer.getAddress();
const chains = [deploymentParams.origin, ...configs.remotes];
const initialBalances = await prepareDeploy(
multiProvider,
userAddress,
chains,
);
await executeDeploy(deploymentParams);
await completeDeploy(
Command.WARP,
initialBalances,
multiProvider,
userAddress,
chains,
dryRun,
);
}
async function runBuildConfigStep({
@ -219,6 +255,7 @@ interface DeployParams {
multiProvider: MultiProvider;
outPath: string;
skipConfirmation: boolean;
dryRun: boolean;
}
async function runDeployPlanStep({
@ -253,17 +290,31 @@ async function executeDeploy(params: DeployParams) {
const { configMap, isNft, multiProvider, outPath } = params;
const [contractsFilePath, tokenConfigPath] = prepNewArtifactsFiles(outPath, [
{ filename: 'warp-route-deployment', description: 'Contract addresses' },
{ filename: 'warp-config', description: 'Warp config' },
]);
const [contractsFilePath, tokenConfigPath] = prepNewArtifactsFiles(
outPath,
getArtifactsFiles(
[
{
filename: 'warp-route-deployment',
description: 'Contract addresses',
},
{ filename: 'warp-config', description: 'Warp config' },
],
params.dryRun,
),
);
const deployer = isNft
? new HypERC721Deployer(multiProvider)
: new HypERC20Deployer(multiProvider);
const deployedContracts = await deployer.deploy(configMap);
logGreen('Hyp token deployments complete');
const config = params.dryRun
? { [params.origin]: configMap[params.origin] }
: configMap;
const deployedContracts = await deployer.deploy(config);
logGreen('✅ Hyp token deployments complete');
log('Writing deployment artifacts');
writeTokenDeploymentArtifacts(contractsFilePath, deployedContracts, params);

@ -163,6 +163,22 @@ export function prepNewArtifactsFiles(
return newPaths;
}
/**
* Retrieves artifacts file metadata for the current command.
* @param dryRun whether or not the current command is being dry-run
* @returns the artifacts files
*/
export function getArtifactsFiles(
defaultFiles: ArtifactsFile[],
dryRun: boolean = false,
): Array<ArtifactsFile> {
if (dryRun)
defaultFiles.map((defaultFile: ArtifactsFile) => {
defaultFile.filename = `dry-run_${defaultFile.filename}`;
});
return defaultFiles;
}
export async function runFileSelectionStep(
folderPath: string,
description: string,

@ -33,7 +33,7 @@ export const resetFork = async () => {
},
]);
logGreen(`Successfully reset forked network`);
logGreen(`Successfully reset forked network`);
};
/**
@ -60,7 +60,7 @@ export const setFork = async (
multiProvider.setProvider(chain, provider);
logGreen(`Successfully forked ${chain} for dry-run`);
logGreen(`Successfully forked ${chain} for dry-run`);
};
/**
@ -76,7 +76,7 @@ export const impersonateAccount = async (
const provider = getLocalProvider();
await provider.send(ANVIL_RPC_METHODS.IMPERSONATE_ACCOUNT, [address]);
logGreen(`Successfully impersonated account (${address})`);
logGreen(`Successfully impersonated account (${address})`);
return provider.getSigner(address);
};
@ -99,7 +99,7 @@ export const stopImpersonatingAccount = async (address: Address) => {
]);
logGreen(
`Successfully stopped account impersonation for address (${address})`,
`Successfully stopped account impersonation for address (${address})`,
);
};

Loading…
Cancel
Save