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: MITpragmasolidity ^0.8.0;/* XCDP Core to implement the interface.*/contract XCDPCore {addresspublic gatewayContractAddress = DESTINATION_X-TALK_GATEWAY_CONTRACT_ADDRESS; //Provided in endpointmapping(bytes32=> XCDPReceiveMessage) public XCDPData;/* Sending message instruction. You can make thiscompatible with Destination or if you want to transformthe message on X-Talk you can send raw instructions andprepare the payload on X-Talk Contract*/structXCDPSendMessage {string message; }/* Same with Receiving the message where you will beable to provide any struct based on how you want toreceive it based on the transformed payload */structXCDPReceiveMessage {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 youwant to run logic and decide where to send the paylaod */eventXTalkMessageBroadcasted(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(stringmemory message,stringmemory destinationSmartContractAddress,stringmemory destinationNetwork ) external { XCDPSendMessage memory myMessage =XCDPSendMessage({ message: message });// Convert the struct to bytesbytesmemory messageBytes = abi.encode(myMessage.message);emitXTalkMessageBroadcasted(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,bytesmemory 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.jsconsthre=require("hardhat");asyncfunctionmain() {const [deployer] =awaithre.ethers.getSigners();console.log("Deploying contracts with the account:",deployer.address );// getBalance is a method on the provider, not the signerconsole.log("Account balance:", (awaitdeployer.provider.getBalance(deployer.address)).toString());constXCDP=awaithre.ethers.getContractFactory("XCDPCore");constXCDPContract=awaitXCDP.deploy([deployer.address]);awaitXCDPContract.waitForDeployment();console.log("XCDP contract deployed to:",awaitXCDPContract.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
cargol1xcreatexcdp
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 modulesuse 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 keysconst STORAGE_CONTRACT_KEY:&[u8; 7] =b"message";const STORAGE_EVENTS_KEY:&[u8; 6] =b"events";// Import ethers utilities for ABI and event parsinguse ethers::{ abi::{decode, ParamType}, prelude::parse_log,};// Define data structures for messages#[derive(Clone, Debug, BorshSerialize, BorshDeserialize, Serialize, Deserialize)]pubstructXCDPSendMessage { message:String,}// This structure is used for solidity compatibility#[derive(Clone, Debug, Serialize, Deserialize)]pubstructXCDPSendMessageSolidity { message:String,}// Conversion trait to allow easy transformations between Solidity and custom Rust structsimplFrom<XCDPSendMessageSolidity> forXCDPSendMessage {fnfrom(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")]pubstructXTalkMessageBroadcasted { 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)]pubstructPayload { data:Vec<u8>, destination_network:String, destination_contract_address:String,}// Main contract structure storing all event data#[derive(BorshSerialize, BorshDeserialize)]pubstructXCDPCore { events:LookupMap<String, XCDPSendMessage>, total_events:u64,}// Default constructor for the contractimplDefaultforXCDPCore {fndefault() -> Self { Self { events:LookupMap::new(STORAGE_EVENTS_KEY.to_vec()), total_events:u64::default(), } }}// Contract trait implementation containing all business logic#[contract]implXCDPCore {// Function to load existing contract data from storagefnload() -> 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 storagefnsave(&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 contractpubfnnew() {letmut contract = Self::default(); contract.save(); }// Handler to process incoming events and save the decoded datapubfnsave_event_data(event_data:Vec<u8>, global_tx_id:String) { l1x_sdk::msg(&format!("********************global tx id {} **************", global_tx_id ));letmut 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();ifletOk(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() }fnsave_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 keypubfnto_key(global_tx_id:String, event_type:String) ->String { global_tx_id.to_owned() +"-"+&event_type }fnget_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())));returnPayload { 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.
cargol1xbuild
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-betawalletcreate
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-betawalletimport<PRIVATEKEY>
Default Wallet
After Importing your wallet set it to be the default interaction account.
l1x-cli-betawalletdefault<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