Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
079695f
Enhance EVM send script and Aptos module with message encoding and de…
AlexanderLiteplo Jun 18, 2025
6d69496
Refactor Aptos module and utility functions for improved message hand…
AlexanderLiteplo Jun 20, 2025
2bcd718
Add script to analyze decoded values from Aptos OApp
AlexanderLiteplo Jun 20, 2025
fea0b63
Remove redundant logging and analysis from `analyze-decoded-values.ts…
AlexanderLiteplo Jun 20, 2025
99d5161
Update `analyze-decoded-values.ts` to correct function calls for retr…
AlexanderLiteplo Jun 20, 2025
7cf5134
Refactor `evm-send.ts` and `utils.move` for clarity and efficiency
AlexanderLiteplo Jun 20, 2025
b20792d
Add parameter encoding and decoding to Move OApp example
AlexanderLiteplo Jun 20, 2025
c3a6dfb
Update README and scripts for improved cross-chain message handling
AlexanderLiteplo Jun 20, 2025
a101b83
lock file
AlexanderLiteplo Jun 20, 2025
1afd8fb
Merge branch 'main' into move-oapp-example
AlexanderLiteplo Jun 20, 2025
e45c4d6
Fix environment variable names and update OApp address in `aptos-move…
AlexanderLiteplo Jun 20, 2025
2eb12f8
smol improvements
AlexanderLiteplo Jun 20, 2025
d1ae147
pnpm lock
AlexanderLiteplo Jun 20, 2025
51efec8
Remove redundant test case for message sending functionality in `MyOA…
AlexanderLiteplo Jun 20, 2025
d584ae1
Enhance cross-chain message handling in `aptos-move-send.ts` and `evm…
AlexanderLiteplo Jun 20, 2025
40527e3
Update `evm-send.ts` to use empty placeholders for addresses and number
AlexanderLiteplo Jun 23, 2025
9d32c14
Remove unused dependency `@layerzerolabs/devtools` from `pnpm-lock.ya…
AlexanderLiteplo Jun 23, 2025
599dc1e
Update examples/oapp-aptos-move/contracts/MyOApp.sol
AlexanderLiteplo Jun 23, 2025
08b767c
correcting licenses
AlexanderLiteplo Jun 23, 2025
0455dd6
Merge branch 'move-oapp-example' of https://github.com/LayerZero-Labs…
AlexanderLiteplo Jun 23, 2025
5a33bf4
Update comments in MyOApp.sol for clarity on unused parameters and me…
AlexanderLiteplo Jun 23, 2025
c07d128
Refactor environment variable validation in `aptos-move-send.ts`
AlexanderLiteplo Jun 23, 2025
e3c2b7d
Refactor `oapp.move` and `utils.move` for code simplification and cla…
AlexanderLiteplo Jun 23, 2025
f5f2ce7
Add message parsing function and improve error code organization in o…
AlexanderLiteplo Jun 23, 2025
7a3a2a0
Refactor address and number extraction in `oapp.move` for improved cl…
AlexanderLiteplo Jun 24, 2025
b9a90a5
Refactor `parse_message` function in `oapp.move` for enhanced efficie…
AlexanderLiteplo Jun 24, 2025
3aba4bc
pnpm lock
AlexanderLiteplo Jun 24, 2025
379fafc
package.json adding devtools
AlexanderLiteplo Jun 24, 2025
f01ca5e
Refactor imports in `oapp.move`, `utils.move`, and `oapp_tests.move` …
AlexanderLiteplo Jun 24, 2025
4069f91
Refactor `MyOApp.sol` and related scripts for improved data handling …
AlexanderLiteplo Jun 24, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .changeset/purple-apes-yell.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
"@layerzerolabs/oft-adapter-aptos-move-example": patch
"@layerzerolabs/mint-burn-oft-adapter-example": patch
"@layerzerolabs/native-oft-adapter-example": patch
"@layerzerolabs/oft-adapter-initia-example": patch
"@layerzerolabs/oapp-aptos-example": patch
"@layerzerolabs/oft-hyperliquid-example": patch
"@layerzerolabs/oft-upgradeable-example": patch
"@layerzerolabs/oft-aptos-move-example": patch
"@layerzerolabs/onft721-zksync-example": patch
"@layerzerolabs/oft-adapter-example": patch
"@layerzerolabs/oft-initia-example": patch
"@layerzerolabs/oft-solana-example": patch
"@layerzerolabs/omni-call-example": patch
"@layerzerolabs/oft-alt-example": patch
"@layerzerolabs/onft721-example": patch
"@layerzerolabs/oapp-example": patch
"@layerzerolabs/oft-example": patch
---

Updating license from UNLICENSED to MIT.
5 changes: 5 additions & 0 deletions .changeset/weak-crabs-chew.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@layerzerolabs/oapp-aptos-example": patch
---

Adding param encoding and decoding to Move OApp example.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// SPDX-License-Identifier: UNLICENSED
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.22;

import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";
Expand Down
2 changes: 1 addition & 1 deletion examples/mint-burn-oft-adapter/contracts/MyOFT.sol
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// SPDX-License-Identifier: UNLICENSED
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.22;

import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// SPDX-License-Identifier: UNLICENSED
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.22;

import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";
Expand Down
2 changes: 1 addition & 1 deletion examples/native-oft-adapter/contracts/MyOFT.sol
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// SPDX-License-Identifier: UNLICENSED
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.22;

import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";
Expand Down
54 changes: 50 additions & 4 deletions examples/oapp-aptos-move/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,15 +96,19 @@ APTOS_PRIVATE_KEY=<your-aptos-private-key>

Then run `source .env` in order for your values to be mapped.

> **Important:** If using Aptos CLI version >= 6.1.0 (required for Aptos chain), you need to uncomment the following lines in Move.toml and remove the existing AptosFramework dependency:
> **Important:** If using Aptos CLI version 3.5.0 (required for Movement chain), you need to uncomment the following lines in Move.toml and remove the existing AptosFramework dependency:
>
> ```
> # [dependencies.AptosFramework]
> # git = "https://github.com/aptos-labs/aptos-framework.git"
> # rev = "mainnet"
> # subdir = "aptos-framework"
> # git = "https://github.com/movementlabsxyz/aptos-core.git"
> # rev = "movement-cli-v3.5.0"
> # subdir = "aptos-move/framework/aptos-framework"
> ```

## Contracts

The starter OApp contracts are located in `./sources/oapp.move` and `./contracts/MyOApp.sol`. Feel free to adjust these based on the needs of you application. There are also some test scripts in `./scripts/` that allow you to test sending messages between your OApps.

## OApp Config Setup

Before running the deploy and wire commands, first inside of `move.layerzero.config.ts`, set the delegate and owner address to your deployer account address. These can be changed in the future with commands shown later in this README, but for now they should be set to the address you will be running the commands from (deployer account address).
Expand Down Expand Up @@ -207,6 +211,48 @@ If `--only-calldata <true>` is specified, only the calldata is generated and not
pnpm run lz:sdk:move:wire --oapp-config move.layerzero.config.ts
```

