Setting up the environment

The first thing we need is to compile the contract. If you didn't compile it beforehand to wasm, running cargo test will result in incorrect results.

The second thing is to import necessary wasm to test. We shall put it in the tests/res folder, just to group them together. At the root of your cargo project folder, run:

mkdir tests
mkdir tests/res

cd tests/res

Then download these files from Github:

NOTE: DO NOT USE "WGET". One got stuck in the error for 3 HOURS because when you use WGET, it downloads the HTML of github rather than the wasm file. What's worse, the error is misleading. The error is "PrepareError: Error happened while deserilaizing the module." and you thought it has something to do with your code rather than the wasm file, otherwise how did you execute it successfully? It's only when inspecting wasm and found that it is actually html code when one realizes it's the error.

These shall get you the necessary contract code for simulating smart contract calls.

Let's look at the contract next. We need to create a rust file first, so:

mkdir tests/sim
touch tests/sim/main.rs
touch tests/sim/utils.rs

They put it in tests/spec.rs, but we don't, following the new convention. Our main tests will locate in tests/sim/main.rs, while some utility functions we shall put in tests/sim/utils.rs.

We import some function from our lockup contract. One uses use lockup because my Cargo file has name the "library" as lockup. In the original contract, they named it lockup-contract, so the use lockup_contract.

We also import necessary functions from near_sdk and near_sdk_sim, and some other libraries. Here, we requires the quickcheck libraries and quickcheck macros too. Let's look into Cargo.toml before continuing.

In your Cargo.toml, it should look like this:

[package]
...

[lib]
crate-type = ["cdylib", "rlib"]

[dependencies]
near-sdk = "=4.0.0-pre.4"
uint = { version = "0.9.3", default-features = false }

[dev-dependencies]
near-sdk-sim = "4.0.0-pre.4"
quickcheck = "1.0.3"
quickcheck_macros = "1.0.0"

Be careful though, because the near docs failed to build for near-sdk-sim "4.0.0-pre.4", we don't know everything about it. If things failed and you can't figure out, consider falling back to "3.2.0" version. Otherwise, one sees that the "4.0.0-pre.1" successfully build docs, so perhaps you could try refer to that version and hopefully infer something from there.

To continue, we import more utilities. In main.rs

use lockup::{
  LockupContractContract, TerminationStatus, TransfersInformation, VestingSchedule,
  VestingScheduleOrHash, VestingScheduleWithSalt, WrappedBalance, MIN_BALANCE_FOR_STORAGE
};
use near_sdk::borsh::BorshSerialize;
use near_sdk::json_types::U128;
use near_sdk::serde_json::json;
use near_sdk::{AccountId, Balance};
use near_sdk_sim::runtime::GenesisConfig;
use near_sdk_sim::{deploy, init_simulator, to_yocto, UserAccount, STORAGE_AMOUNT};
use quickcheck_macros::quickcheck;
use std::convert::TryInto;

mod test_staking_with_helpers;
mod test_termination_with_staking_hashed;

pub(crate) mod utils;  // utils require pub-crate
mod test_staking;  // import other test files like this. 

pub(crate) use crate::utils::*;

pub(crate) fn assert_almost_eq_with_max_delta(left: u128, right: u128, max_delta: u128) {
  assert!(
      std::cmp::max(left, right) - std::cmp::min(left, right) <= max_delta,
      "{}",
      format!(
          "Left {} is not even close to Right {} within delta {}",
          left, right, max_delta
      )
  );
}

pub(crate) fn assert_eq_with_gas(left: u128, right: u128) {
  assert_almost_eq_with_max_delta(left, right, to_yocto("0.005"));
}

pub(crate) fn assert_yocto_eq(left: u128, right: u128) {
  assert_almost_eq_with_max_delta(left, right, 1);
}


