Using ckBTC in dapps
Overview
ckBTC is an ICRC-2 compliant token, meaning it supports the ICRC-1 and ICRC-2 token standards.
To integrate ckBTC into your dapp, you can write code that uses the ICRC-1 standard. For token transfers and account balances, ckBTC requests must be sent to ICRC-1 ledger canister with principal ID mxzaz-hqaaa-aaaar-qaada-cai
.
Write a smart contract that uses ckBTC
To write canister code that makes calls to the ICRC-1 ledger, you will need to use inter-canister calls and specify the principal ID of the ICRC-1 ledger canister. Then, to interact with ckBTC, you can use the ICRC-1 endpoints.
You can deploy the ICRC-1 ledger locally for testing. Learn more in the ICRC-1 ledger setup documentation.
Once you have your project configured to use the ICRC-1 ledger, you can interact with it for workflows such as transferring tokens:
- Motoko
- Rust
import ckbtcLedger "canister:icrc1_ledger_canister";
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<ckbtcLedger.BlockIndex, Text> {
Debug.print(
"Transferring "
# debug_show (args.amount)
# " tokens to account"
# debug_show (args.toAccount)
);
let transferArgs : ckbtcLedger.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 ckbtcLedger.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. The principal is the one we specified in `dfx.json`.
// `expect` will panic if the conversion fails, ensuring the code does not proceed with an invalid principal.
Principal::from_text("mxzaz-hqaaa-aaaar-qaada-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!();
View the ICRC-1 endpoints for more information on sending and receiving tokens.