Build your first XCDP - Solana to EVM Contract and XCDP - EVM to Solana Contract

Step by Step guide with detailed process. In this guide we will use Solana devnet, Sepolia and L1X TestNet to deploy X-Talk Flow Contract.

Building Blocks for XCDP

  1. Smart Contracts on Solana that will be the endpoints for sending and receiving messages. This is runtime agnostic.

  2. X-Talk XCDP Solana-EVM Flow Contract Deployment and Initialisation to facilitate programmability while sending and receiving messages.

  3. X-Talk XCDP EVM-Solana Flow Contract Deployment and Initialisation to facilitate programmability while sending and receiving messages.

  4. Smart Contracts on EVM-compatible chain (Sepolia in this example) that will be the endpoints for sending and receiving messages. This is runtime agnostic.

XCDP Process

This page is divided into 4 parts, for the ease of understanding, as listed below

  1. Basic Steps: Common for both Solana-EVM and EVM-Solana

  2. Solana to EVM Integration

  3. EVM to Solana Integration

  4. X-Talk Gateway Contract Address

I. Basic Steps

This section is categorised into Pre-Requisites, Set up Solana Project and Set up Sepolia Project.

Step 1: Pre-Requisites

Step 2: Set up Solana Project

Open a new terminal to create Solana project

Step 1: Initialize a New Project

  1. Create a New Directory

mkdir solana-project-name
cd solana-project-name
  1. Initialize a New Anchor Project

anchor init flow_contract

Step 2: Write your Solana smart contract

Paste your contract code at /programs/flow_contract/src/lib.rs

//lib.rs
use anchor_lang::prelude::*;
use serde::Serialize;


//Replace with YOUR_SOLANA_PROGRAM_ID
declare_id!("Go9wgRnEAfWpNrKKhV42kteHfFaFKszhDeTQ9THbMe6Y");


#[program]
pub mod flow_contract {
    use super::*;


    pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
        ctx.accounts.called_account.called = false;
        Ok(())
    }


    pub fn emit_event(
        _ctx: Context<EmitEvent>,
        message: String,
        destination_network: String,
        destination_smart_contract_address: String,
    ) -> Result<()> {
        emit!(XTalkMessageBroadcasted {
            message: borsh::to_vec(&message).unwrap(),
            destination_network: destination_network.clone(),
            destination_smart_contract_address,
        });
        Ok(())
    }


    pub fn l1x_receive(ctx: Context<Call>, message: String, global_tx_id: String) -> Result<()> {
        ctx.accounts.called_account.called = true;
        emit!(XTalkMessageBroadcasted {
            message: borsh::to_vec(&message).unwrap(),
            destination_network: "".to_string(),
            destination_smart_contract_address: "".to_string(),
        });
        Ok(())
    }
}


#[derive(Accounts)]
pub struct EmitEvent<'info> {
    pub payer: Signer<'info>,
}


#[derive(Accounts)]
pub struct Initialize<'info> {
    #[account(init, payer = payer, space = 8 + 1 + 3)]
    pub called_account: Account<'info, Called>,
    #[account(mut)]
    pub payer: Signer<'info>,
    pub system_program: Program<'info, System>,
}


#[derive(Accounts)]
pub struct Call<'info> {
    #[account(mut)]
    pub called_account: Account<'info, Called>,
}


#[account]
pub struct Called {
    pub called: bool, // 1, padding 3
}


#[derive(Clone, Debug, Serialize)]
#[event]
pub struct XTalkMessageBroadcasted {
    message: Vec<u8>,
    destination_network: String,
    destination_smart_contract_address: String,
}

Step 3: Update Cargo.toml

Ensure that you update at programs/flow_contract/Cargo.toml

[package]
name = "flow-contract"
version = "0.1.0"
description = "Created with Anchor"
edition = "2021"

[lib]
crate-type = ["cdylib", "lib"]
name = "flow_contract"

[features]
no-entrypoint = []
no-idl = []
no-log-ix-name = []
cpi = ["no-entrypoint"]
default = []

[dependencies]
anchor-lang = "0.29.0"
serde = { version = "1", features = ["derive"] }
serde_json = "1"

Step 4: Install Dependencies

Goto solana-project-name/flow_contract and then run below commands

