API reference
Bridge tokens from origin to destination chain

Bridge tokens from origin to destination chain

  • On-chain name of extrinsic: vector_sendMessage to send a message from Avail to Ethereum.
  • On-chain name of extrinsic: vector_execute to execute a message receieved from Ethereum on Avail.

Bridge tokens from Avail DA to Ethereum

Let us first look at how an token bridge message is being sent using the vector_sendMessage extrinsic below.

// Fungible token message to send
    let message = Message::FungibleToken {
        asset_id: H256::zero(),
        amount: config.amount_to_send as u128,
    };
 
    let da_call = avail::tx()
        .vector()
        .send_message(message, recipient, domain);
    let params = AvailExtrinsicParamsBuilder::new().build();
    let maybe_tx_progress = sdk
        .api
        .tx()
        .sign_and_submit_then_watch(&da_call, &account, params)
        .await;

Full Example

Bridge tokens from Avail DA to Ethereum

Initialize the avail-rust SDK, and generate an avail account using a mnemonic.

se alloy_network::EthereumWallet;
use alloy_provider::ProviderBuilder;
use anyhow::Result;
use avail_bridge_tools::{AvailBridgeContract, BridgeApiMerkleProof, Config};
use avail_rust::avail::vector::calls::types::send_message::Message;
use avail_rust::{avail, AvailExtrinsicParamsBuilder, Keypair, SecretUri, WaitFor, H256, SDK};
use reqwest::Url;
use serde::{Deserialize, Serialize};
use std::fs;
use std::str::FromStr;
use std::time::Duration;
 