### Testing Send

This OApp example includes several test scripts in `./scripts`. **Before running any script, you must manually update the values inside each file with your deployed contract addresses and configuration.**

For EVM scripts (`evm-send.ts`, `evm-get-message.ts`), update the following values inside the files:

- `contractAddress` with your deployed EVM contract address
- RPC URL in the `JsonRpcProvider` with your EVM chain's RPC endpoint
- Set `EVM_PRIVATE_KEY` environment variable for sending transactions

For Aptos/Movement scripts (`aptos-move-send.ts`, `aptos-get-receive-values.ts`), update the following values inside the files:

- `OAPP_ADDRESS` or `oappAddress` with your deployed Aptos/Movement contract address
- Set `APTOS_PRIVATE_KEY` and `ACCOUNT_ADDRESS` environment variables for sending transactions
- Adjust the `Network` configuration if needed (TESTNET/MAINNET)

To test sending from your EVM deployment to your deployed OApp on Aptos or Movement, run:

```bash
ts-node scripts/evm-send.ts
```

For demonstration purposes, we have added encoding and decoding of some useful parameters in `oapp.move` and `evm-send.ts`. These are for demonstration purposes only and should be adjusted based on the needs of your application.

To confirm your values have been sent to your Aptos or Movement OApp, run:

```bash
ts-node scripts/aptos-get-received-values.ts
```

