From ec856b0bc4618bf04087b20292b6cc968329ebb7 Mon Sep 17 00:00:00 2001 From: Ifropc Date: Fri, 29 Nov 2024 14:47:10 -0800 Subject: [PATCH 1/5] 679: add tutorial working with workspaces --- docs/build/guides/conventions/workspace.mdx | 203 ++++++++++++++++++++ docs/build/guides/testing/unit-tests.mdx | 2 + 2 files changed, 205 insertions(+) create mode 100644 docs/build/guides/conventions/workspace.mdx diff --git a/docs/build/guides/conventions/workspace.mdx b/docs/build/guides/conventions/workspace.mdx new file mode 100644 index 000000000..5e0f06da9 --- /dev/null +++ b/docs/build/guides/conventions/workspace.mdx @@ -0,0 +1,203 @@ +--- +title: Organize contracts with workspace +hide_table_of_contents: true +description: Organize your 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: + +```bash +mkdir soroban-project +cd soroban-project +PROJECT_ROOT=. +stellar contract init . --name my-contract +``` + +Running this command will create a root project directory (`soroban-project`) and then initialize a project workspace with a single contract named `my-contract`. + +Adding one more contract template to the project can be done using the same command: + +```bash +stellar contract init . --name second-contract +``` + +The project tree will look like this: + +``` +$ tree +. +├── Cargo.toml +├── contracts +│   ├── my-contract +│   │   ├── Cargo.toml +│   │   ├── Makefile +│   │   └── src +│   │   ├── lib.rs +│   │   └── test.rs +│   └── second-contract +│   ├── Cargo.toml +│   ├── Makefile +│   └── src +│   ├── lib.rs +│   └── test.rs +└── README.md +``` + +Running `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 project's contracts with + +```bash +stellar contract build +``` + +command and check the build directory: + +```bash +$ ls target/wasm32-unknown-unknown/release/ | grep wasm +my_contract.wasm +second_contract.wasm +``` + +### 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 +// contracts/my-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: + +```bash +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/second-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: + +```bash +../../../target/wasm32-unknown-unknown/release/my_contract.wasm +``` + +It can now be used in the rust code: + +```rust +// contracts/second-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/my_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 +// contracts/second-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: + +```bash +cd $PROJECT_ROOT +stellar contract build +``` + +And check that the test is working correctly: + +```bash +$ cd contracts/second-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 +ADD_CONTRACT=`stellar contract deploy --network testnet --wasm target/wasm32-unknown-unknown/release/my_contract.wasm` +MAIN_CONTRACT=`stellar contract deploy --network testnet --wasm target/wasm32-unknown-unknown/release/second_contract.wasm` +``` + +And finally call the main contract: + +```bash +$ 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 +``` diff --git a/docs/build/guides/testing/unit-tests.mdx b/docs/build/guides/testing/unit-tests.mdx index dc70ccfed..87b7e902d 100644 --- a/docs/build/guides/testing/unit-tests.mdx +++ b/docs/build/guides/testing/unit-tests.mdx @@ -25,6 +25,7 @@ fn test() { assert_eq!(client.increment(), 3); } ``` + Ref: https://github.com/stellar/soroban-examples/blob/main/increment/src/test.rs :::tip @@ -42,6 +43,7 @@ The Env created at the beginning of the test is not a simulation of the Soroban It's a simple test, but it's a complete test. There's a full environment setup, used, and torn down in the test, and it happens fast. The Rust test harness runs all the tests for a contract in parallel and each will have its own isolated contract environment. Most tests, even integration tests and fuzz tests, will look very similar to this unit test. They'll do four things: + 1. Create an environment, the `Env`. 2. Register the contract(s) to be tested. 3. Invoke functions using a client. From 60172b4065f034280d6c86cfd0c8927a2464fdbf Mon Sep 17 00:00:00 2001 From: Ifropc Date: Fri, 29 Nov 2024 15:57:47 -0800 Subject: [PATCH 2/5] Expand tutorial for interfaces --- docs/build/guides/conventions/workspace.mdx | 212 ++++++++++++++++++-- 1 file changed, 196 insertions(+), 16 deletions(-) diff --git a/docs/build/guides/conventions/workspace.mdx b/docs/build/guides/conventions/workspace.mdx index 5e0f06da9..3c24ff3af 100644 --- a/docs/build/guides/conventions/workspace.mdx +++ b/docs/build/guides/conventions/workspace.mdx @@ -14,15 +14,15 @@ It's very simple to get started using the cli: mkdir soroban-project cd soroban-project PROJECT_ROOT=. -stellar contract init . --name my-contract +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 `my-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: ```bash -stellar contract init . --name second-contract +stellar contract init . --name main_contract ``` The project tree will look like this: @@ -32,13 +32,13 @@ $ tree . ├── Cargo.toml ├── contracts -│   ├── my-contract +│   ├── add-contract │   │   ├── Cargo.toml │   │   ├── Makefile │   │   └── src │   │   ├── lib.rs │   │   └── test.rs -│   └── second-contract +│   └── main-contract │   ├── Cargo.toml │   ├── Makefile │   └── src @@ -63,8 +63,8 @@ command and check the build directory: ```bash $ ls target/wasm32-unknown-unknown/release/ | grep wasm -my_contract.wasm -second_contract.wasm +add_contract.wasm +main_contract.wasm ``` ### Cross-contract call between workspaces @@ -72,7 +72,7 @@ second_contract.wasm 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 -// contracts/my-contract/src/lib.rs +// contracts/add_contract/src/lib.rs #![no_std] use soroban_sdk::{contract, contractimpl}; @@ -99,23 +99,23 @@ 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/second-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: +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: ```bash -../../../target/wasm32-unknown-unknown/release/my_contract.wasm +../../../target/wasm32-unknown-unknown/release/add_contract.wasm ``` It can now be used in the rust code: ```rust -// contracts/second-contract/src/lib.rs +// 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/my_contract.wasm" + file = "../../../target/wasm32-unknown-unknown/release/add_contract.wasm" ); } @@ -136,7 +136,7 @@ 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 -// contracts/second-contract/src/test.rs +// contracts/main_contract/src/test.rs #![cfg(test)] use crate::{contract_add, ContractMain, ContractMainClient}; @@ -171,7 +171,7 @@ stellar contract build And check that the test is working correctly: ```bash -$ cd contracts/second-contract +$ cd contracts/main_contract $ cargo test running 1 test @@ -190,8 +190,8 @@ STELLAR_SOURCE_ACCOUNT=alice Second is to deploy the contracts: ```bash -ADD_CONTRACT=`stellar contract deploy --network testnet --wasm target/wasm32-unknown-unknown/release/my_contract.wasm` -MAIN_CONTRACT=`stellar contract deploy --network testnet --wasm target/wasm32-unknown-unknown/release/second_contract.wasm` +ADD_CONTRACT=`stellar contract deploy --network testnet --wasm target/wasm32-unknown-unknown/release/add_contract.wasm` +MAIN_CONTRACT=`stellar contract deploy --network testnet --wasm target/wasm32-unknown-unknown/release/main_contract.wasm` ``` And finally call the main contract: @@ -201,3 +201,183 @@ $ stellar contract invoke --id $MAIN_CONTRACT --network testnet -- add_with --co ℹ️ 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. + +```bash +cd $PROJECT_ROOT +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 +// 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 +# $PROJECT_PATH/Cargo.toml +# <...> +[workspace.dependencies] +soroban-sdk = "21.0.0" +adder-interface = { path = "contracts/adder_interface" } +# <...> +``` + +```toml +# $PROJECT_PATH/contracts/add_contract/Cargo.toml +# <...> +[dependencies] +soroban-sdk = { workspace = true } +adder-interface = {workspace = true} +# <...> +``` + +```toml +# $PROJECT_PATH/contracts/main_contract/Cargo.toml +# <...> +[dependencies] +soroban-sdk = { workspace = true } +adder-interface = {workspace = true} +# <...> +``` + +```toml +# $PROJECT_PATH/contracts/add_extra_contract/Cargo.toml +# <...> +[dependencies] +soroban-sdk = { workspace = true } +adder-interface = {workspace = true} +# <...> +``` + +And change lib type of `adder_interface` crate: + +```toml +# $PROJECT_PATH/contracts/adder_interface/Cargo.toml +# <...> +[lib] +crate-type = ["rlib"] +# <...> +``` + +```rust +# $PROJECT_PATH/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 +// 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 +// 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 +// 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 +// 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 +ADD_CONTRACT=`stellar contract deploy --network testnet --wasm target/wasm32-unknown-unknown/release/add_contract.wasm` +WRONG_MATH_CONTRACT=`stellar contract deploy --network testnet --wasm target/wasm32-unknown-unknown/release/add_extra_contract.wasm` +MAIN_CONTRACT=`stellar contract deploy --network testnet --wasm target/wasm32-unknown-unknown/release/main_contract.wasm` +``` + +Now let's try to do sum 2 unsigned integers causing an overflow: + +```bash +$ 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 +``` From 39b88efdab43f3430720eb73f16e2f400184ff93 Mon Sep 17 00:00:00 2001 From: Ifropc Date: Mon, 2 Dec 2024 17:25:18 -0800 Subject: [PATCH 3/5] Address PR comments --- docs/build/guides/conventions/workspace.mdx | 115 +++++++++----------- 1 file changed, 51 insertions(+), 64 deletions(-) diff --git a/docs/build/guides/conventions/workspace.mdx b/docs/build/guides/conventions/workspace.mdx index 3c24ff3af..8b4110bdd 100644 --- a/docs/build/guides/conventions/workspace.mdx +++ b/docs/build/guides/conventions/workspace.mdx @@ -1,7 +1,7 @@ --- -title: Organize contracts with workspace +title: Workspaces hide_table_of_contents: true -description: Organize your contracts using Cargo workspaces +description: Organize contracts using Cargo workspaces --- ### Initializing workspace project @@ -10,10 +10,7 @@ Using Cargo's workspace [feature](https://doc.rust-lang.org/book/ch14-03-cargo-w It's very simple to get started using the cli: -```bash -mkdir soroban-project -cd soroban-project -PROJECT_ROOT=. +``` stellar contract init . --name add_contract ``` @@ -21,7 +18,7 @@ Running this command will create a root project directory (`soroban-project`) an Adding one more contract template to the project can be done using the same command: -```bash +``` stellar contract init . --name main_contract ``` @@ -47,32 +44,25 @@ $ tree └── README.md ``` -Running `contract init` command created a sample contracts subdirectories (located in `./contracts), each contains: +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 project's contracts with +We can now build our workspace's contracts with -```bash +``` stellar contract build ``` -command and check the build directory: - -```bash -$ ls target/wasm32-unknown-unknown/release/ | grep wasm -add_contract.wasm -main_contract.wasm -``` +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 -// contracts/add_contract/src/lib.rs +```rust title="contracts/add_contract/src/lib.rs" #![no_std] use soroban_sdk::{contract, contractimpl}; @@ -89,7 +79,7 @@ impl ContractAdd { 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: -```bash +``` stellar contract build ``` @@ -101,14 +91,13 @@ $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: -```bash +``` ../../../target/wasm32-unknown-unknown/release/add_contract.wasm ``` It can now be used in the rust code: -```rust -// contracts/main_contract/src/lib.rs +```rust title="contracts/main_contract/src/lib.rs" #![no_std] use soroban_sdk::{contract, contractimpl, Address, Env}; @@ -135,8 +124,7 @@ 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 -// contracts/main_contract/src/test.rs +```rust title="contracts/main_contract/src/test.rs" #![cfg(test)] use crate::{contract_add, ContractMain, ContractMainClient}; @@ -161,18 +149,16 @@ fn test_adding_cross_contract() { } ``` -Contracts can now be re-complied: +Contracts can now be re-complied running the following command from the project root: -```bash -cd $PROJECT_ROOT +``` stellar contract build ``` -And check that the test is working correctly: +And check that the test is working correctly, running tests in `contracts/main_contract`: -```bash -$ cd contracts/main_contract -$ cargo test +``` +cargo test running 1 test test test::test_adding_cross_contract ... ok @@ -194,10 +180,23 @@ ADD_CONTRACT=`stellar contract deploy --network testnet --wasm target/wasm32-unk MAIN_CONTRACT=`stellar contract deploy --network testnet --wasm target/wasm32-unknown-unknown/release/main_contract.wasm` ``` -And finally call the main contract: +This contracts can also be referenced using aliases after running following command: ```bash -$ stellar contract invoke --id $MAIN_CONTRACT --network testnet -- add_with --contract $ADD_CONTRACT --x 9 --y 10 +stellar contract alias add --id $ADD_CONTRACT add_contract +stellar contract alias add --id MAIN_CONTRACT main_contract +``` + +And finally call the main contract: + +:::info + +Referencing contract arguments by aliases is not supported in the latest stable version yet, but it's still possible to use a corresponding environmental variable instead + +::: + +``` +$ 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 ``` @@ -206,16 +205,14 @@ $ stellar contract invoke --id $MAIN_CONTRACT --network testnet -- add_with --co 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. -```bash -cd $PROJECT_ROOT +``` 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 -// contracts/adder_interface/src/lib.rs +```rust title="contracts/adder_interface/src/lib.rs" #![no_std] use soroban_sdk::contractclient; @@ -228,8 +225,7 @@ pub trait ContractAInterface { To use the interface definition our workspace members will now have an `adder_interface` as a dependency: -```toml -# $PROJECT_PATH/Cargo.toml +```toml title="./Cargo.toml" # <...> [workspace.dependencies] soroban-sdk = "21.0.0" @@ -237,8 +233,7 @@ adder-interface = { path = "contracts/adder_interface" } # <...> ``` -```toml -# $PROJECT_PATH/contracts/add_contract/Cargo.toml +```toml title="./contracts/add_contract/Cargo.toml" # <...> [dependencies] soroban-sdk = { workspace = true } @@ -246,8 +241,7 @@ adder-interface = {workspace = true} # <...> ``` -```toml -# $PROJECT_PATH/contracts/main_contract/Cargo.toml +```toml title="./contracts/main_contract/Cargo.toml" # <...> [dependencies] soroban-sdk = { workspace = true } @@ -255,8 +249,7 @@ adder-interface = {workspace = true} # <...> ``` -```toml -# $PROJECT_PATH/contracts/add_extra_contract/Cargo.toml +```toml title="./contracts/add_extra_contract/Cargo.toml" # <...> [dependencies] soroban-sdk = { workspace = true } @@ -266,16 +259,14 @@ adder-interface = {workspace = true} And change lib type of `adder_interface` crate: -```toml -# $PROJECT_PATH/contracts/adder_interface/Cargo.toml +```toml title="./contracts/adder_interface/Cargo.toml" # <...> [lib] crate-type = ["rlib"] # <...> ``` -```rust -# $PROJECT_PATH/contracts/adder_interface/src/lib.rs +```rust title="./contracts/adder_interface/src/lib.rs" #![no_std] use soroban_sdk::contractclient; @@ -287,8 +278,7 @@ pub trait Adder { ``` -```rust -// contracts/add_contract/src/lib.rs +```rust title="contracts/add_contract/src/lib.rs" #![no_std] use soroban_sdk::{contract, contractimpl}; use adder_interface::Adder; @@ -305,8 +295,7 @@ impl Adder for ContractAdd { ``` -```rust -// contracts/main_contract/src/lib.rs +```rust title="contracts/main_contract/src/lib.rs" #![no_std] use soroban_sdk::{contract, contractimpl, Address, Env}; @@ -328,8 +317,7 @@ mod test; And update test imports: -```rust -// contracts/main_contract/src/test.rs +```rust title="contracts/main_contract/src/test.rs" #![cfg(test)] use crate::{ContractMain, ContractMainClient}; @@ -345,8 +333,7 @@ mod contract_add { As the final step we can create an alternative `Adder` implementation that adds an extra 1: -```rust -// contracts/add_extra_contract/src/lib.rs +```rust title="contracts/add_extra_contract/src/lib.rs" #![no_std] use soroban_sdk::{contract, contractimpl}; use adder_interface::Adder; @@ -366,18 +353,18 @@ We can now deploy this contracts and test the new behavior: ```bash stellar contract build -ADD_CONTRACT=`stellar contract deploy --network testnet --wasm target/wasm32-unknown-unknown/release/add_contract.wasm` -WRONG_MATH_CONTRACT=`stellar contract deploy --network testnet --wasm target/wasm32-unknown-unknown/release/add_extra_contract.wasm` -MAIN_CONTRACT=`stellar contract deploy --network testnet --wasm target/wasm32-unknown-unknown/release/main_contract.wasm` +stellar contract alias add --id `stellar contract deploy --network testnet --wasm target/wasm32-unknown-unknown/release/add_contract.wasm` add_contract +stellar contract alias add --id `stellar contract deploy --network testnet --wasm target/wasm32-unknown-unknown/release/add_extra_contract.wasm` wrong_math_contract +stellar contract alias add --id `stellar contract deploy --network testnet --wasm target/wasm32-unknown-unknown/release/main_contract.wasm` main_contract ``` Now let's try to do sum 2 unsigned integers causing an overflow: -```bash -$ stellar contract invoke --id $MAIN_CONTRACT --network testnet -- add_with --contract $ADD_CONTRACT --x 2 --y 2 +``` +$ 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 +$ 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 ``` From 614f6ad7361b7281d5eab6ef984dbac5a40b98c0 Mon Sep 17 00:00:00 2001 From: Ifropc Date: Fri, 20 Dec 2024 11:04:13 -0800 Subject: [PATCH 4/5] Update aliases --- docs/build/guides/conventions/workspace.mdx | 23 +++++---------------- 1 file changed, 5 insertions(+), 18 deletions(-) diff --git a/docs/build/guides/conventions/workspace.mdx b/docs/build/guides/conventions/workspace.mdx index 8b4110bdd..764c006da 100644 --- a/docs/build/guides/conventions/workspace.mdx +++ b/docs/build/guides/conventions/workspace.mdx @@ -176,25 +176,12 @@ STELLAR_SOURCE_ACCOUNT=alice Second is to deploy the contracts: ```bash -ADD_CONTRACT=`stellar contract deploy --network testnet --wasm target/wasm32-unknown-unknown/release/add_contract.wasm` -MAIN_CONTRACT=`stellar contract deploy --network testnet --wasm target/wasm32-unknown-unknown/release/main_contract.wasm` -``` - -This contracts can also be referenced using aliases after running following command: - -```bash -stellar contract alias add --id $ADD_CONTRACT add_contract -stellar contract alias add --id MAIN_CONTRACT main_contract +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: -:::info - -Referencing contract arguments by aliases is not supported in the latest stable version yet, but it's still possible to use a corresponding environmental variable instead - -::: - ``` $ 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`. @@ -353,9 +340,9 @@ We can now deploy this contracts and test the new behavior: ```bash stellar contract build -stellar contract alias add --id `stellar contract deploy --network testnet --wasm target/wasm32-unknown-unknown/release/add_contract.wasm` add_contract -stellar contract alias add --id `stellar contract deploy --network testnet --wasm target/wasm32-unknown-unknown/release/add_extra_contract.wasm` wrong_math_contract -stellar contract alias add --id `stellar contract deploy --network testnet --wasm target/wasm32-unknown-unknown/release/main_contract.wasm` main_contract +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: From 780d6702b6b219da39ca337c866207af9d0fe5ce Mon Sep 17 00:00:00 2001 From: Gleb Date: Wed, 19 Feb 2025 19:03:30 -0800 Subject: [PATCH 5/5] Resolve comments --- docs/build/guides/conventions/workspace.mdx | 84 +++++++-------------- 1 file changed, 29 insertions(+), 55 deletions(-) diff --git a/docs/build/guides/conventions/workspace.mdx b/docs/build/guides/conventions/workspace.mdx index 764c006da..a5630e423 100644 --- a/docs/build/guides/conventions/workspace.mdx +++ b/docs/build/guides/conventions/workspace.mdx @@ -4,14 +4,14 @@ hide_table_of_contents: true description: Organize contracts using Cargo workspaces --- -### Initializing workspace project +# Workspace 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 +stellar contract init soroban-project --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`. @@ -19,13 +19,12 @@ Running this command will create a root project directory (`soroban-project`) an Adding one more contract template to the project can be done using the same command: ``` -stellar contract init . --name main_contract +stellar contract init soroban-project --name main_contract ``` -The project tree will look like this: +The project tree in the `soroban-project` directory will look like this: ``` -$ tree . ├── Cargo.toml ├── contracts @@ -44,21 +43,18 @@ $ tree └── README.md ``` -Running `stellar contract init` command created a sample contracts subdirectories (located in `./contracts), each contains: +Running `stellar contract init` command created a new contract, located in `./contracts`, each containing: -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. +- Cargo.toml file with the `soroban-sdk` dependency +- `src` directory with an example hello-world contract and a test. -We can now build our workspace's contracts with +Build the contracts with the following command (don't forget to change working directory to `soroban-project` first before running it), and check the build directory `target/wasm32-unknown-unknown/release/` for the compiled `.wasm` files. ``` stellar contract build ``` -command and check the build directory `target/wasm32-unknown-unknown/release/` - -### Cross-contract call between workspaces +### Integrating Contracts in the Same Workspace With the given project structure, cross-contract calls can be easily made. Starting with modifying the sample hello world contract into an add contract: @@ -77,44 +73,36 @@ impl ContractAdd { } ``` -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: +:::tip -``` -stellar contract build -``` +In this tutorial we use workspaces to import contract client. However, it's also possible to use contract's compiled code instead (for example, if you don't have a source code for it). See [making cross-contract calls](/docs/build/guides/conventions/cross-contract.mdx) guide for more info -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: +Next, in order to call `ContractAdd` from another contract, it's necessary to add a workspace dependency: -``` -../../../target/wasm32-unknown-unknown/release/add_contract.wasm +```toml title="./contracts/main_contract/Cargo.toml" +# <...> +[dependencies] +soroban-sdk = { workspace = true } +add_contract = { path = "../add_contract" } +# <...> ``` -It can now be used in the rust code: +The `ContractAdd` can now be referenced and used from another contracts using `ContractAddClient`: ```rust title="contracts/main_contract/src/lib.rs" #![no_std] - +use add_contract::ContractAddClient; 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); + let client = ContractAddClient::new(&env, &contract); client.add(&x, &y) } } @@ -127,18 +115,19 @@ Here, main contract will invoke `ContractAdd`'s `add` function to calculate the ```rust title="contracts/main_contract/src/test.rs" #![cfg(test)] -use crate::{contract_add, ContractMain, ContractMainClient}; +use crate::{ContractMain, ContractMainClient}; use soroban_sdk::Env; +use add_contract::ContractAdd; #[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 add contract using the imported contract. + let contract_add_id = env.register(ContractAdd, ()); // Register main contract defined in this crate. - let contract_main_id = env.register_contract(None, ContractMain); + let contract_main_id = env.register(ContractMain, ()); // Create a client for calling main contract. let client = ContractMainClient::new(&env, &contract_main_id); @@ -170,7 +159,7 @@ Finally, let's deploy this contracts and call our main contract using cli. If yo ```bash stellar keys generate alice --fund --network testnet -STELLAR_SOURCE_ACCOUNT=alice +stellar keys use alice ``` Second is to deploy the contracts: @@ -233,6 +222,7 @@ adder-interface = {workspace = true} [dependencies] soroban-sdk = { workspace = true } adder-interface = {workspace = true} +add_contract = { path = "../add_contract" } # <...> ``` @@ -302,22 +292,6 @@ impl ContractMain { 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"