npm install

Step 5: Compile the Smart Contract

anchor build

Step 6: Display the list of Key Pairs

anchor keys list

Output is YOUR_SOLANA_PROGRAM_ID. Save it as it is used later at various instances.

flow: 8LHf4FmTPrXkPg9Jtgoexj64GTE8SbaSXNJuNeQBDSUG

Step 7: Declare PROGRAM_ID

Goto program/flow_contract/src/lib.rs and update declare_id! with YOUR_SOLANA_PROGRAM_ID.

Below is the snapshot for your reference

declare_id!("YOUR_SOLANA_PROGRAM_ID");

Step 8: Configure Scripts

Create scripts folder and add below scripts to it.

  1. Script to Initiate Contract

  • Ensure that the connection is set to devnet

  • Load the wallet keypair path (for the file id.json) as YOUR_KEYPAIR_PATH

  • Update programId with YOUR_SOLANA_PROGRAM_ID

  • Update idlString with YOUR_IDL_PATH (the path to /target/idl/flow_contract.json)

//initiate_flow.ts
import * as anchor from "@coral-xyz/anchor";
import { Program, AnchorProvider, Wallet, web3, Idl } from "@coral-xyz/anchor";
import { Connection, PublicKey, Keypair } from "@solana/web3.js";
import fs from "fs";

// Set to devnet
const connection = new Connection("https://api.devnet.solana.com", "confirmed");

// Load the wallet keypair from the file id.json
const walletKeypair = Keypair.fromSecretKey(
  new Uint8Array(JSON.parse(fs.readFileSync("YOUR_KEYPAIR_PATH", "utf8")))
);
const wallet = new Wallet(walletKeypair);
const provider = new AnchorProvider(connection, wallet, AnchorProvider.defaultOptions());
anchor.setProvider(provider);

// Ensure the correct program ID and IDL are used
const programId = new PublicKey("YOUR_SOLANA_PROGRAM_ID");
const idlString = fs.readFileSync("YOUR_IDL_PATH", "utf8");
const idl = JSON.parse(idlString) as Idl;

console.log("Program ID:", programId.toString());
console.log("IDL:", idl);
console.log("Provider:", provider);

// Correctly instantiate the program
const program = new Program(idl, programId, provider as anchor.Provider);

async function airdropSol(connection: Connection, publicKey: PublicKey) {
  const airdropSignature = await connection.requestAirdrop(publicKey, web3.LAMPORTS_PER_SOL);
  await connection.confirmTransaction(airdropSignature);
}

async function callProgramFunction() {
  const dataAccount = Keypair.generate();
  console.log("data_account", dataAccount.publicKey.toBase58());

  // Check if the wallet has sufficient funds, if not, airdrop SOL to the wallet
  const balance = await connection.getBalance(provider.wallet.publicKey);
  if (balance < web3.LAMPORTS_PER_SOL) {
    await airdropSol(connection, provider.wallet.publicKey);
  }

  try {
    const txHash = await program.methods.initialize()
      .accounts({
        calledAccount: dataAccount.publicKey,
        payer: provider.wallet.publicKey,
        systemProgram: web3.SystemProgram.programId
      })
      .signers([dataAccount])
      .rpc();

    console.log("Transaction hash:", txHash);

    // Optional: Wait for confirmation
    await connection.confirmTransaction(txHash);
  } catch (err) {
    console.error("Error calling program function:", err);
  }
}

// Example usage:
callProgramFunction().catch(err => console.error("Error calling program function:", err));

  1. Configure Anchor.toml

  • Update flow with YOUR_SOLANA_PROGRAM_ID

  • Update wallet with YOUR_KEYPAIR_PATH

  • Set path for initiate_flow.ts script

[toolchain]


[features]
seeds = false
skip-lint = false

[programs.devnet]
flow_contract= "YOUR_SOLANA_PROGRAM_ID"

[registry]
url = "https://api.apr.dev"

[provider]
cluster = "devnet"
wallet = "YOUR_KEYPAIR_PATH"

[scripts]
test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts"

initiate_flow = "npx ts-node ./scripts/initiate_flow.ts"

Step 9: Compile, Deploy and Run Solana Program

  1. Compile the Solana Program

