Skip to main content

Make Cross-Contract Calls

Applets are composable, meaning that any deployed Applet may invoke methods on any other deployed Applet, in a cross-contract call. In this tutorial we'll see how to implement such cross contract calls.

note

Composability of contracts is true even for contracts implemented in different languages. To learn about how specificities of languages reflect on the WIDL spec, read the Note on WIDL and Language Ergonomics.

important

The steps are slightly different depending on the language you are using, so make sure to chose the right language now.

You have chosen Rust!

Preparations

This tutorial assumes that

  • you have completed the Deploy and Use an Applet via CLI, as we will develop an Applet that makes cross-contract calls to the Counter applet you developed and deployed earlier.

To start, create a new project:

cargo new cross_counter_rust --lib
cd cross_counter_rust

Definition

Create file cross_counter.widl, with the following contents.

cross_counter.widl
interface CrossCounter {
query func fetch_counter_from(contract_id: string) -> result<uint,string>;
mutate func increment_counter_of(contract_id: string) -> result<(),string>
}

Bindings generation

Next, use the WIDL compiler to generate server-side bindings.

widl generate cross_counter.widl server rust

A file named bindings.rs should have been created. It contains a skeleton for the Applet being developed.

bindings.rs

use serde::{Deserialize, Serialize};
use weil_macros::{constructor, mutate, query, smart_contract, WeilType};


pub trait CrossCounter {
fn new() -> Result<Self, String>
where
Self: Sized;
async fn fetch_counter_from(&self, contract_id: String) -> Result<usize, String>;
async fn increment_counter_of(&mut self, contract_id: String) -> Result<(), String>;
}

#[derive(Serialize, Deserialize, WeilType)]
pub struct CrossCounterContractState {
// define your contract state here!
}

#[smart_contract]
impl CrossCounter for CrossCounterContractState {
#[constructor]
fn new() -> Result<Self, String>
where
Self: Sized,
{
unimplemented!();
}


#[query]
async fn fetch_counter_from(&self, contract_id: String) -> Result<usize, String> {
unimplemented!();
}


#[mutate]
async fn increment_counter_of(&mut self, contract_id: String) -> Result<(), String> {
unimplemented!();
}

}

Compiling the contract

Let's compile the generated skeleton to ensure that everything is correct. In the process, we'll move it to its final location.

Move the skeleton contents to file src/lib.rs.

cat bindings.rs > src/lib.rs
rm bindings.rs

Update the Cargo.toml file to be as follows and include needed dependencies.

[package]
name = "counter"
version = "0.1.0"
edition = "2021"

[dependencies]
serde = "1.0.219"
serde_json = "1.0.140"
anyhow = "1.0.97"

weil_macros = "0.1" # as a subcrate of wadk on crates.io
weil_rs = "0.1" # as a subcrate of wadk on crates.io
weil_contracts = "0.1" # as a subcrate of wadk on crates.io
wadk-utils = "0.1" # Separate public crate on crates.io

[lib]
crate-type = ["cdylib"]

Finally compile the contract into a WASM module.

cargo build --target wasm32-unknown-unknown --release

Before this contract becomes useful, we need to fill in the logic of the methods. But to fill in the logic, we must first see how to make cross-contract calls.

Cross-contract bindings

Any contract can interact with other contracts, querying information and executing functions on them, using the "call contract" function.

fn call_contract(contract_id: String, method_name: String, method_args: Option<String>) -> anyhow::Result<R>

The arguments of the function are:

  • contract_id: is contract address of the contract we want to call into
  • method_name: is the name of the method which we want to call
  • method_args: are the JSON-serialized arguments for the above method

One can either manually call this method using Runtime::call_contract or again use the WIDL compiler to generate this cross-contract call bindings. We'll do the latter one; run the following command, assuming that the counter project is in the same folder as the cross_counter one:

widl generate ../counter/counter.widl cross-contract rust

The generated cross-contract bindings.rs would look like the following:

bindings.rs
use serde::{Deserialize, Serialize};
use anyhow::Result;
use weil_rs::runtime::Runtime;


pub struct Counter {
contract_id: String,
}

impl Counter {
pub fn new(contract_id: String) -> Self {
Counter {
contract_id,
}
}
}