To send a test value from your Aptos/Movement OApp to your EVM OApp, run:

```bash
ts-node scripts/aptos-move-send.ts
```

To check that your message has been registered in your EVM OApp, run:

```bash
ts-node scripts/evm-get-received-message.ts
```

### Transferring Ownership of your Move OApp

There are three steps to transferring ownership of your Move OApp:
Expand Down
133 changes: 91 additions & 42 deletions examples/oapp-aptos-move/contracts/MyOApp.sol
Original file line number Diff line number Diff line change
@@ -1,34 +1,31 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.22;

import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";
import { OApp, MessagingFee, Origin } from "@layerzerolabs/oapp-evm/contracts/oapp/OApp.sol";
import { MessagingReceipt } from "@layerzerolabs/oapp-evm/contracts/oapp/OAppSender.sol";
import { OApp, Origin, MessagingFee } from "@layerzerolabs/oapp-evm/contracts/oapp/OApp.sol";
import { OAppOptionsType3 } from "@layerzerolabs/oapp-evm/contracts/oapp/libs/OAppOptionsType3.sol";
import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";

contract MyOApp is OApp, OAppOptionsType3 {
constructor(address _endpoint, address _delegate) OApp(_endpoint, _delegate) Ownable(_delegate) {}
/// @notice Last string received from any remote chain

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this comment seems inaccurate

it mentions "last string received"

while below there are three variables: address, address, uint256 and none of them is string

maybe better to change comment to "last data received"?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is taken directly from the EVM OApp example (that devrel wrote) I'm just updating these files with their implementation. If you want me to change it across all examples I can do that, but it should be a separate PR.

address public address1;
address public address2;
uint256 public num;

string public data = "Nothing received yet.";
uint256 public counter = 0;
/// @notice The only Message Type in use for this OApp: sending an arbitrary string.
/// Different message types can be assigned different enforced options per destination endpoint ID.
uint16 public constant SEND = 1;

/**
* @notice Sends a message from the source chain to a destination chain.
* @param _dstEid The endpoint ID of the destination chain.
* @param _message The message string to be sent.
* @param _options Additional options for message execution.
* @dev Encodes the message as bytes and sends it using the `_lzSend` internal function.
* @return receipt A `MessagingReceipt` struct containing details of the message sent.
*/
function send(
uint32 _dstEid,
string memory _message,
bytes calldata _options
) external payable returns (MessagingReceipt memory receipt) {
bytes memory _payload = abi.encode(_message);
receipt = _lzSend(_dstEid, _payload, _options, MessagingFee(msg.value, 0), payable(msg.sender));
}
/// @notice Initialize with Endpoint V2 and owner address
/// @param _endpoint The local chain's LayerZero Endpoint V2 address
/// @param _owner The address permitted to configure this OApp
constructor(address _endpoint, address _owner) OApp(_endpoint, _owner) Ownable(_owner) {}

// ──────────────────────────────────────────────────────────────────────────────
// 0. (Optional) Quote business logic
//
// Example: Get a quote from the Endpoint for a cost estimate of sending a message.
// Replace this to mirror your own send business logic.
// ──────────────────────────────────────────────────────────────────────────────

/**
* @notice Quotes the gas needed to pay for the full omnichain transaction in native gas or ZRO token.
Expand All @@ -38,35 +35,87 @@ contract MyOApp is OApp, OAppOptionsType3 {
* @param _payInLzToken Whether to return fee in ZRO token.
* @return fee A `MessagingFee` struct containing the calculated gas fee in either the native token or ZRO token.
*/
function quote(
function quoteSend(
uint32 _dstEid,
string memory _message,
bytes memory _options,
string calldata _message,
bytes calldata _options,
bool _payInLzToken
) public view returns (MessagingFee memory fee) {
bytes memory payload = abi.encode(_message);
fee = _quote(_dstEid, payload, _options, _payInLzToken);
bytes memory _message = abi.encode(_message);
// combineOptions (from OAppOptionsType3) merges enforced options set by the contract owner
// with any additional execution options provided by the caller
fee = _quote(_dstEid, _message, combineOptions(_dstEid, SEND, _options), _payInLzToken);
}

/**
* @dev Internal function override to handle incoming messages from another chain.
* @dev _origin A struct containing information about the message sender.
* @dev _guid A unique global packet identifier for the message.
* @dev payload The encoded message payload being received.
*
* @dev The following params are unused in the current implementation of the OApp.
* @dev _executor The address of the Executor responsible for processing the message.
* @dev _extraData Arbitrary data appended by the Executor to the message.
*
* Increments the counter.
*/
// ──────────────────────────────────────────────────────────────────────────────
// 1. Send business logic
//
// Example: send a simple string to a remote chain. Replace this with your
// own state-update logic, then encode whatever data your application needs.
// ──────────────────────────────────────────────────────────────────────────────

/// @notice Send a string to a remote OApp on another chain
/// @param _dstEid Destination Endpoint ID (uint32)
/// @param _message The string to send
/// @param _options Execution options for gas on the destination (bytes)
function send(uint32 _dstEid, string calldata _message, bytes calldata _options) external payable {
// 1. (Optional) Update any local state here.
// e.g., record that a message was "sent":
// sentCount += 1;

// 2. Encode any data structures you wish to send into bytes
// You can use abi.encode, abi.encodePacked, or directly splice bytes
// if you know the format of your data structures
bytes memory _message = abi.encode(_message);

// 3. Call OAppSender._lzSend to package and dispatch the cross-chain message
// - _dstEid: remote chain's Endpoint ID
// - _message: ABI-encoded string
// - _options: combined execution options (enforced + caller-provided)
// - MessagingFee(msg.value, 0): pay all gas as native token; no ZRO
// - payable(msg.sender): refund excess gas to caller
//
// combineOptions (from OAppOptionsType3) merges enforced options set by the contract owner
// with any additional execution options provided by the caller
_lzSend(
_dstEid,
_message,
combineOptions(_dstEid, SEND, _options),
MessagingFee(msg.value, 0),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is an anti pattern to force users to use native gas for paying fee and hardcoding it in the code.

This makes it impossible in future to pay with ZRO token, I don't think we want to encourage users to follow this pattern and disable it in their OApps they are building on top of this example.

Instead, send method should accept MessagingFee struct, like OFTCore: https://github.com/LayerZero-Labs/devtools/blob/main/packages/oft-evm/contracts/OFTCore.sol#L177

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is taken directly from the EVM OApp example (that devrel wrote) I'm just updating these files with their implementation. If you want me to change it across all examples I can do that, but it should be a separate PR.

payable(msg.sender)
);
}

// ──────────────────────────────────────────────────────────────────────────────
// 2. Receive business logic
//
// Override _lzReceive to decode the incoming bytes and apply your logic.
// The base OAppReceiver.lzReceive ensures:
// • Only the LayerZero Endpoint can call this method
// • The sender is a registered peer (peers[srcEid] == origin.sender)
// ──────────────────────────────────────────────────────────────────────────────

/// @notice Invoked by OAppReceiver when EndpointV2.lzReceive is called
/// @dev _origin Metadata (source chain, sender address, nonce)
/// @dev _guid Global unique ID for tracking this message
/// @param _message ABI-encoded bytes (the string we sent earlier)
/// @dev _executor Executor address that delivered the message
/// @dev _extraData Additional data from the Executor (unused by the LayerZero executor)
function _lzReceive(
Origin calldata /*_origin*/,
bytes32 /*_guid*/,
bytes calldata /*payload*/,
bytes calldata _message,
address /*_executor*/,
bytes calldata /*_extraData*/
) internal override {
counter += 1;
// 1. Decode the incoming bytes into a string
// You can use abi.decode, abi.decodePacked, or directly splice bytes
// if you know the format of your data structures
(address1, address2, num) = abi.decode(_message, (address, address, uint256));

@DanL0 DanL0 Jun 26, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does it work if you are currently encoding a "string" on send:

bytes memory _message = abi.encode(_message);

and on receive you are decoding "address, address, uint"?:

(address1, address2, num) = abi.decode(_message, (address, address, uint256));

I did a Foundry small test and I think if you currently do abi.encode offchain, pass it onchain and then do abi.encode again in Solidity before sending it results in wrong data:

function test_msg_passing() public virtual {
        bytes memory encoded = abi.encode(string(abi.encode(address(1), address(2), 100)));
        console.logBytes(encoded);

        (address address1, address address2, uint256 num) = abi.decode(encoded, (address, address, uint256));
        console.log(address1);
        console.log(address2);
        console.log(num);
    }

result:

Logs:
  0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000064
  0x0000000000000000000000000000000000000020
  0x0000000000000000000000000000000000000060
  1

Expected:

0x000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000064
  0x0000000000000000000000000000000000000001
  0x0000000000000000000000000000000000000002
  100

Could you create a test case in Foundry for sending messages from EVM to EVM using this OApp Solidity code and double check if the encoding and decoding logic is consistent and works?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good. Putting this on pause until sla-tracker work is finished.


// 2. Apply your custom logic. In this example, store it in `lastMessage`.
// 3. (Optional) Trigger further on-chain actions.
// e.g., emit an event, mint tokens, call another contract, etc.
// emit MessageReceived(_origin.srcEid, _message);
}
}
4 changes: 2 additions & 2 deletions examples/oapp-aptos-move/move.layerzero.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ const config: OAppOmniGraphHardhat = {
},
ulnConfig: {
// The number of block confirmations to wait on Aptos before emitting the message from the source chain.
confirmations: BigInt(260),
confirmations: BigInt(10),
// The address of the DVNs you will pay to verify a sent message on the source chain.
// The destination tx will wait until ALL `requiredDVNs` verify the message.
requiredDVNs: ['0x756f8ab056688d22687740f4a9aeec3b361170b28d08b719e28c4d38eed1043e'],
Expand Down Expand Up @@ -143,7 +143,7 @@ const config: OAppOmniGraphHardhat = {
},
receiveConfig: {
ulnConfig: {
confirmations: BigInt(260),
confirmations: BigInt(10),
requiredDVNs: ['0x0eE552262f7B562eFcED6DD4A7e2878AB897d405'],
optionalDVNThreshold: 0,
},
Expand Down
57 changes: 57 additions & 0 deletions examples/oapp-aptos-move/scripts/aptos-get-received-values.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { Aptos, AptosConfig, Network } from '@aptos-labs/ts-sdk'

/**
* A utility script to verify cross-chain message delivery by checking the last received values.
* The values are updated each time a message is successfully received by the OApp,
* providing a simple way to confirm that cross-chain communication is working as expected.
*/
async function main() {
const config = new AptosConfig({ network: Network.TESTNET })
const aptos = new Aptos(config)
const oappAddress = '<your-oapp-address>'

console.log('Fetching receive values...')
console.log('---')

const [counterResult, address1Result, address2Result, numberResult, rawMessageResult] = await Promise.all([
aptos.view({
payload: {
function: `${oappAddress}::oapp::get_counter_value`,
typeArguments: [],
},
}),
aptos.view({
payload: {
function: `${oappAddress}::oapp::get_decoded_address1`,
typeArguments: [],
},
}),
aptos.view({
payload: {
function: `${oappAddress}::oapp::get_decoded_address2`,
typeArguments: [],
},
}),
aptos.view({
payload: {
function: `${oappAddress}::oapp::get_decoded_number`,
typeArguments: [],
},
}),
aptos.view({
payload: {
function: `${oappAddress}::oapp::get_raw_message`,
typeArguments: [],
},
}),
])

console.log('- Counter:', counterResult[0])
console.log('- Address 1:', address1Result[0])
console.log('- Address 2:', address2Result[0])
console.log('- Number:', numberResult[0])
console.log('- Raw Message:', rawMessageResult[0])
console.log('---')
}

main().catch(console.error)
Loading