anchor build
  1. Deploy the Solana Program

anchor deploy --provider.cluster devnet
  1. Run the Solana Program

anchor run initiate_flow --provider.cluster devnet

Output contains YOUR_SOLANA_DATA_ACCOUNT and YOUR_SOLANA_TRANSACTION_HASH

Save it as it is used later.

data_account FZnsAZGFH2F6Q8uFpxq8BuR1LaJftem2rzzZFT6orKxN 
Transaction hash: 5L3mSzBpXXDFHuLBCsCjNp8Lm2MCyZEw6fiVzM14dJqcMUeGJB6m4Xy7hy9h2fZndt34CLPkUU71jxYv6UHC8TFg 

Step 3: Set up EVM Project

You can use Hardhat to compile, deploy and interact with evm contracts. Note, this example is on Sepolia.

Step 1: Initialize a New Project

  1. Create a New Directory (if you're starting fresh):

    mkdir evm-project-name
    cd evm-project-name
  2. Initialize a new NPM project:

    npm init -y

Step 2: Install Hardhat and Set Up the Project

  1. Install Hardhat:

    npm install --save-dev hardhat
  2. Set up the Hardhat project: Run the setup command and choose to create a JavaScript project:

    npx hardhat

    When prompted, select to create a JavaScript project. Follow the prompts to add a .gitignore and install the project's dependencies.

Step 3: Install Necessary Plugins and Dependencies

  1. Install the Hardhat Toolbox plugin

npm i --save-dev @nomicfoundation/hardhat-toolbox

Step 4: Write your EVM Smart Contract

Create your smart contract file as contracts/XCDPCore.sol

// XCDPCore.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;


    // message: String,
contract XCDPCore {
    event XTalkMessageBroadcasted(
        bytes message,
        string destinationNetwork,
        string destinationSmartContractAddress
    );


    event XCDPReceiveMessage(
        string message
    );


    function _l1xSend(
        string memory message,
        string memory destinationNetwork,
        string memory destinationSmartContractAddress
    ) external {
        // Convert the struct to bytes
        bytes memory messageBytes = abi.encode(message);
        emit XTalkMessageBroadcasted(messageBytes, destinationNetwork, destinationSmartContractAddress);
    }


    function _l1xReceive(bytes32 globalTxId, bytes memory message) external {
        string memory decodedMessage = abi.decode(message, (string));
        emit XCDPReceiveMessage(decodedMessage);    
    }
}

Step 5: Scripts and Configuration Settings

  1. Write deployment scripts or tests as needed, using the setup you've created. Example Provided below.

// 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);
 });
  1. Sample Hardhat Configuration file

// hardhat.config.js
require("@nomicfoundation/hardhat-toolbox");


//Update YOUR_INFURA_API_KEY and YOUR_PRIVATE_KEY
const INFURA_API_KEY = "YOUR_INFURA_API_KEY";
const SYSTEM_ACCOUNT_PRIVATE_KEY = "YOUR_PRIVATE_KEY";

/** @type import('hardhat/config').HardhatUserConfig */
module.exports = {
  solidity: {
    version: "0.8.20",
    settings: {
      optimizer: {
        enabled: true,
        runs: 5000,
      },
      viaIR: true,
    },
  },
  networks: {
    goerli: {
      url: `https://goerli.infura.io/v3/${INFURA_API_KEY}`,
      accounts: [SYSTEM_ACCOUNT_PRIVATE_KEY],
    },
    avalanche: {
      url: "https://api.avax.network/ext/bc/C/rpc",
      accounts: [SYSTEM_ACCOUNT_PRIVATE_KEY]
    },
    sepolia: {
      url: "https://rpc.sepolia.org",
      accounts: [SYSTEM_ACCOUNT_PRIVATE_KEY],
    },
    bscTestnet: {
      url: "https://data-seed-prebsc-1-s1.binance.org:8545/",
      accounts: [SYSTEM_ACCOUNT_PRIVATE_KEY],
    },
    optimistic: {
      url: `https://optimism-goerli.infura.io/v3/${INFURA_API_KEY}`,
      accounts: [SYSTEM_ACCOUNT_PRIVATE_KEY],
    },
    devnet: {
      url: "http://localhost:8545",
      accounts: [SYSTEM_ACCOUNT_PRIVATE_KEY],
    }
  }
};

