Advance

 

Rust Testing Framework

Welcome to the tutorial on the Rust Testing Framework in MultiversX! In this tutorial, we’ll be exploring the importance of testing in smart contract development and how the Rust Testing Framework can be used to test your contracts on the MultiversX blockchain.

Smart contracts are at the core of blockchain technology, and it’s essential that they are thoroughly tested before deployment to avoid any potential issues or bugs. The Rust Testing Framework provides a comprehensive testing environment for smart contract developers, making it easier to catch bugs and ensure that contracts are functioning as intended.

In this tutorial, we’ll go through the basics of the Rust Testing Framework and how to use it within the MultiversX ecosystem. By the end of this tutorial, you will have a better understanding of the importance of testing in smart contract development and the tools available to you to ensure the success of your contract.

The Rust testing framework was developed as an alternative to manually writing scenario tests. This comes with many advantages:

  • being able to calculate values using variables

  • type checking

  • automatic serialization

  • far less verbose

  • semi-automatic generation of the scenario tests

The only disadvantage is that you need to learn something new! Jokes aside, keep in mind that this whole framework runs in a mocked environment. So while you get powerful testing and debugging tools, you are ultimately running a mock and have no guarantee that the contract will work identically with the current VM version deployed on the mainnet.

This is where the scenario generation part comes into play. The Rust testing framework allows you to generate scenarios with minimal effort, and then run said scenarios with one click through our MultiversX VSCode extension (alternatively, simply run erdpy contract test). There will be a bit of manual effort required on the developer’s part, but we’ll get to that in its specific section.

Please note that scenario generation is more of an experiment rather than a fully-fledged implementation, which we might even remove in the future. Still, some examples are provided here if you still wish to attempt it.

Prerequisites

You need to have the latest multiversx-sc version (at the time of writing this, the latest version is 0.39.0). You can check the latest version here: https://crates.io/crates/multiversx-sc

Add multiversx-sc-scenario and required packages as dev-dependencies in your Cargo.toml:

[dev-dependencies.multiversx-sc-scenario]
version = "0.39.0"

[dev-dependencies]
num-bigint = "0.4.2"
num-traits = "0.2"
hex = "0.4"

For this tutorial, we’re going to use the crowdfunding SC, so it might be handy to have it open or clone the repository:

You need a tests and a scenarios folder in your contract. Create a .rs file in your tests folder.

In your newly created test file, add the following code (adapt the crowdfunding_esdt namespace, the struct/variable names, and the contract wasm path according to your contract):

use crowdfunding_esdt::*;
use multiversx_sc::{
    sc_error,
    types::{Address, SCResult},
};
use multiversx_sc_scenario::{
    managed_address, managed_biguint, managed_token_id, rust_biguint, whitebox::*,
    DebugApi,
};

const WASM_PATH: &'static str = "crowdfunding-esdt/output/crowdfunding-esdt.wasm";

struct CrowdfundingSetup<CrowdfundingObjBuilder>
where
    CrowdfundingObjBuilder:
        'static + Copy + Fn() -> crowdfunding_esdt::ContractObj<DebugApi>,
{
    pub blockchain_wrapper: BlockchainStateWrapper,
    pub owner_address: Address,
    pub first_user_address: Address,
    pub second_user_address: Address,
    pub cf_wrapper:
        ContractObjWrapper<crowdfunding_esdt::ContractObj<DebugApi>, CrowdfundingObjBuilder>,
}

The CrowdfundingSetup struct isn’t really needed, but it helps de-duplicating some code. You may add other fields in your struct if needed, but for now this is enough for our use-case. The only fields you’ll need for any contract are blockchain_wrapper and cf_wrapper. The rest of the fields can be adapted according to your test scenario.

And that’s all you need to get started.

Writing your first test

The first test you need to write is the one simulating the deployment of your smart contract. For that, you need a user address and a contract address. Then you simply call the init function of the smart contract.

Since we’re going to be using the same token ID everywhere, let’s add it as a constant (and while we’re at it, have the deadline as a constant as well):

const CF_TOKEN_ID: &[u8] = b"CROWD-123456";
const CF_DEADLINE: u64 = 7 * 24 * 60 * 60; // 1 week in seconds

Let’s create our initial setup:

