Step by Step guide and Repository link provided in Templates Section. In this guide we will use Sepolia and bscTestnet to deploy the client chain contracts and L1X TestNet to deploy X-Talk Contract.
Building Blocks for XCDP
Smart Contracts on Client Chains (example ethereum, binance chain) that will be the endpoints for sending and receiving messages. This is runtime agnostic.
L1X X-Talk Contract that includes Client Chain Contract Registration Initialisation and X-Talk XCDP Contract Deployment and Initialisation to facilitate programmability while sending and receiving messages.
In this guide, we use Sepolia as Source and bscTestnet as Destination network.
Implementing Solidity Contract (Comments for Explanation)
Replace the DESTINATION_X-TALK_GATEWAY_CONTRACT_ADDRESS with respective contract address listed in this table.
//XCDPCore.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
/* XCDP Core to implement the interface.*/
contract XCDPCore {
address public gatewayContractAddress = DESTINATION_X-TALK_GATEWAY_CONTRACT_ADDRESS; //Provided in endpoint
mapping(bytes32 => XCDPReceiveMessage) public XCDPData;
/* Sending message instruction. You can make this
compatible with Destination or if you want to transform
the message on X-Talk you can send raw instructions and
prepare the payload on X-Talk Contract*/
struct XCDPSendMessage {
string message;
}
/* Same with Receiving the message where you will be
able to provide any struct based on how you want to
receive it based on the transformed payload */
struct XCDPReceiveMessage {
string message;
}
/* This is the encoded format of the paylaod to be
sent. This will be sent to X-Talk Contract. Provide
destination details here or add it into X-Talk if you
want to run logic and decide where to send the paylaod */
event XTalkMessageBroadcasted(
bytes message,
string destinationNetwork,
string destinationSmartContractAddress
);
/* This is the function to be called when you want to
send the message. The message is encoded into bytes
before emitting */
function _l1xSend(
string memory message,
string memory destinationSmartContractAddress,
string memory destinationNetwork
) external {
XCDPSendMessage memory myMessage = XCDPSendMessage({
message: message
});
// Convert the struct to bytes
bytes memory messageBytes = abi.encode(myMessage.message);
emit XTalkMessageBroadcasted(messageBytes, destinationNetwork, destinationSmartContractAddress);
}
/* decoding the message to retrieve the stringified message,
and storing it along with the global transaction id ( the message identifier ) */
function _l1xReceive(bytes32 globalTxId, bytes memory message) external {
require(msg.sender == gatewayContractAddress, "Caller is not xtalk node");
XCDPReceiveMessage memory XCDPReceiveMessageData;
(XCDPReceiveMessageData.message) = abi.decode(message, (string));
XCDPData[globalTxId] = XCDPReceiveMessageData;
}
}
Deploying the Solidity Contract (scripts/deploy.js)
// deploy.js
const hre = require("hardhat");
async function main() {
const [deployer] = await hre.ethers.getSigners();
console.log(
"Deploying contracts with the account:",
deployer.address
);
// getBalance is a method on the provider, not the signer
console.log("Account balance:", (await deployer.provider.getBalance(deployer.address)).toString());
const XCDP = await hre.ethers.getContractFactory("XCDPCore");
const XCDPContract = await XCDP.deploy([deployer.address]);
await XCDPContract.waitForDeployment();
console.log("XCDP contract deployed to:", await XCDPContract.getAddress());
}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
At this point, you will have the deployed contract addresses for sepolia and bscTestnet.
Implementing L1X X-Talk Flow Contract
Flow Contracts are at the core of decentralised multi-chain and cross-chain application development. They enable logic programmability, reducing integration and maintenance overhead and enable scalability.
Create a new L1X project that provides the necessary files to get started
cargo l1x create xcdp
Use the provided code below to implement the cross-chain xcdp logic. The X-Talk Flow Contract consists of fundamental building blocks which are re-usable across all development standards with X-Talk.
Payload Definition that is being received. This could be deterministic or a generic vector of bytes. You can be deterministic or make this logic programmable.
Saving the event data that will store the event that is received.
Payload Logic that will allow you to implement logic based on what the destination payload required.
Payload Transformation that will be based on the destination runtime.
Emitting the Event to be sent to the Destination.
//lib.rs
// Import necessary crates and modules
use borsh::{BorshDeserialize, BorshSerialize};
use ethers::abi::Token;
use ethers::contract::EthEvent;
use l1x_sdk::{contract, store::LookupMap};
use serde::{Deserialize, Serialize};
// Define constants for storage keys
const STORAGE_CONTRACT_KEY: &[u8; 7] = b"message";
const STORAGE_EVENTS_KEY: &[u8; 6] = b"events";
// Import ethers utilities for ABI and event parsing
use ethers::{
abi::{decode, ParamType},
prelude::parse_log,
};
// Define data structures for messages
#[derive(Clone, Debug, BorshSerialize, BorshDeserialize, Serialize, Deserialize)]
pub struct XCDPSendMessage {
message: String,
}
// This structure is used for solidity compatibility
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct XCDPSendMessageSolidity {
message: String,
}
// Conversion trait to allow easy transformations between Solidity and custom Rust structs
impl From<XCDPSendMessageSolidity> for XCDPSendMessage {
fn from(event: XCDPSendMessageSolidity) -> Self {
Self {
message: event.message,
}
}
}
// Define the event structure that this contract can parse and emit
#[derive(Clone, Debug, EthEvent, Serialize, Deserialize)]
#[ethevent(name = "XTalkMessageBroadcasted")]
pub struct XTalkMessageBroadcasted {
message: ethers::types::Bytes,
destination_network: String,
destination_smart_contract_address: String,
}
// Payload structure for inter-chain messages
#[derive(Clone, Debug, BorshSerialize, BorshDeserialize, Serialize, Deserialize)]
pub struct Payload {
data: Vec<u8>,
destination_network: String,
destination_contract_address: String,
}
// Main contract structure storing all event data
#[derive(BorshSerialize, BorshDeserialize)]
pub struct XCDPCore {
events: LookupMap<String, XCDPSendMessage>,
total_events: u64,
}
// Default constructor for the contract
impl Default for XCDPCore {
fn default() -> Self {
Self {
events: LookupMap::new(STORAGE_EVENTS_KEY.to_vec()),
total_events: u64::default(),
}
}
}
// Contract trait implementation containing all business logic
#[contract]
impl XCDPCore {
// Function to load existing contract data from storage
fn load() -> Self {
match l1x_sdk::storage_read(STORAGE_CONTRACT_KEY) {
Some(bytes) => match Self::try_from_slice(&bytes) {
Ok(contract) => contract,
Err(_) => panic!("Unable to parse contract bytes"),
},
None => panic!("The contract isn't initialized"),
}
}
// Function to save contract state to storage
fn save(&mut self) {
match borsh::BorshSerialize::try_to_vec(self) {
Ok(encoded_contract) => {
l1x_sdk::storage_write(STORAGE_CONTRACT_KEY, &encoded_contract);
log::info!("Saved event data successfully");
}
Err(_) => panic!("Unable to save contract"),
};
}
// Constructor to initialize a new contract
pub fn new() {
let mut contract = Self::default();
contract.save();
}
// Handler to process incoming events and save the decoded data
pub fn save_event_data(event_data: Vec<u8>, global_tx_id: String) {
l1x_sdk::msg(&format!(
"********************global tx id {} **************",
global_tx_id
));
let mut contract = Self::load();
log::info!("Received event data!!!");
assert!(!global_tx_id.is_empty(), "global_tx_id cannot be empty");
assert!(!event_data.is_empty(), "event_data cannot be empty");
assert!(
!contract.events.contains_key(&global_tx_id),
"event is saved already"
);
let event_data = match base64::decode(event_data) {
Ok(data) => data,
Err(_) => panic!("Can't decode base64 event_data"),
};
let log: ethers::types::Log =
serde_json::from_slice(&event_data).expect("Can't deserialize Log object");
l1x_sdk::msg(&format!("{:#?}", log));
let event_id = log.topics[0].to_string();
if let Ok(standard_event) = parse_log::<XTalkMessageBroadcasted>(log.clone()) {
let event = decode(&[ParamType::String], &standard_event.message).unwrap();
let event = XCDPSendMessageSolidity {
message: event[0].clone().into_string().unwrap(),
};
contract.save_standard_event(
global_tx_id,
event_id,
event,
standard_event.destination_smart_contract_address,
standard_event.destination_network
);
} else {
panic!("invalid event!")
}
contract.save()
}
fn save_standard_event(
&mut self,
global_tx_id: String,
event_id: String,
event: XCDPSendMessageSolidity,
destination_contract_address: String,
destination_network: String
) {
let event_data = event.clone().into();
let key = Self::to_key(global_tx_id.clone(), event_id);
self.events.insert(key, event_data);
let payload = Self::get_standard_payload(
global_tx_id.clone(),
destination_network,
destination_contract_address,
event.message.clone(),
);
l1x_sdk::msg(&format!("emitted event: {:?}", payload));
l1x_sdk::emit_event_experimental(payload);
}
// Function to combine parts of an event into a single storage key
pub fn to_key(global_tx_id: String, event_type: String) -> String {
global_tx_id.to_owned() + "-" + &event_type
}
fn get_standard_payload(
global_tx_id: String,
destination_network: String,
destination_contract_address: String,
message: String,
) -> Payload {
let message_encoded = ethers::abi::encode(&[Token::String(message)]);
let signature = "_l1xReceive(bytes32,bytes)";
let payload_encoded = ethers::abi::encode(&[
Token::FixedBytes(hex::decode(global_tx_id).unwrap()),
Token::Bytes(message_encoded),
]);
let hash = ethers::utils::keccak256(signature.as_bytes());
let selector = &hash[..4];
let payload = [&selector[..], &payload_encoded[..]].concat();
l1x_sdk::msg(&format!("payload --> {}", hex::encode(payload.clone())));
return Payload {
data: payload,
destination_network,
destination_contract_address,
};
}
}
The Fundamental Building Blocks of X-Talk require the below building blocks in X-Talk Contract.
Data Structures: Define messages and events used within the contract.
Event Handling: Parse, log, and handle blockchain events.
State Management: Load and save the contract's state to maintain event records.
Message Parsing: Convert blockchain event logs into usable data formats.
Event Emission and Logging: Emit structured logs for cross-chain messaging.
Security: Ensure that only valid and expected data is processed.
Ensure you have all the necessary packages, libraries and dependencies in your cargo.toml file as below.
[package]
name = "xcdp-core"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
borsh = { version = "0.9", features = ["const-generics"] }
l1x-sdk = "0.3.1"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
base64 = "*"
ethers = "2.0.11"
getrandom = { version = "0.2.10", features = ["js"] }
hex = "0.4"
log = "0.4.20"
To deploy the X-Talk flow contract, you can use this command on your project level. After successfully building, you will find the "project_name.o" file in target/l1x/release folder. This is the object file which will be deployed.
cargo l1x build
To deploy your contract, use an existing L1X Account with L1X for Mainnet and L1X TestNet for TestNet.
Create Wallet
Create your own wallet to generate a new keypair.
l1x-cli-beta wallet create
Import Wallet
Import you existing wallet using the l1x-cli-beta tool, by providing a private key. If you don't have one, Create Wallet.
l1x-cli-beta wallet import <PRIVATEKEY>
Default Wallet
After Importing your wallet set it to be the default interaction account.
l1x-cli-beta wallet default <Wallet_Address>
-- Check Endpoints in Interface Essentials for TestNet Faucet
SOURCE_REGISTRY_INSTANCE_ADDRESS => This is provided in below example
source_contract_address => This is the contract address you want to listen to, without '0x'
TOPIC_OF_YOUR_EVENT => Topic of the event
YOUR_FLOW_CONTRACT_ADDRESS => The initiated X-TalK Flow Contract Address