Build your first X-Talk Swap Solana to EVM Contract and X-Talk Swap EVM to Solana Contract
Step by Step guide with detailed process. In this guide we will use Solana devnet, Avalanche mainnet and L1X TestNet to deploy X-Talk Flow Contract.
Building Blocks for X-Talk Swap: Solana-EVM and EVM-Solana
Smart Contracts on Solana that will be the endpoints for swapping tokens. This is runtime agnostic.
L1X X-Talk Solana-EVM Flow Contract Deployment and Initialisation to facilitate token swapping
L1X X-Talk EVM-Solana Flow Contract Deployment and Initialisation to facilitate token swapping
Smart Contracts on EVM-compatible chain (Avalanche in this example) that will be the endpoints for swapping tokens. This is runtime agnostic.
X-Talk Process
This page is divided into 4 parts, for the ease of understanding, as listed below
Basic Steps: Common for both Solana-EVM and EVM-Solana
Solana to EVM Token Swapping
EVM to Solana Token Swapping
X-Talk Gateway Contract Address
I. Basic Steps
This section is categorised into Pre-Requisites, Set up Solana Project and Set up Avalanche 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
Create a New Directory
mkdir solana-swap-project
cd solana-swap-project
Initialize a New Anchor Project
anchor init cross_chain_swap
Step 2: Write your Solana smart contract
In this, below listed smart contracts are to be created at /programs/cross_chain_swap/src/
lib.rs
swap_accounts.rs
instructions.rs
events.rs
errors.rs
utils.rs
Paste your contract code at /programs/cross_chain_swap/src/lib.rs
Update declare_id! with YOUR_SOLANA_PROGRAM_ID
// lib.rs
use anchor_lang::prelude::*;
use anchor_lang::system_program::{transfer, Transfer as NativeTransfer};
use anchor_spl::token;
use anchor_spl::token::{transfer_checked, Transfer, TransferChecked};
use serde::{Deserialize, Serialize};
mod swap_accounts;
use swap_accounts::SwapRequest;
// use swap_accounts::StateAccount;
mod instructions;
use instructions::*;
mod errors;
use errors::ErrorCode;
mod events;
use events::*;
// mod utils;
// use utils::*;
use crate::swap_accounts::TokenNamesMapping;
//Update with YOUR_SOLANA_PROGRAM_ID
declare_id!("GyGqjx6TCiGRpzk6NToj1ri6LHJ95wQD5rPYQM6ucE3A");
#[program]
pub mod cross_chain_swap {
use solana_program::{program::invoke_signed, system_instruction};
use super::*;
pub fn initialize(
ctx: Context<Initialize>,
native_asset_decimals: u8,
authorized_gateway: Pubkey,
admins: Vec<Pubkey>,
treasury_address: Pubkey,
treasury_share_percent: u8,
source_native_fee: u64,
token_names_mapping: Vec<TokenNamesMapping>
) -> Result<()> {
let state_account = &mut ctx.accounts.state_account;
state_account.native_asset_decimals = native_asset_decimals as u32;
state_account.authorized_gateway = authorized_gateway;
state_account.admins = admins;
state_account.treasury_address = treasury_address;
state_account.treasury_share_percent = treasury_share_percent as u32;
state_account.source_native_fee = source_native_fee;
state_account.token_names_mapping = token_names_mapping;
Ok(())
}
pub fn set_treasury_address(ctx: Context<SetTreasuryAddress>, treasury_address: Pubkey) -> Result<()> {
let state_account = &mut ctx.accounts.state_account;
state_account.treasury_address = treasury_address;
Ok(())
}
pub fn set_treasury_share_percent(
ctx: Context<SetTreasurySharePercentage>,
treasury_share_percent: u8,
) -> Result<()> {
let state_account = &mut ctx.accounts.state_account;
state_account.treasury_share_percent = treasury_share_percent as u32;
Ok(())
}
pub fn set_source_native_fee(ctx: Context<SetSourceNativeFee>, source_native_fee: u64) -> Result<()> {
let state_account = &mut ctx.accounts.state_account;
state_account.source_native_fee = source_native_fee;
Ok(())
}
pub fn set_authorized_gateway(
ctx: Context<SetAuthorizedL1XGateway>,
authorized_gateway: Pubkey,
) -> Result<()> {
let state_account = &mut ctx.accounts.state_account;
state_account.authorized_gateway = authorized_gateway;
Ok(())
}
pub fn initiate_swap_erc20(
ctx: Context<InitiateSwapErc20>,
internal_id: String,
source_amount: u64,
destination_amount: u64,
receiver_address: String,
source_asset_address: Pubkey,
destination_asset_address: String,
destination_contract_address: String,
source_asset_symbol: String,
destination_asset_symbol: String,
destination_network: String,
conversion_rate_id: String,
) -> Result<()> {
let state_account = &ctx.accounts.state_account;
let names_mapping = &state_account.token_names_mapping;
let mut xtalk_network_name = "".to_string();
let destination_network = destination_network.as_str(); // Convert to &str for match statement
for map in names_mapping {
if map.application_name == destination_network {
xtalk_network_name = map.xtalk_name.clone();
break;
}
}
// check that the signer have enough funds for the fees.
require!(
ctx.accounts.signer.get_lamports() >= state_account.source_native_fee,
ErrorCode::NotEnoughFunds
);
if source_amount == 0 {
return Err(ErrorCode::InvalidAmount.into());
}
// let treasury_share = source_amount * state_account.treasury_share_percent as u64 / 100
// + state_account.source_native_fee;
// let program_share = source_amount - treasury_share;
// Transfer the source native fee to the treasury
if state_account.source_native_fee > 0 {
let cpi_context = CpiContext::new(
ctx.accounts.system_program.to_account_info(),
NativeTransfer {
from: ctx.accounts.signer.to_account_info(),
to: ctx.accounts.treasury.to_account_info(),
},
);
transfer(cpi_context, state_account.source_native_fee)?;
}
// Calculate from SOL20 token
let treasury_share = source_amount
.checked_mul(state_account.treasury_share_percent as u64)
.and_then(|result| result.checked_div(100))
.ok_or(ErrorCode::ArithmeticError)?;
let program_share = source_amount.checked_sub(treasury_share).ok_or(ErrorCode::ArithmeticError)?;
let transfer_instruction = Transfer {
from: ctx.accounts.signer_token_account.to_account_info(),
to: ctx.accounts.program_token_account.to_account_info(),
authority: ctx.accounts.signer.to_account_info(),
};
anchor_spl::token::transfer(
CpiContext::new(
ctx.accounts.token_program.to_account_info(),
transfer_instruction,
),
program_share,
)?;
let transfer_instruction = Transfer {
from: ctx.accounts.signer_token_account.to_account_info(),
to: ctx.accounts.treasury.to_account_info(),
authority: ctx.accounts.signer.to_account_info(),
};
anchor_spl::token::transfer(
CpiContext::new(
ctx.accounts.token_program.to_account_info(),
transfer_instruction,
),
treasury_share,
)?;
let swap_request = SwapRequest {
source_amount,
destination_amount,
sender_address: ctx.accounts.signer.key(),
receiver_address,
source_asset_address,
destination_asset_address,
source_asset_symbol,
destination_asset_symbol,
destination_contract_address: destination_contract_address.clone(),
destination_network: xtalk_network_name.clone(),
source_contract_address: *ctx.program_id,
source_chain: "solana".to_string(),
conversion_rate_id,
internal_id,
};
emit!(XTalkMessageBroadcasted {
message: borsh::to_vec(&swap_request).unwrap(),
destination_network: xtalk_network_name.clone(),
destination_contract_address: destination_contract_address.to_string(),
});
let swap_initiated =
SwapInitiated::from_swap_request(swap_request, ctx.accounts.signer.key());
emit!(swap_initiated.clone());
let swap_data = &mut ctx.accounts.swap_data;
swap_data.source_amount = swap_initiated.source_amount;
swap_data.destination_amount = swap_initiated.destination_amount;
swap_data.sender_address = swap_initiated.sender_address;
swap_data.receiver_address = swap_initiated.receiver_address;
swap_data.source_asset_address = swap_initiated.source_asset_address;
swap_data.destination_asset_address = swap_initiated.destination_asset_address;
swap_data.destination_contract_address = swap_initiated.destination_contract_address;
swap_data.source_asset_symbol = swap_initiated.source_asset_symbol;
swap_data.destination_asset_symbol = swap_initiated.destination_asset_symbol;
swap_data.destination_network = swap_initiated.destination_network;
swap_data.conversion_rate_id = swap_initiated.conversion_rate_id;
swap_data.internal_id = swap_initiated.internal_id;
Ok(())
}
pub fn initiate_swap_native(
ctx: Context<InitiateSwapNative>,
internal_id: String,
source_amount: u64,
destination_amount: u64,
receiver_address: String,
source_asset_address: Pubkey,
destination_asset_address: String,
destination_contract_address: String,
source_asset_symbol: String,
destination_asset_symbol: String,
destination_network: String,
conversion_rate_id: String,
) -> Result<()> {
let state_account = &ctx.accounts.state_account;
let names_mapping = &state_account.token_names_mapping;
let mut xtalk_network_name = "".to_string();
let destination_network = destination_network.as_str(); // Convert to &str for match statement
for map in names_mapping {
if map.application_name == destination_network {
xtalk_network_name = map.xtalk_name.clone();
break;
}
}
// check that the signer have enough funds for the fees + transfer
require!(
ctx.accounts.signer.get_lamports() >= source_amount + state_account.source_native_fee,
ErrorCode::NotEnoughFunds
);
if source_amount == 0 || source_amount < state_account.source_native_fee {
return Err(ErrorCode::InvalidAmount.into());
}
// let treasury_share = source_amount * state_account.treasury_share_percent as u64 / 100
// + state_account.source_native_fee;
// let program_share = source_amount - treasury_share;
let treasury_share = source_amount
.checked_mul(state_account.treasury_share_percent as u64)
.and_then(|result| result.checked_div(100))
.and_then(|result| result.checked_add(state_account.source_native_fee))
.ok_or(ErrorCode::ArithmeticError)?;
let program_share = source_amount
.checked_sub(treasury_share)
.ok_or(ErrorCode::ArithmeticError)?;
let cpi_context = CpiContext::new(
ctx.accounts.system_program.to_account_info(),
NativeTransfer {
from: ctx.accounts.signer.to_account_info(),
to: ctx.accounts.program_token_account.to_account_info(),
},
);
transfer(cpi_context, program_share)?;
let cpi_context = CpiContext::new(
ctx.accounts.system_program.to_account_info(),
NativeTransfer {
from: ctx.accounts.signer.to_account_info(),
to: ctx.accounts.treasury.to_account_info(),
},
);
transfer(cpi_context, treasury_share)?;
let swap_request = SwapRequest {
source_amount,
destination_amount,
sender_address: ctx.accounts.signer.key(),
receiver_address,
source_asset_address,
destination_asset_address,
source_asset_symbol,
destination_asset_symbol,
destination_contract_address: destination_contract_address.clone(),
destination_network: xtalk_network_name.clone(),
source_contract_address: *ctx.program_id,
source_chain: "solana".to_string(),
conversion_rate_id,
internal_id,
};
emit!(XTalkMessageBroadcasted {
message: borsh::to_vec(&swap_request).unwrap(),
destination_network: xtalk_network_name.clone(),
destination_contract_address: destination_contract_address.to_string(),
});
let swap_initiated =
SwapInitiated::from_swap_request(swap_request, ctx.accounts.signer.key());
emit!(swap_initiated.clone());
let swap_data = &mut ctx.accounts.swap_data;
swap_data.source_amount = swap_initiated.source_amount;
swap_data.destination_amount = swap_initiated.destination_amount;
swap_data.sender_address = swap_initiated.sender_address;
swap_data.receiver_address = swap_initiated.receiver_address;
swap_data.source_asset_address = swap_initiated.source_asset_address;
swap_data.destination_asset_address = swap_initiated.destination_asset_address;
swap_data.destination_contract_address = swap_initiated.destination_contract_address;
swap_data.source_asset_symbol = swap_initiated.source_asset_symbol;
swap_data.destination_asset_symbol = swap_initiated.destination_asset_symbol;
swap_data.destination_network = swap_initiated.destination_network;
swap_data.conversion_rate_id = swap_initiated.conversion_rate_id;
swap_data.internal_id = swap_initiated.internal_id;
Ok(())
}
pub fn execute_swap_native(
ctx: Context<ExecuteSwapNative>,
global_tx_id: String,
internal_id: String,
message: Vec<u8>,
) -> Result<()> {
#[derive(Serialize, Deserialize, PartialEq, Debug, borsh::BorshDeserialize)]
struct Data {
amount: u64,
status: bool,
status_message: String,
}
let Data {
amount,
status,
status_message,
} = serde_json::from_slice(&message).unwrap();
let seeds = &[
"program_token_account".as_bytes(),
&[ctx.bumps.program_token_account],
];
let seeds_account = [&seeds[..]];
let cpi_context = CpiContext::new_with_signer(
ctx.accounts.system_program.to_account_info(),
NativeTransfer {
from: ctx.accounts.program_token_account.to_account_info(),
to: ctx.accounts.user_token_account.to_account_info(),
},
&seeds_account,
);
transfer(cpi_context, amount)?;
emit!(SwapFullfilled {
global_tx_id: global_tx_id.clone(),
internal_id: internal_id.clone(),
amount,
receiver_address: ctx.accounts.user_token_account.key(),
asset_address: Pubkey::default(),
status,
status_message: status_message.clone(),
});
Ok(())
}
pub fn execute_swap_erc20(
ctx: Context<ExecuteSwapErc20>,
internal_id: String,
global_tx_id: String,
message: Vec<u8>,
) -> Result<()> {
#[derive(Serialize, Deserialize, PartialEq, Debug)]
struct Data {
amount: u64,
status: bool,
status_message: String,
}
let Data {
amount,
status,
status_message,
} = serde_json::from_slice(&message).unwrap();
let seeds = &["state_account".as_bytes(), &[ctx.bumps.state_account]];
let seeds_account = [&seeds[..]];
let cpi_context = CpiContext::new_with_signer(
ctx.accounts.token_program.to_account_info(),
TransferChecked {
from: ctx.accounts.program_token_account.to_account_info(),
to: ctx.accounts.user_token_account.to_account_info(),
authority: ctx.accounts.state_account.to_account_info(),
mint: ctx.accounts.token_mint.to_account_info(),
},
&seeds_account,
);
transfer_checked(cpi_context, amount, ctx.accounts.token_mint.decimals)?;
emit!(SwapFullfilled {
global_tx_id: global_tx_id.clone(),
internal_id: internal_id.clone(),
amount,
receiver_address: ctx.accounts.user_token_account.key(),
asset_address: ctx.accounts.token_mint.key(),
status,
status_message: status_message.clone(),
});
Ok(())
}
pub fn transfer_to_treasury_native(
ctx: Context<TransferToTreasuryNative>,
amount: u64,
account_type: String,
) -> Result<()> {
let state_account = &ctx.accounts.state_account;
require!(
state_account.treasury_address != Pubkey::default(),
ErrorCode::TreasuryNotSet
);
require!(
state_account.treasury_address == ctx.accounts.treasury_account.key(),
ErrorCode::InvalidTreasuryAddress
);
match account_type.as_str() {
"program_token_account" => {
let pda = ctx.accounts.program_token_account.to_account_info();
let recipient = ctx.accounts.treasury_account.to_account_info();
let system_program = ctx.accounts.system_program.to_account_info();
let bump = ctx.bumps.program_token_account;
let seeds = &[b"program_token_account".as_ref(), &[bump]];
let transfer_instruction = system_instruction::transfer(
&pda.key,
&recipient.key,
amount,
);
invoke_signed(
&transfer_instruction,
&[pda, recipient, system_program],
&[&seeds[..]],
)?;
Ok(())
}
"state_account" => {
let pda = ctx.accounts.state_account.to_account_info();
let recipient = ctx.accounts.treasury_account.to_account_info();
let system_program = ctx.accounts.system_program.to_account_info();
let bump = ctx.bumps.state_account;
let seeds = &[b"state_account".as_ref(), &[bump]];
let transfer_instruction = system_instruction::transfer(
&pda.key,
&recipient.key,
amount,
);
invoke_signed(
&transfer_instruction,
&[pda, recipient, system_program],
&[&seeds[..]],
)?;
Ok(())
}
_ => {
return Err(ErrorCode::InvalidAccountType.into());
}
}
}
pub fn transfer_to_treasury_erc20(
ctx: Context<TransferToTreasuryErc20>,
amount: u64,
account_type: String,
) -> Result<()> {
let state_account = &ctx.accounts.state_account;
require!(
state_account.treasury_address != Pubkey::default(),
ErrorCode::TreasuryNotSet
);
require!(
state_account.treasury_address == ctx.accounts.treasury_account.key(),
ErrorCode::InvalidTreasuryAddress
);
match account_type.as_str() {
"program_token_account" => {
let cpi_accounts = Transfer {
from: ctx.accounts.program_token_account.to_account_info(),
to: ctx.accounts.treasury_account.to_account_info(),
authority: ctx.accounts.signer.to_account_info(),
};
let cpi_program = ctx.accounts.token_program.to_account_info();
let cpi_ctx = CpiContext::new(cpi_program, cpi_accounts);
token::transfer(cpi_ctx, amount)?;
Ok(())
}
"state_account" => {
let cpi_accounts = Transfer {
from: ctx.accounts.state_account.to_account_info(),
to: ctx.accounts.treasury_account.to_account_info(),
authority: ctx.accounts.signer.to_account_info(),
};
let cpi_program = ctx.accounts.token_program.to_account_info();
let cpi_ctx = CpiContext::new(cpi_program, cpi_accounts);
token::transfer(cpi_ctx, amount)?;
Ok(())
}
_ => {
return Err(ErrorCode::InvalidAccountType.into());
}
}
}
pub fn add_new_network(
ctx: Context<UpdateNetwork>,
new_network: TokenNamesMapping,
) -> Result<()> {
ctx.accounts.state_account.token_names_mapping.push(new_network);
Ok(())
}
pub fn remove_new_network(
ctx: Context<UpdateNetwork>,
application_name: String,
) -> Result<()> {
let mut idx_to_remove: Option<usize> = None;
for (idx, map) in ctx.accounts.state_account.token_names_mapping.iter().enumerate() {
if map.application_name == application_name {
idx_to_remove = Some(idx);
break;
}
}
if let Some(index) = idx_to_remove {
ctx.accounts.state_account.token_names_mapping.remove(index);
}
Ok(())
}
}
Paste your contract code at /programs/cross_chain_swap/src/swap_accounts.rs
// swap_acccount.rs
use anchor_lang::prelude::*;
#[account]
pub struct StateAccount {
pub native_asset_decimals: u32,
pub authorized_gateway: Pubkey,
pub admins: Vec<Pubkey>,
pub treasury_address: Pubkey,
pub treasury_share_percent: u32,
pub source_native_fee: u64,
pub token_names_mapping: Vec<TokenNamesMapping>
}
impl StateAccount {
pub const LEN: usize =
4 + // u32
32 + // Pubkey
4 + 32 * 5 + // Vec<Pubkey>
32 + // Pubkey
4 + // u32
8 + // u64
4 + 48 * 50;
} // total 244 bytes
#[account]
pub struct SwapRequest {
pub source_amount: u64, // 8
pub destination_amount: u64, // 8
pub sender_address: Pubkey, // 32
pub receiver_address: String, // 32
pub source_asset_address: Pubkey,
pub destination_asset_address: String,
pub destination_contract_address: String,
pub source_asset_symbol: String,
pub destination_asset_symbol: String,
pub source_chain: String,
pub source_contract_address: Pubkey,
pub destination_network: String,
pub conversion_rate_id: String,
pub internal_id: String,
}
#[account]
pub struct SwapInitiatedAccount {
pub source_amount: u64,
pub destination_amount: u64,
pub sender_address: Pubkey,
pub receiver_address: String,
pub source_asset_address: Pubkey,
pub destination_asset_address: String,
pub destination_contract_address: String,
pub source_asset_symbol: String,
pub destination_asset_symbol: String,
pub destination_network: String,
pub conversion_rate_id: String,
pub internal_id: String,
}
impl SwapInitiatedAccount {
pub const LEN: usize =
8 + // u64
8 + // u64
32 + // Pubkey
4 + 32 + // String
4 + 32 + // Pubkey
4 + 32 + // String
4 + 32 + // String
4 + 3 + // String
4 + 3 + // String
4 + 10 + // String
4 + 32 + // String
4 + 32; // String
} // total 292 bytes
#[derive(AnchorSerialize, AnchorDeserialize, Clone)]
pub struct TokenNamesMapping {
pub application_name: String,
pub xtalk_name: String,
}
Paste your contract code at /programs/cross_chain_swap/src/instructions.rs
//instructions.rs
use crate::swap_accounts::*;
use anchor_lang::prelude::*;
use anchor_spl::token::{Mint, Token, TokenAccount};
/// This instruction initiate the program account with
#[derive(Accounts)]
pub struct Initialize<'info> {
/// Payer account to pay for the account creation
#[account(mut, signer)]
pub payer: Signer<'info>,
/// Hold the state of our program
#[account(
init,
payer = payer,
space = StateAccount::LEN,
seeds = [b"state_account"],
bump,
)]
pub state_account: Box<Account<'info, StateAccount>>,
/// store in the state the l1x authority we will use to validate treasury ATA
pub token_program: Program<'info, Token>,
pub system_program: Program<'info, System>,
}
#[derive(Accounts)]
#[instruction(internal_id: String)]
pub struct InitiateSwapNative<'info> {
#[account(seeds = [b"state_account"], bump)]
pub state_account: Box<Account<'info, StateAccount>>,
#[account(mut)]
pub signer: Signer<'info>,
#[account(
mut,
constraint = treasury.key() == state_account.treasury_address,
)]
pub treasury: SystemAccount<'info>,
#[account(
mut,
seeds = [b"program_token_account"],
bump,
)]
pub program_token_account: SystemAccount<'info>, // don't I need to initiate this ??
#[account(
init,
payer = signer,
space = SwapInitiatedAccount::LEN,
seeds = [b"initiate-", internal_id.as_bytes()],
bump,
)]
pub swap_data: Box<Account<'info, SwapInitiatedAccount>>,
pub system_program: Program<'info, System>,
pub token_program: Program<'info, Token>,
}
#[derive(Accounts)]
#[instruction(internal_id: String)]
pub struct InitiateSwapErc20<'info> {
#[account(seeds = [b"state_account"], bump)]
pub state_account: Box<Account<'info, StateAccount>>,
#[account(mut)]
pub signer: Signer<'info>,
pub token_mint: Account<'info, Mint>,
#[account(
mut,
associated_token::mint = token_mint,
associated_token::authority = signer
)]
pub signer_token_account: Account<'info, TokenAccount>,
#[account(
mut,
associated_token::mint = token_mint,
associated_token::authority = state_account.treasury_address,
)]
pub treasury: Account<'info, TokenAccount>,
#[account(
mut,
associated_token::mint = token_mint,
associated_token::authority = state_account
)]
pub program_token_account: Box<Account<'info, TokenAccount>>,
#[account(
init,
payer = signer,
space = SwapInitiatedAccount::LEN,
seeds = [b"initiate-", internal_id.as_bytes()],
bump,
)]
pub swap_data: Box<Account<'info, SwapInitiatedAccount>>,
pub system_program: Program<'info, System>,
pub token_program: Program<'info, Token>,
}
#[derive(Accounts)]
#[instruction(global_tx_id: String)]
pub struct ExecuteSwapNative<'info> {
#[account(seeds = [b"state_account"], bump)]
pub state_account: Box<Account<'info, StateAccount>>,
#[account(mut,constraint = gateway.key() == state_account.authorized_gateway)]
pub gateway: Signer<'info>,
#[account(
mut,
seeds = [b"program_token_account"],
bump,
)]
pub program_token_account: SystemAccount<'info>,
#[account(mut)]
pub user_token_account: SystemAccount<'info>,
pub token_program: Program<'info, Token>,
pub system_program: Program<'info, System>,
}
#[derive(Accounts)]
#[instruction(global_tx_id: String)]
pub struct ExecuteSwapErc20<'info> {
#[account(seeds = [b"state_account"], bump)]
pub state_account: Box<Account<'info, StateAccount>>,
pub user: SystemAccount<'info>,
#[account(mut,constraint = gateway.key() == state_account.authorized_gateway)]
pub gateway: Signer<'info>,
pub token_mint: Account<'info, Mint>,
#[account(
mut,
associated_token::mint = token_mint,
associated_token::authority =
user
)]
pub user_token_account: Account<'info, TokenAccount>,
#[account(
mut,
associated_token::mint = token_mint,
associated_token::authority = state_account
)]
pub program_token_account: Box<Account<'info, TokenAccount>>,
pub token_program: Program<'info, Token>,
}
#[derive(Accounts)]
pub struct SetTreasuryAddress<'info> {
#[account(mut, seeds = [b"state_account"], bump)]
pub state_account: Box<Account<'info, StateAccount>>,
#[account(mut, signer)]
pub signer: Signer<'info>,
}
#[derive(Accounts)]
pub struct SetTreasurySharePercentage<'info> {
#[account(mut,seeds = [b"state_account"], bump)]
pub state_account: Box<Account<'info, StateAccount>>,
#[account(mut, signer)]
pub signer: Signer<'info>,
}
#[derive(Accounts)]
pub struct SetSourceNativeFee<'info> {
#[account(mut,seeds = [b"state_account"], bump)]
pub state_account: Box<Account<'info, StateAccount>>,
#[account(mut, signer)]
pub signer: Signer<'info>,
}
#[derive(Accounts)]
pub struct SetAuthorizedL1XGateway<'info> {
#[account(mut,seeds = [b"state_account"], bump)]
pub state_account: Box<Account<'info, StateAccount>>,
#[account(mut, signer)]
pub signer: Signer<'info>,
}
#[derive(Accounts)]
pub struct GetStateAccount<'info> {
#[account(seeds = [b"state_account"], bump)]
pub state_account: Account<'info, StateAccount>,
}
#[derive(Accounts)]
pub struct TransferToTreasuryNative<'info> {
#[account(mut, seeds = [b"state_account"], bump)]
pub state_account: Account<'info, StateAccount>,
#[account(
mut,
signer
)]
pub signer: Signer<'info>,
#[account(mut)]
pub treasury_account: SystemAccount<'info>,
pub system_program: Program<'info, System>,
#[account(
mut,
seeds = [b"program_token_account"],
bump,
)]
pub program_token_account: SystemAccount<'info>
}
#[derive(Accounts)]
pub struct TransferToTreasuryErc20<'info> {
#[account(mut)]
pub state_account: Account<'info, StateAccount>,
#[account(
mut,
signer
)]
pub signer: Signer<'info>,
#[account(
mut,
associated_token::mint = token_mint,
associated_token::authority = signer
)]
pub program_token_account: Account<'info, TokenAccount>,
#[account(mut)]
pub treasury_account: Account<'info, TokenAccount>,
pub token_program: Program<'info, Token>,
pub system_program: Program<'info, System>,
pub token_mint: Account<'info, Mint>,
}
#[derive(Accounts)]
pub struct UpdateNetwork<'info> {
#[account(mut)]
pub state_account: Account<'info, StateAccount>,
#[account(
mut,
signer,
constraint = state_account.admins.contains(&signer.key()),
)]
pub signer: Signer<'info>,
}
Paste your contract code at /programs/cross_chain_swap/src/events.rs
// events.rs
use anchor_lang::prelude::*;
use super::swap_accounts::SwapRequest;
#[event]
pub struct XTalkMessageBroadcasted {
pub message: Vec<u8>,
pub destination_network: String,
pub destination_contract_address: String,
}
#[derive(Clone)]
#[event]
pub struct SwapInitiated {
pub source_amount: u64,
pub destination_amount: u64,
pub sender_address: Pubkey,
pub receiver_address: String,
pub source_asset_address: Pubkey,
pub destination_asset_address: String,
pub destination_contract_address: String,
pub source_asset_symbol: String,
pub destination_asset_symbol: String,
pub destination_network: String,
pub conversion_rate_id: String,
pub internal_id: String,
}
impl SwapInitiated {
pub fn from_swap_request(swap_request: SwapRequest, sender_address: Pubkey) -> Self {
Self {
source_amount: swap_request.source_amount,
destination_amount: swap_request.destination_amount,
sender_address,
receiver_address: swap_request.receiver_address,
source_asset_address: swap_request.source_asset_address,
destination_asset_address: swap_request.destination_asset_address,
destination_contract_address: swap_request.destination_contract_address,
source_asset_symbol: swap_request.source_asset_symbol,
destination_asset_symbol: swap_request.destination_asset_symbol,
destination_network: swap_request.destination_network,
conversion_rate_id: swap_request.conversion_rate_id,
internal_id: swap_request.internal_id,
}
}
}
#[event]
pub struct SwapFullfilled {
pub global_tx_id: String,
pub internal_id: String,
pub amount: u64,
pub receiver_address: Pubkey,
pub asset_address: Pubkey,
pub status: bool,
pub status_message: String
}
Paste your contract code at /programs/cross_chain_swap/src/errors.rs
// errors.rs
use anchor_lang::prelude::*;
#[error_code]
pub enum ErrorCode {
#[msg("A swap request with this id already exists")]
SwapRequestAlreadyExists,
#[msg("Invalid amount for swap")]
InvalidAmount,
#[msg("Not enough funds")]
NotEnoughFunds,
#[msg("Swap request not found")]
SwapRequestNotFound,
#[msg("")]
Any,
}
Paste your contract code at /programs/cross_chain_swap/src/utils.rs
// utils.rs
use anchor_lang::prelude::*;
pub fn transfer_sol<'info>(
from: &AccountInfo<'info>,
to: &AccountInfo<'info>,
amount: u64,
system_program: &AccountInfo<'info>,
) -> Result<()> {
let ix =
anchor_lang::solana_program::system_instruction::transfer(&from.key(), &to.key(), amount);
anchor_lang::solana_program::program::invoke(
&ix,
&[from.clone(), to.clone(), system_program.clone()],
)?;
Ok(())
}
Step 3: Update Cargo.toml
Ensure that you update at programs/cross_chain_swap/Cargo.toml
// Cargo.toml
[package]
name = "cross_chain_swap"
version = "0.1.0"
description = "Created with Anchor"
edition = "2021"
[lib]
crate-type = ["cdylib", "lib"]
name = "cross_chain_swap"
[features]
default = []
cpi = ["no-entrypoint"]
no-entrypoint = []
no-idl = []
no-log-ix-name = []
idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"]
[dependencies]
anchor-lang = { version = "0.29.0", features = ["init-if-needed"]}
anchor-spl = "0.29.0"
solana-program = "1.18.17"
solana-zk-token-sdk = "1.18.7"
# borsh = "1.5.1"
bincode = "1.3.3"
serde = { version = "1.0.203", features = ["derive"] }
serde_json = "1.0.119"
Step 4: Install Dependencies
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.
swap: 8LHf4FmTPrXkPg9Jtgoexj64GTE8SbaSXNJuNeQBDSUG
Step 7: Declare PROGRAM_ID
Goto program/cross_chain_swap/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.
Script to Initiate Contract
// initialize.ts
import * as anchor from "@coral-xyz/anchor";
import { Program, AnchorProvider, Wallet, web3, Idl, BN } from "@coral-xyz/anchor";
import { Connection, PublicKey, Keypair } from "@solana/web3.js";
import { CrossChainSwap } from "../target/types/cross_chain_swap";
import fs from "fs";
import { createMint, getOrCreateAssociatedTokenAccount, mintTo } from "@solana/spl-token";
import env from "./env";
// if not using gatway, set to the public key of YOUR ACCOUNT (run `solana address` to get it)
if(!env.get("L1X_GATEWAY")) {
throw new Error("L1X_GATEWAY environment variable is required");
}
const L1X_GATEWAY = new PublicKey(env.get("L1X_GATEWAY") || null);
type TokenNamesMapping = {
applicationName: string;
xtalkName: string;
};
if(!env.get("SWAP_TREASURY")) {
throw new Error("SWAP_TREASURY environment variable is required");
}
const SWAP_TREASURY = new PublicKey(env.get("SWAP_TREASURY"));
const TREASURY_SHARE_PERCENT = parseInt(env.get("TREASURY_SHARE_PERCENT") || "10");
const SRC_NATIVE_FEE = new BN(env.get("SRC_NATIVE_FEE") || 0);
const ADMINS = []
const provider = anchor.AnchorProvider.env();
anchor.setProvider(provider);
const program = anchor.workspace.CrossChainSwap as Program<CrossChainSwap>;
const connection = (program.provider as anchor.AnchorProvider).connection;
async function createNewMint(provider: AnchorProvider, authority: Keypair): Promise<PublicKey> {
const mint = await createMint(
provider.connection,
authority,
authority.publicKey,
null,
9
);
return mint;
}
async function airdropSol(connection: Connection, publicKey: PublicKey) {
const airdropSignature = await connection.requestAirdrop(publicKey, web3.LAMPORTS_PER_SOL);
await connection.confirmTransaction(airdropSignature);
}
async function callInitialize() {
const [stateAccount, _stateBump] = PublicKey.findProgramAddressSync(
[Buffer.from("state_account")],
program.programId,
);
const [programTokenAccount, _] = PublicKey.findProgramAddressSync(
[Buffer.from("program_native_account")],
program.programId
);
console.log("State Account:", stateAccount.toBase58());
console.log("Fund Native and Sol20 to State Account:", stateAccount.toBase58());
console.log("Program Token Account:", programTokenAccount.toBase58());
const balance = await connection.getBalance(provider.wallet.publicKey);
if (balance < web3.LAMPORTS_PER_SOL) {
await airdropSol(connection, provider.wallet.publicKey);
}
env.set("SWAP_TREASURY", SWAP_TREASURY.toBase58());
const NETWORK_NAMES_MAPPING: TokenNamesMapping[] = [
{ applicationName: "SOL", xtalkName: "solana" },
{ applicationName: "ETH", xtalkName: "ethereum" },
{ applicationName: "MATIC", xtalkName: "polygon" },
{ applicationName: "BSC", xtalkName: "bsc" },
{ applicationName: "AVAX", xtalkName: "avalanche" },
{ applicationName: "ARBITRUM", xtalkName: "arbitrum" },
{ applicationName: "OPTIMISM", xtalkName: "optimism" }
];
try {
const txHash = await program.methods.initialize(
9, // Native asset decimals
L1X_GATEWAY, // Authorized L1 XGateway address (using wallet's public key for this example)
ADMINS, // Admin addresses (using wallet's public key for this example)
SWAP_TREASURY, // Treasury address
TREASURY_SHARE_PERCENT, // Treasury share percent
SRC_NATIVE_FEE, // Source native fee
NETWORK_NAMES_MAPPING
).accounts({
payer: provider.wallet.publicKey,
stateAccount: stateAccount,
}).rpc();
console.log("publick key:", provider.wallet.publicKey);
console.log("Transaction hash:", txHash);
// Optional: Wait for confirmation
await connection.confirmTransaction(txHash);
} catch (err) {
console.log("publick key:", provider.wallet.publicKey);
console.error("Error calling initialize function:", err);
if (err.logs) {
err.logs.forEach(log => console.error(log));
}
}
}
// Example usage:
callInitialize().catch(err => console.error("Error calling initialize function:", err));
Configure Anchor.toml
Update swap with YOUR_SOLANA_PROGRAM_ID
Update wallet with YOUR_KEYPAIR_PATH
Set path for initialize.ts script
// Anchor.toml
[toolchain]
[features]
seeds = false
skip-lint = false
[programs.devnet]
swap= "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"
//Set scripts path
initialize = "npx ts-node ./scripts/initialize.ts"
Step 9: Compile, Deploy and Run Solana Program
Compile the Solana Program
anchor build
Deploy the Solana Program
anchor deploy --provider.cluster devnet
Run the Solana Program
anchor run initialize --provider.cluster devnet
Output contains YOUR_SOLANA_STATE_ACCOUNT, YOUR_SOLANA_PROGRAM_TOKEN_ACCOUNT and YOUR_SOLANA_TRANSACTION_HASH
Save it as it is used later.
Step 3: Set up Avalanche Project
You can use Hardhat to compile, deploy and interact with Avalanche contracts. Note, this example is on Avalanche Mainnet.
Step 1: Initialize a New Project
Create a New Directory (if you're starting fresh):
mkdir avalanche-project-name cd avalanche-project-name
Initialize a new NPM project:
npm init -y
Step 2: Install Hardhat and Set Up the Project
Install Hardhat:
npm install --save-dev hardhat
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
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/SwapV2Contract.sol
//SwapV2Contract.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
interface ISwapGasPriceContract {
function calculateFeeWithSpecificFeedRound(
uint256 sourceAmount,
address sourceAssetAddress, // Will address(0) for Native
string memory destinationNetwork,
uint256 destSurchargeGasPrice,
uint80 sourceRoundId,
uint80 destRoundId
)
external
view
returns (
uint256 srcFeePercentage,
uint256 srcFeePercentageFactor,
uint256 percentFeeAmountInUSD,
uint256 srcConstantFeeInUSD,
uint256 destinationConstantFeeInUSD,
uint256 destinationSurchargeGasFeeInUSD,
uint256 totalFeeInUSD,
uint256 totalFeeInNative
);
}
interface IStakeContract {
function validateRequiredAmount(
string memory internalId
)
external
view
returns (
address _tokenAddress,
uint256 _amount,
uint256 amountAfterPenalty,
uint256 penaltyAmount,
uint256 depositFee,
uint256 amountToTransfer
);
}
contract SwapV2Contract is Ownable, ReentrancyGuard {
using SafeERC20 for IERC20;
// Constant for native asset address
address public constant NATIVE_ASSET_ADDRESS = address(0);
// Decimals for native assets
uint8 public constant NATIVE_ASSET_DECIMAL = 18;
// Swap Status
uint8 public constant SWAP_IN_PROGRESS = 0;
uint8 public constant SWAP_COMPLETED = 1;
uint8 public constant SWAP_FAILED = 2;
// Source Chain
string public sourceChain;
// Xtalk Network Name
mapping(string => string) public xTalkNetworkName;
// Authorized Signer
address public authorizedL1XGateway;
// Mapping to track authorized admins
mapping(address => bool) public admins;
// Address where fees are sent
address public treasuryAddress;
// Percentage of source amount sent to the treasury
uint256 public treasurySharePercent;
// Gas Price Contract
address public GAS_PRICE_CONTRACT_ADDRESS = address(0);
// Stake Contract
address public STAKE_CONTRACT_ADDRESS = address(0);
struct SwapRequest {
uint256 sourceAmount;
uint256 destinationAmount;
address senderAddress;
string receiverAddress;
address sourceAssetAddress; // Will address(0) for Native
string destinationAssetAddress; // Will address(0) for Native
string destinationContractAddress;
string sourceAssetSymbol;
string destinationAssetSymbol;
string destinationNetwork;
string conversionRateId;
string internalId;
uint256 sourceFeePercentage;
uint256 sourceFeePercentageFactor;
uint256 sourceNativeFee;
uint256 destSurchargeGasPrice;
uint80 sourceRoundId;
uint80 destRoundId;
}
// Stores details of each swap using a unique ID
mapping(string => SwapRequest) public swapsInitiated;
struct SwapExecutionData {
bytes32 globalTxId;
uint256 amount;
address receiverAddress;
address assetAddress;
string internalId;
bool status;
string statusMessage;
uint256 swapsCompleted;
}
// Internal Id to Bool
mapping(string => SwapExecutionData) public swapStatus;
// XTalk Events
event XTalkMessageBroadcasted(
bytes message,
string destinationNetwork,
string destinationSmartContractAddress
);
// Emitted when a new swap is initiated
event SwapInitiated(
uint256 sourceAmount,
uint256 destinationAmount,
address indexed senderAddress,
string receiverAddress,
address sourceAssetAddress,
string destinationAssetAddress,
string destinationContractAddress,
string sourceAssetSymbol,
string destinationAssetSymbol,
string destinationNetwork,
string conversionRateId,
string internalId,
uint256 sourceFeePercentage,
uint256 sourceFeePercentageFactor,
uint256 sourceNativeFee,
uint256 _destSurchargeGasPrice,
uint80 _sourceRoundId,
uint80 _destRoundId
);
// Emitted when a swap is successfully executed
event SwapFullfilled(
bytes32 indexed globalTxId,
string internalId,
uint256 amount,
address receiverAddress,
address assetAddress,
bool status,
string statusMessage
);
// Emitted when a new authorized signer is added
event StakeContractAddress(address indexed stakeContractAddress);
event TransferToStake(
address indexed stakeContractAddress,
string internalId,
uint256 amount,
address tokenAddress
);
event GasPriceContractUpdated(address indexed gasContractAddress);
event L1XGatewayUpdated(address indexed authorizedL1XGateway);
event AdminUpdated(address indexed signer, bool status);
constructor(
address _authorizedL1XGateway,
string memory _sourceChain,
address _treasuryAddress,
uint256 _treasurySharePercent,
address _gasPriceContractAddress,
address _stakeContractAddress
) Ownable(msg.sender) {
require(
_authorizedL1XGateway != address(0),
"Authorized Signer cannot be the zero address"
);
// Set the default signer as an authorized signer
authorizedL1XGateway = _authorizedL1XGateway;
// Set the contract owner as an admin
admins[msg.sender] = true;
// Set the source chain
sourceChain = _sourceChain;
// Treasury Contract
treasuryAddress = _treasuryAddress;
treasurySharePercent = _treasurySharePercent;
// Gas Price Contract
GAS_PRICE_CONTRACT_ADDRESS = _gasPriceContractAddress;
// Stake Contract
STAKE_CONTRACT_ADDRESS = _stakeContractAddress;
}
// Modifier to check if the caller is an authorized signer
modifier onlyL1XGateway() {
require(
authorizedL1XGateway == msg.sender,
"Not Authorized L1X Gateway"
);
_;
}
// Modifier to check if the caller is an admin
modifier onlyAdmin() {
require(admins[msg.sender] == true, "Not Authorized Admin");
_;
}
// Function to set the treasury address
function setTreasuryAddress(address _treasuryAddress) external onlyAdmin {
treasuryAddress = _treasuryAddress;
}
// Function to set the treasury share for source amount
function setTreasurySharePercent(
uint256 _treasurySharePercent
) external onlyAdmin {
treasurySharePercent = _treasurySharePercent;
}
// Update the Gas Price Contract address (only callable by the owner)
function updateStakeContractAddress(address newAddress) external onlyAdmin {
require(
newAddress != address(0),
"Address cannot be the zero address."
);
STAKE_CONTRACT_ADDRESS = newAddress;
emit StakeContractAddress(newAddress);
}
// Update the Gas Price Contract address (only callable by the owner)
function updateGasPriceContractAddress(
address newAddress
) external onlyAdmin {
require(
newAddress != address(0),
"Address cannot be the zero address."
);
GAS_PRICE_CONTRACT_ADDRESS = newAddress;
emit GasPriceContractUpdated(newAddress);
}
// Function to add a new authorized signer
function updateL1XGateway(
address _authorizedL1XGateway
) external onlyAdmin {
require(
_authorizedL1XGateway != address(0),
"L1X Gateway is the zero address"
);
authorizedL1XGateway = _authorizedL1XGateway;
emit L1XGatewayUpdated(_authorizedL1XGateway);
}
// Function to add a new admin
function updateAdmin(address _admin, bool _status) external onlyOwner {
require(_admin != address(0), "Admin is the zero address");
admins[_admin] = _status;
emit AdminUpdated(_admin, _status);
}
// Function to add a new admin
function updateXTalkNetworkName(
string memory _destinationNetwork,
string memory _xTalkNetworkname
) external onlyAdmin {
xTalkNetworkName[_destinationNetwork] = _xTalkNetworkname;
}
// Function to transfer asset to treasury
function transferToTreasury(
address _tokenAddress,
uint256 _amount
) external onlyOwner {
require(treasuryAddress != address(0), "Treasury address is not set");
if (_tokenAddress == NATIVE_ASSET_ADDRESS) {
// Send Native to Treasury with reentrancy Protection for Native
payable(treasuryAddress).transfer(_amount);
} else {
// Send Native to Treasury with reentrancy Protection for ERC20
IERC20(_tokenAddress).safeTransfer(treasuryAddress, _amount);
}
}
// Function to transfer asset to treasury
function transferToStake(string memory _internalId) external {
(
address _tokenAddress,
uint256 _amount,
uint256 amountAfterPenalty,
uint256 penaltyAmount,
uint256 depositFee,
uint256 amountToTransfer
) = IStakeContract(STAKE_CONTRACT_ADDRESS).validateRequiredAmount(
_internalId
);
require(
STAKE_CONTRACT_ADDRESS != address(0),
"Stake contract not configured"
);
require(STAKE_CONTRACT_ADDRESS == msg.sender, "Invalid caller");
require(_amount > 0, "Invalid Amount");
if (_tokenAddress == NATIVE_ASSET_ADDRESS) {
// Send Native to Treasury with reentrancy Protection for Native
payable(STAKE_CONTRACT_ADDRESS).transfer(_amount);
} else {
// Send Native to Treasury with reentrancy Protection for ERC20
IERC20(_tokenAddress).safeTransfer(
STAKE_CONTRACT_ADDRESS,
_amount
);
}
emit TransferToStake(
STAKE_CONTRACT_ADDRESS,
_internalId,
_amount,
_tokenAddress
);
}
receive() external payable {}
// Function to initiate a swap
function initiateSwap(
uint256 _sourceAmount,
uint256 _destinationAmount,
string memory _receiverAddress,
address _sourceAssetAddress, // Will address(0) for Native
string memory _destinationAssetAddress, // Will address(0) for Native
string memory _destinationContractAddress,
string memory _sourceAssetSymbol,
string memory _destinationAssetSymbol,
string memory _destinationNetwork,
string memory _conversionRateId,
string memory _internalId,
uint256 _destSurchargeGasPrice,
uint80 _sourceRoundId,
uint80 _destRoundId
) external payable nonReentrant {
require(
swapsInitiated[_internalId].sourceAmount == 0 &&
swapStatus[_internalId].swapsCompleted == 0,
"Swap with given internal ID already initiated"
);
require(
getLength(xTalkNetworkName[_destinationNetwork]) > 0,
"Invalid Destination Network"
);
uint256 _sourceFeePercentage = 0;
uint256 _sourceFeePercentageFactor = 0;
uint256 _sourceNativeFee = 0;
// Source Native Fee
(
_sourceFeePercentage,
_sourceFeePercentageFactor,
,
,
,
,
,
_sourceNativeFee
) = calculateSourceFee(
_sourceAmount,
_sourceAssetAddress, // Will address(0) for Native
_destinationNetwork,
_destSurchargeGasPrice,
_sourceRoundId,
_destRoundId
);
if (_sourceAssetAddress == NATIVE_ASSET_ADDRESS) {
require(_sourceAmount > 0, "Swap amount should be greater than 0");
require(
getLength(_receiverAddress) > 0,
"Receiving address is the zero address"
);
require(
msg.value >= _sourceAmount + _sourceNativeFee,
"Insufficient Fee Provided"
);
} else {
require(
_sourceAmount > 0,
"Swap source amount should be greater than 0"
);
require(
_sourceAssetAddress != address(0),
"Token address is the zero address"
);
require(
getLength(_receiverAddress) > 0,
"Receiving address is the zero address"
);
// Transfer ERC20 asset to this contract
IERC20(_sourceAssetAddress).safeTransferFrom(
msg.sender,
address(this),
_sourceAmount
);
require(msg.value >= _sourceNativeFee, "Insufficient Fee Provided");
}
if (_sourceNativeFee > 0) {
// Transfer Native asset to this treasury
payable(treasuryAddress).transfer(_sourceNativeFee);
}
// Calculate Treasury Share of Source Amount
uint256 treasuryShareForSourceAmount = calculatePercentAmount(
treasurySharePercent,
_sourceAmount
);
if (treasuryShareForSourceAmount > 0) {
// Transfer Treasury Share to Treasury
if (_sourceAssetAddress == NATIVE_ASSET_ADDRESS) {
payable(treasuryAddress).transfer(treasuryShareForSourceAmount);
} else {
IERC20(_sourceAssetAddress).safeTransfer(
treasuryAddress,
treasuryShareForSourceAmount
);
}
}
// Record the initiated swap
swapsInitiated[_internalId] = SwapRequest(
_sourceAmount,
_destinationAmount,
msg.sender,
_receiverAddress,
_sourceAssetAddress,
_destinationAssetAddress,
_destinationContractAddress,
_sourceAssetSymbol,
_destinationAssetSymbol,
_destinationNetwork,
_conversionRateId,
_internalId,
_sourceFeePercentage,
_sourceFeePercentageFactor,
_sourceNativeFee,
_destSurchargeGasPrice,
_sourceRoundId,
_destRoundId
);
// Emit SwapInitiated event
emit SwapInitiated(
_sourceAmount,
_destinationAmount,
msg.sender,
_receiverAddress,
_sourceAssetAddress,
_destinationAssetAddress,
_destinationContractAddress,
_sourceAssetSymbol,
_destinationAssetSymbol,
_destinationNetwork,
_conversionRateId,
_internalId,
_sourceFeePercentage,
_sourceFeePercentageFactor,
_sourceNativeFee,
_destSurchargeGasPrice,
_sourceRoundId,
_destRoundId
);
// XTalk Request
bytes memory messageBytes = abi.encode(
_sourceAmount,
_destinationAmount,
msg.sender,
_receiverAddress,
_sourceAssetAddress,
_destinationAssetAddress,
_sourceAssetSymbol,
_destinationAssetSymbol,
sourceChain,
address(this),
xTalkNetworkName[_destinationNetwork],
_conversionRateId,
_internalId
);
// Broadcast cross-network message
emit XTalkMessageBroadcasted(
messageBytes,
xTalkNetworkName[_destinationNetwork],
_destinationContractAddress
);
}
function _l1xReceive(
bytes32 globalTxId,
bytes memory message
) external nonReentrant onlyL1XGateway {
(
uint256 _amount,
address _receiverAddress,
address _assetAddress,
string memory _internalId,
bool _status,
string memory _statusMessage
) = abi.decode(
message,
(uint256, address, address, string, bool, string)
);
require(
getLength(swapStatus[_internalId].internalId) == 0,
"Message already acknowledged"
);
// Check if swap already completed
require(
_status == true ||
(_status == false &&
swapsInitiated[_internalId].sourceAmount != 0),
"Invalid X-Talk execution"
);
uint256 _swapsCompleted;
if (_status == true) {
_swapsCompleted = SWAP_COMPLETED;
} else {
_swapsCompleted = SWAP_FAILED;
}
swapStatus[_internalId] = SwapExecutionData(
globalTxId,
_amount,
_receiverAddress,
_assetAddress,
_internalId,
_status,
_statusMessage,
_swapsCompleted
);
// Execute Swap
executeSwap(_amount, _receiverAddress, _assetAddress);
emit SwapFullfilled(
globalTxId,
_internalId,
_amount,
_receiverAddress,
_assetAddress,
_status,
_statusMessage
);
emit XTalkMessageBroadcasted(message, "", "");
}
// Function to execute a swap after validation (Destination Chain)
function executeSwap(
uint256 amount,
address receiverAddress,
address assetAddress
) internal {
require(amount > 0, "Swap amount should be greater than 0");
require(
receiverAddress != address(0),
"Receiving address is the zero address"
);
if (assetAddress == NATIVE_ASSET_ADDRESS) {
// Send Native to Treasury with reentrancy Protection for Native
payable(receiverAddress).transfer(amount);
} else {
// Send Native to Treasury with reentrancy Protection for ERC20
IERC20(assetAddress).safeTransfer(receiverAddress, amount);
}
}
// Internal function to any percent amount
function calculatePercentAmount(
uint256 percent,
uint256 amount
) internal pure returns (uint256) {
if (percent > 0) {
return (amount * (percent)) / 100;
}
return 0;
}
// Internal function to deposit fee
function calculateSourceFee(
uint256 sourceAmount,
address sourceAssetAddress, // Will address(0) for Native
string memory destinationNetwork,
uint256 destSurchargeGasPrice,
uint80 sourceRoundId,
uint80 destRoundId
)
internal
view
returns (
uint256 srcFeePercentage,
uint256 srcFeePercentageFactor,
uint256 percentFeeAmountInUSD,
uint256 srcConstantFeeInUSD,
uint256 destinationConstantFeeInUSD,
uint256 destinationSurchargeGasFeeInUSD,
uint256 totalFeeInUSD,
uint256 totalFeeInNative
)
{
return
ISwapGasPriceContract(GAS_PRICE_CONTRACT_ADDRESS)
.calculateFeeWithSpecificFeedRound(
sourceAmount,
sourceAssetAddress, // Will address(0) for Native
destinationNetwork,
destSurchargeGasPrice,
sourceRoundId,
destRoundId
);
}
function getLength(string memory s) public pure returns (uint256) {
bytes memory b = bytes(s);
return b.length;
}
}
Step 5: Scripts and Configuration Settings
Write deployment scripts or tests as needed, using the setup you've created. Example Provided below.
// deploySwapContract.ts
import fs from "fs/promises";
import { NetworkWiseConfigPath } from "../constant";
import { NetworkType, GeneralConfigType } from "../types";
import { ethers, network } from "hardhat";
import RawNetworkConfig from "../networkWise.json";
import RawGeneralConfig from "../general.json";
const NetworkConfig: NetworkType = RawNetworkConfig as NetworkType;
const GeneralConfig: GeneralConfigType = RawGeneralConfig as GeneralConfigType;
async function main() {
const [owner] = await ethers.getSigners();
// Check Allowed Network
const currentNetwork = network.name.toUpperCase();
const treasuryContractAddress = NetworkConfig[currentNetwork].TREASURY_CONTRACT_ADDRESS;
const swapGasPriceContractAddress = NetworkConfig[currentNetwork].SWAP_GAS_PRICE_V2_CONTRACT || "";
const stakeContractAddress = NetworkConfig[currentNetwork].STAKE_V2_CONTRACT_ADDRESS || "";
const arrNetwork = Object.keys(NetworkConfig);
if(arrNetwork.includes(currentNetwork) == false){
console.log("Please use network out of : ",arrNetwork.join(", "));
return false;
}
// Check if Treasury is deployed for given network
if(ethers.isAddress(treasuryContractAddress) == false ){
console.log("Please deploy Treasury for ",currentNetwork);
return false;
}
if(ethers.isAddress(swapGasPriceContractAddress) == false ){
console.log("Please deploy SwapGasPriceV2Contract for ",currentNetwork);
return false;
}
console.log("Deployer Account: ",owner.address);
console.log("Using Parameters: ",
NetworkConfig[currentNetwork]['XTALK_GATEWAY_CONTRACT_ADDRESS'],
NetworkConfig[currentNetwork]['XTALK_NETWORK_NAME'],
treasuryContractAddress,
GeneralConfig.swap.treasuryShareForSourceAmountPercent,
swapGasPriceContractAddress,
stakeContractAddress
);
console.log("Deploying SwapV2Contract");
const SwapV2Contract = await ethers.deployContract("SwapV2Contract", [
NetworkConfig[currentNetwork]['XTALK_GATEWAY_CONTRACT_ADDRESS'],
NetworkConfig[currentNetwork]['XTALK_NETWORK_NAME'],
treasuryContractAddress,
GeneralConfig.swap.treasuryShareForSourceAmountPercent,
swapGasPriceContractAddress,
stakeContractAddress
]);
NetworkConfig[currentNetwork].SWAP_V2_CONTRACT_ADDRESS = await SwapV2Contract.getAddress();
console.log("Deployed SwapV2Contract: ",NetworkConfig[currentNetwork].SWAP_V2_CONTRACT_ADDRESS);
// Update if File
await fs.writeFile(NetworkWiseConfigPath, JSON.stringify(NetworkConfig,null,2));
}
// We recommend this pattern to be able to use async/await everywhere
// and properly handle errors.
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
Sample Hardhat Configuration file
// hardhat.config.ts
import { HardhatUserConfig } from "hardhat/config";
import "@nomicfoundation/hardhat-toolbox";
import "hardhat-gas-reporter";
const config: HardhatUserConfig = {
solidity: {
version: "0.8.24",
settings: {
optimizer: {
enabled: true,
runs: 5000,
},
viaIR: true,
},
},
defaultNetwork: "hardhat",
networks: {
hardhat: {
accounts: [
{
privateKey: process.env.DEPLOYER_PRIVATE_KEY || "",
balance: "65000000000",
},
],
},
avax: {
url: "https://api.avax.network/ext/bc/C/rpc",
accounts: [process.env.DEPLOYER_PRIVATE_KEY || ""]
},
eth: {
url: "https://eth.llamarpc.com",
accounts: [process.env.DEPLOYER_PRIVATE_KEY || ""]
},
bsc: {
url: "https://bsc-dataseed1.ninicoin.io",
accounts: [process.env.DEPLOYER_PRIVATE_KEY || ""]
},
optimism: {
url: "https://optimism.publicnode.com",
accounts: [process.env.DEPLOYER_PRIVATE_KEY || ""]
},
arbitrum: {
url: "https://winter-responsive-film.arbitrum-mainnet.quiknode.pro/9b0931ce258b9f1cdffa279f4a6e7336af7a3f9e",
accounts: [process.env.DEPLOYER_PRIVATE_KEY || ""]
},
matic: {
url: "https://polygon.meowrpc.com",
// url: "https://polygon-rpc.com",
accounts: [process.env.DEPLOYER_PRIVATE_KEY || ""]
}
}
};
export default config;
Step 6: Contract Deployment
npx hardhat run scripts/deploySwapContracts.ts --network avax
Output you get YOUR_AVAX_CONTRACT_ADDRESS. Save it for now.
Deployed SwapV2Contract: YOUR_AVAX_CONTRACT_ADDRESS
II. Solana to EVM Swap
This section is dedicated to swap a token from Solana to any EVM-compatible chain, Avalanche 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 sol-evm-swap
Use the provided code below to implement the cross-chain swap Solana to EVM logic.
Your L1X Solana-EVM Flow consist of below listed smart contracts that are to be created at /programs/swap_from_solana/src/
lib.rs
solana.rs
types.rs
Create src/lib.rs
//lib.rs
use borsh::{BorshDeserialize, BorshSerialize};
use l1x_sdk::contract_interaction::ContractCall;
use l1x_sdk::types::U256;
use l1x_sdk::{call_contract, caller_address};
use l1x_sdk::{contract, store::LookupMap};
use serde::Serialize;
mod types;
use types::*;
mod solana;
const STORAGE_CONTRACT_KEY: &[u8; 16] = b"swap-from-solana";
const STORAGE_EVENTS_KEY: &[u8; 6] = b"events";
const EVENT_STATUS: &[u8; 6] = b"status";
const TRANSACTION_HASH: &[u8; 16] = b"transaction-hash";
const L1X_GATEAWY: &str = "c4a615a2853b11ffe9a2e27ebb36a4f0d474dc1c";
const SUPPORTED_TOKENS: &[u8; 16] = b"supported-tokens";
const ETH_CONTRACT: &str = "54111ca05abfd11704079ec71dac17fd1fd48152";
const XTALK_SIGNER: &str = "BeeNfY43m8n6qEHqG1sCTif4rvTo3iRCVo3NTNH5A3mG";
#[derive(BorshSerialize, BorshDeserialize)]
pub struct CrossChainSwapFromSolana {
events: LookupMap<String, Event>,
supported_token: LookupMap<String, bool>,
conversion_rate_address: String,
event_status: LookupMap<String, String>,
transaction: LookupMap<String, String>,
deviation: U256,
total_events: u64,
}
impl Default for CrossChainSwapFromSolana {
fn default() -> Self {
Self {
events: LookupMap::new(STORAGE_EVENTS_KEY.to_vec()),
supported_token: LookupMap::new(SUPPORTED_TOKENS.to_vec()),
conversion_rate_address: "".to_string(),
event_status: LookupMap::new(EVENT_STATUS.to_vec()),
transaction: LookupMap::new(TRANSACTION_HASH.to_vec()),
deviation: U256::from(0),
total_events: u64::default(),
}
}
}
#[contract]
impl CrossChainSwapFromSolana {
/// Generate contract based on bytes in 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")
}
}
}
/// 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"),
};
}
/// Instantiate and save contract to storage with default values
pub fn new() {
let mut contract = Self::default();
// Self::set_all_receivers();
contract.supported_token.insert("USDT".to_string(), true);
contract.supported_token.insert("USDC".to_string(), true);
contract.supported_token.insert("BNB".to_string(), true);
contract.supported_token.insert("AVAX".to_string(), true);
contract.supported_token.insert("ETH".to_string(), true);
contract.supported_token.insert("MATIC".to_string(), true);
contract.supported_token.insert("L1X".to_string(), true);
contract.supported_token.insert("SOL".to_string(), true);
contract.save();
}
/// Save event to contract storage
///
/// - `global_tx_id`: Global transaction identifier
/// - `source_id`: Source Identifier
/// - `event_data`: Date to store in contract's storage
pub fn save_event_data(event_data: Vec<u8>, global_tx_id: String) {
l1x_sdk::msg(&format!(
"********************global tx id {} **************",
global_tx_id
));
assert_eq!(
caller_address(),
l1x_sdk::types::Address::try_from(L1X_GATEAWY).unwrap(),
"Only the gateway can call this function"
);
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");
// let event_data = match base64::decode(event_data) {
// Ok(data) => data,
// Err(_) => panic!("Can't decode base64 event_data"),
// };
let event_data_str = String::from_utf8(event_data.clone()).unwrap();
let event = XTalkMessageBroadcasted::decode_event_data(&event_data_str).unwrap();
// let event = serde_json::from_slice::<XTalkMessageBroadcasted>(&event_data).unwrap();
if let Ok(swap_request) = SwapRequest::try_from_slice(&event.data) {
let event_id = swap_request.internal_id.clone();
let key = Self::to_key(global_tx_id.clone(), event_id);
assert!(!contract.events.contains_key(&key), "already executed");
if Self::validate_destination_amount(
swap_request.clone(),
event.destination_network,
global_tx_id.clone(),
event.destination_smart_contract_address.clone(),
) {
contract.save_swap_request_event(
global_tx_id,
swap_request.internal_id.clone(),
swap_request.clone(),
swap_request.destination_contract_address.clone(),
);
}
// make cross contract call if destination chain is l1x
} else if let Ok(event) = SwapExecuted::try_from_slice(&event.data) {
contract.save_swap_executed_event(global_tx_id, event.internal_id.clone(), event)
} else {
panic!("invalid event");
}
contract.save();
}
fn get_execute_swap_payload(
global_tx_id: String,
mut request: SwapRequest,
revert: bool,
mut destination_contract_address: String,
) -> Payload {
if revert {
request.destination_amount = request.source_amount.clone();
request.destination_asset_symbol = request.source_asset_symbol.clone();
request.receiver_address = String::from_utf8(request.sender_address.to_ascii_uppercase()).unwrap();
request.destination_network = request.source_chain.clone();
destination_contract_address = bs58::encode(request.source_contract_address).into_string();
}
if request.destination_network == "solana" {
if request.destination_asset_symbol == "SOL" {
solana::get_bytecode_native(request, destination_contract_address, global_tx_id)
} else {
solana::get_bytecode_erc20(request, destination_contract_address, global_tx_id)
}
} else {
let args = {
#[derive(Serialize)]
struct Args {
params: Vec<Vec<u8>>,
param_types: Vec<String>,
global_tx_id: String,
}
l1x_sdk::msg(&format!("{:#?}", U256::from(request.destination_amount)));
l1x_sdk::msg(&format!("{}", request.receiver_address));
l1x_sdk::msg(&format!("{}", request.destination_asset_address));
l1x_sdk::msg(&format!("{}", request.destination_asset_symbol));
l1x_sdk::msg(&format!("{}", request.destination_network));
l1x_sdk::msg(&format!("{}", global_tx_id));
Args {
params: vec![
serde_json::to_vec(&U256::from(request.destination_amount)).unwrap(),
serde_json::to_vec(&request.receiver_address).unwrap(),
serde_json::to_vec(&request.destination_asset_address).unwrap(),
serde_json::to_vec(&request.destination_asset_symbol).unwrap(),
serde_json::to_vec(&request.destination_network).unwrap(),
serde_json::to_vec(&global_tx_id).unwrap(),
],
param_types: vec![
"uint".to_string(),
"address".to_string(),
"address".to_string(),
"string".to_string(),
"string".to_string(),
"string".to_string(),
],
global_tx_id,
}
};
let call = ContractCall {
contract_address: l1x_sdk::types::Address::try_from(ETH_CONTRACT).unwrap(),
method_name: "get_byte_code".to_string(),
args: serde_json::to_vec(&args).unwrap(),
gas_limit: l1x_sdk::gas_left().checked_sub(30000).expect("Out of gas"),
read_only: false,
};
let response = call_contract(&call).expect("Function returned nothing");
let data = serde_json::from_slice::<Vec<u8>>(&response).unwrap();
Payload {
data,
destination_contract_address: request.destination_contract_address,
destination_network: request.destination_network,
}
}
}
fn save_swap_request_event(
&mut self,
global_tx_id: String,
event_id: String,
event: SwapRequest,
destination_contract_address: String,
) {
let event_data: SwapRequest = event.clone().into();
l1x_sdk::msg(&format!("{:#?}", event_data));
let key = CrossChainSwapFromSolana::to_key(global_tx_id.clone(), event_id);
self.events.insert(key, Event::Request(event_data.clone()));
self.event_status
.insert(event_data.internal_id, "Pending".to_string());
l1x_sdk::msg(&format!("event saved!"));
let payload = CrossChainSwapFromSolana::get_execute_swap_payload(
global_tx_id,
event,
false,
destination_contract_address,
);
l1x_sdk::msg(&format!("emitted event: {:?}", payload));
l1x_sdk::emit_event_experimental(payload);
}
fn save_swap_executed_event(
&mut self,
global_tx_id: String,
event_id: String,
event: SwapExecuted,
) {
let event_data: SwapExecuted = event.clone().into();
l1x_sdk::msg(&format!("{:#?}", event_data));
let key = CrossChainSwapFromSolana::to_key(global_tx_id.clone(), event_id);
self.events.insert(key, Event::Executed(event_data.clone()));
self.event_status
.set(event_data.internal_id, Some("Success".to_string()));
}
pub fn get_transaction_status(internal_id: String) -> String {
let contract = Self::load();
match contract.event_status.get(&internal_id) {
Some(status) => status.clone(),
None => "Invalid internal id".to_string(),
}
}
pub fn to_key(global_tx_id: String, event_type: String) -> String {
global_tx_id.to_owned() + "-" + &event_type
}
pub fn validate_destination_amount(
event: SwapRequest,
destination_network: String,
global_tx_id: String,
destination_contract_address: String
) -> bool {
let mut _destination_asset_symbol = event.destination_asset_symbol.clone();
let mut _source_asset_symbol = event.source_asset_symbol.clone();
if event.destination_asset_symbol == "ARBITRUM"
|| event.destination_asset_symbol == "OPTIMISM"
{
_destination_asset_symbol = "ETH".to_string();
} else if event.source_asset_symbol == "ARBITRUM" || event.source_asset_symbol == "OPTIMISM"
{
_source_asset_symbol = "ETH".to_string();
}
if Self::load()
.supported_token
.get(&_destination_asset_symbol.to_uppercase())
.is_none()
{
panic!("invalid destination asset symbol");
}
let args = {
#[derive(Serialize)]
struct Args {
conversion_rate_id: String,
source_token: String,
destination_token: String,
}
Args {
conversion_rate_id: event.conversion_rate_id.clone(),
source_token: _source_asset_symbol,
destination_token: {
if _destination_asset_symbol == "L1X" {
"USDT".to_string()
} else {
_destination_asset_symbol
}
},
}
};
let call = ContractCall {
contract_address: l1x_sdk::types::Address::try_from(
Self::get_conversion_rate_contract(),
)
.unwrap(),
method_name: "get_conversion_rate_by_id".to_string(),
args: serde_json::to_vec(&args).unwrap(),
gas_limit: l1x_sdk::gas_left().checked_sub(30000).expect("Out of gas"),
read_only: false,
};
let response = call_contract(&call).expect("Function returned nothing");
let rate = serde_json::from_slice::<l1x_sdk::types::U256>(&response).unwrap();
let source_decimals = Self::get_decimals(&event.source_chain, &event.source_asset_symbol);
let destination_decimals =
Self::get_decimals(&destination_network, &event.destination_asset_symbol);
if source_decimals >= destination_decimals {
let calculated_destination_amount = U256::from(event.source_amount) * rate
/ 10u64.pow((source_decimals - destination_decimals).into())
/ 100000000;
l1x_sdk::msg(&format!(
"calcualted destination amount --> {}",
calculated_destination_amount
));
let maximum_allowed_amount = calculated_destination_amount
+ calculated_destination_amount * Self::get_allowed_deviation() / U256::from(100);
let minimum_allowed_amount = calculated_destination_amount
- calculated_destination_amount * Self::get_allowed_deviation() / U256::from(100);
if (U256::from(event.destination_amount) > maximum_allowed_amount)
|| U256::from(event.destination_amount) < minimum_allowed_amount
{
let payload = Self::get_execute_swap_payload(global_tx_id, event, true, destination_contract_address);
l1x_sdk::msg(&format!("emitted event: {:?}", payload));
l1x_sdk::emit_event_experimental(payload);
return false;
}
} else {
let calculated_destination_amount = U256::from(event.source_amount)
* rate
* 10u64.pow((destination_decimals - source_decimals).into())
/ 100000000;
l1x_sdk::msg(&format!(
"calcualted destination amount --> {}",
calculated_destination_amount
));
let maximum_allowed_amount = calculated_destination_amount
+ (calculated_destination_amount * Self::get_allowed_deviation() / U256::from(100));
let minimum_allowed_amount = calculated_destination_amount
- (calculated_destination_amount * Self::get_allowed_deviation() / U256::from(100));
if (U256::from(event.destination_amount) > maximum_allowed_amount)
|| (U256::from(event.destination_amount) < minimum_allowed_amount)
{
let payload = Self::get_execute_swap_payload(global_tx_id, event, true, destination_contract_address);
l1x_sdk::msg(&format!("emitted event: {:?}", payload));
l1x_sdk::emit_event_experimental(payload);
return false;
}
}
return true;
}
pub fn set_conversion_rate_contract(address: String) {
let mut contract = Self::load();
contract.conversion_rate_address = address;
contract.save();
}
pub fn get_conversion_rate_contract() -> String {
let contract = Self::load();
contract.conversion_rate_address
}
pub fn set_transaction_hash(internal_id: String, transaction_hash: String) {
let mut contract = Self::load();
contract.transaction.insert(internal_id, transaction_hash);
contract.save();
}
pub fn get_transaction_hash(internal_id: String) -> String {
let contract = Self::load();
contract.transaction.get(&internal_id).unwrap().clone()
}
pub fn set_allowed_deviation(deviation: U256) {
let mut contract = Self::load();
contract.deviation = deviation;
contract.save()
}
fn get_allowed_deviation() -> U256 {
Self::load().deviation
}
fn get_decimals(network_name: &str, token_symbol: &str) -> u8 {
if network_name == "bsc" {
return 18;
} else if network_name == "solana" {
match token_symbol {
"USDT" => 6,
"USDC" => 6,
"BNB" => 8,
"AVAX" => 8,
"ETH" => 8,
"MATIC" => 8,
"L1X" => panic!("Unsupportd token: l1x on solana"),
"SOL" => 9,
_ => panic!("invalid token symbol"),
}
} else if network_name == "eth" {
if token_symbol == "ETH" {
18
} else {
6
}
} else {
panic!("unsupported network");
}
}
}
Create src/solana.rs
// solana.rs
use super::types::SwapRequest;
use crate::Payload;
use borsh::{BorshDeserialize, BorshSerialize};
use serde::{Deserialize, Serialize};
use solana_sdk::{instruction::AccountMeta, pubkey::Pubkey};
use spl_associated_token_account::*;
use std::str::FromStr;
use crate::XTALK_SIGNER;
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
pub struct Instruction {
pub program_id: Pubkey,
pub accounts: Vec<AccountMeta>,
pub data: Vec<u8>,
}
#[derive(BorshSerialize, BorshDeserialize)]
struct ExecuteSwap {
internal_id: String,
global_tx_id: String,
/// Data struct, bincode encoded
message: Vec<u8>,
}
// amount, _receiver_address, _asset_address, internal_id, status, status_message
#[derive(Serialize, Deserialize, PartialEq, Debug)]
struct Data(u64, bool, String);
pub fn get_bytecode_native(event: SwapRequest, destination_contract_address: String, global_tx_id: String) -> Payload {
let mut bytecode = vec![209, 12, 21, 169, 184, 62, 52, 75];
let signer = Pubkey::from_str(XTALK_SIGNER).unwrap();
let data = Data(
event.destination_amount,
true,
"executed".to_string(),
);
let message = bincode::serialize(&data).unwrap();
let execute_swap_native = ExecuteSwap {
internal_id: event.internal_id.clone(),
global_tx_id,
message,
};
let encoded_data = borsh::to_vec(&execute_swap_native).unwrap();
bytecode.extend(encoded_data);
l1x_sdk::msg(&format!(
"______________ destination_contract_address{}",
destination_contract_address
));
let program_id = Pubkey::from_str(destination_contract_address.as_str()).unwrap();
let state_account = Pubkey::find_program_address(&["state_account".as_bytes()], &program_id).0;
let program_token_acc =
Pubkey::find_program_address(&["program_token_account".as_bytes()], &program_id).0;
let user_token_acc = Pubkey::from_str(event.receiver_address.as_str()).unwrap();
let accounts = vec![
AccountMeta::new(state_account, false),
AccountMeta::new(signer, true),
AccountMeta::new(program_token_acc, false),
AccountMeta::new(user_token_acc, false),
AccountMeta::new(spl_token::id(), false),
AccountMeta::new_readonly(solana_sdk::system_program::id(), false),
];
let instruction = Instruction {
program_id,
accounts: accounts.clone(),
data: bytecode,
};
let encoded_instruction = bincode::serialize(&instruction).unwrap();
let encoded_accounts = bincode::serialize(&accounts).unwrap();
let data = serde_json::to_vec(&(encoded_instruction, encoded_accounts)).unwrap();
Payload {
data,
destination_contract_address,
destination_network: event.destination_network,
}
}
pub fn get_bytecode_erc20(event: SwapRequest, destination_contract_address: String, global_tx_id: String) -> Payload {
let mut bytecode = vec![25, 146, 197, 68, 231, 230, 81, 60];
let signer = Pubkey::from_str(XTALK_SIGNER).unwrap();
let data = Data(
event.destination_amount,
true,
"executed".to_string(),
);
let message = bincode::serialize(&data).unwrap();
let execute_swap_native = ExecuteSwap {
internal_id: event.internal_id.clone(),
global_tx_id,
message,
};
let encoded_data = borsh::to_vec(&execute_swap_native).unwrap();
bytecode.extend(encoded_data);
l1x_sdk::msg(&format!(
"______________ destination_contract_address{}",
destination_contract_address
));
let program_id = Pubkey::from_str(destination_contract_address.as_str()).unwrap();
let state_account =
Pubkey::find_program_address(&["state_account".as_bytes()], &program_id).0;
let user = Pubkey::from_str(event.receiver_address.as_str()).unwrap();
let token_mint = Pubkey::from_str(event.destination_asset_address.as_str()).unwrap();
let user_token_account = get_associated_token_address(&user, &token_mint);
let flow_authority =
Pubkey::find_program_address(&["state_account".as_bytes()], &program_id).0;
let program_token_account = get_associated_token_address(&flow_authority, &token_mint);
let accounts = vec![
AccountMeta::new(state_account, false),
AccountMeta::new(user, false),
AccountMeta::new(signer, true),
AccountMeta::new(token_mint, false),
AccountMeta::new(user_token_account, false),
AccountMeta::new(program_token_account, false),
AccountMeta::new(spl_token::id(), false),
];
let instruction = Instruction {
program_id,
accounts: accounts.clone(),
data: bytecode,
};
let encoded_instruction = bincode::serialize(&instruction).unwrap();
let encoded_accounts = bincode::serialize(&accounts).unwrap();
let data = serde_json::to_vec(&(encoded_instruction, encoded_accounts)).unwrap();
Payload {
data,
destination_contract_address,
destination_network: event.destination_network,
}
}
Create src/types.rs
// types.rs
use borsh::{BorshDeserialize, BorshSerialize};
use serde::{Deserialize, Serialize};
/// Enumerates two types of events, SwapRequest and SwapExecuted.
#[derive(BorshSerialize, BorshDeserialize, Serialize, Deserialize)]
pub enum Event {
/// Emitted when swap is initiated.
Request(SwapRequest),
/// Emitted when swap is executed.
Executed(SwapExecuted),
}
#[derive(Clone, Debug, Serialize, BorshSerialize, BorshDeserialize)]
pub struct XTalkMessageBroadcasted {
pub data: Vec<u8>,
pub destination_network: String,
pub destination_smart_contract_address: String,
}
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)
}
}
#[derive(Clone, Debug, BorshSerialize, BorshDeserialize, Serialize, Deserialize)]
pub struct SwapRequest {
pub source_amount: u64,
pub destination_amount: u64,
pub sender_address: [u8; 32],
pub receiver_address: String,
pub source_asset_address: [u8; 32],
pub destination_asset_address: String,
pub destination_contract_address: String,
pub source_asset_symbol: String,
pub destination_asset_symbol: String,
pub source_chain: String,
pub source_contract_address: [u8; 32],
pub destination_network: String,
pub conversion_rate_id: String,
pub internal_id: String,
}
#[derive(Clone, Debug, BorshSerialize, BorshDeserialize, Serialize, Deserialize)]
pub struct SwapExecuted {
pub global_tx_id: String,
pub internal_id: String,
pub amount: u64,
pub receiver_address: [u8; 32],
pub asset_address: [u8; 32],
pub status: bool,
pub status_message: String
}
#[derive(Clone, Debug, BorshSerialize, BorshDeserialize, Serialize, Deserialize)]
pub struct ExecuteSwapPayload {
pub destination_amount: l1x_sdk::types::U256,
pub receiver_address: l1x_sdk::types::Address,
pub destination_asset_address: l1x_sdk::types::Address,
pub destination_asset_symbol: String,
pub destination_network: String,
pub l1x_destination_contract_address: l1x_sdk::types::Address,
pub global_tx_id: [u8; 32],
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct GetPayloadResponse {
pub destination_network: String,
pub payload: Vec<u8>,
}
#[derive(Clone, Debug, BorshSerialize, BorshDeserialize, Serialize, Deserialize)]
pub struct Payload {
pub data: Vec<u8>,
pub destination_network: String,
pub destination_contract_address: String,
}
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 = "swap_from_solana"
version = "0.1.0"
edition = "2021"
authors = ["The L1X Project Developers"]
license = "Apache-2.0"
description = """
L1X contract example
"""
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[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 = "*"
getrandom = { version = "0.2.10", features = ["js"] }
hex = "0.4"
log = "0.4.20"
solana-sdk = { version = "2.0.1", features = ["borsh"] }
solana-program = {version = "2.0.1", features = ["borsh"]}
spl-associated-token-account = "3.0.2"
bs58 = "0.5.1"
bincode = "1.3.3"
spl-token = "4.0.0"
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 78c4fa1139a9f86693f943e2184256b868c3c716 register_new_source --args "{\\"destination_contract_address\\": \\"8756b6d6cf68934a4261eafecc364e7fb3759010\\", \\
\\"source_contract_address\\": \\"54326C013CFce492eF57F4948D42Bf1b31D0113b\\", \\
\\"source_chain\\": \\"Solana\\", \\
\\"event_filters\\": []}" --
Step 3: Native Token Swap Solana - EVM
Step A: Implementing Script to Initiate Native Token Swap from Solana to Avalanche
Step 1: Add Script to Initiate Swap
In your solana project, inside scripts folder add init_swap.ts script.
Update walletKeypair with YOUR_KEYPAIR_PATH
Update other relevant network details in the script.
// init_swap.ts
import * as anchor from "@coral-xyz/anchor";
import { Program, AnchorProvider, Wallet, web3, Idl, BN } from "@coral-xyz/anchor";
import { Connection, PublicKey, Keypair } from "@solana/web3.js";
import { CrossChainSwap } from "../target/types/cross_chain_swap";
import { createMint, getOrCreateAssociatedTokenAccount, mintTo } from "@solana/spl-token";
import env from "./env";
const INTERNAL_ID = "3243243545432";
const CONVERSION_RATE_ID = "0x";
const DESTINATION_NETWORK = "Solana";
const SOURCE_AMOUNT = new BN(0.1 * (10 ** 9));
const DESTINATION_AMOUNT = new BN(0.1 * (10 ** 9));
const RECEIVER_ADDRESS = "0x";
const DESTINATION_CONTRACT = "0x";
const SOURCE_SYMBOL = "sol";
const DESTINATION_SYMBOL = "sol";
const DESTINATION_ASSET_ADDRESS = "0x";
// Set the connection to the Solana network
const provider = anchor.AnchorProvider.env();
anchor.setProvider(provider);
const program = anchor.workspace.CrossChainSwap as Program<CrossChainSwap>;
const connection = (program.provider as anchor.AnchorProvider).connection;
const [stateAccount, _stateBump] = PublicKey.findProgramAddressSync(
[Buffer.from("state_account")],
program.programId
);
const [swapDataAccount, _dataBump] = PublicKey.findProgramAddressSync(
[Buffer.from("initiate-"), Buffer.from(INTERNAL_ID)],
program.programId
);
const [programTokenAccount, _] = PublicKey.findProgramAddressSync(
[Buffer.from("program_token_account")],
program.programId
);
async function airdropSol(connection: Connection, publicKey: PublicKey) {
const airdropSignature = await connection.requestAirdrop(publicKey, web3.LAMPORTS_PER_SOL);
await connection.confirmTransaction(airdropSignature);
}
const treasury_pubkey = new PublicKey(env.get("SWAP_TREASURY"));
console.log("Treasury pubkey: ", treasury_pubkey.toString());
async function callInitSwap() {
const balance = await connection.getBalance(provider.wallet.publicKey);
if (balance < web3.LAMPORTS_PER_SOL) {
await airdropSol(connection, provider.wallet.publicKey);
}
let our_balance = await connection.getBalance(provider.wallet.publicKey);
console.log("Our balance: ", our_balance);
let treasury_balance = await connection.getBalance(treasury_pubkey);
console.log("Treasury balance: ", treasury_balance);
let program_balance = await connection.getBalance(programTokenAccount);
console.log("Program balance: ", program_balance);
try {
const txHash = await program.methods.initiateSwapNative(
INTERNAL_ID, // internal_id
SOURCE_AMOUNT, // Source native amount
DESTINATION_AMOUNT, // Target native amount
RECEIVER_ADDRESS,// receiver address
PublicKey.default, // source asset address
DESTINATION_ASSET_ADDRESS, // destination asset address
DESTINATION_CONTRACT, // destination contract
SOURCE_SYMBOL, // src symbol
DESTINATION_SYMBOL, // dest symbol
DESTINATION_NETWORK, // destination network
CONVERSION_RATE_ID, // conversion rate id
).accounts({
stateAccount: stateAccount,
signer: provider.wallet.publicKey,
systemProgram: web3.SystemProgram.programId,
programTokenAccount: programTokenAccount,
swapData: swapDataAccount,
treasury: treasury_pubkey,
}).rpc();
console.log("Transaction hash:", txHash);
// Optional: Wait for confirmation
await connection.confirmTransaction(txHash);
} catch (err) {
console.error("Error calling initialize function:", err);
if (err.logs) {
err.logs.forEach(log => console.error(log));
}
}
let new_our_balance = await connection.getBalance(provider.wallet.publicKey);
console.log("Our balance: ", new_our_balance);
let new_treasury_balance = await connection.getBalance(treasury_pubkey);
console.log("Treasury balance: ", new_treasury_balance);
let new_program_balance = await connection.getBalance(programTokenAccount);
console.log("Program balance: ", new_program_balance);
}
// Example usage:
callInitSwap().catch(err => console.error("Error calling initialize function:", err));
Step 2: Update script path in Anchor.toml
Add the path for init_swap.ts script in Anchor.toml
// Anchor.toml
[toolchain]
[features]
seeds = false
skip-lint = false
[programs.devnet]
cross_chain_swap = "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"
initialize = "npx ts-node ./scripts/initialize.ts"
init_swap = "npx ts-node ./scripts/init_swap.ts"
Step B: Initiate Swap from Solana to Avalanche
anchor run init_swap --provider.cluster devnet
To verify that sawp is triggered at YOUR_AVAX_CONTRACT_ADDRESS, goto Avalanche explorer, check for the transaction with X-Talk Gateway for Avalanche mentioned in the table.
Step 4: ERC20 Token Swap Solana - EVM
Step A: Implementing Script to Initiate ERC20 Token Swap from Solana to Avalanche
Step 1: Add Script to Initiate ERC20 Swap
In your solana project, inside scripts folder add init_swap_erc20.ts script.
Update walletKeypair with YOUR_KEYPAIR_PATH
Update other relevant network details in the script.
// init_swap_erc20.ts
import * as anchor from "@coral-xyz/anchor";
import { Program, AnchorProvider, Wallet, web3, Idl, BN } from "@coral-xyz/anchor";
import { Connection, PublicKey, Keypair, Commitment } from "@solana/web3.js";
import { CrossChainSwap } from "../target/types/cross_chain_swap";
import { createMint, getOrCreateAssociatedTokenAccount, mintTo, createAccount, getAssociatedTokenAddressSync } from "@solana/spl-token";
import env from "./env";
import fs from 'fs';
const PATH_TO_KEYPAIR = "./id.json";
const INTERNAL_ID = "T2mP9kN5xL3yR1aQ7vH4";
const CONVERSION_RATE_ID = "0x";
const DESTINATION_NETWORK = "Solana";
const SOURCE_AMOUNT = new BN(1_000_000);
const DESTINATION_AMOUNT = new BN(1_000_000);
const RECEIVER_ADDRESS = "0x";
const DESTINATION_CONTRACT = "0x";
const SOURCE_SYMBOL = "sol";
const DESTINATION_SYMBOL = "sol";
const DESTINATION_ASSET_ADDRESS = "0x";
const provider = anchor.AnchorProvider.env();
anchor.setProvider(provider);
const program = anchor.workspace.CrossChainSwap as Program<CrossChainSwap>;
const connection = (program.provider as anchor.AnchorProvider).connection;
const [stateAccount, _stateBump] = PublicKey.findProgramAddressSync(
[Buffer.from("state_account")],
program.programId
);
const [programTokenAccount, _] = PublicKey.findProgramAddressSync(
[Buffer.from("program_token_account")],
program.programId
);
const [swapDataAccount, _dataBump] = PublicKey.findProgramAddressSync(
[Buffer.from("initiate-"), Buffer.from(INTERNAL_ID)],
program.programId
);
async function airdropSol(connection: Connection, publicKey: PublicKey) {
const airdropSignature = await connection.requestAirdrop(publicKey, web3.LAMPORTS_PER_SOL);
await connection.confirmTransaction(airdropSignature);
}
const treasury_pubkey = new PublicKey(env.get("SWAP_TREASURY"));
console.log("Treasury pubkey: ", treasury_pubkey.toString());
async function callInitSwap() {
const payerKeypair = YOUR_KEYPAIR_PATH;
console.log("payerKeypair: ", payerKeypair.publicKey.toString());
console.log("wallet: ", provider.wallet.publicKey.toString());
let myMint;
try {
let env_mint_address = new PublicKey(env.get("TOKEN_MINT"));
const mintAccountInfo = await connection.getAccountInfo(env_mint_address);
if (mintAccountInfo === null) {
console.log("Mint account not found");
throw new Error("Mint account not found");
}
myMint = env_mint_address;
console.log("Mint account found: ", myMint.toString());
} catch (_) {
const mint_address = await createMint(connection, payerKeypair, payerKeypair.publicKey, null, 9);
// this is to wait to be sure that the mint is created
myMint = mint_address;
env.set("TOKEN_MINT", myMint.toString());
console.log("Mint account created: ", myMint.toString());
}
const userTokenAccount = getAssociatedTokenAddressSync(
myMint,
provider.wallet.publicKey
);
if (connection.getAccountInfo(userTokenAccount) === null) {
console.log("Creating user token account");
await createAccount(connection, payerKeypair, myMint, userTokenAccount);
}
console.log("User token account: ", userTokenAccount.toString());
const programAssociatedAccount = getAssociatedTokenAddressSync(
myMint,
stateAccount,
true
);
if (connection.getAccountInfo(programAssociatedAccount) === null) {
console.log("Creating program associated account");
await createAccount(connection, payerKeypair, myMint, programAssociatedAccount)
};
console.log("Program associated account: ", programAssociatedAccount.toString());
const treasuryTokenAccount = getAssociatedTokenAddressSync(
myMint,
treasury_pubkey
);
if (connection.getAccountInfo(treasuryTokenAccount) === null) {
console.log("Creating treasury token account");
await createAccount(connection, payerKeypair, myMint, treasuryTokenAccount);
}
console.log("Treasury token account: ", treasuryTokenAccount.toString());
/////////////////////////////
const balance = await connection.getBalance(provider.wallet.publicKey);
if (balance < web3.LAMPORTS_PER_SOL) {
await airdropSol(connection, provider.wallet.publicKey);
}
let our_balance = await connection.getBalance(provider.wallet.publicKey);
console.log("Our balance: ", our_balance);
let treasury_balance = await connection.getBalance(treasury_pubkey);
console.log("Treasury balance: ", treasury_balance);
let program_balance = await connection.getBalance(programTokenAccount);
console.log("Program balance: ", program_balance);
try {
const txHash = await program.methods.initiateSwapErc20(
INTERNAL_ID, // internal_id
SOURCE_AMOUNT, // Source native amount
DESTINATION_AMOUNT, // Target native amount
RECEIVER_ADDRESS,// receiver address
PublicKey.default, // source asset address
DESTINATION_ASSET_ADDRESS, // destination asset address
DESTINATION_CONTRACT, // destination contract
SOURCE_SYMBOL, // src symbol
DESTINATION_SYMBOL, // dest symbol
DESTINATION_NETWORK, // destination network
CONVERSION_RATE_ID, // conversion rate id
).accounts({
stateAccount: stateAccount,
signer: provider.wallet.publicKey,
systemProgram: web3.SystemProgram.programId,
programTokenAccount: programAssociatedAccount,
signerTokenAccount: userTokenAccount,
treasury: treasuryTokenAccount,
tokenMint: myMint,
swapData: swapDataAccount,
})
.rpc();
console.log("Transaction hash:", txHash);
// Optional: Wait for confirmation
await connection.confirmTransaction(txHash);
} catch (err) {
console.error("Error calling initialize function:", err);
if (err.logs) {
err.logs.forEach(log => console.error(log));
}
}
let new_our_balance = await connection.getBalance(provider.wallet.publicKey);
console.log("Our balance: ", new_our_balance);
let new_treasury_balance = await connection.getBalance(treasury_pubkey);
console.log("Treasury balance: ", new_treasury_balance);
let new_program_balance = await connection.getBalance(programTokenAccount);
console.log("Program balance: ", new_program_balance);
}
// Example usage:
callInitSwap().catch(err => console.error("Error calling initialize function:", err));
async function createNewMint(provider: AnchorProvider, authority: Keypair): Promise<PublicKey> {
const mint = await createMint(
provider.connection,
authority,
authority.publicKey,
null,
9
);
return mint;
}
Step 2: Update script path in Anchor.toml
Add the path for init_swap_erc20.ts script in Anchor.toml
// Anchor.toml
[toolchain]
[features]
seeds = false
skip-lint = false
[programs.devnet]
cross_chain_swap = "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"
initialize = "npx ts-node ./scripts/initialize.ts"
init_swap = "npx ts-node ./scripts/init_swap.ts"
init_swap_erc20 = "npx ts-node ./scripts/init_swap_erc20.ts"
Step B: Initiate ERC20 Swap from Solana to Avalanche
anchor run init_swap_erc20 --provider.cluster devnet
To verify that sawp is triggered at YOUR_AVAX_CONTRACT_ADDRESS, goto Avalanche explorer, check for the transaction with X-Talk Gateway for Avalanche mentioned in the table.
III. EVM to Solana Swap
This section is dedicated to send a token from any EVM-compatible chain (Avalanche 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 evm-sol-swap
Use the provided code below to implement the EVM to Solana swap cross-chain logic.
Your L1X EVM-Solana Flow consist of below listed smart contracts that are to be created at /programs/swap_from_evm/src/
lib.rs
solana.rs
types.rs
Create src/lib.rs
// lib.rs
use borsh::{BorshDeserialize, BorshSerialize};
use l1x_sdk::contract_interaction::ContractCall;
use l1x_sdk::types::*;
use l1x_sdk::{call_contract, caller_address};
use l1x_sdk::{contract, store::LookupMap};
use serde::{Deserialize, Serialize};
mod types;
use types::*;
mod solana;
const STORAGE_CONTRACT_KEY: &[u8; 13] = b"swap-from-evm";
const STORAGE_EVENTS_KEY: &[u8; 6] = b"events";
const EVENT_STATUS: &[u8; 6] = b"status";
const TRANSACTION_HASH: &[u8; 16] = b"transaction-hash";
const L1X_GATEAWY: &str = "799ee99fe4384b2753778868c2df38350c11dcf0";
const SUPPORTED_TOKENS: &[u8; 16] = b"supported-tokens";
const ETH_CONTRACT: &str = "1324a37390b328b33f64368854f8939e88be7556";
const XTALK_SIGNER: &str = "BeeNfY43m8n6qEHqG1sCTif4rvTo3iRCVo3NTNH5A3mG";
#[derive(BorshSerialize, BorshDeserialize)]
pub struct CrossChainSwapFromEvm {
events: LookupMap<String, Event>,
supported_token: LookupMap<String, bool>,
conversion_rate_address: String,
event_status: LookupMap<String, String>,
transaction: LookupMap<String, String>,
deviation: U256,
total_events: u64,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ParsedEthereumLog {
data: Vec<Vec<u8>>,
destination_network: String,
destination_contract_address: String,
}
impl Default for CrossChainSwapFromEvm {
fn default() -> Self {
Self {
events: LookupMap::new(STORAGE_EVENTS_KEY.to_vec()),
supported_token: LookupMap::new(SUPPORTED_TOKENS.to_vec()),
conversion_rate_address: "".to_string(),
event_status: LookupMap::new(EVENT_STATUS.to_vec()),
transaction: LookupMap::new(TRANSACTION_HASH.to_vec()),
deviation: U256::from(0),
total_events: u64::default(),
}
}
}
#[contract]
impl CrossChainSwapFromEvm {
/// Generate contract based on bytes in 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")
}
}
}
/// 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"),
};
}
/// Instantiate and save contract to storage with default values
pub fn new() {
let mut contract = Self::default();
// Self::set_all_receivers();
contract.supported_token.insert("USDT".to_string(), true);
contract.supported_token.insert("USDC".to_string(), true);
contract.supported_token.insert("BNB".to_string(), true);
contract.supported_token.insert("AVAX".to_string(), true);
contract.supported_token.insert("ETH".to_string(), true);
contract.supported_token.insert("MATIC".to_string(), true);
contract.supported_token.insert("L1X".to_string(), true);
contract.supported_token.insert("SOL".to_string(), true);
contract.save();
}
/// Save event to contract storage
///
/// - `global_tx_id`: Global transaction identifier
/// - `source_id`: Source Identifier
/// - `event_data`: Date to store in contract's storage
pub fn save_event_data(event_data: Vec<u8>, global_tx_id: String) {
l1x_sdk::msg(&format!(
"********************global tx id {} **************",
global_tx_id
));
assert_eq!(
caller_address(),
l1x_sdk::types::Address::try_from(L1X_GATEAWY).unwrap(),
"Only the gateway can call this function"
);
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");
let param_types_for_swap_request = [
"uint256".to_string(), // source_amount
"uint256".to_string(), // destination_amount
"address".to_string(), // sender
"string".to_string(), // receiver_address
"address".to_string(), // source_asset_address
"string".to_string(), // destination_asset_address
"string".to_string(), // source_asset_symbol
"string".to_string(), // destination_asset_symbol
"string".to_string(), // source_chain
"address".to_string(), // source contract address (this)
"string".to_string(), // destination_network
"string".to_string(), // conversion_rate_id
"string".to_string(), // internal_id
];
let param_types_for_swap_executed = [
"string".to_string(), // internal_id
"uint256".to_string(), // amount
"string".to_string(), // receiver_address
"address".to_string(), // asset_address
"string".to_string(), // status
"string".to_string(), // status_message
];
if let Ok(raw_swap_request) =
Self::parse_ethereum_log(event_data.clone(), param_types_for_swap_request.to_vec())
{
#[rustfmt::skip]
let swap_request = SwapRequest {
source_amount: serde_json::from_slice::<l1x_sdk::types::U256>(&raw_swap_request.data[0]).unwrap(),
destination_amount: serde_json::from_slice::<l1x_sdk::types::U256>(&raw_swap_request.data[1]).unwrap(),
sender_address: serde_json::from_slice::<l1x_sdk::types::Address>(&raw_swap_request.data[2]).unwrap(),
receiver_address: serde_json::from_slice::<String>(&raw_swap_request.data[3]).unwrap(),
source_asset_address: serde_json::from_slice::<l1x_sdk::types::Address>(&raw_swap_request.data[4]).unwrap(),
destination_asset_address: serde_json::from_slice::<String>(&raw_swap_request.data[5]).unwrap(),
source_asset_symbol: serde_json::from_slice::<String>(&raw_swap_request.data[6]).unwrap(),
destination_asset_symbol: serde_json::from_slice::<String>(&raw_swap_request.data[7]).unwrap(),
source_chain: serde_json::from_slice::<String>(&raw_swap_request.data[8]).unwrap(),
source_contract_address: serde_json::from_slice::<l1x_sdk::types::Address>(&raw_swap_request.data[9]).unwrap(),
destination_network: serde_json::from_slice::<String>(&raw_swap_request.data[10]).unwrap(),
conversion_rate_id: serde_json::from_slice::<String>(&raw_swap_request.data[11]).unwrap(),
internal_id: serde_json::from_slice::<String>(&raw_swap_request.data[12]).unwrap(),
};
l1x_sdk::msg(&format!("{:#?}", swap_request));
let event_id = swap_request.internal_id.clone();
let key = CrossChainSwapFromEvm::to_key(global_tx_id.clone(), event_id);
assert!(!contract.events.contains_key(&key), "already executed");
if Self::validate_destination_amount(
swap_request.clone(),
swap_request.destination_network.clone(),
global_tx_id.clone(),
raw_swap_request.destination_contract_address.clone(),
) {
contract.save_swap_request_event(
global_tx_id,
swap_request.internal_id.clone(),
swap_request.clone(),
swap_request.destination_network.clone(),
raw_swap_request.destination_contract_address.clone(),
);
}
} else if let Ok(raw_swap_executed) =
Self::parse_ethereum_log(event_data.clone(), param_types_for_swap_executed.to_vec())
{
let swap_executed = SwapExecuted {
global_tx_id: global_tx_id.clone(),
internal_id: serde_json::from_slice(&raw_swap_executed.data[0]).unwrap(),
amount: serde_json::from_slice(&raw_swap_executed.data[1]).unwrap(),
receiver_address: serde_json::from_slice(&raw_swap_executed.data[2]).unwrap(),
asset_address: serde_json::from_slice(&raw_swap_executed.data[3]).unwrap(),
status: serde_json::from_slice(&raw_swap_executed.data[4]).unwrap(),
status_message: serde_json::from_slice(&raw_swap_executed.data[5]).unwrap(),
};
contract.save_swap_executed_event(
global_tx_id,
swap_executed.internal_id.clone(),
swap_executed,
)
} else {
panic!("Invalid event data");
}
contract.save();
}
fn get_execute_swap_payload(
global_tx_id: String,
mut request: SwapRequest,
revert: bool,
mut destination_contract_address: String,
) -> Payload {
if revert {
request.destination_amount = request.source_amount.clone();
request.destination_asset_symbol = request.source_asset_symbol.clone();
request.receiver_address = request.sender_address.to_string();
request.destination_network = request.source_chain.clone();
destination_contract_address = request.source_contract_address.to_string();
}
if request.destination_network.to_lowercase() == "solana" && !revert {
l1x_sdk::msg("Building bytecode for solana");
if request.destination_asset_symbol == "SOL" {
l1x_sdk::msg(&format!(
"______________ destination asset symbol{}",
request.destination_asset_symbol
));
solana::get_bytecode_native(request, destination_contract_address,global_tx_id)
} else {
l1x_sdk::msg(&format!(
"______________ destination asset symbol{}",
request.destination_asset_symbol
));
solana::get_bytecode_erc20(request, destination_contract_address,global_tx_id)
}
} else {
l1x_sdk::msg("Building bytecode for evm");
let args = {
#[derive(Serialize)]
struct Args {
params: Vec<Vec<u8>>,
param_types: Vec<String>,
global_tx_id: String,
}
let bytes_global_tx_id: [u8; 32] = hex::decode(global_tx_id.clone())
.unwrap()
.try_into()
.unwrap();
Args {
params: vec![
serde_json::to_vec(&U256::from(request.destination_amount)).unwrap(),
serde_json::to_vec(&request.receiver_address).unwrap(),
serde_json::to_vec(&request.destination_asset_address).unwrap(),
serde_json::to_vec(&request.destination_asset_symbol).unwrap(),
serde_json::to_vec(&request.destination_network).unwrap(),
serde_json::to_vec(&bytes_global_tx_id).unwrap(),
],
param_types: vec![
"uint".to_string(),
"address".to_string(),
"address".to_string(),
"string".to_string(),
"string".to_string(),
"bytes32".to_string(),
],
global_tx_id,
}
};
let call = ContractCall {
contract_address: l1x_sdk::types::Address::try_from(ETH_CONTRACT).unwrap(),
method_name: "get_byte_code".to_string(),
args: serde_json::to_vec(&args).unwrap(),
gas_limit: l1x_sdk::gas_left().checked_sub(300000).expect("Out of gas"),
read_only: false,
};
let response = call_contract(&call).expect("Function returned nothing");
let data = serde_json::from_slice::<Vec<u8>>(&response).unwrap();
Payload {
data,
destination_contract_address,
destination_network: request.destination_network,
}
}
}
fn save_swap_request_event(
&mut self,
global_tx_id: String,
event_id: String,
event: SwapRequest,
_destination_network: String,
_destination_contract_address: String,
) {
let event_data: SwapRequest = event.clone().into();
l1x_sdk::msg(&format!("{:#?}", event_data));
let key = CrossChainSwapFromEvm::to_key(global_tx_id.clone(), event_id);
self.events.insert(key, Event::Request(event_data.clone()));
self.event_status
.insert(event_data.internal_id, "Pending".to_string());
l1x_sdk::msg(&format!("event saved!"));
let payload = CrossChainSwapFromEvm::get_execute_swap_payload(
global_tx_id,
event,
false,
_destination_contract_address,
);
l1x_sdk::msg(&format!("emitted event: {:?}", payload));
l1x_sdk::emit_event_experimental(payload);
}
fn save_swap_executed_event(
&mut self,
global_tx_id: String,
event_id: String,
event: SwapExecuted,
) {
let event_data: SwapExecuted = event.clone().into();
l1x_sdk::msg(&format!("{:#?}", event_data));
let key = CrossChainSwapFromEvm::to_key(global_tx_id.clone(), event_id);
self.events.insert(key, Event::Executed(event_data.clone()));
self.event_status
.set(event_data.internal_id, Some("Success".to_string()));
}
pub fn get_transaction_status(internal_id: String) -> String {
let contract = Self::load();
match contract.event_status.get(&internal_id) {
Some(status) => status.clone(),
None => "Invalid internal id".to_string(),
}
}
pub fn to_key(global_tx_id: String, event_type: String) -> String {
global_tx_id.to_owned() + "-" + &event_type
}
pub fn validate_destination_amount(
event: SwapRequest,
destination_network: String,
global_tx_id: String,
destination_contract_address: String,
) -> bool {
let mut _destination_asset_symbol = event.destination_asset_symbol.clone();
let mut _source_asset_symbol = event.source_asset_symbol.clone();
if event.destination_asset_symbol == "ARBITRUM"
|| event.destination_asset_symbol == "OPTIMISM"
{
_destination_asset_symbol = "ETH".to_string();
} else if event.source_asset_symbol == "ARBITRUM" || event.source_asset_symbol == "OPTIMISM"
{
_source_asset_symbol = "ETH".to_string();
}
if Self::load()
.supported_token
.get(&_destination_asset_symbol.to_uppercase())
.is_none()
{
panic!("invalid destination asset symbol");
}
let args = {
#[derive(Serialize)]
struct Args {
conversion_rate_id: String,
source_token: String,
destination_token: String,
}
Args {
conversion_rate_id: event.conversion_rate_id.clone(),
source_token: _source_asset_symbol,
destination_token: {
if _destination_asset_symbol == "L1X" {
"USDT".to_string()
} else {
_destination_asset_symbol
}
},
}
};
let call = ContractCall {
contract_address: l1x_sdk::types::Address::try_from(
Self::get_conversion_rate_contract(),
)
.unwrap(),
method_name: "get_conversion_rate_by_id".to_string(),
args: serde_json::to_vec(&args).unwrap(),
gas_limit: 30000000,
read_only: false,
};
let response = call_contract(&call).expect("Function returned nothing");
let rate = serde_json::from_slice::<l1x_sdk::types::U256>(&response).unwrap();
let source_decimals = Self::get_decimals(
&event.source_chain,
&event.source_asset_symbol,
event.source_asset_address.to_string(),
);
let destination_decimals = Self::get_decimals(
&destination_network,
&event.destination_asset_symbol,
event.destination_asset_address.clone(),
);
if source_decimals >= destination_decimals {
let calculated_destination_amount = U256::from(event.source_amount) * rate
/ 10u64.pow((source_decimals - destination_decimals).into())
/ 100000000;
l1x_sdk::msg(&format!(
"calcualted destination amount --> {}",
calculated_destination_amount
));
let maximum_allowed_amount = calculated_destination_amount
+ calculated_destination_amount * Self::get_allowed_deviation() / U256::from(100);
let minimum_allowed_amount = calculated_destination_amount
- calculated_destination_amount * Self::get_allowed_deviation() / U256::from(100);
if (U256::from(event.destination_amount) > maximum_allowed_amount)
|| U256::from(event.destination_amount) < minimum_allowed_amount
{
let payload = Self::get_execute_swap_payload(
global_tx_id,
event,
true,
destination_contract_address,
);
l1x_sdk::msg(&format!("emitted event: {:?}", payload));
l1x_sdk::emit_event_experimental(payload);
return false;
}
} else {
let calculated_destination_amount = U256::from(event.source_amount)
* rate
* 10u64.pow((destination_decimals - source_decimals).into())
/ 100000000;
l1x_sdk::msg(&format!(
"calcualted destination amount --> {}",
calculated_destination_amount
));
let maximum_allowed_amount = calculated_destination_amount
+ (calculated_destination_amount * Self::get_allowed_deviation() / U256::from(100));
let minimum_allowed_amount = calculated_destination_amount
- (calculated_destination_amount * Self::get_allowed_deviation() / U256::from(100));
if (U256::from(event.destination_amount) > maximum_allowed_amount)
|| (U256::from(event.destination_amount) < minimum_allowed_amount)
{
let payload = Self::get_execute_swap_payload(
global_tx_id,
event,
true,
destination_contract_address,
);
l1x_sdk::msg(&format!("emitted event: {:?}", payload));
l1x_sdk::emit_event_experimental(payload);
return false;
}
}
return true;
}
pub fn set_conversion_rate_contract(address: String) {
let mut contract = Self::load();
contract.conversion_rate_address = address;
contract.save();
}
pub fn get_conversion_rate_contract() -> String {
let contract = Self::load();
contract.conversion_rate_address
}
pub fn set_transaction_hash(internal_id: String, transaction_hash: String) {
let mut contract = Self::load();
contract.transaction.insert(internal_id, transaction_hash);
contract.save();
}
pub fn get_transaction_hash(internal_id: String) -> String {
let contract = Self::load();
contract.transaction.get(&internal_id).unwrap().clone()
}
pub fn set_allowed_deviation(deviation: U256) {
let mut contract = Self::load();
contract.deviation = deviation;
contract.save()
}
fn get_allowed_deviation() -> U256 {
Self::load().deviation
}
fn get_decimals(network_name: &str, token_symbol: &str, token_address: String) -> u32 {
if network_name == "bsc" {
return 18;
} else if network_name == "solana" {
return match token_symbol {
"USDT" => 6,
"USDC" => 6,
"BNB" => 8,
"AVAX" => 8,
"ETH" => 8,
"MATIC" => 8,
"L1X" => panic!("Unsupported token: l1x on solana"),
"SOL" => 9,
_ => panic!("invalid token symbol"),
};
} else if token_address == "0x0000000000000000000000000000000000000000"
|| token_address == "0000000000000000000000000000000000000000"
{
return 18;
} else {
return 6;
}
}
fn parse_ethereum_log(
event_data: Vec<u8>,
param_types: Vec<String>,
) -> Result<ParsedEthereumLog, ()> {
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(ETH_CONTRACT).unwrap(),
method_name: "parse_ethereum_log".to_string(),
args: serde_json::to_vec(&args).unwrap(),
gas_limit: l1x_sdk::gas_left().checked_sub(300000).expect("Out of gas"),
read_only: true,
};
let response = call_contract(&call).unwrap();
serde_json::from_slice::<Result<ParsedEthereumLog, String>>(&response).unwrap().map_err(|_| ())
}
}
Create src/solana.rs
// solana.rs
use super::types::SwapRequest;
use crate::Payload;
use borsh::{BorshDeserialize, BorshSerialize};
use serde::{Deserialize, Serialize};
use solana_sdk::{instruction::AccountMeta, pubkey::Pubkey};
use spl_associated_token_account::*;
use std::str::FromStr;
use crate::XTALK_SIGNER;
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
pub struct Instruction {
pub program_id: Pubkey,
pub accounts: Vec<AccountMeta>,
pub data: Vec<u8>,
}
#[derive(BorshSerialize, BorshDeserialize)]
struct ExecuteSwap {
internal_id: String,
global_tx_id: String,
/// Data struct, bincode encoded
message: Vec<u8>,
}
// amount, _receiver_address, _asset_address, internal_id, status, status_message
#[derive(Serialize, Deserialize, PartialEq, Debug)]
struct Data(u64, bool, String);
pub fn get_bytecode_native(event: SwapRequest, destination_contract_address: String, global_tx_id: String) -> Payload {
let mut bytecode = vec![209, 12, 21, 169, 184, 62, 52, 75];
let signer = Pubkey::from_str(XTALK_SIGNER).unwrap();
let data = Data(
event.destination_amount.as_u64(),
true,
"executed".to_string(),
);
let message = bincode::serialize(&data).unwrap();
let execute_swap_native = ExecuteSwap {
internal_id: event.internal_id.clone(),
global_tx_id,
message,
};
let encoded_data = borsh::to_vec(&execute_swap_native).unwrap();
bytecode.extend(encoded_data);
l1x_sdk::msg(&format!(
"______________ destination_contract_address{}",
destination_contract_address
));
let program_id = Pubkey::from_str(destination_contract_address.as_str()).unwrap();
let state_account = Pubkey::find_program_address(&["state_account".as_bytes()], &program_id).0;
let program_token_acc =
Pubkey::find_program_address(&["program_token_account".as_bytes()], &program_id).0;
let user_token_acc = Pubkey::from_str(event.receiver_address.as_str()).unwrap();
let accounts = vec![
AccountMeta::new(state_account, false),
AccountMeta::new(signer, true),
AccountMeta::new(program_token_acc, false),
AccountMeta::new(user_token_acc, false),
AccountMeta::new(spl_token::id(), false),
AccountMeta::new_readonly(solana_sdk::system_program::id(), false),
];
let instruction = Instruction {
program_id,
accounts: accounts.clone(),
data: bytecode,
};
let encoded_instruction = bincode::serialize(&instruction).unwrap();
let encoded_accounts = bincode::serialize(&accounts).unwrap();
let data = serde_json::to_vec(&(encoded_instruction, encoded_accounts)).unwrap();
Payload {
data,
destination_contract_address,
destination_network: event.destination_network,
}
}
pub fn get_bytecode_erc20(event: SwapRequest, destination_contract_address: String, global_tx_id: String) -> Payload {
let mut bytecode = vec![25, 146, 197, 68, 231, 230, 81, 60];
let signer = Pubkey::from_str(XTALK_SIGNER).unwrap();
let data = Data(
event.destination_amount.as_u64(),
true,
"executed".to_string(),
);
let message = bincode::serialize(&data).unwrap();
let execute_swap_native = ExecuteSwap {
internal_id: event.internal_id.clone(),
global_tx_id,
message,
};
let encoded_data = borsh::to_vec(&execute_swap_native).unwrap();
bytecode.extend(encoded_data);
l1x_sdk::msg(&format!(
"______________ destination_contract_address{}",
destination_contract_address
));
let program_id = Pubkey::from_str(destination_contract_address.as_str()).unwrap();
let state_account =
Pubkey::find_program_address(&["state_account".as_bytes()], &program_id).0;
let user = Pubkey::from_str(event.receiver_address.as_str()).unwrap();
let token_mint = Pubkey::from_str(event.destination_asset_address.as_str()).unwrap();
let user_token_account = get_associated_token_address(&user, &token_mint);
let flow_authority =
Pubkey::find_program_address(&["state_account".as_bytes()], &program_id).0;
let program_token_account = get_associated_token_address(&flow_authority, &token_mint);
let accounts = vec![
AccountMeta::new(state_account, false),
AccountMeta::new(user, false),
AccountMeta::new(signer, true),
AccountMeta::new(token_mint, false),
AccountMeta::new(user_token_account, false),
AccountMeta::new(program_token_account, false),
AccountMeta::new(spl_token::id(), false),
];
let instruction = Instruction {
program_id,
accounts: accounts.clone(),
data: bytecode,
};
let encoded_instruction = bincode::serialize(&instruction).unwrap();
let encoded_accounts = bincode::serialize(&accounts).unwrap();
let data = serde_json::to_vec(&(encoded_instruction, encoded_accounts)).unwrap();
Payload {
data,
destination_contract_address,
destination_network: event.destination_network,
}
}
Create src/types.rs
// types.rs
use borsh::{BorshDeserialize, BorshSerialize};
use serde::{Deserialize, Serialize};
/// Enumerates two types of events, SwapRequest and SwapExecuted.
#[derive(BorshSerialize, BorshDeserialize, Serialize, Deserialize)]
pub enum Event {
/// Emitted when swap is initiated.
Request(SwapRequest),
/// Emitted when swap is executed.
Executed(SwapExecuted),
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct XTalkMessageBroadcasted {
pub data: Vec<u8>,
pub destination_network: String,
pub destination_smart_contract_address: String,
}
#[derive(Clone, Debug, BorshSerialize, BorshDeserialize, Serialize, Deserialize)]
pub struct SwapRequest {
pub source_amount: l1x_sdk::types::U256,
pub destination_amount: l1x_sdk::types::U256,
pub sender_address: l1x_sdk::types::Address,
pub receiver_address: String,
pub source_asset_address: l1x_sdk::types::Address,
pub destination_asset_address: String,
pub source_asset_symbol: String,
pub destination_asset_symbol: String,
pub source_chain: String,
pub source_contract_address: l1x_sdk::types::Address,
pub destination_network: String,
pub conversion_rate_id: String,
pub internal_id: String,
}
#[derive(Clone, Debug, BorshSerialize, BorshDeserialize, Serialize, Deserialize)]
pub struct SwapExecuted {
pub global_tx_id: String,
pub internal_id: String,
pub amount: u64,
pub receiver_address: [u8; 32],
pub asset_address: [u8; 32],
pub status: bool,
pub status_message: String
}
#[derive(Clone, Debug, BorshSerialize, BorshDeserialize, Serialize, Deserialize)]
pub struct ExecuteSwapPayload {
pub destination_amount: l1x_sdk::types::U256,
pub receiver_address: l1x_sdk::types::Address,
pub destination_asset_address: l1x_sdk::types::Address,
pub destination_asset_symbol: String,
pub destination_network: String,
pub l1x_destination_contract_address: l1x_sdk::types::Address,
pub global_tx_id: [u8; 32],
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct GetPayloadResponse {
pub destination_network: String,
pub payload: Vec<u8>,
}
#[derive(Clone, Debug, BorshSerialize, BorshDeserialize, Serialize, Deserialize)]
pub struct Payload {
pub data: Vec<u8>,
pub destination_network: String,
pub destination_contract_address: String,
}
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 = "swap_from_evm"
version = "0.1.0"
edition = "2021"
authors = ["The L1X Project Developers"]
license = "Apache-2.0"
description = """
L1X contract example
"""
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[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 = "*"
getrandom = { version = "0.2.10", features = ["js"] }
hex = "0.4"
log = "0.4.20"
solana-sdk = { version = "2.0.1", features = ["borsh"] }
solana-program = {version = "2.0.1", features = ["borsh"]}
spl-associated-token-account = "3.0.2"
bs58 = "0.5.1"
bincode = "1.3.3"
spl-token = "4.0.0"
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 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 Avalanche 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_AVAX_CONTRACT_ADDRESS\\", \\
\\"source_chain\\": \\"Avalanche\\", \\
\\"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_AVAX_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\\": \\"Avalanche\\", \\
\\"event_filters\\": [\"5c6877990d83003ae27cf7c8f1a9d622868080df757847943133b78663358e42\"]}" --
Step 3: Native Token Swap EVM - Solana
Step A: Implementing Script to Swap Native from Avalanche to Solana
Step 1: Add Script to Swap Native
In your Avalanche project, inside scripts folder add swapNative.ts script.
Update relevant network details
//swapNative.ts
import { NetworkWiseConfigPath } from "../constant";
import { NetworkType, GeneralConfigType } from "../types";
import { ethers, network } from "hardhat";
import RawNetworkConfig from "../networkWise.json";
import RawGeneralConfig from "../general.json";
import { ZeroAddress } from "ethers";
import { uuid } from "uuidv4";
const NetworkConfig: NetworkType = RawNetworkConfig as NetworkType;
const GeneralConfig: GeneralConfigType = RawGeneralConfig as GeneralConfigType;
async function main() {
const [owner] = await ethers.getSigners();
console.log("Deployer Account: ",owner.address);
// Check Allowed Network
const currentNetwork = network.name.toUpperCase();
const swapContractAddress = NetworkConfig[currentNetwork].SWAP_V2_CONTRACT_ADDRESS;
const swapGasPriceContractAddress = NetworkConfig[currentNetwork].SWAP_GAS_PRICE_V2_CONTRACT || "";
const arrNetwork = Object.keys(NetworkConfig);
if(arrNetwork.includes(currentNetwork) == false){
console.log("Please use network out of : ",arrNetwork.join(", "));
return false;
}
// Check if Treasury is deployed for given network
if(ethers.isAddress(swapContractAddress) == false ){
console.log("Please deploy SwapV2Contract for ",currentNetwork);
return false;
}
if(ethers.isAddress(swapGasPriceContractAddress) == false ){
console.log("Please deploy SwapGasPriceV2Contract for ",currentNetwork);
return false;
}
const testCase:any = "MATIC/MATIC-BSC/USDC";
const _networksArr = testCase?.split("-");
const _sourceTokenArr = (_networksArr[0])?.split("/");
const _destinationTokenArr = (_networksArr[1])?.split("/");
let sourceChain = _sourceTokenArr[0];
let destinationChain = _destinationTokenArr[0];
console.log("🚀 ~ file: swapERC20.ts:46 ~ main ~ sourceChain:", sourceChain)
console.log("🚀 ~ file: swapERC20.ts:48 ~ main ~ destinationChain:", destinationChain)
let sourceToken = _sourceTokenArr[1];
let destinationToken = _destinationTokenArr[1];
console.log("🚀 ~ file: swapERC20.ts:49 ~ main ~ sourceToken:", sourceToken)
console.log("🚀 ~ file: swapERC20.ts:51 ~ main ~ destinationToken:", destinationToken)
let ASSET_ADDRESS = '0x0000000000000000000000000000000000000000';
let DEST_ASSET_ADDRESS ='YOUR_DEST_ASSET_ADDRESS'; //USDC
console.log("🚀 ~ file: swapERC20.ts:52 ~ main ~ ERC20_ADDRESS:", ASSET_ADDRESS)
console.log("🚀 ~ file: swapERC20.ts:53 ~ main ~ DEST_ERC20_ADDRESS:", DEST_ASSET_ADDRESS)
let sourceAmount = 0.01 * ( 10 ** 18 );
let destinationAmount = 0.005286 * ( 10 ** 18 );
console.log("🚀 ~ file: swapERC20.ts:64 ~ main ~ sourceAmount:", sourceAmount)
console.log("🚀 ~ file: swapERC20.ts:66 ~ main ~ destinationAmount:", destinationAmount)
// Get Deployed Swap
const SwapContractFactory = await ethers.getContractFactory("SwapV2Contract");
const SwapV2Contract = SwapContractFactory.attach(swapContractAddress);
console.log("SwapV2Contract: ",await SwapV2Contract.getAddress());
// Estimate Gas Price
const SwapGasPriceContractFactory = await ethers.getContractFactory("SwapGasPriceV2Contract");
const SwapGasPriceV2Contract = SwapGasPriceContractFactory.attach(swapGasPriceContractAddress);
console.log("🚀 ~ main ~ SwapGasPriceV2Contract:", SwapGasPriceV2Contract)
let destGasPrice = (
destinationChain == "ETH" ||
destinationChain == "BSC" ||
destinationChain == "MATIC"
) ? 30000000000 : 0;
console.log("🚀 ~ main ~ destGasPrice:", destGasPrice)
console.log("🚀 ~ file: swapERC20.ts:137 ~ main ~ ",
""+sourceAmount,
""+destinationAmount,
owner.address,
ASSET_ADDRESS,
sourceToken,
destinationToken,
destinationChain,
""+destGasPrice
);
let estimateGasPrice = await SwapGasPriceV2Contract.calculateFeeWithLatestFeedRound(
ethers.toBigInt(""+sourceAmount),
ASSET_ADDRESS,
destinationChain,
ethers.toBigInt(""+destGasPrice)
);
console.log("🚀 ~ file: swapERC20.ts:147 ~ main ~ estimateGasPrice:", estimateGasPrice)
// let increaseAmount:any = (parseInt(estimateGasPrice[7]) * 10) / 100;
// const actualSourceAmount = estimateGasPrice[7] + ethers.toBigInt(""+parseInt(increaseAmount)) + ethers.toBigInt(""+sourceAmount)
const sourceFeeAmount = (ethers.toBigInt(""+sourceAmount) * estimateGasPrice[0]) /estimateGasPrice[1];
let sourceNativeFeeAmount = estimateGasPrice[7];
sourceNativeFeeAmount+= (sourceNativeFeeAmount * ethers.toBigInt(""+1)) / ethers.toBigInt(""+100);
const actualSourceAmount = sourceNativeFeeAmount + ethers.toBigInt(""+sourceAmount) + sourceFeeAmount
console.log("🚀 ~ file: swapNative.ts:115 ~ main ~ actualDestinationAmount:", actualSourceAmount)
const destSwapContractAddress = NetworkConfig[destinationChain?.toUpperCase()].SWAP_V2_CONTRACT_ADDRESS;
// StakeContract Deposit Nativw for 1 Token
console.log("SwapV2Contract Deposit Native");
const txSwapForNative = await SwapV2Contract.initiateSwap(
ethers.toBigInt(""+sourceAmount),
destinationAmount,
owner.address,
ZeroAddress,
DEST_ASSET_ADDRESS,
destSwapContractAddress,
sourceToken,
destinationToken,
destinationChain,
"1234567",
uuid(),
""+destGasPrice,
""+estimateGasPrice[8],
""+estimateGasPrice[9],
{
from: owner.address,
value: BigInt(actualSourceAmount+"")
}
);
console.log("TX Hash: ",JSON.stringify(txSwapForNative));
await displayEvents(SwapV2Contract, txSwapForNative);
let eventData = await getEventData(SwapV2Contract, txSwapForNative,"SwapInitiated");
// Since We are BAckdating Deposit
console.log("Check Swap Details");
let globalTxId = eventData[0];
console.log("Global TX ID: ",globalTxId);
// console.log(await SwapV2Contract.swaps(owner.address,globalTxId));
}
async function displayEvents(contract : any,tx:any){
const receipt = await tx.wait();
for(const logs of receipt.logs){
try
{
const parsedLog = contract.interface.parseLog(logs);
console.log(parsedLog.name + " event emitted with values: ",parsedLog.args);
}
catch(e){
// console.log("Exception",e);
}
}
}
async function getEventData(contract : any,tx:any, event:any){
const receipt = await tx.wait();
let data:any = {};
for(const logs of receipt.logs){
try
{
const parsedLog = contract.interface.parseLog(logs);
if(parsedLog.name == event)
{
data = parsedLog.args;
break;
}
}
catch(e){
// console.log("Exception",e);
}
}
return data;
}
// We recommend this pattern to be able to use async/await everywhere
// and properly handle errors.
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
Step B: Send Native Token from Avalanche to Solana
npx hardhat run --network AVAX ./scripts/swapNative.ts
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.
Step 4: ERC20 Token Swap EVM - Solana
Step A: Implementing Script to Swap ERC20 Token from Avalanche to Solana
Step 1: Add Script to Swap ERC20 Token
In your Avalanche project, inside scripts folder add swapERC20.ts script.
Update relevant network details
// swapERC20.ts
import { NetworkWiseConfigPath } from "../constant";
import { NetworkType, GeneralConfigType } from "../types";
import { ethers, network } from "hardhat";
import RawNetworkConfig from "../networkWise.json";
import RawGeneralConfig from "../general.json";
import { uuid } from "uuidv4";
const NetworkConfig: NetworkType = RawNetworkConfig as NetworkType;
const GeneralConfig: GeneralConfigType = RawGeneralConfig as GeneralConfigType;
async function main() {
const [owner] = await ethers.getSigners();
console.log("Deployer Account: ",owner.address);
// Check Allowed Network
const currentNetwork = network.name.toUpperCase();
const swapContractAddress = NetworkConfig[currentNetwork].SWAP_V2_CONTRACT_ADDRESS;
const swapGasPriceContractAddress = NetworkConfig[currentNetwork].SWAP_GAS_PRICE_V2_CONTRACT || "";
const arrNetwork = Object.keys(NetworkConfig);
if(arrNetwork.includes(currentNetwork) == false){
console.log("Please use network out of : ",arrNetwork.join(", "));
return false;
}
// Check if Treasury is deployed for given network
if(ethers.isAddress(swapContractAddress) == false ){
console.log("Please deploy SwapV2Contract for ",currentNetwork);
return false;
}
if(ethers.isAddress(swapGasPriceContractAddress) == false ){
console.log("Please deploy SwapGasPriceV2Contract for ",currentNetwork);
return false;
}
const testCase:any = "MATIC/USDC-BSC/USDT";
const _networksArr = testCase?.split("-");
const _sourceTokenArr = (_networksArr[0])?.split("/");
const _destinationTokenArr = (_networksArr[1])?.split("/");
let sourceChain = _sourceTokenArr[0];
let destinationChain = _destinationTokenArr[0];
console.log("🚀 ~ file: swapERC20.ts:46 ~ main ~ sourceChain:", sourceChain)
console.log("🚀 ~ file: swapERC20.ts:48 ~ main ~ destinationChain:", destinationChain)
let sourceToken = _sourceTokenArr[1];
let destinationToken = _destinationTokenArr[1];
console.log("🚀 ~ file: swapERC20.ts:49 ~ main ~ sourceToken:", sourceToken)
console.log("🚀 ~ file: swapERC20.ts:51 ~ main ~ destinationToken:", destinationToken)
let ERC20_ADDRESS = NetworkConfig[sourceChain].TOKEN_CONTRACT_ADDRESS[sourceToken];
let DEST_ERC20_ADDRESS = NetworkConfig[destinationChain].TOKEN_CONTRACT_ADDRESS[destinationToken];
console.log("🚀 ~ file: swapERC20.ts:52 ~ main ~ ERC20_ADDRESS:", ERC20_ADDRESS)
console.log("🚀 ~ file: swapERC20.ts:53 ~ main ~ DEST_ERC20_ADDRESS:", DEST_ERC20_ADDRESS)
let AMOUNT = 0.3;
let sourceAmount = 0.1 * ( 10 ** (sourceChain == "BSC" ? 18 : 6) );
let destinationAmount = 0.0099 * ( 10 ** (destinationChain == "BSC" ? 18 : 6) );
console.log("🚀 ~ file: swapERC20.ts:64 ~ main ~ sourceAmount:", sourceAmount)
console.log("🚀 ~ file: swapERC20.ts:66 ~ main ~ destinationAmount:", destinationAmount)
console.log("TestCase: ",testCase);
// Get Deployed ERC20
const ERC20TokenFactory = await ethers.getContractFactory("TestERC20");
const ERC20Token = ERC20TokenFactory.attach(ERC20_ADDRESS);
console.log("Source Token "+ERC20_ADDRESS+" Balance on Source Network: ",await ERC20Token.getAddress());
// Get Deployed Swap
const SwapContractFactory = await ethers.getContractFactory("SwapV2Contract");
const SwapV2Contract = SwapContractFactory.attach(swapContractAddress);
console.log("SwapV2Contract: ",await SwapV2Contract.getAddress());
// const txMint = await ERC20Token.mint(owner.address,ethers.parseUnits("10000","ether"));
// Estimate Gas Price
const SwapGasPriceContractFactory = await ethers.getContractFactory("SwapGasPriceV2Contract");
const SwapGasPriceV2Contract = SwapGasPriceContractFactory.attach(swapGasPriceContractAddress);
let destGasPrice = (
destinationChain == "ETH" ||
destinationChain == "BSC" ||
destinationChain == "MATIC"
) ? 25000000000 : 0; // InWei
console.log("🚀 ~ file: swapERC20.ts:135 ~ main ~ destGasPrice:", destGasPrice)
console.log("🚀 ~ file: swapERC20.ts:137 ~ main ~ ",
""+sourceAmount,
""+destinationAmount,
owner.address,
ERC20_ADDRESS,
DEST_ERC20_ADDRESS,
sourceToken,
destinationToken,
destinationChain,
""+destGasPrice
)
let estimateGasPrice = await SwapGasPriceV2Contract.calculateFeeWithLatestFeedRound(
ethers.toBigInt(""+sourceAmount),
ERC20_ADDRESS,
destinationChain,
ethers.toBigInt(""+destGasPrice)
);
console.log("🚀 ~ file: swapERC20.ts:147 ~ main ~ estimateGasPrice:", estimateGasPrice)
// Get Balance
console.log("Checking Balance",await ERC20Token.balanceOf(owner.address));
// Providing TestERC20 Approval to StakeContract for 1 Token
console.log("Providing Source Token Approval to SwapV2Contract for "+sourceAmount+" Token");
if(await ERC20Token.allowance(owner.address,await SwapV2Contract.getAddress()) >= sourceAmount){
console.log("Existing Allowance Sufficient ");
}
else
{
await ERC20Token.approve(await SwapV2Contract.getAddress(),"0");
const txApprove = await ERC20Token.approve(await SwapV2Contract.getAddress(),""+sourceAmount);
await displayEvents(ERC20Token, txApprove);
}
await new Promise((resolve,reject) => setTimeout(resolve, 3000));
let _responseGas = await SwapGasPriceV2Contract.calculateFeeWithSpecificFeedRound(
ethers.toBigInt(""+sourceAmount),
ERC20_ADDRESS,
destinationChain,
ethers.toBigInt(""+destGasPrice),
ethers.toBigInt(""+estimateGasPrice[8]),
ethers.toBigInt(""+estimateGasPrice[9])
);
console.log("SwapGasPriceV2Contract > _responseGas",_responseGas);
// SwapV2Contract Deposit ERC20 for 1 Token
console.log("SwapV2Contract Deposit ERC20 for "+sourceAmount+" Token");
const destSwapContractAddress = NetworkConfig[destinationChain?.toUpperCase()].SWAP_V2_CONTRACT_ADDRESS;
/* AVAX to BSC */
let txSwapForERC20 = await SwapV2Contract.initiateSwapERC20(
""+sourceAmount,
""+destinationAmount,
owner.address,
ERC20_ADDRESS,
DEST_ERC20_ADDRESS,
destSwapContractAddress,
sourceToken,
destinationToken,
destinationChain,
"1234567",
uuid(),
""+destGasPrice,
""+estimateGasPrice[8], //round id
""+estimateGasPrice[9], //round id
{
from: owner.address,
value: estimateGasPrice[7] //native fee
}
);
console.log("TX Hash: ",JSON.stringify(txSwapForERC20));
await displayEvents(SwapV2Contract, txSwapForERC20);
let eventData = await getEventData(SwapV2Contract, txSwapForERC20,"SwapInitiated");
// Since We are BAckdating Deposit
console.log("Check Swap Details");
let globalTxId = eventData[0];
console.log("Global TX ID: ",globalTxId);
// console.log(await SwapV2Contract.swaps(owner.address,globalTxId));
}
async function displayEvents(contract : any,tx:any){
const receipt = await tx.wait();
for(const logs of receipt.logs){
try
{
const parsedLog = contract.interface.parseLog(logs);
console.log(parsedLog.name + " event emitted with values: ",parsedLog.args);
}
catch(e){
// console.log("Exception",e);
}
}
}
async function getEventData(contract : any,tx:any, event:any){
const receipt = await tx.wait();
let data:any = {};
for(const logs of receipt.logs){
try
{
const parsedLog = contract.interface.parseLog(logs);
if(parsedLog.name == event)
{
data = parsedLog.args;
break;
}
}
catch(e){
// console.log("Exception",e);
}
}
return data;
}
// We recommend this pattern to be able to use async/await everywhere
// and properly handle errors.
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
Step B: Send ERC20 Token from Avalanche to Solana
npx hardhat run --network AVAX ./scripts/swapERC20.ts
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.
Network | Contract Address |
---|---|
AVALANCHE (Mainnnet) | 0xf650162aF059734523E4Be23Ec5bAB9a5b878f57 |
SOLANA (Devnet) | GsU9N7gPtkMCSMwcEsyvj4sHcdoptHRyVwstbKiKmXeU |
Last updated