Your First Cross-Chain dApp with XTalk: A Step-by-Step Guide
Welcome, developer! This guide will walk you through integrating your Solidity smart contracts with the L1X XTalk Protocol to send and receive messages across different blockchains. We'll use a simplified example, similar to SimpleMessageTest.sol
, to illustrate the core concepts.
Prerequisites
Before you start, you should have a basic understanding of:
Solidity and smart contract development.
The concept of cross-chain communication.
An XTalk environment set up (or access to a testnet where XTalk contracts like
XTalkBeacon
are deployed).
Known XTalkBeacon Testnet Addresses
For your convenience, here are some known deployed XTalkBeacon
contract addresses on common testnets:
Sepolia Testnet:
Chain ID:
11155111
XTalkBeacon
Address:0x1a99f64254D998d9F6a2912Ca5b19c1DFE326eF8
BSC (Binance Smart Chain) Testnet:
Chain ID:
97
XTalkBeacon
Address:0x1a99f64254D998d9F6a2912Ca5b19c1DFE326eF8
Note: Always verify contract addresses from official project documentation or trusted sources before interacting with them on mainnet or for critical operations.
Goal
Our goal is to create a smart contract that can:
Send a simple text message to a contract on another EVM-compatible chain using XTalk.
Receive a simple text message from another chain via XTalk.
Step 1: Define the IXTalkBeacon
Interface
IXTalkBeacon
InterfaceTo interact with the XTalk system on any EVM chain, your contract needs to know how to call the XTalkBeacon
contract. This is done by defining an interface in your Solidity file.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
interface IXTalkBeacon {
// Function to initiate a cross-chain message
function registerMessage(
uint32 _destinationChainId,
bytes32 _destinationAddress,
bytes memory _messageData,
uint32 _messageType,
uint256 _messageNonce
) external;
// Function to get the next valid nonce for your contract
function getSenderRegistrationNextNonce(address _sender) external view returns (uint256);
}
// Your contract will go here
// contract MyXTalkDApp is ...
registerMessage
: This is the function you'll call on theXTalkBeacon
to send your message.getSenderRegistrationNextNonce
: You'll use this to get a unique nonce for each message your contract sends, which is important for security.
Step 2: Setting Up Your Contract to Send Messages
Let's create a contract MyXTalkDApp
and add a function to send a message. For a production or testnet dApp, the XTalkBeacon
address on a specific chain is fixed. You can store this as a constant or an immutable variable in your contract for clarity and safety.
contract MyXTalkDApp {
// Address of the XTalkBeacon on the current chain.
// For Sepolia Testnet, for example:
address public immutable XTALK_BEACON_ADDRESS;
// Event to log message sending attempts (optional)
event MessageSentToXTalk(
uint32 indexed destinationChainId,
bytes32 indexed destinationContractAddress,
string messageContent,
uint256 nonceUsed
);
constructor(address beaconContractAddress) {
XTALK_BEACON_ADDRESS = beaconContractAddress;
}
// Function to send a cross-chain message
function sendMessageToOtherChain(
uint32 destinationChainId, // The ID of the chain you want to send to
bytes32 destinationContractAddress, // The address of your target contract on the other chain (as bytes32)
string memory myTextMessage // The actual text you want to send
) public {
// 1. Define a message type (application-specific)
uint32 messageType = 1; // You can define different types for different actions
// 2. Encode your payload (the string message)
// The XTalk system transmits data as `bytes`. So, we ABI-encode our string.
bytes memory encodedMessagePayload = abi.encode(myTextMessage);
// 3. Get the next valid nonce for your contract
// This makes each message unique from your contract's perspective on the source chain.
IXTalkBeacon beacon = IXTalkBeacon(XTALK_BEACON_ADDRESS); // Use the stored beacon address
uint256 nonce = beacon.getSenderRegistrationNextNonce(address(this));
// 4. Call `registerMessage` on the XTalkBeacon
beacon.registerMessage(
destinationChainId,
destinationContractAddress,
encodedMessagePayload,
messageType,
nonce
);
// Optional: Emit an event in your contract to log that a message was sent
emit MessageSentToXTalk(destinationChainId, destinationContractAddress, myTextMessage, nonce);
}
}
Explanation:
XTALK_BEACON_ADDRESS
: This is now animmutable
state variable, set in theconstructor
. When deployingMyXTalkDApp
, you would pass the knownXTalkBeacon
address for the chain you are deploying to (e.g.,0x1a99f64254D998d9F6a2912Ca5b19c1DFE326eF8
for Sepolia or BSC Testnet as per our known addresses list). This makes it clear which beacon your contract is configured to use.sendMessageToOtherChain
Parameters: The function now only takes parameters specific to the message itself, as thebeaconAddress
is already configured for the contract instance.Using the Stored Address: Inside
sendMessageToOtherChain
,IXTalkBeacon(XTALK_BEACON_ADDRESS)
is used, referencing the immutable address.Important Security Note: In a production system, you might want to add a check to ensure
msg.sender
is the legitimate XTalkBeacon contract on this chain. For example: require(msg.sender == XTALK_BEACON_ADDRESS, "Unauthorized XTalk caller");
Step 3: Setting Up Your Contract to Receive Messages
To receive a message from another chain via XTalk, your contract (or a specific contract you deploy on the destination chain) must implement a special callback function: _xtalkMessageReceived
.
// Continuing our MyXTalkDApp contract...
contract MyXTalkDApp {
// ... (sendMessageToOtherChain function from above) ...
// Data structure to store received message details
struct ReceivedMessage {
bytes32 messageId; // Unique ID from XTalk system
address sourceInvokerAddress; // Address of the contract that sent the message (on source chain)
uint32 sourceChainId; // ID of the chain the message came from
uint32 messageType; // Application-specific type from sender
string actualMessage; // The decoded text message
bytes rawMessageData; // The raw payload received
}
mapping(bytes32 => ReceivedMessage) public receivedXTalkMessages;
event MessageReceived(bytes32 indexed messageId, address sourceInvoker, string message);
// This is the REQUIRED callback function for XTalk
function _xtalkMessageReceived(
bytes32 _messageId, // The unique XTalk message ID
address _sourceInvokerAddress, // Who sent it on the source chain
uint32 _sourceChainId, // Which chain it came from
uint32 _messageType, // The type defined by the sender
bytes memory _messageData // The raw payload (our ABI-encoded string)
) public {
// Important Security Note: In a production system, you might want to add a check
// to ensure `msg.sender` is the legitimate XTalkBeacon contract on this chain.
// For example: require(msg.sender == XTALK_BEACON_ADDRESS, "Unauthorized XTalk caller");
// 1. Decode the message payload if you know its type
// Assuming the sender used `abi.encode(string)` like our `sendMessageToOtherChain` function
string memory decodedTextMessage = abi.decode(_messageData, (string));
// 2. Store the received message details
receivedXTalkMessages[_messageId] = ReceivedMessage({
messageId: _messageId,
sourceInvokerAddress: _sourceInvokerAddress,
sourceChainId: _sourceChainId,
messageType: _messageType,
actualMessage: decodedTextMessage,
rawMessageData: _messageData
});
// 3. Emit an event to signal the message was received and processed
emit MessageReceived(_messageId, _sourceInvokerAddress, decodedTextMessage);
// 4. (Optional) Add any custom logic here to act upon the received message
// For example, update state, call another function, etc.
}
}
Explanation:
_xtalkMessageReceived
Signature: The function name and parameters (bytes32
,address
,uint32
,uint32
,bytes
) must exactly match this signature. TheXTalkBeacon
on your contract's chain is programmed to call this specific function when it has a message for your contract.Security (Caller Verification - Optional but Recommended): Although not shown in the most basic
SimpleMessageTest.sol
, in a real-world scenario, you should verify thatmsg.sender
of the_xtalkMessageReceived
call is indeed the trustedXTalkBeacon
contract address on the current chain. This prevents unauthorized contracts from spoofing XTalk messages directly to your callback.Decoding Payload: The
_messageData
arrives asbytes
. Since oursendMessageToOtherChain
function sent an ABI-encoded string, we useabi.decode(_messageData, (string))
to get the original text back.Storing Data: The example stores the message details in a mapping. You can adapt this to your dApp's needs.
Custom Logic: After decoding and storing, you can add any logic your dApp needs to perform based on the received message content or type.
Step 4: Deployment and Interaction Flow
Deploy
MyXTalkDApp
(or similar contracts) on both the source and destination chains.The instance on the source chain will use
sendMessageToOtherChain
.The instance on the destination chain will receive the message via
_xtalkMessageReceived
.
Identify
XTalkBeacon
Addresses: You'll need the deployed addresses of theXTalkBeacon
contracts on both chains. (See the "Known XTalkBeacon Testnet Addresses" section above for examples).Sending a Message:
Call
sendMessageToOtherChain
on the source chain with the appropriate parameters.The message will be sent to the destination chain via XTalk.
The destination chain's
_xtalkMessageReceived
function will handle the message reception.
By following these steps, you'll be able to send and receive messages across different blockchains using the XTalk Protocol.
Last updated