near_sdk_sim::lazy_static_include::lazy_static_include_bytes! {
  LOCKUP_WASM_BYTES => "res/lockup.wasm",
  STAKING_POOL_WASM_BYTES => "tests/res/staking_pool.wasm",
  FAKE_VOTING_WASM_BYTES => "tests/res/fake_voting.wasm",
  WHITELIST_WASM_BYTES => "tests/res/whitelist.wasm"
}

Let's look at some initialization function moved to utils.rs. Here, we add the constants, functions, and wasm file imports. Again, mine is lockup.wasm, but yours might be lockup_contract.wasm, so double check that to match the "name" defined in cargo.toml.

build.sh

#!/bin/bash
set -e

export CONTRACT=lockup.wasm
RUSTFLAGS='-C link-arg=-s' cargo build --target wasm32-unknown-unknown --release
cp target/wasm32-unknown-unknown/release/$CONTRACT res/

In utils.rs at the top, include this.

// Include contract files
// The directory is relative to the top-level Cargo directory. 
near_sdk_sim::lazy_static_include::lazy_static_include_bytes! {
    LOCKUP_WASM_BYTES => "res/lockup.wasm",
    STAKING_POOL_WASM_BYTES => "tests/res/staking_pool.wasm",
    FAKE_VOTING_WASM_BYTES => "tests/res/fake_voting.wasm",
    WHITELIST_WASM_BYTES => "tests/res/whitelist.wasm"
}



use crate::*;


pub const MAX_GAS: u64 = 300_000_000_000_000;
pub const NO_DEPOSIT: Balance = 0;
pub const LOCKUP_ACCOUNT_ID: &str = "lockup";

pub(crate) const STAKING_POOL_WHITELIST_ACCOUNT_ID: &str = "staking-pool-whitelist";
pub(crate) const STAKING_POOL_ACCOUNT_ID: &str = "staking-pool";
// pub(crate) const TRANSFER_POOL_ACCOUNT_ID: &str = "transfer-pool";


pub(crate) fn basic_setup() -> (UserAccount, UserAccount, UserAccount, UserAccount) {
    let mut genesis_config = GenesisConfig::default();
    genesis_config.block_prod_time = 0;
    let root = init_simulator(Some(genesis_config));

    let foundation = root.create_user("foundation".parse().unwrap(), to_yocto("10000"));
    let owner = root.create_user("owner".parse().unwrap(), to_yocto("30"));

    let _whitelist = root.deploy_and_init(
      &WHITELIST_WASM_BYTES, 
      STAKING_POOL_WHITELIST_ACCOUNT_ID.parse().unwrap(), 
      "new", 
      &json!({
        "foundation_account_id": foundation.account_id().to_string(),
      }).to_string().into_bytes(), 
      to_yocto("30"), 
      MAX_GAS,
    );

    // Whitelist staking pool
    foundation.call(
      STAKING_POOL_WHITELIST_ACCOUNT_ID.parse().unwrap(),
      "add_staking_pool",
      &json!({
        "staking_pool_account_id": STAKING_POOL_ACCOUNT_ID.to_string(),
      }).to_string().into_bytes(),
      MAX_GAS,
      NO_DEPOSIT,
    ).assert_success();


    // Staking pool
    let staking_pool = root.deploy_and_init(
      &STAKING_POOL_WASM_BYTES, 
      STAKING_POOL_ACCOUNT_ID.parse().unwrap(), 
      "new", 
      &json!({
        "owner_id": foundation.account_id(),
        "stake_public_key": "ed25519:3tysLvy7KGoE8pznUgXvSHa4vYyGvrDZFcT8jgb8PEQ6",
        "reward_fee_fraction": {
          "numerator": 10u64,
          "denominator": 100u64
        }
      }).to_string().into_bytes(), 
      to_yocto("40"), 
      MAX_GAS,
    );

    (root, foundation, owner, staking_pool)

}


