diff --git a/packages/ua-utils-evm-hardhat-test/package.json b/packages/ua-utils-evm-hardhat-test/package.json index f5e14247e..989927f29 100644 --- a/packages/ua-utils-evm-hardhat-test/package.json +++ b/packages/ua-utils-evm-hardhat-test/package.json @@ -12,7 +12,7 @@ "scripts": { "lint": "npx eslint '**/*.{js,ts,json}'", "pretest": "npx hardhat compile", - "test": "jest --runInBand" + "test": "jest --runInBand --forceExit --verbose" }, "devDependencies": { "@ethersproject/abstract-signer": "^5.7.0", diff --git a/packages/ua-utils-evm-hardhat-test/test/__utils__/endpoint.ts b/packages/ua-utils-evm-hardhat-test/test/__utils__/endpoint.ts index a9d3fe633..901b53420 100644 --- a/packages/ua-utils-evm-hardhat-test/test/__utils__/endpoint.ts +++ b/packages/ua-utils-evm-hardhat-test/test/__utils__/endpoint.ts @@ -5,7 +5,6 @@ import { OmniGraphBuilderHardhat, type OmniGraphHardhat, } from '@layerzerolabs/utils-evm-hardhat' -import deploy from '../../deploy/001_bootstrap' import { createLogger } from '@layerzerolabs/io-utils' import { EndpointId } from '@layerzerolabs/lz-definitions' import { omniContractToPoint } from '@layerzerolabs/utils-evm' @@ -78,8 +77,10 @@ export const setupDefaultEndpoint = async (): Promise => { const endpointSdkFactory = createEndpointFactory(contractFactory, ulnSdkFactory) // First we deploy the endpoint - await deploy(await environmentFactory(EndpointId.ETHEREUM_MAINNET)) - await deploy(await environmentFactory(EndpointId.AVALANCHE_MAINNET)) + const eth = await environmentFactory(EndpointId.ETHEREUM_MAINNET) + const avax = await environmentFactory(EndpointId.AVALANCHE_MAINNET) + + await Promise.all([eth.deployments.fixture('EndpointV2'), avax.deployments.fixture('EndpointV2')]) // For the graphs, we'll also need the pointers to the contracts const ethSendUlnPoint = omniContractToPoint(await contractFactory(ethSendUln)) diff --git a/packages/ua-utils-evm-hardhat-test/test/__utils__/oapp.ts b/packages/ua-utils-evm-hardhat-test/test/__utils__/oapp.ts index 886b012ea..07eb1842b 100644 --- a/packages/ua-utils-evm-hardhat-test/test/__utils__/oapp.ts +++ b/packages/ua-utils-evm-hardhat-test/test/__utils__/oapp.ts @@ -1,10 +1,10 @@ import { EndpointId } from '@layerzerolabs/lz-definitions' import { createNetworkEnvironmentFactory } from '@layerzerolabs/utils-evm-hardhat' -import deploy from '../../deploy/002_oapp' export const deployOApp = async () => { const environmentFactory = createNetworkEnvironmentFactory() + const eth = await environmentFactory(EndpointId.ETHEREUM_MAINNET) + const avax = await environmentFactory(EndpointId.AVALANCHE_MAINNET) - await deploy(await environmentFactory(EndpointId.ETHEREUM_MAINNET)) - await deploy(await environmentFactory(EndpointId.AVALANCHE_MAINNET)) + await Promise.all([eth.deployments.fixture('OApp'), avax.deployments.fixture('OApp')]) } diff --git a/packages/ua-utils-evm-hardhat-test/test/oapp/config.test.ts b/packages/ua-utils-evm-hardhat-test/test/oapp/config.test.ts index 8462cc4cc..4a0a42624 100644 --- a/packages/ua-utils-evm-hardhat-test/test/oapp/config.test.ts +++ b/packages/ua-utils-evm-hardhat-test/test/oapp/config.test.ts @@ -38,8 +38,11 @@ describe('oapp/config', () => { ], } - beforeEach(async () => { + beforeAll(async () => { await setupDefaultEndpoint() + }) + + beforeEach(async () => { await deployOApp() }) diff --git a/packages/ua-utils-evm-hardhat-test/test/task/oapp/__data__/configs/valid.config.connected.js b/packages/ua-utils-evm-hardhat-test/test/task/oapp/__data__/configs/valid.config.connected.js new file mode 100644 index 000000000..0395a59a2 --- /dev/null +++ b/packages/ua-utils-evm-hardhat-test/test/task/oapp/__data__/configs/valid.config.connected.js @@ -0,0 +1,33 @@ +// eslint-disable-next-line @typescript-eslint/no-var-requires +const { EndpointId } = require('@layerzerolabs/lz-definitions'); + +const ethContract = { + eid: EndpointId.ETHEREUM_MAINNET, + contractName: 'DefaultOApp', +}; + +const avaxContract = { + eid: EndpointId.AVALANCHE_MAINNET, + contractName: 'DefaultOApp', +}; + +module.exports = { + contracts: [ + { + contract: avaxContract, + }, + { + contract: ethContract, + }, + ], + connections: [ + { + from: avaxContract, + to: ethContract, + }, + { + from: ethContract, + to: avaxContract, + }, + ], +}; diff --git a/packages/ua-utils-evm-hardhat-test/test/task/oapp/__data__/configs/valid.config.misconfigured.001.js b/packages/ua-utils-evm-hardhat-test/test/task/oapp/__data__/configs/valid.config.misconfigured.001.js new file mode 100644 index 000000000..c9fde9d55 --- /dev/null +++ b/packages/ua-utils-evm-hardhat-test/test/task/oapp/__data__/configs/valid.config.misconfigured.001.js @@ -0,0 +1,33 @@ +// eslint-disable-next-line @typescript-eslint/no-var-requires +const { EndpointId } = require('@layerzerolabs/lz-definitions'); + +const ethContract = { + eid: EndpointId.ETHEREUM_MAINNET, + contractName: 'DefaultOApp', +}; + +const avaxContract = { + eid: EndpointId.AVALANCHE_MAINNET, + contractName: 'NonExistent', +}; + +module.exports = { + contracts: [ + { + contract: avaxContract, + }, + { + contract: ethContract, + }, + ], + connections: [ + { + from: avaxContract, + to: ethContract, + }, + { + from: ethContract, + to: avaxContract, + }, + ], +}; diff --git a/packages/ua-utils-evm-hardhat-test/test/task/oapp/__snapshots__/wire.test.ts.snap b/packages/ua-utils-evm-hardhat-test/test/task/oapp/__snapshots__/wire.test.ts.snap index 9509d574b..0451b2fad 100644 --- a/packages/ua-utils-evm-hardhat-test/test/task/oapp/__snapshots__/wire.test.ts.snap +++ b/packages/ua-utils-evm-hardhat-test/test/task/oapp/__snapshots__/wire.test.ts.snap @@ -17,6 +17,8 @@ connections: - Property 'connections': Required] `; +exports[`task/oapp/wire with invalid configs should fail with a misconfigured file (001) 1`] = `[Error: Config from file '/app/packages/ua-utils-evm-hardhat-test/test/task/oapp/__data__/configs/valid.config.misconfigured.001.js' is invalid: AssertionError [ERR_ASSERTION]: Could not find a deployment for contract 'NonExistent']`; + exports[`task/oapp/wire with invalid configs should fail with an empty JS file 1`] = ` [Error: Config from file '/app/packages/ua-utils-evm-hardhat-test/test/task/oapp/__data__/configs/invalid.config.empty.js' is malformed. Please fix the following errors: diff --git a/packages/ua-utils-evm-hardhat-test/test/task/oapp/wire.test.ts b/packages/ua-utils-evm-hardhat-test/test/task/oapp/wire.test.ts index b1bd8cfbd..d039f7fb9 100644 --- a/packages/ua-utils-evm-hardhat-test/test/task/oapp/wire.test.ts +++ b/packages/ua-utils-evm-hardhat-test/test/task/oapp/wire.test.ts @@ -1,8 +1,8 @@ -import { setupDefaultEndpoint } from '../../__utils__/endpoint' import hre from 'hardhat' import { isFile, promptToContinue } from '@layerzerolabs/io-utils' import { resolve } from 'path' import { TASK_LZ_WIRE_OAPP } from '@layerzerolabs/ua-utils-evm-hardhat' +import { deployOApp } from '../../__utils__/oapp' jest.mock('@layerzerolabs/io-utils', () => { const original = jest.requireActual('@layerzerolabs/io-utils') @@ -27,11 +27,13 @@ describe('task/oapp/wire', () => { beforeEach(async () => { promptToContinueMock.mockReset() - - await setupDefaultEndpoint() }) describe('with invalid configs', () => { + beforeAll(async () => { + await deployOApp() + }) + it('should fail if the config file does not exist', async () => { await expect(hre.run(TASK_LZ_WIRE_OAPP, { oappConfig: './does-not-exist.js' })).rejects.toMatchSnapshot() }) @@ -65,33 +67,64 @@ describe('task/oapp/wire', () => { await expect(hre.run(TASK_LZ_WIRE_OAPP, { oappConfig })).rejects.toMatchSnapshot() }) + + it('should fail with a misconfigured file (001)', async () => { + const oappConfig = configPathFixture('valid.config.misconfigured.001.js') + + await expect(hre.run(TASK_LZ_WIRE_OAPP, { oappConfig })).rejects.toMatchSnapshot() + }) }) describe('with valid configs', () => { - it('should ask the user whether they want to continue', async () => { + beforeEach(async () => { + await deployOApp() + }) + + it('should exit if there is nothing to wire', async () => { const oappConfig = configPathFixture('valid.config.empty.js') + await hre.run(TASK_LZ_WIRE_OAPP, { oappConfig }) + + expect(promptToContinueMock).not.toHaveBeenCalled() + }) + + it('should have debug output if requested (so called eye test, check the test output)', async () => { + const oappConfig = configPathFixture('valid.config.connected.js') + + promptToContinueMock.mockResolvedValue(false) + + await hre.run(TASK_LZ_WIRE_OAPP, { oappConfig, logLevel: 'debug' }) + + expect(promptToContinueMock).toHaveBeenCalledTimes(2) + }) + + it('should ask the user whether they want to see the transactions & continue', async () => { + const oappConfig = configPathFixture('valid.config.connected.js') + promptToContinueMock.mockResolvedValue(true) await hre.run(TASK_LZ_WIRE_OAPP, { oappConfig }) - expect(promptToContinueMock).toHaveBeenCalledTimes(1) + expect(promptToContinueMock).toHaveBeenCalledTimes(2) }) it('should return undefined if the user decides not to continue', async () => { - const oappConfig = configPathFixture('valid.config.empty.js') + const oappConfig = configPathFixture('valid.config.connected.js') promptToContinueMock.mockResolvedValue(false) const result = await hre.run(TASK_LZ_WIRE_OAPP, { oappConfig }) expect(result).toBeUndefined() + expect(promptToContinueMock).toHaveBeenCalledTimes(2) }) it('should return a list of transactions if the user decides to continue', async () => { - const oappConfig = configPathFixture('valid.config.empty.js') + const oappConfig = configPathFixture('valid.config.connected.js') - promptToContinueMock.mockResolvedValue(true) + promptToContinueMock + .mockResolvedValueOnce(false) // We don't want to see the list + .mockResolvedValueOnce(true) // We want to continue const result = await hre.run(TASK_LZ_WIRE_OAPP, { oappConfig }) diff --git a/packages/ua-utils-evm-hardhat/src/tasks/oapp/wire.ts b/packages/ua-utils-evm-hardhat/src/tasks/oapp/wire.ts index 79185ed0e..bae7b6ab8 100644 --- a/packages/ua-utils-evm-hardhat/src/tasks/oapp/wire.ts +++ b/packages/ua-utils-evm-hardhat/src/tasks/oapp/wire.ts @@ -1,15 +1,34 @@ import { task, types } from 'hardhat/config' import type { ActionType } from 'hardhat/types' import { TASK_LZ_WIRE_OAPP } from '@/constants/tasks' -import { isFile, isReadable, promptToContinue } from '@layerzerolabs/io-utils' +import { + isFile, + isReadable, + createLogger, + setDefaultLogLevel, + promptToContinue, + printJson, +} from '@layerzerolabs/io-utils' import { OAppOmniGraphHardhat, OAppOmniGraphHardhatSchema } from '@/oapp' +import { OAppOmniGraph, configureOApp } from '@layerzerolabs/ua-utils' +import { createOAppFactory } from '@layerzerolabs/ua-utils-evm' +import { OmniGraphBuilderHardhat, createConnectedContractFactory } from '@layerzerolabs/utils-evm-hardhat' +import { OmniTransaction } from '@layerzerolabs/utils' +import { printTransactions } from '@layerzerolabs/utils' interface TaskArgs { oappConfig: string + logLevel?: string } -const action: ActionType = async ({ oappConfig: oappConfigPath }) => { +const action: ActionType = async ({ oappConfig: oappConfigPath, logLevel = 'info' }) => { + // We'll set the global logging level to get as much info as needed + setDefaultLogLevel(logLevel) + + const logger = createLogger() + // First we check that the config file is indeed there and we can read it + logger.verbose(`Checking config file '${oappConfigPath}' for existence & readability`) const isConfigReadable = isFile(oappConfigPath) && isReadable(oappConfigPath) if (!isConfigReadable) { throw new Error( @@ -17,19 +36,27 @@ const action: ActionType = async ({ oappConfig: oappConfigPath }) => { ) } + // Keep talking to the user + logger.verbose(`Config file '${oappConfigPath}' exists & is readable`) + // Now let's see if we can load the config file let rawConfig: unknown try { + logger.verbose(`Loading config file '${oappConfigPath}'`) + rawConfig = require(oappConfigPath) } catch (error) { throw new Error(`Unable to read config file '${oappConfigPath}': ${error}`) } + logger.verbose(`Loaded config file '${oappConfigPath}'`) + // It's time to make sure that the config is not malformed // // At this stage we are only interested in the shape of the data, // we are not checking whether the information makes sense (e.g. // whether there are no missing nodes etc) + logger.verbose(`Validating the structure of config file '${oappConfigPath}'`) const configParseResult = OAppOmniGraphHardhatSchema.safeParse(rawConfig) if (configParseResult.success === false) { // FIXME Error formatting @@ -47,10 +74,67 @@ const action: ActionType = async ({ oappConfig: oappConfigPath }) => { ) } - // At this point we have a correctly typed config - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const config: OAppOmniGraphHardhat = configParseResult.data + // At this point we have a correctly typed config in the hardhat format + const hardhatGraph: OAppOmniGraphHardhat = configParseResult.data + + // We'll also print out the whole config for verbose loggers + logger.verbose(`Config file '${oappConfigPath}' has correct structure`) + logger.debug(`The hardhat config is:\n\n${printJson(hardhatGraph)}`) + + // What we need to do now is transform the config from hardhat format to the generic format + // with addresses instead of contractNames + logger.verbose(`Transforming '${oappConfigPath}' from hardhat-specific format to generic format`) + let graph: OAppOmniGraph + try { + // The transformation is achieved using a builder that also validates the resulting graph + // (i.e. makes sure that all the contracts exist and connections are valid) + const builder = await OmniGraphBuilderHardhat.fromConfig(hardhatGraph) + + // We only need the graph so we throw away the builder + graph = builder.graph + } catch (error) { + throw new Error(`Config from file '${oappConfigPath}' is invalid: ${error}`) + } + + // Show more detailed logs to interested users + logger.verbose(`Transformed '${oappConfigPath}' from hardhat-specific format to generic format`) + logger.debug(`The resulting config is:\n\n${printJson(graph)}`) + + // At this point we are ready to create the list of transactions + logger.verbose(`Creating a list of wiring transactions`) + const contractFactory = createConnectedContractFactory() + const oAppFactory = createOAppFactory(contractFactory) + + let transactions: OmniTransaction[] + try { + transactions = await configureOApp(graph, oAppFactory) + } catch (error) { + throw new Error(`An error occurred while getting the OApp configuration: ${error}`) + } + + // Flood users with debug output + logger.verbose(`Created a list of wiring transactions`) + logger.debug(`Following transactions are necessary:\n\n${printJson(transactions)}`) + + // If there are no transactions that need to be executed, we'll just exit + if (transactions.length === 0) { + logger.info(`The OApp is wired, no action is necessary`) + + return [] + } + + // Tell the user about the transactions + logger.info( + transactions.length === 1 + ? `There is 1 transaction required to configure the OApp` + : `There are ${transactions.length} transactions required to configure the OApp` + ) + + // Ask them whether they want to see them + const previewTransactions = await promptToContinue(`Would you like to preview the transactions before continuing?`) + if (previewTransactions) logger.info(`\n${printTransactions(transactions)}`) + // Now ask the user whether they want to go ahead with signing them const go = await promptToContinue() if (!go) { return undefined @@ -60,4 +144,5 @@ const action: ActionType = async ({ oappConfig: oappConfigPath }) => { } task(TASK_LZ_WIRE_OAPP, 'Wire LayerZero OApp') .addParam('oappConfig', 'Path to your LayerZero OApp config', './layerzero.config.js', types.string) + .addParam('logLevel', 'Logging level. One of: error, warn, info, verbose, debug, silly', 'info', types.string) .setAction(action) diff --git a/packages/utils-evm/src/errors/parser.ts b/packages/utils-evm/src/errors/parser.ts index 956bc9821..b0b61d721 100644 --- a/packages/utils-evm/src/errors/parser.ts +++ b/packages/utils-evm/src/errors/parser.ts @@ -147,9 +147,14 @@ const createContractDecoder = * @returns `string[]` A list of possible error revert strings */ const getErrorDataCandidates = (error: unknown): string[] => - [(error as any)?.error?.data?.data, (error as any)?.error?.data, (error as any)?.data].filter( - (candidate: unknown) => typeof candidate === 'string' - ) + [ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (error as any)?.error?.data?.data, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (error as any)?.error?.data, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (error as any)?.data, + ].filter((candidate: unknown) => typeof candidate === 'string') /** * Solves an issue with objects that cannot be converted to primitive values diff --git a/packages/utils/package.json b/packages/utils/package.json index 3028da4c4..4bf2af289 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -30,6 +30,7 @@ "test": "jest" }, "devDependencies": { + "@layerzerolabs/io-utils": "~0.0.1", "@layerzerolabs/lz-definitions": "~1.5.72", "@layerzerolabs/test-utils": "~0.0.1", "@types/jest": "^29.5.10", @@ -42,6 +43,7 @@ "zod": "^3.22.4" }, "peerDependencies": { + "@layerzerolabs/io-utils": "~0.0.1", "@layerzerolabs/lz-definitions": "~1.5.72", "zod": "^3.22.4" } diff --git a/packages/utils/src/transactions/format.ts b/packages/utils/src/transactions/format.ts new file mode 100644 index 000000000..4b1935997 --- /dev/null +++ b/packages/utils/src/transactions/format.ts @@ -0,0 +1,19 @@ +import { printRecord } from '@layerzerolabs/io-utils' +import { OmniTransaction } from './types' + +/** + * Placeholder for a more detailed OmniTransaction printer + * + * @param {OmniTransaction} transaction + * @returns {string} + */ +export const printTransaction = (transaction: OmniTransaction): string => printRecord(transaction) + +/** + * Placeholder for a more detailed OmniTransaction list printer + * + * @param {OmniTransaction[]} transactions + * @returns {string} + */ +export const printTransactions = (transactions: OmniTransaction[]): string => + printRecord(transactions.map(printTransaction)) diff --git a/packages/utils/src/transactions/index.ts b/packages/utils/src/transactions/index.ts index ce4acb574..0228ff9de 100644 --- a/packages/utils/src/transactions/index.ts +++ b/packages/utils/src/transactions/index.ts @@ -1,2 +1,3 @@ +export * from './format' export * from './types' export * from './utils'