On ext_contract

A.K.A. Callbacks.

One used to have some problem with this, because one was trying to find a trait but cannot find it. Here, we shall discuss how to use ext_contract and "why some code cannot be found from import". We discuss about a code and see how the logic goes. And finally, we discuss some confusing elements at the final part of this page.

This applies if you haven't really get deep into Rust (like myself). That is, you don't really know what the code is trying to do.

Let's look at this from the resource example:

use near_sdk::ext_contract;

#[ext_contract(ext_calculator)]
trait Calculator {
  fn mult(&self, a: U64, b: U64) -> U128;

  fn sum(&self, a: U128, b: U128) -> U128;
}

At first, one thought the line #[ext_contract(ext_calculator)] is a wrapper that pass in the trait to some wrapper.

Well, ext_contract is indeed a wrapper, but ext_calculator isn't. ext_calculator is, after the trait is being wrapped by ext_contract, you can call the trait with ext_calculator. Hence, if you want to find ext_calculator in near-sdk-rs or other imports but cannot find it, this is the reason.

And ext_contract just adds a few more variables to whatever you defined. These are:

  • receiver_id (of type &AccountId): The account calling the contract.
  • attached_deposit (of type Balance): did you deposit some NEAR to the cross contract call? Usually will be NO_DEPOSIT. And when you have deposit, the function needs to be #[payable] or it'll panic.
  • attached_gas (of type Gas): attached gas to pay the gas fee.

Conclusion

So whenever you call the function, make sure to add the extra three parameters in. Let's make some example:

const CONTRACT_ACCOUNT_WE_ARE_CALLING: &str = "calc.near";
const NO_DEPOSIT: Balance = 0;
const BASE_GAS: Gas = 5_000_000_000_000;

#[near_bindgen]
impl Contract {
  pub fn sum_a_b(&mut self, a: U128, b: U128) -> Promise {
    let deploy_acc: AccountId = CONTRACT_ACCOUNT_WE_ARE_CALLING.to_string();

    ext_calculator::sum(
      a,
      b,
      &deploy_acc,
      NO_DEPOSIT,
      BASE_GAS
    )
  }
}

Similarly for multiplication

pub fn mult_a_b(&mut self, a: U64, b: U64) -> Promise {
  let deploy_acc: AccountId = CONTRACT_ACCOUNT_WE_ARE_CALLING.to_string();

  ext_calculator::mult(
    a, 
    b,
    // extra params
    &deploy_acc,
    NO_DEPOSIT,
    BASE_GAS
  )
}

An example, a problem

Then let's look at a problem that's hard to understand.

fn nft_resolve_transfer(
        &mut self,
        owner_id: AccountId,
        receiver_id: AccountId,
        token_id: TokenId,
    ) -> bool {
        // whether receiver returns token back to sender, based on 
        // `nft_on_transfer` call result. 

        if let PromiseResult::Successful(value) = env::promise_result(0) {
          if let Ok(return_token) = near_sdk::serde_json::from_slice::<bool>(&value) {
            // don't need to return token, simply return true. 
            // everything went fine. 
            if !return_token {
              return true;
            }
          }
        }

        // get token object if got some token object
        let mut token = if let Some(token) = self.tokens_by_id.get(&token_id) {
          if token.owner_id != receiver_id {  // receiver_id is the receiver. 
            return true;  
          }
          token
        } else {  // no token object, it was burned. 
          return true;
        };

    // --snip--

Now the confusing part is this:

if token.owner_id != receiver_id

Wait, if we have token equals receiver, that means receiver already receive it, then why we need to return to the owner?

Finally one understand, the logic isn't with this line, but with the previous block of code:

if let PromiseResult::Successful(value) = env::promise_result(0) {

We had already pass this stage before, so that means if we gone through and it hadn't return true, the transaction wasn't successful. We want to resolve it by transferring the token back to the owner. Then it makes sense. Our token.owner_id is set to receiver_id, it needs to be returned to the previous_owner. That's why we want to set the value back, to allow the token to be transfer back to owner.

Alternatively, the token can be burnt. So, one guess the token is lost forever in this case? One isn't sure, though.

Also, this means that if we don't have the first code block of confirming that the PromiseResult is successful or not, then the code token.owner_id != receiver_id per se doesn't make sense. If you don't have the first code block checking for successful promise, we would assume to return true if token.owner_id == receiver_id, that means the transfer is "assumed to be successful". In fact, here, it's not "assumed to be not successful", so we flip the double equal sign as well.

Some confusing element

Note the calculator example uses receiver_id and it says calculator is deployed on calc.near. This has nothing wrong per se. This is because the cross-contract call is "to itself", meaning owner_id = receiver_id = "calc.near". So when both are the same, it doesn't really matter who's the caller and who's the host.

But of course, to the readers, owner_id and receiver_id isn't the best name to use. One suggests you use the following name when writing your code so as to not confuse your readers (or requires elaborated explanation at different phase):


#![allow(unused)]
fn main() {
contract_caller  // whoever calling the contract, hosted elsewhere

contract_host  // the account hosting the contract
}

These are easier to understand when someone reads your code. (Of course you can always use elaborated explanations. Make sure to put it near where you call it, or the reader won't be able to find out about it, or at least difficult to find out about it. The choice is yours).

Resource