// #[quickcheck]
fn lockup_fn(
  lockup_amount: Balance,
  lockup_duration: u64,
  lockup_timestamp: u64,
) {
    let (root, _foundation, owner, _staking_pool) = basic_setup();

    let lockup_account: AccountId = LOCKUP_ACCOUNT_ID.parse().unwrap();
    let staking_account: AccountId = STAKING_POOL_WHITELIST_ACCOUNT_ID.parse().unwrap();

    let lockup = deploy!(
      contract: LockupContractContract,
      contract_id: lockup_account,
      bytes: &LOCKUP_WASM_BYTES,
      signer_account: root,
      deposit: MIN_BALANCE_FOR_STORAGE + lockup_amount,
      gas: MAX_GAS,
      init_method: new(
        owner.account_id.clone(),
        lockup_duration.into(),
        None,
        TransfersInformation::TransfersEnabled {
          transfers_timestamp: lockup_timestamp.saturating_add(1).into(),
        },
        None,
        None,
        staking_account,
        None
      )
    );


    root.borrow_runtime_mut().cur_block.block_timestamp = lockup_timestamp
        .saturating_add(lockup_duration).saturating_sub(1);

    let locked_amount: U128 = owner
        .view_method_call(lockup.contract.get_locked_amount())
        .unwrap_json();
        
    assert_eq!(
      locked_amount.0,
      MIN_BALANCE_FOR_STORAGE + lockup_amount
    );

    let block_timestamp = root.borrow_runtime().cur_block.block_timestamp;

    root.borrow_runtime_mut().cur_block.block_timestamp = block_timestamp.saturating_add(2);

    let locked_amount: U128 = owner 
        .view_method_call(lockup.contract.get_locked_amount())
        .unwrap_json();

    assert_eq!(locked_amount.0, 0);

}

Then below that, we initialize our simulator.

// Include contract files
// The directory is relative to the top-level Cargo directory. 
near_sdk_sim::lazy_static_include::lazy_static_include_bytes! {
    LOCKUP_WASM_BYTES => "res/lockup.wasm",
    STAKING_POOL_WASM_BYTES => "tests/res/staking_pool.wasm",
    FAKE_VOTING_WASM_BYTES => "tests/res/fake_voting.wasm",
    WHITELIST_WASM_BYTES => "tests/res/whitelist.wasm"
}



use crate::*;


pub const MAX_GAS: u64 = 300_000_000_000_000;
pub const NO_DEPOSIT: Balance = 0;
pub const LOCKUP_ACCOUNT_ID: &str = "lockup";

pub(crate) const STAKING_POOL_WHITELIST_ACCOUNT_ID: &str = "staking-pool-whitelist";
pub(crate) const STAKING_POOL_ACCOUNT_ID: &str = "staking-pool";
// pub(crate) const TRANSFER_POOL_ACCOUNT_ID: &str = "transfer-pool";


pub(crate) fn basic_setup() -> (UserAccount, UserAccount, UserAccount, UserAccount) {
    let mut genesis_config = GenesisConfig::default();
    genesis_config.block_prod_time = 0;
    let root = init_simulator(Some(genesis_config));

    let foundation = root.create_user("foundation".parse().unwrap(), to_yocto("10000"));
    let owner = root.create_user("owner".parse().unwrap(), to_yocto("30"));

    let _whitelist = root.deploy_and_init(
      &WHITELIST_WASM_BYTES, 
      STAKING_POOL_WHITELIST_ACCOUNT_ID.parse().unwrap(), 
      "new", 
      &json!({
        "foundation_account_id": foundation.account_id().to_string(),
      }).to_string().into_bytes(), 
      to_yocto("30"), 
      MAX_GAS,
    );

    // Whitelist staking pool
    foundation.call(
      STAKING_POOL_WHITELIST_ACCOUNT_ID.parse().unwrap(),
      "add_staking_pool",
      &json!({
        "staking_pool_account_id": STAKING_POOL_ACCOUNT_ID.to_string(),
      }).to_string().into_bytes(),
      MAX_GAS,
      NO_DEPOSIT,
    ).assert_success();


    // Staking pool
    let staking_pool = root.deploy_and_init(
      &STAKING_POOL_WASM_BYTES, 
      STAKING_POOL_ACCOUNT_ID.parse().unwrap(), 
      "new", 
      &json!({
        "owner_id": foundation.account_id(),
        "stake_public_key": "ed25519:3tysLvy7KGoE8pznUgXvSHa4vYyGvrDZFcT8jgb8PEQ6",
        "reward_fee_fraction": {
          "numerator": 10u64,
          "denominator": 100u64
        }
      }).to_string().into_bytes(), 
      to_yocto("40"), 
      MAX_GAS,
    );

    (root, foundation, owner, staking_pool)

}


