diff --git a/packages/omnicounter-devtools-evm/src/omnicounter/sdk.ts b/packages/omnicounter-devtools-evm/src/omnicounter/sdk.ts index e3562bf7a..30b64437d 100644 --- a/packages/omnicounter-devtools-evm/src/omnicounter/sdk.ts +++ b/packages/omnicounter-devtools-evm/src/omnicounter/sdk.ts @@ -1,8 +1,9 @@ -import { IOmniCounter } from '@layerzerolabs/omnicounter-devtools' +import { EndpointId } from '@layerzerolabs/lz-definitions' +import { IncrementOutput, IncrementType, IOmniCounter } from '@layerzerolabs/omnicounter-devtools' import { EndpointFactory } from '@layerzerolabs/protocol-devtools' import { OApp } from '@layerzerolabs/ua-devtools-evm' -import { OmniTransaction } from '@layerzerolabs/devtools' -import { OmniContract } from '@layerzerolabs/devtools-evm' +import { Address } from '@layerzerolabs/devtools' +import { makeBytes32, OmniContract } from '@layerzerolabs/devtools-evm' export class OmniCounter extends OApp implements IOmniCounter { public constructor( @@ -12,8 +13,24 @@ export class OmniCounter extends OApp implements IOmniCounter { super(contract, endpointFactory) } - public async increment(eid: number, type: number, options: string): Promise { + public async increment( + eid: EndpointId, + type: IncrementType, + options: Uint8Array, + receiver: Address + ): Promise { const data = this.contract.contract.interface.encodeFunctionData('increment', [eid, type, options]) - return super.createTransaction(data) + const endpointSdk = await super.getEndpointSDK() + const messagingFee = await endpointSdk.quote( + { dstEid: eid, options, message: data, receiver: makeBytes32(receiver), payInLzToken: false }, + this.contract.contract.address + ) + const gasLimit = (await this.contract.contract.estimateGas.increment!(eid, type, options)).toBigInt() + + return { + omniTransaction: super.createTransaction(data), + messagingFee, + gasLimit, + } } } diff --git a/packages/omnicounter-devtools/package.json b/packages/omnicounter-devtools/package.json index 1c82a8e09..71e3bb326 100644 --- a/packages/omnicounter-devtools/package.json +++ b/packages/omnicounter-devtools/package.json @@ -32,6 +32,7 @@ "devDependencies": { "@layerzerolabs/devtools": "~0.0.1", "@layerzerolabs/lz-definitions": "~2.0.7", + "@layerzerolabs/protocol-devtools": "~0.0.1", "@types/jest": "^29.5.11", "jest": "^29.7.0", "ts-jest": "^29.1.1", diff --git a/packages/omnicounter-devtools/src/omnicounter/types.ts b/packages/omnicounter-devtools/src/omnicounter/types.ts index d34c67bfd..984ef077c 100644 --- a/packages/omnicounter-devtools/src/omnicounter/types.ts +++ b/packages/omnicounter-devtools/src/omnicounter/types.ts @@ -1,6 +1,20 @@ -import { OmniTransaction } from '@layerzerolabs/devtools' +import { Address, OmniTransaction } from '@layerzerolabs/devtools' import { EndpointId } from '@layerzerolabs/lz-definitions' +import { MessagingFee } from '@layerzerolabs/protocol-devtools' + +export type IncrementOutput = { + omniTransaction: OmniTransaction + messagingFee: MessagingFee + gasLimit: bigint +} + +export enum IncrementType { + VANILLA_TYPE = 1, + COMPOSED_TYPE = 2, + ABA_TYPE = 3, + COMPOSED_ABA_TYPE = 4, +} export interface IOmniCounter { - increment(eid: EndpointId, type: number, options: string): Promise + increment(eid: EndpointId, type: IncrementType, options: Uint8Array, receiver: Address): Promise } diff --git a/packages/protocol-devtools-evm/src/endpoint/sdk.ts b/packages/protocol-devtools-evm/src/endpoint/sdk.ts index a10ecb908..c6428f0c9 100644 --- a/packages/protocol-devtools-evm/src/endpoint/sdk.ts +++ b/packages/protocol-devtools-evm/src/endpoint/sdk.ts @@ -1,3 +1,4 @@ +import { MessageParams, MessagingFee } from '@layerzerolabs/protocol-devtools' import assert from 'assert' import type { IEndpoint, @@ -198,4 +199,12 @@ export class Endpoint extends OmniSDK implements IEndpoint { description: `Registering library ${lib}`, } } + + public async quote(params: MessageParams, sender: Address): Promise { + const { nativeFee, lzTokenFee } = await this.contract.contract.quote(params, sender) + return { + nativeFee: BigInt(nativeFee), + lzTokenFee: BigInt(lzTokenFee), + } + } } diff --git a/packages/protocol-devtools/src/endpoint/types.ts b/packages/protocol-devtools/src/endpoint/types.ts index 31ad06321..dd4196bc2 100644 --- a/packages/protocol-devtools/src/endpoint/types.ts +++ b/packages/protocol-devtools/src/endpoint/types.ts @@ -48,6 +48,8 @@ export interface IEndpoint extends IOmniSDK { getUlnConfig(oapp: Address, lib: Address, eid: EndpointId): Promise setUlnConfig(oapp: Address, lib: Address, setUlnConfig: Uln302SetUlnConfig[]): Promise + + quote(params: MessageParams, sender: Address): Promise } export type Uln302SetExecutorConfig = { eid: EndpointId; executorConfig: Uln302ExecutorConfig } @@ -59,6 +61,19 @@ export interface SetConfigParam { config: string } +export interface MessageParams { + dstEid: EndpointId + receiver: Address + message: string | Uint8Array + options: string | Uint8Array + payInLzToken: boolean +} + +export interface MessagingFee { + nativeFee: bigint + lzTokenFee: bigint +} + export interface Timeout { lib: string expiry: number diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 40f010079..e14c12aab 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -459,6 +459,9 @@ importers: '@layerzerolabs/lz-definitions': specifier: ~2.0.7 version: 2.0.7 + '@layerzerolabs/protocol-devtools': + specifier: ~0.0.1 + version: link:../protocol-devtools '@types/jest': specifier: ^29.5.11 version: 29.5.11 @@ -1061,6 +1064,12 @@ importers: '@layerzerolabs/lz-utility-v2': specifier: ~2.0.7 version: 2.0.7 + '@layerzerolabs/omnicounter-devtools': + specifier: ~0.0.1 + version: link:../../packages/omnicounter-devtools + '@layerzerolabs/omnicounter-devtools-evm': + specifier: ~0.0.1 + version: link:../../packages/omnicounter-devtools-evm '@layerzerolabs/protocol-devtools': specifier: ~0.0.1 version: link:../../packages/protocol-devtools @@ -1091,6 +1100,9 @@ importers: ethers: specifier: ^5.7.0 version: 5.7.2 + fast-check: + specifier: ^3.15.0 + version: 3.15.0 hardhat: specifier: ^2.19.4 version: 2.19.4(ts-node@10.9.2)(typescript@5.3.3) 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 3af33a3da..9faa4bca2 100644 --- a/tests/ua-devtools-evm-hardhat-test/deploy/001_bootstrap.ts +++ b/tests/ua-devtools-evm-hardhat-test/deploy/001_bootstrap.ts @@ -1,9 +1,11 @@ -import { type DeployFunction } from 'hardhat-deploy/types' 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 { Contract } from 'ethers' +import { BigNumber, Contract } from 'ethers' +import { parseEther } from 'ethers/lib/utils' +import { type DeployFunction } from 'hardhat-deploy/types' import { HardhatRuntimeEnvironment } from 'hardhat/types' const DEFAULT_NATIVE_DECIMALS_RATE = '18' //ethers.utils.parseUnits('1', 18).toString() @@ -28,6 +30,13 @@ 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, @@ -78,6 +87,19 @@ 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', { @@ -128,6 +150,23 @@ 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/jest.config.js b/tests/ua-devtools-evm-hardhat-test/jest.config.js index 2f60d2661..ff17d9c22 100644 --- a/tests/ua-devtools-evm-hardhat-test/jest.config.js +++ b/tests/ua-devtools-evm-hardhat-test/jest.config.js @@ -3,7 +3,7 @@ module.exports = { preset: 'ts-jest', cache: false, testEnvironment: 'node', - testTimeout: 15000, + testTimeout: 150000, moduleNameMapper: { '^@/(.*)$': '/src/$1', }, diff --git a/tests/ua-devtools-evm-hardhat-test/package.json b/tests/ua-devtools-evm-hardhat-test/package.json index 45ef76b8d..35f67a33d 100644 --- a/tests/ua-devtools-evm-hardhat-test/package.json +++ b/tests/ua-devtools-evm-hardhat-test/package.json @@ -31,6 +31,8 @@ "@layerzerolabs/lz-evm-sdk-v1": "~2.0.7", "@layerzerolabs/lz-evm-sdk-v2": "~2.0.7", "@layerzerolabs/lz-utility-v2": "~2.0.7", + "@layerzerolabs/omnicounter-devtools": "~0.0.1", + "@layerzerolabs/omnicounter-devtools-evm": "~0.0.1", "@layerzerolabs/protocol-devtools": "~0.0.1", "@layerzerolabs/protocol-devtools-evm": "~0.0.1", "@layerzerolabs/toolbox-hardhat": "~0.0.1", @@ -41,6 +43,7 @@ "@openzeppelin/contracts": "^4.9.5", "@types/jest": "^29.5.11", "ethers": "^5.7.0", + "fast-check": "^3.15.0", "hardhat": "^2.19.4", "hardhat-deploy": "^0.11.45", "jest": "^29.7.0", diff --git a/tests/ua-devtools-evm-hardhat-test/test/__utils__/omnicounter.ts b/tests/ua-devtools-evm-hardhat-test/test/__utils__/omnicounter.ts index a19c48f3a..46463f677 100644 --- a/tests/ua-devtools-evm-hardhat-test/test/__utils__/omnicounter.ts +++ b/tests/ua-devtools-evm-hardhat-test/test/__utils__/omnicounter.ts @@ -1,17 +1,6 @@ import { EndpointId } from '@layerzerolabs/lz-definitions' import { createGetHreByEid } from '@layerzerolabs/devtools-evm-hardhat' -export const deployOmniCounter = async () => { - const environmentFactory = createGetHreByEid() - const eth = await environmentFactory(EndpointId.ETHEREUM_V2_MAINNET) - const avax = await environmentFactory(EndpointId.AVALANCHE_V2_MAINNET) - - await Promise.all([ - eth.deployments.run('OmniCounter', { writeDeploymentsToFiles: true }), - avax.deployments.run('OmniCounter', { writeDeploymentsToFiles: true }), - ]) -} - export const deployOmniCounterFixture = async () => { const environmentFactory = createGetHreByEid() const eth = await environmentFactory(EndpointId.ETHEREUM_V2_MAINNET) diff --git a/tests/ua-devtools-evm-hardhat-test/test/oapp/config.test.ts b/tests/ua-devtools-evm-hardhat-test/test/oapp/config.test.ts index cd624eb03..71c08c784 100644 --- a/tests/ua-devtools-evm-hardhat-test/test/oapp/config.test.ts +++ b/tests/ua-devtools-evm-hardhat-test/test/oapp/config.test.ts @@ -25,7 +25,7 @@ import { import { OAppEdgeConfig } from '@layerzerolabs/ua-devtools' import { OmniTransaction } from '@layerzerolabs/devtools' -type OAppTestConfig = { +export type OAppTestConfig = { sendLibrary: string receiveLibrary: string executorLibrary: string @@ -204,7 +204,7 @@ const getLibraryAddress = async (library: OmniPointHardhat): Promise => return executorPoint.address } -const setUpConfig = async (testConfig: OAppTestConfig): Promise => { +export const setUpConfig = async (testConfig: OAppTestConfig): Promise => { return { sendLibrary: testConfig.sendLibrary, receiveLibraryConfig: { @@ -242,7 +242,7 @@ const setUpConfig = async (testConfig: OAppTestConfig): Promise } } -const setUpOmniGraphHardhat = ( +export const setUpOmniGraphHardhat = ( ethContract: OmniPointHardhat, ethOAppConfig: OAppEdgeConfig, avaxContract, @@ -272,7 +272,7 @@ const setUpOmniGraphHardhat = ( } } -const getDefaultEthConfig = async (): Promise => { +export const getDefaultEthConfig = async (): Promise => { const ethDVNAddress = await getLibraryAddress(ethDvn) const avaxDvnPoint = await getLibraryAddress(avaxDvn) const ethSendUlnRequiredDVNs: string[] = [avaxDvnPoint] @@ -299,7 +299,7 @@ const getDefaultEthConfig = async (): Promise => { } } -const getDefaultAvaxConfig = async (): Promise => { +export const getDefaultAvaxConfig = async (): Promise => { const ethDVNAddress = await getLibraryAddress(ethDvn) const avaxDvnPoint = await getLibraryAddress(avaxDvn) const avaxSendUlnRequiredDVNs: string[] = [ethDVNAddress] diff --git a/tests/ua-devtools-evm-hardhat-test/test/omnicounter/options.test.ts b/tests/ua-devtools-evm-hardhat-test/test/omnicounter/options.test.ts new file mode 100644 index 000000000..6094c7241 --- /dev/null +++ b/tests/ua-devtools-evm-hardhat-test/test/omnicounter/options.test.ts @@ -0,0 +1,295 @@ +import { makeBytes32, OmniSignerEVM, parseLogsWithName } from '@layerzerolabs/devtools-evm' +import { parseEther } from 'ethers/lib/utils' +import fc from 'fast-check' +import 'hardhat' +import { TransactionReceipt } from '@ethersproject/providers' +import { EndpointId } from '@layerzerolabs/lz-definitions' +import { Options } from '@layerzerolabs/lz-utility-v2' +import { IncrementType } from '@layerzerolabs/omnicounter-devtools' +import { createOmniCounterFactory, OmniCounter } from '@layerzerolabs/omnicounter-devtools-evm' +import { createEndpointFactory } from '@layerzerolabs/protocol-devtools-evm' +import { configureOAppPeers, OAppEdgeConfig } from '@layerzerolabs/ua-devtools' +import { createSignAndSend, OmniPoint, OmniTransaction } from '@layerzerolabs/devtools' +import { omniContractToPoint } from '@layerzerolabs/devtools-evm' +import { + createConnectedContractFactory, + createSignerFactory, + OmniContractFactoryHardhat, + OmniGraphBuilderHardhat, + OmniGraphHardhat, +} from '@layerzerolabs/devtools-evm-hardhat' +import { setupDefaultEndpoint } from '../__utils__/endpoint' +import { deployOmniCounterFixture } from '../__utils__/omnicounter' +import { + getDefaultAvaxConfig, + getDefaultEthConfig, + OAppTestConfig, + setUpConfig, + setUpOmniGraphHardhat, +} from '../oapp/config.test' + +// The maximum value of an uint16. +const MAX_UINT_16: number = 0xffff + +// The maximum value of an uint128. +const MAX_UINT_128: bigint = BigInt('0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF') + +// The minimum native drop measured in native gas unit. +const DEFAULT_MIN_NATIVE_DROP: bigint = BigInt(0) + +// The maximum native drop measured in native gas unit (currently configured in 001_bootstap.ts). +const DEFAULT_MAX_NATIVE_DROP: bigint = parseEther('0.25').toBigInt() + +// The minimum gasLimit for an Option. +const MIN_GAS_LIMIT = BigInt(0) + +// The maximum gasLimit for an Option. +const MAX_GAS_LIMIT = MAX_UINT_128 + +// The minimum index for a Composed Option. +const MIN_COMPOSED_INDEX: number = 0 + +// The maximum index for a Composed Option. +const MAX_COMPOSED_INDEX: number = MAX_UINT_16 + +// An OmniCounter User Application specific arbitrary using the different types of "increment". +const omnicounterIncrementTypeArbitrary: fc.Arbitrary = fc.constantFrom( + IncrementType.VANILLA_TYPE, + IncrementType.COMPOSED_TYPE, + IncrementType.ABA_TYPE, + IncrementType.COMPOSED_ABA_TYPE +) + +// An arbitrary for gasLimit. +const gasLimitArbitrary: fc.Arbitrary = fc.bigInt({ min: MIN_GAS_LIMIT, max: MAX_GAS_LIMIT }) + +// An arbitrary for value (native drop). +const nativeDropArbitrary: fc.Arbitrary = fc.bigInt({ + min: DEFAULT_MIN_NATIVE_DROP, + max: DEFAULT_MAX_NATIVE_DROP, +}) + +// An arbitrary for Composed Option index. +const composedIndexArbitrary: fc.Arbitrary = fc.integer({ min: MIN_COMPOSED_INDEX, max: MAX_COMPOSED_INDEX }) + +/** + * Helper used to apply a 10% premium to input. + * @param {bigint} input + */ +const applyPremium = (input: bigint) => (BigInt(input) * BigInt(110)) / BigInt(100) + +// Test the OApp options using the OmniCounter OApp as the test contract. +describe('oapp/options', () => { + const ethOmniCounter = { eid: EndpointId.ETHEREUM_V2_MAINNET, contractName: 'OmniCounter' } + const avaxOmniCounter = { eid: EndpointId.AVALANCHE_V2_MAINNET, contractName: 'OmniCounter' } + const ethEndpoint = { eid: EndpointId.ETHEREUM_V2_MAINNET, contractName: 'EndpointV2' } + + let ethSdk: OmniCounter + let ethSigner: OmniSignerEVM + let avaxPoint: OmniPoint + let contractFactory: OmniContractFactoryHardhat + + beforeEach(async () => { + await deployOmniCounterFixture() + await setupDefaultEndpoint() + contractFactory = createConnectedContractFactory() + const sdkFactory = createOmniCounterFactory(contractFactory) + const signerFactory = createSignerFactory() + + const ethPoint = omniContractToPoint(await contractFactory(ethOmniCounter)) + const ethTestConfig: OAppTestConfig = await getDefaultEthConfig() + const ethOAppConfig: OAppEdgeConfig = await setUpConfig(ethTestConfig) + ethSdk = await sdkFactory(ethPoint) + ethSigner = await signerFactory(ethOmniCounter.eid) + + avaxPoint = omniContractToPoint(await contractFactory(avaxOmniCounter)) + const avaxTestConfig: OAppTestConfig = await getDefaultAvaxConfig() + const avaxOAppConfig: OAppEdgeConfig = await setUpConfig(avaxTestConfig) + const avaxSdk = await sdkFactory(avaxPoint) + + const config: OmniGraphHardhat = setUpOmniGraphHardhat( + ethOmniCounter, + ethOAppConfig, + avaxOmniCounter, + avaxOAppConfig + ) + + const builder = await OmniGraphBuilderHardhat.fromConfig(config) + const transactions = await configureOAppPeers(builder.graph, sdkFactory) + const signAndSend = createSignAndSend(createSignerFactory()) + const [transactionReceipts, errors] = await signAndSend(transactions) + expect(transactionReceipts).toBeDefined() + expect(errors).toHaveLength(0) + + expect(await ethSdk.hasPeer(avaxPoint.eid, avaxPoint.address)).toBe(true) + expect(await avaxSdk.hasPeer(ethPoint.eid, ethPoint.address)).toBe(true) + }) + + // Helper function to perform OmniCounter.increment(...) and return the matching PacketSent logs. + const incrementAndReturnLogs = async (type: number, options: Options) => { + const incrementOutput = await ethSdk.increment(avaxPoint.eid, type, options.toBytes(), avaxPoint.address) + const incrementTx: OmniTransaction = { + ...incrementOutput.omniTransaction, + gasLimit: applyPremium(incrementOutput.gasLimit), + value: applyPremium(incrementOutput.messagingFee.nativeFee), + } + + const ethEndpointPoint = omniContractToPoint(await contractFactory(ethEndpoint)) + const endpointSdkFactory = createEndpointFactory(contractFactory) + const ethEndpointSdk = await endpointSdkFactory(ethEndpointPoint) + const incrementTxResponse = await ethSigner.signAndSend(incrementTx) + const incrementTxReceipt: TransactionReceipt = await incrementTxResponse.wait() + expect(incrementTxReceipt.status).toEqual(1) + return parseLogsWithName(incrementTxReceipt, ethEndpointSdk.contract.contract, 'PacketSent') + } + + it('executorLzReceiveOption', async () => { + // sanity check that generated options are the same with value as 0 and not provided. + expect(Options.newOptions().addExecutorLzReceiveOption(1, 0)).toEqual( + Options.newOptions().addExecutorLzReceiveOption(1) + ) + await fc.assert( + fc.asyncProperty( + omnicounterIncrementTypeArbitrary, + gasLimitArbitrary, + nativeDropArbitrary, + + // Test the generation and submission of arbitrary LZ_RECEIVE Options. The transaction should succeed, + // and the options from the transaction receipt logs should match the generated input. + async (type: number, gasLimit: bigint, value: bigint) => { + const options = Options.newOptions().addExecutorLzReceiveOption( + gasLimit.toString(), + value.toString() + ) + const packetSentEvents = await incrementAndReturnLogs(type, options) + expect(packetSentEvents).toHaveLength(1) + expect(packetSentEvents[0]!.args.options.toLowerCase() === options.toHex().toLowerCase()) + } + ) + ) + }) + + it('executorComposeOption', async () => { + await fc.assert( + fc.asyncProperty( + omnicounterIncrementTypeArbitrary, + composedIndexArbitrary, + gasLimitArbitrary, + nativeDropArbitrary, + + // Test the generation and submission of arbitrary COMPOSE Options. The transaction should succeed, and + // the options from the transaction receipt logs should match the generated input. + async (type: number, index: number, gasLimit: bigint, value: bigint) => { + const options = Options.newOptions().addExecutorComposeOption( + index, + gasLimit.toString(), + value.toString() + ) + const packetSentEvents = await incrementAndReturnLogs(type, options) + expect(packetSentEvents).toHaveLength(1) + expect(packetSentEvents[0]!.args.options.toLowerCase() === options.toHex().toLowerCase()) + } + ) + ) + }) + + it('executorLzNativeDrop', async () => { + const address = await ethSigner.signer.getAddress() + await fc.assert( + fc.asyncProperty( + omnicounterIncrementTypeArbitrary, + nativeDropArbitrary, + + // Test the generation and submission of arbitrary NATIVE_DROP Options. The transaction should succeed, + // and the options from the transaction receipt logs should match the generated input. + async (type: number, value: bigint) => { + const options = Options.newOptions().addExecutorNativeDropOption(value.toString(), address) + const packetSentEvents = await incrementAndReturnLogs(type, options) + expect(packetSentEvents).toHaveLength(1) + expect(packetSentEvents[0]!.args.options.toLowerCase() === options.toHex().toLowerCase()) + } + ) + ) + }) + + it('executorOrderedExecutionOption', async () => { + await fc.assert( + fc.asyncProperty( + omnicounterIncrementTypeArbitrary, + + // Test the generation and submission of arbitrary ORDERED Options. The transaction should succeed, and the + // options from the transaction receipt logs should match the generated input. + async (type) => { + const options = Options.newOptions().addExecutorOrderedExecutionOption() + const packetSentEvents = await incrementAndReturnLogs(type, options) + expect(packetSentEvents).toHaveLength(1) + expect(packetSentEvents[0]!.args.options.toLowerCase() === options.toHex().toLowerCase()) + } + ) + ) + }) + + it('stacked options', async () => { + // custom gasLimit arbitrary so "stacked" compose options won't have a gasLimit sum that overflows MAX_UINT_128 + const stackedGasLimitArbitrary: fc.Arbitrary = fc.bigInt({ + min: MIN_GAS_LIMIT, + max: BigInt('0xFFFFFFFFFFFFFFFF'), + }) + + // custom nativeDrop arbitrary so "stacked" compose options won't have a nativeDrop sum that exceeds DEFAULT_MAX_NATIVE_DROP + const stackedValueArbitrary: fc.Arbitrary = fc.bigInt({ + min: DEFAULT_MIN_NATIVE_DROP, + max: DEFAULT_MAX_NATIVE_DROP / BigInt(4), + }) + + const address = await ethSigner.signer.getAddress() + await fc.assert( + fc.asyncProperty( + omnicounterIncrementTypeArbitrary, + composedIndexArbitrary, + stackedGasLimitArbitrary, + stackedValueArbitrary, + + // Test the generation of multiple Options in a single Packet. The transaction should succeed. Options + // should be decoded to match inputs. gasLimit and nativeDrop should be summed for Packets that have + // multiple COMPOSE options for the same index. + async (type: number, index: number, gasLimit: bigint, value: bigint) => { + const gasLimitStr = gasLimit.toString() + const valueStr = value.toString() + const options = Options.newOptions() + .addExecutorComposeOption(index, gasLimitStr, valueStr) + .addExecutorLzReceiveOption(gasLimitStr, valueStr) + .addExecutorNativeDropOption(valueStr, address) + .addExecutorComposeOption(index, gasLimitStr, valueStr) // Repeat executor compose option to make sure values/gasLimits are summed + + const packetSentEvents = await incrementAndReturnLogs(type, options) + expect(packetSentEvents).toHaveLength(1) + expect(packetSentEvents[0]!.args.options.toLowerCase() === options.toHex().toLowerCase()) + const packetOptions = Options.fromOptions(packetSentEvents[0]!.args.options.toLowerCase()) + + // check executorComposeOption + const packetComposeOptions = packetOptions.decodeExecutorComposeOption() + expect(packetComposeOptions).toHaveLength(1) + const packetComposeOption = packetComposeOptions[0]! + expect(packetComposeOption.index).toEqual(index) + expect(packetComposeOption.gas.toBigInt()).toEqual(gasLimit * BigInt(2)) + // compose options with same index are summed (in this specific case, just multiplied by 2) + expect(packetComposeOption.value.toBigInt()).toEqual(value * BigInt(2)) + + // check executorLzReceiveOption + const packetLzReceiveOption = packetOptions.decodeExecutorLzReceiveOption() + expect(packetLzReceiveOption).toBeDefined() + expect(packetLzReceiveOption!.gas.toBigInt()).toEqual(gasLimit) + expect(packetLzReceiveOption!.value.toBigInt()).toEqual(value) + + // check executorNativeDropOption + const packetNativeDropOptions = packetOptions.decodeExecutorNativeDropOption() + expect(packetNativeDropOptions).toHaveLength(1) + const packetNativeDropOption = packetNativeDropOptions[0]! + expect(packetNativeDropOption.amount.toBigInt()).toEqual(value) + expect(packetNativeDropOption.receiver.toLowerCase()).toEqual(makeBytes32(address).toLowerCase()) + } + ) + ) + }) +})