diff --git a/.changeset/flat-bears-doubt.md b/.changeset/flat-bears-doubt.md new file mode 100644 index 0000000000..fb2b508169 --- /dev/null +++ b/.changeset/flat-bears-doubt.md @@ -0,0 +1,5 @@ +--- +"@layerzerolabs/oft-example": minor +--- + +Adds a normalized send script that can be extended for other VMs diff --git a/.changeset/orange-sheep-provide.md b/.changeset/orange-sheep-provide.md new file mode 100644 index 0000000000..e7596afa23 --- /dev/null +++ b/.changeset/orange-sheep-provide.md @@ -0,0 +1,5 @@ +--- +"@layerzerolabs/mint-burn-oft-adapter-example": minor +--- + +Adds a send script and simple config to the MABA example diff --git a/.changeset/tricky-spies-dream.md b/.changeset/tricky-spies-dream.md new file mode 100644 index 0000000000..9bf9fae888 --- /dev/null +++ b/.changeset/tricky-spies-dream.md @@ -0,0 +1,5 @@ +--- +"@layerzerolabs/oft-adapter-example": minor +--- + +Adds send task to oft-adapter diff --git a/examples/mint-burn-oft-adapter/.eslintrc.js b/examples/mint-burn-oft-adapter/.eslintrc.js index 8ef2c6dbe9..30bd543101 100644 --- a/examples/mint-burn-oft-adapter/.eslintrc.js +++ b/examples/mint-burn-oft-adapter/.eslintrc.js @@ -7,5 +7,6 @@ module.exports = { // @layerzerolabs/eslint-config-next defines rules for turborepo-based projects // that are not relevant for this particular project 'turbo/no-undeclared-env-vars': 'off', + 'import/no-unresolved': 'warn', // Updated to warn to avoid erratic popping up of this particular error for '@layerzerolabs/metadata-tools' even when the package has been installed correctly }, }; diff --git a/examples/mint-burn-oft-adapter/deploy/MintBurnERC20Mock.ts b/examples/mint-burn-oft-adapter/deploy/MintBurnERC20Mock.ts new file mode 100644 index 0000000000..1a05f2b9b1 --- /dev/null +++ b/examples/mint-burn-oft-adapter/deploy/MintBurnERC20Mock.ts @@ -0,0 +1,49 @@ +import assert from 'assert' + +import { type DeployFunction } from 'hardhat-deploy/types' + +const contractName = 'MintBurnERC20Mock' + +const deploy: DeployFunction = async (hre) => { + const { getNamedAccounts, deployments } = hre + + const { deploy } = deployments + const { deployer } = await getNamedAccounts() + + assert(deployer, 'Missing named deployer account') + + console.log(`Network: ${hre.network.name}`) + console.log(`Deployer: ${deployer}`) + + // Token configuration + const tokenName = 'Mock Mint Burn Token' + const tokenSymbol = 'MMBT' + const initialMintAmount = hre.ethers.utils.parseEther('1000000') // 1M tokens + + const { address } = await deploy(contractName, { + from: deployer, + args: [ + tokenName, // Token name + tokenSymbol, // Token symbol + ], + log: true, + skipIfAlreadyDeployed: false, + }) + + console.log(`Deployed contract: ${contractName}, network: ${hre.network.name}, address: ${address}`) + console.log(`Token: ${tokenName} (${tokenSymbol})`) + + // Mint initial tokens to the deployer + const [signer] = await hre.ethers.getSigners() + const mintBurnToken = await hre.ethers.getContractAt(contractName, address, signer) + + const mintTx = await mintBurnToken.mint(deployer, initialMintAmount) + await mintTx.wait() + + const balance = await mintBurnToken.balanceOf(deployer) + console.log(`Minted ${hre.ethers.utils.formatEther(balance)} ${tokenSymbol} tokens to deployer: ${deployer}`) +} + +deploy.tags = [contractName] + +export default deploy diff --git a/examples/mint-burn-oft-adapter/hardhat.config.ts b/examples/mint-burn-oft-adapter/hardhat.config.ts index b1fbe86f84..095fc886c8 100644 --- a/examples/mint-burn-oft-adapter/hardhat.config.ts +++ b/examples/mint-burn-oft-adapter/hardhat.config.ts @@ -15,6 +15,8 @@ import { EndpointId } from '@layerzerolabs/lz-definitions' import './type-extensions' +import './tasks/sendOFT' + // Set your preferred authentication method // // If you prefer using a mnemonic, set a MNEMONIC environment variable @@ -54,9 +56,9 @@ const config: HardhatUserConfig = { ], }, networks: { - 'sepolia-testnet': { - eid: EndpointId.SEPOLIA_V2_TESTNET, - url: process.env.RPC_URL_SEPOLIA || 'https://rpc.sepolia.org/', + 'optimism-testnet': { + eid: EndpointId.OPTSEP_V2_TESTNET, + url: process.env.RPC_URL_OP_SEPOLIA || 'https://optimism-sepolia.gateway.tenderly.co', accounts, oftAdapter: { tokenAddress: '0x0', // Set the token address for the OFT adapter @@ -66,11 +68,17 @@ const config: HardhatUserConfig = { eid: EndpointId.AVALANCHE_V2_TESTNET, url: process.env.RPC_URL_FUJI || 'https://rpc.ankr.com/avalanche_fuji', accounts, + oftAdapter: { + tokenAddress: '0x0', // Set the token address for the OFT adapter + }, }, - 'amoy-testnet': { - eid: EndpointId.AMOY_V2_TESTNET, - url: process.env.RPC_URL_AMOY || 'https://polygon-amoy-bor-rpc.publicnode.com', + 'arbitrum-testnet': { + eid: EndpointId.ARBSEP_V2_TESTNET, + url: process.env.RPC_URL_ARB_SEPOLIA || 'https://arbitrum-sepolia.gateway.tenderly.co', accounts, + oftAdapter: { + tokenAddress: '0x0', // Set the token address for the OFT adapter + }, }, hardhat: { // Need this for testing because TestHelperOz5.sol is exceeding the compiled contract size limit diff --git a/examples/mint-burn-oft-adapter/layerzero.config.ts b/examples/mint-burn-oft-adapter/layerzero.config.ts index af4efe4cd0..c0d54ff8ac 100644 --- a/examples/mint-burn-oft-adapter/layerzero.config.ts +++ b/examples/mint-burn-oft-adapter/layerzero.config.ts @@ -1,75 +1,73 @@ import { EndpointId } from '@layerzerolabs/lz-definitions' +import { ExecutorOptionType } from '@layerzerolabs/lz-v2-utilities' +import { TwoWayConfig, generateConnectionsConfig } from '@layerzerolabs/metadata-tools' +import { OAppEnforcedOption } from '@layerzerolabs/toolbox-hardhat' -import type { OAppOmniGraphHardhat, OmniPointHardhat } from '@layerzerolabs/toolbox-hardhat' +import type { OmniPointHardhat } from '@layerzerolabs/toolbox-hardhat' -/** - * WARNING: ONLY 1 OFTAdapter should exist for a given global mesh. - * The token address for the adapter should be defined in hardhat.config. This will be used in deployment. - * - * for example: - * - * sepolia: { - * eid: EndpointId.SEPOLIA_V2_TESTNET, - * url: process.env.RPC_URL_SEPOLIA || 'https://rpc.sepolia.org/', - * accounts, - * oftAdapter: { - * tokenAddress: '0x0', // Set the token address for the OFT adapter - * }, - * }, - */ -const sepoliaContract: OmniPointHardhat = { - eid: EndpointId.SEPOLIA_V2_TESTNET, +const optimismContract: OmniPointHardhat = { + eid: EndpointId.OPTSEP_V2_TESTNET, contractName: 'MyMintBurnOFTAdapter', } -const fujiContract: OmniPointHardhat = { +const avalancheContract: OmniPointHardhat = { eid: EndpointId.AVALANCHE_V2_TESTNET, contractName: 'MyOFT', } -const amoyContract: OmniPointHardhat = { - eid: EndpointId.AMOY_V2_TESTNET, +const arbitrumContract: OmniPointHardhat = { + eid: EndpointId.ARBSEP_V2_TESTNET, contractName: 'MyOFT', } -const config: OAppOmniGraphHardhat = { - contracts: [ - { - contract: fujiContract, - }, - { - contract: sepoliaContract, - }, - { - contract: amoyContract, - }, +// To connect all the above chains to each other, we need the following pathways: +// Optimism <-> Avalanche +// Optimism <-> Arbitrum +// Avalanche <-> Arbitrum + +// For this example's simplicity, we will use the same enforced options values for sending to all chains +// For production, you should ensure `gas` is set to the correct value through profiling the gas usage of calling OFT._lzReceive(...) on the destination chain +// To learn more, read https://docs.layerzero.network/v2/concepts/applications/oapp-standard#execution-options-and-enforced-settings +const EVM_ENFORCED_OPTIONS: OAppEnforcedOption[] = [ + { + msgType: 1, + optionType: ExecutorOptionType.LZ_RECEIVE, + gas: 80000, + value: 0, + }, +] + +// With the config generator, pathways declared are automatically bidirectional +// i.e. if you declare A,B there's no need to declare B,A +const pathways: TwoWayConfig[] = [ + [ + optimismContract, // Chain A contract + avalancheContract, // Chain B contract + [['LayerZero Labs'], []], // [ requiredDVN[], [ optionalDVN[], threshold ] ] + [1, 1], // [A to B confirmations, B to A confirmations] + [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], // Chain B enforcedOptions, Chain A enforcedOptions ], - connections: [ - { - from: fujiContract, - to: sepoliaContract, - }, - { - from: fujiContract, - to: amoyContract, - }, - { - from: sepoliaContract, - to: fujiContract, - }, - { - from: sepoliaContract, - to: amoyContract, - }, - { - from: amoyContract, - to: sepoliaContract, - }, - { - from: amoyContract, - to: fujiContract, - }, + [ + optimismContract, // Chain A contract + arbitrumContract, // Chain C contract + [['LayerZero Labs'], []], // [ requiredDVN[], [ optionalDVN[], threshold ] ] + [1, 1], // [A to B confirmations, B to A confirmations] + [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], // Chain C enforcedOptions, Chain A enforcedOptions ], -} + [ + avalancheContract, // Chain B contract + arbitrumContract, // Chain C contract + [['LayerZero Labs'], []], // [ requiredDVN[], [ optionalDVN[], threshold ] ] + [1, 1], // [A to B confirmations, B to A confirmations] + [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], // Chain C enforcedOptions, Chain B enforcedOptions + ], +] -export default config +export default async function () { + // Generate the connections config based on the pathways + const connections = await generateConnectionsConfig(pathways) + return { + contracts: [{ contract: optimismContract }, { contract: avalancheContract }, { contract: arbitrumContract }], + connections, + } +} diff --git a/examples/mint-burn-oft-adapter/package.json b/examples/mint-burn-oft-adapter/package.json index 6891bc23c7..b35136e47e 100644 --- a/examples/mint-burn-oft-adapter/package.json +++ b/examples/mint-burn-oft-adapter/package.json @@ -22,12 +22,15 @@ }, "devDependencies": { "@babel/core": "^7.23.9", + "@layerzerolabs/devtools-evm-hardhat": "^3.1.0", "@layerzerolabs/eslint-config-next": "~2.3.39", + "@layerzerolabs/io-devtools": "~0.2.0", "@layerzerolabs/lz-definitions": "^3.0.75", "@layerzerolabs/lz-evm-messagelib-v2": "^3.0.75", "@layerzerolabs/lz-evm-protocol-v2": "^3.0.75", "@layerzerolabs/lz-evm-v1-0.7": "^3.0.75", "@layerzerolabs/lz-v2-utilities": "^3.0.75", + "@layerzerolabs/metadata-tools": "^2.0.0", "@layerzerolabs/oapp-evm": "^0.3.2", "@layerzerolabs/oft-evm": "^3.1.3", "@layerzerolabs/prettier-config-next": "^2.3.39", diff --git a/examples/mint-burn-oft-adapter/tasks/sendEvm.ts b/examples/mint-burn-oft-adapter/tasks/sendEvm.ts new file mode 100644 index 0000000000..7a9cf98959 --- /dev/null +++ b/examples/mint-burn-oft-adapter/tasks/sendEvm.ts @@ -0,0 +1,242 @@ +import path from 'path' + +import { BigNumber, ContractTransaction } from 'ethers' +import { parseUnits } from 'ethers/lib/utils' +import { HardhatRuntimeEnvironment } from 'hardhat/types' + +import { OmniPointHardhat, createGetHreByEid } from '@layerzerolabs/devtools-evm-hardhat' +import { createLogger } from '@layerzerolabs/io-devtools' +import { ChainType, endpointIdToChainType, endpointIdToNetwork } from '@layerzerolabs/lz-definitions' +import { Options, addressToBytes32 } from '@layerzerolabs/lz-v2-utilities' + +import { SendResult } from './types' +import { DebugLogger, KnownErrors, getLayerZeroScanLink } from './utils' + +const logger = createLogger() + +export interface EvmArgs { + srcEid: number + dstEid: number + amount: string + to: string + oappConfig: string + minAmount?: string + extraLzReceiveOptions?: string[] + extraLzComposeOptions?: string[] + extraNativeDropOptions?: string[] + composeMsg?: string + oftAddress?: string +} + +export async function sendEvm( + { + srcEid, + dstEid, + amount, + to, + oappConfig, + minAmount, + extraLzReceiveOptions, + extraLzComposeOptions, + extraNativeDropOptions, + composeMsg, + oftAddress, + }: EvmArgs, + hre: HardhatRuntimeEnvironment +): Promise { + if (endpointIdToChainType(srcEid) !== ChainType.EVM) { + throw new Error(`non-EVM srcEid (${srcEid}) not supported here`) + } + + const getHreByEid = createGetHreByEid(hre) + let srcEidHre: HardhatRuntimeEnvironment + try { + srcEidHre = await getHreByEid(srcEid) + } catch (error) { + DebugLogger.printErrorAndFixSuggestion( + KnownErrors.ERROR_GETTING_HRE, + `For network: ${endpointIdToNetwork(srcEid)}, OFT: ${oftAddress}` + ) + throw error + } + const signer = (await srcEidHre.ethers.getSigners())[0] + + // 1️⃣ resolve the OFT wrapper address + let wrapperAddress: string + if (oftAddress) { + wrapperAddress = oftAddress + } else { + const layerZeroConfig = (await import(path.resolve('./', oappConfig))).default + const { contracts } = typeof layerZeroConfig === 'function' ? await layerZeroConfig() : layerZeroConfig + const wrapper = contracts.find((c: { contract: OmniPointHardhat }) => c.contract.eid === srcEid) + if (!wrapper) throw new Error(`No config for EID ${srcEid}`) + wrapperAddress = wrapper.contract.contractName + ? (await srcEidHre.deployments.get(wrapper.contract.contractName)).address + : wrapper.contract.address || '' + } + + // 2️⃣ load IOFT ABI, extend it with token() + const ioftArtifact = await srcEidHre.artifacts.readArtifact('IOFT') + + // now attach + const oft = await srcEidHre.ethers.getContractAt(ioftArtifact.abi, wrapperAddress, signer) + + // 3️⃣ fetch the underlying ERC-20 + const underlying = await oft.token() + + // 4️⃣ fetch decimals from the underlying token + const erc20 = await srcEidHre.ethers.getContractAt('ERC20', underlying, signer) + const decimals: number = await erc20.decimals() + + // 5️⃣ normalize the user-supplied amount + const amountUnits: BigNumber = parseUnits(amount, decimals) + + // 6️⃣ Check if approval is required (for OFT Adapters) and handle approval + try { + const approvalRequired = await oft.approvalRequired() + if (approvalRequired) { + logger.info('OFT Adapter detected - checking ERC20 allowance...') + + // Check current allowance + const currentAllowance = await erc20.allowance(signer.address, wrapperAddress) + logger.info(`Current allowance: ${currentAllowance.toString()}`) + logger.info(`Required amount: ${amountUnits.toString()}`) + + if (currentAllowance.lt(amountUnits)) { + logger.info('Insufficient allowance - approving ERC20 tokens...') + const approveTx = await erc20.approve(wrapperAddress, amountUnits) + logger.info(`Approval transaction hash: ${approveTx.hash}`) + await approveTx.wait() + logger.info('ERC20 approval confirmed') + } else { + logger.info('Sufficient allowance already exists') + } + } + } catch (error) { + // If approvalRequired() doesn't exist or fails, assume it's a regular OFT (not an adapter) + logger.info('No approval required (regular OFT detected)') + } + + // 7️⃣ hex string → Uint8Array → zero-pad to 32 bytes + const toBytes = addressToBytes32(to) + + // 8️⃣ Build options dynamically using Options.newOptions() + let options = Options.newOptions() + + // Add lzReceive options + if (extraLzReceiveOptions && extraLzReceiveOptions.length > 0) { + // Handle case where Hardhat's CSV parsing splits "gas,value" into separate elements + if (extraLzReceiveOptions.length % 2 !== 0) { + throw new Error( + `Invalid lzReceive options: received ${extraLzReceiveOptions.length} values, but expected pairs of gas,value` + ) + } + + for (let i = 0; i < extraLzReceiveOptions.length; i += 2) { + const gas = Number(extraLzReceiveOptions[i]) + const value = Number(extraLzReceiveOptions[i + 1]) || 0 + options = options.addExecutorLzReceiveOption(gas, value) + logger.info(`Added lzReceive option: ${gas} gas, ${value} value`) + } + } + + // Add lzCompose options + if (extraLzComposeOptions && extraLzComposeOptions.length > 0) { + // Handle case where Hardhat's CSV parsing splits "index,gas,value" into separate elements + if (extraLzComposeOptions.length % 3 !== 0) { + throw new Error( + `Invalid lzCompose options: received ${extraLzComposeOptions.length} values, but expected triplets of index,gas,value` + ) + } + + for (let i = 0; i < extraLzComposeOptions.length; i += 3) { + const index = Number(extraLzComposeOptions[i]) + const gas = Number(extraLzComposeOptions[i + 1]) + const value = Number(extraLzComposeOptions[i + 2]) || 0 + options = options.addExecutorComposeOption(index, gas, value) + logger.info(`Added lzCompose option: index ${index}, ${gas} gas, ${value} value`) + } + } + + // Add native drop options + if (extraNativeDropOptions && extraNativeDropOptions.length > 0) { + // Handle case where Hardhat's CSV parsing splits "amount,recipient" into separate elements + if (extraNativeDropOptions.length % 2 !== 0) { + throw new Error( + `Invalid native drop options: received ${extraNativeDropOptions.length} values, but expected pairs of amount,recipient` + ) + } + + for (let i = 0; i < extraNativeDropOptions.length; i += 2) { + const amountStr = extraNativeDropOptions[i] + const recipient = extraNativeDropOptions[i + 1] + + if (!amountStr || !recipient) { + throw new Error( + `Invalid native drop option: Both amount and recipient must be provided. Got amount="${amountStr}", recipient="${recipient}"` + ) + } + + try { + options = options.addExecutorNativeDropOption(amountStr.trim(), recipient.trim()) + logger.info(`Added native drop option: ${amountStr.trim()} wei to ${recipient.trim()}`) + } catch (error) { + // Provide helpful context if the amount exceeds protocol limits + const maxUint128 = BigInt('340282366920938463463374607431768211455') // 2^128 - 1 + const maxUint128Ether = Number(maxUint128) / 1e18 // Convert to ETH for readability + + throw new Error( + `Failed to add native drop option with amount ${amountStr.trim()} wei. ` + + `LayerZero protocol constrains native drop amounts to uint128 maximum ` + + `(${maxUint128.toString()} wei ≈ ${maxUint128Ether.toFixed(2)} ETH). ` + + `Original error: ${error instanceof Error ? error.message : String(error)}` + ) + } + } + } + + const extraOptions = options.toHex() + + // 9️⃣ build sendParam and dispatch + const sendParam = { + dstEid, + to: toBytes, + amountLD: amountUnits.toString(), + minAmountLD: minAmount ? parseUnits(minAmount, decimals).toString() : amountUnits.toString(), + extraOptions: extraOptions, + composeMsg: composeMsg ? composeMsg.toString() : '0x', + oftCmd: '0x', + } + + // 10️⃣ Quote (MessagingFee = { nativeFee, lzTokenFee }) + logger.info('Quoting the native gas cost for the send transaction...') + let msgFee: { nativeFee: BigNumber; lzTokenFee: BigNumber } + try { + msgFee = await oft.quoteSend(sendParam, false) + } catch (error) { + DebugLogger.printErrorAndFixSuggestion( + KnownErrors.ERROR_QUOTING_NATIVE_GAS_COST, + `For network: ${endpointIdToNetwork(srcEid)}, OFT: ${oftAddress}` + ) + throw error + } + logger.info('Sending the transaction...') + let tx: ContractTransaction + try { + tx = await oft.send(sendParam, msgFee, signer.address, { + value: msgFee.nativeFee, + }) + } catch (error) { + DebugLogger.printErrorAndFixSuggestion( + KnownErrors.ERROR_SENDING_TRANSACTION, + `For network: ${endpointIdToNetwork(srcEid)}, OFT: ${oftAddress}` + ) + throw error + } + const receipt = await tx.wait() + + const txHash = receipt.transactionHash + const scanLink = getLayerZeroScanLink(txHash, srcEid >= 40_000 && srcEid < 50_000) + + return { txHash, scanLink } +} diff --git a/examples/mint-burn-oft-adapter/tasks/sendOFT.ts b/examples/mint-burn-oft-adapter/tasks/sendOFT.ts new file mode 100644 index 0000000000..ae1f061c1b --- /dev/null +++ b/examples/mint-burn-oft-adapter/tasks/sendOFT.ts @@ -0,0 +1,109 @@ +import { task, types } from 'hardhat/config' +import { HardhatRuntimeEnvironment } from 'hardhat/types' + +import { types as devtoolsTypes } from '@layerzerolabs/devtools-evm-hardhat' +import { ChainType, endpointIdToChainType, endpointIdToNetwork } from '@layerzerolabs/lz-definitions' + +import { EvmArgs, sendEvm } from './sendEvm' +import { SendResult } from './types' +import { DebugLogger, KnownOutputs, KnownWarnings, getBlockExplorerLink } from './utils' + +interface MasterArgs { + srcEid: number + dstEid: number + amount: string + to: string + /** Path to LayerZero config file (default: layerzero.config.ts) */ + oappConfig: string + /** Minimum amount to receive in case of custom slippage or fees (human readable units, e.g. "1.5") */ + minAmount?: string + /** Array of lzReceive options as comma-separated values "gas,value" - e.g. --extra-lz-receive-options "200000,0" */ + extraLzReceiveOptions?: string[] + /** Array of lzCompose options as comma-separated values "index,gas,value" - e.g. --extra-lz-compose-options "0,500000,0" */ + extraLzComposeOptions?: string[] + /** Array of native drop options as comma-separated values "amount,recipient" - e.g. --extra-native-drop-options "1000000000000000000,0x1234..." */ + extraNativeDropOptions?: string[] + /** Arbitrary bytes message to deliver alongside the OFT */ + composeMsg?: string + /** EVM: 20-byte hex address */ + oftAddress?: string +} + +task('lz:oft:send', 'Sends OFT tokens cross‐chain from EVM chains') + .addParam('srcEid', 'Source endpoint ID', undefined, types.int) + .addParam('dstEid', 'Destination endpoint ID', undefined, types.int) + .addParam('amount', 'Amount to send (human readable units, e.g. "1.5")', undefined, types.string) + .addParam('to', 'Recipient address (20-byte hex for EVM)', undefined, types.string) + .addOptionalParam('oappConfig', 'Path to LayerZero config file', 'layerzero.config.ts', types.string) + .addOptionalParam( + 'minAmount', + 'Minimum amount to receive in case of custom slippage or fees (human readable units, e.g. "1.5")', + undefined, + types.string + ) + .addOptionalParam( + 'extraLzReceiveOptions', + 'Array of lzReceive options as comma-separated values "gas,value"', + undefined, + devtoolsTypes.csv + ) + .addOptionalParam( + 'extraLzComposeOptions', + 'Array of lzCompose options as comma-separated values "index,gas,value"', + undefined, + devtoolsTypes.csv + ) + .addOptionalParam( + 'extraNativeDropOptions', + 'Array of native drop options as comma-separated values "amount,recipient"', + undefined, + devtoolsTypes.csv + ) + .addOptionalParam('composeMsg', 'Arbitrary bytes message to deliver alongside the OFT', undefined, types.string) + .addOptionalParam( + 'oftAddress', + 'Override the source local deployment OFT address (20-byte hex for EVM)', + undefined, + types.string + ) + .setAction(async (args: MasterArgs, hre: HardhatRuntimeEnvironment) => { + const chainType = endpointIdToChainType(args.srcEid) + let result: SendResult + + if (args.oftAddress) { + DebugLogger.printWarning( + KnownWarnings.USING_OVERRIDE_OFT, + `For network: ${endpointIdToNetwork(args.srcEid)}, OFT: ${args.oftAddress}` + ) + } + + // Only support EVM chains in this example + if (chainType === ChainType.EVM) { + result = await sendEvm(args as EvmArgs, hre) + } else { + throw new Error( + `The chain type ${chainType} is not supported in this OFT example. Only EVM chains are supported.` + ) + } + + DebugLogger.printLayerZeroOutput( + KnownOutputs.SENT_VIA_OFT, + `Successfully sent ${args.amount} tokens from ${endpointIdToNetwork(args.srcEid)} to ${endpointIdToNetwork(args.dstEid)}` + ) + + // print the explorer link for the srcEid from metadata + const explorerLink = await getBlockExplorerLink(args.srcEid, result.txHash) + // if explorer link is available, print the tx hash link + if (explorerLink) { + DebugLogger.printLayerZeroOutput( + KnownOutputs.TX_HASH, + `Explorer link for source chain ${endpointIdToNetwork(args.srcEid)}: ${explorerLink}` + ) + } + + // print the LayerZero Scan link from metadata + DebugLogger.printLayerZeroOutput( + KnownOutputs.EXPLORER_LINK, + `LayerZero Scan link for tracking all cross-chain transaction details: ${result.scanLink}` + ) + }) diff --git a/examples/mint-burn-oft-adapter/tasks/types.ts b/examples/mint-burn-oft-adapter/tasks/types.ts new file mode 100644 index 0000000000..8d2b681692 --- /dev/null +++ b/examples/mint-burn-oft-adapter/tasks/types.ts @@ -0,0 +1,4 @@ +export interface SendResult { + txHash: string // EVM: receipt.transactionHash + scanLink: string // LayerZeroScan link for cross-chain tracking +} diff --git a/examples/mint-burn-oft-adapter/tasks/utils.ts b/examples/mint-burn-oft-adapter/tasks/utils.ts new file mode 100644 index 0000000000..934c9a046d --- /dev/null +++ b/examples/mint-burn-oft-adapter/tasks/utils.ts @@ -0,0 +1,51 @@ +import { createLogger } from '@layerzerolabs/io-devtools' +import { endpointIdToNetwork } from '@layerzerolabs/lz-definitions' +import { Options } from '@layerzerolabs/lz-v2-utilities' + +const logger = createLogger() + +export const deploymentMetadataUrl = 'https://metadata.layerzero-api.com/v1/metadata/deployments' + +/** + * Given a srcEid and on-chain tx hash, return + * `https://…blockExplorers[0].url/tx/`, or undefined. + */ +export async function getBlockExplorerLink(srcEid: number, txHash: string): Promise { + const network = endpointIdToNetwork(srcEid) // e.g. "ethereum-mainnet" + const res = await fetch(deploymentMetadataUrl) + if (!res.ok) return + const all = (await res.json()) as Record + const meta = all[network] + const explorer = meta?.blockExplorers?.[0]?.url + if (explorer) { + // many explorers use `/tx/` + return `${explorer.replace(/\/+$/, '')}/tx/${txHash}` + } + return +} + +function formatBigIntForDisplay(n: bigint) { + return n.toLocaleString().replace(/,/g, '_') +} + +export function decodeLzReceiveOptions(hex: string): string { + try { + // Handle empty/undefined values first + if (!hex || hex === '0x') return 'No options set' + const options = Options.fromOptions(hex) + const lzReceiveOpt = options.decodeExecutorLzReceiveOption() + return lzReceiveOpt + ? `gas: ${formatBigIntForDisplay(lzReceiveOpt.gas)} , value: ${formatBigIntForDisplay(lzReceiveOpt.value)} wei` + : 'No executor options' + } catch (e) { + return `Invalid options (${hex.slice(0, 12)}...)` + } +} + +// Get LayerZero scan link +export function getLayerZeroScanLink(txHash: string, isTestnet = false): string { + const baseUrl = isTestnet ? 'https://testnet.layerzeroscan.com' : 'https://layerzeroscan.com' + return `${baseUrl}/tx/${txHash}` +} + +export { DebugLogger, KnownErrors, KnownOutputs, KnownWarnings } from '@layerzerolabs/io-devtools' diff --git a/examples/oft-adapter/.eslintrc.js b/examples/oft-adapter/.eslintrc.js index 8ef2c6dbe9..30bd543101 100644 --- a/examples/oft-adapter/.eslintrc.js +++ b/examples/oft-adapter/.eslintrc.js @@ -7,5 +7,6 @@ module.exports = { // @layerzerolabs/eslint-config-next defines rules for turborepo-based projects // that are not relevant for this particular project 'turbo/no-undeclared-env-vars': 'off', + 'import/no-unresolved': 'warn', // Updated to warn to avoid erratic popping up of this particular error for '@layerzerolabs/metadata-tools' even when the package has been installed correctly }, }; diff --git a/examples/oft-adapter/deploy/MyERC20Mock.ts b/examples/oft-adapter/deploy/MyERC20Mock.ts new file mode 100644 index 0000000000..bbb93605e0 --- /dev/null +++ b/examples/oft-adapter/deploy/MyERC20Mock.ts @@ -0,0 +1,49 @@ +import assert from 'assert' + +import { type DeployFunction } from 'hardhat-deploy/types' + +const contractName = 'MyERC20Mock' + +const deploy: DeployFunction = async (hre) => { + const { getNamedAccounts, deployments } = hre + + const { deploy } = deployments + const { deployer } = await getNamedAccounts() + + assert(deployer, 'Missing named deployer account') + + console.log(`Network: ${hre.network.name}`) + console.log(`Deployer: ${deployer}`) + + // Token configuration + const tokenName = 'My ERC20 Mock' + const tokenSymbol = 'MERC20' + const initialMintAmount = hre.ethers.utils.parseEther('10') // 10 tokens + + const { address } = await deploy(contractName, { + from: deployer, + args: [ + tokenName, // Token name + tokenSymbol, // Token symbol + ], + log: true, + skipIfAlreadyDeployed: false, + }) + + console.log(`Deployed contract: ${contractName}, network: ${hre.network.name}, address: ${address}`) + console.log(`Token: ${tokenName} (${tokenSymbol})`) + + // Mint initial tokens to the deployer + const [signer] = await hre.ethers.getSigners() + const mintBurnToken = await hre.ethers.getContractAt(contractName, address, signer) + + const mintTx = await mintBurnToken.mint(deployer, initialMintAmount) + await mintTx.wait() + + const balance = await mintBurnToken.balanceOf(deployer) + console.log(`Minted ${hre.ethers.utils.formatEther(balance)} ${tokenSymbol} tokens to deployer: ${deployer}`) +} + +deploy.tags = [contractName] + +export default deploy diff --git a/examples/oft-adapter/hardhat.config.ts b/examples/oft-adapter/hardhat.config.ts index b1fbe86f84..13fb2653a9 100644 --- a/examples/oft-adapter/hardhat.config.ts +++ b/examples/oft-adapter/hardhat.config.ts @@ -14,6 +14,7 @@ import { HardhatUserConfig, HttpNetworkAccountsUserConfig } from 'hardhat/types' import { EndpointId } from '@layerzerolabs/lz-definitions' import './type-extensions' +import './tasks/sendOFT' // Set your preferred authentication method // @@ -54,9 +55,9 @@ const config: HardhatUserConfig = { ], }, networks: { - 'sepolia-testnet': { - eid: EndpointId.SEPOLIA_V2_TESTNET, - url: process.env.RPC_URL_SEPOLIA || 'https://rpc.sepolia.org/', + 'optimism-testnet': { + eid: EndpointId.OPTSEP_V2_TESTNET, + url: process.env.RPC_URL_OP_SEPOLIA || 'https://optimism-sepolia.gateway.tenderly.co', accounts, oftAdapter: { tokenAddress: '0x0', // Set the token address for the OFT adapter @@ -64,12 +65,12 @@ const config: HardhatUserConfig = { }, 'avalanche-testnet': { eid: EndpointId.AVALANCHE_V2_TESTNET, - url: process.env.RPC_URL_FUJI || 'https://rpc.ankr.com/avalanche_fuji', + url: process.env.RPC_URL_FUJI || 'https://avalanche-fuji.drpc.org', accounts, }, - 'amoy-testnet': { - eid: EndpointId.AMOY_V2_TESTNET, - url: process.env.RPC_URL_AMOY || 'https://polygon-amoy-bor-rpc.publicnode.com', + 'arbitrum-testnet': { + eid: EndpointId.ARBSEP_V2_TESTNET, + url: process.env.RPC_URL_ARB_SEPOLIA || 'https://arbitrum-sepolia.gateway.tenderly.co', accounts, }, hardhat: { diff --git a/examples/oft-adapter/layerzero.config.ts b/examples/oft-adapter/layerzero.config.ts index 01f77dd4b3..ae5eac4ccc 100644 --- a/examples/oft-adapter/layerzero.config.ts +++ b/examples/oft-adapter/layerzero.config.ts @@ -1,6 +1,9 @@ import { EndpointId } from '@layerzerolabs/lz-definitions' +import { ExecutorOptionType } from '@layerzerolabs/lz-v2-utilities' +import { TwoWayConfig, generateConnectionsConfig } from '@layerzerolabs/metadata-tools' +import { OAppEnforcedOption } from '@layerzerolabs/toolbox-hardhat' -import type { OAppOmniGraphHardhat, OmniPointHardhat } from '@layerzerolabs/toolbox-hardhat' +import type { OmniPointHardhat } from '@layerzerolabs/toolbox-hardhat' /** * WARNING: ONLY 1 OFTAdapter should exist for a given global mesh. @@ -17,59 +20,69 @@ import type { OAppOmniGraphHardhat, OmniPointHardhat } from '@layerzerolabs/tool * }, * }, */ -const sepoliaContract: OmniPointHardhat = { - eid: EndpointId.SEPOLIA_V2_TESTNET, +const optimismContract: OmniPointHardhat = { + eid: EndpointId.OPTSEP_V2_TESTNET, contractName: 'MyOFTAdapter', } -const fujiContract: OmniPointHardhat = { +const avalancheContract: OmniPointHardhat = { eid: EndpointId.AVALANCHE_V2_TESTNET, contractName: 'MyOFT', } -const amoyContract: OmniPointHardhat = { - eid: EndpointId.AMOY_V2_TESTNET, +const arbitrumContract: OmniPointHardhat = { + eid: EndpointId.ARBSEP_V2_TESTNET, contractName: 'MyOFT', } -const config: OAppOmniGraphHardhat = { - contracts: [ - { - contract: fujiContract, - }, - { - contract: sepoliaContract, - }, - { - contract: amoyContract, - }, +// To connect all the above chains to each other, we need the following pathways: +// Optimism <-> Avalanche +// Optimism <-> Arbitrum +// Avalanche <-> Arbitrum + +// For this example's simplicity, we will use the same enforced options values for sending to all chains +// For production, you should ensure `gas` is set to the correct value through profiling the gas usage of calling OFT._lzReceive(...) on the destination chain +// To learn more, read https://docs.layerzero.network/v2/concepts/applications/oapp-standard#execution-options-and-enforced-settings +const EVM_ENFORCED_OPTIONS: OAppEnforcedOption[] = [ + { + msgType: 1, + optionType: ExecutorOptionType.LZ_RECEIVE, + gas: 80000, + value: 0, + }, +] + +// With the config generator, pathways declared are automatically bidirectional +// i.e. if you declare A,B there's no need to declare B,A +const pathways: TwoWayConfig[] = [ + [ + optimismContract, // Chain A contract + avalancheContract, // Chain B contract + [['LayerZero Labs'], []], // [ requiredDVN[], [ optionalDVN[], threshold ] ] + [1, 1], // [A to B confirmations, B to A confirmations] + [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], // Chain B enforcedOptions, Chain A enforcedOptions ], - connections: [ - { - from: fujiContract, - to: sepoliaContract, - }, - { - from: fujiContract, - to: amoyContract, - }, - { - from: sepoliaContract, - to: fujiContract, - }, - { - from: sepoliaContract, - to: amoyContract, - }, - { - from: amoyContract, - to: sepoliaContract, - }, - { - from: amoyContract, - to: fujiContract, - }, + [ + optimismContract, // Chain A contract + arbitrumContract, // Chain C contract + [['LayerZero Labs'], []], // [ requiredDVN[], [ optionalDVN[], threshold ] ] + [1, 1], // [A to B confirmations, B to A confirmations] + [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], // Chain C enforcedOptions, Chain A enforcedOptions ], -} + [ + avalancheContract, // Chain B contract + arbitrumContract, // Chain C contract + [['LayerZero Labs'], []], // [ requiredDVN[], [ optionalDVN[], threshold ] ] + [1, 1], // [A to B confirmations, B to A confirmations] + [EVM_ENFORCED_OPTIONS, EVM_ENFORCED_OPTIONS], // Chain C enforcedOptions, Chain B enforcedOptions + ], +] -export default config +export default async function () { + // Generate the connections config based on the pathways + const connections = await generateConnectionsConfig(pathways) + return { + contracts: [{ contract: optimismContract }, { contract: avalancheContract }, { contract: arbitrumContract }], + connections, + } +} diff --git a/examples/oft-adapter/package.json b/examples/oft-adapter/package.json index f9f61750f0..6ef0ae2a9b 100644 --- a/examples/oft-adapter/package.json +++ b/examples/oft-adapter/package.json @@ -22,12 +22,15 @@ }, "devDependencies": { "@babel/core": "^7.23.9", + "@layerzerolabs/devtools-evm-hardhat": "^3.1.0", "@layerzerolabs/eslint-config-next": "~2.3.39", + "@layerzerolabs/io-devtools": "~0.2.0", "@layerzerolabs/lz-definitions": "^3.0.75", "@layerzerolabs/lz-evm-messagelib-v2": "^3.0.75", "@layerzerolabs/lz-evm-protocol-v2": "^3.0.75", "@layerzerolabs/lz-evm-v1-0.7": "^3.0.75", "@layerzerolabs/lz-v2-utilities": "^3.0.75", + "@layerzerolabs/metadata-tools": "^2.0.0", "@layerzerolabs/oapp-evm": "^0.3.2", "@layerzerolabs/oft-evm": "^3.1.3", "@layerzerolabs/prettier-config-next": "^2.3.39", diff --git a/examples/oft-adapter/tasks/sendEvm.ts b/examples/oft-adapter/tasks/sendEvm.ts new file mode 100644 index 0000000000..7a9cf98959 --- /dev/null +++ b/examples/oft-adapter/tasks/sendEvm.ts @@ -0,0 +1,242 @@ +import path from 'path' + +import { BigNumber, ContractTransaction } from 'ethers' +import { parseUnits } from 'ethers/lib/utils' +import { HardhatRuntimeEnvironment } from 'hardhat/types' + +import { OmniPointHardhat, createGetHreByEid } from '@layerzerolabs/devtools-evm-hardhat' +import { createLogger } from '@layerzerolabs/io-devtools' +import { ChainType, endpointIdToChainType, endpointIdToNetwork } from '@layerzerolabs/lz-definitions' +import { Options, addressToBytes32 } from '@layerzerolabs/lz-v2-utilities' + +import { SendResult } from './types' +import { DebugLogger, KnownErrors, getLayerZeroScanLink } from './utils' + +const logger = createLogger() + +export interface EvmArgs { + srcEid: number + dstEid: number + amount: string + to: string + oappConfig: string + minAmount?: string + extraLzReceiveOptions?: string[] + extraLzComposeOptions?: string[] + extraNativeDropOptions?: string[] + composeMsg?: string + oftAddress?: string +} + +export async function sendEvm( + { + srcEid, + dstEid, + amount, + to, + oappConfig, + minAmount, + extraLzReceiveOptions, + extraLzComposeOptions, + extraNativeDropOptions, + composeMsg, + oftAddress, + }: EvmArgs, + hre: HardhatRuntimeEnvironment +): Promise { + if (endpointIdToChainType(srcEid) !== ChainType.EVM) { + throw new Error(`non-EVM srcEid (${srcEid}) not supported here`) + } + + const getHreByEid = createGetHreByEid(hre) + let srcEidHre: HardhatRuntimeEnvironment + try { + srcEidHre = await getHreByEid(srcEid) + } catch (error) { + DebugLogger.printErrorAndFixSuggestion( + KnownErrors.ERROR_GETTING_HRE, + `For network: ${endpointIdToNetwork(srcEid)}, OFT: ${oftAddress}` + ) + throw error + } + const signer = (await srcEidHre.ethers.getSigners())[0] + + // 1️⃣ resolve the OFT wrapper address + let wrapperAddress: string + if (oftAddress) { + wrapperAddress = oftAddress + } else { + const layerZeroConfig = (await import(path.resolve('./', oappConfig))).default + const { contracts } = typeof layerZeroConfig === 'function' ? await layerZeroConfig() : layerZeroConfig + const wrapper = contracts.find((c: { contract: OmniPointHardhat }) => c.contract.eid === srcEid) + if (!wrapper) throw new Error(`No config for EID ${srcEid}`) + wrapperAddress = wrapper.contract.contractName + ? (await srcEidHre.deployments.get(wrapper.contract.contractName)).address + : wrapper.contract.address || '' + } + + // 2️⃣ load IOFT ABI, extend it with token() + const ioftArtifact = await srcEidHre.artifacts.readArtifact('IOFT') + + // now attach + const oft = await srcEidHre.ethers.getContractAt(ioftArtifact.abi, wrapperAddress, signer) + + // 3️⃣ fetch the underlying ERC-20 + const underlying = await oft.token() + + // 4️⃣ fetch decimals from the underlying token + const erc20 = await srcEidHre.ethers.getContractAt('ERC20', underlying, signer) + const decimals: number = await erc20.decimals() + + // 5️⃣ normalize the user-supplied amount + const amountUnits: BigNumber = parseUnits(amount, decimals) + + // 6️⃣ Check if approval is required (for OFT Adapters) and handle approval + try { + const approvalRequired = await oft.approvalRequired() + if (approvalRequired) { + logger.info('OFT Adapter detected - checking ERC20 allowance...') + + // Check current allowance + const currentAllowance = await erc20.allowance(signer.address, wrapperAddress) + logger.info(`Current allowance: ${currentAllowance.toString()}`) + logger.info(`Required amount: ${amountUnits.toString()}`) + + if (currentAllowance.lt(amountUnits)) { + logger.info('Insufficient allowance - approving ERC20 tokens...') + const approveTx = await erc20.approve(wrapperAddress, amountUnits) + logger.info(`Approval transaction hash: ${approveTx.hash}`) + await approveTx.wait() + logger.info('ERC20 approval confirmed') + } else { + logger.info('Sufficient allowance already exists') + } + } + } catch (error) { + // If approvalRequired() doesn't exist or fails, assume it's a regular OFT (not an adapter) + logger.info('No approval required (regular OFT detected)') + } + + // 7️⃣ hex string → Uint8Array → zero-pad to 32 bytes + const toBytes = addressToBytes32(to) + + // 8️⃣ Build options dynamically using Options.newOptions() + let options = Options.newOptions() + + // Add lzReceive options + if (extraLzReceiveOptions && extraLzReceiveOptions.length > 0) { + // Handle case where Hardhat's CSV parsing splits "gas,value" into separate elements + if (extraLzReceiveOptions.length % 2 !== 0) { + throw new Error( + `Invalid lzReceive options: received ${extraLzReceiveOptions.length} values, but expected pairs of gas,value` + ) + } + + for (let i = 0; i < extraLzReceiveOptions.length; i += 2) { + const gas = Number(extraLzReceiveOptions[i]) + const value = Number(extraLzReceiveOptions[i + 1]) || 0 + options = options.addExecutorLzReceiveOption(gas, value) + logger.info(`Added lzReceive option: ${gas} gas, ${value} value`) + } + } + + // Add lzCompose options + if (extraLzComposeOptions && extraLzComposeOptions.length > 0) { + // Handle case where Hardhat's CSV parsing splits "index,gas,value" into separate elements + if (extraLzComposeOptions.length % 3 !== 0) { + throw new Error( + `Invalid lzCompose options: received ${extraLzComposeOptions.length} values, but expected triplets of index,gas,value` + ) + } + + for (let i = 0; i < extraLzComposeOptions.length; i += 3) { + const index = Number(extraLzComposeOptions[i]) + const gas = Number(extraLzComposeOptions[i + 1]) + const value = Number(extraLzComposeOptions[i + 2]) || 0 + options = options.addExecutorComposeOption(index, gas, value) + logger.info(`Added lzCompose option: index ${index}, ${gas} gas, ${value} value`) + } + } + + // Add native drop options + if (extraNativeDropOptions && extraNativeDropOptions.length > 0) { + // Handle case where Hardhat's CSV parsing splits "amount,recipient" into separate elements + if (extraNativeDropOptions.length % 2 !== 0) { + throw new Error( + `Invalid native drop options: received ${extraNativeDropOptions.length} values, but expected pairs of amount,recipient` + ) + } + + for (let i = 0; i < extraNativeDropOptions.length; i += 2) { + const amountStr = extraNativeDropOptions[i] + const recipient = extraNativeDropOptions[i + 1] + + if (!amountStr || !recipient) { + throw new Error( + `Invalid native drop option: Both amount and recipient must be provided. Got amount="${amountStr}", recipient="${recipient}"` + ) + } + + try { + options = options.addExecutorNativeDropOption(amountStr.trim(), recipient.trim()) + logger.info(`Added native drop option: ${amountStr.trim()} wei to ${recipient.trim()}`) + } catch (error) { + // Provide helpful context if the amount exceeds protocol limits + const maxUint128 = BigInt('340282366920938463463374607431768211455') // 2^128 - 1 + const maxUint128Ether = Number(maxUint128) / 1e18 // Convert to ETH for readability + + throw new Error( + `Failed to add native drop option with amount ${amountStr.trim()} wei. ` + + `LayerZero protocol constrains native drop amounts to uint128 maximum ` + + `(${maxUint128.toString()} wei ≈ ${maxUint128Ether.toFixed(2)} ETH). ` + + `Original error: ${error instanceof Error ? error.message : String(error)}` + ) + } + } + } + + const extraOptions = options.toHex() + + // 9️⃣ build sendParam and dispatch + const sendParam = { + dstEid, + to: toBytes, + amountLD: amountUnits.toString(), + minAmountLD: minAmount ? parseUnits(minAmount, decimals).toString() : amountUnits.toString(), + extraOptions: extraOptions, + composeMsg: composeMsg ? composeMsg.toString() : '0x', + oftCmd: '0x', + } + + // 10️⃣ Quote (MessagingFee = { nativeFee, lzTokenFee }) + logger.info('Quoting the native gas cost for the send transaction...') + let msgFee: { nativeFee: BigNumber; lzTokenFee: BigNumber } + try { + msgFee = await oft.quoteSend(sendParam, false) + } catch (error) { + DebugLogger.printErrorAndFixSuggestion( + KnownErrors.ERROR_QUOTING_NATIVE_GAS_COST, + `For network: ${endpointIdToNetwork(srcEid)}, OFT: ${oftAddress}` + ) + throw error + } + logger.info('Sending the transaction...') + let tx: ContractTransaction + try { + tx = await oft.send(sendParam, msgFee, signer.address, { + value: msgFee.nativeFee, + }) + } catch (error) { + DebugLogger.printErrorAndFixSuggestion( + KnownErrors.ERROR_SENDING_TRANSACTION, + `For network: ${endpointIdToNetwork(srcEid)}, OFT: ${oftAddress}` + ) + throw error + } + const receipt = await tx.wait() + + const txHash = receipt.transactionHash + const scanLink = getLayerZeroScanLink(txHash, srcEid >= 40_000 && srcEid < 50_000) + + return { txHash, scanLink } +} diff --git a/examples/oft-adapter/tasks/sendOFT.ts b/examples/oft-adapter/tasks/sendOFT.ts new file mode 100644 index 0000000000..ae1f061c1b --- /dev/null +++ b/examples/oft-adapter/tasks/sendOFT.ts @@ -0,0 +1,109 @@ +import { task, types } from 'hardhat/config' +import { HardhatRuntimeEnvironment } from 'hardhat/types' + +import { types as devtoolsTypes } from '@layerzerolabs/devtools-evm-hardhat' +import { ChainType, endpointIdToChainType, endpointIdToNetwork } from '@layerzerolabs/lz-definitions' + +import { EvmArgs, sendEvm } from './sendEvm' +import { SendResult } from './types' +import { DebugLogger, KnownOutputs, KnownWarnings, getBlockExplorerLink } from './utils' + +interface MasterArgs { + srcEid: number + dstEid: number + amount: string + to: string + /** Path to LayerZero config file (default: layerzero.config.ts) */ + oappConfig: string + /** Minimum amount to receive in case of custom slippage or fees (human readable units, e.g. "1.5") */ + minAmount?: string + /** Array of lzReceive options as comma-separated values "gas,value" - e.g. --extra-lz-receive-options "200000,0" */ + extraLzReceiveOptions?: string[] + /** Array of lzCompose options as comma-separated values "index,gas,value" - e.g. --extra-lz-compose-options "0,500000,0" */ + extraLzComposeOptions?: string[] + /** Array of native drop options as comma-separated values "amount,recipient" - e.g. --extra-native-drop-options "1000000000000000000,0x1234..." */ + extraNativeDropOptions?: string[] + /** Arbitrary bytes message to deliver alongside the OFT */ + composeMsg?: string + /** EVM: 20-byte hex address */ + oftAddress?: string +} + +task('lz:oft:send', 'Sends OFT tokens cross‐chain from EVM chains') + .addParam('srcEid', 'Source endpoint ID', undefined, types.int) + .addParam('dstEid', 'Destination endpoint ID', undefined, types.int) + .addParam('amount', 'Amount to send (human readable units, e.g. "1.5")', undefined, types.string) + .addParam('to', 'Recipient address (20-byte hex for EVM)', undefined, types.string) + .addOptionalParam('oappConfig', 'Path to LayerZero config file', 'layerzero.config.ts', types.string) + .addOptionalParam( + 'minAmount', + 'Minimum amount to receive in case of custom slippage or fees (human readable units, e.g. "1.5")', + undefined, + types.string + ) + .addOptionalParam( + 'extraLzReceiveOptions', + 'Array of lzReceive options as comma-separated values "gas,value"', + undefined, + devtoolsTypes.csv + ) + .addOptionalParam( + 'extraLzComposeOptions', + 'Array of lzCompose options as comma-separated values "index,gas,value"', + undefined, + devtoolsTypes.csv + ) + .addOptionalParam( + 'extraNativeDropOptions', + 'Array of native drop options as comma-separated values "amount,recipient"', + undefined, + devtoolsTypes.csv + ) + .addOptionalParam('composeMsg', 'Arbitrary bytes message to deliver alongside the OFT', undefined, types.string) + .addOptionalParam( + 'oftAddress', + 'Override the source local deployment OFT address (20-byte hex for EVM)', + undefined, + types.string + ) + .setAction(async (args: MasterArgs, hre: HardhatRuntimeEnvironment) => { + const chainType = endpointIdToChainType(args.srcEid) + let result: SendResult + + if (args.oftAddress) { + DebugLogger.printWarning( + KnownWarnings.USING_OVERRIDE_OFT, + `For network: ${endpointIdToNetwork(args.srcEid)}, OFT: ${args.oftAddress}` + ) + } + + // Only support EVM chains in this example + if (chainType === ChainType.EVM) { + result = await sendEvm(args as EvmArgs, hre) + } else { + throw new Error( + `The chain type ${chainType} is not supported in this OFT example. Only EVM chains are supported.` + ) + } + + DebugLogger.printLayerZeroOutput( + KnownOutputs.SENT_VIA_OFT, + `Successfully sent ${args.amount} tokens from ${endpointIdToNetwork(args.srcEid)} to ${endpointIdToNetwork(args.dstEid)}` + ) + + // print the explorer link for the srcEid from metadata + const explorerLink = await getBlockExplorerLink(args.srcEid, result.txHash) + // if explorer link is available, print the tx hash link + if (explorerLink) { + DebugLogger.printLayerZeroOutput( + KnownOutputs.TX_HASH, + `Explorer link for source chain ${endpointIdToNetwork(args.srcEid)}: ${explorerLink}` + ) + } + + // print the LayerZero Scan link from metadata + DebugLogger.printLayerZeroOutput( + KnownOutputs.EXPLORER_LINK, + `LayerZero Scan link for tracking all cross-chain transaction details: ${result.scanLink}` + ) + }) diff --git a/examples/oft-adapter/tasks/types.ts b/examples/oft-adapter/tasks/types.ts new file mode 100644 index 0000000000..8d2b681692 --- /dev/null +++ b/examples/oft-adapter/tasks/types.ts @@ -0,0 +1,4 @@ +export interface SendResult { + txHash: string // EVM: receipt.transactionHash + scanLink: string // LayerZeroScan link for cross-chain tracking +} diff --git a/examples/oft-adapter/tasks/utils.ts b/examples/oft-adapter/tasks/utils.ts new file mode 100644 index 0000000000..934c9a046d --- /dev/null +++ b/examples/oft-adapter/tasks/utils.ts @@ -0,0 +1,51 @@ +import { createLogger } from '@layerzerolabs/io-devtools' +import { endpointIdToNetwork } from '@layerzerolabs/lz-definitions' +import { Options } from '@layerzerolabs/lz-v2-utilities' + +const logger = createLogger() + +export const deploymentMetadataUrl = 'https://metadata.layerzero-api.com/v1/metadata/deployments' + +/** + * Given a srcEid and on-chain tx hash, return + * `https://…blockExplorers[0].url/tx/`, or undefined. + */ +export async function getBlockExplorerLink(srcEid: number, txHash: string): Promise { + const network = endpointIdToNetwork(srcEid) // e.g. "ethereum-mainnet" + const res = await fetch(deploymentMetadataUrl) + if (!res.ok) return + const all = (await res.json()) as Record + const meta = all[network] + const explorer = meta?.blockExplorers?.[0]?.url + if (explorer) { + // many explorers use `/tx/` + return `${explorer.replace(/\/+$/, '')}/tx/${txHash}` + } + return +} + +function formatBigIntForDisplay(n: bigint) { + return n.toLocaleString().replace(/,/g, '_') +} + +export function decodeLzReceiveOptions(hex: string): string { + try { + // Handle empty/undefined values first + if (!hex || hex === '0x') return 'No options set' + const options = Options.fromOptions(hex) + const lzReceiveOpt = options.decodeExecutorLzReceiveOption() + return lzReceiveOpt + ? `gas: ${formatBigIntForDisplay(lzReceiveOpt.gas)} , value: ${formatBigIntForDisplay(lzReceiveOpt.value)} wei` + : 'No executor options' + } catch (e) { + return `Invalid options (${hex.slice(0, 12)}...)` + } +} + +// Get LayerZero scan link +export function getLayerZeroScanLink(txHash: string, isTestnet = false): string { + const baseUrl = isTestnet ? 'https://testnet.layerzeroscan.com' : 'https://layerzeroscan.com' + return `${baseUrl}/tx/${txHash}` +} + +export { DebugLogger, KnownErrors, KnownOutputs, KnownWarnings } from '@layerzerolabs/io-devtools' diff --git a/examples/oft/hardhat.config.ts b/examples/oft/hardhat.config.ts index 45ab695a6e..44e95bde00 100644 --- a/examples/oft/hardhat.config.ts +++ b/examples/oft/hardhat.config.ts @@ -13,6 +13,8 @@ import { HardhatUserConfig, HttpNetworkAccountsUserConfig } from 'hardhat/types' import { EndpointId } from '@layerzerolabs/lz-definitions' +import './tasks/sendOFT' + // Set your preferred authentication method // // If you prefer using a mnemonic, set a MNEMONIC environment variable diff --git a/examples/oft/package.json b/examples/oft/package.json index 607016bed5..3cf56a12af 100644 --- a/examples/oft/package.json +++ b/examples/oft/package.json @@ -25,8 +25,10 @@ }, "devDependencies": { "@babel/core": "^7.23.9", + "@layerzerolabs/devtools": "~1.0.0", "@layerzerolabs/devtools-evm-hardhat": "^3.1.0", "@layerzerolabs/eslint-config-next": "~2.3.39", + "@layerzerolabs/io-devtools": "~0.2.0", "@layerzerolabs/lz-definitions": "^3.0.75", "@layerzerolabs/lz-evm-messagelib-v2": "^3.0.75", "@layerzerolabs/lz-evm-protocol-v2": "^3.0.75", diff --git a/examples/oft/tasks/sendEvm.ts b/examples/oft/tasks/sendEvm.ts new file mode 100644 index 0000000000..7a9cf98959 --- /dev/null +++ b/examples/oft/tasks/sendEvm.ts @@ -0,0 +1,242 @@ +import path from 'path' + +import { BigNumber, ContractTransaction } from 'ethers' +import { parseUnits } from 'ethers/lib/utils' +import { HardhatRuntimeEnvironment } from 'hardhat/types' + +import { OmniPointHardhat, createGetHreByEid } from '@layerzerolabs/devtools-evm-hardhat' +import { createLogger } from '@layerzerolabs/io-devtools' +import { ChainType, endpointIdToChainType, endpointIdToNetwork } from '@layerzerolabs/lz-definitions' +import { Options, addressToBytes32 } from '@layerzerolabs/lz-v2-utilities' + +import { SendResult } from './types' +import { DebugLogger, KnownErrors, getLayerZeroScanLink } from './utils' + +const logger = createLogger() + +export interface EvmArgs { + srcEid: number + dstEid: number + amount: string + to: string + oappConfig: string + minAmount?: string + extraLzReceiveOptions?: string[] + extraLzComposeOptions?: string[] + extraNativeDropOptions?: string[] + composeMsg?: string + oftAddress?: string +} + +export async function sendEvm( + { + srcEid, + dstEid, + amount, + to, + oappConfig, + minAmount, + extraLzReceiveOptions, + extraLzComposeOptions, + extraNativeDropOptions, + composeMsg, + oftAddress, + }: EvmArgs, + hre: HardhatRuntimeEnvironment +): Promise { + if (endpointIdToChainType(srcEid) !== ChainType.EVM) { + throw new Error(`non-EVM srcEid (${srcEid}) not supported here`) + } + + const getHreByEid = createGetHreByEid(hre) + let srcEidHre: HardhatRuntimeEnvironment + try { + srcEidHre = await getHreByEid(srcEid) + } catch (error) { + DebugLogger.printErrorAndFixSuggestion( + KnownErrors.ERROR_GETTING_HRE, + `For network: ${endpointIdToNetwork(srcEid)}, OFT: ${oftAddress}` + ) + throw error + } + const signer = (await srcEidHre.ethers.getSigners())[0] + + // 1️⃣ resolve the OFT wrapper address + let wrapperAddress: string + if (oftAddress) { + wrapperAddress = oftAddress + } else { + const layerZeroConfig = (await import(path.resolve('./', oappConfig))).default + const { contracts } = typeof layerZeroConfig === 'function' ? await layerZeroConfig() : layerZeroConfig + const wrapper = contracts.find((c: { contract: OmniPointHardhat }) => c.contract.eid === srcEid) + if (!wrapper) throw new Error(`No config for EID ${srcEid}`) + wrapperAddress = wrapper.contract.contractName + ? (await srcEidHre.deployments.get(wrapper.contract.contractName)).address + : wrapper.contract.address || '' + } + + // 2️⃣ load IOFT ABI, extend it with token() + const ioftArtifact = await srcEidHre.artifacts.readArtifact('IOFT') + + // now attach + const oft = await srcEidHre.ethers.getContractAt(ioftArtifact.abi, wrapperAddress, signer) + + // 3️⃣ fetch the underlying ERC-20 + const underlying = await oft.token() + + // 4️⃣ fetch decimals from the underlying token + const erc20 = await srcEidHre.ethers.getContractAt('ERC20', underlying, signer) + const decimals: number = await erc20.decimals() + + // 5️⃣ normalize the user-supplied amount + const amountUnits: BigNumber = parseUnits(amount, decimals) + + // 6️⃣ Check if approval is required (for OFT Adapters) and handle approval + try { + const approvalRequired = await oft.approvalRequired() + if (approvalRequired) { + logger.info('OFT Adapter detected - checking ERC20 allowance...') + + // Check current allowance + const currentAllowance = await erc20.allowance(signer.address, wrapperAddress) + logger.info(`Current allowance: ${currentAllowance.toString()}`) + logger.info(`Required amount: ${amountUnits.toString()}`) + + if (currentAllowance.lt(amountUnits)) { + logger.info('Insufficient allowance - approving ERC20 tokens...') + const approveTx = await erc20.approve(wrapperAddress, amountUnits) + logger.info(`Approval transaction hash: ${approveTx.hash}`) + await approveTx.wait() + logger.info('ERC20 approval confirmed') + } else { + logger.info('Sufficient allowance already exists') + } + } + } catch (error) { + // If approvalRequired() doesn't exist or fails, assume it's a regular OFT (not an adapter) + logger.info('No approval required (regular OFT detected)') + } + + // 7️⃣ hex string → Uint8Array → zero-pad to 32 bytes + const toBytes = addressToBytes32(to) + + // 8️⃣ Build options dynamically using Options.newOptions() + let options = Options.newOptions() + + // Add lzReceive options + if (extraLzReceiveOptions && extraLzReceiveOptions.length > 0) { + // Handle case where Hardhat's CSV parsing splits "gas,value" into separate elements + if (extraLzReceiveOptions.length % 2 !== 0) { + throw new Error( + `Invalid lzReceive options: received ${extraLzReceiveOptions.length} values, but expected pairs of gas,value` + ) + } + + for (let i = 0; i < extraLzReceiveOptions.length; i += 2) { + const gas = Number(extraLzReceiveOptions[i]) + const value = Number(extraLzReceiveOptions[i + 1]) || 0 + options = options.addExecutorLzReceiveOption(gas, value) + logger.info(`Added lzReceive option: ${gas} gas, ${value} value`) + } + } + + // Add lzCompose options + if (extraLzComposeOptions && extraLzComposeOptions.length > 0) { + // Handle case where Hardhat's CSV parsing splits "index,gas,value" into separate elements + if (extraLzComposeOptions.length % 3 !== 0) { + throw new Error( + `Invalid lzCompose options: received ${extraLzComposeOptions.length} values, but expected triplets of index,gas,value` + ) + } + + for (let i = 0; i < extraLzComposeOptions.length; i += 3) { + const index = Number(extraLzComposeOptions[i]) + const gas = Number(extraLzComposeOptions[i + 1]) + const value = Number(extraLzComposeOptions[i + 2]) || 0 + options = options.addExecutorComposeOption(index, gas, value) + logger.info(`Added lzCompose option: index ${index}, ${gas} gas, ${value} value`) + } + } + + // Add native drop options + if (extraNativeDropOptions && extraNativeDropOptions.length > 0) { + // Handle case where Hardhat's CSV parsing splits "amount,recipient" into separate elements + if (extraNativeDropOptions.length % 2 !== 0) { + throw new Error( + `Invalid native drop options: received ${extraNativeDropOptions.length} values, but expected pairs of amount,recipient` + ) + } + + for (let i = 0; i < extraNativeDropOptions.length; i += 2) { + const amountStr = extraNativeDropOptions[i] + const recipient = extraNativeDropOptions[i + 1] + + if (!amountStr || !recipient) { + throw new Error( + `Invalid native drop option: Both amount and recipient must be provided. Got amount="${amountStr}", recipient="${recipient}"` + ) + } + + try { + options = options.addExecutorNativeDropOption(amountStr.trim(), recipient.trim()) + logger.info(`Added native drop option: ${amountStr.trim()} wei to ${recipient.trim()}`) + } catch (error) { + // Provide helpful context if the amount exceeds protocol limits + const maxUint128 = BigInt('340282366920938463463374607431768211455') // 2^128 - 1 + const maxUint128Ether = Number(maxUint128) / 1e18 // Convert to ETH for readability + + throw new Error( + `Failed to add native drop option with amount ${amountStr.trim()} wei. ` + + `LayerZero protocol constrains native drop amounts to uint128 maximum ` + + `(${maxUint128.toString()} wei ≈ ${maxUint128Ether.toFixed(2)} ETH). ` + + `Original error: ${error instanceof Error ? error.message : String(error)}` + ) + } + } + } + + const extraOptions = options.toHex() + + // 9️⃣ build sendParam and dispatch + const sendParam = { + dstEid, + to: toBytes, + amountLD: amountUnits.toString(), + minAmountLD: minAmount ? parseUnits(minAmount, decimals).toString() : amountUnits.toString(), + extraOptions: extraOptions, + composeMsg: composeMsg ? composeMsg.toString() : '0x', + oftCmd: '0x', + } + + // 10️⃣ Quote (MessagingFee = { nativeFee, lzTokenFee }) + logger.info('Quoting the native gas cost for the send transaction...') + let msgFee: { nativeFee: BigNumber; lzTokenFee: BigNumber } + try { + msgFee = await oft.quoteSend(sendParam, false) + } catch (error) { + DebugLogger.printErrorAndFixSuggestion( + KnownErrors.ERROR_QUOTING_NATIVE_GAS_COST, + `For network: ${endpointIdToNetwork(srcEid)}, OFT: ${oftAddress}` + ) + throw error + } + logger.info('Sending the transaction...') + let tx: ContractTransaction + try { + tx = await oft.send(sendParam, msgFee, signer.address, { + value: msgFee.nativeFee, + }) + } catch (error) { + DebugLogger.printErrorAndFixSuggestion( + KnownErrors.ERROR_SENDING_TRANSACTION, + `For network: ${endpointIdToNetwork(srcEid)}, OFT: ${oftAddress}` + ) + throw error + } + const receipt = await tx.wait() + + const txHash = receipt.transactionHash + const scanLink = getLayerZeroScanLink(txHash, srcEid >= 40_000 && srcEid < 50_000) + + return { txHash, scanLink } +} diff --git a/examples/oft/tasks/sendOFT.ts b/examples/oft/tasks/sendOFT.ts new file mode 100644 index 0000000000..32101a05af --- /dev/null +++ b/examples/oft/tasks/sendOFT.ts @@ -0,0 +1,108 @@ +import { task, types } from 'hardhat/config' +import { HardhatRuntimeEnvironment } from 'hardhat/types' + +import { types as cliTypes } from '@layerzerolabs/devtools-evm-hardhat' +import { ChainType, endpointIdToChainType, endpointIdToNetwork } from '@layerzerolabs/lz-definitions' + +import { EvmArgs, sendEvm } from './sendEvm' +import { SendResult } from './types' +import { DebugLogger, KnownOutputs, KnownWarnings, getBlockExplorerLink } from './utils' + +interface MasterArgs { + srcEid: number + dstEid: number + amount: string + to: string + oappConfig: string + /** Minimum amount to receive in case of custom slippage or fees (human readable units, e.g. "1.5") */ + minAmount?: string + /** Array of lzReceive options as comma-separated values "gas,value" - e.g. --extra-lz-receive-options "200000,0" */ + extraLzReceiveOptions?: string[] + /** Array of lzCompose options as comma-separated values "index,gas,value" - e.g. --extra-lz-compose-options "0,500000,0" */ + extraLzComposeOptions?: string[] + /** Array of native drop options as comma-separated values "amount,recipient" - e.g. --extra-native-drop-options "1000000000000000000,0x1234..." */ + extraNativeDropOptions?: string[] + /** Arbitrary bytes message to deliver alongside the OFT */ + composeMsg?: string + /** EVM: 20-byte hex address */ + oftAddress?: string +} + +task('lz:oft:send', 'Sends OFT tokens cross‐chain from EVM chains') + .addParam('srcEid', 'Source endpoint ID', undefined, types.int) + .addParam('dstEid', 'Destination endpoint ID', undefined, types.int) + .addParam('amount', 'Amount to send (human readable units, e.g. "1.5")', undefined, types.string) + .addParam('to', 'Recipient address (20-byte hex for EVM)', undefined, types.string) + .addOptionalParam('oappConfig', 'Path to the LayerZero config file', 'layerzero.config.ts', types.string) + .addOptionalParam( + 'minAmount', + 'Minimum amount to receive in case of custom slippage or fees (human readable units, e.g. "1.5")', + undefined, + types.string + ) + .addOptionalParam( + 'extraLzReceiveOptions', + 'Array of extra lzReceive options in format "gas,value" (e.g. ["200000,0", "100000,1000000000000000000"])', + undefined, + cliTypes.csv + ) + .addOptionalParam( + 'extraLzComposeOptions', + 'Array of extra lzCompose options in format "index,gas,value" (e.g. ["0,500000,0", "1,300000,1000000000000000000"])', + undefined, + cliTypes.csv + ) + .addOptionalParam( + 'extraNativeDropOptions', + 'Array of extra native drop options in format "amount,recipient" (e.g. ["1000000000000000000,0x1234..."])', + undefined, + cliTypes.csv + ) + .addOptionalParam('composeMsg', 'Arbitrary bytes message to deliver alongside the OFT', undefined, types.string) + .addOptionalParam( + 'oftAddress', + 'Override the source local deployment OFT address (20-byte hex for EVM)', + undefined, + types.string + ) + .setAction(async (args: MasterArgs, hre: HardhatRuntimeEnvironment) => { + const chainType = endpointIdToChainType(args.srcEid) + let result: SendResult + + if (args.oftAddress) { + DebugLogger.printWarning( + KnownWarnings.USING_OVERRIDE_OFT, + `For network: ${endpointIdToNetwork(args.srcEid)}, OFT: ${args.oftAddress}` + ) + } + + // Only support EVM chains in this example + if (chainType === ChainType.EVM) { + result = await sendEvm(args as EvmArgs, hre) + } else { + throw new Error( + `The chain type ${chainType} is not supported in this OFT example. Only EVM chains are supported.` + ) + } + + DebugLogger.printLayerZeroOutput( + KnownOutputs.SENT_VIA_OFT, + `Successfully sent ${args.amount} tokens from ${endpointIdToNetwork(args.srcEid)} to ${endpointIdToNetwork(args.dstEid)}` + ) + + // print the explorer link for the srcEid from metadata + const explorerLink = await getBlockExplorerLink(args.srcEid, result.txHash) + // if explorer link is available, print the tx hash link + if (explorerLink) { + DebugLogger.printLayerZeroOutput( + KnownOutputs.TX_HASH, + `Explorer link for source chain ${endpointIdToNetwork(args.srcEid)}: ${explorerLink}` + ) + } + + // print the LayerZero Scan link from metadata + DebugLogger.printLayerZeroOutput( + KnownOutputs.EXPLORER_LINK, + `LayerZero Scan link for tracking all cross-chain transaction details: ${result.scanLink}` + ) + }) diff --git a/examples/oft/tasks/types.ts b/examples/oft/tasks/types.ts new file mode 100644 index 0000000000..8d2b681692 --- /dev/null +++ b/examples/oft/tasks/types.ts @@ -0,0 +1,4 @@ +export interface SendResult { + txHash: string // EVM: receipt.transactionHash + scanLink: string // LayerZeroScan link for cross-chain tracking +} diff --git a/examples/oft/tasks/utils.ts b/examples/oft/tasks/utils.ts new file mode 100644 index 0000000000..934c9a046d --- /dev/null +++ b/examples/oft/tasks/utils.ts @@ -0,0 +1,51 @@ +import { createLogger } from '@layerzerolabs/io-devtools' +import { endpointIdToNetwork } from '@layerzerolabs/lz-definitions' +import { Options } from '@layerzerolabs/lz-v2-utilities' + +const logger = createLogger() + +export const deploymentMetadataUrl = 'https://metadata.layerzero-api.com/v1/metadata/deployments' + +/** + * Given a srcEid and on-chain tx hash, return + * `https://…blockExplorers[0].url/tx/`, or undefined. + */ +export async function getBlockExplorerLink(srcEid: number, txHash: string): Promise { + const network = endpointIdToNetwork(srcEid) // e.g. "ethereum-mainnet" + const res = await fetch(deploymentMetadataUrl) + if (!res.ok) return + const all = (await res.json()) as Record + const meta = all[network] + const explorer = meta?.blockExplorers?.[0]?.url + if (explorer) { + // many explorers use `/tx/` + return `${explorer.replace(/\/+$/, '')}/tx/${txHash}` + } + return +} + +function formatBigIntForDisplay(n: bigint) { + return n.toLocaleString().replace(/,/g, '_') +} + +export function decodeLzReceiveOptions(hex: string): string { + try { + // Handle empty/undefined values first + if (!hex || hex === '0x') return 'No options set' + const options = Options.fromOptions(hex) + const lzReceiveOpt = options.decodeExecutorLzReceiveOption() + return lzReceiveOpt + ? `gas: ${formatBigIntForDisplay(lzReceiveOpt.gas)} , value: ${formatBigIntForDisplay(lzReceiveOpt.value)} wei` + : 'No executor options' + } catch (e) { + return `Invalid options (${hex.slice(0, 12)}...)` + } +} + +// Get LayerZero scan link +export function getLayerZeroScanLink(txHash: string, isTestnet = false): string { + const baseUrl = isTestnet ? 'https://testnet.layerzeroscan.com' : 'https://layerzeroscan.com' + return `${baseUrl}/tx/${txHash}` +} + +export { DebugLogger, KnownErrors, KnownOutputs, KnownWarnings } from '@layerzerolabs/io-devtools' diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 96fe71d44f..5eef19b208 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -275,9 +275,15 @@ importers: '@babel/core': specifier: ^7.23.9 version: 7.23.9 + '@layerzerolabs/devtools-evm-hardhat': + specifier: ^3.1.0 + version: link:../../packages/devtools-evm-hardhat '@layerzerolabs/eslint-config-next': specifier: ~2.3.39 version: 2.3.44(typescript@5.5.3) + '@layerzerolabs/io-devtools': + specifier: ~0.2.0 + version: link:../../packages/io-devtools '@layerzerolabs/lz-definitions': specifier: ^3.0.75 version: 3.0.75 @@ -293,6 +299,9 @@ importers: '@layerzerolabs/lz-v2-utilities': specifier: ^3.0.75 version: 3.0.75 + '@layerzerolabs/metadata-tools': + specifier: ^2.0.0 + version: link:../../packages/metadata-tools '@layerzerolabs/oapp-evm': specifier: ^0.3.2 version: link:../../packages/oapp-evm @@ -896,12 +905,18 @@ importers: '@babel/core': specifier: ^7.23.9 version: 7.23.9 + '@layerzerolabs/devtools': + specifier: ~1.0.0 + version: link:../../packages/devtools '@layerzerolabs/devtools-evm-hardhat': specifier: ^3.1.0 version: link:../../packages/devtools-evm-hardhat '@layerzerolabs/eslint-config-next': specifier: ~2.3.39 version: 2.3.44(typescript@5.5.3) + '@layerzerolabs/io-devtools': + specifier: ~0.2.0 + version: link:../../packages/io-devtools '@layerzerolabs/lz-definitions': specifier: ^3.0.75 version: 3.0.75 @@ -1016,9 +1031,15 @@ importers: '@babel/core': specifier: ^7.23.9 version: 7.23.9 + '@layerzerolabs/devtools-evm-hardhat': + specifier: ^3.1.0 + version: link:../../packages/devtools-evm-hardhat '@layerzerolabs/eslint-config-next': specifier: ~2.3.39 version: 2.3.44(typescript@5.5.3) + '@layerzerolabs/io-devtools': + specifier: ~0.2.0 + version: link:../../packages/io-devtools '@layerzerolabs/lz-definitions': specifier: ^3.0.75 version: 3.0.75 @@ -1034,6 +1055,9 @@ importers: '@layerzerolabs/lz-v2-utilities': specifier: ^3.0.75 version: 3.0.75 + '@layerzerolabs/metadata-tools': + specifier: ^2.0.0 + version: link:../../packages/metadata-tools '@layerzerolabs/oapp-evm': specifier: ^0.3.2 version: link:../../packages/oapp-evm