// #[quickcheck]
fn lockup_fn(
  lockup_amount: Balance,
  lockup_duration: u64,
  lockup_timestamp: u64,
) {
    let (root, _foundation, owner, _staking_pool) = basic_setup();

    let lockup_account: AccountId = LOCKUP_ACCOUNT_ID.parse().unwrap();
    let staking_account: AccountId = STAKING_POOL_WHITELIST_ACCOUNT_ID.parse().unwrap();

    let lockup = deploy!(
      contract: LockupContractContract,
      contract_id: lockup_account,
      bytes: &LOCKUP_WASM_BYTES,
      signer_account: root,
      deposit: MIN_BALANCE_FOR_STORAGE + lockup_amount,
      gas: MAX_GAS,
      init_method: new(
        owner.account_id.clone(),
        lockup_duration.into(),
        None,
        TransfersInformation::TransfersEnabled {
          transfers_timestamp: lockup_timestamp.saturating_add(1).into(),
        },
        None,
        None,
        staking_account,
        None
      )
    );


    root.borrow_runtime_mut().cur_block.block_timestamp = lockup_timestamp
        .saturating_add(lockup_duration).saturating_sub(1);

    let locked_amount: U128 = owner
        .view_method_call(lockup.contract.get_locked_amount())
        .unwrap_json();
        
    assert_eq!(
      locked_amount.0,
      MIN_BALANCE_FOR_STORAGE + lockup_amount
    );

    let block_timestamp = root.borrow_runtime().cur_block.block_timestamp;

    root.borrow_runtime_mut().cur_block.block_timestamp = block_timestamp.saturating_add(2);

    let locked_amount: U128 = owner 
        .view_method_call(lockup.contract.get_locked_amount())
        .unwrap_json();

    assert_eq!(locked_amount.0, 0);

}

We call it basic_setup() here. You could also call it init().

Some of the stuffs we don't really know how to solve. For example, they uses .valid_account_id() previously, we don't know what we need here, so we'll just try to use AccountId as ValidAccountId is deprecated, like we usually do in smart contract code. We'll derive the correct answer as we start testing. So, we call account_id() instead.

Another is integer cannot be inferred, so we need to state it clearly. The error is:

error[E0283]: type annotations needed
   --> tests/sim/utils.rs:63:8
    |
63  |         &json!({
    |  ________^
64  | |         "owner_id": foundation,
65  | |         "stake_public_key": "ed25519:3tysLvy7KGoE8pznUgXvSHa4vYyGvrDZFcT8jgb8PEQ6",
66  | |         "reward_fee_fraction": {
...   |
69  | |         }
70  | |       }).to_string().into_bytes(), 
    | |________^ cannot infer type for type `{integer}`
    |
    = note: multiple `impl`s satisfying `{integer}: Serialize` found in the `serde` crate:
            - impl Serialize for i128;
            - impl Serialize for i16;
            - impl Serialize for i32;
            - impl Serialize for i64;
            and 8 more
    = note: required because of the requirements on the impl of `Serialize` for `&{integer}`
note: required by a bound in `to_value`

Perhaps the safest to use is u128, but let's just try u16 first and we can change later if it fails.

Some imports on main.rs might not reflect on utils.rs, for whatever reason. If so, just move the imports there from main.rs.

If you got this error:

 no rules expected this token in macro call

That's because you accidentally put a comma in the final argument before the close-bracket. Remove the comma should work.

Let's talk about basic_setup()

basic_setup()

The first thing you see is these 3 lines:

