diff --git a/.changeset/perfect-apes-sparkle.md b/.changeset/perfect-apes-sparkle.md new file mode 100644 index 000000000..b58046233 --- /dev/null +++ b/.changeset/perfect-apes-sparkle.md @@ -0,0 +1,7 @@ +--- +"@layerzerolabs/ua-devtools-evm-hardhat-test": patch +"@layerzerolabs/protocol-devtools-evm": patch +"@layerzerolabs/protocol-devtools": patch +--- + +Add PriceFeed SDK diff --git a/packages/protocol-devtools-evm/src/index.ts b/packages/protocol-devtools-evm/src/index.ts index ff12d505a..11739325c 100644 --- a/packages/protocol-devtools-evm/src/index.ts +++ b/packages/protocol-devtools-evm/src/index.ts @@ -1,2 +1,3 @@ export * from './endpoint' +export * from './priceFeed' export * from './uln302' diff --git a/packages/protocol-devtools-evm/src/priceFeed/factory.ts b/packages/protocol-devtools-evm/src/priceFeed/factory.ts new file mode 100644 index 000000000..b04f54ed2 --- /dev/null +++ b/packages/protocol-devtools-evm/src/priceFeed/factory.ts @@ -0,0 +1,15 @@ +import pMemoize from 'p-memoize' +import type { OmniContractFactory } from '@layerzerolabs/devtools-evm' +import type { PriceFeedFactory } from '@layerzerolabs/protocol-devtools' +import { PriceFeed } from './sdk' + +/** + * Syntactic sugar that creates an instance of EVM `PriceFeed` SDK + * based on an `OmniPoint` with help of an `OmniContractFactory` + * + * @param {OmniContractFactory} contractFactory + * @returns {PriceFeedFactory} + */ +export const createPriceFeedFactory = ( + contractFactory: OmniContractFactory +): PriceFeedFactory => pMemoize(async (point) => new PriceFeed(await contractFactory(point))) diff --git a/packages/protocol-devtools-evm/src/priceFeed/index.ts b/packages/protocol-devtools-evm/src/priceFeed/index.ts new file mode 100644 index 000000000..166c7fe16 --- /dev/null +++ b/packages/protocol-devtools-evm/src/priceFeed/index.ts @@ -0,0 +1,3 @@ +export * from './factory' +export * from './schema' +export * from './sdk' diff --git a/packages/protocol-devtools-evm/src/priceFeed/schema.ts b/packages/protocol-devtools-evm/src/priceFeed/schema.ts new file mode 100644 index 000000000..e8b1d01ab --- /dev/null +++ b/packages/protocol-devtools-evm/src/priceFeed/schema.ts @@ -0,0 +1,13 @@ +import { BigNumberishBigintSchema } from '@layerzerolabs/devtools-evm' +import { PriceData } from '@layerzerolabs/protocol-devtools' +import { PriceDataSchema as PriceDataSchemaBase } from '@layerzerolabs/protocol-devtools' +import { z } from 'zod' + +/** + * Schema for parsing an ethers-specific PriceData into a common format + */ +export const PriceDataSchema = PriceDataSchemaBase.extend({ + priceRatio: BigNumberishBigintSchema, + gasPriceInUnit: BigNumberishBigintSchema, + gasPerByte: BigNumberishBigintSchema, +}) satisfies z.ZodSchema diff --git a/packages/protocol-devtools-evm/src/priceFeed/sdk.ts b/packages/protocol-devtools-evm/src/priceFeed/sdk.ts new file mode 100644 index 000000000..0dbaa6e86 --- /dev/null +++ b/packages/protocol-devtools-evm/src/priceFeed/sdk.ts @@ -0,0 +1,34 @@ +import type { EndpointId } from '@layerzerolabs/lz-definitions' +import type { IPriceFeed, PriceData } from '@layerzerolabs/protocol-devtools' +import { formatEid, type OmniTransaction } from '@layerzerolabs/devtools' +import { OmniSDK } from '@layerzerolabs/devtools-evm' +import { printRecord } from '@layerzerolabs/io-devtools' +import { PriceDataSchema } from './schema' + +export class PriceFeed extends OmniSDK implements IPriceFeed { + async getPrice(eid: EndpointId): Promise { + const config = await this.contract.contract['getPrice(uint32)'](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 PriceDataSchema.parse({ ...config }) + } + + async setPrice(eid: EndpointId, priceData: PriceData): Promise { + const data = this.contract.contract.interface.encodeFunctionData('setPrice', [ + [ + { + eid, + price: priceData, + }, + ], + ]) + + return { + ...this.createTransaction(data), + description: `Setting price for ${formatEid(eid)}: ${printRecord(priceData)}`, + } + } +} diff --git a/packages/protocol-devtools/src/endpoint/config.ts b/packages/protocol-devtools/src/endpoint/config.ts index 0f9d27e78..9127b6eaf 100644 --- a/packages/protocol-devtools/src/endpoint/config.ts +++ b/packages/protocol-devtools/src/endpoint/config.ts @@ -5,8 +5,8 @@ export type EndpointConfigurator = (graph: EndpointOmniGraph, createSdk: Endpoin export const configureEndpoint: EndpointConfigurator = async (graph, createSdk) => flattenTransactions([ - ...(await configureEndpointDefaultReceiveLibraries(graph, createSdk)), - ...(await configureEndpointDefaultSendLibraries(graph, createSdk)), + await configureEndpointDefaultReceiveLibraries(graph, createSdk), + await configureEndpointDefaultSendLibraries(graph, createSdk), ]) export const configureEndpointDefaultReceiveLibraries: EndpointConfigurator = async (graph, createSdk) => diff --git a/packages/protocol-devtools/src/index.ts b/packages/protocol-devtools/src/index.ts index ff12d505a..11739325c 100644 --- a/packages/protocol-devtools/src/index.ts +++ b/packages/protocol-devtools/src/index.ts @@ -1,2 +1,3 @@ export * from './endpoint' +export * from './priceFeed' export * from './uln302' diff --git a/packages/protocol-devtools/src/priceFeed/config.ts b/packages/protocol-devtools/src/priceFeed/config.ts new file mode 100644 index 000000000..b7f0a25f6 --- /dev/null +++ b/packages/protocol-devtools/src/priceFeed/config.ts @@ -0,0 +1,25 @@ +import { flattenTransactions, isDeepEqual, type OmniTransaction } from '@layerzerolabs/devtools' +import type { PriceFeedFactory, PriceFeedOmniGraph } from './types' + +export type PriceFeedConfigurator = ( + graph: PriceFeedOmniGraph, + createSdk: PriceFeedFactory +) => Promise + +export const configurePriceFeed: PriceFeedConfigurator = async (graph, createSdk) => + flattenTransactions([await configurePriceFeedPriceData(graph, createSdk)]) + +export const configurePriceFeedPriceData: PriceFeedConfigurator = async (graph, createSdk) => + flattenTransactions( + await Promise.all( + graph.connections.map(async ({ vector: { from, to }, config }): Promise => { + const sdk = await createSdk(from) + const priceData = await sdk.getPrice(to.eid) + + // TODO Normalize the config values using a schema before comparing them + if (isDeepEqual(priceData, config.priceData)) return [] + + return [await sdk.setPrice(to.eid, config.priceData)] + }) + ) + ) diff --git a/packages/protocol-devtools/src/priceFeed/index.ts b/packages/protocol-devtools/src/priceFeed/index.ts new file mode 100644 index 000000000..1273e8646 --- /dev/null +++ b/packages/protocol-devtools/src/priceFeed/index.ts @@ -0,0 +1,3 @@ +export * from './config' +export * from './schema' +export * from './types' diff --git a/packages/protocol-devtools/src/priceFeed/schema.ts b/packages/protocol-devtools/src/priceFeed/schema.ts new file mode 100644 index 000000000..bd33cb9cf --- /dev/null +++ b/packages/protocol-devtools/src/priceFeed/schema.ts @@ -0,0 +1,9 @@ +import { z } from 'zod' +import type { PriceData } from './types' +import { UIntSchema } from '@layerzerolabs/devtools' + +export const PriceDataSchema = z.object({ + priceRatio: UIntSchema, + gasPriceInUnit: UIntSchema, + gasPerByte: UIntSchema, +}) satisfies z.ZodSchema diff --git a/packages/protocol-devtools/src/priceFeed/types.ts b/packages/protocol-devtools/src/priceFeed/types.ts new file mode 100644 index 000000000..a89e3e3dc --- /dev/null +++ b/packages/protocol-devtools/src/priceFeed/types.ts @@ -0,0 +1,24 @@ +import type { Factory, IOmniSDK, OmniGraph, OmniPoint, OmniTransaction } from '@layerzerolabs/devtools' +import type { EndpointId } from '@layerzerolabs/lz-definitions' + +export interface IPriceFeed extends IOmniSDK { + getPrice(eid: EndpointId): Promise + setPrice(eid: EndpointId, priceData: PriceData): Promise +} + +export interface PriceData { + priceRatio: bigint | string | number + gasPriceInUnit: bigint | string | number + gasPerByte: bigint | string | number +} + +export interface PriceFeedEdgeConfig { + priceData: PriceData +} + +export type PriceFeedOmniGraph = OmniGraph + +export type PriceFeedFactory = Factory< + [TOmniPoint], + TPriceFeed +> 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 9faa4bca2..9c0237d4a 100644 --- a/tests/ua-devtools-evm-hardhat-test/deploy/001_bootstrap.ts +++ b/tests/ua-devtools-evm-hardhat-test/deploy/001_bootstrap.ts @@ -87,19 +87,6 @@ const deploy: DeployFunction = async ({ getUnnamedAccounts, deployments, network }, }, }) - const priceFeedContract = new Contract(priceFeed.address, priceFeed.abi).connect(signer) - const setPriceResp: TransactionResponse = await priceFeedContract.setPrice([ - { - eid: dstEid, - price: { - priceRatio: '100000000000000000000', - gasPriceInUnit: 1, - gasPerByte: 1, - }, - }, - ]) - const setPriceReceipt = await setPriceResp.wait() - assert(setPriceReceipt?.status === 1) await deployments.delete('ExecutorFeeLib') const executorFeeLib = await deployments.deploy('ExecutorFeeLib', { 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 043bdf0f1..7a4bc6b61 100644 --- a/tests/ua-devtools-evm-hardhat-test/test/__utils__/endpoint.ts +++ b/tests/ua-devtools-evm-hardhat-test/test/__utils__/endpoint.ts @@ -14,13 +14,21 @@ import { Uln302ExecutorConfig, configureUln302, Uln302UlnConfig, + configurePriceFeed, + PriceFeedEdgeConfig, + PriceData, } from '@layerzerolabs/protocol-devtools' -import { createEndpointFactory, createUln302Factory } from '@layerzerolabs/protocol-devtools-evm' +import { + createEndpointFactory, + createPriceFeedFactory, + createUln302Factory, +} from '@layerzerolabs/protocol-devtools-evm' import { createSignAndSend } from '@layerzerolabs/devtools' export const ethEndpoint = { eid: EndpointId.ETHEREUM_V2_MAINNET, contractName: 'EndpointV2' } export const ethReceiveUln = { eid: EndpointId.ETHEREUM_V2_MAINNET, contractName: 'ReceiveUln302' } export const ethSendUln = { eid: EndpointId.ETHEREUM_V2_MAINNET, contractName: 'SendUln302' } +export const ethPriceFeed = { eid: EndpointId.ETHEREUM_V2_MAINNET, contractName: 'PriceFeed' } export const ethReceiveUln2_Opt2 = { eid: EndpointId.ETHEREUM_V2_MAINNET, contractName: 'ReceiveUln302_Opt2' } export const ethSendUln2_Opt2 = { eid: EndpointId.ETHEREUM_V2_MAINNET, contractName: 'SendUln302_Opt2' } export const ethExecutor = { eid: EndpointId.ETHEREUM_V2_MAINNET, contractName: 'Executor' } @@ -28,6 +36,7 @@ export const ethDvn = { eid: EndpointId.ETHEREUM_V2_MAINNET, contractName: 'DVN' export const avaxEndpoint = { eid: EndpointId.AVALANCHE_V2_MAINNET, contractName: 'EndpointV2' } export const avaxReceiveUln = { eid: EndpointId.AVALANCHE_V2_MAINNET, contractName: 'ReceiveUln302' } export const avaxSendUln = { eid: EndpointId.AVALANCHE_V2_MAINNET, contractName: 'SendUln302' } +export const avaxPriceFeed = { eid: EndpointId.AVALANCHE_V2_MAINNET, contractName: 'PriceFeed' } export const avaxReceiveUln2_Opt2 = { eid: EndpointId.AVALANCHE_V2_MAINNET, contractName: 'ReceiveUln302_Opt2' } export const avaxSendUln2_Opt2 = { eid: EndpointId.AVALANCHE_V2_MAINNET, contractName: 'SendUln302_Opt2' } export const avaxExecutor = { eid: EndpointId.AVALANCHE_V2_MAINNET, contractName: 'Executor' } @@ -35,6 +44,12 @@ export const avaxDvn = { eid: EndpointId.AVALANCHE_V2_MAINNET, contractName: 'DV export const MAX_MESSAGE_SIZE = 10000 // match on-chain value +const defaultPriceData: PriceData = { + priceRatio: '100000000000000000000', + gasPriceInUnit: 1, + gasPerByte: 1, +} + /** * Helper function to generate the default Uln302ExecutorConfig for a given chain. * @@ -97,6 +112,7 @@ export const setupDefaultEndpoint = async (): Promise => { const signAndSend = createSignAndSend(createSignerFactory()) const ulnSdkFactory = createUln302Factory(contractFactory) const endpointSdkFactory = createEndpointFactory(contractFactory, ulnSdkFactory) + const priceFeedSdkFactory = createPriceFeedFactory(contractFactory) // For the graphs, we'll also need the pointers to the contracts const ethSendUlnPoint = omniContractToPoint(await contractFactory(ethSendUln)) @@ -111,6 +127,34 @@ export const setupDefaultEndpoint = async (): Promise => { const ethUlnConfig: Uln302UlnConfig = getDefaultUlnConfig(ethDvnPoint.address) const avaxUlnConfig: Uln302UlnConfig = getDefaultUlnConfig(avaxDvnPoint.address) + // This is the graph for PriceFeed + const priceFeedConfig: OmniGraphHardhat = { + contracts: [ + { + contract: ethPriceFeed, + }, + { + contract: avaxPriceFeed, + }, + ], + connections: [ + { + from: ethPriceFeed, + to: avaxPriceFeed, + config: { + priceData: defaultPriceData, + }, + }, + { + from: avaxPriceFeed, + to: ethPriceFeed, + config: { + priceData: defaultPriceData, + }, + }, + ], + } + // This is the graph for SendUln302 const sendUlnConfig: OmniGraphHardhat = { contracts: [ @@ -236,6 +280,9 @@ export const setupDefaultEndpoint = async (): Promise => { const builderEndpoint = await OmniGraphBuilderHardhat.fromConfig(config) const endpointTransactions = await configureEndpoint(builderEndpoint.graph, endpointSdkFactory) + const builderPriceFeed = await OmniGraphBuilderHardhat.fromConfig(priceFeedConfig) + const priceFeedTransactions = await configurePriceFeed(builderPriceFeed.graph, priceFeedSdkFactory) + const builderSendUln = await OmniGraphBuilderHardhat.fromConfig(sendUlnConfig) const sendUlnTransactions = await configureUln302(builderSendUln.graph, ulnSdkFactory) @@ -249,6 +296,7 @@ export const setupDefaultEndpoint = async (): Promise => { const receiveUlnTransactions_Opt2 = await configureUln302(builderReceiveUln_Opt2.graph, ulnSdkFactory) const transactions = [ + ...priceFeedTransactions, ...sendUlnTransactions, ...receiveUlnTransactions, ...endpointTransactions,