-
Notifications
You must be signed in to change notification settings - Fork 26
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[WIP] reference impl of mocked corss-contract calls in ink unit tests #136
base: main
Are you sure you want to change the base?
Conversation
7c2c884
to
99ab889
Compare
78c2d41
to
69c2f5e
Compare
I thought about that idea and came up with the following solution: We will create a separate repository with a forked extracted The engine will have several traits that will be automatically implemented by OpenBrush(via an additional feature aka "advanced-test-engine" or a separate argument of If someone wants to do a cross-contract call, he should register(deploy or instantiate) a contract first via util functions provided by the new engine. The contract only should implement those new traits to be able to be registered. The calls will be done in the same way via To use a new |
Sounds perfect.
I'm not sure if the ink macro generates the the dispatch function (given selector and call data, call the corresponding rust functions). If yes, then we can even keep the
Agree. |
This is a reference PR to enable cross-contract call in unit tests. I will try to improve it and finally integrate to openbrush in the future.
Usage
Only call contract via
openbrush::trait_definition
. Calling via ink!'sContractRef
or the CallBuiler is not supported.When declaring the
trait_definition
, add amock = <your contract>
attribute to the macro:In this case,
fat_badges::FatBadges
is the real ink contract struct with the messagefn issue(...)
implemented. We defined a trait calledIssuable
because we want to callfn issue()
in our contract. The mock object must implement all the message you declared in thetrait_definition
. Otherwise the code generated by the macro will not compile.Call the contract ref as usual:
In your unit test, you need to restructure your code as below:
Note that each
#[openbrush::trait_definition]
will generate a correspondingmock_<trait-name>
module. If there are more traits, you will need to call nestedusing
multiple times. Heremock_issuable
is generated bytrait Issuable
.Your contracts are either deployed by
mock_<trait-name>::deploy()
orAddressable::create_native
. In both case you get anAddressable<T>
object that allows you to call the contract with the call stack managed properly:If you define the
#[openbrush::trait_definition]
in a remote crate, you need to enablefeature = mockable
in that crate.Full Example
Check
trait Issuable
andfn end_to_end()
:How it works
ink! doesn't support cross-contract call in unit tests because:
Therefore the idea is to:
It turns out
#[openbrush::trait_definition]
is a perfect place to inject code. It generates the stub functions to do the actual corss-contract calls as you defined. These functions invoke the ink syscall to initiate the cross-contract calls. However, in a unit test, we just want to call the target contract object. To simulate the cross-contract call, we also want thecaller
andcallee
updated properly. This can be easily done via a#[cfg(test)]
switch.Another problem is to manage the contract address register and the call states. This is as easy as creating a map from the contract address and the object, and a call stack in the unit test function. However, to make it easily accessible in the contract and the
trait_definition
stub functions, we need to useenvironmental
crate. It loads the states to a global variable safely, and allows the access in the very deep functions.To make everything easy, we have created a few types:
Addressable<T>
: A wrapper struct around a contract. It stores the address of the contract, and a reference to the call stack. Whenever you invoke the contract, you will calla.call()
ora.call_mut()
, which pushes the contract address to the stack and pop it out in the lifecycle of the returned reference object.ManagedCallStack
: Implements a call stack, and updates the ink! test env (caller
andcallee
) accordinglyWe generate a
mod mock_<trait-name>
for each defined trait and provides the following functions:deploy(contract)
: Deploy a plain contract, allocate an address, and insert it to the register map. Finally it returns you aAddressable<T>
.using(call_stack, lambda)
: Load aManagedCallStack
to the inner global variable in the scope oflambda
. Only within thelambda
the cross-contract call are allowed.