// Include contract files
// The directory is relative to the top-level Cargo directory. 
near_sdk_sim::lazy_static_include::lazy_static_include_bytes! {
    LOCKUP_WASM_BYTES => "res/lockup.wasm",
    STAKING_POOL_WASM_BYTES => "tests/res/staking_pool.wasm",
    FAKE_VOTING_WASM_BYTES => "tests/res/fake_voting.wasm",
    WHITELIST_WASM_BYTES => "tests/res/whitelist.wasm"
}



use crate::*;


pub const MAX_GAS: u64 = 300_000_000_000_000;
pub const NO_DEPOSIT: Balance = 0;
pub const LOCKUP_ACCOUNT_ID: &str = "lockup";

pub(crate) const STAKING_POOL_WHITELIST_ACCOUNT_ID: &str = "staking-pool-whitelist";
pub(crate) const STAKING_POOL_ACCOUNT_ID: &str = "staking-pool";
// pub(crate) const TRANSFER_POOL_ACCOUNT_ID: &str = "transfer-pool";


pub(crate) fn basic_setup() -> (UserAccount, UserAccount, UserAccount, UserAccount) {
    let mut genesis_config = GenesisConfig::default();
    genesis_config.block_prod_time = 0;
    let root = init_simulator(Some(genesis_config));

    let foundation = root.create_user("foundation".parse().unwrap(), to_yocto("10000"));
    let owner = root.create_user("owner".parse().unwrap(), to_yocto("30"));

    let _whitelist = root.deploy_and_init(
      &WHITELIST_WASM_BYTES, 
      STAKING_POOL_WHITELIST_ACCOUNT_ID.parse().unwrap(), 
      "new", 
      &json!({
        "foundation_account_id": foundation.account_id().to_string(),
      }).to_string().into_bytes(), 
      to_yocto("30"), 
      MAX_GAS,
    );

    // Whitelist staking pool
    foundation.call(
      STAKING_POOL_WHITELIST_ACCOUNT_ID.parse().unwrap(),
      "add_staking_pool",
      &json!({
        "staking_pool_account_id": STAKING_POOL_ACCOUNT_ID.to_string(),
      }).to_string().into_bytes(),
      MAX_GAS,
      NO_DEPOSIT,
    ).assert_success();


    // Staking pool
    let staking_pool = root.deploy_and_init(
      &STAKING_POOL_WASM_BYTES, 
      STAKING_POOL_ACCOUNT_ID.parse().unwrap(), 
      "new", 
      &json!({
        "owner_id": foundation.account_id(),
        "stake_public_key": "ed25519:3tysLvy7KGoE8pznUgXvSHa4vYyGvrDZFcT8jgb8PEQ6",
        "reward_fee_fraction": {
          "numerator": 10u64,
          "denominator": 100u64
        }
      }).to_string().into_bytes(), 
      to_yocto("40"), 
      MAX_GAS,
    );

    (root, foundation, owner, staking_pool)

}


// #[quickcheck]
fn lockup_fn(
  lockup_amount: Balance,
  lockup_duration: u64,
  lockup_timestamp: u64,
) {
    let (root, _foundation, owner, _staking_pool) = basic_setup();

    let lockup_account: AccountId = LOCKUP_ACCOUNT_ID.parse().unwrap();
    let staking_account: AccountId = STAKING_POOL_WHITELIST_ACCOUNT_ID.parse().unwrap();

    let lockup = deploy!(
      contract: LockupContractContract,
      contract_id: lockup_account,
      bytes: &LOCKUP_WASM_BYTES,
      signer_account: root,
      deposit: MIN_BALANCE_FOR_STORAGE + lockup_amount,
      gas: MAX_GAS,
      init_method: new(
        owner.account_id.clone(),
        lockup_duration.into(),
        None,
        TransfersInformation::TransfersEnabled {
          transfers_timestamp: lockup_timestamp.saturating_add(1).into(),
        },
        None,
        None,
        staking_account,
        None
      )
    );


    root.borrow_runtime_mut().cur_block.block_timestamp = lockup_timestamp
        .saturating_add(lockup_duration).saturating_sub(1);

    let locked_amount: U128 = owner
        .view_method_call(lockup.contract.get_locked_amount())
        .unwrap_json();
        
    assert_eq!(
      locked_amount.0,
      MIN_BALANCE_FOR_STORAGE + lockup_amount
    );

    let block_timestamp = root.borrow_runtime().cur_block.block_timestamp;

    root.borrow_runtime_mut().cur_block.block_timestamp = block_timestamp.saturating_add(2);

    let locked_amount: U128 = owner 
        .view_method_call(lockup.contract.get_locked_amount())
        .unwrap_json();

    assert_eq!(locked_amount.0, 0);

}

