Skip to main content

Rust Cross Contract Calls Explored

When a signed transaction comes into the runtime, it is converted into an ActionReceipt (code here). Next, the runtime processes the ActionReceipt and then applies each action inside the receipt (code here).

If the action is a function call, then the function is run (code here) with the near-vm-logic (NEAR bindings) injected (See the Runtime Diagram and Bindings Specification).

When a cross contract call is made, the env::promise_batch_create function is invoked and a new ActionReceipt is created (code here).

When a callback is registered, the env::promise_batch_then function is invoked and another ActionReceipt is created (code here). This time, however, the ActionReceipt is created with receipt_dependencies, which means the ActionReceipt will be postponed until an associated DataReceipt is received (code here).

Low Level

We can see this process a bit clearer if we use the low-level Promise Bindings to make cross contract calls.

pub fn my_method(&self) {
// Create a new promise, which will create a new (empty) ActionReceipt
let promise_id = env::promise_batch_create(
"wrap.testnet".to_string(), // the recipient of this ActionReceipt (contract account id)
);

// attach a function call action to the ActionReceipt
env::promise_batch_action_function_call(
promise_id, // associate the function call with the above Receipt via promise_id
b"ft_balance_of", // the function call will invoke the ft_balance_of method on the wrap.testnet
&json!({ "account_id": "rnm.testnet".to_string() }) // method arguments
.to_string()
.into_bytes(),
0, // amount of yoctoNEAR to attach
5_000_000_000_000, // gas to attach
);

// Create another promise, which will create another (empty) ActionReceipt.
// This time, the ActionReceipt is dependent on the previous receipt
let callback_promise_id = env::promise_batch_then(
promise_id, // postpone until a DataReceipt associated with promise_id is received
env::current_account_id(), // the recipient of this ActionReceipt (&self)
);

// attach a function call action to the ActionReceipt
env::promise_batch_action_function_call(
callback_promise_id, // associate the function call with callback_promise_id
b"my_callback", // the function call will be a callback function
b"{}", // method arguments
0, // amount of yoctoNEAR to attach
5_000_000_000_000, // gas to attach
);

// return the resulting DataReceipt from callback_promise_id as the result of this function
env::promise_return(callback_promise_id);
}

Mid Level

near-sdk-rs provides some intermediate syntax that can help abstract away from all the low level Promise Bindings (SDK Promise Documentation). Internally env::promise_batch_create and env::promise_batch_then are still being used (code here).

pub fn my_method(&self) -> Promise {
// Create a new promise, which will create a new (empty) ActionReceipt
// Internally this will use env:promise_batch_create
let cross_contract_call = Promise::new(
"wrap.testnet".to_string(), // the recipient of this ActionReceipt (contract account id)
)
// attach a function call action to the ActionReceipt
.function_call(
b"ft_balance_of".to_vec(), // the function call will invoke the ft_balance_of method on the wrap.testnet
json!({ "account_id": "rnm.testnet".to_string() }) // method arguments
.to_string()
.into_bytes(),
0, // amount of yoctoNEAR to attach
5_000_000_000_000, // gas to attach
);

// Create another promise, which will create another (empty) ActionReceipt.
let callback = Promise::new(
env::current_account_id(), // the recipient of this ActionReceipt (&self)
)
.function_call(
b"my_callback".to_vec(), // the function call will be a callback function
b"{}".to_vec(), // method arguments
0, // amount of yoctoNEAR to attach
5_000_000_000_000, // gas to attach
);

// Make the callback ActionReceipt dependent on the cross_contract_call ActionReceipt
// callback will now remain postponed until cross_contract_call finishes
cross_contract_call.then(callback)
}

High Level

Ultimately, near-sdk-rs abstracts away from all the internal Receipt and Promise details. Instead, using the ext_contract macro a developer can define the interface of a contract and then use that interface to make cross contract calls. Under the hood, the Promise::new method is used to create a Promise and eventually create an ActionReceipt with either env::promise_batch_create or env::promise_batch_then (code here).

// define an interface for the other contract
#[ext_contract(ext_ft)]
trait FungibleToken {
fn ft_balance_of(&self, account_id: String) -> U128;
}

// define an interface for callbacks
#[ext_contract(ext_self)]
trait SelfContract {
fn my_callback(&self) -> String;
}

// ...

pub fn my_method_high(&self) -> Promise {
// This creates a new ActionReceipt with a function call action
// Ultimately this uses env:promise_batch_create via Promise::new
ext_ft::ft_balance_of(
"rnm.testnet".to_string(), // method arguments (ft_balance_of takes an account id)
&"wrap.testnet", // the recipient of this ActionReceipt (contract account id)
0, // amount of yoctoNEAR to attach
5_000_000_000_000, // gas to attach
)
// Make the ext_self::my_callback ActionReceipt dependent on the ext_ft::ft_balance_of ActionReceipt
.then(ext_self::my_callback(
&env::current_account_id(),
0, // amount of yoctoNEAR to attach
5_000_000_000_000, // gas to attach
))
}