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:
- Install required dependencies: Rust and
uniond
. - Clone code example and install some dependencies,
- Build the contract,
- Create a Union account through the CLI then fund it from the faucet,
- Deploy the contract to the Union Network,
- Query the deployed transaction to obtain
code_id
, - Instantiate the contract with the
code_id
, - Query the instantiation transaction to obtain
_contract_address
, - 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
# determine your system architectureRELEASE="uniond-release-$(uname -m)-linux"# get the version we want to installVERSION="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
Install CLI packages: ripgrep
and jaq
cargo install ripgrep jaq
Create a new directory and initialize a git repository
mkdir example-ucs01-cosmwasm && cd example-ucs01-cosmwasmgit init .
Set up a new Rust project
cargo new --lib example-ucs01-cosmwasm
Add the required dependencies to the Cargo.toml
file
[package]name = "example-ucs01-cosmwasm" # name of the projectversion = "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 }
[profile.release]opt-level = "z"strip = true
Our end directory structure will look like this:
Directorysrc
- lib.rs
- Cargo.toml
- Cargo.lock
- .gitignore
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.
use cosmwasm_schema::cw_serde;use cosmwasm_std::{ entry_point, to_json_binary, Coin, DepsMut, Env, MessageInfo, Response, StdResult, Uint128, WasmMsg,};use std::collections::BTreeMap;
#[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,}
#[entry_point]pub fn instantiate( _deps: DepsMut, _env: Env, _info: MessageInfo, _msg: InstantiateMsg,) -> StdResult<Response> { Ok(Response::new().add_attribute("action", "instantiate"))}
#[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.
-
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
-
mkdir -p buildwasm-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
uniond keys add $WALLET_NAME \ --home /home/$USER/.union \ --keyring-backend test
Save the mnemonic in a safe place.
To fund the wallet, we will use the faucet. You can find the faucet here.
curl https://rest.testnet-8.union.build/cosmos/bank/v1beta1/balances/$WALLET_ADDRESS | jaq '.balances'[0]
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, use it to query the transaction to get the code_id
:
uniond query tx $DEPLOY_TX_HASH --node https://rpc.testnet-8.union.build:443 | rg "_id"
Instantiate the contract:
$CODE_ID
is thecode_id
returned from the previous step.
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, use it to query the transaction to get the _contract_address
:
query tx $INSTANTIATE_TX_HASH --node https://rpc.testnet-8.union.build:443 | rg "_contract_address"
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. Usemuno
for this example,contract_address
is the UCS01 Relay contract address (defined at the beginning),channel
is the channel to use for the transfer.
{ "transfer": { "receiver": "0xd8da6bf26964af9d7eed9e03e53415d37aa96045", "amount": "1", "denom": "muno", "contract_address": "union1m87a5scxnnk83wfwapxlufzm58qe2v65985exff70z95a2yr86yq7hl08h", "channel": "channel-86" }}
Execute the contract:
uniond \ tx wasm execute $CONTRACT_ADDRESS "$(jaq -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 yours: here