API reference
Bridge an arbitrary message from origin to destination chain

Bridge a message 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.

Send an arbitrary message from Avail DA to Ethereum

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

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

Sending arbitrary message from Avail to Ethereum.

Initialize the avail-rust SDK.

 
use alloy_network::EthereumWallet;
use alloy_provider::ProviderBuilder;
use anyhow::Result;
use avail_bridge_tools::{address_to_h256, AvailBridgeContract, BridgeApiMerkleProof, Config};
use avail_rust::avail::runtime_types::bounded_collections::bounded_vec::BoundedVec;
use avail_rust::avail::vector::calls::types::send_message::Message;
use avail_rust::{avail, AvailExtrinsicParamsBuilder, Keypair, SecretUri, WaitFor, 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("Initializing 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");
 
}
 
 

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 contract address on the Ethereum network
    let recipient = address_to_h256(config.receive_message_contract_address.parse()?);
 

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

 
 let data = BoundedVec(config.message_data.as_bytes().to_vec());
    // Arbitrary message to send
    let message = Message::ArbitraryMessage(data);
 
    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");
 

Retrieving data on Ethereum

Once we have sent the arbitrary message, we can interact with the Bridge API to determine if it has been sent to 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>()?;
 
  1. Interact with the VectorX Bridge contract to retrieve the message
    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.receiveMessage(proof.clone().try_into().unwrap(), proof.into());
    let pending_tx = call.send().await?;
    let res = pending_tx.watch().await?;
    println!("Result: {res:?}");
 
    Ok(())
 

  • 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::{address_to_h256, AvailBridgeContract, BridgeApiMerkleProof, Config};
use avail_rust::avail::runtime_types::bounded_collections::bounded_vec::BoundedVec;
use avail_rust::avail::vector::calls::types::send_message::Message;
use avail_rust::{avail, AvailExtrinsicParamsBuilder, Keypair, SecretUri, WaitFor, 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("Initializing 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 contract address on the Ethereum network
    let recipient = address_to_h256(config.receive_message_contract_address.parse()?);
 
    let data = BoundedVec(config.message_data.as_bytes().to_vec());
    // Arbitrary message to send
    let message = Message::ArbitraryMessage(data);
 
    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.receiveMessage(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

Send an arbitrary message 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 arbitrary message 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;
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, Keypair, SecretUri, WaitFor, SDK};
use reqwest::Url;
use serde::{Deserialize, Deserializer};
use sp_core::H256;
use std::fs;
use std::str::FromStr;
use std::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).expect("Parse config.toml");
 
    let secret_uri = SecretUri::from_str(config.avail_mnemonic.as_str())
        .expect("parse avail sender mnemonic");
    let account = Keypair::from_uri(&secret_uri).expect("create keypair");
 
    let recipient = account.public_key().0;
 
    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);
 
}
 
  1. Now we send a message using contract.sendMessage from Ethereum to Avail.
let call = contract.sendMessage(recipient.into(), config.clone().message_data.into());
    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::ArbitraryMessage(
            config.message_data.as_bytes().to_vec().try_into().unwrap(),
        ),
        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 block number: {}", 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
        .expect("create sdk");
 
  1. We then call in Avail to 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 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!("Finalized block hash: {:?}", tx_in_block.block_hash());

Bringing it all together in src/main.rs:

use alloy::primitives::Address;
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, Keypair, SecretUri, WaitFor, SDK};
use reqwest::Url;
use serde::{Deserialize, Deserializer};
use sp_core::H256;
use std::fs;
use std::str::FromStr;
use std::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).expect("Parse config.toml");
 
    let secret_uri = SecretUri::from_str(config.avail_mnemonic.as_str())
        .expect("parse avail sender mnemonic");
    let account = Keypair::from_uri(&secret_uri).expect("create keypair");
 
    let recipient = account.public_key().0;
 
    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.sendMessage(recipient.into(), config.clone().message_data.into());
    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::ArbitraryMessage(
            config.message_data.as_bytes().to_vec().try_into().unwrap(),
        ),
        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 block number: {}", 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
        .expect("create sdk");
 
    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!("Finalized block hash: {:?}", 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