This introduces the genesis engine; aka "blockchain". You can treat this as the NEAR blockchain, where all the accounts, contract, etc are held on. root is the simulator.

When we next call create_user:

// Include contract files
// The directory is relative to the top-level Cargo directory. 
near_sdk_sim::lazy_static_include::lazy_static_include_bytes! {
    LOCKUP_WASM_BYTES => "res/lockup.wasm",
    STAKING_POOL_WASM_BYTES => "tests/res/staking_pool.wasm",
    FAKE_VOTING_WASM_BYTES => "tests/res/fake_voting.wasm",
    WHITELIST_WASM_BYTES => "tests/res/whitelist.wasm"
}



use crate::*;


pub const MAX_GAS: u64 = 300_000_000_000_000;
pub const NO_DEPOSIT: Balance = 0;
pub const LOCKUP_ACCOUNT_ID: &str = "lockup";

pub(crate) const STAKING_POOL_WHITELIST_ACCOUNT_ID: &str = "staking-pool-whitelist";
pub(crate) const STAKING_POOL_ACCOUNT_ID: &str = "staking-pool";
// pub(crate) const TRANSFER_POOL_ACCOUNT_ID: &str = "transfer-pool";


pub(crate) fn basic_setup() -> (UserAccount, UserAccount, UserAccount, UserAccount) {
    let mut genesis_config = GenesisConfig::default();
    genesis_config.block_prod_time = 0;
    let root = init_simulator(Some(genesis_config));

    let foundation = root.create_user("foundation".parse().unwrap(), to_yocto("10000"));
    let owner = root.create_user("owner".parse().unwrap(), to_yocto("30"));

    let _whitelist = root.deploy_and_init(
      &WHITELIST_WASM_BYTES, 
      STAKING_POOL_WHITELIST_ACCOUNT_ID.parse().unwrap(), 
      "new", 
      &json!({
        "foundation_account_id": foundation.account_id().to_string(),
      }).to_string().into_bytes(), 
      to_yocto("30"), 
      MAX_GAS,
    );

    // Whitelist staking pool
    foundation.call(
      STAKING_POOL_WHITELIST_ACCOUNT_ID.parse().unwrap(),
      "add_staking_pool",
      &json!({
        "staking_pool_account_id": STAKING_POOL_ACCOUNT_ID.to_string(),
      }).to_string().into_bytes(),
      MAX_GAS,
      NO_DEPOSIT,
    ).assert_success();


    // Staking pool
    let staking_pool = root.deploy_and_init(
      &STAKING_POOL_WASM_BYTES, 
      STAKING_POOL_ACCOUNT_ID.parse().unwrap(), 
      "new", 
      &json!({
        "owner_id": foundation.account_id(),
        "stake_public_key": "ed25519:3tysLvy7KGoE8pznUgXvSHa4vYyGvrDZFcT8jgb8PEQ6",
        "reward_fee_fraction": {
          "numerator": 10u64,
          "denominator": 100u64
        }
      }).to_string().into_bytes(), 
      to_yocto("40"), 
      MAX_GAS,
    );

    (root, foundation, owner, staking_pool)

}


