Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add tutorial working with workspaces #1106

Merged
merged 6 commits into from
Feb 20, 2025
Merged
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
357 changes: 357 additions & 0 deletions docs/build/guides/conventions/workspace.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,357 @@
---
title: Workspaces
hide_table_of_contents: true
description: Organize contracts using Cargo workspaces
---

### Initializing workspace project

Using Cargo's workspace [feature](https://doc.rust-lang.org/book/ch14-03-cargo-workspaces.html) makes it very convenient to organize your smart contracts in subdirectories of your project's root.

It's very simple to get started using the cli:

```
stellar contract init . --name add_contract
```

Running this command will create a root project directory (`soroban-project`) and then initialize a project workspace with a single contract named `add_contract`.

Adding one more contract template to the project can be done using the same command:

```
stellar contract init . --name main_contract
```

The project tree will look like this:

```
$ tree
.
├── Cargo.toml
├── contracts
│   ├── add-contract
│   │   ├── Cargo.toml
│   │   ├── Makefile
│   │   └── src
│   │   ├── lib.rs
│   │   └── test.rs
│   └── main-contract
│   ├── Cargo.toml
│   ├── Makefile
│   └── src
│   ├── lib.rs
│   └── test.rs
└── README.md
```

Running `stellar contract init` command created a sample contracts subdirectories (located in `./contracts), each contains:

1. Cargo.toml file with all (minimal) necessary dependencies
2. `Makefile` to easily build contract using `make` command
3. `src` directory with an example hello-world contract and a simple test.

We can now build our workspace's contracts with

```
stellar contract build
```

command and check the build directory `target/wasm32-unknown-unknown/release/`

### Cross-contract call between workspaces

With the given project structure, cross-contract calls can be easily made. Starting with modifying the sample hello world contract into an add contract:

```rust title="contracts/add_contract/src/lib.rs"
#![no_std]
use soroban_sdk::{contract, contractimpl};

#[contract]
pub struct ContractAdd;

#[contractimpl]
impl ContractAdd {
pub fn add(x: u32, y: u32) -> u32 {
x.checked_add(y).expect("no overflow")
}
}
```

The `ContractAdd` can now be referenced and used from another contracts in the same workspace using [`contractimport`](https://docs.rs/soroban-sdk/latest/soroban_sdk/macro.contractimport.html) macros. Note that the `contractimport` requires relative path of the compiled wasm file, so first it's required to recompile the contract with:

```
stellar contract build
```

As seen previously, target wasm file is located in

```
$PROJECT_ROOT/target/wasm32-unknown-unknown/release/
```

In order to reference this wasm file from `contracts/main_contract/src/lib.rs`, we need to first construct the path to the project root: `../..` (relative to second contract's Cargo.toml file location) followed by path to the wasm file:

```
../../../target/wasm32-unknown-unknown/release/add_contract.wasm
```

It can now be used in the rust code:

```rust title="contracts/main_contract/src/lib.rs"
#![no_std]

use soroban_sdk::{contract, contractimpl, Address, Env};

mod contract_add {
soroban_sdk::contractimport!(
file = "../../../target/wasm32-unknown-unknown/release/add_contract.wasm"
);
}

#[contract]
pub struct ContractMain;

#[contractimpl]
impl ContractMain {
pub fn add_with(env: Env, contract: Address, x: u32, y: u32) -> u32 {
let client = contract_add::Client::new(&env, &contract);
client.add(&x, &y)
}
}

mod test;
```

Here, main contract will invoke `ContractAdd`'s `add` function to calculate the sum of 2 numbers. It's a good idea to update tests for our main contract as well:

```rust title="contracts/main_contract/src/test.rs"
#![cfg(test)]

use crate::{contract_add, ContractMain, ContractMainClient};
use soroban_sdk::Env;

#[test]
fn test_adding_cross_contract() {
let env = Env::default();

// Register add contract using the imported WASM.
let contract_add_id = env.register_contract_wasm(None, contract_add::WASM);

// Register main contract defined in this crate.
let contract_main_id = env.register_contract(None, ContractMain);

// Create a client for calling main contract.
let client = ContractMainClient::new(&env, &contract_main_id);

// Invoke main contract via its client. Main contract will invoke add contract.
let sum = client.add_with(&contract_add_id, &5, &7);
assert_eq!(sum, 12);
}
```

Contracts can now be re-complied running the following command from the project root:

```
stellar contract build
```

And check that the test is working correctly, running tests in `contracts/main_contract`:

```
cargo test

running 1 test
test test::test_adding_cross_contract ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s
```

Finally, let's deploy this contracts and call our main contract using cli. If you haven't already, set up an account (alice) first

```bash
stellar keys generate alice --fund --network testnet
STELLAR_SOURCE_ACCOUNT=alice
```

Second is to deploy the contracts:

```bash
stellar contract deploy --network testnet --wasm target/wasm32-unknown-unknown/release/add_contract.wasm --alias add_contract
stellar contract deploy --network testnet --wasm target/wasm32-unknown-unknown/release/main_contract.wasm --alias main_contract
```

And finally call the main contract:

```
$ stellar contract invoke --id main_contract --network testnet -- add_with --contract add_contract --x 9 --y 10
ℹ️ Send skipped because simulation identified as read-only. Send by rerunning with `--send=yes`.
19
```

### Adding contract interfaces

As the next step, we can abstract away add contract and allow it to have multiple implementations. Main contract will in turn use the contract interface that is not bound to its implementation.

```
stellar contract init . --name adder_interface
stellar contract init . --name add_extra_contract
```

First, let's create an interface and change our existing implementation to use this interface:

```rust title="contracts/adder_interface/src/lib.rs"
#![no_std]

use soroban_sdk::contractclient;

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

To use the interface definition our workspace members will now have an `adder_interface` as a dependency:

```toml title="./Cargo.toml"
# <...>
[workspace.dependencies]
soroban-sdk = "21.0.0"
adder-interface = { path = "contracts/adder_interface" }
# <...>
```

```toml title="./contracts/add_contract/Cargo.toml"
# <...>
[dependencies]
soroban-sdk = { workspace = true }
adder-interface = {workspace = true}
# <...>
```

```toml title="./contracts/main_contract/Cargo.toml"
# <...>
[dependencies]
soroban-sdk = { workspace = true }
adder-interface = {workspace = true}
# <...>
```

```toml title="./contracts/add_extra_contract/Cargo.toml"
# <...>
[dependencies]
soroban-sdk = { workspace = true }
adder-interface = {workspace = true}
# <...>
```

And change lib type of `adder_interface` crate:

```toml title="./contracts/adder_interface/Cargo.toml"
# <...>
[lib]
crate-type = ["rlib"]
# <...>
```

```rust title="./contracts/adder_interface/src/lib.rs"
#![no_std]

use soroban_sdk::contractclient;

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

```

```rust title="contracts/add_contract/src/lib.rs"
#![no_std]
use soroban_sdk::{contract, contractimpl};
use adder_interface::Adder;

#[contract]
pub struct ContractAdd;

#[contractimpl]
impl Adder for ContractAdd {
fn add(x: u32, y: u32) -> u32 {
x.checked_add(y).expect("no overflow")
}
}

```

```rust title="contracts/main_contract/src/lib.rs"
#![no_std]

use soroban_sdk::{contract, contractimpl, Address, Env};
use adder_interface::AdderClient;

#[contract]
pub struct ContractMain;

#[contractimpl]
impl ContractMain {
pub fn add_with(env: Env, contract: Address, x: u32, y: u32) -> u32 {
let client = AdderClient::new(&env, &contract);
client.add(&x, &y)
}
}

mod test;
```

And update test imports:

```rust title="contracts/main_contract/src/test.rs"
#![cfg(test)]

use crate::{ContractMain, ContractMainClient};
use soroban_sdk::Env;

mod contract_add {
soroban_sdk::contractimport!(
file = "../../target/wasm32-unknown-unknown/release/my_contract.wasm"
);
}
// <...>
```

As the final step we can create an alternative `Adder` implementation that adds an extra 1:

```rust title="contracts/add_extra_contract/src/lib.rs"
#![no_std]
use soroban_sdk::{contract, contractimpl};
use adder_interface::Adder;

#[contract]
pub struct ContractAdd;

#[contractimpl]
impl Adder for ContractAdd {
fn add(x: u32, y: u32) -> u32 {
x.checked_add(y).expect("no overflow").checked_add(1).expect("no overflow")
}
}
```

We can now deploy this contracts and test the new behavior:

```bash
stellar contract build
stellar contract deploy --network testnet --wasm target/wasm32-unknown-unknown/release/add_contract.wasm --alias add_contract
stellar contract deploy --network testnet --wasm target/wasm32-unknown-unknown/release/add_extra_contract.wasm --alias wrong_math_contract
stellar contract deploy --network testnet --wasm target/wasm32-unknown-unknown/release/main_contract.wasm --alias main_contract
```

Now let's try to do sum 2 unsigned integers causing an overflow:

```
$ stellar contract invoke --id main_contract --network testnet -- add_with --contract add_contract --x 2 --y 2
ℹ️ Send skipped because simulation identified as read-only. Send by rerunning with `--send=yes`.
4
$ stellar contract invoke --id main_contract --network testnet -- add_with --contract wrong_math_contract --x 2 --y 2
ℹ️ Send skipped because simulation identified as read-only. Send by rerunning with `--send=yes`.
5
```
Loading