Skip to content

Commit

Permalink
🪚 OmniGraph™ OApp wire task: The saga continues (#102)
Browse files Browse the repository at this point in the history
  • Loading branch information
janjakubnanista authored Dec 12, 2023
1 parent bf95cd1 commit 1185ca7
Show file tree
Hide file tree
Showing 13 changed files with 241 additions and 24 deletions.
2 changes: 1 addition & 1 deletion packages/ua-utils-evm-hardhat-test/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
7 changes: 4 additions & 3 deletions packages/ua-utils-evm-hardhat-test/test/__utils__/endpoint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -78,8 +77,10 @@ export const setupDefaultEndpoint = async (): Promise<void> => {
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))
Expand Down
6 changes: 3 additions & 3 deletions packages/ua-utils-evm-hardhat-test/test/__utils__/oapp.ts
Original file line number Diff line number Diff line change
@@ -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')])
}
5 changes: 4 additions & 1 deletion packages/ua-utils-evm-hardhat-test/test/oapp/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,11 @@ describe('oapp/config', () => {
],
}

beforeEach(async () => {
beforeAll(async () => {
await setupDefaultEndpoint()
})

beforeEach(async () => {
await deployOApp()
})

Expand Down
Original file line number Diff line number Diff line change
@@ -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,
},
],
};
Original file line number Diff line number Diff line change
@@ -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,
},
],
};
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
49 changes: 41 additions & 8 deletions packages/ua-utils-evm-hardhat-test/test/task/oapp/wire.test.ts
Original file line number Diff line number Diff line change
@@ -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')
Expand All @@ -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()
})
Expand Down Expand Up @@ -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 })

Expand Down
95 changes: 90 additions & 5 deletions packages/ua-utils-evm-hardhat/src/tasks/oapp/wire.ts
Original file line number Diff line number Diff line change
@@ -1,35 +1,62 @@
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<TaskArgs> = async ({ oappConfig: oappConfigPath }) => {
const action: ActionType<TaskArgs> = 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(
`Unable to read config file '${oappConfigPath}'. Check that the file exists and is readable to your terminal user`
)
}

// 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
Expand All @@ -47,10 +74,67 @@ const action: ActionType<TaskArgs> = 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
Expand All @@ -60,4 +144,5 @@ const action: ActionType<TaskArgs> = 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)
11 changes: 8 additions & 3 deletions packages/utils-evm/src/errors/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading

0 comments on commit 1185ca7

Please sign in to comment.