@hyperlane-xyz/cli
Version:
A command-line utility for common Hyperlane operations
300 lines • 15.6 kB
JavaScript
import { JsonRpcProvider } from '@ethersproject/providers';
import * as chai from 'chai';
import chaiAsPromised from 'chai-as-promised';
import { Wallet } from 'ethers';
import { HookType, IsmType, TokenType, normalizeConfig, } from '@hyperlane-xyz/sdk';
import { readYamlOrJson, writeYamlOrJson } from '../../utils/files.js';
import { ANVIL_KEY, CHAIN_2_METADATA_PATH, CHAIN_NAME_2, CHAIN_NAME_3, CORE_CONFIG_PATH, DEFAULT_E2E_TEST_TIMEOUT, KeyBoardKeys, WARP_DEPLOY_OUTPUT_PATH, deploy4626Vault, deployOrUseExistingCore, deployToken, getCombinedWarpRoutePath, handlePrompts, } from '../commands/helpers.js';
import { hyperlaneWarpDeploy, hyperlaneWarpDeployRaw, hyperlaneWarpSendRelay, readWarpConfig, } from '../commands/warp.js';
chai.use(chaiAsPromised);
const expect = chai.expect;
chai.should();
const WARP_CORE_CONFIG_PATH_2_3 = getCombinedWarpRoutePath('VAULT', [
CHAIN_NAME_2,
CHAIN_NAME_3,
]);
describe('hyperlane warp deploy e2e tests', async function () {
this.timeout(DEFAULT_E2E_TEST_TIMEOUT);
let chain2Addresses = {};
let chain3Addresses = {};
let ownerAddress;
let walletChain2;
before(async function () {
const chain2Metadata = readYamlOrJson(CHAIN_2_METADATA_PATH);
const providerChain2 = new JsonRpcProvider(chain2Metadata.rpcUrls[0].http);
walletChain2 = new Wallet(ANVIL_KEY).connect(providerChain2);
ownerAddress = walletChain2.address;
[chain2Addresses, chain3Addresses] = await Promise.all([
deployOrUseExistingCore(CHAIN_NAME_2, CORE_CONFIG_PATH, ANVIL_KEY),
deployOrUseExistingCore(CHAIN_NAME_3, CORE_CONFIG_PATH, ANVIL_KEY),
]);
});
async function assertWarpRouteConfig(warpDeployConfig, warpCoreConfigPath, chainName, expectedMetadata) {
const currentWarpDeployConfig = await readWarpConfig(chainName, warpCoreConfigPath, WARP_DEPLOY_OUTPUT_PATH);
expect(currentWarpDeployConfig[chainName].type).to.equal(warpDeployConfig[chainName].type);
expect(currentWarpDeployConfig[chainName].decimals).to.equal(warpDeployConfig[chainName].decimals ?? expectedMetadata.decimals);
expect(currentWarpDeployConfig[chainName].symbol).to.equal(warpDeployConfig[chainName].symbol ?? expectedMetadata.symbol);
expect(currentWarpDeployConfig[chainName].mailbox).to.equal(chain2Addresses.mailbox);
}
describe('hyperlane warp deploy --config ...', () => {
it(`should exit early when the provided deployment file does not exist`, async function () {
const nonExistingFilePath = 'non-existing-path';
// Currently if the file provided in the config flag does not exist a prompt will still be shown to the
// user to enter a valid file and then it will finally fail
const steps = [
{
check: (currentOutput) => currentOutput.includes('Select Warp route deployment config file'),
input: `${KeyBoardKeys.ARROW_DOWN}${KeyBoardKeys.ENTER}`,
},
{
check: (currentOutput) => currentOutput.includes('Enter Warp route deployment config filepath'),
input: `${nonExistingFilePath}${KeyBoardKeys.ENTER}`,
},
];
const output = hyperlaneWarpDeployRaw({
warpCorePath: nonExistingFilePath,
})
.stdio('pipe')
.nothrow();
const finalOutput = await handlePrompts(output, steps);
expect(finalOutput.exitCode).to.equal(1);
expect(finalOutput
.text()
.includes(`No "Warp route deployment config" found in`) ||
finalOutput
.text()
.includes(`Invalid file format for ${nonExistingFilePath}`)).to.be.true;
});
it(`should successfully deploy a ${TokenType.collateral} -> ${TokenType.synthetic} warp route`, async function () {
const token = await deployToken(ANVIL_KEY, CHAIN_NAME_2);
const [expectedTokenSymbol, expectedTokenDecimals] = await Promise.all([
token.symbol(),
token.decimals(),
]);
const COMBINED_WARP_CORE_CONFIG_PATH = getCombinedWarpRoutePath(expectedTokenSymbol, [CHAIN_NAME_2, CHAIN_NAME_3]);
const warpConfig = {
[CHAIN_NAME_2]: {
type: TokenType.collateral,
token: token.address,
mailbox: chain2Addresses.mailbox,
owner: ownerAddress,
},
[CHAIN_NAME_3]: {
type: TokenType.synthetic,
mailbox: chain3Addresses.mailbox,
owner: ownerAddress,
},
};
writeYamlOrJson(WARP_DEPLOY_OUTPUT_PATH, warpConfig);
const steps = [
{
check: (currentOutput) => currentOutput.includes('Please enter the private key for chain'),
input: `${ANVIL_KEY}${KeyBoardKeys.ENTER}`,
},
{
check: (currentOutput) => currentOutput.includes('Please enter the private key for chain'),
input: `${ANVIL_KEY}${KeyBoardKeys.ENTER}`,
},
{
check: (currentOutput) => currentOutput.includes('Is this deployment plan correct?'),
input: KeyBoardKeys.ENTER,
},
];
// Deploy
const output = hyperlaneWarpDeployRaw({
warpCorePath: WARP_DEPLOY_OUTPUT_PATH,
})
.stdio('pipe')
.nothrow();
const finalOutput = await handlePrompts(output, steps);
// Assertions
expect(finalOutput.exitCode).to.equal(0);
for (const chainName of [CHAIN_NAME_2, CHAIN_NAME_3]) {
await assertWarpRouteConfig(warpConfig, COMBINED_WARP_CORE_CONFIG_PATH, chainName, { decimals: expectedTokenDecimals, symbol: expectedTokenSymbol });
}
});
});
describe('hyperlane warp deploy --config ... --yes', () => {
it(`should exit early when the provided deployment file does not exist and the skip flag is provided`, async function () {
const nonExistingFilePath = 'non-existing-path';
// Currently if the file provided in the config flag does not exist a prompt will still be shown to the
// user to enter a valid file and then it will finally fail
const steps = [
{
check: (currentOutput) => currentOutput.includes('Select Warp route deployment config file'),
input: `${KeyBoardKeys.ARROW_DOWN}${KeyBoardKeys.ENTER}`,
},
{
check: (currentOutput) => currentOutput.includes('Enter Warp route deployment config filepath'),
input: `${nonExistingFilePath}${KeyBoardKeys.ENTER}`,
},
];
const output = hyperlaneWarpDeployRaw({
warpCorePath: nonExistingFilePath,
skipConfirmationPrompts: true,
})
.stdio('pipe')
.nothrow();
const finalOutput = await handlePrompts(output, steps);
expect(finalOutput.exitCode).to.equal(1);
expect(finalOutput.text()).to.include(`Warp route deployment config is required`);
});
it(`should successfully deploy a ${TokenType.collateral} -> ${TokenType.synthetic} warp route`, async function () {
const token = await deployToken(ANVIL_KEY, CHAIN_NAME_2);
const [expectedTokenSymbol, expectedTokenDecimals] = await Promise.all([
token.symbol(),
token.decimals(),
]);
console.log(expectedTokenDecimals);
const COMBINED_WARP_CORE_CONFIG_PATH = getCombinedWarpRoutePath(expectedTokenSymbol, [CHAIN_NAME_2, CHAIN_NAME_3]);
const warpConfig = {
[CHAIN_NAME_2]: {
type: TokenType.collateral,
token: token.address,
mailbox: chain2Addresses.mailbox,
owner: ownerAddress,
},
[CHAIN_NAME_3]: {
type: TokenType.synthetic,
mailbox: chain3Addresses.mailbox,
owner: ownerAddress,
},
};
writeYamlOrJson(WARP_DEPLOY_OUTPUT_PATH, warpConfig);
const steps = [
{
check: (currentOutput) => currentOutput.includes('Please enter the private key for chain'),
input: `${ANVIL_KEY}${KeyBoardKeys.ENTER}`,
},
{
check: (currentOutput) => currentOutput.includes('Please enter the private key for chain'),
input: `${ANVIL_KEY}${KeyBoardKeys.ENTER}`,
},
];
// Deploy
const output = hyperlaneWarpDeployRaw({
warpCorePath: WARP_DEPLOY_OUTPUT_PATH,
skipConfirmationPrompts: true,
})
.stdio('pipe')
.nothrow();
const finalOutput = await handlePrompts(output, steps);
// Assertions
expect(finalOutput.exitCode).to.equal(0);
for (const chainName of [CHAIN_NAME_2, CHAIN_NAME_3]) {
await assertWarpRouteConfig(warpConfig, COMBINED_WARP_CORE_CONFIG_PATH, chainName, { decimals: expectedTokenDecimals, symbol: expectedTokenSymbol });
}
});
});
describe(`hyperlane warp deploy --config ... --yes --key ...`, () => {
let tokenChain2;
let vaultChain2;
before(async () => {
tokenChain2 = await deployToken(ANVIL_KEY, CHAIN_NAME_2);
vaultChain2 = await deploy4626Vault(ANVIL_KEY, CHAIN_NAME_2, tokenChain2.address);
});
it('should only allow rebasing yield route to be deployed with rebasing synthetic', async function () {
const warpConfig = {
[CHAIN_NAME_2]: {
type: TokenType.collateralVaultRebase,
token: vaultChain2.address,
mailbox: chain2Addresses.mailbox,
owner: chain2Addresses.mailbox,
},
[CHAIN_NAME_3]: {
type: TokenType.synthetic,
mailbox: chain2Addresses.mailbox,
owner: chain2Addresses.mailbox,
},
};
writeYamlOrJson(WARP_DEPLOY_OUTPUT_PATH, warpConfig);
await hyperlaneWarpDeploy(WARP_DEPLOY_OUTPUT_PATH).should.be.rejected; // TODO: revisit this to figure out how to parse the error.
});
it('should deploy with an ISM config', async () => {
// 1. Define ISM configuration
const ism = {
type: IsmType.MESSAGE_ID_MULTISIG,
validators: [chain2Addresses.mailbox], // Using mailbox address as example validator
threshold: 1,
};
// 2. Create Warp configuration with ISM
const warpConfig = {
[CHAIN_NAME_2]: {
type: TokenType.collateralVaultRebase,
token: vaultChain2.address,
mailbox: chain2Addresses.mailbox,
owner: chain2Addresses.mailbox,
interchainSecurityModule: ism, // Add ISM config here
},
[CHAIN_NAME_3]: {
type: TokenType.syntheticRebase,
mailbox: chain3Addresses.mailbox,
owner: chain3Addresses.mailbox,
collateralChainName: CHAIN_NAME_2,
},
};
// 3. Write config and deploy
writeYamlOrJson(WARP_DEPLOY_OUTPUT_PATH, warpConfig);
await hyperlaneWarpDeploy(WARP_DEPLOY_OUTPUT_PATH);
// 4. Verify deployed ISM configuration
const collateralRebaseConfig = (await readWarpConfig(CHAIN_NAME_2, WARP_CORE_CONFIG_PATH_2_3, WARP_DEPLOY_OUTPUT_PATH))[CHAIN_NAME_2];
expect(normalizeConfig(collateralRebaseConfig.interchainSecurityModule)).to.deep.equal(normalizeConfig(ism));
});
it('should deploy with a hook config', async () => {
const hook = {
type: HookType.PROTOCOL_FEE,
beneficiary: chain2Addresses.mailbox,
owner: chain2Addresses.mailbox,
maxProtocolFee: '1337',
protocolFee: '1337',
};
const warpConfig = {
[CHAIN_NAME_2]: {
type: TokenType.collateralVaultRebase,
token: vaultChain2.address,
mailbox: chain2Addresses.mailbox,
owner: chain2Addresses.mailbox,
hook,
},
[CHAIN_NAME_3]: {
type: TokenType.syntheticRebase,
mailbox: chain3Addresses.mailbox,
owner: chain3Addresses.mailbox,
collateralChainName: CHAIN_NAME_2,
},
};
writeYamlOrJson(WARP_DEPLOY_OUTPUT_PATH, warpConfig);
await hyperlaneWarpDeploy(WARP_DEPLOY_OUTPUT_PATH);
// Check collateralRebase
const collateralRebaseConfig = (await readWarpConfig(CHAIN_NAME_2, WARP_CORE_CONFIG_PATH_2_3, WARP_DEPLOY_OUTPUT_PATH))[CHAIN_NAME_2];
expect(normalizeConfig(collateralRebaseConfig.hook)).to.deep.equal(normalizeConfig(hook));
});
it('should send a message from origin to destination in the correct order', async function () {
const warpConfig = {
[CHAIN_NAME_2]: {
type: TokenType.collateralVaultRebase,
token: vaultChain2.address,
mailbox: chain2Addresses.mailbox,
owner: chain2Addresses.mailbox,
},
[CHAIN_NAME_3]: {
type: TokenType.syntheticRebase,
mailbox: chain3Addresses.mailbox,
owner: chain3Addresses.mailbox,
collateralChainName: CHAIN_NAME_2,
},
};
writeYamlOrJson(WARP_DEPLOY_OUTPUT_PATH, warpConfig);
await hyperlaneWarpDeploy(WARP_DEPLOY_OUTPUT_PATH);
// Try to send a transaction with the origin destination
const { stdout: chain2Tochain3Stdout } = await hyperlaneWarpSendRelay(CHAIN_NAME_2, CHAIN_NAME_3, WARP_CORE_CONFIG_PATH_2_3);
expect(chain2Tochain3Stdout).to.include('anvil2 ➡️ anvil3');
// Send another message with swapped origin destination
const { stdout: chain3Tochain2Stdout } = await hyperlaneWarpSendRelay(CHAIN_NAME_3, CHAIN_NAME_2, WARP_CORE_CONFIG_PATH_2_3);
expect(chain3Tochain2Stdout).to.include('anvil3 ➡️ anvil2');
// Should throw if invalid origin or destination
await hyperlaneWarpSendRelay('anvil1', CHAIN_NAME_3, WARP_CORE_CONFIG_PATH_2_3).should.be.rejectedWith('Error: Origin (anvil1) or destination (anvil3) are not part of the warp route.');
});
});
});
//# sourceMappingURL=warp-deploy.e2e-test.js.map