N3 to EVM
This document details the complete flow for sending messages from N3 to Neo X (EVM blockchain) using the example scenario of querying a Neo token's balance on EVM from N3 and receiving the result back.
Overview
The message bridge allows N3 applications to execute operations on EVM-based blockchains and receive results back.
This is accomplished by encoding an AMBTypes.Call
struct and sending the encoded bytes to the N3 message bridge contract. The decoupled relayer then ensures safe transfer of the data to the EVM chain. Once the data has been bridged, the message can be executed on the EVM chain and its result may be returned.
Currently, BaneLabs does not provide a service to execute messages. In future versions execution rewards might be added to messages, so that users can attach an incentive for anyone active on the destination network to execute a message for them.
Complete Flow Steps
In the following we'll use an example call to elaborate on the flow. For simplicity, let's assume we want to get the Neo balance of an account on Neo X.
As the bridging process is an asynchronous process, fetching a value that can be fluctuent like a Neo balance might not be the most intuitive use case. However, for the purpose of this example it should sufficiently elaborate on the steps required to use the message bridge.
Step 1: Prepare the EVM Call Structure
First, prepare the EVM call by creating an AMBTypes.Call
structure on the N3 side using the ethers library for proper ABI encoding:
const { ethers } = require('ethers');
// Define the Neo token contract address on EVM and target address
const neoTokenContractAddress = "0xc28736dc83f4fd43d6fb832Fd93c3eE7bB26828f"; // Neo token address on Neo X Testnet
const targetAddress = "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd"; // Address to check the balance for
// Create the interface for ERC20 balanceOf function
const erc20Interface = new ethers.Interface([
"function balanceOf(address account) view returns (uint256)"
]);
// Encode the balanceOf function call
const callData = erc20Interface.encodeFunctionData("balanceOf", [targetAddress]);
console.log("Encoded balanceOf callData:", callData);
// Output: 0x70a08231000000000000000000000000abcdefabcdefabcdefabcdefabcdefabcdefabcd
// Create the EVM Call structure (equivalent to AMBTypes.Call)
const evmCall = {
target: neoTokenContractAddress, // Target EVM Neo token contract
allowFailure: false, // Don't allow failure
value: 0, // No gas value needed for the call
callData: callData // Encoded balanceOf(address) call data
};
// Serialize the entire Call structure using ethers ABI encoder
const callStructAbi = [
"tuple(address target, bool allowFailure, uint256 value, bytes callData)"
];
const abiCoder = new ethers.AbiCoder();
const serializedMessage = abiCoder.encode(callStructAbi, [evmCall]);
console.log("Serialized Call message:", serializedMessage);
Step 2: Send Executable Message from N3
Send the EVM call as an executable message from N3 (Java example code):
// Get the fee for sending a message across the bridge.
BigInteger sendingFee = n3MessageBridge.callFunctionReturningInt("sendingFee");
// Build the transaction to send the executable message to Neo X using the sendingFee as maximal allowed fee and your account as bridge fee payer.
TransactionBuilder b = n3MessageBridge.invokeFunction("sendExecutableMessage", byteArray(rawMessage), bool(storeResult), hash160(myAccount), integer(sendingFee));
Transaction transaction = b.signers(AccountSigner.calledByEntry(myAccount)).sign();
NeoSendRawTransaction response = transaction.send();
Await.waitUntilTransactionIsExecuted(response.getSendRawTransaction().getHash(), neow3j);
Parameters:
rawMessage
: The encoded EVM contract call data, i.e., the outputserializedMessage
above.true
: Once the message is executed, the result of the execution should be stored on-chain, so that it can be returned to N3 if needed. If set tofalse
, the result will not be stored. The result will be included in an event that is fired when executing regardless of this value.
This creates an EXECUTABLE type message with:
Message Type:
0
(EXECUTABLE)Timestamp: Current N3 block time
Sender: The N3 contract/account sending the message
Store Result Flag:
true
(result should be stored on-chain on Neo X)Payload: The EVM contract call data
The message will be assigned a number (a nonce
). You can get it by either reading the remaining stack item of the transaction (the nonce is returned by the sendExecutableMessage()
function), or you can check the events of the transaction for the event MessageSend
.
ApplicationLog log = transaction.getApplicationLog();
BigInteger messageNonce = log.getFirstExecution().getFirstStackItem().getInteger();
Step 3: Message Relay to the EVM Chain
In this step, you need to wait until the decoupled relayer has transferred the message to the EVM chain. This should only take a couple of seconds.
You can listen to the MessageDeposit
event emitted by the MessageBridge contract. The first argument in the event is the message nonce. Once the event appears, your message has been transferred to the EVM chain and is now ready to be executed.
Step 4: Message Execution on EVM
Once the message has been stored on the EVM chain, the message can be executed by anyone calling executeMessage(uint256)
with the message nonce as parameter:
// Anyone can execute a stored message by providing its nonce
await evmMessageBridge.executeMessage(nonce);
Step 5: Verify Execution Results
After execution, you can verify the execution results on EVM:
const { success, returnData } = await messageBridge.getResult(nonce);
If the message's
storeResult
was set tofalse
when sending it, the result is only accessible in theMessageExecuted
event emitted when the message was executed.
Step 6: Return Result back to N3
This and the following steps are only needed if you want to return the result to N3.
In order to send a result back, you can call the sendResultMessage(uint256)
function:
await evmMessageBridge.sendResultMessage(nonce);
This will send the result back to N3 in form of a normal message sent from EVM to N3. It will get a new nonce. You can check the MessageSent
event of the transaction to get this message nonce.
Step 7: Message Relay back to N3
Now, the decoupled relayer transferres the response message back to N3. Take the message nonce from the returning message (i.e., the one from the MessageSent
event in Step 6) and wait for the Store
event on the MessageBridge contract on N3 that has this nonce.
In this step, the message bridge is used in the reverse direction from the EVM chain to the N3 chain. As in Step 3, you now need to wait until the decoupled relayer has transferred the message to N3. The event emitted when messages are transferred is Store
and its first state entry is the nonce.
Once the result is transferred, it can be read on-chain on N3.
Example Implementation (JavaScript)
const { ethers } = require('ethers');
// Define the Neo token contract address on EVM and target address
const neoTokenContractAddress = "0xc28736dc83f4fd43d6fb832Fd93c3eE7bB26828f"; // Neo token address on Neo X Testnet
const targetAddress = "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd"; // Address to check the balance for
// Create the interface for ERC20 balanceOf function
const erc20Interface = new ethers.Interface([
"function balanceOf(address account) view returns (uint256)"
]);
function exampleFunc() {
const encodedMessage = getEncodedMessage("0xc28736dc83f4fd43d6fb832Fd93c3eE7bB26828f", "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd");
// TBD: Send message on N3 using Javascript
// Invoke the method sendExecutableMessage on the MessageBridge contract on N3 using the encodedMessage bytes.
// const nonce = n3MessageBridge.sendExecutableMessage(messageBytes, true, sender, messageFee);
// Wait until the message arrives on the EVM chain...
// Once the message has been bridged over to the EVM chain, you can execute it
await evmMessageBridge.connect(sender).executeMessage(nonce, {maxFeePerGas, maxPriorityFeePerGas});
// If the result should be stored on-chain, you can return the result now.
await evmMessageBridge.connect(sender).sendResultMessage(nonce, {value: sendingFee, maxFeePerGas, maxPriorityFeePerGas});
}
function getEncodedMessage(tokenAddress, targetAddress) {
// Encode the balanceOf call using ethers
const erc20Interface = new ethers.Interface(["function balanceOf(address account) view returns (uint256)"]);
const callData = erc20Interface.encodeFunctionData("balanceOf", [targetAddress]);
// Create the Call structure
const evmCall = {
target: tokenAddress,
allowFailure: false,
value: 0,
callData: callData
};
// Serialize the Call structure
const callStructAbi = ["tuple(address target, bool allowFailure, uint256 value, bytes callData)"];
const abiCoder = new ethers.AbiCoder();
const serializedMessage = abiCoder.encode(callStructAbi, [evmCall]);
return {
serializedMessage: serializedMessage,
callData: callData,
evmCall: evmCall
};
}
function decodeBalanceResult(resultData) {
// Decode the uint256 balance result
const abiCoder = new ethers.AbiCoder();
const balance = abiCoder.decode(["uint256"], resultData)[0];
return balance;
}
Visualization
JavaScript + ethers for building the EVM call
┌─────────────────────────────────────────────────────────┐
│ const evmCall = { │
│ target: "0x1234...7890", // Neo token contract │
│ allowFailure: false, │
│ value: 0, │
│ callData: "0x70a08231000...abcdef" // balanceOf() │
│ }; │
└─────────────────────────────────────────────────────────┘
│
▼ ethers.AbiCoder.encode()
┌─────────────────────────────────────────────────────────┐
│ Serialized Message (hex string) │
│ 0x0000002000000000000000001234...7890000000000000... │
└─────────────────────────────────────────────────────────┘
│
▼ Send the message on N3
┌─────────────────────────────────────────────────────────┐
│ messageBridge.sendExecutableMessage(bytes, bool) │
└─────────────────────────────────────────────────────────┘
│
▼ Cross-chain relay - then execute on EVM
┌─────────────────────────────────────────────────────────┐
│ messageBridge.executeMessage(nonce) │
└─────────────────────────────────────────────────────────┘
│
▼ Return the result to N3
┌─────────────────────────────────────────────────────────┐
│ messageBridge.sendResultMessage(nonce) │
└─────────────────────────────────────────────────────────┘
This completes the full N3 to EVM flow showing how to properly prepare the call data using the ethers library for encoding the AMBTypes.Call
structure and balanceOf
method call, serializing it, sending it, executing it, and then sending the results back to N3.
Last updated