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_nearupon 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
VMContextand createRuntimeExtwhich 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::runwhich 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_tripon the smart contract.- During the execution of the smart contract it will at some point call
promise_createandpromise_then, which will call method onRuntimeExtthat will record that two promises were created and that the second one should wait on the first one. Specifically,promise_createwill callRuntimeExt::create_receipt(vec![], "hotel_near")returning0and thenRuntimeExt::create_receipt(vec![0], "travel_agency");
- During the execution of the smart contract it will at some point call
action_function_callthen collects receipts fromVMContextalong with the execution result, logs, and information about used gas;apply_action_receiptthen goes over the collected receipts from each action and returns them at the end ofRuntime::applytogether 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.