Skip to content

UCS01 Relay Rust Integration

This example demonstrates how to create a CosmWasm contract that calls Union’s UCS01 Relay to execute a cross-chain asset transfer. Summary of the steps:

  1. Install required dependencies: Rust and uniond.
  2. Clone code example and install some dependencies,
  3. Build the contract,
  4. Create a Union account through the CLI then fund it from the faucet,
  5. Deploy the contract to the Union Network,
  6. Query the deployed transaction to obtain code_id,
  7. Instantiate the contract with the code_id,
  8. Query the instantiation transaction to obtain _contract_address,
  9. Execute the contract to transfer assets 🎉

Source code for this entire example can be found here


Prerequisites

Install the uniond binary which you will use to publish the contract and execute the transfer

cd $HOME
# determine your system architecture
RELEASE="uniond-release-$(uname -m)-linux"
# get the version we want to install
VERSION="v0.24.0"
curl --location \
https://github.com/unionlabs/union/releases/download/$VERSION/$RELEASE \
--output uniond

Make uniond executable

chmod +x uniond

Install Rust

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
. "$HOME/.cargo/env"

Install development tools

sudo apt update
sudo apt install jq tree git ripgrep build-essential

Install wasm packages: wasm-pack and wasm-opt

cargo install wasm-pack wasm-opt

Set up a new Rust project

cargo new --lib example-ucs01-cosmwasm
cd example-ucs01-cosmwasm

Add the required dependencies to the Cargo.toml file

Cargo.toml
[package]
name = "example-ucs01-cosmwasm" # name of the project
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", 'rlib']
[dependencies]
cosmwasm-schema = { version = "2.1.4" }
cosmwasm-std = { version = "2.1.4", default-features = false, features = ["std", "staking", "stargate"] }
serde = { version = "1.0.210", default-features = false, features = ["derive"] }
thiserror = { version = "1.0.64", default-features = false }
[features]
library = []
[profile.release]
opt-level = "z"
strip = true

Configure Rust-nightly

rustup override set nightly-2024-10-11
rustup component add rust-src --toolchain nightly-2024-10-11-aarch64-unknown-linux-gnu

Our end directory structure will look like this:

  • Cargo.toml
  • Directorysrc
    • lib.rs

The UCS01 Relay contract address:

export UCS01_RELAY="union1m87a5scxnnk83wfwapxlufzm58qe2v65985exff70z95a2yr86yq7hl08h"

Basic Contract

Let’s write a dead simple contract that transfers an asset from a CosmWasm contract to an EVM contract.

src/lib.rs
use cosmwasm_schema::cw_serde;
use cosmwasm_std::{
entry_point, to_json_binary, Coin, DepsMut, Env, MessageInfo, Response, StdResult, Uint128,
WasmMsg,
};
#[cw_serde]
pub struct InstantiateMsg {}
#[cw_serde]
pub enum ExecuteMsg {
Transfer {
channel: String,
receiver: String,
amount: Uint128,
denom: String,
contract_address: String,
},
}
#[cw_serde]
pub enum Ucs01ExecuteMsg {
Transfer(TransferMsg),
}
/// This is the message we accept via Receive
#[cw_serde]
pub struct TransferMsg {
/// The local channel to send the packets on
pub channel: String,
/// The remote address to send to.
pub receiver: String,
/// How long the packet lives in seconds. If not specified, use default_timeout
pub timeout: Option<u64>,
/// The memo
pub memo: String,
}
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn instantiate(
_deps: DepsMut,
_env: Env,
_info: MessageInfo,
_msg: InstantiateMsg,
) -> StdResult<Response> {
Ok(Response::new().add_attribute("action", "instantiate"))
}
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn execute(
deps: DepsMut,
_env: Env,
info: MessageInfo,
msg: ExecuteMsg,
) -> StdResult<Response> {
match msg {
ExecuteMsg::Transfer {
receiver,
channel,
amount,
denom,
contract_address,
} => execute_transfer(
deps,
info,
receiver,
amount,
denom,
channel,
contract_address,
),
}
}
pub fn execute_transfer(
_deps: DepsMut,
_info: MessageInfo,
receiver: String,
amount: Uint128,
denom: String,
channel: String,
contract_address: String,
) -> StdResult<Response> {
let msg = WasmMsg::Execute {
contract_addr: contract_address.to_string(),
msg: to_json_binary(&Ucs01ExecuteMsg::Transfer(TransferMsg {
channel,
receiver: receiver.clone(),
timeout: None,
memo: "".into(),
}))?,
funds: vec![Coin { denom, amount }],
};
Ok(Response::new()
.add_message(msg)
.add_attribute("action", "transfer")
.add_attribute("recipient", receiver)
.add_attribute("amount", amount.to_string()))
}

