Cross Contract Calls
The cross contract call example demonstrates how to call a contract from another contract.
In this example there are two contracts that are compiled separately, deployed separately, and then tested together. There are a variety of ways to develop and test contracts with dependencies on other contracts, and the Soroban SDK and tooling is still building out the tools to support these workflows. Feedback appreciated here.
Run the Example
First go through the Setup process to get your development environment configured, then clone the v21.6.0
tag of soroban-examples
repository:
git clone -b v21.6.0 https://github.com/stellar/soroban-examples
Or, skip the development environment setup and open this example in Gitpod.
To run the tests for the example, navigate to the cross_contract/contract_b
directory, and use cargo test
.
cd cross_contract/contract_b
cargo test
You should see the output:
running 1 test
test test::test ... ok
Code
#[contract]
pub struct ContractA;
#[contractimpl]
impl ContractA {
pub fn add(x: u32, y: u32) -> u32 {
x.checked_add(y).expect("no overflow")
}
}
mod contract_a {
soroban_sdk::contractimport!(
file = "../contract_a/target/wasm32-unknown-unknown/release/soroban_cross_contract_a_contract.wasm"
);
}
#[contract]
pub struct ContractB;
#[contractimpl]
impl ContractB {
pub fn add_with(env: Env, contract: Address, x: u32, y: u32) -> u32 {
let client = contract_a::Client::new(&env, &contract);
client.add(&x, &y)
}
}
Ref: https://github.com/stellar/soroban-examples/tree/v21.6.0/cross_contract
How it Works
Cross contract calls are made by invoking another contract by its contract ID.
Contracts to invoke can be imported into your contract with the use of contractimport!(file = "...")
. The import will code generate:
- A
ContractClient
type that can be used to invoke functions on the contract. - Any types in the contract that were annotated with
#[contracttype]
.
The contractimport!
macro will generate the types in the module it is used, so it's a good idea to use the macro inside a mod { ... }
block, or inside its own file, so that the names of generated types don't collide with names of types in your own contract.
Open the files above to follow along.
Contract A: The Contract to be Called
The contract to be called is Contract A. It is a simple contract that accepts x
and y
parameters, adds them together and returns the result.
#[contract]
pub struct ContractA;
#[contractimpl]
impl ContractA {
pub fn add(x: u32, y: u32) -> u32 {
x.checked_add(y).expect("no overflow")
}
}
The contract uses the checked_add
method to ensure that there is no overflow, and if there is overflow, panics rather than returning an overflowed value. Rust's primitive integer types all have checked operations available as functions with the prefix checked_
.
Contract B: The Contract doing the Calling
The contract that does the calling is Contract B. It accepts a contract ID that it will call, as well as the same parameters to pass through. In many contracts the contract to call might have been stored as contract data and be retrieved, but in this simple example it is being passed in as a parameter each time.
The contract imports Contract A into the contract_a
module.
The contract_a::Client
is constructed pointing at the contract ID passed in.
The client is used to execute the add
function with the x
and y
parameters on Contract A.
mod contract_a {
soroban_sdk::contractimport!(
file = "../contract_a/target/wasm32-unknown-unknown/release/soroban_cross_contract_a_contract.wasm"
);
}
#[contract]
pub struct ContractB;
#[contractimpl]
impl ContractB {
pub fn add_with(env: Env, contract: Address, x: u32, y: u32) -> u32 {
let client = contract_a::Client::new(&env, &contract);
client.add(&x, &y)
}
}
Tests
Open the cross_contract/contract_b/src/test.rs
file to follow along.
#[test]
fn test() {
let env = Env::default();
// Register contract A using the imported Wasm.
let contract_a_id = env.register_contract_wasm(None, contract_a::Wasm);
// Register contract B defined in this crate.
let contract_b_id = env.register_contract(None, ContractB);
// Create a client for calling contract B.
let client = ContractBClient::new(&env, &contract_b_id);
// Invoke contract B via its client. Contract B will invoke contract A.
let sum = client.add_with(&contract_a_id, &5, &7);
assert_eq!(sum, 12);
}
In any test the first thing that is always required is an Env
, which is the Soroban environment that the contract will run in.
let env = Env::default();
Contract A is registered with the environment using the imported Wasm.
let contract_a_id = env.register_contract_wasm(None, contract_a::Wasm);
Contract B is registered with the environment using the contract type.
let contract_b_id = env.register_contract(None, ContractB);
All public functions within an impl
block that is annotated with the #[contractimpl]
attribute have a corresponding function generated in a generated client type. The client type will be named the same as the contract type with Client
appended. For example, in our contract the contract type is ContractB
, and the client is named ContractBClient
. The client can be constructed and used in the same way that client generated for Contract A can be.
let client = ContractBClient::new(&env, &contract_b_id);
The client is used to invoke the add_with
function on Contract B. Contract B will invoke Contract A, and the result will be returned.
let sum = client.add_with(&contract_a_id, &5, &7);
The test asserts that the result that is returned is as we expect.
assert_eq!(sum, 12);
Build the Contracts
To build the contract into a .wasm
file, use the stellar contract build
command. Both contract_call/contract_a
and contract_call/contract_b
must be built, with contract_a
being built first.
stellar contract build
Both .wasm
files should be found in both contract target
directories after building both contracts:
target/wasm32-unknown-unknown/release/soroban_cross_contract_a_contract.wasm
target/wasm32-unknown-unknown/release/soroban_cross_contract_b_contract.wasm
Run the Contract
If you have stellar-cli
installed, you can invoke contract functions. Both contracts must be deployed.
- macOS/Linux
- Windows (PowerShell)
stellar contract deploy \
--wasm target/wasm32-unknown-unknown/release/soroban_cross_contract_a_contract.wasm \
--id a
stellar contract deploy \
--wasm target/wasm32-unknown-unknown/release/soroban_cross_contract_b_contract.wasm \
--id b
stellar contract deploy `
--wasm target/wasm32-unknown-unknown/release/soroban_cross_contract_a_contract.wasm `
--id a
stellar contract deploy `
--wasm target/wasm32-unknown-unknown/release/soroban_cross_contract_b_contract.wasm `
--id b
Invoke Contract B's add_with
function, passing in values for x
and y
(e.g. as 5
and 7
), and then pass in the contract ID of Contract A.
- macOS/Linux
- Windows (PowerShell)
stellar contract invoke \
--id b \
-- \
add_with \
--contract_id a \
--x 5 \
--y 7
stellar contract invoke `
--id b `
-- `
add_with `
--contract_id a `
--x 5 `
--y 7
The following output should occur using the code above.
12
Contract B's add_with
function invoked Contract A's add
function to do the addition.