// #[quickcheck]
fn lockup_fn(
  lockup_amount: Balance,
  lockup_duration: u64,
  lockup_timestamp: u64,
) {
    let (root, _foundation, owner, _staking_pool) = basic_setup();

    let lockup_account: AccountId = LOCKUP_ACCOUNT_ID.parse().unwrap();
    let staking_account: AccountId = STAKING_POOL_WHITELIST_ACCOUNT_ID.parse().unwrap();

    let lockup = deploy!(
      contract: LockupContractContract,
      contract_id: lockup_account,
      bytes: &LOCKUP_WASM_BYTES,
      signer_account: root,
      deposit: MIN_BALANCE_FOR_STORAGE + lockup_amount,
      gas: MAX_GAS,
      init_method: new(
        owner.account_id.clone(),
        lockup_duration.into(),
        None,
        TransfersInformation::TransfersEnabled {
          transfers_timestamp: lockup_timestamp.saturating_add(1).into(),
        },
        None,
        None,
        staking_account,
        None
      )
    );


    root.borrow_runtime_mut().cur_block.block_timestamp = lockup_timestamp
        .saturating_add(lockup_duration).saturating_sub(1);

    let locked_amount: U128 = owner
        .view_method_call(lockup.contract.get_locked_amount())
        .unwrap_json();
        
    assert_eq!(
      locked_amount.0,
      MIN_BALANCE_FOR_STORAGE + lockup_amount
    );

    let block_timestamp = root.borrow_runtime().cur_block.block_timestamp;

    root.borrow_runtime_mut().cur_block.block_timestamp = block_timestamp.saturating_add(2);

    let locked_amount: U128 = owner 
        .view_method_call(lockup.contract.get_locked_amount())
        .unwrap_json();

    assert_eq!(locked_amount.0, 0);

}

This is simulating wallet creation on-chain. When you create a wallet, you need to specify the wallet address ("example.near"), then you need to transfer some funds to it (either to cover storage costs; or more than that). So we create a foundation account and transfer 10000 NEAR to it; and owner has 30 NEAR.

Next, we need to deploy the whitelist account. It has an initialization function that needs to run once after deployment. On-chain, we do this by deploy, and separately call the function. We could also do that here:

let _whitelist = root.deploy(
  &WHITELIST_WASM_BYTES,
  STAKING_POOL_WHITELIST_ACCOUNT_ID.parse().unwrap(),
  to_yocto("30")
);

foundation.call(
  STAKING_POOL_WHITELIST_ACCOUNT_ID.parse().unwrap(),
  "new",
  &!json({
    "foundation_account_id": foundation.account_id()
  }).to_string().into_bytes(),
  MAX_GAS,
  NO_DEPOSIT,
).assert_success();

But since this is so common, there is a deploy_and_init function for it, just to reduce code typing.

// Include contract files
// The directory is relative to the top-level Cargo directory. 
near_sdk_sim::lazy_static_include::lazy_static_include_bytes! {
    LOCKUP_WASM_BYTES => "res/lockup.wasm",
    STAKING_POOL_WASM_BYTES => "tests/res/staking_pool.wasm",
    FAKE_VOTING_WASM_BYTES => "tests/res/fake_voting.wasm",
    WHITELIST_WASM_BYTES => "tests/res/whitelist.wasm"
}



use crate::*;


pub const MAX_GAS: u64 = 300_000_000_000_000;
pub const NO_DEPOSIT: Balance = 0;
pub const LOCKUP_ACCOUNT_ID: &str = "lockup";

pub(crate) const STAKING_POOL_WHITELIST_ACCOUNT_ID: &str = "staking-pool-whitelist";
pub(crate) const STAKING_POOL_ACCOUNT_ID: &str = "staking-pool";
// pub(crate) const TRANSFER_POOL_ACCOUNT_ID: &str = "transfer-pool";