You will be using the same RPC url across examples, so good to export it to an environment variable:

export RPC_URL="https://rpc.testnet-8.union.build:443"

Building the Contract

The build is two steps; first we compile the Rust code to WASM, and then we optimize the WASM to be smaller.

  1. RUSTFLAGS='-C target-cpu=mvp -C opt-level=z' cargo build \
    --target wasm32-unknown-unknown \
    --no-default-features \
    --lib \
    --release \
    -Z build-std=std,panic_abort \
    -Z build-std-features=panic_immediate_abort
  2. mkdir -p build
    wasm-opt target/wasm32-unknown-unknown/release/example_ucs01_cosmwasm.wasm \
    -o build/contract.wasm \
    -O3 --converge

Deploying the Contract

we need a wallet with some gas money in it to deploy the contract

Pick a name for the wallet and create it

export WALLET_NAME="throwaway"

Let’s create a new wallet and fund it

$HOME/uniond keys add $WALLET_NAME \
--home /home/$USER/.union \
--keyring-backend test

Save the mnemonic and address in a safe place

Set the WALLET_ADDRESS

WALLET_ADDRESS=union1...

To fund the wallet, we will use the faucet. You can find the faucet here.

Ensure the wallet is funded
curl https://rest.testnet-8.union.build/cosmos/bank/v1beta1/balances/$WALLET_ADDRESS | jq '.balances'[0]
Deploy the contract
$HOME/uniond tx wasm store ./build/contract.wasm \
--from $WALLET_NAME \
--gas auto \
--gas-adjustment 1.4 \
--chain-id union-testnet-8 \
--keyring-backend test \
--home /home/$USER/.union \
--node $RPC_URL --yes

The above will return a transaction hash at the end as txhash. Record it:

DEPLOY_TX_HASH=txhash-value-of-previous-command

… and use it to query the transaction to get the code_id:

$HOME/uniond query tx $DEPLOY_TX_HASH --node https://rpc.testnet-8.union.build:443 | rg -C 1 "code_id"

Record the code-id:

CODE_ID=code_id-value-of-previous-command

… and instantiate the contract:

$HOME/uniond \
tx wasm instantiate $CODE_ID '{}' \
--label foobar \
--no-admin \
--from $WALLET_NAME \
--gas auto \
--gas-adjustment 1.4 \
--chain-id union-testnet-8 \
--keyring-backend test \
--home /home/$USER/.union \
--node $RPC_URL --yes

The above will return a transaction hash at the end as txhash. record it:

INSTANTIATE_TX_HASH=txhash-value-of-previous-command

… and use it to query the transaction to get the _contract_address (you’ll see it twice):

$HOME/uniond query tx $INSTANTIATE_TX_HASH --node https://rpc.testnet-8.union.build:443 | rg -C 1 "_contract_address"

Record the contract address

CONTRACT_ADDRESS=_contract_address-value-of-previous-command

Now you can execute the contract to transfer assets:

Let’s construct the JSON payload for the contract execution.

  • receiver field we are using Vitalik’s address,
  • amount is the amount of tokens to transfer,
  • denom is the token’s denomination. Use muno for this example,
  • contract_address is the UCS01 Relay contract address (defined at the beginning),
  • channel is the channel to use for the transfer.
payload.json
{
"transfer": {
"receiver": "d8da6bf26964af9d7eed9e03e53415d37aa96045",
"amount": "1",
"denom": "muno",
"contract_address": "union1m87a5scxnnk83wfwapxlufzm58qe2v65985exff70z95a2yr86yq7hl08h",
"channel": "channel-86"
}
}

Execute the contract:

$HOME/uniond \
tx wasm execute $CONTRACT_ADDRESS "$(jq -c '.' payload.json)" \
--from $WALLET_NAME \
--gas auto \
--gas-adjustment 1.4 \
--chain-id union-testnet-8 \
--keyring-backend test \
--home /home/$USER/.union \
--node $RPC_URL --amount 2muno

To see the result of your cross chain transfer, go to this query and replace the sender with your $WALLET_ADDRESS: here