Using an ICRC-1 ledger
How you interact with an ICRC ledger is dependent on whether you want to interact with it from the command line, from your web app, or from another canister.
From the command line
You can use the dfx canister call
command, which is a generic command for calling any function's methods, not specifically an ICRC ledger's methods.
For example, to fetch the token symbol of an ICRC-1 ledger, call the icrc1_symbol
method:
dfx canister call ss2fx-dyaaa-aaaar-qacoq-cai icrc1_symbol '()'
Whether your ICRC-1 ledger will have all available methods enabled will depend on if you choose to support any of the extensions of ICRC-1 (ICRC-2, ICRC-3,...).
You can always check which standards are supported by a certain ICRC-1 ledger by calling:
dfx canister call <canister-id> icrc1_supported_standards '()'
You can find all available methods for your ICRC-1 ledger within the ICRC-1 ledger canister's Candid file or, if your ICRC-1 ledger has been deployed to the mainnet, view your ICRC-1 ledger canister on the dashboard. An example of an ICRC-1 ledger deployed on the mainnet that you can reference is the ckETH ledger canister.
For ledgers that support ICRC-2, you can also call ICRC-2 methods. ICRC-2 methods include those for approve
workflows. For specifics, see the ICRC-2 standard.
View the dfx canister call
documentation for more information on calling canister methods.
From a web application
To simplify working with an ICRC ledger from JavaScript applications, you can use the ledger-icrc-js JavaScript library. This package and its dependencies can be installed with npm
:
npm i @dfinity/agent @dfinity/candid @dfinity/principal @dfinity/utils npm i @dfinity/ledger-icrc
Then you can use it programmatically in your application. For example, to get token metadata:
import { IcrcLedgerCanister } from "@dfinity/ledger-icrc";
import { createAgent } from "@dfinity/utils";
const agent = await createAgent({
identity,
host: HOST,
});
const { metadata } = IcrcLedgerCanister.create({
agent,
canisterId: MY_LEDGER_CANISTER_ID,
});
const data = await metadata({});
From inter-canister calls via ic-cdk
When calling into arbitrary ICRC-1 ledgers, it is recommended that you use bounded wait (aka best-effort response) calls. These calls ensure that your canister does not get stuck waiting for a response from the ledger.
Below is an example that demonstrates how to fetch the transfer fee from an ICRC-1 ledger using Rust and the ic-cdk
library from within a canister. This example includes retry logic to handle errors when possible.
pub enum GetFeeError {
/// The ledger didn't implement the ICRC-1 API correctly, e.g., returning an invalid response.
Icrc1ApiViolation(String),
/// A CDK CallError that we cannot recover from synchronously.
FatalCallError(CallFailed),
}
/// Obtain the fee that the ledger canister charges for a transfer.
/// This function will keep retrying to fetch the fees for as long possible, and for as long as the
/// `should_retry` predicate returns true. Note that using a predicate that just always returns
/// `true` can keep your canister in a retry loop, and potentially unable to upgrade. The
/// recommended way is to set a limit on the number of retries, use a timeout, or abort when the
/// caller canister enters the stopping state.
pub async fn icrc1_get_fee<P>(ledger: Principal, should_retry: &P) -> Result<NumTokens, GetFeeError>
where
P: Fn() -> bool,
{
loop {
match Call::bounded_wait(ledger, "icrc1_fee").await {
Ok(res) => match res.candid() {
Ok(fee) => return Ok(fee),
Err(msg) => {
return Err(GetFeeError::Icrc1ApiViolation(format!(
"Unable to decode the fee: {:?}; failed with error {:?}",
res, msg
)))
}
},
// The system rejected our call, but it is possible to retry immediately.
// Since obtaining the fees is idempotent, it's always safe to retry.
Err(err) if err.is_immediately_retryable() && should_retry() => continue,
// The system rejected our call, but it is not possible to retry immediately.
Err(err) => return Err(GetFeeError::FatalCallError(err)),
}
}
}
Below is an example that demonstrates transferring ICRC-1 ledger tokens using bounded wait response calls. It handles the unknown state case by using the transaction deduplication feature of ICRC-1 ledgers. However, if the transaction keeps failing beyond the deduplication window, the transaction state will be unknown and you will need to perform manual recovery.
pub enum TransferErrorCause {
Icrc1ApiViolation(String),
FatalCallError(CallFailed),
LedgerError(TransferError),
}
pub enum Icrc1TransferError {
// The transfer didn't happen.
TransferFailed(TransferErrorCause),
// The transfer may have happened.
UnknownState(TransferErrorCause),
}
/// Transfer the tokens on the specified ledger. The caller must ensure that:
/// 1. The `created_at` time of the `TransferArg` is set.
/// 2. The transaction described by the `TransferArg` has not yet been executed by the ledger.
/// Otherwise, the function may return `Ok` even if the transfer didn't happen.
pub async fn icrc1_transfer<P>(
ledger: Principal,
arg: TransferArg,
should_retry: &P,
) -> Result<BlockIndex, Icrc1TransferError>
where
P: Fn() -> bool,
{
assert!(
arg.created_at_time.is_some(),
"The created_at_time must be set in the TransferArg"
);
let mut no_unknowns = true;
loop {
match Call::bounded_wait(ledger, "icrc1_transfer")
.with_arg(&arg)
// Use the longest timeout supported by the system, as we'll retry later anyways.
.change_timeout(u32::MAX)
.await
{
Ok(res) => match res.candid() {
Ok(Ok(i)) => return Ok(i),
Ok(Err(e)) => match e {
// Since the assumption is that the transaction didn't happen before the call,
// treat a duplicate error as a success.
TransferError::Duplicate { duplicate_of } => return Ok(duplicate_of),
e if no_unknowns => {
return Err(Icrc1TransferError::TransferFailed(
TransferErrorCause::LedgerError(e),
))
}
// Unknown state. To recover, you can query the ledger blocks to see if the
// transaction was executed.
e => {
return Err(Icrc1TransferError::UnknownState(
TransferErrorCause::LedgerError(e),
))
}
},
Err(e) => {
return Err(Icrc1TransferError::TransferFailed(
TransferErrorCause::Icrc1ApiViolation(e.to_string()),
))
}
},
// Since the ICRC1 transfer is idempotent, it's always safe to retry.
Err(e) if e.is_immediately_retryable() && should_retry() => {
// If the reject wasn't clean, mark the state as unknown.
if !e.is_clean_reject() {
no_unknowns = false;
}
continue;
}
Err(e) if e.is_clean_reject() && no_unknowns => {
return Err(Icrc1TransferError::TransferFailed(
TransferErrorCause::FatalCallError(e),
))
}
Err(e) => {
return Err(Icrc1TransferError::UnknownState(
TransferErrorCause::FatalCallError(e),
))
}
}
}
}
You can find all available methods for your ICRC-1 ledger within the ICRC-1 ledger canister's Candid file or, if your ICRC-1 ledger has been deployed to the mainnet, view your ICRC-1 ledger canister on the dashboard. An example of an ICRC-1 ledger deployed on the mainnet that you can reference is the ckETH ledger canister.
icrc-ledger-types
Rust crate
You can also use the icrc-ledger-types
Rust crate interact with the ICRC-1 and ICRC-2 endpoints. It can be used for interacting with any canister that supports any of the ICRC-1 methods and its extensions (ICRC-2, ICRC-3, etc.) including the ICP ledger.