Skip to main content

Cross Contract Calls and Receipts

A cross contract call happens when a smart contract invokes a method on another smart contract. This ability allows a developer to create complicated interactions by composing various smart contracts together. In order to make this composability easier for developers, NEAR introduces promises. Promises allow developers to synchronize cross contract calls using familiar .then syntax.

Since NEAR is a sharded blockchain, much of this is accomplished by something akin to the Actor Model. While making cross contract calls, we are sending messages (ActionReceipt) to other contracts. When the other contract finishes, they send back a message (DataReceipt) containing returned data. This returned data can be processed by registering a callback using the .then syntax.

Prerequisites

Terminology

  • Runtime - the layer responsible for running smart contracts. It converts transactions into receipts and processes receipts. More info here
  • Receipt - messages passed between blocks. More info here
  • ActionReceipt - a Receipt used to apply some actions to a receiver (such as apply a smart contract method). More info here
  • DataReceipt - represents the final contract execution result . More info here

Calling Smart Contract Methods

Since NEAR is a sharded blockchain, the runtime packages the call from A to B into an ActionReceipt. At the same time, the shard containing A registers a callback by creating a pending ActionReceipt. On the next block, the shard containing B will process the ActionReceipt from A invoking the method on B. It will then take the returned value from B and package it into a DataReceipt. Then, on the next block, the shard containing A will process the DataReceipt from B and trigger the pending ActionReceipt from earlier, invoking the registered callback.

  1. Contract A calls contract B
  2. The runtime prepares the cross contract call by:
    • creating an ActionReceipt to send to the shard containing B
    • creating a pending ActionReceipt that it stores locally
  3. On the next block, the shard containing B:
    • processes the ActionReceipt invoking the method on B
    • takes the return value from B and packages it into a DataReceipt
  4. On the next block, the shard containing A triggers the pending ActionReceipt with the data from B
// define the methods we'll use on ContractB
#[ext_contract(ext_contract_b)]
pub trait ContractB {
fn method_on_b(&mut self, arg_1: String) -> U128;
}

// define methods we'll use as callbacks on ContractA
#[ext_contract(ext_self)]
pub trait ContractA {
fn my_callback(&self) -> String;
}

// Inside a contract function on ContractA, a cross contract call is started
// From ContractA to ContractB
ext_contract_b::method_on_b(
"arg_1".to_string(),
&"contract-b.near", // contract account id
0, // yocto NEAR to attach
5_000_000_000_000 // gas to attach
)
// When the cross contract call from A to B finishes the my_callback method is triggered.
// Since my_callback is a callback, it will have access to the returned data from B
.then(ext_self::my_callback(
&env::current_account_id(), // this contract's account id
0, // yocto NEAR to attach to the callback
5_000_000_000_000 // gas to attach to the callback
))

Common Patterns

Callback Pattern

The callback pattern is used when contract A calls contract B and wants to do something with the data returned from B. In the following example, Contract B is a Fungible Token contract. Contract A makes a cross contract call to contract B to check the balance of an account and registers a callback. If the cross contract call fails the callback returns oops!. If the cross contract call is successful and the balance is > 100000 the callback returns Wow!, otherwise it returns Hmmmm.

use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize};
use near_sdk::json_types::{U128, ValidAccountId};
use near_sdk::{env, ext_contract, near_bindgen, Promise, PromiseResult};

near_sdk::setup_alloc!();

// define the methods we'll use on the other contract
#[ext_contract(ext_ft)]
pub trait FungibleToken {
fn ft_balance_of(&mut self, account_id: AccountId) -> U128;
}

// define methods we'll use as callbacks on our contract
#[ext_contract(ext_self)]
pub trait MyContract {
fn my_callback(&self) -> String;
}

#[near_bindgen]
#[derive(Default, BorshDeserialize, BorshSerialize)]
pub struct Contract {}

#[near_bindgen]
impl Contract {
pub fn my_first_cross_contract_call(&self, account_id: ValidAccountId) -> Promise {
// Invoke a method on another contract
// This will send an ActionReceipt to the shard where the contract lives.
ext_ft::ft_balance_of(
account_id.into(),
&"banana.ft-fin.testnet", // contract account id
0, // yocto NEAR to attach
5_000_000_000_000 // gas to attach
)
// After the smart contract method finishes a DataReceipt will be sent back
// .then registers a method to handle that incoming DataReceipt
.then(ext_self::my_callback(
&env::current_account_id(), // this contract's account id
0, // yocto NEAR to attach to the callback
5_000_000_000_000 // gas to attach to the callback
))
}

pub fn my_callback(&self) -> String {
assert_eq!(
env::promise_results_count(),
1,
"This is a callback method"
);

// handle the result from the cross contract call this method is a callback for
match env::promise_result(0) {
PromiseResult::NotReady => unreachable!(),
PromiseResult::Failed => "oops!".to_string(),
PromiseResult::Successful(result) => {
let balance = near_sdk::serde_json::from_slice::<U128>(&result).unwrap();
if balance.0 > 100000 {
"Wow!".to_string()
} else {
"Hmmmm".to_string()
}
},
}
}
}

Event Pattern

