From fd0c10e67efd2289083975da525cc780bcac39e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A1n=20Jakub=20Nani=C5=A1ta?= Date: Tue, 9 Jan 2024 17:11:32 -0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=AA=9A=20Executor=20SDK=20(#176)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/omnigraph/coordinates.ts | 9 +++- .../src/executor/factory.ts | 15 +++++++ .../src/executor/index.ts | 3 ++ .../src/executor/schema.ts | 14 ++++++ .../protocol-devtools-evm/src/executor/sdk.ts | 37 ++++++++++++++++ packages/protocol-devtools-evm/src/index.ts | 1 + .../src/priceFeed/schema.ts | 2 +- .../protocol-devtools/src/executor/config.ts | 22 ++++++++++ .../protocol-devtools/src/executor/index.ts | 3 ++ .../protocol-devtools/src/executor/schema.ts | 10 +++++ .../protocol-devtools/src/executor/types.ts | 25 +++++++++++ packages/protocol-devtools/src/index.ts | 1 + .../deploy/001_bootstrap.ts | 28 +----------- .../test/__utils__/endpoint.ts | 44 +++++++++++++++++++ 14 files changed, 185 insertions(+), 29 deletions(-) create mode 100644 packages/protocol-devtools-evm/src/executor/factory.ts create mode 100644 packages/protocol-devtools-evm/src/executor/index.ts create mode 100644 packages/protocol-devtools-evm/src/executor/schema.ts create mode 100644 packages/protocol-devtools-evm/src/executor/sdk.ts create mode 100644 packages/protocol-devtools/src/executor/config.ts create mode 100644 packages/protocol-devtools/src/executor/index.ts create mode 100644 packages/protocol-devtools/src/executor/schema.ts create mode 100644 packages/protocol-devtools/src/executor/types.ts diff --git a/packages/devtools-evm-hardhat/src/omnigraph/coordinates.ts b/packages/devtools-evm-hardhat/src/omnigraph/coordinates.ts index ee3125ee5..943b0acbd 100644 --- a/packages/devtools-evm-hardhat/src/omnigraph/coordinates.ts +++ b/packages/devtools-evm-hardhat/src/omnigraph/coordinates.ts @@ -51,7 +51,14 @@ export const createContractFactory = (environmentFactory = createGetHreByEid()): assert(deployments.length > 0, `Could not find a deployment for address '${address}'`) const mergedAbis = deployments.flatMap((deployment) => deployment.abi) - return { eid, contract: new Contract(address, mergedAbis) } + + // Even though duplicated fragments don't throw errors, they still pollute the interface with warning console.logs + // To prevent this, we'll run a simple deduplication algorithm - use JSON encoded values as hashes + const deduplicatedAbi = Object.values( + Object.fromEntries(mergedAbis.map((abi) => [JSON.stringify(abi), abi])) + ) + + return { eid, contract: new Contract(address, deduplicatedAbi) } } assert(false, 'At least one of contractName, address must be specified for OmniPointHardhat') diff --git a/packages/protocol-devtools-evm/src/executor/factory.ts b/packages/protocol-devtools-evm/src/executor/factory.ts new file mode 100644 index 000000000..ce327f54a --- /dev/null +++ b/packages/protocol-devtools-evm/src/executor/factory.ts @@ -0,0 +1,15 @@ +import pMemoize from 'p-memoize' +import type { OmniContractFactory } from '@layerzerolabs/devtools-evm' +import type { ExecutorFactory } from '@layerzerolabs/protocol-devtools' +import { Executor } from './sdk' + +/** + * Syntactic sugar that creates an instance of EVM `Executor` SDK + * based on an `OmniPoint` with help of an `OmniContractFactory` + * + * @param {OmniContractFactory} contractFactory + * @returns {ExecutorFactory} + */ +export const createExecutorFactory = ( + contractFactory: OmniContractFactory +): ExecutorFactory => pMemoize(async (point) => new Executor(await contractFactory(point))) diff --git a/packages/protocol-devtools-evm/src/executor/index.ts b/packages/protocol-devtools-evm/src/executor/index.ts new file mode 100644 index 000000000..166c7fe16 --- /dev/null +++ b/packages/protocol-devtools-evm/src/executor/index.ts @@ -0,0 +1,3 @@ +export * from './factory' +export * from './schema' +export * from './sdk' diff --git a/packages/protocol-devtools-evm/src/executor/schema.ts b/packages/protocol-devtools-evm/src/executor/schema.ts new file mode 100644 index 000000000..b1c1ddda5 --- /dev/null +++ b/packages/protocol-devtools-evm/src/executor/schema.ts @@ -0,0 +1,14 @@ +import { BigNumberishBigintSchema } from '@layerzerolabs/devtools-evm' +import type { ExecutorDstConfig } from '@layerzerolabs/protocol-devtools' +import { ExecutorDstConfigSchema as ExecutorDstConfigSchemaBase } from '@layerzerolabs/protocol-devtools' +import { z } from 'zod' + +/** + * Schema for parsing an ethers-specific PriceData into a common format + */ +export const ExecutorDstConfigSchema = ExecutorDstConfigSchemaBase.extend({ + baseGas: BigNumberishBigintSchema, + multiplierBps: BigNumberishBigintSchema, + floorMarginUSD: BigNumberishBigintSchema, + nativeCap: BigNumberishBigintSchema, +}) satisfies z.ZodSchema diff --git a/packages/protocol-devtools-evm/src/executor/sdk.ts b/packages/protocol-devtools-evm/src/executor/sdk.ts new file mode 100644 index 000000000..bd7f1ab59 --- /dev/null +++ b/packages/protocol-devtools-evm/src/executor/sdk.ts @@ -0,0 +1,37 @@ +import type { EndpointId } from '@layerzerolabs/lz-definitions' +import type { IExecutor, ExecutorDstConfig } from '@layerzerolabs/protocol-devtools' +import { formatEid, type OmniTransaction } from '@layerzerolabs/devtools' +import { OmniSDK } from '@layerzerolabs/devtools-evm' +import { printRecord } from '@layerzerolabs/io-devtools' +import { ExecutorDstConfigSchema } from './schema' + +export class Executor extends OmniSDK implements IExecutor { + async getDstConfig(eid: EndpointId): Promise { + const config = await this.contract.contract.dstConfig(eid) + + // Now we convert the ethers-specific object into the common structure + // + // Here we need to spread the config into an object because what ethers gives us + // is actually an array with extra properties + return ExecutorDstConfigSchema.parse({ ...config }) + } + + async setDstConfig(eid: EndpointId, value: ExecutorDstConfig): Promise { + const data = this.contract.contract.interface.encodeFunctionData('setDstConfig', [ + [ + { + dstEid: eid, + baseGas: value.baseGas, + multiplierBps: value.multiplierBps, + floorMarginUSD: value.floorMarginUSD, + nativeCap: value.nativeCap, + }, + ], + ]) + + return { + ...this.createTransaction(data), + description: `Setting dstConfig for ${formatEid(eid)}: ${printRecord(value)}`, + } + } +} diff --git a/packages/protocol-devtools-evm/src/index.ts b/packages/protocol-devtools-evm/src/index.ts index 11739325c..f3bb34bec 100644 --- a/packages/protocol-devtools-evm/src/index.ts +++ b/packages/protocol-devtools-evm/src/index.ts @@ -1,3 +1,4 @@ export * from './endpoint' +export * from './executor' export * from './priceFeed' export * from './uln302' diff --git a/packages/protocol-devtools-evm/src/priceFeed/schema.ts b/packages/protocol-devtools-evm/src/priceFeed/schema.ts index e8b1d01ab..5da78b08d 100644 --- a/packages/protocol-devtools-evm/src/priceFeed/schema.ts +++ b/packages/protocol-devtools-evm/src/priceFeed/schema.ts @@ -1,5 +1,5 @@ import { BigNumberishBigintSchema } from '@layerzerolabs/devtools-evm' -import { PriceData } from '@layerzerolabs/protocol-devtools' +import type { PriceData } from '@layerzerolabs/protocol-devtools' import { PriceDataSchema as PriceDataSchemaBase } from '@layerzerolabs/protocol-devtools' import { z } from 'zod' diff --git a/packages/protocol-devtools/src/executor/config.ts b/packages/protocol-devtools/src/executor/config.ts new file mode 100644 index 000000000..0e2343715 --- /dev/null +++ b/packages/protocol-devtools/src/executor/config.ts @@ -0,0 +1,22 @@ +import { flattenTransactions, isDeepEqual, type OmniTransaction } from '@layerzerolabs/devtools' +import type { ExecutorFactory, ExecutorOmniGraph } from './types' + +export type ExecutorConfigurator = (graph: ExecutorOmniGraph, createSdk: ExecutorFactory) => Promise + +export const configureExecutor: ExecutorConfigurator = async (graph, createSdk) => + flattenTransactions([await configureExecutorDstConfig(graph, createSdk)]) + +export const configureExecutorDstConfig: ExecutorConfigurator = async (graph, createSdk) => + flattenTransactions( + await Promise.all( + graph.connections.map(async ({ vector: { from, to }, config }): Promise => { + const sdk = await createSdk(from) + const dstConfig = await sdk.getDstConfig(to.eid) + + // TODO Normalize the config values using a schema before comparing them + if (isDeepEqual(dstConfig, config.dstConfig)) return [] + + return [await sdk.setDstConfig(to.eid, config.dstConfig)] + }) + ) + ) diff --git a/packages/protocol-devtools/src/executor/index.ts b/packages/protocol-devtools/src/executor/index.ts new file mode 100644 index 000000000..1273e8646 --- /dev/null +++ b/packages/protocol-devtools/src/executor/index.ts @@ -0,0 +1,3 @@ +export * from './config' +export * from './schema' +export * from './types' diff --git a/packages/protocol-devtools/src/executor/schema.ts b/packages/protocol-devtools/src/executor/schema.ts new file mode 100644 index 000000000..aed5884b1 --- /dev/null +++ b/packages/protocol-devtools/src/executor/schema.ts @@ -0,0 +1,10 @@ +import { z } from 'zod' +import type { ExecutorDstConfig } from './types' +import { UIntSchema } from '@layerzerolabs/devtools' + +export const ExecutorDstConfigSchema = z.object({ + baseGas: UIntSchema, + multiplierBps: UIntSchema, + floorMarginUSD: UIntSchema, + nativeCap: UIntSchema, +}) satisfies z.ZodSchema diff --git a/packages/protocol-devtools/src/executor/types.ts b/packages/protocol-devtools/src/executor/types.ts new file mode 100644 index 000000000..4f4c4c26b --- /dev/null +++ b/packages/protocol-devtools/src/executor/types.ts @@ -0,0 +1,25 @@ +import type { Factory, IOmniSDK, OmniGraph, OmniPoint, OmniTransaction } from '@layerzerolabs/devtools' +import type { EndpointId } from '@layerzerolabs/lz-definitions' + +export interface IExecutor extends IOmniSDK { + getDstConfig(eid: EndpointId): Promise + setDstConfig(eid: EndpointId, value: ExecutorDstConfig): Promise +} + +export interface ExecutorDstConfig { + baseGas: bigint | string | number + multiplierBps: bigint | string | number + floorMarginUSD: bigint | string | number + nativeCap: bigint | string | number +} + +export interface ExecutorEdgeConfig { + dstConfig: ExecutorDstConfig +} + +export type ExecutorOmniGraph = OmniGraph + +export type ExecutorFactory = Factory< + [TOmniPoint], + TExecutor +> diff --git a/packages/protocol-devtools/src/index.ts b/packages/protocol-devtools/src/index.ts index 11739325c..f3bb34bec 100644 --- a/packages/protocol-devtools/src/index.ts +++ b/packages/protocol-devtools/src/index.ts @@ -1,3 +1,4 @@ export * from './endpoint' +export * from './executor' export * from './priceFeed' export * from './uln302' diff --git a/tests/ua-devtools-evm-hardhat-test/deploy/001_bootstrap.ts b/tests/ua-devtools-evm-hardhat-test/deploy/001_bootstrap.ts index 9c0237d4a..9a4b1f895 100644 --- a/tests/ua-devtools-evm-hardhat-test/deploy/001_bootstrap.ts +++ b/tests/ua-devtools-evm-hardhat-test/deploy/001_bootstrap.ts @@ -1,10 +1,8 @@ import { TransactionReceipt, TransactionResponse } from '@ethersproject/providers' import { formatEid } from '@layerzerolabs/devtools' import { wrapEIP1193Provider } from '@layerzerolabs/devtools-evm-hardhat' -import { EndpointId } from '@layerzerolabs/lz-definitions' import assert from 'assert' -import { BigNumber, Contract } from 'ethers' -import { parseEther } from 'ethers/lib/utils' +import { Contract } from 'ethers' import { type DeployFunction } from 'hardhat-deploy/types' import { HardhatRuntimeEnvironment } from 'hardhat/types' @@ -30,13 +28,6 @@ const deploy: DeployFunction = async ({ getUnnamedAccounts, deployments, network assert(deployer, 'Missing deployer') const signer = wrapEIP1193Provider(network.provider).getSigner() - // TODO: move price configuration to a separate sdk and make bootstrap of the configuration optional - const dstEid = BigNumber.from( - network.config.eid === EndpointId.ETHEREUM_V2_MAINNET - ? EndpointId.AVALANCHE_V2_MAINNET - : EndpointId.ETHEREUM_V2_MAINNET - ) - await deployments.delete('EndpointV2') const endpointV2Deployment = await deployments.deploy('EndpointV2', { from: deployer, @@ -137,23 +128,6 @@ const deploy: DeployFunction = async ({ getUnnamedAccounts, deployments, network 'Executor worker fee lib not set correctly' ) - const nativeCap = parseEther('0.25') - const baseGas = BigNumber.from(200000) - const setDstConfigResp: TransactionResponse = await executorContract.setDstConfig([ - { - dstEid, - baseGas, - multiplierBps: BigNumber.from(0), - floorMarginUSD: BigNumber.from(0), - nativeCap, - }, - ]) - const setDstConfigReceipt: TransactionReceipt = await setDstConfigResp.wait() - assert(setDstConfigReceipt?.status === 1) - const polledDstConfig = await executorContract.dstConfig(dstEid) - assert(BigNumber.from(polledDstConfig[0]).eq(baseGas), 'Executor dst config baseGas not set correctly') - assert(BigNumber.from(polledDstConfig[3]).eq(nativeCap), 'Executor dst config nativeCap not set correctly') - await deployments.delete('DVN') const dvn = await deployments.deploy('DVN', { from: deployer, diff --git a/tests/ua-devtools-evm-hardhat-test/test/__utils__/endpoint.ts b/tests/ua-devtools-evm-hardhat-test/test/__utils__/endpoint.ts index 7a4bc6b61..049daa468 100644 --- a/tests/ua-devtools-evm-hardhat-test/test/__utils__/endpoint.ts +++ b/tests/ua-devtools-evm-hardhat-test/test/__utils__/endpoint.ts @@ -17,9 +17,13 @@ import { configurePriceFeed, PriceFeedEdgeConfig, PriceData, + ExecutorEdgeConfig, + ExecutorDstConfig, + configureExecutor, } from '@layerzerolabs/protocol-devtools' import { createEndpointFactory, + createExecutorFactory, createPriceFeedFactory, createUln302Factory, } from '@layerzerolabs/protocol-devtools-evm' @@ -50,6 +54,13 @@ const defaultPriceData: PriceData = { gasPerByte: 1, } +const defaultExecutorDstConfig: ExecutorDstConfig = { + baseGas: BigInt(200_000), + multiplierBps: BigInt(0), + floorMarginUSD: BigInt(0), + nativeCap: BigInt(250_000_000_000_000_000), // 0.25 ether +} + /** * Helper function to generate the default Uln302ExecutorConfig for a given chain. * @@ -113,6 +124,7 @@ export const setupDefaultEndpoint = async (): Promise => { const ulnSdkFactory = createUln302Factory(contractFactory) const endpointSdkFactory = createEndpointFactory(contractFactory, ulnSdkFactory) const priceFeedSdkFactory = createPriceFeedFactory(contractFactory) + const executorSdkFactory = createExecutorFactory(contractFactory) // For the graphs, we'll also need the pointers to the contracts const ethSendUlnPoint = omniContractToPoint(await contractFactory(ethSendUln)) @@ -127,6 +139,34 @@ export const setupDefaultEndpoint = async (): Promise => { const ethUlnConfig: Uln302UlnConfig = getDefaultUlnConfig(ethDvnPoint.address) const avaxUlnConfig: Uln302UlnConfig = getDefaultUlnConfig(avaxDvnPoint.address) + // This is the graph for Executor + const executorConfig: OmniGraphHardhat = { + contracts: [ + { + contract: ethExecutor, + }, + { + contract: avaxExecutor, + }, + ], + connections: [ + { + from: ethExecutor, + to: avaxExecutor, + config: { + dstConfig: defaultExecutorDstConfig, + }, + }, + { + from: avaxExecutor, + to: ethExecutor, + config: { + dstConfig: defaultExecutorDstConfig, + }, + }, + ], + } + // This is the graph for PriceFeed const priceFeedConfig: OmniGraphHardhat = { contracts: [ @@ -283,6 +323,9 @@ export const setupDefaultEndpoint = async (): Promise => { const builderPriceFeed = await OmniGraphBuilderHardhat.fromConfig(priceFeedConfig) const priceFeedTransactions = await configurePriceFeed(builderPriceFeed.graph, priceFeedSdkFactory) + const builderExecutor = await OmniGraphBuilderHardhat.fromConfig(executorConfig) + const executorTransactions = await configureExecutor(builderExecutor.graph, executorSdkFactory) + const builderSendUln = await OmniGraphBuilderHardhat.fromConfig(sendUlnConfig) const sendUlnTransactions = await configureUln302(builderSendUln.graph, ulnSdkFactory) @@ -297,6 +340,7 @@ export const setupDefaultEndpoint = async (): Promise => { const transactions = [ ...priceFeedTransactions, + ...executorTransactions, ...sendUlnTransactions, ...receiveUlnTransactions, ...endpointTransactions,