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

create a page for the workspace example contract #706

Merged
merged 5 commits into from
Aug 19, 2024
Merged
Changes from all 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
276 changes: 276 additions & 0 deletions docs/build/smart-contracts/example-contracts/workspace.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,276 @@
---
title: Workspace
description: Develop multiple contracts side-by-side.
sidebar_position: 170
---

import Tabs from "@theme/Tabs";
import TabItem from "@theme/TabItem";
import { getPlatform } from "@site/src/helpers/getPlatform";

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](https://gitpod.io/button/open-in-gitpod.svg)][oigp]

[oigp]: https://gitpod.io/#https://github.com/stellar/soroban-examples/tree/main
[workspace example]: https://github.com/stellar/soroban-examples/tree/main/workspace

## Run the Example

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

[Setup]: ../getting-started/setup.mdx

```shell
git clone https://github.com/stellar/soroban-examples
```

Or, skip the development environment setup and open this example in [Gitpod][oigp].

To run the tests for the example use `cargo test`.

```shell
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:

```text
running 1 test
test test::test_token_auth ... ok
```

## Code

<Tabs>
<TabItem value="Contract A Interface" default>

```rust title="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;
}
```

</TabItem>
<TabItem value="Contract A">

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

#[contract]
pub struct ContractA;

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

</TabItem>
<TabItem value="Contract B">

```rust title="workspace/contract_b/src/lib.rs"
#![no_std]
use soroban_sdk::{contract, contractimpl, Address, Env};
use soroban_workspace_contract_a_interface::ContractAClient;

#[contract]
pub struct ContractB;

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

mod test;
```

</TabItem>
<TabItem value="Contract B Test">

```rust title="workspace/src/contract_b/src/test.rs"
#![cfg(test)]
use soroban_sdk::Env;

use crate::{ContractB, ContractBClient};

use soroban_workspace_contract_a::ContractA;

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

// Register contract A using the native contract imported.
let contract_a_id = env.register_contract(None, ContractA);

// 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);
}
```

</TabItem>
</Tabs>

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:

```shell
stellar contract build
```

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

```text
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.

<Tabs groupId="platform" defaultValue={getPlatform()}>
<TabItem value="unix" label="macOS/Linux">

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

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

</TabItem>
<TabItem value="windows" label="Windows (PowerShell)">

```powershell
stellar contract deploy `
--wasm target/wasm32-unknown-unknown/release/soroban_workspace_contract_a.wasm `
```

```powershell
stellar contract deploy `
--wasm target/wasm32-unknown-unknown/release/soroban_workspace_contract_b.wasm `
```

</TabItem>
</Tabs>

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`).

<Tabs groupId="platform" defaultValue={getPlatform()}>
<TabItem value="unix" label="macOS/Linux">

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

</TabItem>
<TabItem value="windows" label="Windows (PowerShell)">

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

</TabItem>
</Tabs>

[`stellar-cli`]: ../getting-started/setup.mdx#install-the-stellar-cli
Loading