fn setup_crowdfunding<CrowdfundingObjBuilder>(
    cf_builder: CrowdfundingObjBuilder,
) -> CrowdfundingSetup<CrowdfundingObjBuilder>
where
    CrowdfundingObjBuilder: 'static + Copy + Fn() -> crowdfunding_esdt::ContractObj<DebugApi>,
{
    let rust_zero = rust_biguint!(0u64);
    let mut blockchain_wrapper = BlockchainStateWrapper::new();
    let owner_address = blockchain_wrapper.create_user_account(&rust_zero);
    let first_user_address = blockchain_wrapper.create_user_account(&rust_zero);
    let second_user_address = blockchain_wrapper.create_user_account(&rust_zero);
    let cf_wrapper = blockchain_wrapper.create_sc_account(
        &rust_zero,
        Some(&owner_address),
        cf_builder,
        WASM_PATH,
    );

    blockchain_wrapper.set_esdt_balance(&first_user_address, CF_TOKEN_ID, &rust_biguint!(1_000));
    blockchain_wrapper.set_esdt_balance(&second_user_address, CF_TOKEN_ID, &rust_biguint!(1_000));

    blockchain_wrapper
        .execute_tx(&owner_address, &cf_wrapper, &rust_zero, |sc| {
            let target = managed_biguint!(2_000);
            let token_id = managed_token_id!(CF_TOKEN_ID);

            sc.init(target, CF_DEADLINE, token_id);
        })
        .assert_ok();

    blockchain_wrapper.add_mandos_set_account(cf_wrapper.address_ref());

    CrowdfundingSetup {
        blockchain_wrapper,
        owner_address,
        first_user_address,
        second_user_address,
        cf_wrapper,
    }
}

The main object you’re going to be interacting with is the BlockchainStateWrapper. It holds the entire (mocked) blockchain state at any given moment, and allows you to interact with the accounts.

As you can see in the above test, we use the said wrapper to create an owner account, two other user accounts, and the Crowdfunding smart contract account.

Then, we set the ESDT balances for the two users, and deploy the smart contract, by using the execute_tx function of the BlockchainStateWrapper object. The arguments are:

  • caller address

  • contract wrapper (which contains the contract address and the contract object builder)

  • EGLD payment amount

  • a lambda function, which contains the actual execution

Since this is a SC deploy, we call the init function. Since the contract works with managed objects, we can’t use the built-in Rust BigUint, so we use the one provided by multiversx_sc instead. To create managed types, we use the managed_ functions. Alternatively, you can create those objects by:

let target = BigUint::<DebugApi>::from(2_000u32);

Keep in mind you can’t create managed types outside of the execute_tx functions.

Some observations for the execute_tx function:

  • The return type for the lambda function is a TxResult, which has methods for checking for success or error: assert_ok() is used to check the tx worked. If you want to check error cases, you would use assert_user_error("message").

  • After running the init function, we add a setState step in the generated scenario, to simulate our deploy: blockchain_wrapper.add_mandos_set_account(cf_wrapper.address_ref());

To test the scenario and generate the trace file, you have to create a test function:

#[test]
fn init_test() {
    let cf_setup = setup_crowdfunding(crowdfunding_esdt::contract_obj);
    cf_setup
        .blockchain_wrapper
        .write_mandos_output("_generated_init.scen.json");
}

And you’re done for this step. You successfully tested your contract’s init function, and generated a scenario for it.

Testing transactions

Let’s test the fund function. For this, we’re going to use the previous setup, but now we use the execute_esdt_transfer method instead of execute_tx, because we’re sending ESDT to the contract while calling fund:

#[test]
fn fund_test() {
    let mut cf_setup = setup_crowdfunding(crowdfunding_esdt::contract_obj);
    let b_wrapper = &mut cf_setup.blockchain_wrapper;
    let user_addr = &cf_setup.first_user_address;

    b_wrapper
        .execute_esdt_transfer(
            user_addr,
            &cf_setup.cf_wrapper,
            CF_TOKEN_ID,
            0,
            &rust_biguint!(1_000),
            |sc| {
                sc.fund();

                let user_deposit = sc.deposit(&managed_address!(user_addr)).get();
                let expected_deposit = managed_biguint!(1_000);
                assert_eq!(user_deposit, expected_deposit);
            },
        )
        .assert_ok();
}

