SanchezDario
Posted on February 12, 2022
Initial clarifications:
- The following article is focused on the development of smart contracts using Rust.
- Although the examples are developed in NEAR, much of the following information can be transposed to any other blockhain where it can be developed using Rust.
- The examples are taken from the following repository: BlockJobs-Marketplace, where several cross-contract calls can be seen in addition to those used below.
- This article shows a cross-contract call from a professional services marketplace, to the DAI contract, to allow payments for services in DAI or others fungible tokens.
Introduction:
There are various reasons for requiring calls between contracts, such as requesting and/or modifying information, using code from a third-party contract without copying it, transferring tokens, etc. and this can be done by working in a similar way to how API calls work.
An example of this is shown below, providing a contextual explanation of how a cross-contract call works.
1- Calling contract
To be able to call an external function, the first thing to do is to indicate to our contract what information the function that will be called requires, specifying parameters with their respective data types (in several contracts it is seen that &self
is added and the type of data to return, but this can be omitted). For this you must first import near_sdk::ext_contract
and implement it as shown below.
use near_sdk::ext_contract;
impl Contract {
…
}
#[ext_contract(ext_contract)]
trait ExtContract {
fn ft_transfer(&mut self, receiver_id: AccountId, amount: U128, memo: Option<String>);
}
#[ext_contract(ext_self)]
pub trait ExtSelf {
fn on_buy_service(service_id: u64) -> Service;
}
- The 'ext_contract' in parentheses is the name that is being given and will then be used to indicate which trait is being called, since more than one can be set in order to have a better order.
- 'ExtContract' is a name that is not required later, that is, it is indifferent.
- If the function that is called or not, is public, it is also indifferent, placing 'ft' is enough.
- The name of the function and the parameters must be exactly the same as those of the contract that is called, in the event of any typing error we will receive an "insufficient gas amount" error.
- The trait ExtSelf will be explained later.
1.1 - Make the call
Within the corresponding function, the call is made as follows:
ext_contract::ft_transfer(
self.contract_me.clone(),
service.metadata.price.into(),
None,
&token,
ONE_YOCTO,
GAS_FT_TRANSFER
).then(ext_self::on_buy_service(
service_id,
&env::current_account_id(),
NO_DEPOSIT,
BASE_GAS)
);
-
ft_transfer
is being called passing first the 3 corresponding parameters, then the address of the contract (the DAI address must be passed in this case, which is assigned totoken
), then the amount of NEAR attached to the in the case of a payable type function and finally the amount of gas, a topic that will be expanded on later. The order of the parameters must be strictly respected, otherwise it will fail and the compiler does not always show an error. - The deposit and the gas are in this case assigned in constants, but the numerical value can also be specified directly.
- The
then
method can be omitted, it is used in the case of wanting to execute a second function depending on what the called function returns, this is shown in more detail below.
1.2 - Call back
A second function can be created in the contract that makes the call, which, based on what is returned, executes certain actions. It is not strictly necessary but it is normally called with an on preceding the name of the first function, in the example case the function from which it is initially called is buy_service
, so now the name will be on_buy_service
and is given by the following code:
pub fn on_buy_service(&mut self, service_id: u64) -> Service {
match env::promise_result(0) {
PromiseResult::Successful(_data) => {
service.sold = true;
service.on_sale = false;
self.service_by_id.insert(&service_id, &service);
return service;
}
PromiseResult::Failed => env::panic(b"Callback faild"),
PromiseResult::NotReady => env::panic(b"Callback faild"),
};
}
- In the event of an error in the call, a panic type error is executed with the message "Callback failed", if the call has been executed correctly, the code that modifies the state of the contract that initially calls is executed (this could done directly in the first function without the
then
method, but this is good practice. - In this case, the value that is returned is not being used, which is why the
data
parameter has a "_" in front of it, preventing a warning from being issued, but this value could be necessary for the status modification. - In the initially shared repository you can also see examples where this is omitted.
- Although it is not used in this example, it can be useful to return a promise, for which
near_sdk::PromiseOrValue
must be imported, in this link expands on this topic.
2 - Called contract
The called contract can be one of its own or another, it is only necessary to know the address and the method to call with its parameters.
In the example case, the transfer function of the DAI contract is being called, so if the transfer is executed correctly, on_buy_service
is subsequently executed and the status modified as indicated. However, the returned value could be other data such as a boolean for example and from this modify the information. In the example code you can see this by going to the validate_user
and validate_tokens
functions, both of which are in the mediator.rs
contract.
3 - Gas Management
The most common error is "Insufficient gas amount", which can be due to an insufficient amount of gas set or attached, but also due to an error that prevents the called function from finishing executing and returning a result. In the event that this happens, it may be a good idea to issue logs at the beginning and end of each function to see how far the execution is carried out and also to comment the code where state changes are made to verify if the problem is in the call or in the internal code of some of the functions.
If the error is not found by performing the aforementioned, it may be that an amount of gas greater than the maximum supported by Near, which is 300 Tgas, is being established, if in the example 200 Tgas had been passed as a parameter to execute ft_transfer
and 120 Tgas for on_buy_service
, those 300 would never be enough, or in the case of working on testnet, it may be that not enough gas is being attached. For testing it may be advisable to specify the maximum amount of gas by adding --gas 300000000000000
to the end of the function call. Then the remaining gas is returned to the person who signs the transaction, but if you prefer this value can be adjusted by seeing how much gas is consumed, it is advisable to always indicate a higher amount, perhaps twice as much to ensure that it is not insufficient.
Final clarifications
- Remember that it is advisable to write tests for the functions and the calls between contracts that are made.
- To view information pertaining to gas usage and the values returned by the functions, you can use Near Explorer.
- For questions, we recommend StackOverflow, or the Telegram channel for devs.
I hope this information has been useful to you.
Posted on February 12, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.