pub(crate) fn basic_setup() -> (UserAccount, UserAccount, UserAccount, UserAccount) {
    let mut genesis_config = GenesisConfig::default();
    genesis_config.block_prod_time = 0;
    let root = init_simulator(Some(genesis_config));

    let foundation = root.create_user("foundation".parse().unwrap(), to_yocto("10000"));
    let owner = root.create_user("owner".parse().unwrap(), to_yocto("30"));

    let _whitelist = root.deploy_and_init(
      &WHITELIST_WASM_BYTES, 
      STAKING_POOL_WHITELIST_ACCOUNT_ID.parse().unwrap(), 
      "new", 
      &json!({
        "foundation_account_id": foundation.account_id().to_string(),
      }).to_string().into_bytes(), 
      to_yocto("30"), 
      MAX_GAS,
    );

    // Whitelist staking pool
    foundation.call(
      STAKING_POOL_WHITELIST_ACCOUNT_ID.parse().unwrap(),
      "add_staking_pool",
      &json!({
        "staking_pool_account_id": STAKING_POOL_ACCOUNT_ID.to_string(),
      }).to_string().into_bytes(),
      MAX_GAS,
      NO_DEPOSIT,
    ).assert_success();


    // Staking pool
    let staking_pool = root.deploy_and_init(
      &STAKING_POOL_WASM_BYTES, 
      STAKING_POOL_ACCOUNT_ID.parse().unwrap(), 
      "new", 
      &json!({
        "owner_id": foundation.account_id(),
        "stake_public_key": "ed25519:3tysLvy7KGoE8pznUgXvSHa4vYyGvrDZFcT8jgb8PEQ6",
        "reward_fee_fraction": {
          "numerator": 10u64,
          "denominator": 100u64
        }
      }).to_string().into_bytes(), 
      to_yocto("40"), 
      MAX_GAS,
    );

    (root, foundation, owner, staking_pool)

}


// #[quickcheck]
fn lockup_fn(
  lockup_amount: Balance,
  lockup_duration: u64,
  lockup_timestamp: u64,
) {
    let (root, _foundation, owner, _staking_pool) = basic_setup();

    let lockup_account: AccountId = LOCKUP_ACCOUNT_ID.parse().unwrap();
    let staking_account: AccountId = STAKING_POOL_WHITELIST_ACCOUNT_ID.parse().unwrap();

    let lockup = deploy!(
      contract: LockupContractContract,
      contract_id: lockup_account,
      bytes: &LOCKUP_WASM_BYTES,
      signer_account: root,
      deposit: MIN_BALANCE_FOR_STORAGE + lockup_amount,
      gas: MAX_GAS,
      init_method: new(
        owner.account_id.clone(),
        lockup_duration.into(),
        None,
        TransfersInformation::TransfersEnabled {
          transfers_timestamp: lockup_timestamp.saturating_add(1).into(),
        },
        None,
        None,
        staking_account,
        None
      )
    );


    root.borrow_runtime_mut().cur_block.block_timestamp = lockup_timestamp
        .saturating_add(lockup_duration).saturating_sub(1);

    let locked_amount: U128 = owner
        .view_method_call(lockup.contract.get_locked_amount())
        .unwrap_json();
        
    assert_eq!(
      locked_amount.0,
      MIN_BALANCE_FOR_STORAGE + lockup_amount
    );

    let block_timestamp = root.borrow_runtime().cur_block.block_timestamp;

    root.borrow_runtime_mut().cur_block.block_timestamp = block_timestamp.saturating_add(2);

    let locked_amount: U128 = owner 
        .view_method_call(lockup.contract.get_locked_amount())
        .unwrap_json();

    assert_eq!(locked_amount.0, 0);

}

The rest is now easy to understand. foundation call the add_staking_pool so we can add the staking pool to whitelist; which we then can deploy the staking pool. Staking pool also have an initialization function we call at the start. We return everything back.

There is a quickcheck function; but unfortunately it doesn't run on near-sdk-sim v4.0.0-pre.4. It does run on v3.2.0 though; but that requires you use near-sdk-rs v3.1.0 also.

Nevertheless, you can view it in line 54-77 in the code (check References).

Next, we shall look at one of the test function.

References