Using ckERC20 tokens in dapps
Advanced
Ethereum
All ckERC20 tokens are ICRC-2 compliant tokens, which means that they support the ICRC-1 and ICRC-2 token standards.
To integrate ckERC20 tokens into your dapp, you can simply write code that uses the ICRC-1 and ICRC-2 standards. For token transfers and account balances, requests must be sent to the corresponding ckERC20 ledger canister.
Write a smart contract that uses ckERC20 tokens
Since ckERC20 tokens are managed by ICRC-2 compliant ledgers, the development project needs to be configured to interact with the ledger of the targeted ckERC20 tokens.
Once you have your project configured to use the correct ledger, you can interact with it for workflows such as transferring tokens. The following code sample uses the ckUSDC ledger as an example:
- Motoko
- Rust
import ckUsdcLedger "canister:ledger";
import Debug "mo:base/Debug";
import Result "mo:base/Result";
import Option "mo:base/Option";
import Blob "mo:base/Blob";
import Error "mo:base/Error";
actor {
type Account = {
owner : Principal;
subaccount : ?[Nat8];
};
type TransferArgs = {
amount : Nat;
toAccount : Account;
};
public shared ({ caller }) func transfer(args : TransferArgs) : async Result.Result<ckUsdcLedger.BlockIndex, Text> {
Debug.print(
"Transferring "
# debug_show (args.amount)
# " tokens to account"
# debug_show (args.toAccount)
);
let transferArgs : ckUsdcLedger.TransferArg = {
// can be used to distinguish between transactions
memo = null;
// the amount we want to transfer
amount = args.amount;
// we want to transfer tokens from the default subaccount of the canister
from_subaccount = null;
// if not specified, the default fee for the canister is used
fee = null;
// we take the principal and subaccount from the arguments and convert them into an account identifier
to = args.toAccount;
// a timestamp indicating when the transaction was created by the caller; if it is not specified by the caller then this is set to the current ICP time
created_at_time = null;
};
try {
// initiate the transfer
let transferResult = await ckUsdcLedger.icrc1_transfer(transferArgs);
// check if the transfer was successful
switch (transferResult) {
case (#Err(transferError)) {
return #err("Couldn't transfer funds:\n" # debug_show (transferError));
};
case (#Ok(blockIndex)) { return #ok blockIndex };
};
} catch (error : Error) {
// catch any errors that might occur during the transfer
return #err("Reject message: " # Error.message(error));
};
};
};
use candid::{CandidType, Deserialize, Principal};
use icrc_ledger_types::icrc1::account::Account;
use icrc_ledger_types::icrc1::transfer::{BlockIndex, NumTokens, TransferArg, TransferError};
use serde::Serialize;
#[derive(CandidType, Deserialize, Serialize)]
pub struct TransferArgs {
amount: NumTokens,
to_account: Account,
}
#[ic_cdk::update]
async fn transfer(args: TransferArgs) -> Result<BlockIndex, String> {
ic_cdk::println!(
"Transferring {} tokens to account {}",
&args.amount,
&args.to_account,
);
let transfer_args: TransferArg = TransferArg {
// can be used to distinguish between transactions
memo: None,
// the amount we want to transfer
amount: args.amount,
// we want to transfer tokens from the default subaccount of the canister
from_subaccount: None,
// if not specified, the default fee for the canister is used
fee: None,
// the account we want to transfer tokens to
to: args.to_account,
// a timestamp indicating when the transaction was created by the caller; if it is not specified by the caller then this is set to the current ICP time
created_at_time: None,
};
// 1. Asynchronously call another canister function using `ic_cdk::call`.
ic_cdk::call::<(TransferArg,), (Result<BlockIndex, TransferError>,)>(
// 2. Convert a textual representation of a Principal into an actual `Principal` object.
// `expect` will panic if the conversion fails, ensuring the code does not proceed with an invalid principal.
Principal::from_text("xevnm-gaaaa-aaaar-qafnq-cai")
.expect("Could not decode the principal."),
// 3. Specify the method name on the target canister to be called, in this case, "icrc1_transfer".
"icrc1_transfer",
// 4. Provide the arguments for the call in a tuple, here `transfer_args` is encapsulated as a single-element tuple.
(transfer_args,),
)
.await // 5. Await the completion of the asynchronous call, pausing the execution until the future is resolved.
// 6. Apply `map_err` to transform any network or system errors encountered during the call into a more readable string format.
// The `?` operator is then used to propagate errors: if the result is an `Err`, it returns from the function with that error,
// otherwise, it unwraps the `Ok` value, allowing the chain to continue.
.map_err(|e| format!("failed to call ledger: {:?}", e))?
// 7. Access the first element of the tuple, which is the `Result<BlockIndex, TransferError>`, for further processing.
.0
// 8. Use `map_err` again to transform any specific ledger transfer errors into a readable string format, facilitating error handling and debugging.
.map_err(|e| format!("ledger transfer error {:?}", e))
}
// Enable Candid export (see /docs/current/developer-docs/backend/rust/generating-candid)
ic_cdk::export_candid!();