Step 6: Contract Deployment

npx hardhat run scripts/deploy.js --network sepolia

Output you get YOUR_EVM_CONTRACT_ADDRESS. Save it for now.

Compiled 1 Solidity file successfully (evm target: paris).
Deploying contracts with the account: YOUR_EVM_WALLET_ADDRESS
Account balance: YOUR_WALLET_BALANCE
XCDP contract deployed to: YOUR_EVM_CONTRACT_ADDRESS

II. Solana to EVM Integration

This section is dedicated to send a message from Solana to any EVM-compatible chain, Sepolia in this example.

Step 1: Implementing L1X X-Talk Solana-EVM Flow Contract

Flow Contracts are at the core of 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-sol-evm

Use the provided code below to implement the cross-chain xcdp Solana to EVM logic.

Create your L1X XCDP Solana-EVM Flow Contact at src/lib.rs

//lib.rs
use borsh::{BorshDeserialize, BorshSerialize};
use l1x_sdk::caller_address;
use l1x_sdk::{call_contract, contract, contract_interaction::ContractCall, store::LookupMap};
use serde::{Deserialize, Serialize};

const STORAGE_CONTRACT_KEY: &[u8; 7] = b"message";
const STORAGE_EVENTS_KEY: &[u8; 6] = b"events";
const XTALK_GATEWAY: &str = "cc274587e4f014c21cdb5942cf7cceb4f1339f58";

#[derive(Clone, Debug, Serialize, BorshSerialize, BorshDeserialize)]
pub struct XTalkMessageBroadcasted {
    data: Vec<u8>,
    destination_network: String,
    destination_smart_contract_address: String,
}

#[derive(Clone, Debug, BorshSerialize, BorshDeserialize, Serialize, Deserialize)]
pub struct XCDPSendMessage {
    message: String,
}

#[derive(Clone, Debug, BorshSerialize, BorshDeserialize, Serialize, Deserialize)]
pub struct Payload {
    data: Vec<u8>,
    destination_network: String,
    destination_contract_address: String,
}


#[derive(BorshSerialize, BorshDeserialize)]
pub struct XCDPCore {
    events: LookupMap<String, XCDPSendMessage>,
    total_events: u64,
}

impl Default for XCDPCore {
    fn default() -> Self {
        Self {
            events: LookupMap::new(STORAGE_EVENTS_KEY.to_vec()),
            total_events: u64::default(),
        }
    }
}

#[contract]
impl XCDPCore {
    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")
            }
        }
    }

    /// Save contract 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"),
        };
    }

    pub fn new() {
        let mut contract = Self::default();
        contract.save();
    }

    pub fn save_event_data(event_data: Vec<u8>, global_tx_id: String) {
        assert_eq!(
            caller_address(),
            l1x_sdk::types::Address::try_from(XTALK_GATEWAY).unwrap()
        );
        l1x_sdk::msg(&format!(
            "********************global tx id {} **************",
            global_tx_id
        ));

        let mut contract = Self::load();

        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_str = String::from_utf8(event_data.clone()).unwrap();

        let param_types = vec!["string".to_string()];
        let event = XTalkMessageBroadcasted::decode_event_data(&event_data_str).unwrap();
        if let Ok(message) = String::try_from_slice(&event.data) {
            let payload = XCDPCore::get_evm_payload(
                message,
                param_types,
                global_tx_id,
                event.destination_network,
                event.destination_smart_contract_address,
            );
            l1x_sdk::emit_event_experimental(payload);
        }
        contract.save()
    }

    fn get_evm_payload(
        message: String,
        param_types: Vec<String>,
        global_tx_id: String,
        destination_network: String,
        destination_contract_address: String,
    ) -> Payload {
        let mut params: Vec<Vec<u8>> = vec![];
        params.push(serde_json::to_vec(&message).unwrap());

        let args = {
            #[derive(Serialize)]
            struct Args {
                params: Vec<Vec<u8>>,
                param_types: Vec<String>,
                global_tx_id: String,
            }
            Args {
                params,
                param_types,
                global_tx_id,
            }
        };

        let call = ContractCall {
            contract_address: l1x_sdk::types::Address::try_from(
                "14448a8530d3798ff67a33102f5eb92a5de75c6f",
            )
            .unwrap(),
            method_name: "get_byte_code".to_string(),
            args: serde_json::to_vec(&args).unwrap(),
            gas_limit: 3_000_000,
            read_only: true,
        };

        let response = call_contract(&call).unwrap();
        let data = serde_json::from_slice::<Vec<u8>>(&response).unwrap();
        l1x_sdk::msg(&format!("{:#?}", data));

        Payload {
            data,
            destination_contract_address,
            destination_network,
        }
    }

    pub fn get_events() -> u64 {
        Self::load().total_events
    }
}