#[test]
fn fund_test() {
let mut cf_setup = setup_crowdfunding(crowdfunding_esdt::contract_obj);
let b_wrapper = &mut cf_setup.blockchain_wrapper;
let user_addr = &cf_setup.first_user_address;

b_wrapper
.execute_esdt_transfer(
user_addr,
&cf_setup.cf_wrapper,
CF_TOKEN_ID,
0,
&rust_biguint!(1_000),
|sc| {
sc.fund();

let user_deposit = sc.deposit(&managed_address!(user_addr)).get();
let expected_deposit = managed_biguint!(1_000);
assert_eq!(user_deposit, expected_deposit);
},
)
.assert_ok();
}

let mut sc_call = ScCallMandos::new(user_addr, cf_setup.cf_wrapper.address_ref(), "fund");
sc_call.add_esdt_transfer(CF_TOKEN_ID, 0, &rust_biguint!(1_000));

let expect = TxExpectMandos::new(0);
b_wrapper.add_mandos_sc_call(sc_call, Some(expect));

cf_setup
    .blockchain_wrapper
    .write_mandos_output("_generated_fund.scen.json");

You have to add this at the end of your fund_test. The more complex the call, the more arguments you’ll have to add and such. The SCCallMandos struct has the add_argument method so you don’t have to do any encoding by yourself.

Testing queries

Testing queries is similar to testing transactions, just with less arguments (since there is no caller, and no payment and any modifications are automatically reverted):

#[test]
fn status_test() {
    let mut cf_setup = setup_crowdfunding(crowdfunding_esdt::contract_obj);
    let b_wrapper = &mut cf_setup.blockchain_wrapper;

    b_wrapper
        .execute_query(&cf_setup.cf_wrapper, |sc| {
            let status = sc.status();
            assert_eq!(status, Status::FundingPeriod);
        })
        .assert_ok();

    let sc_query = ScQueryMandos::new(cf_setup.cf_wrapper.address_ref(), "status");
    let mut expect = TxExpectMandos::new(0);
    expect.add_out_value(&Status::FundingPeriod);

    b_wrapper.add_mandos_sc_query(sc_query, Some(expect));

    cf_setup
        .blockchain_wrapper
        .write_mandos_output("_generated_query_status.scen.json");
}

Testing smart contract errors

In the previous transaction test, we’ve tested the happy flow. Now let’s see how we can check for errors:

#[test]
fn test_sc_error() {
let mut cf_setup = setup_crowdfunding(crowdfunding_esdt::contract_obj);
let b_wrapper = &mut cf_setup.blockchain_wrapper;
let user_addr = &cf_setup.first_user_address;

b_wrapper.set_egld_balance(user_addr, &rust_biguint!(1_000));

b_wrapper
    .execute_tx(
        user_addr,
        &cf_setup.cf_wrapper,
        &rust_biguint!(1_000),
        |sc| {
            sc.fund();
        },
    )
    .assert_user_error("wrong token");

b_wrapper
    .execute_tx(user_addr, &cf_setup.cf_wrapper, &rust_biguint!(0), |sc| {
        let user_deposit = sc.deposit(&managed_address!(user_addr)).get();
        let expected_deposit = managed_biguint!(0);
        assert_eq!(user_deposit, expected_deposit);
    })
    .assert_ok();

let mut sc_call = ScCallMandos::new(user_addr, cf_setup.cf_wrapper.address_ref(), "fund");
sc_call.add_egld_value(&rust_biguint!(1_000));

let mut expect = TxExpectMandos::new(4);
expect.set_message("wrong token");

b_wrapper.add_mandos_sc_call(sc_call, Some(expect));

cf_setup
    .blockchain_wrapper
    .write_mandos_output("_generated_sc_err.scen.json");
}

Notice how we’ve changed the payment intentionally to an invalid token to check the error case. Also, we’ve changed the expected deposit to “0” instead of the previous “1_000”. And lastly: the .assert_user_error("wrong token") call on the result.

Testing a successful funding campaign

For this scenario, we need both users to fund the full amount, and then the owner to claim the funds. For simplicity, we’ve left the scenario generation out of this one:

