Cross-Contract Calls
Your contract can interact with other deployed contracts, querying information and executing functions on them.
Since NEAR is a sharded blockchain, its cross-contract calls behave differently than calls do in other chains. In NEAR. cross-contract calls are asynchronous and independent.
You will need two independent functions: one to make the call, and another to receive the result
There is a delay between the call and the callback execution, usually of 1 or 2 blocks. During this time, the contract is still active and can receive other calls.
Snippet: Querying Information
While making your contract, it is likely that you will want to query information from another contract. Below, you can see a basic example in which we query the greeting message from our Hello NEAR example.
- 🌐 Javascript
- 🦀 Rust
- python
Loading...
- lib.rs
- external_contract.rs
- high_level.rs
- low_level.rs
Loading...
Loading...
Loading...
Loading...
from near_sdk_py import call, view, Contract, callback, PromiseResult, CrossContract, init
class CrossContractExample(Contract):
# Contract we want to interact with
hello_contract = "hello-near.testnet"
@init
def new(self):
"""Initialize the contract"""
# Any initialization logic goes here
pass
@view
def query_greeting_info(self):
"""View function showing how to make a cross-contract call"""
# Create a reference to the Hello NEAR contract
# This is a simple call that will execute in the current transaction
hello = CrossContract(self.hello_contract)
return hello.call("get_greeting").value()
@call
def query_greeting(self):
"""Calls Hello NEAR contract to get the greeting with a callback"""
# Create a reference to the Hello NEAR contract
hello = CrossContract(self.hello_contract)
# Call get_greeting and chain a callback
# The Promise API handles serialization and callback chaining
promise = hello.call("get_greeting").then("query_greeting_callback")
return promise.value()
@callback
def query_greeting_callback(self, result: PromiseResult):
"""Processes the greeting result from Hello NEAR contract"""
# The @callback decorator automatically parses the promise result
# result will have a data property and a success boolean
if not result.success:
return {"success": False, "message": "Failed to get greeting"}
return {
"success": True,
"greeting": result.data,
"message": f"Successfully got greeting: {result.data}"
}
Snippet: Sending Information
Calling another contract passing information is also a common scenario. Below you can see a function that interacts with the Hello NEAR example to change its greeting message.
- 🌐 Javascript
- 🦀 Rust
- python
Loading...
- lib.rs
- external_contract.rs
- high_level.rs
- low_level.rs
Loading...
Loading...
Loading...
Loading...
from near_sdk_py import call, Contract, callback, PromiseResult, CrossContract
class CrossContractExample(Contract):
# Contract we want to interact with
hello_contract = "hello-near.testnet"
@call
def change_greeting(self, new_greeting):
"""Changes the greeting on the Hello NEAR contract"""
# Create a reference to the Hello NEAR contract
hello = CrossContract(self.hello_contract)
# Create a promise to call set_greeting with the new greeting
# Pass context data to the callback directly as kwargs
promise = hello.call(
"set_greeting",
message=new_greeting
).then(
"change_greeting_callback",
original_greeting=new_greeting # Additional context passed to callback
)
return promise.value()
@callback
def change_greeting_callback(self, result: PromiseResult, original_greeting):
"""Processes the result of set_greeting"""
# The original_greeting parameter is passed from the change_greeting method
if not result.success:
return {
"success": False,
"message": f"Failed to set greeting to '{original_greeting}'"
}
return {
"success": True,
"message": f"Successfully set greeting to '{original_greeting}'",
"result": result.data
}
Promises
Cross-contract calls work by creating two promises in the network:
- A promise to execute code in the external contract (
Promise.create
) - Optional: A promise to call another function with the result (
Promise.then
)
Both promises will contain the following information:
- The address of the contract you want to interact with
- The function that you want to execute
- The (encoded) arguments to pass to the function
- The amount of GAS to use (deducted from the attached Gas)
- The amount of NEAR to attach (deducted from your contract's balance)
The callback can be made to any contract. Meaning that the result could potentially be handled by another contract
Creating a Cross Contract Call
To create a cross-contract call with a callback, create two promises and use the .then
method to link them:
- 🌐 JavaScript
- 🦀 Rust
- 🐍 Python
NearPromise.new("external_address").functionCall("function_name", JSON.stringify(arguments), DEPOSIT, GAS)
.then(
// this function is the callback
NearPromise.new(near.currentAccountId()).functionCall("callback_name", JSON.stringify(arguments), DEPOSIT, GAS)
);
There is a helper macro that allows you to make cross-contract calls with the syntax #[ext_contract(...)]
. It takes a Rust Trait and converts it to a module with static methods. Each of these static methods takes positional arguments defined by the Trait, then the receiver_id
, the attached deposit and the amount of gas and returns a new Promise
. That's the high-level way to make cross-contract calls.
#[ext_contract(external_trait)]
trait Contract {
fn function_name(&self, param1: T, param2: T) -> T;
}
external_trait::ext("external_address")
.with_attached_deposit(DEPOSIT)
.with_static_gas(GAS)
.function_name(arguments)
.then(
// this is the callback
Self::ext(env::current_account_id())
.with_attached_deposit(DEPOSIT)
.with_static_gas(GAS)
.callback_name(arguments)
);
There is another way to achieve the same result. You can create a new Promise
without using a helper macro. It's the low-level way to make cross-contract calls.
let arguments = json!({ "foo": "bar" })
.to_string()
.into_bytes();
let promise = Promise::new("external_address").function_call(
"function_name".to_owned(),
arguments,
DEPOSIT,
GAS
);
promise.then(
// Create a promise to callback query_greeting_callback
Self::ext(env::current_account_id())
.with_static_gas(GAS)
.callback_name(),
);
Gas
You can attach an unused GAS weight by specifying the .with_unused_gas_weight()
method but it is defaulted to 1. The unused GAS will be split amongst all the functions in the current execution depending on their weights. If there is only 1 function, any weight above 1 will result in all the unused GAS being attached to that function. If you specify a weight of 0, however, the unused GAS will not be attached to that function. If you have two functions, one with a weight of 3, and one with a weight of 1, the first function will get 3/4
of the unused GAS and the other function will get 1/4
of the unused GAS.
from near_sdk_py import Contract, Context, ONE_TGAS
# High-level Contract API (recommended)
CrossContract("external_address").call(
"function_name", # Method to call
arg1="value1", # Keyword arguments for the method
arg2="value2"
).then(
"callback_name", # Method name in this contract to use as callback
context_data="saved_for_callback" # Additional context data for the callback
).value()
# Lower-level Promise API
from near_sdk_py import Promise
Promise.create_batch("external_address").function_call(
"function_name",
{"arg1": "value1", "arg2": "value2"}, # Arguments as a dictionary
amount=0, # Deposit in yoctoNEAR
gas=5 * ONE_TGAS # Gas allowance
).then(
Context.current_account_id() # The contract to call for the callback
).function_call(
"callback_name", # Method name for callback
{"context_data": "saved_for_callback"} # Arguments for the callback
).value()
If a function returns a promise, then it will delegate the return value and status of transaction execution, but if you return a value or nothing, then the Promise
result will not influence the transaction status
The Promises you are creating will not execute immediately. In fact, they will be queued in the network an:
- The cross-contract call will execute 1 or 2 blocks after your function finishes correctly.
Callback Function
If your function finishes correctly, then eventually your callback function will execute. This will happen whether the external contract fails or not.
In the callback function you will have access to the result, which will contain the status of the external function (if it worked or not), and the values in case of success.
- 🌐 Javascript
- 🦀 Rust
- python
Loading...
Loading...
from near_sdk_py import callback, PromiseResult, Contract
class CrossContractExample(Contract):
@callback
def query_greeting_callback(self, result: PromiseResult, additional_context=None):
"""
Process the result of a cross-contract call.
The @callback decorator automatically:
1. Reads the promise result data
2. Handles serialization/deserialization
3. Provides proper error handling
Parameters:
- result: The PromiseResult object with status and data
- additional_context: Optional context passed from the calling function
"""
if not result.success:
# This means the external call failed or returned nothing
return {
"success": False,
"message": "Failed to get greeting",
"context": additional_context
}
# Process successful result
return {
"success": True,
"greeting": result.data,
"message": f"Successfully got greeting: {result.data}",
"context": additional_context
}
We repeat, if your function finishes correctly, then your callback will always execute. This will happen no matter if the external function finished correctly or not
Always make sure to have enough Gas for your callback function to execute
Remember to mark your callback function as private using macros/decorators, so it can only be called by the contract itself
What happens if the function I call fails?
If the external function fails (i.e. it panics), then your callback will be executed anyway. Here you need to manually rollback any changes made in your contract during the original call. Particularly:
- Refund the predecessor if needed: If the contract attached NEAR to the call, the funds are now back in the contract's account
- Revert any state changes: If the original function made any state changes (i.e. changed or stored data), you need to manually roll them back. They won't revert automatically
If your original function finishes correctly then the callback executes even if the external function panics. Your state will not rollback automatically, and $NEAR will not be returned to the signer automatically. Always make sure to check in the callback if the external function failed, and manually rollback any operation if necessary.
Concatenating Functions and Promises
✅ Promises can be concatenate using the .join
operator: P1.join([P2, P3], "callback")
: P1
, P2
, and P3
execute in parallel, after they finish, the callback will execute and have access to all their results
⛔ You cannot return a joint promise without a callback: return P1.join([P2])
is invalid since it misses the callback parameter
✅ You can concatenate then
promises: P1.then("callback1").then("callback2")
: P1
executes, then callback1 executes with the result of P1
, then callback2 executes with the result of callback1
⛔ You cannot use a join
within a then
: P1.then(P2.join([P3]))
is invalid
⛔ You cannot use a then
within a then
: P1.then(P2.then("callback"))
is invalid
Multiple Functions, Same Contract
You can call multiple functions in the same external contract, which is known as a batch call.
An important property of batch calls is that they act as a unit: they execute in the same receipt, and if any function fails, then they all get reverted.
- 🌐 JavaScript
- 🦀 Rust
- 🐍 Python
Loading...
Loading...
from near_sdk_py import call, Context, Contract, callback, PromiseResult, ONE_TGAS, CrossContract, init
class BatchCallsExample(Contract):
# Contract we want to interact with
hello_contract = "hello-near.testnet"
@init
def new(self):
"""Initialize the contract"""
pass
@call
def call_multiple_methods(self, greeting1, greeting2):
"""Call multiple methods on the same contract in a batch"""
# Create a contract instance
hello = CrossContract(self.hello_contract)
# Create a batch for the hello contract
batch = hello.batch()
# Add function calls to the batch
batch.function_call("set_greeting", {"message": greeting1})
batch.function_call("another_method", {"arg1": greeting2})
# Add a callback to process the result
promise = batch.then(Context.current_account_id()).function_call(
"batch_callback",
{"original_data": [greeting1, greeting2]},
gas=10 * ONE_TGAS
)
return promise.value()
@callback
def batch_callback(self, result: PromiseResult, original_data=None):
"""Process batch result - only gets the result of the last operation"""
return {
"success": result.success,
"result": result.data,
"original_data": original_data
}
Callbacks only have access to the result of the last function in a batch call
Multiple Functions: Different Contracts
You can also call multiple functions in different contracts. These functions will be executed in parallel, and do not impact each other. This means that, if one fails, the others will execute, and NOT be reverted.
- 🌐 JavaScript
- 🦀 Rust
- 🐍 Python
Loading...
Loading...
from near_sdk_py import call, Contract, multi_callback, PromiseResult, CrossContract, init
class MultiContractExample(Contract):
# Contract addresses we want to interact with
contract_a = "contract-a.testnet"
contract_b = "contract-b.testnet"
@init
def new(self):
"""Initialize the contract"""
pass
@call
def call_multiple_contracts(self):
"""Calls multiple different contracts in parallel"""
# Create promises for each contract
contract_a = CrossContract(self.contract_a)
promise_a = contract_a.call("method_a")
contract_b = CrossContract(self.contract_b)
promise_b = contract_b.call("method_b")
# Join the promises and add a callback
# The first promise's join method can combine multiple promises
combined_promise = promise_a.join(
[promise_b],
"multi_contract_callback",
contract_ids=[self.contract_a, self.contract_b] # Context data
)
return combined_promise.value()
@multi_callback
def multi_contract_callback(self, results, contract_ids=None):
"""Process results from multiple contracts"""
# results is an array containing all promise results in order
return {
"contract_a": {
"id": contract_ids[0],
"result": results[0].data,
"success": results[0].success
},
"contract_b": {
"id": contract_ids[1],
"result": results[1].data,
"success": results[1].success
},
"success": all(result.success for result in results)
}
Callbacks have access to the result of all functions in a parallel call
Security Concerns
While writing cross-contract calls there is a significant aspect to keep in mind: all the calls are independent and asynchronous. In other words:
- The function in which you make the call and function for the callback are independent.
- There is a delay between the call and the callback, in which people can still interact with the contract
This has important implications on how you should handle the callbacks. Particularly:
- Make sure you don't leave the contract in a exploitable state between the call and the callback.
- Manually rollback any changes to the state in the callback if the external call failed.
We have a whole security section dedicated to these specific errors, so please go and check it.
Not following these basic security guidelines could expose your contract to exploits. Please check the security section, and if still in doubt, join us in Discord.