impl XTalkMessageBroadcasted {
    pub fn decode_event_data(data: &str) -> Result<Self, Box<dyn std::error::Error>> {
        let decoded_data = base64::decode(data)?;
        // let discriminator = &decoded_data[0..8];
        let payload = &decoded_data[8..];

        let event = Self::deserialize(&mut &payload[..])?;
        Ok(event)
    }
}

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 = "l1x-sol-evm-xcdp"
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"
getrandom = { version = "0.2.10", features = ["js"] }
log = "0.4.20"
bincode = "1.3.3"
base64 = "*"

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 on 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

Deploy your X-Talk Contract with l1x-cli-beta

l1x-cli-beta contract deploy /path/to/object/file --endpoint <https://v2-testnet-rpc.l1x.foundation>

Response expected:

Contract Deployed Successfully:
-----------------------------------------------------------------------
{
  "contract_address": "YOUR_CONTRACT_ADDRESS",
  "hash": "HASH"
}

To initiate your smart contract, use this command:

l1x-cli-beta contract init YOUR_CONTRACT_ADDRESS --endpoint <https://v2-testnet-rpc.l1x.foundation> --fee_limit 100000 

Response expected:

Contract Initialized Successfully:
-----------------------------------------------------------------------
{
  "contract_address": "YOUR_INSTANCE_ADDRESS",
  "hash": "HASH"
}

The instance address is your contract address which you can use to call your functions and interact.

Step 2: Register Solana Contract and X-Talk Solana-EVM Flow Contract with X-Talk Node

X-Talk Nodes allow for data listening and send to the X-Talk Solana-EVM Flow contract.

To register your addresses to the source registry, you need to call this:

l1x-cli-beta contract call SOURCE_REGISTRY_INSTANCE_ADDRESS register_new_source --args "{\\"destination_contract_address\\": \\"<YOUR_FLOW_CONTRACT_ADDRESS>\\", \\
\\"source_contract_address\\": \\"YOUR_SOLANA_PROGRAM_ID\\", \\
\\"source_chain\\": \\"Solana\\", \\
\\"event_filters\\": []}" --endpoint <https://v2-testnet-rpc.l1x.foundation> --fee_limit 100000

SOURCE_REGISTRY_INSTANCE_ADDRESS => This is provided
source_contract_address => YOUR_SOLANA_PROGRAM_ID
YOUR_FLOW_CONTRACT_ADDRESS => The initiated X-TalK Solana - EVM Flow Contract Address
Note that event_filters is blank for Solana

Request Example:

l1x-cli-beta contract call b97f1a3708ae6d7df6ada0c695ce29e8acef954e register_new_source --args "{\"destination_contract_address\": \"b87220f3d6851d5efb2f5a5abd5782f76df72477\", \"source_contract_address\": \"Gsd8t91TRyzd3ASwXBS45gqDYCsSZgnPJpJYQoBJCKcb\", \"source_chain\": \"Solana\", \"event_filters\": []}" --endpoint https://v2-testnet-rpc.l1x.foundation --fee_limit 100000

Step 3: Implementing Script to Emit Event from Solana to EVM

Step 1: Add Script to Emit Events

In your solana project, inside scripts folder add emit_event.ts script.

  • Update walletKeypair with YOUR_KEYPAIR_PATH

  • emitEvent() arguments are in the sequence: message, destinationNetwork and destinationAddress.

    • message is set to "Hello world!". You can send the message you want.

    • destinationNetwork is set to "sepolia". (Update your EVM-compatible destination network here.)

    • Update destinationAddress with YOUR_EVM_CONTRACT_ADDRESS

