Cross-Contract Call
This guide assumes that you have read the Financial Transaction section.
Suppose Alice is a calling a function reserve_trip(city: String, date: u64)
on a smart contract deployed to a travel_agency
account which in turn calls reserve(date: u64)
on a smart contract deployed to a hotel_near
account and attaches
a callback to method hotel_reservation_complete(date: u64)
on travel_agency
.
Pre-requisites
It possible for Alice to call the travel_agency
in several different ways.
In the simplest scenario Alice has an account alice_near
and she has a full access key.
She then composes the following transaction that calls the travel_agency
:
Transaction {
signer_id: "alice_near",
public_key: "ed25519:32zVgoqtuyRuDvSMZjWQ774kK36UTwuGRZMmPsS6xpMy",
nonce: 57,
receiver_id: "travel_agency",
block_hash: "CjNSmWXTWhC3EhRVtqLhRmWMTkRbU96wUACqxMtV1uGf",
actions: vec![
Action::FunctionCall(FunctionCallAction {
method_name: "reserve_trip",
args: "{\"city\": \"Venice\", \"date\": 20191201}",
gas: 1000000,
tokens: 100,
})
],
}
Here the public key corresponds to the full access key of alice_near
account. All other fields in Transaction
were
discussed in the Financial Transaction section. The FunctionCallAction
action describes how
the contract should be called. The receiver_id
field in Transaction
already establishes what contract should be executed,
FunctionCallAction
merely describes how it should be executed. Interestingly, the arguments is just a blob of bytes,
it is up to the contract developer what serialization format they choose for their arguments. In this example, the contract
developer has chosen to use JSON and so the tool that Alice uses to compose this transaction is expected to use JSON too
to pass the arguments. gas
declares how much gas alice_near
has prepaid for dynamically calculated fees of the smart
contract executions and other actions that this transaction may spawn. The tokens
is the amount of alice_near
attaches
to be deposited to whatever smart contract that it is calling to. Notice, gas
and tokens
are in different units of
measurement.
Now, consider a slightly more complex scenario. In this scenario Alice uses a restricted access key to call the function.
That is the permission of the access key is not AccessKeyPermission::FullAccess
but is instead: AccessKeyPermission::FunctionCall(FunctionCallPermission)
where
FunctionCallPermission {
allowance: Some(3000),
receiver_id: "travel_agency",
method_names: [ "reserve_trip", "cancel_trip" ]
}
This scenario might arise when someone Alice's parent has given them a restricted access to alice_near
account by
creating an access key that can be used strictly for trip management.
This access key allows up to 3000
tokens to be spent (which includes token transfers and payments for gas), it can
be only used to call travel_agency
and it can be only used with the reserve_trip
and cancel_trip
methods.
The way runtime treats this case is almost exactly the same as the previous one, with the only difference on how it verifies
the signature of on the signed transaction, and that it also checks for allowance to not be exceeded.
Finally, in the last scenario, Alice does not have an account (or the existence of alice_near
is irrelevant). However,
alice has full or restricted access key directly on travel_agency
account. In that case signer_id == receiver_id
in the
Transaction
object and runtime will convert transaction to the first receipt and apply that receipt in the same block.
This section will focus on the first scenario, since the other two are the same with some minor differences.
Transaction to receipt
The process of converting transaction to receipt is very similar to the Financial Transaction with several key points to note:
- Since Alice attaches 100 tokens to the function call, we subtract them from
alice_near
upon converting transaction to receipt, similar to the regular financial transaction; - Since we are attaching 1000000 prepaid gas, we will not only subtract the gas costs of processing the receipt from
alice_near
, but will also purchase 1000000 gas using the current gas price.
Processing the reserve_trip
receipt
The receipt created on the shard that hosts alice_near
will eventually arrive to the shard hosting travel_agency
account.
It will be processed in Runtime::apply
which will check that receipt does not have data dependencies (which is the case because
this function call is not a callback) and will call Runtime::apply_action_receipt
.
At this point receipt processing is similar to receipt processing from the Financial Transaction
section, with one difference that we will also call action_function_call
which will do the following:
- Retrieve the Wasm code of the smart contract (either from the database or from the cache);
- Initialize runtime context through
VMContext
and createRuntimeExt
which provides access to the trie when the smart contract call the storage API. Specifically"{\"city\": \"Venice\", \"date\": 20191201}"
arguments will be set inVMContext
. - Calls
near_vm_runner::run
which does the following:- Inject gas, stack, and other kinds of metering;
- Verify that Wasm code does not use floats;
- Checks that bindings API functions that the smart contract is trying to call are actually those provided by
near_vm_logic
; - Compiles Wasm code into the native binary;
- Calls
reserve_trip
on the smart contract.- During the execution of the smart contract it will at some point call
promise_create
andpromise_then
, which will call method onRuntimeExt
that will record that two promises were created and that the second one should wait on the first one. Specifically,promise_create
will callRuntimeExt::create_receipt(vec![], "hotel_near")
returning0
and thenRuntimeExt::create_receipt(vec![0], "travel_agency")
;
- During the execution of the smart contract it will at some point call
action_function_call
then collects receipts fromVMContext
along with the execution result, logs, and information about used gas;apply_action_receipt
then goes over the collected receipts from each action and returns them at the end ofRuntime::apply
together with other receipts.
Processing the reserve
receipt
This receipt will have output_data_receivers
with one element corresponding to the receipt that calls hotel_reservation_complete
,
which will tell the runtime that it should create DataReceipt
and send it towards travel_agency
once the execution of reserve(date: u64)
is complete.
The rest of the smart contract execution is similar to the above.
Processing the hotel_reservation_complete
receipt
Upon receiving the hotel_reservation_complete
receipt the runtime will notice that its input_data_ids
is not empty
which means that it cannot be executed until reserve
receipt is complete. It will store the receipt in the trie together
with the counter of how many DataReceipt
it is waiting on.
It will not call the Wasm smart contract at this point.
Processing the DataReceipt
Once the runtime receives the DataReceipt
it takes the receipt with hotel_reservation_complete
function call
and executes it following the same execution steps as with the reserve_trip
receipt.