Part 2: Inter-contract calls and testing
Previously, you learned how to create your first dApp. In this second session, you will enhance your skills on:
- How to do inter-contract calls.
- How to use views.
- How to do unit & mutation tests.
On the first version of the Poke game, you were able to poke any deployed contract. Now, you will add a new function to store on the trace an additional feedback message coming from another contract.
Poke and Get Feedback sequence diagram
Get the code
Get the code from the first session: https://github.com/marigold-dev/training-dapp-1/blob/main/solution
git clone https://github.com/marigold-dev/training-dapp-1.git
Reuse the code from the previous smart contract: https://github.com/marigold-dev/training-dapp-1/blob/main/solution/contracts/pokeGame.jsligo
Install all libraries locally:
cd solution && npm i && cd app && yarn install && cd ..
Modify the poke function
Change the storage to reflect the changes:
- If you poke directly, you must register the contract's owner's address and no feedback.
- If you poke and ask to get feedback from another contract, then you register the other contract address and an additional feedback message. Here is the new sequence diagram of the poke function.
-
Edit
./contracts/pokeGame.jsligoand replace the storage definition with this one:export type pokeMessage = {
receiver : address,
feedback : string
};
export type storage = {
pokeTraces : map<address, pokeMessage>,
feedback : string
}; -
Replace your poke function with these lines:
@entry
const poke = (_ : unit, store : storage) : return_ => {
let feedbackMessage = {receiver : Tezos.get_self_address() ,feedback: ""};
return [ list([]) as list<operation>, {...store,
pokeTraces : Map.add(Tezos.get_source(), feedbackMessage, store.pokeTraces) }];
};Explanation:
...storedo a copy by value of your object. Note: you cannot do an assignment like thisstore.pokeTraces=...in jsLIGO.Map.add(...: Add a key, value entry to a map. For more information about Map.export type storage = {...};aRecordtype is declared, it is an object structure known in LIGO as a Record.Tezos.get_self_address()is a native function that returns the current contract address running this code. Have a look at Tezos native functions.feedback: "": poking directly does not store feedback.
-
Edit
pokeGame.storageList.jsligoto change the storage initialization.#import "pokeGame.jsligo" "Contract"
const default_storage: Contract.storage = {
pokeTraces: Map.empty as map<address, Contract.pokeMessage>,
feedback: "kiss"
}; -
Compile your contract.
TAQ_LIGO_IMAGE=ligolang/ligo:1.6.0 taq compile pokeGame.jsligoWrite a second function
pokeAndGetFeedbackinvolving the call to another contract a bit later, let's do unit testing first!
Write unit tests
-
Add a new unit test smart-contract file
unit_pokeGame.jsligo.taq create contract unit_pokeGame.jsligoℹ️ Testing documentation can be found here
-
Edit the file.
#import "./pokeGame.jsligo" "PokeGame"
export type main_fn = module_contract<parameter_of PokeGame, PokeGame.storage>;
// reset state
const _ = Test.reset_state(2 as nat, list([]) as list<tez>);
const faucet = Test.nth_bootstrap_account(0);
const sender1: address = Test.nth_bootstrap_account(1);
const _2 = Test.log("Sender 1 has balance : ");
const _3 = Test.log(Test.get_balance_of_address(sender1));
const _4 = Test.set_baker(faucet);
const _5 = Test.set_source(faucet);
export const initial_storage = {
pokeTraces: Map.empty as map<address, PokeGame.pokeMessage>,
feedback: "kiss"
};
export const initial_tez = 0mutez;
//functions
export const _testPoke = (
taddr: typed_address<parameter_of PokeGame, PokeGame.storage>,
s: address
): unit => {
const contr = Test.to_contract(taddr);
const contrAddress = Tezos.address(contr);
Test.log("contract deployed with values : ");
Test.log(contr);
Test.set_source(s);
const status = Test.transfer_to_contract(contr, Poke(), 0 as tez);
Test.log(status);
const store: PokeGame.storage = Test.get_storage(taddr);
Test.log(store);
//check poke is registered
match(Map.find_opt(s, store.pokeTraces)) {
when (Some(pokeMessage)):
do {
assert_with_error(
pokeMessage.feedback == "",
"feedback " + pokeMessage.feedback + " is not equal to expected "
+ "(empty)"
);
assert_with_error(
pokeMessage.receiver == contrAddress,
"receiver is not equal"
);
}
when (None()):
assert_with_error(false, "don't find traces")
};
};
// TESTS //
const testSender1Poke =
(
(): unit => {
const orig =
Test.originate(contract_of(PokeGame), initial_storage, initial_tez);
_testPoke(orig.addr, sender1);
}
)();Explanations:
#import "./pokeGame.jsligo" "PokeGame"to import the source file as a module to call functions and use object definitions.export type main_fnit will be useful later for the mutation tests to point to the main function to call/mutate.Test.reset_state ( 2...this creates two implicit accounts on the test environment.Test.nth_bootstrap_accountThis returns the nth account from the environment.Test.to_contract(taddr)andTezos.address(contr)are util functions to convert typed addresses, contract, and contract addresses.let _testPoke = (s : address) : unit => {...}declaring function starting with_is escaping the test for execution. Use this to factorize tests changing only the parameters of the function for different scenarios.Test.set_sourcedo not forget to set this value for the transaction signer.Test.transfer_to_contract(CONTRACT, PARAMS, TEZ_COST)A transaction to send, it returns an operation.Test.get_storageThis is how to retrieve the contract's storage.assert_with_error(CONDITION,MESSAGE)Use assertion for unit testing.const testSender1Poke = ...This test function will be part of the execution report.Test.originate_module(MODULE_CONVERTED_TO_CONTRACT,INIT_STORAGE, INIT_BALANCE)It originates a smart contract into the Test environment. A module is converted to a smart contract.
-
Run the test
TAQ_LIGO_IMAGE=ligolang/ligo:1.6.0 taq test unit_pokeGame.jsligoThe output should give you intermediary logs and finally the test results.
┌──────────────────────┬────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Contract │ Test Results │
├──────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── ─────────────┤
│ unit_pokeGame.jsligo │ "Sender 1 has balance : " │
│ │ 3800000000000mutez │
│ │ "contract deployed with values : " │
│ │ KT1KwMWUjU6jYyLCTWpZAtT634Vai7paUnRN(None) │
│ │ Success (2130n) │
│ │ {feedback = "kiss" ; pokeTraces = [tz1TDZG4vFoA2xutZMYauUnS4HVucnAGQSpZ -> {feedback = "" ; receiver = KT1KwMWUjU6jYyLCTWpZAtT634Vai7paUnRN}]} │
│ │ Everything at the top-level was executed. │