// emit_event.ts

import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { FlowContract } from "../target/types/flow_contract";
import { Keypair } from "@solana/web3.js";
import fs from "fs";


//set this "https://api.devnet.solana.com" in case of using devnet instead of localnet
const connection = new anchor.web3.Connection("https://api.devnet.solana.com", "confirmed");


// Load the wallet keypair from the file - path to id.json
const walletKeypair = Keypair.fromSecretKey(
  new Uint8Array(JSON.parse(fs.readFileSync("YOUR_KEYPAIR_PATH", "utf8")))
);
const wallet = new anchor.Wallet(walletKeypair);
const provider = new anchor.AnchorProvider(connection, wallet, anchor.AnchorProvider.defaultOptions());
anchor.setProvider(provider);


const program = anchor.workspace.FlowContract as Program<FlowContract>;


async function callProgramFunction() {
  const payer = provider.wallet;

// Update YOUR_AVAX_CONTRACT_ADDRESS. Hellow world! is the message you want to send to EVM chain. Feel free to update message.
  try {
    const txHash = await program.methods.emitEvent("Hello World", "sepolia", "EVM_CONTRACT_ADDRESS")
      .accounts({
        payer: payer.publicKey
      })
      .rpc();


    console.log("Transaction hash:", txHash);


    // Increase timeout period for confirmation
    const confirmed = await connection.confirmTransaction(txHash, 'confirmed');


    console.log("Confirmation:", confirmed);
  } catch (err) {
    console.error("Error calling program function:", err);
  }
}


// Example usage:
callProgramFunction().catch(err => console.error("Error calling program function:", err));

Step 2: Update script path in Anchor.toml

Add the path for emit_event.ts script in Anchor.toml

[toolchain]


[features]
seeds = false
skip-lint = false

[programs.devnet]
flow_contract= "YOUR_SOLANA_PROGRAM_ID"

[registry]
url = "https://api.apr.dev"

[provider]
cluster = "devnet"
wallet = "YOUR_KEYPAIR_PATH"

[scripts]
test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts"

initiate_flow = "npx ts-node ./scripts/initiate_flow.ts"
emit_event = "npx ts-node ./scripts/emit_event.ts"

Step 4: Send Message from Solana to EVM

anchor run emit_event --provider.cluster devnet

To verify that event is received at YOUR_EVM_CONTRACT_ADDRESS, goto EVM blockchain explorer, check for the transaction with X-Talk Gateway for Sepolia mentioned in the table.

III. EVM to Solana Integration

This section is dedicated to send a message from any EVM-compatible chain (Sepolia in this example) to Solana.

Step 1: Implementing L1X X-Talk EVM-Solana Flow Contract

Flow Contracts are at the core of cross-chain application development.

Create a new L1X project that provides the necessary files to get started

cargo l1x create xcdp-evm-sol

Use the provided code below to implement the EVM to Solana cross-chain logic.

Create your L1X XCDP EVM-Solana Flow Contact at src/lib.rs

// lib.rs

use borsh::{BorshDeserialize, BorshSerialize};
use l1x_sdk::{call_contract, contract, contract_interaction::ContractCall, store::LookupMap};
use serde::{Deserialize, Serialize};
const STORAGE_CONTRACT_KEY: &[u8; 7] = b"message";
const STORAGE_EVENTS_KEY: &[u8; 6] = b"events";

#[derive(Clone, Debug, BorshSerialize, BorshDeserialize, Serialize, Deserialize)]
pub struct Payload {
    data: Vec<u8>,
    destination_network: String,
    destination_contract_address: String,
}

#[derive(Clone, Debug, BorshSerialize, BorshDeserialize)]
pub struct L1xReceiveInstruction {
    message: String,
    global_tx_id: String,
}

#[derive(BorshSerialize, BorshDeserialize)]
pub struct XCDPCore {
    events: LookupMap<String, String>,
    total_events: u64,
}

impl Default for XCDPCore {
    fn default() -> Self {
        Self {
            events: LookupMap::new(STORAGE_EVENTS_KEY.to_vec()),
            total_events: u64::default(),
        }
    }
}

