Skip to content

Commit 36f58da

Browse files
Merge pull request #1164 from multiversx/development
Development
2 parents 4ec1e88 + d702acb commit 36f58da

File tree

1 file changed

+60
-125
lines changed

1 file changed

+60
-125
lines changed

docs/developers/tutorials/interactors-guide.md

Lines changed: 60 additions & 125 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ title: Deploy a SC in 5 minutes - SpaceCraft interactors
55

66
[comment]: # (mx-abstract)
77

8-
This short guide demonstrates how to deploy and interact with a smart contract on MultiversX using the SpaceCraft interactors. We will cover essential topics such as the SC framework, integration tests, sc-meta, and interactors (devtools).
8+
This short guide demonstrates how to deploy and interact with a smart contract (SC) on MultiversX using the SpaceCraft interactors. We will cover essential topics such as the SC framework, integration tests, `sc-meta`, and interactors (devtools).
99

1010
[comment]: # (mx-context-auto)
1111

@@ -14,23 +14,24 @@ This short guide demonstrates how to deploy and interact with a smart contract o
1414
Building smart contracts involves complex tasks. Beyond the syntax, a smart contract acts as a public server where users pay for actions. Enforcing rules to ensure safety (treating possible exploits) and efficiency (timewise - transaction speed, costwise - gas fees) for all users interacting with the contract is crucial.
1515