#[test]
fn test_successful_cf() {
    let mut cf_setup = setup_crowdfunding(crowdfunding_esdt::contract_obj);
    let b_wrapper = &mut cf_setup.blockchain_wrapper;
    let owner = &cf_setup.owner_address;
    let first_user = &cf_setup.first_user_address;
    let second_user = &cf_setup.second_user_address;

    // first user fund
    b_wrapper
        .execute_esdt_transfer(
            first_user,
            &cf_setup.cf_wrapper,
            CF_TOKEN_ID,
            0,
            &rust_biguint!(1_000),
            |sc| {
                sc.fund();

                let user_deposit = sc.deposit(&managed_address!(first_user)).get();
                let expected_deposit = managed_biguint!(1_000);
                assert_eq!(user_deposit, expected_deposit);
            },
        )
        .assert_ok();

    // second user fund
    b_wrapper
        .execute_esdt_transfer(
            second_user,
            &cf_setup.cf_wrapper,
            CF_TOKEN_ID,
            0,
            &rust_biguint!(1_000),
            |sc| {
                sc.fund();

                let user_deposit = sc.deposit(&managed_address!(second_user)).get();
                let expected_deposit = managed_biguint!(1_000);
                assert_eq!(user_deposit, expected_deposit);
            },
        )
        .assert_ok();

    // set block timestamp after deadline
    b_wrapper.set_block_timestamp(CF_DEADLINE + 1);

    // check status
    b_wrapper
        .execute_query(&cf_setup.cf_wrapper, |sc| {
            let status = sc.status();
            assert_eq!(status, Status::Successful);
        })
        .assert_ok();

    // user try claim
    b_wrapper
        .execute_tx(first_user, &cf_setup.cf_wrapper, &rust_biguint!(0), |sc| {
            sc.claim();
        })
        .assert_user_error("only owner can claim successful funding");

    // owner claim
    b_wrapper
        .execute_tx(owner, &cf_setup.cf_wrapper, &rust_biguint!(0), |sc| {
            sc.claim();
        })
        .assert_ok();

    b_wrapper.check_esdt_balance(owner, CF_TOKEN_ID, &rust_biguint!(2_000));
    b_wrapper.check_esdt_balance(first_user, CF_TOKEN_ID, &rust_biguint!(0));
    b_wrapper.check_esdt_balance(second_user, CF_TOKEN_ID, &rust_biguint!(0));
}

You’ve already seen most of the code in this test before already. The only new things are the set_block_timestamp and the check_esdt_balance methods of the wrapper. There are similar methods for setting block nonce, block random seed, etc., and checking EGLD and SFT/NFT balances.

Testing a failed funding campaign

This is similar to the previous one, but instead, we have the users claim instead of the owner after the deadline.

#[test]
fn test_failed_cf() {
    let mut cf_setup = setup_crowdfunding(crowdfunding_esdt::contract_obj);
    let b_wrapper = &mut cf_setup.blockchain_wrapper;
    let owner = &cf_setup.owner_address;
    let first_user = &cf_setup.first_user_address;
    let second_user = &cf_setup.second_user_address;

    // first user fund
    b_wrapper
        .execute_esdt_transfer(
            first_user,
            &cf_setup.cf_wrapper,
            CF_TOKEN_ID,
            0,
            &rust_biguint!(300),
            |sc| {
                sc.fund();

                let user_deposit = sc.deposit(&managed_address!(first_user)).get();
                let expected_deposit = managed_biguint!(300);
                assert_eq!(user_deposit, expected_deposit);
            },
        )
        .assert_ok();

    // second user fund
    b_wrapper
        .execute_esdt_transfer(
            second_user,
            &cf_setup.cf_wrapper,
            CF_TOKEN_ID,
            0,
            &rust_biguint!(600),
            |sc| {
                sc.fund();

                let user_deposit = sc.deposit(&managed_address!(second_user)).get();
                let expected_deposit = managed_biguint!(600);
                assert_eq!(user_deposit, expected_deposit);
            },
        )
        .assert_ok();

    // set block timestamp after deadline
    b_wrapper.set_block_timestamp(CF_DEADLINE + 1);

    // check status
    b_wrapper
        .execute_query(&cf_setup.cf_wrapper, |sc| {
            let status = sc.status();
            assert_eq!(status, Status::Failed);
        })
        .assert_ok();

    // first user claim
    b_wrapper
        .execute_tx(first_user, &cf_setup.cf_wrapper, &rust_biguint!(0), |sc| {
            sc.claim();
        })
        .assert_ok();

    // second user claim
    b_wrapper
        .execute_tx(second_user, &cf_setup.cf_wrapper, &rust_biguint!(0), |sc| {
            sc.claim();
        })
        .assert_ok();

    b_wrapper.check_esdt_balance(owner, CF_TOKEN_ID, &rust_biguint!(0));
    b_wrapper.check_esdt_balance(first_user, CF_TOKEN_ID, &rust_biguint!(1_000));
    b_wrapper.check_esdt_balance(second_user, CF_TOKEN_ID, &rust_biguint!(1_000));
}