impl Counter {
pub async fn get_count(&self) -> Result<usize> {
let serialized_args = None;

let resp = Runtime::call_contract::<usize>(
self.contract_id.clone(),
"get_count".to_string(),
serialized_args,
)?;

Ok(resp)
}

pub async fn increment(&self) -> Result<()> {
let serialized_args = None;

let resp = Runtime::call_contract::<()>(
self.contract_id.clone(),
"increment".to_string(),
serialized_args,
)?;

Ok(resp)
}

pub async fn set_value(&self, val: usize) -> Result<()> {

#[derive(Debug, Serialize)]
struct set_valueArgs {
val: usize,
}

let serialized_args = Some(serde_json::to_string(&set_valueArgs { val })?);

let resp = Runtime::call_contract::<()>(
self.contract_id.clone(),
"set_value".to_string(),
serialized_args,
)?;

Ok(resp)
}
}

Now copy the binding.rs file to the src folder.

cp bindings.rs src/counter.rs

Filling in the logic

The Counter type in src/counter.rs acts like a proxy for a Counter Applet instance. The exact instance will be determined by the contract_id parameter passed to the implementation of CrossCounter::fetch_from_counter and CrossCounter::set_counter_of. To do so, implement methods in CrossCounter in src/lib.rs as follows:

mod counter;

use serde::{Deserialize, Serialize};
use weil_macros::{constructor, mutate, query, smart_contract, WeilType};
use crate::counter::Counter;

...

#[smart_contract]
impl CrossCounter for CrossCounterContractState {
#[constructor]
fn new() -> Result<Self, String>
where
Self: Sized,
{
Ok(CrossCounterContractState {})
}

#[query]
async fn fetch_counter_from(&self, contract_id: String) -> Result<usize,String> {
let counter = Counter::new(contract_id);
counter.get_count().map_err(|err| err.to_string())
}

#[mutate]
async fn increment_counter_of(&mut self, contract_id: String) -> Result<(),String> {
let counter = Counter::new(contract_id);
counter.increment().await.map_err(|err| err.to_string())
}
}

Deploy Contracts

Once the contract is compiled and assuming your Counter contract is also compiled, let's deploy them.

First, start wcli.

WC_PATH=~/.weilliptic 
WC_PRIVATE_KEY=~/.weilliptic cli

Execute command connect -h <sentinel-node> The following response should be seen.

{"message":"Connected successfully to <sentinel-node>.","status":"Ok"}

Deploy the Counter applet and note the contract_address in the response:

deploy --file-path path-to-counter-project/target/wasm32-unknown-unknown/release/counter.wasm --widl-file path-to-counter-widl-file 

And deploy the CrossCounter applet and note the contract_address in the response:

deploy --file-path path-to-cross-counter-project/target/wasm32-unknown-unknown/release/cross_counter.wasm --widl-file path-to-cross_counter-widl-file 

Now let's retrieve the counter of Counter Applet using the CrossCounter contract, using fetch_counter_from as in the following example.

note

In --name we pass the contract_address of CrossCounter and in --method-args we pass in the contract_address of Counter.

note

The name of the method is as defined in the WIDL, that is, in snake-case, even if the implementation had it in Camel-case

execute --name 6d1...37b --method fetch_counter_from --method-args '{"contract_id":"7b2...27d"}'

The result should inform that the counter has value 0.

{
"batch_author":"",
"batch_id":"",
"block_height":0,
"creation_time":"",
"status":"Finalized",
"tx_idx":0,
"txn_result":"{\"Ok\":\"0\"}"
}

Next, let's increment the counter through `CrossCounter.

execute --name 6d1...37b --method increment_counter_of --method-args '{"contract_id":"7b2...27d"}'

The result should show the absence of errors

{
"batch_author":"<pod-id>",
"batch_id":"853acf02df133929ff364115f123bf0730b993e58f82822a32b19c09cd416d77",
"block_height":358893,
"creation_time":"2024-10-02T10:35:15Z",
"status":"Finalized",
"tx_idx":0,
"txn_result":"{\"Ok\":\"null\"}"}

Finally, let's repeat the query:

execute --name 6d1...37b --method fetch_counter_from --method-args '{"contract_id":"7b2...27d"}'

And confirm that the counter got incremented to 1.

{
"batch_author":"",
"batch_id":"",
"block_height":0,
"creation_time":"",
"status":"Finalized",
"tx_idx":0,
"txn_result":"{\"Ok\":\"1\"}"
}

Now query the Counter Applet directly and you will see the same result.

execute --name 7b2...27d --method get_count
{
"batch_author":"",
"batch_id":"",
"block_height":0,
"creation_time":"",
"status":"Finalized",
"tx_idx":0,
"txn_result":"{\"Ok\":\"1\"}"
}

Next steps

Congratulations! You have implemented cross-contract calls in Weilliptic Applets. Next you should understand the restrictions that are put on these calls.