From 61ee1a8bcc5f76ed3a9146006b0d0f139e2f8bf3 Mon Sep 17 00:00:00 2001 From: Krak Date: Mon, 2 Jun 2025 17:07:03 +0700 Subject: [PATCH 01/14] chore: fix changeset --- .changeset/khaki-hounds-itch.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/khaki-hounds-itch.md diff --git a/.changeset/khaki-hounds-itch.md b/.changeset/khaki-hounds-itch.md new file mode 100644 index 0000000000..a79815536c --- /dev/null +++ b/.changeset/khaki-hounds-itch.md @@ -0,0 +1,5 @@ +--- +"@layerzerolabs/hyperliquid-composer": patch +--- + +Fix registry From 022b6ffa936cc37563963cc64bbede7f7b561d94 Mon Sep 17 00:00:00 2001 From: Krak Date: Tue, 10 Jun 2025 11:22:32 -0700 Subject: [PATCH 02/14] feat: add send task to oft example --- .changeset/flat-bears-doubt.md | 5 ++ examples/oft/contracts/MyOFT.sol | 4 +- examples/oft/hardhat.config.ts | 2 + examples/oft/package.json | 3 + examples/oft/tasks/sendEvm.ts | 132 +++++++++++++++++++++++++++++++ examples/oft/tasks/sendOFT.ts | 88 +++++++++++++++++++++ examples/oft/tasks/types.ts | 4 + examples/oft/tasks/utils.ts | 51 ++++++++++++ pnpm-lock.yaml | 86 ++++++-------------- 9 files changed, 311 insertions(+), 64 deletions(-) create mode 100644 .changeset/flat-bears-doubt.md create mode 100644 examples/oft/tasks/sendEvm.ts create mode 100644 examples/oft/tasks/sendOFT.ts create mode 100644 examples/oft/tasks/types.ts create mode 100644 examples/oft/tasks/utils.ts 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/examples/oft/contracts/MyOFT.sol b/examples/oft/contracts/MyOFT.sol index f8bc7b47f6..2632f6cc37 100644 --- a/examples/oft/contracts/MyOFT.sol +++ b/examples/oft/contracts/MyOFT.sol @@ -10,5 +10,7 @@ contract MyOFT is OFT { string memory _symbol, address _lzEndpoint, address _delegate - ) OFT(_name, _symbol, _lzEndpoint, _delegate) Ownable(_delegate) {} + ) OFT(_name, _symbol, _lzEndpoint, _delegate) Ownable(_delegate) { + _mint(msg.sender, 100 ether); + } } 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..f1809c0dd7 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", @@ -48,6 +50,7 @@ "@types/chai": "^4.3.11", "@types/mocha": "^10.0.6", "@types/node": "~18.18.14", + "bs58": "^5.0.0", "chai": "^4.4.1", "concurrently": "~9.1.0", "dotenv": "^16.4.1", diff --git a/examples/oft/tasks/sendEvm.ts b/examples/oft/tasks/sendEvm.ts new file mode 100644 index 0000000000..4160a96538 --- /dev/null +++ b/examples/oft/tasks/sendEvm.ts @@ -0,0 +1,132 @@ +import bs58 from 'bs58' +import { BigNumber, ContractTransaction } from 'ethers' +import { parseUnits } from 'ethers/lib/utils' +import { HardhatRuntimeEnvironment } from 'hardhat/types' + +import { makeBytes32 } from '@layerzerolabs/devtools' +import { createGetHreByEid } from '@layerzerolabs/devtools-evm-hardhat' +import { createLogger } from '@layerzerolabs/io-devtools' +import { ChainType, endpointIdToChainType, endpointIdToNetwork } from '@layerzerolabs/lz-definitions' + +import layerzeroConfig from '../layerzero.config' + +import { SendResult } from './types' +import { DebugLogger, KnownErrors, getLayerZeroScanLink } from './utils' + +const logger = createLogger() + +export interface EvmArgs { + srcEid: number + dstEid: number + amount: string + to: string + minAmount?: string + extraOptions?: string + composeMsg?: string + oftAddress?: string +} + +export async function sendEvm( + { srcEid, dstEid, amount, to, minAmount, extraOptions, 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 { contracts } = typeof layerzeroConfig === 'function' ? await layerzeroConfig() : layerzeroConfig + const wrapper = contracts.find((c) => 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) + + // Decide how to encode `to` based on target chain: + const dstChain = endpointIdToChainType(dstEid) + let toBytes: string + if (dstChain === ChainType.SOLANA) { + // Base58→32-byte buffer + toBytes = makeBytes32(bs58.decode(to)) + } else { + // hex string → Uint8Array → zero-pad to 32 bytes + toBytes = makeBytes32(to) + } + + // 6️⃣ build sendParam and dispatch + const sendParam = { + dstEid, + to: toBytes, + amountLD: amountUnits.toString(), + minAmountLD: minAmount ? parseUnits(minAmount, decimals).toString() : amountUnits.toString(), + extraOptions: extraOptions ? extraOptions.toString() : '0x', + composeMsg: composeMsg ? composeMsg.toString() : '0x', + oftCmd: '0x', + } + + // 6️⃣ 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..6875e2783b --- /dev/null +++ b/examples/oft/tasks/sendOFT.ts @@ -0,0 +1,88 @@ +import { task, types } from 'hardhat/config' +import { HardhatRuntimeEnvironment } from 'hardhat/types' + +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 + /** Minimum amount to receive in case of custom slippage or fees (human readable units, e.g. "1.5") */ + minAmount?: string + /** Extra options for sending additional gas units to lzReceive, lzCompose, or receiver address */ + extraOptions?: 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( + 'minAmount', + 'Minimum amount to receive in case of custom slippage or fees (human readable units, e.g. "1.5")', + undefined, + types.string + ) + .addOptionalParam( + 'extraOptions', + 'Extra options for sending additional gas units to lzReceive, lzCompose, or receiver address', + 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..f281c78173 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -896,12 +896,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 @@ -965,6 +971,9 @@ importers: '@types/node': specifier: ~18.18.14 version: 18.18.14 + bs58: + specifier: ^5.0.0 + version: 5.0.0 chai: specifier: ^4.4.1 version: 4.4.1 @@ -3688,7 +3697,7 @@ importers: version: 2.16.2 jest: specifier: ^29.6.2 - version: 29.7.0(@types/node@18.18.14) + version: 29.7.0(@types/node@18.18.14)(ts-node@10.9.2) tsup: specifier: ~8.0.1 version: 8.0.1(@swc/core@1.4.0)(typescript@5.5.3) @@ -3873,7 +3882,7 @@ importers: version: 29.5.12 jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@18.18.14) + version: 29.7.0(@types/node@18.18.14)(ts-node@10.9.2) tslib: specifier: ~2.6.2 version: 2.6.3 @@ -4401,13 +4410,13 @@ importers: version: 3.0.75 '@layerzerolabs/lz-solana-sdk-v2': specifier: ^3.0.0 - version: 3.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.5.3) + version: 3.0.0(typescript@5.5.3) '@layerzerolabs/lz-v2-utilities': specifier: ^3.0.75 version: 3.0.75 '@layerzerolabs/oft-v2-solana-sdk': specifier: ^3.0.38 - version: 3.0.38(@swc/core@1.4.0)(@types/node@18.18.14)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.5.3) + version: 3.0.38(@swc/core@1.4.0)(@types/node@18.18.14)(typescript@5.5.3) '@layerzerolabs/protocol-devtools': specifier: ~2.0.0 version: link:../protocol-devtools @@ -5028,13 +5037,13 @@ importers: version: 3.0.75 '@layerzerolabs/lz-solana-sdk-v2': specifier: ^3.0.59 - version: 3.0.66(@swc/core@1.4.0)(@types/node@18.18.14)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.5.3) + version: 3.0.66(@swc/core@1.4.0)(@types/node@18.18.14)(typescript@5.5.3) '@layerzerolabs/lz-v2-utilities': specifier: ^3.0.75 version: 3.0.75 '@layerzerolabs/oft-v2-solana-sdk': specifier: ^3.0.59 - version: 3.0.66(@swc/core@1.4.0)(@types/node@18.18.14)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.5.3) + version: 3.0.66(@swc/core@1.4.0)(@types/node@18.18.14)(typescript@5.5.3) '@layerzerolabs/protocol-devtools': specifier: ~2.0.0 version: link:../protocol-devtools @@ -5139,7 +5148,7 @@ importers: version: 12.6.1 jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@18.18.14) + version: 29.7.0(@types/node@18.18.14)(ts-node@10.9.2) tsup: specifier: ^8.0.1 version: 8.0.1(@swc/core@1.4.0)(typescript@5.5.3) @@ -9715,7 +9724,7 @@ packages: - utf-8-validate dev: false - /@layerzerolabs/lz-solana-sdk-v2@3.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.5.3): + /@layerzerolabs/lz-solana-sdk-v2@3.0.0(typescript@5.5.3): resolution: {integrity: sha512-sPvLXeQUO9QLpjOuWE7V+V8yfoI4E/NBYsH9lO2aPx0LYkQa+88ACgPq43B/zFROUD8238WuSb+doGrn3PKtJQ==} dependencies: '@ethersproject/abi': 5.7.0 @@ -9746,7 +9755,7 @@ packages: - utf-8-validate dev: true - /@layerzerolabs/lz-solana-sdk-v2@3.0.66(@swc/core@1.4.0)(@types/node@18.18.14)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.5.3): + /@layerzerolabs/lz-solana-sdk-v2@3.0.66(@swc/core@1.4.0)(@types/node@18.18.14)(typescript@5.5.3): resolution: {integrity: sha512-zyuqBYxaVtSa+STdcbO/uzzQV2kyxmBX3flNbvOq7kS2QHBivuaYd/XDbNLE54/egQ63yMtFcilqwy3VzNpclw==} dependencies: '@layerzerolabs/lz-corekit-solana': 3.0.66(@swc/core@1.4.0)(@types/node@18.18.14)(typescript@5.5.3) @@ -9854,7 +9863,7 @@ packages: - utf-8-validate dev: true - /@layerzerolabs/lz-solana-sdk-v2@3.0.86(@swc/core@1.4.0)(@types/node@18.18.14)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.5.3): + /@layerzerolabs/lz-solana-sdk-v2@3.0.86(@swc/core@1.4.0)(@types/node@18.18.14)(typescript@5.5.3): resolution: {integrity: sha512-FfZLkFHIOPIFLkevQD0QSrfVR2UgREG6YF6oSR92XPViuVpAq0LAyCTXQi915bdiSdQ/Mwd+9eU/9ZPlk9f6sA==} dependencies: '@layerzerolabs/lz-corekit-solana': 3.0.86(@swc/core@1.4.0)(@types/node@18.18.14)(typescript@5.5.3) @@ -10172,12 +10181,12 @@ packages: '@layerzerolabs/lz-definitions': 3.0.75 dev: true - /@layerzerolabs/oft-v2-solana-sdk@3.0.38(@swc/core@1.4.0)(@types/node@18.18.14)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.5.3): + /@layerzerolabs/oft-v2-solana-sdk@3.0.38(@swc/core@1.4.0)(@types/node@18.18.14)(typescript@5.5.3): resolution: {integrity: sha512-P06/a5+ixph0u1AQkDZ0P0oFaIAdfGPl/UezMfWXUpiWLth428RT0rrMR6qI7z6X1uxqlUFNIotz2ET1fyFcpQ==} dependencies: '@ethersproject/bytes': 5.7.0 '@layerzerolabs/lz-foundation': 3.0.38(@swc/core@1.4.0)(@types/node@18.18.14)(typescript@5.5.3) - '@layerzerolabs/lz-solana-sdk-v2': 3.0.86(@swc/core@1.4.0)(@types/node@18.18.14)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.5.3) + '@layerzerolabs/lz-solana-sdk-v2': 3.0.86(@swc/core@1.4.0)(@types/node@18.18.14)(typescript@5.5.3) '@layerzerolabs/lz-v2-utilities': 3.0.86 '@metaplex-foundation/beet': 0.7.2 '@metaplex-foundation/beet-solana': 0.4.1 @@ -10203,12 +10212,12 @@ packages: - utf-8-validate dev: true - /@layerzerolabs/oft-v2-solana-sdk@3.0.66(@swc/core@1.4.0)(@types/node@18.18.14)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.5.3): + /@layerzerolabs/oft-v2-solana-sdk@3.0.66(@swc/core@1.4.0)(@types/node@18.18.14)(typescript@5.5.3): resolution: {integrity: sha512-ijbvj6/Gc4O4WLHfqnrBuKUtIhpaYws/ORj2apZFT1RKSgAX8CCJ9aZmn0ClamEG98i+PpXoroPPU46LMOMZyA==} dependencies: '@ethersproject/bytes': 5.7.0 '@layerzerolabs/lz-foundation': 3.0.66(@swc/core@1.4.0)(@types/node@18.18.14)(typescript@5.5.3) - '@layerzerolabs/lz-solana-sdk-v2': 3.0.86(@swc/core@1.4.0)(@types/node@18.18.14)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.5.3) + '@layerzerolabs/lz-solana-sdk-v2': 3.0.86(@swc/core@1.4.0)(@types/node@18.18.14)(typescript@5.5.3) '@layerzerolabs/lz-v2-utilities': 3.0.86 '@metaplex-foundation/beet': 0.7.2 '@metaplex-foundation/beet-solana': 0.4.1 @@ -18968,34 +18977,6 @@ packages: - babel-plugin-macros - supports-color - /jest-cli@29.7.0(@types/node@18.18.14): - resolution: {integrity: sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - hasBin: true - peerDependencies: - node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 - peerDependenciesMeta: - node-notifier: - optional: true - dependencies: - '@jest/core': 29.7.0(ts-node@10.9.2) - '@jest/test-result': 29.7.0 - '@jest/types': 29.6.3 - chalk: 4.1.2 - create-jest: 29.7.0(@types/node@18.18.14)(ts-node@10.9.2) - exit: 0.1.2 - import-local: 3.1.0 - jest-config: 29.7.0(@types/node@18.18.14)(ts-node@10.9.2) - jest-util: 29.7.0 - jest-validate: 29.7.0 - yargs: 17.7.2 - transitivePeerDependencies: - - '@types/node' - - babel-plugin-macros - - supports-color - - ts-node - dev: true - /jest-cli@29.7.0(@types/node@18.18.14)(ts-node@10.9.2): resolution: {integrity: sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -19408,27 +19389,6 @@ packages: merge-stream: 2.0.0 supports-color: 8.1.1 - /jest@29.7.0(@types/node@18.18.14): - resolution: {integrity: sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - hasBin: true - peerDependencies: - node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 - peerDependenciesMeta: - node-notifier: - optional: true - dependencies: - '@jest/core': 29.7.0(ts-node@10.9.2) - '@jest/types': 29.6.3 - import-local: 3.1.0 - jest-cli: 29.7.0(@types/node@18.18.14) - transitivePeerDependencies: - - '@types/node' - - babel-plugin-macros - - supports-color - - ts-node - dev: true - /jest@29.7.0(@types/node@18.18.14)(ts-node@10.9.2): resolution: {integrity: sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} From be51a03ca266123f4ff8fdfd841c5cf807864ab7 Mon Sep 17 00:00:00 2001 From: Matthew Krak Date: Tue, 10 Jun 2025 11:24:18 -0700 Subject: [PATCH 03/14] Delete .changeset/khaki-hounds-itch.md --- .changeset/khaki-hounds-itch.md | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 .changeset/khaki-hounds-itch.md diff --git a/.changeset/khaki-hounds-itch.md b/.changeset/khaki-hounds-itch.md deleted file mode 100644 index a79815536c..0000000000 --- a/.changeset/khaki-hounds-itch.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@layerzerolabs/hyperliquid-composer": patch ---- - -Fix registry From 9f1d0bbaeb30a3eca304da329267880893694585 Mon Sep 17 00:00:00 2001 From: Krak Date: Tue, 10 Jun 2025 11:24:45 -0700 Subject: [PATCH 04/14] chore: reset oft --- examples/oft/contracts/MyOFT.sol | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/examples/oft/contracts/MyOFT.sol b/examples/oft/contracts/MyOFT.sol index 2632f6cc37..f8bc7b47f6 100644 --- a/examples/oft/contracts/MyOFT.sol +++ b/examples/oft/contracts/MyOFT.sol @@ -10,7 +10,5 @@ contract MyOFT is OFT { string memory _symbol, address _lzEndpoint, address _delegate - ) OFT(_name, _symbol, _lzEndpoint, _delegate) Ownable(_delegate) { - _mint(msg.sender, 100 ether); - } + ) OFT(_name, _symbol, _lzEndpoint, _delegate) Ownable(_delegate) {} } From fafbd4c1c7214123734179827e48ee0ac2447729 Mon Sep 17 00:00:00 2001 From: Krak Date: Tue, 10 Jun 2025 12:52:03 -0700 Subject: [PATCH 05/14] chore: remove solana dst logic --- examples/oft/package.json | 1 - examples/oft/tasks/sendEvm.ts | 15 ++----- pnpm-lock.yaml | 80 +++++++++++++++++++++++++++-------- 3 files changed, 66 insertions(+), 30 deletions(-) diff --git a/examples/oft/package.json b/examples/oft/package.json index f1809c0dd7..3cf56a12af 100644 --- a/examples/oft/package.json +++ b/examples/oft/package.json @@ -50,7 +50,6 @@ "@types/chai": "^4.3.11", "@types/mocha": "^10.0.6", "@types/node": "~18.18.14", - "bs58": "^5.0.0", "chai": "^4.4.1", "concurrently": "~9.1.0", "dotenv": "^16.4.1", diff --git a/examples/oft/tasks/sendEvm.ts b/examples/oft/tasks/sendEvm.ts index 4160a96538..5b8ce871e9 100644 --- a/examples/oft/tasks/sendEvm.ts +++ b/examples/oft/tasks/sendEvm.ts @@ -1,12 +1,11 @@ -import bs58 from 'bs58' import { BigNumber, ContractTransaction } from 'ethers' import { parseUnits } from 'ethers/lib/utils' import { HardhatRuntimeEnvironment } from 'hardhat/types' -import { makeBytes32 } from '@layerzerolabs/devtools' import { createGetHreByEid } from '@layerzerolabs/devtools-evm-hardhat' import { createLogger } from '@layerzerolabs/io-devtools' import { ChainType, endpointIdToChainType, endpointIdToNetwork } from '@layerzerolabs/lz-definitions' +import { addressToBytes32 } from '@layerzerolabs/lz-v2-utilities' import layerzeroConfig from '../layerzero.config' @@ -76,16 +75,8 @@ export async function sendEvm( // 5️⃣ normalize the user-supplied amount const amountUnits: BigNumber = parseUnits(amount, decimals) - // Decide how to encode `to` based on target chain: - const dstChain = endpointIdToChainType(dstEid) - let toBytes: string - if (dstChain === ChainType.SOLANA) { - // Base58→32-byte buffer - toBytes = makeBytes32(bs58.decode(to)) - } else { - // hex string → Uint8Array → zero-pad to 32 bytes - toBytes = makeBytes32(to) - } + // hex string → Uint8Array → zero-pad to 32 bytes + const toBytes = addressToBytes32(to).toString() // 6️⃣ build sendParam and dispatch const sendParam = { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f281c78173..663063ae4f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -971,9 +971,6 @@ importers: '@types/node': specifier: ~18.18.14 version: 18.18.14 - bs58: - specifier: ^5.0.0 - version: 5.0.0 chai: specifier: ^4.4.1 version: 4.4.1 @@ -3697,7 +3694,7 @@ importers: version: 2.16.2 jest: specifier: ^29.6.2 - version: 29.7.0(@types/node@18.18.14)(ts-node@10.9.2) + version: 29.7.0(@types/node@18.18.14) tsup: specifier: ~8.0.1 version: 8.0.1(@swc/core@1.4.0)(typescript@5.5.3) @@ -3882,7 +3879,7 @@ importers: version: 29.5.12 jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@18.18.14)(ts-node@10.9.2) + version: 29.7.0(@types/node@18.18.14) tslib: specifier: ~2.6.2 version: 2.6.3 @@ -4410,13 +4407,13 @@ importers: version: 3.0.75 '@layerzerolabs/lz-solana-sdk-v2': specifier: ^3.0.0 - version: 3.0.0(typescript@5.5.3) + version: 3.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.5.3) '@layerzerolabs/lz-v2-utilities': specifier: ^3.0.75 version: 3.0.75 '@layerzerolabs/oft-v2-solana-sdk': specifier: ^3.0.38 - version: 3.0.38(@swc/core@1.4.0)(@types/node@18.18.14)(typescript@5.5.3) + version: 3.0.38(@swc/core@1.4.0)(@types/node@18.18.14)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.5.3) '@layerzerolabs/protocol-devtools': specifier: ~2.0.0 version: link:../protocol-devtools @@ -5037,13 +5034,13 @@ importers: version: 3.0.75 '@layerzerolabs/lz-solana-sdk-v2': specifier: ^3.0.59 - version: 3.0.66(@swc/core@1.4.0)(@types/node@18.18.14)(typescript@5.5.3) + version: 3.0.66(@swc/core@1.4.0)(@types/node@18.18.14)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.5.3) '@layerzerolabs/lz-v2-utilities': specifier: ^3.0.75 version: 3.0.75 '@layerzerolabs/oft-v2-solana-sdk': specifier: ^3.0.59 - version: 3.0.66(@swc/core@1.4.0)(@types/node@18.18.14)(typescript@5.5.3) + version: 3.0.66(@swc/core@1.4.0)(@types/node@18.18.14)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.5.3) '@layerzerolabs/protocol-devtools': specifier: ~2.0.0 version: link:../protocol-devtools @@ -5148,7 +5145,7 @@ importers: version: 12.6.1 jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@18.18.14)(ts-node@10.9.2) + version: 29.7.0(@types/node@18.18.14) tsup: specifier: ^8.0.1 version: 8.0.1(@swc/core@1.4.0)(typescript@5.5.3) @@ -9724,7 +9721,7 @@ packages: - utf-8-validate dev: false - /@layerzerolabs/lz-solana-sdk-v2@3.0.0(typescript@5.5.3): + /@layerzerolabs/lz-solana-sdk-v2@3.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.5.3): resolution: {integrity: sha512-sPvLXeQUO9QLpjOuWE7V+V8yfoI4E/NBYsH9lO2aPx0LYkQa+88ACgPq43B/zFROUD8238WuSb+doGrn3PKtJQ==} dependencies: '@ethersproject/abi': 5.7.0 @@ -9755,7 +9752,7 @@ packages: - utf-8-validate dev: true - /@layerzerolabs/lz-solana-sdk-v2@3.0.66(@swc/core@1.4.0)(@types/node@18.18.14)(typescript@5.5.3): + /@layerzerolabs/lz-solana-sdk-v2@3.0.66(@swc/core@1.4.0)(@types/node@18.18.14)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.5.3): resolution: {integrity: sha512-zyuqBYxaVtSa+STdcbO/uzzQV2kyxmBX3flNbvOq7kS2QHBivuaYd/XDbNLE54/egQ63yMtFcilqwy3VzNpclw==} dependencies: '@layerzerolabs/lz-corekit-solana': 3.0.66(@swc/core@1.4.0)(@types/node@18.18.14)(typescript@5.5.3) @@ -9863,7 +9860,7 @@ packages: - utf-8-validate dev: true - /@layerzerolabs/lz-solana-sdk-v2@3.0.86(@swc/core@1.4.0)(@types/node@18.18.14)(typescript@5.5.3): + /@layerzerolabs/lz-solana-sdk-v2@3.0.86(@swc/core@1.4.0)(@types/node@18.18.14)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.5.3): resolution: {integrity: sha512-FfZLkFHIOPIFLkevQD0QSrfVR2UgREG6YF6oSR92XPViuVpAq0LAyCTXQi915bdiSdQ/Mwd+9eU/9ZPlk9f6sA==} dependencies: '@layerzerolabs/lz-corekit-solana': 3.0.86(@swc/core@1.4.0)(@types/node@18.18.14)(typescript@5.5.3) @@ -10181,12 +10178,12 @@ packages: '@layerzerolabs/lz-definitions': 3.0.75 dev: true - /@layerzerolabs/oft-v2-solana-sdk@3.0.38(@swc/core@1.4.0)(@types/node@18.18.14)(typescript@5.5.3): + /@layerzerolabs/oft-v2-solana-sdk@3.0.38(@swc/core@1.4.0)(@types/node@18.18.14)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.5.3): resolution: {integrity: sha512-P06/a5+ixph0u1AQkDZ0P0oFaIAdfGPl/UezMfWXUpiWLth428RT0rrMR6qI7z6X1uxqlUFNIotz2ET1fyFcpQ==} dependencies: '@ethersproject/bytes': 5.7.0 '@layerzerolabs/lz-foundation': 3.0.38(@swc/core@1.4.0)(@types/node@18.18.14)(typescript@5.5.3) - '@layerzerolabs/lz-solana-sdk-v2': 3.0.86(@swc/core@1.4.0)(@types/node@18.18.14)(typescript@5.5.3) + '@layerzerolabs/lz-solana-sdk-v2': 3.0.86(@swc/core@1.4.0)(@types/node@18.18.14)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.5.3) '@layerzerolabs/lz-v2-utilities': 3.0.86 '@metaplex-foundation/beet': 0.7.2 '@metaplex-foundation/beet-solana': 0.4.1 @@ -10212,12 +10209,12 @@ packages: - utf-8-validate dev: true - /@layerzerolabs/oft-v2-solana-sdk@3.0.66(@swc/core@1.4.0)(@types/node@18.18.14)(typescript@5.5.3): + /@layerzerolabs/oft-v2-solana-sdk@3.0.66(@swc/core@1.4.0)(@types/node@18.18.14)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.5.3): resolution: {integrity: sha512-ijbvj6/Gc4O4WLHfqnrBuKUtIhpaYws/ORj2apZFT1RKSgAX8CCJ9aZmn0ClamEG98i+PpXoroPPU46LMOMZyA==} dependencies: '@ethersproject/bytes': 5.7.0 '@layerzerolabs/lz-foundation': 3.0.66(@swc/core@1.4.0)(@types/node@18.18.14)(typescript@5.5.3) - '@layerzerolabs/lz-solana-sdk-v2': 3.0.86(@swc/core@1.4.0)(@types/node@18.18.14)(typescript@5.5.3) + '@layerzerolabs/lz-solana-sdk-v2': 3.0.86(@swc/core@1.4.0)(@types/node@18.18.14)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.5.3) '@layerzerolabs/lz-v2-utilities': 3.0.86 '@metaplex-foundation/beet': 0.7.2 '@metaplex-foundation/beet-solana': 0.4.1 @@ -18977,6 +18974,34 @@ packages: - babel-plugin-macros - supports-color + /jest-cli@29.7.0(@types/node@18.18.14): + resolution: {integrity: sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + dependencies: + '@jest/core': 29.7.0(ts-node@10.9.2) + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + chalk: 4.1.2 + create-jest: 29.7.0(@types/node@18.18.14)(ts-node@10.9.2) + exit: 0.1.2 + import-local: 3.1.0 + jest-config: 29.7.0(@types/node@18.18.14)(ts-node@10.9.2) + jest-util: 29.7.0 + jest-validate: 29.7.0 + yargs: 17.7.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + dev: true + /jest-cli@29.7.0(@types/node@18.18.14)(ts-node@10.9.2): resolution: {integrity: sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -19389,6 +19414,27 @@ packages: merge-stream: 2.0.0 supports-color: 8.1.1 + /jest@29.7.0(@types/node@18.18.14): + resolution: {integrity: sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + dependencies: + '@jest/core': 29.7.0(ts-node@10.9.2) + '@jest/types': 29.6.3 + import-local: 3.1.0 + jest-cli: 29.7.0(@types/node@18.18.14) + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + dev: true + /jest@29.7.0(@types/node@18.18.14)(ts-node@10.9.2): resolution: {integrity: sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} From 481461a52cd1a4d64cf8837eef10f5ebac057eac Mon Sep 17 00:00:00 2001 From: Krak Date: Tue, 10 Jun 2025 13:23:37 -0700 Subject: [PATCH 06/14] chore: update MABA example --- .changeset/orange-sheep-provide.md | 5 + .../deploy/MintBurnERC20Mock.ts | 49 +++++++ .../mint-burn-oft-adapter/hardhat.config.ts | 20 ++- .../mint-burn-oft-adapter/layerzero.config.ts | 116 ++++++++--------- examples/mint-burn-oft-adapter/package.json | 3 + .../mint-burn-oft-adapter/tasks/sendEvm.ts | 123 ++++++++++++++++++ .../mint-burn-oft-adapter/tasks/sendOFT.ts | 88 +++++++++++++ examples/mint-burn-oft-adapter/tasks/types.ts | 4 + examples/mint-burn-oft-adapter/tasks/utils.ts | 51 ++++++++ examples/oft/tasks/sendEvm.ts | 2 +- pnpm-lock.yaml | 9 ++ 11 files changed, 404 insertions(+), 66 deletions(-) create mode 100644 .changeset/orange-sheep-provide.md create mode 100644 examples/mint-burn-oft-adapter/deploy/MintBurnERC20Mock.ts create mode 100644 examples/mint-burn-oft-adapter/tasks/sendEvm.ts create mode 100644 examples/mint-burn-oft-adapter/tasks/sendOFT.ts create mode 100644 examples/mint-burn-oft-adapter/tasks/types.ts create mode 100644 examples/mint-burn-oft-adapter/tasks/utils.ts 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/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..89a3d6ef71 --- /dev/null +++ b/examples/mint-burn-oft-adapter/tasks/sendEvm.ts @@ -0,0 +1,123 @@ +import { BigNumber, ContractTransaction } from 'ethers' +import { parseUnits } from 'ethers/lib/utils' +import { HardhatRuntimeEnvironment } from 'hardhat/types' + +import { createGetHreByEid } from '@layerzerolabs/devtools-evm-hardhat' +import { createLogger } from '@layerzerolabs/io-devtools' +import { ChainType, endpointIdToChainType, endpointIdToNetwork } from '@layerzerolabs/lz-definitions' +import { addressToBytes32 } from '@layerzerolabs/lz-v2-utilities' + +import layerzeroConfig from '../layerzero.config-test' + +import { SendResult } from './types' +import { DebugLogger, KnownErrors, getLayerZeroScanLink } from './utils' + +const logger = createLogger() + +export interface EvmArgs { + srcEid: number + dstEid: number + amount: string + to: string + minAmount?: string + extraOptions?: string + composeMsg?: string + oftAddress?: string +} + +export async function sendEvm( + { srcEid, dstEid, amount, to, minAmount, extraOptions, 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 { contracts } = typeof layerzeroConfig === 'function' ? await layerzeroConfig() : layerzeroConfig + const wrapper = contracts.find((c) => 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) + + // hex string → Uint8Array → zero-pad to 32 bytes + const toBytes = addressToBytes32(to) + + // 6️⃣ build sendParam and dispatch + const sendParam = { + dstEid, + to: toBytes, + amountLD: amountUnits.toString(), + minAmountLD: minAmount ? parseUnits(minAmount, decimals).toString() : amountUnits.toString(), + extraOptions: extraOptions ? extraOptions.toString() : '0x', + composeMsg: composeMsg ? composeMsg.toString() : '0x', + oftCmd: '0x', + } + + // 6️⃣ 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..6875e2783b --- /dev/null +++ b/examples/mint-burn-oft-adapter/tasks/sendOFT.ts @@ -0,0 +1,88 @@ +import { task, types } from 'hardhat/config' +import { HardhatRuntimeEnvironment } from 'hardhat/types' + +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 + /** Minimum amount to receive in case of custom slippage or fees (human readable units, e.g. "1.5") */ + minAmount?: string + /** Extra options for sending additional gas units to lzReceive, lzCompose, or receiver address */ + extraOptions?: 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( + 'minAmount', + 'Minimum amount to receive in case of custom slippage or fees (human readable units, e.g. "1.5")', + undefined, + types.string + ) + .addOptionalParam( + 'extraOptions', + 'Extra options for sending additional gas units to lzReceive, lzCompose, or receiver address', + 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/tasks/sendEvm.ts b/examples/oft/tasks/sendEvm.ts index 5b8ce871e9..4c9118d645 100644 --- a/examples/oft/tasks/sendEvm.ts +++ b/examples/oft/tasks/sendEvm.ts @@ -76,7 +76,7 @@ export async function sendEvm( const amountUnits: BigNumber = parseUnits(amount, decimals) // hex string → Uint8Array → zero-pad to 32 bytes - const toBytes = addressToBytes32(to).toString() + const toBytes = addressToBytes32(to) // 6️⃣ build sendParam and dispatch const sendParam = { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 663063ae4f..b57fea8a84 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 From 095853e854a1b165ebe2ba2e37336092e6b508b7 Mon Sep 17 00:00:00 2001 From: Krak Date: Tue, 10 Jun 2025 13:28:26 -0700 Subject: [PATCH 07/14] fix: update no resolve for eslint --- examples/mint-burn-oft-adapter/.eslintrc.js | 1 + 1 file changed, 1 insertion(+) 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 }, }; From eef42fee26af22349e3582546c8b7786e6b12693 Mon Sep 17 00:00:00 2001 From: Krak Date: Tue, 10 Jun 2025 13:33:28 -0700 Subject: [PATCH 08/14] fix: reference correct config file --- examples/mint-burn-oft-adapter/tasks/sendEvm.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/mint-burn-oft-adapter/tasks/sendEvm.ts b/examples/mint-burn-oft-adapter/tasks/sendEvm.ts index 89a3d6ef71..4c9118d645 100644 --- a/examples/mint-burn-oft-adapter/tasks/sendEvm.ts +++ b/examples/mint-burn-oft-adapter/tasks/sendEvm.ts @@ -7,7 +7,7 @@ import { createLogger } from '@layerzerolabs/io-devtools' import { ChainType, endpointIdToChainType, endpointIdToNetwork } from '@layerzerolabs/lz-definitions' import { addressToBytes32 } from '@layerzerolabs/lz-v2-utilities' -import layerzeroConfig from '../layerzero.config-test' +import layerzeroConfig from '../layerzero.config' import { SendResult } from './types' import { DebugLogger, KnownErrors, getLayerZeroScanLink } from './utils' From fba883055c0a16245f795709c0505c0293abe1ef Mon Sep 17 00:00:00 2001 From: Krak Date: Wed, 2 Jul 2025 11:04:54 +0200 Subject: [PATCH 09/14] chore: add optional gas params --- examples/oft/tasks/sendEvm.ts | 117 ++++++++++++++++++++++++++++++---- examples/oft/tasks/sendOFT.ts | 30 +++++++-- 2 files changed, 130 insertions(+), 17 deletions(-) diff --git a/examples/oft/tasks/sendEvm.ts b/examples/oft/tasks/sendEvm.ts index 4c9118d645..8b42fb82a2 100644 --- a/examples/oft/tasks/sendEvm.ts +++ b/examples/oft/tasks/sendEvm.ts @@ -1,13 +1,13 @@ +import path from 'path' + import { BigNumber, ContractTransaction } from 'ethers' import { parseUnits } from 'ethers/lib/utils' import { HardhatRuntimeEnvironment } from 'hardhat/types' -import { createGetHreByEid } from '@layerzerolabs/devtools-evm-hardhat' +import { OmniPointHardhat, createGetHreByEid } from '@layerzerolabs/devtools-evm-hardhat' import { createLogger } from '@layerzerolabs/io-devtools' import { ChainType, endpointIdToChainType, endpointIdToNetwork } from '@layerzerolabs/lz-definitions' -import { addressToBytes32 } from '@layerzerolabs/lz-v2-utilities' - -import layerzeroConfig from '../layerzero.config' +import { Options, addressToBytes32 } from '@layerzerolabs/lz-v2-utilities' import { SendResult } from './types' import { DebugLogger, KnownErrors, getLayerZeroScanLink } from './utils' @@ -19,14 +19,29 @@ export interface EvmArgs { dstEid: number amount: string to: string + oappConfig: string minAmount?: string - extraOptions?: string + extraLzReceiveOptions?: string[] + extraLzComposeOptions?: string[] + extraNativeDropOptions?: string[] composeMsg?: string oftAddress?: string } export async function sendEvm( - { srcEid, dstEid, amount, to, minAmount, extraOptions, composeMsg, oftAddress }: EvmArgs, + { + srcEid, + dstEid, + amount, + to, + oappConfig, + minAmount, + extraLzReceiveOptions, + extraLzComposeOptions, + extraNativeDropOptions, + composeMsg, + oftAddress, + }: EvmArgs, hre: HardhatRuntimeEnvironment ): Promise { if (endpointIdToChainType(srcEid) !== ChainType.EVM) { @@ -51,12 +66,13 @@ export async function sendEvm( if (oftAddress) { wrapperAddress = oftAddress } else { - const { contracts } = typeof layerzeroConfig === 'function' ? await layerzeroConfig() : layerzeroConfig - const wrapper = contracts.find((c) => c.contract.eid === srcEid) + 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! + : wrapper.contract.address || '' } // 2️⃣ load IOFT ABI, extend it with token() @@ -78,18 +94,95 @@ export async function sendEvm( // hex string → Uint8Array → zero-pad to 32 bytes const toBytes = addressToBytes32(to) - // 6️⃣ build sendParam and dispatch + // 6️⃣ 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() + + // 7️⃣ build sendParam and dispatch const sendParam = { dstEid, to: toBytes, amountLD: amountUnits.toString(), minAmountLD: minAmount ? parseUnits(minAmount, decimals).toString() : amountUnits.toString(), - extraOptions: extraOptions ? extraOptions.toString() : '0x', + extraOptions: extraOptions, composeMsg: composeMsg ? composeMsg.toString() : '0x', oftCmd: '0x', } - // 6️⃣ Quote (MessagingFee = { nativeFee, lzTokenFee }) + // 8️⃣ Quote (MessagingFee = { nativeFee, lzTokenFee }) logger.info('Quoting the native gas cost for the send transaction...') let msgFee: { nativeFee: BigNumber; lzTokenFee: BigNumber } try { diff --git a/examples/oft/tasks/sendOFT.ts b/examples/oft/tasks/sendOFT.ts index 6875e2783b..32101a05af 100644 --- a/examples/oft/tasks/sendOFT.ts +++ b/examples/oft/tasks/sendOFT.ts @@ -1,6 +1,7 @@ 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' @@ -12,10 +13,15 @@ interface MasterArgs { 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 - /** Extra options for sending additional gas units to lzReceive, lzCompose, or receiver address */ - extraOptions?: 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 */ @@ -27,6 +33,7 @@ task('lz:oft:send', 'Sends OFT tokens cross‐chain from EVM chains') .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")', @@ -34,11 +41,24 @@ task('lz:oft:send', 'Sends OFT tokens cross‐chain from EVM chains') types.string ) .addOptionalParam( - 'extraOptions', - 'Extra options for sending additional gas units to lzReceive, lzCompose, or receiver address', + 'extraLzReceiveOptions', + 'Array of extra lzReceive options in format "gas,value" (e.g. ["200000,0", "100000,1000000000000000000"])', undefined, - types.string + 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)', From 1280dee60dc19d59c8a829fc4d241b67eae65b9a Mon Sep 17 00:00:00 2001 From: Krak Date: Wed, 2 Jul 2025 11:10:37 +0200 Subject: [PATCH 10/14] chore: add adapter checks for approval --- examples/oft/tasks/sendEvm.ts | 34 ++++++++++++++++++++++++++++++---- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/examples/oft/tasks/sendEvm.ts b/examples/oft/tasks/sendEvm.ts index 8b42fb82a2..7a9cf98959 100644 --- a/examples/oft/tasks/sendEvm.ts +++ b/examples/oft/tasks/sendEvm.ts @@ -91,10 +91,36 @@ export async function sendEvm( // 5️⃣ normalize the user-supplied amount const amountUnits: BigNumber = parseUnits(amount, decimals) - // hex string → Uint8Array → zero-pad to 32 bytes + // 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) - // 6️⃣ Build options dynamically using Options.newOptions() + // 8️⃣ Build options dynamically using Options.newOptions() let options = Options.newOptions() // Add lzReceive options @@ -171,7 +197,7 @@ export async function sendEvm( const extraOptions = options.toHex() - // 7️⃣ build sendParam and dispatch + // 9️⃣ build sendParam and dispatch const sendParam = { dstEid, to: toBytes, @@ -182,7 +208,7 @@ export async function sendEvm( oftCmd: '0x', } - // 8️⃣ Quote (MessagingFee = { nativeFee, lzTokenFee }) + // 10️⃣ Quote (MessagingFee = { nativeFee, lzTokenFee }) logger.info('Quoting the native gas cost for the send transaction...') let msgFee: { nativeFee: BigNumber; lzTokenFee: BigNumber } try { From eacea32b96b001288df5137c8c577dd9028d206e Mon Sep 17 00:00:00 2001 From: Krak Date: Wed, 2 Jul 2025 11:15:15 +0200 Subject: [PATCH 11/14] chore: add changes to mintburnoftadapter --- .../mint-burn-oft-adapter/tasks/sendEvm.ts | 145 ++++++++++++++++-- .../mint-burn-oft-adapter/tasks/sendOFT.ts | 31 +++- 2 files changed, 158 insertions(+), 18 deletions(-) diff --git a/examples/mint-burn-oft-adapter/tasks/sendEvm.ts b/examples/mint-burn-oft-adapter/tasks/sendEvm.ts index 4c9118d645..7a9cf98959 100644 --- a/examples/mint-burn-oft-adapter/tasks/sendEvm.ts +++ b/examples/mint-burn-oft-adapter/tasks/sendEvm.ts @@ -1,13 +1,13 @@ +import path from 'path' + import { BigNumber, ContractTransaction } from 'ethers' import { parseUnits } from 'ethers/lib/utils' import { HardhatRuntimeEnvironment } from 'hardhat/types' -import { createGetHreByEid } from '@layerzerolabs/devtools-evm-hardhat' +import { OmniPointHardhat, createGetHreByEid } from '@layerzerolabs/devtools-evm-hardhat' import { createLogger } from '@layerzerolabs/io-devtools' import { ChainType, endpointIdToChainType, endpointIdToNetwork } from '@layerzerolabs/lz-definitions' -import { addressToBytes32 } from '@layerzerolabs/lz-v2-utilities' - -import layerzeroConfig from '../layerzero.config' +import { Options, addressToBytes32 } from '@layerzerolabs/lz-v2-utilities' import { SendResult } from './types' import { DebugLogger, KnownErrors, getLayerZeroScanLink } from './utils' @@ -19,14 +19,29 @@ export interface EvmArgs { dstEid: number amount: string to: string + oappConfig: string minAmount?: string - extraOptions?: string + extraLzReceiveOptions?: string[] + extraLzComposeOptions?: string[] + extraNativeDropOptions?: string[] composeMsg?: string oftAddress?: string } export async function sendEvm( - { srcEid, dstEid, amount, to, minAmount, extraOptions, composeMsg, oftAddress }: EvmArgs, + { + srcEid, + dstEid, + amount, + to, + oappConfig, + minAmount, + extraLzReceiveOptions, + extraLzComposeOptions, + extraNativeDropOptions, + composeMsg, + oftAddress, + }: EvmArgs, hre: HardhatRuntimeEnvironment ): Promise { if (endpointIdToChainType(srcEid) !== ChainType.EVM) { @@ -51,12 +66,13 @@ export async function sendEvm( if (oftAddress) { wrapperAddress = oftAddress } else { - const { contracts } = typeof layerzeroConfig === 'function' ? await layerzeroConfig() : layerzeroConfig - const wrapper = contracts.find((c) => c.contract.eid === srcEid) + 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! + : wrapper.contract.address || '' } // 2️⃣ load IOFT ABI, extend it with token() @@ -75,21 +91,124 @@ export async function sendEvm( // 5️⃣ normalize the user-supplied amount const amountUnits: BigNumber = parseUnits(amount, decimals) - // hex string → Uint8Array → zero-pad to 32 bytes + // 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) - // 6️⃣ build sendParam and dispatch + // 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 ? extraOptions.toString() : '0x', + extraOptions: extraOptions, composeMsg: composeMsg ? composeMsg.toString() : '0x', oftCmd: '0x', } - // 6️⃣ Quote (MessagingFee = { nativeFee, lzTokenFee }) + // 10️⃣ Quote (MessagingFee = { nativeFee, lzTokenFee }) logger.info('Quoting the native gas cost for the send transaction...') let msgFee: { nativeFee: BigNumber; lzTokenFee: BigNumber } try { diff --git a/examples/mint-burn-oft-adapter/tasks/sendOFT.ts b/examples/mint-burn-oft-adapter/tasks/sendOFT.ts index 6875e2783b..ae1f061c1b 100644 --- a/examples/mint-burn-oft-adapter/tasks/sendOFT.ts +++ b/examples/mint-burn-oft-adapter/tasks/sendOFT.ts @@ -1,6 +1,7 @@ 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' @@ -12,10 +13,16 @@ interface MasterArgs { 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 - /** Extra options for sending additional gas units to lzReceive, lzCompose, or receiver address */ - extraOptions?: 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 */ @@ -27,6 +34,7 @@ task('lz:oft:send', 'Sends OFT tokens cross‐chain from EVM chains') .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")', @@ -34,11 +42,24 @@ task('lz:oft:send', 'Sends OFT tokens cross‐chain from EVM chains') types.string ) .addOptionalParam( - 'extraOptions', - 'Extra options for sending additional gas units to lzReceive, lzCompose, or receiver address', + 'extraLzReceiveOptions', + 'Array of lzReceive options as comma-separated values "gas,value"', undefined, - types.string + 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)', From 9606db58b88802e9a67923b0f10f2684605061d8 Mon Sep 17 00:00:00 2001 From: Krak Date: Wed, 2 Jul 2025 11:32:14 +0200 Subject: [PATCH 12/14] chore: add task to oft-adapter example --- .changeset/tricky-spies-dream.md | 5 + examples/oft-adapter/hardhat.config.ts | 1 + examples/oft-adapter/package.json | 2 + examples/oft-adapter/tasks/sendEvm.ts | 242 +++++++++++++++++++++++++ examples/oft-adapter/tasks/sendOFT.ts | 109 +++++++++++ examples/oft-adapter/tasks/types.ts | 4 + examples/oft-adapter/tasks/utils.ts | 51 ++++++ pnpm-lock.yaml | 6 + 8 files changed, 420 insertions(+) create mode 100644 .changeset/tricky-spies-dream.md create mode 100644 examples/oft-adapter/tasks/sendEvm.ts create mode 100644 examples/oft-adapter/tasks/sendOFT.ts create mode 100644 examples/oft-adapter/tasks/types.ts create mode 100644 examples/oft-adapter/tasks/utils.ts 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/oft-adapter/hardhat.config.ts b/examples/oft-adapter/hardhat.config.ts index b1fbe86f84..82aad1d50b 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 // diff --git a/examples/oft-adapter/package.json b/examples/oft-adapter/package.json index f9f61750f0..27f32aba0e 100644 --- a/examples/oft-adapter/package.json +++ b/examples/oft-adapter/package.json @@ -22,7 +22,9 @@ }, "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", 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/pnpm-lock.yaml b/pnpm-lock.yaml index b57fea8a84..6f5f4e8db8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1031,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 From 1447855598f977320853f517262bc9f6032ff33b Mon Sep 17 00:00:00 2001 From: Krak Date: Wed, 2 Jul 2025 12:03:36 +0200 Subject: [PATCH 13/14] chore: add metadata tools to oft-adapter repo --- examples/oft-adapter/.eslintrc.js | 1 + examples/oft-adapter/hardhat.config.ts | 14 ++-- examples/oft-adapter/layerzero.config.ts | 101 +++++++++++++---------- examples/oft-adapter/package.json | 1 + pnpm-lock.yaml | 3 + 5 files changed, 69 insertions(+), 51 deletions(-) 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/hardhat.config.ts b/examples/oft-adapter/hardhat.config.ts index 82aad1d50b..13fb2653a9 100644 --- a/examples/oft-adapter/hardhat.config.ts +++ b/examples/oft-adapter/hardhat.config.ts @@ -55,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 @@ -65,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 27f32aba0e..6ef0ae2a9b 100644 --- a/examples/oft-adapter/package.json +++ b/examples/oft-adapter/package.json @@ -30,6 +30,7 @@ "@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/pnpm-lock.yaml b/pnpm-lock.yaml index 6f5f4e8db8..5eef19b208 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1055,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 From 6e15074a71a55c4a6ae698cf82d0c541a056b02e Mon Sep 17 00:00:00 2001 From: Krak Date: Wed, 2 Jul 2025 12:17:36 +0200 Subject: [PATCH 14/14] chore: add mockerc20 deployment task --- examples/oft-adapter/deploy/MyERC20Mock.ts | 49 ++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 examples/oft-adapter/deploy/MyERC20Mock.ts 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