Skip to main content

Workspace

The workspace example demonstrates how multiple smart contracts can be developed, tested, and built side-by-side in the same Rust workspace.

Open in Gitpod

Run the Example

First go through the Setup process to get your development environment configured, then clone the soroban-examples repository:

git clone 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 use cargo test.

cd workspace
cargo test

You should see three sets of output, one for contract_a, contract_a_interface, and contract_b. The first two crates in the workspace contain no tests, but the third crate should give you the following output:

running 1 test
test test::test_token_auth ... ok

Code

workspace/contract_a_interface/src/lib.rs
#![no_std]
use soroban_sdk::contractclient;

#[contractclient(name = "ContractAClient")]
pub trait ContractAInterface {
fn add(x: u32, y: u32) -> u32;
}

Ref: https://github.com/stellar/soroban-examples/tree/main/workspace

How It Works

There are three crates that are part of the workspace.

  1. contract_a_interface contains a trait, ContractAInterface, that only serves as a place to define the interface trait for Contract A.
  2. contract_a contains a smart contract, ContractA, that implements logic, and conforms to the ContractAInterface trait.
  3. contract_b is another smart contract, implementing a different interface, and makes a call to ContractA, and cross-calls the contract_a function. This is also the only crate in the workspace with defined tests.

Let's take a look at each crate, and see how they all work together.

Contract A Interface: The Trait

The contract_a_interface crate defines a trait containing a contract interface. This interface is defined separate from the implementation and only defines what the exported functions of the implementation.

The interface defines add as the only function the contract will contain. The add function requires two inputs, x and y, both u32 integers, and returns a u32 integer as well.

The use of contractclient as an attribute macro on ContractAInterface means a client will be created, conforming to this interface, that can be used by contracts existing outside of the contract_a_interface crate. As you'll see later, the client, ContractAClient, is used in the contract_b crate to call the add function.

Contract A: The Logic

The contract_a crate contains the implementation for Contract A, defining for each function what it should actually do.

The implementation takes the value of x, along with the value of y, and performs a checked_add, returning the sum of both numbers (while avoiding an overflow error).

info

The add function doesn't require an Env argument. That's totally fine! Env has many useful features that are available to your smart contracts, if you need them. But, you're not at all required to use it, if you don't.

All that is required to make use of the previously defined ContractAInterface is to use the interface, define a ContractA struct, and impl the interface for that struct.

This crate uses the contractimpl attribute macro on the ContractA implementation, making the add function public and invocable by others on the Stellar network.

Contract B: The Invocation

Now that we've created a trait in contract_a_interface, and implemented it in contract_a, we can use the contract_b crate to invoke the add function and get the sum of our integers.

We're creating and implementing an entirely new contract, ContractB. We're skipping the trait, and getting right to the contract itself.

::: info

Defining the contract interface with a trait separately is optional. It's a great way to share the interface of a contract, and a contract client, with multiple contracts in a multi-contract workspace.

:::

ContractB has one function, add_with, that requires three arguments:

  • contract: Address - The address of a contract which implements that required client interface, ContractAInterface.
  • x: u32 and y: u32 - The two numbers we want to (safely) compute the sum of.

ContractB invokes ContractA's add function, returning its value back to the original invoker. It's a bit of a long round-trip for this simple example, but it illustrates a really powerful way you can separate out interface/trait definitions from contract logic and share it and its client in a multi-contract workspace.

Practical Use-Case Examples

Beyond this simple example, this technique is versatile and useful. For example, this strategy could be used to:

  • Create and reference a standardized, consistent token interface.
  • Reuse a single interface that you want to incorporate across many different contracts.

Build the Contracts

To build the contracts into a set of .wasm files, use the stellar contract build command. Both workspace/contract_a and workspace/contract_b will be built, and you can use a single command, since our workspace defines its members in the Cargo.toml file:

stellar contract build

Two .wasm files should be found in the workspace/target directory:

target/wasm32-unknown-unknown/release/soroban_workspace_contract_a.wasm
target/wasm32-unknown-unknown/release/soroban_workspace_contract_b.wasm

The stellar-cli knows the contract_a_interface is not intended to be compiled into a .wasm, because the contract_a_interface's Cargo.toml has its crate-type configured as rlib (rust library). Nice!

Run the Contract

If you have stellar-cli installed, you can invoke contract the functions. Both contracts must be deployed.

stellar contract deploy \
--wasm target/wasm32-unknown-unknown/release/soroban_workspace_contract_a.wasm \
stellar contract deploy \
--wasm target/wasm32-unknown-unknown/release/soroban_workspace_contract_b.wasm \

Invoke ContractB's add_with function, passing in ContractA's address for contract, and integer values for x and y (e.g. as 5 and 7).

stellar contract invoke \
--id CONTRACT_B_ADDRESS \
-- \
add_with \
--contract CONTRACT_A_ADDRESS \
--x 5 \
--y 7