Cross-canister
Examples:
- async_await
- bitcoin
- composite_queries
- cross_canister_calls
- cycles
- ethereum_json_rpc
- func_types
- heartbeat
- inline_types
- ledger_canister
- management_canister
- outgoing_http_requests
- threshold_ecdsa
- rejections
- timers
- tuple_types
- whoami
Canisters are generally able to call the query or update methods of other canisters in any subnet. We refer to these types of calls as cross-canister calls.
A cross-canister call begins with a definition of the canister to be called.
Imagine a simple canister called
token_canister
:
import {
Canister,
ic,
nat64,
Opt,
Principal,
StableBTreeMap,
update
} from 'azle/experimental';
let accounts = StableBTreeMap<Principal, nat64>(0);
export default Canister({
transfer: update([Principal, nat64], nat64, (to, amount) => {
const from = ic.caller();
const fromBalance = getBalance(accounts.get(from));
const toBalance = getBalance(accounts.get(to));
accounts.insert(from, fromBalance - amount);
accounts.insert(to, toBalance + amount);
return amount;
})
});
function getBalance(accountOpt: Opt<nat64>): nat64 {
if ('None' in accountOpt) {
return 0n;
} else {
return accountOpt.Some;
}
}
Now that you have the canister definition, you can import and instantiate it in another canister:
import { Canister, ic, nat64, Principal, update } from 'azle/experimental';
import TokenCanister from './token_canister';
const tokenCanister = TokenCanister(
Principal.fromText('r7inp-6aaaa-aaaaa-aaabq-cai')
);
export default Canister({
payout: update([Principal, nat64], nat64, async (to, amount) => {
return await ic.call(tokenCanister.transfer, {
args: [to, amount]
});
})
});
If you don't have the actual definition of the token canister with the canister method implementations, you can always create your own canister definition without method implementations:
import { Canister, ic, nat64, Principal, update } from 'azle/experimental';
const TokenCanister = Canister({
transfer: update([Principal, nat64], nat64)
});
const tokenCanister = TokenCanister(
Principal.fromText('r7inp-6aaaa-aaaaa-aaabq-cai')
);
export default Canister({
payout: update([Principal, nat64], nat64, async (to, amount) => {
return await ic.call(tokenCanister.transfer, {
args: [to, amount]
});
})
});
The IC guarantees that cross-canister calls will
return. This means that, generally speaking, you
will always receive a response from
ic.call
. If there are errors during
the call, ic.call
will throw.
Wrapping your cross-canister call in a
try...catch
allows you to handle
these errors.
Let's add to our example code and explore adding some practical error-handling to stop people from stealing tokens.
token_canister
:
import {
Canister,
ic,
nat64,
Opt,
Principal,
StableBTreeMap,
update
} from 'azle/experimental';
let accounts = StableBTreeMap<Principal, nat64>(0);
export default Canister({
transfer: update([Principal, nat64], nat64, (to, amount) => {
const from = ic.caller();
const fromBalance = getBalance(accounts.get(from));
if (amount > fromBalance) {
throw new Error(`${from} has an insufficient balance`);
}
const toBalance = getBalance(accounts.get(to));
accounts.insert(from, fromBalance - amount);
accounts.insert(to, toBalance + amount);
return amount;
})
});
function getBalance(accountOpt: Opt<nat64>): nat64 {
if ('None' in accountOpt) {
return 0n;
} else {
return accountOpt.Some;
}
}
payout_canister
:
import { Canister, ic, nat64, Principal, update } from 'azle/experimental';
import TokenCanister from './index';
const tokenCanister = TokenCanister(
Principal.fromText('bkyz2-fmaaa-aaaaa-qaaaq-cai')
);
export default Canister({
payout: update([Principal, nat64], nat64, async (to, amount) => {
try {
return await ic.call(tokenCanister.transfer, {
args: [to, amount]
});
} catch (error) {
console.log(error);
}
return 0n;
})
});
Throwing will allow you to express error
conditions and halt execution, but you may find
embracing the Result
variant as a
better solution for error handling because of
its composability and predictability.
So far we have only shown a cross-canister call from an update method. Update methods can call other update methods or query methods (but not composite query methods as discussed below). If an update method calls a query method, that query method will be called in replicated mode. Replicated mode engages the consensus process, but for queries the state will still be discarded.
Cross-canister calls can also be initiated from
query methods. These are known as composite
queries, and in Azle they are simply
async
query methods. Composite
queries can call other composite query methods
and regular query methods. Composite queries
cannot call update methods.
Here's an example of a composite query method:
import { bool, Canister, ic, Principal, query } from 'azle/experimental';
const SomeCanister = Canister({
queryForBoolean: query([], bool)
});
const someCanister = SomeCanister(
Principal.fromText('ryjl3-tyaaa-aaaaa-aaaba-cai')
);
export default Canister({
querySomeCanister: query([], bool, async () => {
return await ic.call(someCanister.queryForBoolean);
})
});
You can expect cross-canister calls within the same subnet to take up to a few seconds to complete, and cross-canister calls across subnets take about double that time. Composite queries should be much faster, similar to query calls in latency.
If you don't need to wait for your
cross-canister call to return, you can use
notify
:
import { Canister, ic, Principal, update, Void } from 'azle/experimental';
const SomeCanister = Canister({
receiveNotification: update([], Void)
});
const someCanister = SomeCanister(
Principal.fromText('ryjl3-tyaaa-aaaaa-aaaba-cai')
);
export default Canister({
sendNotification: update([], Void, () => {
return ic.notify(someCanister.receiveNotification);
})
});
If you need to send cycles with your
cross-canister call, you can add
cycles
to the
config
object of
ic.notify
:
import { Canister, ic, Principal, update, Void } from 'azle/experimental';
const SomeCanister = Canister({
receiveNotification: update([], Void)
});
const someCanister = SomeCanister(
Principal.fromText('ryjl3-tyaaa-aaaaa-aaaba-cai')
);
export default Canister({
sendNotification: update([], Void, () => {
return ic.notify(someCanister.receiveNotification, {
cycles: 1_000_000n
});
})
});