Please check the build logs for more information.
See Builds for ideas on how to fix a failed build, or Metadata for how to configure docs.rs builds.
If you believe this is docs.rs' fault, open an issue.
NEAR Simulator & cross-contract testing library
When writing NEAR contracts, with Rust or other Wasm-compiled languages like AssemblyScript, the default testing approach for your language of choice (such as mod test
in your Rust project's src/lib.rs
file) is great for testing the behavior of a single contract in isolation.
But the true power of blockchains & smart contracts comes from cross-contract calls. How do you make sure your cross-contract code works as you expect?
As a first step, you can use this library! With it, you can:
- Test cross-contract calls
- Profile gas & storage usage for your contract, establishing lower bounds for costs of deployed contracts and rapidly identifying problematic areas prior to deploying.
- Inspect intermediate state of all calls in a complicated chain of transactions
To view this documentation locally, clone this repo and from this folder run cargo doc --open
.
Changelog
3.2.0
- Introduce
block_prod_time
duration in nanoseconds toGenesisConfig
that defines the duration between produced blocks. - Expose
cur_block
andgenesis_config
fromRuntimeStandalone
. This allows to manipulate block time. - Use
RuntimeConfig::from_protocol_version
that fixes storage costs issue. - Set root account balance to one billion tokens.
Getting started
This section will guide you through our suggested approach to adding simulation tests to your project. Want an example? Check out the Fungible Token Example.
Dependency versions
Currently this crate depends on a the GitHub repo of nearcore, so this crate must be a git dependency too. Furthermore, this crate's dependencies conflict with building the Wasm smart contract, so you must add it under the following:
[]
= "4.0.0-pre.8"
And update near-sdk
too:
[]
= "4.0.0-pre.8"
Note that you need to add the tag
(or commit
) of the version.
Workspace setup
If you want to check gas & storage usage of one Rust contract, you can add the above dependencies to Cargo.toml
in the root of your project. If you want to test cross-contract calls, we recommend setting up a cargo workspace. Here's how it works:
Let's say you have an existing contract project in a folder called contract
.
Go ahead and make a subfolder within it called contract
, and move the original contents of contract
into this subfolder. Now you'll have contract/contract
. You can rename the root folder to something like contracts
or contract-wrap
, if you want. Some bash commands to do this:
Now in the root of the project (contract-wrap
), create a new Cargo.toml
. You'll have to add the normal [package]
section, but unlike most projects you won't have any dependencies
, only dev-dependencies
and a workspace
:
[]
= "4.0.0-pre.8"
= "4.0.0-pre.8"
= { = "./contract" }
[]
= [
"contract"
]
Now when you want to create test contracts, you can add a new subfolder to your project and add a line for it to both [dev-dependencies]
and [workspace]
in this root Cargo.toml
.
Other cleanup:
- You can move any
[profile.release]
settings from your nested project up to the root of the workspace, since workspace members inherit these settings from the workspace root. - You can remove the nested project's
target
, since all workspace members will be built to the root project'starget
directory - If you were building with
cargo build
, you can now build all workspace members at once withcargo build --all
Test files
In the root of your project (contract-wrap
in the example above), create a tests
directory with a Rust file inside. Anything in here will automatically be run by cargo test
.
Inside this folder, set up a new test crate for yourself by creating a tests/sim
directory with a tests/sim/main.rs
file. This file will glue together the other files (aka modules) in this folder. We'll add things to it soon. For now you can leave it empty.
Now create a tests/sim/utils.rs
file. This file will export common functions for all your tests. In it you need to include the bytes of the contract(s) you want to test:
!
lazy_static_include_bytes
Note that this means you must build
before you test
! Since cargo test
does not re-generate the wasm
files that your simulation tests rely on, you will need to cargo build --all --target wasm32-unknown-unknown
before running cargo test
. If you made contract changes and you swear it should pass now, try rebuilding!
Now you can make a function to initialize your simulator:
use ;
const CONTRACT_ID: &str = "contract";
Now you can add a test file that uses this init
function in tests/sim/first_tests.rs
. For every file you add to this directory, you'll need to add a line to tests/sim/main.rs
. Let's add one for both files so far:
// in tests/sim/main.rs
Now add some tests to first_tests.rs
:
use json;
use DEFAULT_GAS;
use crate init;
Optional macros
The above approach is a good start, and will work even if your Wasm files are compiled from a language other than Rust.
But if your original files are Rust and you want better ergonomics while testing, near-sdk-sim
provides a nice bonus feature.
near-sdk-sim
modifies the near_bindgen
macro from near-sdk
to create an additional struct+implementation from your contract, with Contract
added to the end of the name, like xxxxxContract
. So if you have a contract with [package].name
set to token
with this in its src/lib.rs
:
Then in your simulation test you can import TokenContract
:
use TokenContract;
// or rename it maybe
use TokenContract as OtherNamedContract;
Now you can simplify the init
& test code from the previous section:
// in utils.rs
use ;
use TokenContract;
const CONTRACT_ID: &str = "contract";
// in first_tests.rs
use ;
use crate init;
Common patterns
Profile gas costs
For a chain of transactions kicked off by call
or call!
, you can check the gas_burnt
and tokens_burnt
, where tokens_burnt
will equal gas_burnt
multiplied by the gas_price
set in the genesis config. You can also print out profile_data
to see an in-depth gas-use breakdown.
let outcome = some_account.call;
println!;
let expected_gas_ceiling = 5 * u64 pow; // 5 TeraGas
assert!;
TeraGas units are explained here.
Remember to run tests with --nocapture
to see output from println!
:
cargo test -- --nocapture
The output from this println!
might look something like this:
profile_data: ------------------------------
Total gas: 1891395594588
Host gas: 1595600369775 [84% total]
Action gas: 0 [0% total]
Wasm execution: 295795224813 [15% total]
------ Host functions --------
base -> 7678275219 [0% total, 0% host]
contract_compile_base -> 35445963 [0% total, 0% host]
contract_compile_bytes -> 48341969250 [2% total, 3% host]
read_memory_base -> 28708495200 [1% total, 1% host]
read_memory_byte -> 634822611 [0% total, 0% host]
write_memory_base -> 25234153749 [1% total, 1% host]
write_memory_byte -> 539306856 [0% total, 0% host]
read_register_base -> 20137321488 [1% total, 1% host]
read_register_byte -> 17938284 [0% total, 0% host]
write_register_base -> 25789702374 [1% total, 1% host]
write_register_byte -> 821137824 [0% total, 0% host]
utf8_decoding_base -> 3111779061 [0% total, 0% host]
utf8_decoding_byte -> 15162184908 [0% total, 0% host]
log_base -> 3543313050 [0% total, 0% host]
log_byte -> 686337132 [0% total, 0% host]
storage_write_base -> 192590208000 [10% total, 12% host]
storage_write_key_byte -> 1621105941 [0% total, 0% host]
storage_write_value_byte -> 2047223574 [0% total, 0% host]
storage_write_evicted_byte -> 2119742262 [0% total, 0% host]
storage_read_base -> 169070537250 [8% total, 10% host]
storage_read_key_byte -> 711908259 [0% total, 0% host]
storage_read_value_byte -> 370326330 [0% total, 0% host]
touching_trie_node -> 1046627135190 [55% total, 65% host]
------ Actions --------
------------------------------
tokens_burnt: 0.00043195379520539996Ⓝ
Profile storage costs
For a ContractAccount
created with deploy!
or a UserAccount
created with root.create_user
, you can call account()
to get the Account information stored in the simulated blockchain.
let account = root.account.unwrap;
let balance = account.amount;
let locked_in_stake = account.locked;
let storage_usage = account.storage_usage;
You can use this info to do detailed profiling of how contract calls alter the storage usage of accounts.
Inspect intermediate state of all calls in a complicated chain of transactions
Say you have a call
or call!
:
let outcome = some_account.call;
If some_contract.method
here makes cross-contract calls, near-sdk-sim
will allow all of these calls to complete. You can then inspect the entire chain of calls via the outcome
struct. Some useful methods:
You can use these with println!
and pretty print interpolation:
println!;
Remember to run your tests with --nocapture
to see the println!
output:
cargo test -- --nocapture
You might see something like this:
[
Some(
ExecutionResult {
outcome: ExecutionOutcome {
logs: [],
receipt_ids: [
`2bCDBfWgRkzGggXLuiXqhnVGbxwRz7RP3qa8WS5nNw8t`,
],
burnt_gas: 2428220615156,
tokens_burnt: 0,
status: SuccessReceiptId(2bCDBfWgRkzGggXLuiXqhnVGbxwRz7RP3qa8WS5nNw8t),
},
},
),
Some(
ExecutionResult {
outcome: ExecutionOutcome {
logs: [],
receipt_ids: [],
burnt_gas: 18841799405111,
tokens_burnt: 0,
status: Failure(Action #0: Smart contract panicked: panicked at 'Not an integer: ParseIntError { kind: InvalidDigit }', test-contract/src/lib.rs:85:56),
},
},
)
]
You can see it's a little hard to tell which call is which, since the ExecutionResult does not yet include the name of the contract or method. To help debug, you can use log!
in your contract methods. All log!
output will show up in the logs
arrays in the ExecutionOutcomes shown above.
Check expected transaction failures
If you want to check something in the logs
or status
of one of the transactions in one of these call chains mentioned above, you can use string matching. To check that the Failure above matches your expectations, you could:
use ExecutionStatus;
This promise_errors
returns a filtered version of the promise_results
method mentioned above.
Parsing logs
is much simpler, whether from get_receipt_results
or from logs
directly.
Tweaking the genesis config
For many simulation tests, using init_simulator(None)
is good enough. This uses the default genesis configuration settings:
GenesisConfig
If you want to override some of these values, for example to simulate how your contract will behave if gas price goes up 10x:
use GenesisConfig;