Rust Testing Framework Functions Reference

State-checking functions

These functions check the blockchain state. They will panic and the test will fail if the check is unsuccessful.

check_egld_balance

check_egld_balance(&self, address: &Address, expected_balance: &num_bigint::BigUint)

Checks the EGLD balance for the given address.

check_esdt_balance

check_esdt_balance(&self, address: &Address, token_id: &[u8], expected_balance: &num_bigint::BigUint)

Checks the fungible ESDT balance for the given address.

check_nft_balance

check_nft_balance<T>(&self, address: &Address, token_id: &[u8], nonce: u64, expected_balance: &num_bigint::BigUint, opt_expected_attributes: Option<&T>)

Where T has to implement TopEncode, TopDecode, PartialEq, and core::fmt::Debug. This is usually done through #[derive(TopEncode, TopDecode, PartialEq, Debug)].

This function checks the NFT balance for a specific nonce for an address, and optionally checks the NFT attributes as well. If you are only interested in the balance, pass Option::None for opt_expected_attributes. The Rust compiler might complain that it can’t deduce the generic T, in which case, you can do one of the following:

b_mock.check_nft_balance::<Empty>(..., None);b_mock.check_nft_balance(..., Option::<Empty>::None);

Where ... are the rest of the arguments.

State-getter functions

These functions get the current state. They are generally used after a transaction to check that the tokens reached their intended destination. Most functions will panic if they’re given an invalid address as argument.

get_egld_balance

get_egld_balance(&self, address: &Address) -> num_bigint::BigUint

Gets the EGLD balance for the given account.

get_esdt_balance

get_esdt_balance(&self, address: &Address, token_id: &[u8], token_nonce: u64) -> num_bigint::BigUint

Gets the ESDT balance for the given account. If you’re interested in fungible token balance, set token_nonce to 0.

get_nft_attributes

get_nft_attributes<T: TopDecode>(&self, address: &Address, token_id: &[u8], token_nonce: u64) -> Option<T>

Gets the NFT attributes for a token owned by the given address. Will return Option::None if there are no attributes.

dump_state

dump_state_for_account_hex_attributes(&self, address: &Address)

Similar to the function before, but dumps state only for the given account.

dump_state_for_account

dump_state_for_account<AttributesType: TopDecode + core::fmt::Debug>(&self, address: &Address)

Similar to the function before, but prints the attributes in a user-friendly format, given by the generic type given. This is useful for debugging NFT attributes.

State-altering functions

These functions alter the state in some way.

create_user_account

create_user_account(&mut self, egld_balance: &num_bigint::BigUint) -> Address

Creates a new user account, with the given EGLD balance. The Address is pseudo-randomly generated by the framework.

create_user_account_fixed_address

create_user_account_fixed_address(&mut self, address: &Address, egld_balance: &num_bigint::BigUint)

Same as the function above, but it lets you create an account with a fixed address, for the few cases when it’s needed.

create_sc_account

create_sc_account<CB, ContractObjBuilder>(&mut self, egld_balance: &num_bigint::BigUint, owner: Option<&Address>, obj_builder: ContractObjBuilder, contract_wasm_path: &str) -> ContractObjWrapper<CB, ContractObjBuilder>

Where:

CB: ContractBase<Api = DebugApi> + CallableContract + 'static,    ContractObjBuilder: 'static + Copy + Fn() -> CB,

Creates a smart contract account. For obj_builder, you will have to pass sc_namespace::contract_obj. This function will return a ContractObjWrapper, which contains the address of the newly created SC, and the function that is used to create instances of your contract.

The ContractObjWrapper will be used whenever you interact with the SC, which will be through the execution functions. If you only need the address (for setting balance, for example), you can use the address_ref method to get a reference to the stored address.

contract_wasm_path is the path towards the wasm file. This path is relative to the tests folder that the current test file resides in. The most usual path will be ouput/wasm_file_name.wasm.

create_sc_account_fixed_address

create_sc_account_fixed_address<CB, ContractObjBuilder>(&mut self, address: &Address, egld_balance: &num_bigint::BigUint, owner: Option<&Address>, obj_builder: ContractObjBuilder, contract_wasm_path: &str) -> ContractObjWrapper<CB, ContractObjBuilder>