#[tokio::main]
async fn main() -> Result<()> {
    let content = fs::read_to_string("./config.toml").expect("Read config.toml");
    let config = toml::from_str::<Config>(&content).expect("Parse config.toml");
 
    println!("Using config:\n{:#?}", config);
 
    let sdk = SDK::new(config.avail_rpc_url.as_str())
        .await
        .expect("Init SDK");
 
    //Generate Avail Account Keypair
    let secret_uri =
        SecretUri::from_str(config.avail_mnemonic.as_str()).expect("Valid secret URI");
    let account = Keypair::from_uri(&secret_uri).expect("Valid secret URI");
 

Now we set our domain (similar to chainID in EVM chains). We also set the recipient contract address, in this case it will be the VectorX Bridge contract address.

    // Ethereum domain
    let domain = 2u32;
    // Recipient address on the Ethereum network
    let recipient = config.recipient.parse()?;

After we have all this ready, we just need to define our message which includes 'asset_id' and 'amount' and send it using the vector_sendMessage extrinsic which is defined as vector().sendMessage(message, recipient, domain)

// Fungible token message to send
    let message = Message::FungibleToken {
        asset_id: H256::zero(),
        amount: config.amount_to_send as u128,
    };
 
    let da_call = avail::tx()
        .vector()
        .send_message(message, recipient, domain);
    let params = AvailExtrinsicParamsBuilder::new().build();
    let maybe_tx_progress = sdk
        .api
        .tx()
        .sign_and_submit_then_watch(&da_call, &account, params)
        .await;

Finally we wait for the transaction to be included in Avail's block and do some transaction handling.

let transaction = sdk
        .util
        .progress_transaction(maybe_tx_progress, WaitFor::BlockFinalization)
        .await;
 
    let tx_in_block = match transaction {
        Ok(tx_in_block) => tx_in_block,
        Err(message) => {
            panic!("Error: {}", message);
        }
    };
 
    println!("Finalized block hash: {:?}", tx_in_block.block_hash());
    let events = tx_in_block
        .wait_for_success()
        .await
        .expect("Waiting for success");
    println!("Transaction result: {:?}", events);
 
    let block_hash = tx_in_block.block_hash();
    let extrinsic_index = events.extrinsic_index();
 
    let block = sdk
        .rpc
        .chain
        .get_block(None)
        .await
        .expect("Get block by hash");
 

Receiving assets on Ethereum

Once we have sent the token bridging message, we can interact with the Bridge API to determine if it has been received on Ethereum.

  1. Wait and check if the block range in which our message is sent has been received on Ethereum.
let block_num = block.block.header.number;
 
    loop {
        let avail_head_info: AvailHeadInfo =
            reqwest::get(format!("{}/avl/head", config.bridge_api_url))
                .await
                .unwrap()
                .json()
                .await?;
        println!("New range: {avail_head_info:?}");
 
        if (avail_head_info.data.start..=avail_head_info.data.end).contains(&(block_num as u64)) {
            println!("Stored avail head is in range!");
            break;
        }
        tokio::time::sleep(Duration::from_secs(60)).await;
    }
 
  1. Get the proof from the VectorX Bridge API.

     
    let url: String = format!(
        "{}/eth/proof/{:?}?index={}",
        config.bridge_api_url, block_hash, extrinsic_index
    );
    println!("Proof url: {url}");
    let proof: BridgeApiMerkleProof = reqwest::get(url).await.unwrap().json().await.unwrap();
     
    println!("Proof: {proof:?}");
    let signer = config
        .ethereum_mnemonic
        .parse::<alloy_signer_local::PrivateKeySigner>()?;
    let provider = ProviderBuilder::new()
        .with_recommended_fillers()
        .wallet(EthereumWallet::from(signer))
        .on_http(Url::parse(config.ethereum_url.as_str())?);
  2. Interact with the VectorX Bridge contract to receive the AVAIL (contract.receiveAVAIL()) token by using the proof

let contract_address = config.contract_address.parse()?;
 
    let contract = AvailBridgeContract::new(contract_address, &provider);
 
    let call = contract.receiveAVAIL(proof.clone().try_into().unwrap(), proof.into());
    let pending_tx = call.send().await?;
    let res = pending_tx.watch().await?;
    println!("Result: {res:?}");
 

Bringing it all together, we have the following. Inside src/main.rs, paste the following code:

use alloy_network::EthereumWallet;
use alloy_provider::ProviderBuilder;
use anyhow::Result;
use avail_bridge_tools::{AvailBridgeContract, BridgeApiMerkleProof, Config};
use avail_rust::avail::vector::calls::types::send_message::Message;
use avail_rust::{avail, AvailExtrinsicParamsBuilder, Keypair, SecretUri, WaitFor, H256, SDK};
use reqwest::Url;
use serde::{Deserialize, Serialize};
use std::fs;
use std::str::FromStr;
use std::time::Duration;
 
#[tokio::main]
async fn main() -> Result<()> {
   let content = fs::read_to_string("./config.toml").expect("Read config.toml");
   let config = toml::from_str::<Config>(&content).expect("Parse config.toml");
 
   println!("Using config:\n{:#?}", config);
 
   let sdk = SDK::new(config.avail_rpc_url.as_str())
       .await
       .expect("Init SDK");
   let secret_uri =
       SecretUri::from_str(config.avail_mnemonic.as_str()).expect("Valid secret URI");
   let account = Keypair::from_uri(&secret_uri).expect("Valid secret URI");
 
   // Ethereum domain
   let domain = 2u32;
   // Recipient address on the Ethereum network
   let recipient = config.recipient.parse()?;
 
   // Fungible token message to send
   let message = Message::FungibleToken {
       asset_id: H256::zero(),
       amount: config.amount_to_send as u128,
   };
 
   let da_call = avail::tx()
       .vector()
       .send_message(message, recipient, domain);
   let params = AvailExtrinsicParamsBuilder::new().build();
   let maybe_tx_progress = sdk
       .api
       .tx()
       .sign_and_submit_then_watch(&da_call, &account, params)
       .await;
 
   let transaction = sdk
       .util
       .progress_transaction(maybe_tx_progress, WaitFor::BlockFinalization)
       .await;
 
   let tx_in_block = match transaction {
       Ok(tx_in_block) => tx_in_block,
       Err(message) => {
           panic!("Error: {}", message);
       }
   };
 
   println!("Finalized block hash: {:?}", tx_in_block.block_hash());
   let events = tx_in_block
       .wait_for_success()
       .await
       .expect("Waiting for success");
   println!("Transaction result: {:?}", events);
 
   let block_hash = tx_in_block.block_hash();
   let extrinsic_index = events.extrinsic_index();
 
   let block = sdk
       .rpc
       .chain
       .get_block(None)
       .await
       .expect("Get block by hash");
 
   let block_num = block.block.header.number;
 
   loop {
       let avail_head_info: AvailHeadInfo =
           reqwest::get(format!("{}/avl/head", config.bridge_api_url))
               .await
               .unwrap()
               .json()
               .await?;
       println!("New range: {avail_head_info:?}");
 
       if (avail_head_info.data.start..=avail_head_info.data.end).contains(&(block_num as u64)) {
           println!("Stored avail head is in range!");
           break;
       }
       tokio::time::sleep(Duration::from_secs(60)).await;
   }
 
   let url: String = format!(
       "{}/eth/proof/{:?}?index={}",
       config.bridge_api_url, block_hash, extrinsic_index
   );
   println!("Proof url: {url}");
   let proof: BridgeApiMerkleProof = reqwest::get(url).await.unwrap().json().await.unwrap();
 
   println!("Proof: {proof:?}");
   let signer = config
       .ethereum_mnemonic
       .parse::<alloy_signer_local::PrivateKeySigner>()?;
   let provider = ProviderBuilder::new()
       .with_recommended_fillers()
       .wallet(EthereumWallet::from(signer))
       .on_http(Url::parse(config.ethereum_url.as_str())?);
 
   let contract_address = config.contract_address.parse()?;
 
   let contract = AvailBridgeContract::new(contract_address, &provider);
 
   let call = contract.receiveAVAIL(proof.clone().try_into().unwrap(), proof.into());
   let pending_tx = call.send().await?;
   let res = pending_tx.watch().await?;
   println!("Result: {res:?}");
 
   Ok(())
}
 
#[derive(Serialize, Deserialize, Clone, Debug, Default)]
struct AvailHeadInfo {
   data: AvailHeadData,
}
 
#[derive(Serialize, Deserialize, Clone, Debug, Default)]
struct AvailHeadData {
   start: u64,
   end: u64,
}
 
  • Run the code using:
cargo run

Bridge tokens from Ethereum to Avail DA

Let us look at how a message sent from Ethereum is being executed on Avail using the vector pallet.

let da_call = avail::tx().vector().execute(
        avail_stored_slot,
        convert_addressed_message(sent_message),
        acc_proof,
        stor_proof,
    );

Full Example

Sending AVAIL tokens from Ethereum to Avail

Let us initialize some of out essentials such as signer, provider, contract, etc. This will allow us to interact with Ethereum easily.

use alloy::primitives::{Address, U256};
use alloy_network::EthereumWallet;
use alloy_provider::ProviderBuilder;
use alloy_sol_types::sol;
use anyhow::{anyhow, Result};
use avail_bridge_tools::{address_to_h256, convert_addressed_message, eth_seed_to_address, Config};
use avail_rust::avail::runtime_types::bounded_collections::bounded_vec::BoundedVec;
use avail_rust::avail_core::data_proof::AddressedMessage;
use avail_rust::{avail, AvailExtrinsicParamsBuilder, WaitFor, SDK};
use avail_rust::{subxt_signer::SecretUri, Keypair};
use reqwest::Url;
use serde::{Deserialize, Deserializer};
use sp_core::H256;
use std::{fs, str::FromStr, time::Duration};
 
sol!(
    #[sol(rpc)]
    AvailBridgeContract,
    "src/availbridge.json"
);
 
#[tokio::main]
async fn main() -> Result<()> {
    let content = fs::read_to_string("./config.toml").expect("Read config.toml");
    let config = toml::from_str::<Config>(&content).unwrap();
 
    let secret_uri =
        SecretUri::from_str(config.avail_mnemonic.as_str()).expect("Valid secret URI");
    let account = Keypair::from_uri(&secret_uri).expect("Valid secret URI");
    let recipient = account.public_key().0;
    let amount: u128 = 100000;
 
    let ethereum_signer = config
        .ethereum_mnemonic
        .parse::<alloy_signer_local::PrivateKeySigner>()?;
 
    let sender = eth_seed_to_address(config.ethereum_mnemonic.as_str());
    let provider = ProviderBuilder::new()
        .with_recommended_fillers()
        .wallet(EthereumWallet::from(ethereum_signer))
        .on_http(Url::parse(config.ethereum_url.as_str())?);
  1. Now we send a message using contract.sendAVAIL from Ethereum to Avail.
    let contract = AvailBridgeContract::new(contract_addr, &provider);
    let call = contract.sendAVAIL(recipient.into(), U256::from(amount));
    let pending_tx = call.from(sender.0.into());
    let pending_tx = pending_tx.send().await?;
    let receipt = pending_tx.get_receipt().await?;
    let block_number = receipt.block_number.ok_or(anyhow!("No block number!"))?;
    println!("Included in block no: {block_number}");
    let logs = receipt
        .inner
        .as_receipt()
        .ok_or(anyhow!("Cannot convert to receipt"))?
        .logs
        .clone();
    assert!(!logs.is_empty(), "Logs are empty!");
 
  1. Let us also define our message_id and define the parameters of our AVAIL that is being sent to be understandable by the Avail network, such as message.asset_id, message.amount, from addressm to address, origin_domain (Ethereum), destination_domain (Avail)
let message_id = u64::from_be_bytes(
        logs[0].clone().inner.data.data[32 - 8..]
            .try_into()
            .unwrap(),
    );
 
    let sent_message = AddressedMessage {
        message: avail_rust::avail_core::data_proof::Message::FungibleToken {
            asset_id: H256::zero(),
            amount,
        },
        from: address_to_h256(sender),
        to: H256(recipient),
        origin_domain: 2,
        destination_domain: 1,
        id: message_id,
    };

Retrieving and Executing the message sent to Avail

  1. Using the Bridge API we check if the Ethereum block in which we sent the message is in range for the bridge. If it is, then we get the account and storage proofs.
let (avail_stored_block_hash, avail_stored_slot) = loop {
        let ethereum_slot_info: EthereumSlotInfo =
            reqwest::get(format!("{}/eth/head", config.bridge_api_url))
                .await
                .unwrap()
                .json()
                .await?;
        println!("New slot: {ethereum_slot_info:?}");
        let block_info: BlockInfo = reqwest::get(format!(
            "{}/beacon/slot/{}",
            config.bridge_api_url, ethereum_slot_info.slot
        ))
        .await
        .unwrap()
        .json()
        .await?;
        println!("Slot to num: {}", block_info.block_number);
        if block_info.block_number >= block_number {
            println!("Stored eth head is in range!");
            break (block_info.block_hash, ethereum_slot_info.slot);
        }
 
        tokio::time::sleep(Duration::from_secs(60)).await;
    };
 
    let account_storage_proof: AccountStorageProof = reqwest::get(format!(
        "{}/avl/proof/{:?}/{}",
        config.bridge_api_url, avail_stored_block_hash, message_id
    ))
    .await
    .expect("Cannot get account/storage proofs.")
    .json()
    .await
    .expect("Cannot deserialize");
    println!("Got proof! {account_storage_proof:?}");
 
    let acc_proof = BoundedVec(
        account_storage_proof
            .account_proof
            .into_iter()
            .map(BoundedVec)
            .collect::<Vec<_>>(),
    );
    let stor_proof = BoundedVec(
        account_storage_proof
            .storage_proof
            .into_iter()
            .map(BoundedVec)
            .collect::<Vec<_>>(),
    );
 
    println!("Message: {sent_message:?}");
  1. We then initialize the Avail SDK and call the execute method in the vector pallet, which is for executing a bridged message from Ethereum. We are submitting proof against the roots stored in the pallet storage. The pallet then executes the message we are giving proof for. We only get the encoded tx, that we then have to sign and send, and wait for the transaction to included and executed on Avail.
let sdk = SDK::new(config.avail_rpc_url.as_str()).await.unwrap();
    let da_call = avail::tx().vector().execute(
        avail_stored_slot,
        convert_addressed_message(sent_message),
        acc_proof,
        stor_proof,
    );
    let params = AvailExtrinsicParamsBuilder::new().build();
    let maybe_tx_progress = sdk
        .api
        .tx()
        .sign_and_submit_then_watch(&da_call, &account, params)
        .await;
 
    let transaction = sdk
        .util
        .progress_transaction(maybe_tx_progress, WaitFor::BlockFinalization)
        .await;
 
    let tx_in_block = match transaction {
        Ok(tx_in_block) => tx_in_block,
        Err(message) => {
            panic!("Error: {}", message);
        }
    };
 
    println!("Executed at block: {:?}", tx_in_block.block_hash());

  • Bringing it all together in src/main.rs:
use alloy::primitives::{Address, U256};
use alloy_network::EthereumWallet;
use alloy_provider::ProviderBuilder;
use alloy_sol_types::sol;
use anyhow::{anyhow, Result};
use avail_bridge_tools::{address_to_h256, convert_addressed_message, eth_seed_to_address, Config};
use avail_rust::avail::runtime_types::bounded_collections::bounded_vec::BoundedVec;
use avail_rust::avail_core::data_proof::AddressedMessage;
use avail_rust::{avail, AvailExtrinsicParamsBuilder, WaitFor, SDK};
use avail_rust::{subxt_signer::SecretUri, Keypair};
use reqwest::Url;
use serde::{Deserialize, Deserializer};
use sp_core::H256;
use std::{fs, str::FromStr, time::Duration};
 
sol!(
    #[sol(rpc)]
    AvailBridgeContract,
    "src/availbridge.json"
);
 
#[tokio::main]
async fn main() -> Result<()> {
    let content = fs::read_to_string("./config.toml").expect("Read config.toml");
    let config = toml::from_str::<Config>(&content).unwrap();
 
    let secret_uri =
        SecretUri::from_str(config.avail_mnemonic.as_str()).expect("Valid secret URI");
    let account = Keypair::from_uri(&secret_uri).expect("Valid secret URI");
    let recipient = account.public_key().0;
    let amount: u128 = 100000;
 
    let ethereum_signer = config
        .ethereum_mnemonic
        .parse::<alloy_signer_local::PrivateKeySigner>()?;
 
    let sender = eth_seed_to_address(config.ethereum_mnemonic.as_str());
    let provider = ProviderBuilder::new()
        .with_recommended_fillers()
        .wallet(EthereumWallet::from(ethereum_signer))
        .on_http(Url::parse(config.ethereum_url.as_str())?);
 
    let contract_addr: Address = config.contract_address.parse()?;
 
    let contract = AvailBridgeContract::new(contract_addr, &provider);
 
    let call = contract.sendAVAIL(recipient.into(), U256::from(amount));
    let pending_tx = call.from(sender.0.into());
    let pending_tx = pending_tx.send().await?;
    let receipt = pending_tx.get_receipt().await?;
    let block_number = receipt.block_number.ok_or(anyhow!("No block number!"))?;
    println!("Included in block no: {block_number}");
    let logs = receipt
        .inner
        .as_receipt()
        .ok_or(anyhow!("Cannot convert to receipt"))?
        .logs
        .clone();
    assert!(!logs.is_empty(), "Logs are empty!");
 
    let message_id = u64::from_be_bytes(
        logs[0].clone().inner.data.data[32 - 8..]
            .try_into()
            .unwrap(),
    );
 
    let sent_message = AddressedMessage {
        message: avail_rust::avail_core::data_proof::Message::FungibleToken {
            asset_id: H256::zero(),
            amount,
        },
        from: address_to_h256(sender),
        to: H256(recipient),
        origin_domain: 2,
        destination_domain: 1,
        id: message_id,
    };
 
    let (avail_stored_block_hash, avail_stored_slot) = loop {
        let ethereum_slot_info: EthereumSlotInfo =
            reqwest::get(format!("{}/eth/head", config.bridge_api_url))
                .await
                .unwrap()
                .json()
                .await?;
        println!("New slot: {ethereum_slot_info:?}");
        let block_info: BlockInfo = reqwest::get(format!(
            "{}/beacon/slot/{}",
            config.bridge_api_url, ethereum_slot_info.slot
        ))
        .await
        .unwrap()
        .json()
        .await?;
        println!("Slot to num: {}", block_info.block_number);
        if block_info.block_number >= block_number {
            println!("Stored eth head is in range!");
            break (block_info.block_hash, ethereum_slot_info.slot);
        }
 
        tokio::time::sleep(Duration::from_secs(60)).await;
    };
 
    let account_storage_proof: AccountStorageProof = reqwest::get(format!(
        "{}/avl/proof/{:?}/{}",
        config.bridge_api_url, avail_stored_block_hash, message_id
    ))
    .await
    .expect("Cannot get account/storage proofs.")
    .json()
    .await
    .expect("Cannot deserialize");
    println!("Got proof! {account_storage_proof:?}");
 
    let acc_proof = BoundedVec(
        account_storage_proof
            .account_proof
            .into_iter()
            .map(BoundedVec)
            .collect::<Vec<_>>(),
    );
    let stor_proof = BoundedVec(
        account_storage_proof
            .storage_proof
            .into_iter()
            .map(BoundedVec)
            .collect::<Vec<_>>(),
    );
 
    println!("Message: {sent_message:?}");
 
    let sdk = SDK::new(config.avail_rpc_url.as_str()).await.unwrap();
    let da_call = avail::tx().vector().execute(
        avail_stored_slot,
        convert_addressed_message(sent_message),
        acc_proof,
        stor_proof,
    );
    let params = AvailExtrinsicParamsBuilder::new().build();
    let maybe_tx_progress = sdk
        .api
        .tx()
        .sign_and_submit_then_watch(&da_call, &account, params)
        .await;
 
    let transaction = sdk
        .util
        .progress_transaction(maybe_tx_progress, WaitFor::BlockFinalization)
        .await;
 
    let tx_in_block = match transaction {
        Ok(tx_in_block) => tx_in_block,
        Err(message) => {
            panic!("Error: {}", message);
        }
    };
 
    println!("Executed at block: {:?}", tx_in_block.block_hash());
 
    Ok(())
}
 
#[derive(Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
struct BlockInfo {
    block_number: u64,
    block_hash: H256,
}
 
#[derive(Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
struct EthereumSlotInfo {
    pub slot: u64,
    pub _timestamp: u64,
    pub _timestamp_diff: u64,
}
 
#[derive(Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
struct AccountStorageProof {
    #[serde(deserialize_with = "bytes_from_hex")]
    account_proof: Vec<Vec<u8>>,
    #[serde(deserialize_with = "bytes_from_hex")]
    storage_proof: Vec<Vec<u8>>,
}
 
fn bytes_from_hex<'de, D>(deserializer: D) -> Result<Vec<Vec<u8>>, D::Error>
where
    D: Deserializer<'de>,
{
    let buf = <Vec<String>>::deserialize(deserializer)?;
    let res = buf
        .iter()
        .map(|e| {
            let without_prefix = e.trim_start_matches("0x");
            hex::decode(without_prefix).unwrap()
        })
        .collect::<Vec<_>>();
 
    Ok(res)
}
  • Run the code using:
cargo run