1616
In order to make sure that the smart contract works as expected, there are at least three stages of testing that we recommend to be performed before live deployment:
17-
- unit testing ([SpaceCraft testing framework](https://docs.multiversx.com/developers/testing/rust/sc-test-overview#overview) - Rust unit tests, RustVM)
18-
- scenarios ([mandos](https://docs.multiversx.com/developers/testing/scenario/concept#what-is-mandos) - json files, can be generated from Rust unit tests). Mandos can be used to test the logic on the [GoVM](https://docs.multiversx.com/technology/the-wasm-vm) as well, which is the actual VM running on the node
19-
- integration testing ([SpaceCraft Rust interactors](https://docs.multiversx.com/developers/interactor/interactors-overview/#overview) - testing on the blockchain). Integration tests cover real life scenarios across the different MultiversX blockchain environments - devnet/testnet/mainnet
17+
18+
- unit testing ([SpaceCraft testing framework](/docs/developers/testing/rust/sc-test-overview.md#overview) - Rust unit tests, RustVM)
19+
- scenarios ([mandos](/docs/developers/testing/scenario/concept.md#what-is-mandos) - json files, can be generated from Rust unit tests). Mandos can be used to test the logic on the [GoVM](/docs/learn/space-vm.md) as well, which is the actual VM running on the node
20+
- integration testing ([SpaceCraft Rust interactors](/docs/developers/meta/interactor/interactors-overview.md#overview) - testing on the blockchain). Integration tests cover real life scenarios across the different MultiversX blockchain environments - devnet/testnet/mainnet
2021

2122
In this tutorial we will focus on integration testing using the interactors made available by the SpaceCraft smart contract framework.
2223

2324
::::important Prerequisites
2425

25-
- `stable` Rust version `1.78.0 or above` (install via [rustup](/docs/developers/toolchain-setup.md#installing-rust-and-sc-meta)):
26-
- `multiversx-sc-meta` version `0.50.0 or above` (cargo install [multiversx-sc-meta](/docs/developers/meta/sc-meta-cli.md))
26+
- `stable` Rust version `≥1.83.0` (install via [rustup](/docs/developers/toolchain-setup.md#installing-rust-and-sc-meta)):
27+
- `multiversx-sc-meta` (cargo install [multiversx-sc-meta](/docs/developers/meta/sc-meta-cli.md))
2728
::::
2829

2930
[comment]: # (mx-context-auto)
3031

3132
## Step 1: Start from a template
3233

33-
Get a headstart by using sc-meta to generate one of our smart contract templates as a starting point for your smart contract. Let’s say we start from the `empty` template contract and name it `my-contract`.
34+
Get a headstart by using `sc-meta` to generate one of our smart contract templates as a starting point for your smart contract. Let’s say we start from the `empty` template contract and name it `my-contract`.
3435

3536
```bash
3637
sc-meta new --template empty --name my-contract
@@ -111,21 +112,22 @@ sc-meta all snippets
111112

112113
This command compiled the contract and generated a new folder called `interactor`. The interactor is by default a Rust CLI program that uses the smart contract proxy to send calls to the contract.
113114

114-
Inside the source folder *(interactor/src)*, we should find the newly generated proxy of the contract *(proxy.rs)* and the `interactor_main.rs` file, which is the main file of the project. A *sc-config.toml* file has also been created (if not existent) containing the path of the proxy file.
115+
Inside the source folder *(my-contract/interactor/src)*, we should find the newly generated proxy of the contract *(my_contract_proxy.rs)* and the `interactor_main.rs` file, which is the main file of the project. A *sc-config.toml* file has also been created (if not existent) in the contract root containing the path of the proxy file.
115116

116-
If we navigate to *interactor/src/interactor_main.rs*, inside the `main` function, we can find all the CLI command available to us:
117+
If we navigate to *interactor/src/interact.rs*, inside the `my_contract_cli()` function, we can find all the CLI command available to us:
117118

118-
```rust title=interactor_main.rs
119-
#[tokio::main]
120-
async fn main() {
119+
```rust title=interact.rs
120+
pub async fn my_contract_cli() {
121121
env_logger::init();
122122

123123
let mut args = std::env::args();
124124
let _ = args.next();
125125
let cmd = args.next().expect("at least one argument required");
126-
let mut interact = ContractInteract::new().await;
126+
let config = Config::new();
127+
let mut interact = ContractInteract::new(config).await;
127128
match cmd.as_str() {
128129
"deploy" => interact.deploy().await,
130+
"upgrade" => interact.upgrade().await,
129131
"register_me" => interact.register_me().await,
130132
"deregister_me" => interact.deregister_me().await,
131133
"already_registered" => interact.already_registered().await,
@@ -136,7 +138,7 @@ async fn main() {
136138

137139
As you can see, `sc-meta` automatically generated all the logic behind calling the smart contract endpoints. The interactor uses asynchronous Rust, so all the functions are marked as `async` and need to be awaited to get a result.
138140

139-
In order to compile the project, we need to include it in the project tree. In this case, we have to add the interactor project to the smart contract’s workspaces, in the *Cargo.toml* file:
141+
In order to compile the project, we need to include it in the project tree. In this case, we have to add the interactor project to the smart contract’s workspaces, in the *my-contract/Cargo.toml* file:
140142

141143
```toml title=Cargo.toml
142144
[workspace]
@@ -151,7 +153,7 @@ members = [
151153

152154
## Step 5: Create scenarios & run
153155

154-
Now the setup is complete, it’s time to create some scenarios to test. For our use-case, the perfect scenario is: deploy the contract, register from a user and deregister from the same user. Anything else should result in an error.
156+
Now the setup is complete, it’s time to create some scenarios to test. For our use-case, the perfect scenario is: **deploy the contract**, **register from a user** and **deregister from the same user**. Anything else should result in an error.
155157

156158
In order to test the perfect scenario first, we will first deploy the contract:
157159

@@ -160,24 +162,30 @@ cd interactor
160162
cargo run deploy
161163
```
162164

163-
After deploying the contract, a new file named *state.toml* will be created, which contains the newly deployed sc address. For each deploy, a new address will be printed into the file.
165+
After deploying the contract, a new file named *state.toml* will be created in the `interactor` directory, which contains the newly deployed SC address. For each deploy, a new address will be printed into the file.
164166

165167
```toml title=state.toml
166168
contract_address = "erd1qqqqqqqqqqqqqpgqpsev0x4nufh240l44gf2t6qzkh9xvutqd8ssrnydzr"
167169
```
168170

169-
By default, the testing environment is `devnet`, specified by the `GATEWAY` constant:
171+
By default, the testing environment is `devnet`, specified by the `my-contract/interactor/config.toml`:
170172

171-
```rust title=interactor_main.rs
172-
const GATEWAY: &str = sdk::blockchain::DEVNET_GATEWAY;
173+
```toml title=config.toml
174+
chain_type = 'real'
175+
gateway_uri = 'https://devnet-gateway.multiversx.com'
173176
```
174-
Changing the value of this constant will change the testing environment for a quick setup (other options are `TESTNET_GATEWAY` and `MAINNET_GATEWAY`).
177+
178+
Changing the value of the `gateway_uri` will change the testing environment for a quick setup. Other options are:
179+
180+
- Testnet: `https://testnet-gateway.multiversx.com`
181+
- Mainnet: `https://gateway.multiversx.com`
175182

176183
Each command has some waiting time and returns the result inside a variable in the function, but also prints it in the console for easy tracking.
177184

178185
In this case, the console shows:
186+
179187
```bash
180-
you@PC interactor % cargo run deploy
188+
interactor % cargo run deploy
181189
Compiling rust-interact v0.0.0 (/Users/you/Documents/my-contract/interact-rs)
182190
Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.96s
183191
Running `/Users/you/Documents/my-contract/target/debug/rust-interact deploy`
@@ -197,15 +205,18 @@ cargo run deregister_me
197205
198206
These commands will send two transactions to the newly deployed contract from the `test_wallets::alice()` wallet, each of them calling one endpoint of the contract in the specified order.
199207
200-
```bash
201-
you@PC interactor % cargo run register_me
208+
```bash title=register_me
209+
interactor % cargo run register_me
202210
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.35s
203211
Running `/Users/you/Documents/my-contract/target/debug/rust-interact register_me`
204212
sender's recalled nonce: 1718
205213
-- tx nonce: 1718
206214
sc call tx hash: 97bea2b18ca0d1305200dc4ea0d1b2b32a666430f8d24ab042f59c324bf47eec
207215
Result: ()
208-
you@PC interactor % cargo run deregister_me
216+
```
217+
218+
```bash title=deregister_me
219+
interactor % cargo run deregister_me
209220
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.11s
210221
Running `/Users/you/Documents/my-contract/target/debug/rust-interact deregister_me
211222
sender's recalled nonce: 1719
@@ -221,18 +232,37 @@ Result: ()
221232
Using the functions generated by `sc-meta`, we can extend the interactor to cover a series of integration tests. Organized tests help with maintenance and long-term testing.
222233
223234
We can create a quick integration test as such:
224-
```rust title=interactor_main.rs
235+
236+
Inside the `tests` directory of the `interactor`, you can add integration tests that run either on the **chain simulator** or on the **gateway** configured in `config.toml`.
237+
238+
For this example, we can create an integration test that runs on `devnet`, starting with the template provided in the file `my-contract/interactor/interact_tests.rs`:
239+
240+
```rust title=interact_tests.rs
225241
#[tokio::test]
226242
async fn integration_test() {
227-
let mut interact = ContractInteract::new().await;
243+
let mut interactor = ContractInteract::new(Config::new()).await;
228244
229-
interact.deploy().await;
230-
interact.register_me().await;
231-
interact.deregister_me().await;
245+
interactor.deploy().await;
246+
interactor.register_me().await;
247+
interactor.deregister_me().await;
232248
}
233249
```
234250
235-
Running this test will perform the previous CLI actions in the same order, on the real blockchain. The console will show all the intermediate actions at the end of the test, as such:
251+
Alternatively, we can create an integration test that runs only on the [chain simulator](/docs/developers/tutorials/chain-simulator-adder.md):
252+
253+
```rust
254+
#[tokio::test]
255+
#[cfg_attr(not(feature = "chain-simulator-tests"), ignore)]
256+
async fn chain_simulator_integration_test() {
257+
let mut interactor = ContractInteract::new(Config::chain_simulator_config()).await;
258+
259+
interactor.deploy().await;
260+
interactor.register_me().await;
261+
interactor.deregister_me().await;
262+
}
263+
```
264+
265+
Running `integration_test()` will perform the previous CLI actions in the same order, on the real blockchain. The console will show all the intermediate actions at the end of the test, as such:
236266
237267
```bash
238268
running 1 test
@@ -264,101 +294,6 @@ test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; fini
264294

265295
[comment]: # (mx-context-auto)
266296

267-
## Improvements
268-
269-
This setup can be used for extensive testing, but also as a tool for live deployment on mainnet, tracking and interaction. In a multi-contract setup, one can, for example, create different modules for specific interactions with each contract and development environment and further structure all the interactions into different integration tests.
270-
271-
Let’s take the example of the [DEX smart contract interactors](https://github.com/multiversx/mx-exchange-sc/tree/feat/unified/dex/interactor). Here, all the proxy files are organized in a different crate for easier access.
272-
273-
![img](/img/dex_interactor_file_structure.jpeg)
274-
275-
Furthermore, all the contracts that are part of the DEX flow have separate interaction modules, so we can easily keep track of the flow when writing complex tests.
276-
277-
This is the `energy_factory` file, containing only interactions with the `energy factory smart contract`, using the specific proxy and contract address:
278-
```rust title=energy_factory.rs
279-
use multiversx_sc_snippets::imports::*;
280-
use proxies::energy_factory_proxy;
281-
282-
use crate::{
283-
structs::{to_rust_biguint, InteractorEnergy},
284-
DexInteract,
285-
};
286-
287-
pub(crate) async fn get_energy_amount_for_user(
288-
dex_interact: &mut DexInteract,
289-
user: Address,
290-
) -> RustBigUint {
291-
let result_token = dex_interact
292-
.interactor
293-
.query()
294-
.to(dex_interact.state.current_energy_factory_address())
295-
.typed(energy_factory_proxy::SimpleLockEnergyProxy)
296-
.get_energy_amount_for_user(ManagedAddress::from(user))
297-
.returns(ReturnsResult)
298-
.prepare_async()
299-
.run()
300-
.await;
301-
302-
to_rust_biguint(result_token)
303-
}
304-
```
305-
306-
After having implemented this structure, writing integration test is a smooth process, even though the logic gets complicated:
307-
308-
```rust title=dex_interact.rs
309-
impl DexInteract {
310-
async fn full_farm_scenario(&mut self, args: &AddArgs) {
311-
// adds liquidity to the pair SC
312-
let (_, _, lp_token) = pair::add_liquidity(self, args).await.0;
313-
// enters farm in the farm locked SC
314-
let _result = farm_locked::enter_farm(self, lp_token).await;
315-
// query the energy factory SC
316-
let _query = energy_factory::get_energy_amount_for_user(self, Address::zero()).await;
317-
// stake farm tokens in the farm staking proxy SC
318-
let _farm_token = farm_staking_proxy::stake_farm_tokens(self, Vec::new(), None).await;
319-
// more logic
320-
}
321-
}
322-
323-
#[cfg(test)]
324-
pub mod integration_tests {
325-
use multiversx_sc_snippets::tokio;
326-
327-
use crate::{dex_interact_cli::SwapArgs, pair, DexInteract};
328-
329-
#[tokio::test]
330-
async fn test_swap() {
331-
// initialize interactor
332-
let mut dex_interact = DexInteract::init().await;
333-
// test users
334-
dex_interact.register_wallets();
335-
// mock arguments
336-
let args = SwapArgs::default();
337-
338-
// swap tokens with the pair SC
339-
let result = pair::swap_tokens_fixed_input(&mut dex_interact, &args).await;
340-
println!("result {:#?}", result);
341-
}
342-
343-
#[tokio::test]
344-
async fn test_full_farm_scenario() {
345-
// initialize interactor
346-
let mut dex_interact = DexInteract::init().await;
347-
// test users
348-
dex_interact.register_wallets();
349-
// mock arguments
350-
let args = AddArgs::default();
351-
352-
// runs a full farm scenario
353-
dex_interact.full_farm_scenario(args).await;
354-
}
355-
}
356-
```
357-
358-
Organizing the code this way streamlines the process even further. Now, it is just a matter of using a different datatype or a different module in order to keep track of the various contracts and development environments and be able to rerun everything quickly if needed.
359-
360-
[comment]: # (mx-context-auto)
361-
362297
## Conclusion
363298

364299
The interactors are a versatile tool that greatly simplifies various processes around a smart contract, including deployment, upgrades, interaction, and testing. These tools not only save time but also offer a robust starting codebase for Rust developers. They also provide a valuable learning opportunity for non-Rust developers to explore asynchronous Rust and other advanced features.

0 commit comments

Comments
 (0)