The event pattern is used when a contract EventPublisher defines a method with an event_subscriber_id parameter. When that method is called, the EventPublisher makes a cross contract call to the event_subscriber_id smart contract invoking an event handler on that contract. For example, a VotingContract (EventPublisher) allows people to candidate_vote_call on a candidate. When a candidate is voted for the candidate's smart contract candidate_on_vote method (event handler) will be invoked.

use near_sdk::collections::LookupMap;
use near_sdk::json_types::{ValidAccountId, U128};
use near_sdk::{
assert_one_yocto,
borsh::{self, BorshDeserialize, BorshSerialize},
};
use near_sdk::{env, ext_contract, near_bindgen, AccountId, PanicOnDefault, Promise};

near_sdk::setup_alloc!();

// define the methods we'll use on the other contract
#[ext_contract(ext_candidate)]
pub trait CandidateContract {
fn candidate_on_vote(&self, voter_id: AccountId, total_votes: U128) -> String;
}

#[near_bindgen]
#[derive(BorshDeserialize, BorshSerialize, PanicOnDefault)]
pub struct Contract {
votes: LookupMap<AccountId, u128>,
}

#[near_bindgen]
impl Contract {
// Event Pattern Method
// usually methods following the Event Pattern are named *_call.
// this indicates that they will make a cross contract **call** to
// a smart contract specified by the arguments passed to this method.
// In this case, the candidate_id is an address to a smart contract.
#[payable]
pub fn candidate_vote_call(&mut self, candidate_id: ValidAccountId) -> Promise {
assert_one_yocto();

let candidate_id = candidate_id.as_ref();
assert!(
self.votes.contains_key(candidate_id),
"Candidate has not been nominated"
);

// update total votes for a candidate
let total_votes = self.votes.get(candidate_id).unwrap() + 1;
self.votes.insert(candidate_id, &total_votes);

// after votes have been updated make a cross contract call to the candidate_id contract
ext_candidate::candidate_on_vote(
env::predecessor_account_id(), // voter AccountId
total_votes.into(), // total votes for candidate
candidate_id, // contract AccountId for candidate
0, // attached yocto NEAR
5_000_000_000_000, // attached gas
)
}

#[init]
pub fn new() -> Self {
Self {
votes: LookupMap::new(b"p".to_vec()),
}
}

pub fn nominate(&mut self, candidate_id: ValidAccountId) {
let candidate_id = candidate_id.as_ref();
assert!(
!self.votes.contains_key(candidate_id),
"Candidate has already been nominated"
);

self.votes.insert(candidate_id, &0);
}
}

When using the event pattern, there are some conventions to follow. On the EventPublisher method:

  • prefixed with a contract identifier (e.g. candidate_, ft_, nft_)
  • suffixed with _call (e.g. candidate_vote_call, ft_transfer_call, nft_transfer_call)

On the other side of the EventPublisher is an EventSubscriber contract. This contract has to define a predetermined method that will be invoked by a cross contract call from the EventPublisher contract. In the above code you'll notice that the candidate_vote_call method makes a cross contract call to a candidate_on_vote method. This candidate_on_vote method has to be defined by the EventSubscriber contract.

use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize};
use near_sdk::json_types::{ValidAccountId, U128};
use near_sdk::{env, near_bindgen};

near_sdk::setup_alloc!();

#[near_bindgen]
#[derive(BorshDeserialize, BorshSerialize, Default)]
pub struct CandidateContract {}

#[near_bindgen]
impl CandidateContract {
pub fn candidate_on_vote(voter_id: ValidAccountId, total_votes: U128) -> String {
assert_eq!(
env::predecessor_account_id(),
"VotingContract_ID",
"Only the candidate voting contract can call this method"
);

"Thanks for voting for me! I look forward to serving".to_string()
}
}

On the EventSubscriber method:

  • prefixed with a contract identifier (e.g. candidate_, ft_, nft_)
  • followed by on and then the name of the event it handles (e.g. candidate_on_vote, ft_on_transfer, nft_on_transfer)

Common Use Cases

Both the Fungible Token Standard (NEP-141) and the Non-Fungible Token Standard (NEP-171) make use of the Callback Pattern and the Event Pattern.

Fungible Token Standard

NEP-141 defines a ft_transfer_call method that transfers tokens from the sender to a receiver. As the name suggests, this method is an event publisher (_call is our clue). The receiver contract is expected to define a method, ft_on_transfer, which should return the number of unused tokens.

The ft_transfer_call method will make a cross contract call to the ft_on_transfer method, it will also register a callback, ft_resolve_transfer, that will take the unused tokens returned by ft_on_transfer and refund those tokens back to the sender.

Non-Fungible Token Standard

Similarly, the Non-Fungible Token Standard (NEP-171) defines a method nft_transfer_call. Again, the name suggests that this is an event publisher (_call is our clue).

  1. a sender invokes the nft_transfer_call method to send an NFT to a receiver
  2. the nft_transfer_call method transfers the NFT from sender to receiver
  3. after the transfer a cross contract call is started
    • an ActionReceipt is created to call the nft_on_transfer method on the receiver contract
    • a callback nft_resolve_transfer is registered by creating a pending ActionReceipt
  4. on the next block, the nft_on_transfer method is executed on the receiver contract and a DataReceipt is created
  5. on the next block, the pending ActionReceipt from above is ready and the nft_resolve_transfer callback is executed

Further Resources