Same as the function above, but the address can be set by the caller instead of being randomly generated.

set_egld_balance

set_egld_balance(&mut self, address: &Address, balance: &num_bigint::BigUint)

Sets the EGLD balance for the given account.

set_esdt_balance

set_esdt_balance(&mut self, address: &Address, token_id: &[u8], balance: &num_bigint::BigUint)

Sets the fungible token balance for the given account.

set_nft_balance

set_nft_balance<T: TopEncode>(&mut self, address: &Address, token_id: &[u8], nonce: u64, balance: &num_bigint::BigUint, attributes: &T)

Sets the non-fungible token balance for the given account, and the attributes. Attributes can be any serializable type. If you don’t need attributes, you can pass “empty” in various ways: &(), &Vec::<u8>::new(), BoxedBytes::empty(), etc.

set_esdt_local_roles

set_esdt_local_roles(&mut self, address: &Address, token_id: &[u8], roles: &[EsdtLocalRole])

Sets the ESDT token roles for the given address and token. Usually used during setup steps.

set_block_epoch

set_block_epoch(&mut self, block_epoch: u64)

set_block_nonce

et_block_nonce(&mut self, block_nonce: u64)

set_block_round

set_block_round(&mut self, block_round: u64)

set_block_timestamp

set_block_timestamp(&mut self, block_timestamp: u64)

set_block_random_seed

set_block_random_seed(&mut self, block_random_seed: Box<[u8; 48]>)

Set various values for the current block info.

set_prev_block_epoch

set_prev_block_epoch(&mut self, block_epoch: u64)

set_prev_block_nonce

set_prev_block_nonce(&mut self, block_nonce: u64)

set_prev_block_round

set_prev_block_round(&mut self, block_round: u64)

set_prev_block_timestamp

set_prev_block_timestamp(&mut self, block_timestamp: u64)

set_prev_block_random_seed

set_prev_block_random_seed(&mut self, block_random_seed: Box<[u8; 48]>)

Same as the ones above, but sets the block info for the previous block.

Smart Contract execution functions

These functions help interact

CB: ContractBase<Api = DebugApi> + CallableContract + 'static,    ContractObjBuilder: 'static + Copy + Fn() -> CB,

with smart contracts. While they would still fit into the state-altering category, we feel they deserve their own section.

Note: We will shorten the signatures by not specifying the complete types for the ContractObjWrapper. For reference, the contract wrapper is of type ContractObjWrapper<CB, ContractObjBuilder>, where

CB: ContractBase<Api = DebugApi> + CallableContract + 'static,    ContractObjBuilder: 'static + Copy + Fn() -> CB,

execute_tx

execute_tx(&mut self, caller: &Address, sc_wrapper: &ContractObjWrapper<...>, egld_payment: &num_bigint::BigUint, tx_fn: TxFn) -> TxResult

Executes a transaction towards the given SC (defined by the wrapper), with optional EGLD payment (pass 0 if you want no payment). tx_fn is a lambda function that accepts a contract object as an argument. For more details about how to write such a lambda, you can take a look at the Crowdfunding test examples.

execute_esdt_transfer

execute_esdt_transfer(&mut self, caller: &Address, sc_wrapper: &ContractObjWrapper<...>, token_id: &[u8], esdt_nonce: u64, esdt_amount: &num_bigint::BigUint, tx_fn: TxFn) -> TxResult

Same as the function above, but executes an ESDT/NFT transfer instead of EGLD transfer.

execute_esdt_multi_transfer

execute_esdt_multi_transfer(&mut self, caller: &Address, sc_wrapper: &ContractObjWrapper<...>, esdt_transfers: &[TxInputESDT], tx_fn: TxFn) -> TxResult

Same as the function above, but executes a MultiESDTNFT transfer instead.

execute_query

execute_query(&mut self, sc_wrapper: &ContractObjWrapper<...>, query_fn: TxFn) -> TxResult

Executes a SCQuery on the SC. None of the changes are committed to the state, but it still needs to be a mutable function to perform the temporary changes. Just like on the real blockchain, there is no caller and no token transfer for queries.

execute_in_managed_environment

execute_in_managed_environment(&self, f: Func) -> T

Executes an arbitrary function and returns its result. The result can be any type. This function is rarely used. It can be useful when you want to perform some checks that involve managed types and such. (since you cannot create managed types outside of the lambda functions).

With this, you complete this workshop successfully !!