#[contract]
impl XCDPCore {
    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")
            }
        }
    }

    /// Save contract 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"),
        };
    }

    pub fn new() {
        let mut contract = Self::default();
        contract.save();
    }

    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();

        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 param_types = vec!["string".to_string()];
        let (response, destination_network, destination_contract_address) =
            Self::parse_ethereum_log(event_data, param_types);

        let event = serde_json::from_slice::<String>(&response[0]).unwrap();

        contract.save_message_event(
            global_tx_id,
            event,
            destination_network,
            destination_contract_address,
        );
        contract.save()
    }

    fn save_message_event(
        &mut self,
        global_tx_id: String,
        event_data: String,
        destination_network: String,
        destination_contract_address: String,
    ) {
        self.events.insert(global_tx_id.clone(), event_data.clone());
        l1x_sdk::msg(&format!("{:#?}", event_data));
        let (flow_program_id, flow_data_account) = destination_contract_address.split_at(44);

        let payload = Self::get_solana_payload(
            event_data.as_bytes().to_vec(),
            global_tx_id,
            destination_network,
            flow_program_id.to_string(),
            flow_data_account.to_string(),
        );

        l1x_sdk::emit_event_experimental(payload);
    }

    pub fn to_key(global_tx_id: String, event_type: String) -> String {
        global_tx_id.to_owned() + "-" + &event_type
    }

    fn parse_ethereum_log(
        event_data: Vec<u8>,
        param_types: Vec<String>,
    ) -> (Vec<Vec<u8>>, String, String) {
        let args = {
            #[derive(Serialize)]
            struct Args {
                event_data: Vec<u8>,
                param_types: Vec<String>,
            }
            Args {
                event_data,
                param_types,
            }
        };

        let call = ContractCall {
            contract_address: l1x_sdk::types::Address::try_from(
                "743449c47c9657a0fb5271416b81c7c450ac15f2",
            )
            .unwrap(),
            method_name: "parse_ethereum_log".to_string(),
            args: serde_json::to_vec(&args).unwrap(),
            gas_limit: 3_000_000,
            read_only: true,
        };

        let response = call_contract(&call).unwrap();
        serde_json::from_slice::<(Vec<Vec<u8>>, String, String)>(&response).unwrap()
    }

    fn get_solana_payload(
        payload: Vec<u8>,
        global_tx_id: String,
        destination_network: String,
        flow_program_id: String,
        flow_data_account: String,
    ) -> Payload {
        let args = {
            #[derive(Serialize)]
            struct Args {
                payload: Vec<u8>,
                global_tx_id: String,
                flow_program_id: String,
                flow_data_account: String,
            }
            Args {
                payload,
                global_tx_id,
                flow_program_id: flow_program_id.clone(),
                flow_data_account,
            }
        };

        let call = ContractCall {
            contract_address: l1x_sdk::types::Address::try_from(
                "29169c5cbceedb76e8ce14ed05616f452b4d63fe",
            )
            .unwrap(),
            method_name: "get_solana_payload".to_string(),
            args: serde_json::to_vec(&args).unwrap(),
            gas_limit: 3_000_000,
            read_only: true,
        };

        let response = call_contract(&call).unwrap();
        let data = serde_json::from_slice::<Vec<u8>>(&response).unwrap();
        Payload {
            data,
            destination_network,
            destination_contract_address: flow_program_id,
        }
    }
}

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.

// Cargo.toml

[package]
name = "evm-solana"
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"
getrandom = { version = "0.2.10", features = ["js"] }
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

Deploy your X-Talk Contract with l1x-cli-beta

l1x-cli-beta contract deploy /path/to/object/file --endpoint <https://v2-testnet-rpc.l1x.foundation>

Response expected:

Contract Deployed Successfully:
-----------------------------------------------------------------------
{
  "contract_address": "YOUR_CONTRACT_ADDRESS",
  "hash": "HASH"
}

To initiate your smart contract, use this command:

l1x-cli-beta contract init YOUR_CONTRACT_ADDRESS --endpoint <https://v2-testnet-rpc.l1x.foundation> --fee_limit 100000 

Response expected:

