v1.2 (Beta - Developer Guide)
Using SimpleMessageTest to Understand the XTalk Protocol
1. Overview
Purpose
This document provides a comprehensive guide for developers on how to use the SimpleMessageTest.sol
contract. It serves as a practical, hands-on example for sending and receiving cross-chain messages using the L1X XTalk protocol.
What is SimpleMessageTest.sol
?
SimpleMessageTest.sol
?SimpleMessageTest.sol
is a minimal, developer-friendly smart contract that demonstrates the core functionality of the XTalk protocol. It is designed to be a clear and lucid starting point for building more complex cross-chain applications.
Target Audience & Limitations
This guide is intended for developers building applications on EVM-compatible blockchains.
Current Scope: This version of the XTalk protocol and this example support EVM-compatible networks only.
Supported Test Networks:
BSC Testnet
Ethereum Sepolia
Core Concepts
The heart of the XTalk protocol is the XTalkBeacon
contract. It acts as a cross-chain "post office," where your contract drops off messages for other chains (registerMessage
) and receives incoming messages via a standardized callback (_xtalkMessageReceived
).
2. Contract Anatomy
The SimpleMessageTest.sol
contract is structured to clearly separate the mandatory XTalk protocol code from the customizable user logic. This is highlighted by the in-code comments.
A. XTalk Infrastructure
These are the parts you must include and configure correctly for your contract to be XTalk-compatible.
IXTalkBeacon
Interface: A standard Solidity interface that defines how your contract will communicate with the deployedXTalkBeacon
.xtalkBeaconAddress
State Variable &constructor
:// The XTalkBeacon contract instance. address public immutable xtalkBeaconAddress; /** * @dev Constructor to set the address of the XTalkBeacon contract. */ constructor(address _beaconAddress) { require(_beaconAddress != address(0), "Invalid beacon address"); xtalkBeaconAddress = _beaconAddress; }
This is a critical setup step. When you deploy your contract, you must provide the correct address of the
XTalkBeacon
for the specific blockchain you are deploying on. The contract then casts this address to theIXTalkBeacon
interface when it needs to call a beacon function._xtalkMessageReceived
Function:function _xtalkMessageReceived(bytes32 _messageId, /*...*/) public { require(msg.sender == address(xtalkBeacon), "Caller is not the XTalkBeacon"); // ... }
This is the mandatory callback function that the beacon executes to deliver a message to your contract.
Function Signature: The name and parameters must be exactly
_xtalkMessageReceived(bytes32,address,uint32,uint32,bytes)
.Security: It is crucial to include the
require(msg.sender == address(xtalkBeacon), ...)
check to ensure that only the legitimate beacon can call this function.
B. User-Defined Logic
This is the part of the contract that you customize to build your application's specific features.
Message
Struct &receivedMessages
Mapping:struct Message { /* ... */ } mapping(bytes32 => Message) public receivedMessages;
In this example, we use these to store the data from a received message. For your application, you might process a token swap, mint an NFT, or update some other state.
MessageReceived
Event: This event provides off-chain tracking and visibility for when your contract receives a message. Note that the event for sending a message is emitted by theXTalkBeacon
contract itself.invokeMessage
Function: This function demonstrates how the user-defined logic prepares data and then hands it off to the XTalk Infrastructure to be sent.function invokeMessage(...) public { // 1. User-Defined: Prepare your message data uint32 messageType = 1; bytes memory messageData = abi.encode(_message); // 2. XTalk Infrastructure: Get nonce and register the message uint256 nonce = IXTalkBeacon(xtalkBeaconAddress).getSenderRegistrationNextNonce(address(this)); IXTalkBeacon(xtalkBeaconAddress).registerMessage(...); // NO event is emitted here. The XTalkBeacon contract emits the // 'XTalkMessageBroadcasted' event, which should be used for tracking. }
3. How It Works: The Cross-Chain Flow
The following diagram illustrates the end-to-end journey of a message. The L1X Validator Network and the XTalk Consensus Contract on the L1X chain are the core components that make this possible.
sequenceDiagram
participant User
participant SMT_Source as SimpleMessageTest (Source Chain)
participant Beacon_Source as XTalkBeacon (Source Chain)
participant L1X_Network as L1X Validator Network
participant Consensus_Contract as XTalk Consensus (L1X)
participant Beacon_Dest as XTalkBeacon (Destination Chain)
participant SMT_Dest as SimpleMessageTest (Destination Chain)
User->>+SMT_Source: invokeMessage(...)
SMT_Source->>+Beacon_Source: registerMessage(...)
note right of Beacon_Source: Emits 'XTalkMessageBroadcasted'<br/>(messageId is retrieved here)
Beacon_Source-->>-SMT_Source:
SMT_Source-->>-User:
L1X_Network->>Beacon_Source: Observes event on Source Chain
L1X_Network->>+Consensus_Contract: Reaches consensus on the message
note over L1X_Network, Consensus_Contract: Validators sign the message payload
User->>Consensus_Contract: (Optional) get_consensus_status(messageId)
Consensus_Contract-->>User: Returns signer count, status, etc.
L1X_Network->>+Beacon_Dest: Relays message with signatures
Beacon_Dest->>+SMT_Dest: _xtalkMessageReceived(...)
note right of SMT_Dest: Executes user logic
SMT_Dest-->>-Beacon_Dest:
Beacon_Dest-->>-L1X_Network:
The process unfolds in these steps:
Invocation (Source Chain): A user calls
invokeMessage
on theSimpleMessageTest
contract.Registration (Source Chain): The contract calls
registerMessage
on the source chain'sXTalkBeacon
, which emits theXTalkMessageBroadcasted
event containing a uniquemessageId
.Consensus (L1X Network): The L1X network validators observe this event. They process the message, and a sufficient number of them sign it, reaching consensus. This status is recorded in the
XTalk Consensus Contract
on the L1X chain.Execution (Destination Chain): Once consensus is reached, the validators relay the message and the collected signatures to the
XTalkBeacon
on the destination chain. The beacon verifies the signatures and calls_xtalkMessageReceived
on the target contract, completing the cross-chain transaction.
4. Practical Guide: Deployment and Interaction
Supported Networks & Chain IDs
List the currently supported test networks and their corresponding Chain IDs, which you will need for the _destinationChainId
parameter when sending a message.
BSC Testnet:
Chain ID:
97
Ethereum Sepolia:
Chain ID:
11155111
Deployment
Compile: Use a Solidity compiler (version
^0.8.0
or compatible) to compileSimpleMessageTest.sol
.Deploy: Deploy the compiled contract to a supported network (e.g., BSC Testnet or ETH Sepolia).
Provide Beacon Address: During deployment, you must provide the correct
XTalkBeacon
address for that network in the constructor.BSC Testnet
XTalkBeacon
Address:0x283217CFb842A235994397a36714eF90F7744B51
ETH Sepolia
XTalkBeacon
Address:0x8cA818D30c01AD6ebD2273CC419553F38fE7c92f
Sending a Message
To send a message, call the invokeMessage
function on your deployed contract.
_destinationChainId
: The unique identifier for the destination chain. Use97
for BSC Testnet or11155111
for Ethereum Sepolia._destinationAddress
: The address of the recipient contract on the destination chain, formatted asbytes32
._message
: The string message you want to send.
Confirming a Message on the Destination Chain
When you call
invokeMessage
, listen for theXTalkMessageBroadcasted
event on theXTalkBeacon
contract. It contains themessageId
.After a short delay for the L1X Network to relay the message, you can check for its arrival on the destination chain.
Call the
receivedMessages
public mapping on the destination contract with themessageId
to retrieve the stored message data and confirm its receipt.
Tracking Message Status on the L1X Network
The ultimate source of truth for a message's journey is the XTalk Consensus Contract deployed on the L1X Network. You can query this contract directly using the messageId
to see its current consensus status, including how many validators have signed it.
The address for this contract on L1X Testnet v2 is:
XTalk Consensus Contract:
ce527bdc3eeaa201a2706ab2e447fc7f0db5cb68
The following script shows how to do this using the @l1x/l1x-wallet-sdk
.
// Example script to check the consensus status of a message on the L1X Network.
// This requires the `@l1x/l1x-wallet-sdk`. You can install it via npm:
// npm install @l1x/l1x-wallet-sdk
const { L1XProvider } = require("@l1x/l1x-wallet-sdk");
// The address of the XTalk Consensus Contract on the L1X Network
const CONSENSUS_CONTRACT_ADDRESS = "ce527bdc3eeaa201a2706ab2e447fc7f0db5cb68";
/**
* Initializes and returns an L1XProvider instance for the L1X V2 Testnet.
* @returns {L1XProvider}
*/
function getL1XProvider() {
// For mainnet, change clusterType to "mainnet" and use the mainnet RPC endpoint.
return new L1XProvider({
clusterType: "testnet",
endpoint: "https://v2-testnet-rpc.l1x.foundation"
});
}
/**
* Queries the L1X Consensus Contract for the status of a specific message.
* @param {string} messageId The message ID obtained from the 'XTalkMessageBroadcasted' event.
* @returns {Promise<object>} The consensus status of the message.
*/
async function checkL1XConsensusStatus(messageId) {
console.log(`\nQuerying L1X for status of message: ${messageId}`);
try {
// Initialize the L1X provider
const l1xProvider = getL1XProvider();
// Make a read-only call to the smart contract
const responseJson = await l1xProvider.core.makeReadOnlyCall({
contract_address: CONSENSUS_CONTRACT_ADDRESS,
function_name: "get_consensus_status",
args: {
message_id: messageId
},
});
// The response is a JSON string, so we parse it
return JSON.parse(responseJson);
} catch (error) {
console.error("Error fetching consensus status:", error);
throw error;
}
}
// --- Main execution ---
(async () => {
// Replace with the messageId you want to track
const messageIdToTrack = "0xffd6da09d5047e7ce19bf818e0d93c11a0bd2fc79ec80c18a523569f6a902973";
const status = await checkL1XConsensusStatus(messageIdToTrack);
console.log("\n--- L1X Consensus Status ---");
console.log(JSON.stringify(status, null, 2)); // Pretty-print the status object
})();
Understanding the Consensus Status Response
The JSON object returned by get_consensus_status
represents the complete state of a cross-chain message as it moves through the L1X consensus process. Here is a breakdown of each field:
Validator and Confirmation Counts
These fields provide an overview of the validator set and the progress of votes for this specific message.
total_listener_validators
: The total number of registered "Listener" validators at the time the message was processed. Listeners are responsible for observing and confirming the initial event on the source chain.total_signer_validators
: The total number of registered "Signer" validators. Signers are responsible for cryptographically signing the message payload that will be executed on the destination chain.total_relayer_validators
: The total number of registered "Relayer" validators. Relayers are responsible for broadcasting the transaction to the destination chain and confirming its final state.agreed_listener_validators
: The current count of Listener validators who have successfully voted and agreed on the validity of the source chain event.agreed_signer_validators
: The current count of Signer validators who have successfully voted on and signed the destination payload.agreed_relayer_confirmations
: The current count of Relayer validators who have confirmed that the transaction was successfully executed on the destination chain.disagreed_relayer_confirmations
: The current count of Relayer validators who have reported that the transaction failed on the destination chain.
Lifecycle and Timestamps
These fields track the message's current stage and when key milestones were reached.
consensus_stage
: The current stage of the message. Possible values are:'Pending'
: The message has not yet reached consensus in any phase.'ListenerFinalized'
: The Listeners have reached consensus on the source chain event.'SignerFinalized'
: The Signers have reached consensus on the destination payload.'RelayerBroadcasted'
: The Relayers have successfully broadcasted the transaction to the destination chain.'RelayerFinalized'
: The Relayers have confirmed the final status (success or failure) of the destination transaction.
listener_finalized_timestamp
: A Unix timestamp (in milliseconds) of when the Listener consensus was reached.signer_finalized_timestamp
: A Unix timestamp (in milliseconds) of when the Signer consensus was reached.relayer_finalized_timestamp
: A Unix timestamp (in milliseconds) of when the Relayer consensus was reached.Note on
relayer_finalized_timestamp
: This field is reserved for future protocol enhancements where relayers explicitly confirm the final on-chain status (e.g., after a certain number of block confirmations). In the current implementation, the most reliable indicator that a message has been successfully passed to the destination chain is the presence of a non-emptydestination_tx_hash
.
Message Payload and Routing
These fields contain the core data of the cross-chain message and the necessary information to route it correctly.
signer_payload
: The raw byte payload that the Signer validators have agreed upon. This is the exact data passed to theXTalkBeacon
on the destination chain.signer_signatures
: A list of the raw, recoverable signatures from the Signer validators. These are submitted to the destinationXTalkBeacon
to prove the payload's authenticity.
Transaction and Address Details
These fields provide the specific identifiers and addresses involved in the cross-chain transaction.
source_network_id
: The chain ID of the network where the message originated.source_tx_hash
: The transaction hash of the initial call on the source chain.flow_contract_address
: The address of an intermediary "flow" contract on the L1X network that may have processed the event data.source_contract_address
: The address that emitted the event on the source chain (i.e., theXTalkBeacon
).destination_tx_hash
: The transaction hash of the execution call on the destination chain, as reported by the first relayer.destination_network_id
: The chain ID of the target network.destination_contract_address
: The address of the target contract on the destination chain.destination_relayer_address
: The address of the relayer validator that broadcast the transaction.
5. Next Steps
This SimpleMessageTest
contract and its usage guide are designed to be your launchpad. You can use this contract as a boilerplate for your own projects. By replacing the user-defined logic, you can build powerful cross-chain applications such as:
Cross-chain token swaps
Multi-chain NFT marketplaces
Decentralized governance across multiple networks
6. Example: Standalone Ethers.js Script
This example shows how to interact with the contract in a standalone Node.js script using the ethers
library (v6), without relying on a framework like Hardhat.
Important Note: Listening for the Correct Event
When you call invokeMessage
, your contract does not emit an event. The event is emitted by the XTalkBeacon
contract that invokeMessage
calls. You must listen to the XTalkBeacon
contract for the XTalkMessageBroadcasted
event to get the messageId
and confirm the message was sent.
Important Note: Address to bytes32 Conversion
The _destinationAddress
parameter in the invokeMessage
function expects a bytes32
value. However, standard Ethereum addresses are 20-byte values. You must pad the 20-byte address with leading zeros to convert it to a 32-byte value. With ethers.js
, you can do this easily:
const destinationAddress = "0x..."; // Your 20-byte destination address
const destinationAddressBytes32 = ethers.zeroPadValue(destinationAddress, 32);
Sample Interaction Script
This script shows a complete flow: setting up a connection, sending a message, and retrieving the messageId
by listening to the XTalkBeacon contract's events.
// A standalone script to interact with the SimpleMessageTest contract using ethers.js v6
const { ethers } = require("ethers");
// ======== 1. Configuration ========
// Provider setup: Replace with your RPC URL (e.g., from Infura, Alchemy)
const provider = new ethers.JsonRpcProvider("https://sepolia.infura.io/v3/YOUR_PROJECT_ID");
// Signer setup: Replace with the private key of the account you want to send the transaction from
const privateKey = "0x..."; // WARNING: Do not hardcode private keys in production. Use environment variables.
const wallet = new ethers.Wallet(privateKey, provider);
// Your contract details
const simpleMessageTestAddress = "0x..."; // Address of your deployed SimpleMessageTest contract
const simpleMessageTestAbi = [
// We only need the function signature for the function we are calling
"function invokeMessage(uint32 _destinationChainId, bytes32 _destinationAddress, string memory _message)"
];
// XTalkBeacon contract details
const beaconAddress = "0x8cA818D30c01AD6ebD2273CC419553F38fE7c92f"; // The XTalkBeacon address for Sepolia
const beaconAbi = [
// The ABI for the event we want to listen for
"event XTalkMessageBroadcasted(bytes32 indexed messageId, uint32 sourceChainId, uint32 destinationChainId, uint32 messageType, address indexed sourceInvokerAddress, bytes32 destinationAddress, uint256 timestamp, bytes messageData)"
];
// Create contract instances
const simpleMessageContract = new ethers.Contract(simpleMessageTestAddress, simpleMessageTestAbi, wallet);
const beaconContract = new ethers.Contract(beaconAddress, beaconAbi, provider);
// ======== 2. Interaction Parameters ========
const destinationChainId = 97; // e.g., BSC Testnet
const destinationAddress = "0x..."; // Address of the recipient contract on the destination chain
const message = "Hello from a standalone ethers.js script!";
async function main() {
console.log(`Sending transaction from: ${wallet.address}`);
// 3. Convert the 20-byte destination address to bytes32
const destinationAddressBytes32 = ethers.zeroPadValue(destinationAddress, 32);
// 4. Invoke the message by calling the contract function
console.log(`Invoking message on contract at ${simpleMessageTestAddress}...`);
const tx = await simpleMessageContract.invokeMessage(
destinationChainId,
destinationAddressBytes32,
message
);
console.log("Transaction sent! Hash:", tx.hash);
const receipt = await tx.wait();
console.log(`Transaction confirmed in block: ${receipt.blockNumber}`);
// 5. Find and parse the 'XTalkMessageBroadcasted' event from the transaction receipt
// We must look for this event from the BEACON's address in the logs.
const eventTopic = beaconContract.interface.getEvent("XTalkMessageBroadcasted").topicHash;
const log = receipt.logs.find(l => l.address === beaconAddress && l.topics[0] === eventTopic);
if (log) {
const parsedLog = beaconContract.interface.parseLog(log);
const { messageId } = parsedLog.args;
console.log(`\n✅ Message Broadcasted via Beacon!`);
console.log(` - Message ID: ${messageId}`);
console.log(` - Use this ID to track the message status on the destination chain.`);
} else {
console.log("Could not find 'XTalkMessageBroadcasted' event in the transaction receipt.");
}
}
main().catch((error) => {
console.error("Script failed:", error);
process.exit(1);
});
Last updated