Contract Initialized Successfully:
-----------------------------------------------------------------------
{
  "contract_address": "YOUR_INSTANCE_ADDRESS",
  "hash": "HASH"
}

The instance address is your contract address which you can use to call your functions and interact.

Step 2: Register Sepolia Contract and X-Talk EVM-Solana Flow Contract with X-Talk Node

X-Talk Nodes allow for data listening and send to the X-Talk EVM-Solana Flow contract.

To register your addresses to the source registry, you need to call this:

l1x-cli-beta contract call SOURCE_REGISTRY_INSTANCE_ADDRESS register_new_source --args "{\\"destination_contract_address\\": \\"<YOUR_FLOW_CONTRACT_ADDRESS>\\", \\
\\"source_contract_address\\": \\"YOUR_EVM_CONTRACT_ADDRESS\\", \\
\\"source_chain\\": \\"Sepolia\\", \\
\\"event_filters\\": [\"TOPIC_OF_YOUR_EVENT\"]}" --endpoint <https://v2-testnet-rpc.l1x.foundation> --fee_limit 100000

SOURCE_REGISTRY_INSTANCE_ADDRESS => This is provided
source_contract_address => YOUR_EVM_CONTRACT_ADDRESS
TOPIC_OF_YOUR_EVENT => Topic of the event
YOUR_FLOW_CONTRACT_ADDRESS => The initiated X-TalK EVM-Solana Flow Contract Address

Request Example:

l1x-cli-beta contract call 78c4fa1139a9f86693f943e2184256b868c3c716 register_new_source --args "{\\"destination_contract_address\\": \\"3011d3226cd06tf6d4d28fw0acfe003da6daad93\\", \\
\\"source_contract_address\\": \\"AFcd2ad1B4ECB6B2330876590a9247050ba0B3f2\\", \\
\\"source_chain\\": \\"Sepolia\\", \\
\\"event_filters\\": [\"5c6877990d83003ae27cf7c8f1a9d622868080df757847943133b78663358e42\"]}" --

Step 3: Implementing Script to Emit Event from EVM to Solana

Step 1: Add Script to Emit Events

In your EVM project, inside scripts folder add emit_event.js script.

  • Update XCDPContractAddress with YOUR_EVM_CONTRACT_ADDRESS

  • _l1xSend() arguments are in the sequence: message, destinationNetwork and destinationAddress.

    • message is set to "Hello from L1X!". You can send the message you want.

    • destinationNetwork is set to "Solana".

    • Update destinationAddress with Solana Program ID concatenated with Solana Data Account i.e. YOUR_SOLANA_PROGRAM_ID_YOUR_SOLANA_DATA_ACCOUNT

// emit_event.js
const hre = require("hardhat");


const { ethers } = hre;


async function main() {
    const [deployer, user] = await ethers.getSigners();


    console.log("Deployer address:", deployer.address);


    const XCDP = await ethers.getContractFactory("XCDPCore");
    const XCDPContractAddress = "YOUR_EVM_CONTRACT_ADDRESS";
    const XCDPContract = XCDP.attach(XCDPContractAddress);


    let tx = await XCDPContract.connect(deployer)._l1xSend(
        "Hello from L1X!",
        "Solana",
        "YOUR_SOLANA_PROGRAM_ID_YOUR_SOLANA_DATA_ACCOUNT"
    );


    let receipt = await tx.wait(); // Wait for the transaction to be mined


    // Get the logs from the receipt
    let logs = receipt.logs;


    // Assuming there is only one log emitted
    if (logs.length > 0) {
        console.log("Event topic:", logs[0].topics[0]);
    } else {
        console.log("No logs emitted.");
    }


    console.log("send message tx hash:", tx.hash);
}


main()
    .then(() => process.exit(0))
    .catch((error) => {
        console.error(error);
        process.exit(1);
    });
```

Step 4: Send Message from Sepolia to Solana

npx hardhat run --network sepolia ./scripts/emit_event.js

To verify that event is received at YOUR_SOLANA_PROGRAM_ID, go to Solana explorer, check for the transaction with X-Talk Gateway for Solana mentioned in the table.

IV. X-Talk Gateway Contract Address

Below table contains a list of contract address that serve as an authenticated entry to interact with X-Talk from respective client chain network.

Last updated