icrc-ledger
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseICRC Ledger Standards
ICRC账本标准
What This Is
概述
ICRC-1 is the fungible token standard on Internet Computer, defining transfer, balance, and metadata interfaces. ICRC-2 extends it with approve/transferFrom (allowance) mechanics, enabling third-party spending like ERC-20 on Ethereum.
ICRC-1是Internet Computer上的 fungible token 标准,定义了转账、余额和元数据接口。ICRC-2对其进行了扩展,增加了approve/transferFrom(授权)机制,支持类似以太坊ERC-20的第三方代付功能。
Prerequisites
前置条件
- For Motoko: mops with in mops.toml
core = "2.0.0" - For Rust: ,
ic-cdk = "0.19",candid = "0.10"in Cargo.tomlicrc-ledger-types = "0.1"
- 对于Motoko:mops工具,且mops.toml中配置
core = "2.0.0" - 对于Rust:Cargo.toml中配置、
ic-cdk = "0.19"、candid = "0.10"icrc-ledger-types = "0.1"
Canister IDs
容器ID
| Token | Ledger Canister ID | Fee | Decimals |
|---|---|---|---|
| ICP | | 10000 e8s (0.0001 ICP) | 8 |
| ckBTC | | 10 satoshis | 8 |
| ckETH | | 2000000000000 wei (0.000002 ETH) | 18 |
Index canisters (for transaction history):
- ICP Index:
qhbym-qaaaa-aaaaa-aaafq-cai - ckBTC Index:
n5wcd-faaaa-aaaar-qaaea-cai - ckETH Index:
s3zol-vqaaa-aaaar-qacpa-cai
| 代币 | 账本容器ID | 手续费 | 小数位数 |
|---|---|---|---|
| ICP | | 10000 e8s(0.0001 ICP) | 8 |
| ckBTC | | 10 satoshis | 8 |
| ckETH | | 2000000000000 wei(0.000002 ETH) | 18 |
索引容器(用于交易历史):
- ICP索引:
qhbym-qaaaa-aaaaa-aaafq-cai - ckBTC索引:
n5wcd-faaaa-aaaar-qaaea-cai - ckETH索引:
s3zol-vqaaa-aaaar-qacpa-cai
Mistakes That Break Your Build
导致构建失败的常见错误
-
Wrong fee amount -- ICP fee is 10000 e8s, NOT 10000 ICP. ckBTC fee is 10 satoshis, NOT 10 ckBTC. Using the wrong unit drains your entire balance in one transfer.
-
Forgetting approve before transferFrom -- ICRC-2 transferFrom will reject withif the token owner has not called
InsufficientAllowancefirst. This is a two-step flow: owner approves, then spender calls transferFrom.icrc2_approve -
Not handling Err variants --returns
icrc1_transfer, not justResult<Nat, TransferError>. The error variants are:Nat,BadFee,BadBurn,InsufficientFunds,TooOld,CreatedInFuture,Duplicate,TemporarilyUnavailable. You must match on every variant or at minimum propagate the error.GenericError -
Using wrong Account format -- An ICRC-1 Account is, NOT just a Principal. The subaccount is a 32-byte blob. Passing null/None for subaccount uses the default subaccount (all zeros).
{ owner: Principal; subaccount: ?Blob } -
Omitting created_at_time -- Without, you lose deduplication protection. Two identical transfers submitted within 24h will both execute. Set
created_at_timetocreated_at_time(Motoko) orTime.now()(Rust) for dedup.ic_cdk::api::time() -
Hardcoding canister IDs as text -- Always use(Motoko) or
Principal.fromText("ryjl3-tyaaa-aaaaa-aaaba-cai")(Rust). Never pass raw strings where a Principal is expected.Principal::from_text("ryjl3-tyaaa-aaaaa-aaaba-cai") -
Calling ledger from frontend -- ICRC-1 transfers should originate from a backend canister, not directly from the frontend. Frontend-initiated transfers expose the user to reentrancy and can bypass business logic. Use a backend canister as the intermediary.
-
Shell substitution in/
--argument-file-- Expressions likeinit_arg_filedo NOT expand inside files referenced by$(icp identity principal)orinit_arg_file. The file is read as literal text. Either use--argument-fileon the command line (where the shell expands variables), or pre-generate the file with--argument/envsubstbefore deploying.sed
-
手续费金额错误 -- ICP的手续费是10000 e8s,而非10000 ICP。ckBTC的手续费是10 satoshis,而非10 ckBTC。使用错误单位会导致一次转账耗尽全部余额。
-
调用transferFrom前未执行approve -- 如果代币所有者未先调用,ICRC-2的transferFrom会返回
icrc2_approve错误。这是一个两步流程:所有者先授权,然后代付方调用transferFrom。InsufficientAllowance -
未处理错误变体 --返回的是
icrc1_transfer,而非单纯的Result<Nat, TransferError>。错误变体包括:Nat、BadFee、BadBurn、InsufficientFunds、TooOld、CreatedInFuture、Duplicate、TemporarilyUnavailable。你必须匹配所有变体,或至少向上传播错误。GenericError -
使用错误的Account格式 -- ICRC-1的Account是,而非单纯的Principal。subaccount是32字节的blob。如果subaccount传入null/None,则使用默认子账户(全零值)。
{ owner: Principal; subaccount: ?Blob } -
遗漏created_at_time -- 未设置会失去重复操作保护。24小时内提交的两个相同转账请求都会被执行。请将
created_at_time设置为created_at_time(Motoko)或Time.now()(Rust)以启用去重。ic_cdk::api::time() -
将容器ID硬编码为文本 -- 请始终使用(Motoko)或
Principal.fromText("ryjl3-tyaaa-aaaaa-aaaba-cai")(Rust)。切勿在需要传入Principal的位置直接传入原始字符串。Principal::from_text("ryjl3-tyaaa-aaaaa-aaaba-cai") -
从前端直接调用账本 -- ICRC-1转账应从后端容器发起,而非直接从前端发起。前端发起的转账会让用户面临重入风险,且可能绕过业务逻辑。请使用后端容器作为中间层。
-
在/
--argument-file中使用Shell替换 -- 类似init_arg_file的表达式在$(icp identity principal)或init_arg_file引用的文件中不会被展开。文件内容会被当作字面量读取。请在命令行中使用--argument-file参数(Shell会在此处展开变量),或在部署前使用--argument/envsubst预先生成文件。sed
Implementation
代码实现
Motoko
Motoko
Imports and Types
导入与类型定义
motoko
import Principal "mo:core/Principal";
import Nat "mo:core/Nat";
import Nat8 "mo:core/Nat8";
import Nat64 "mo:core/Nat64";
import Blob "mo:core/Blob";
import Time "mo:core/Time";
import Int "mo:core/Int";
import Runtime "mo:core/Runtime";motoko
import Principal "mo:core/Principal";
import Nat "mo:core/Nat";
import Nat8 "mo:core/Nat8";
import Nat64 "mo:core/Nat64";
import Blob "mo:core/Blob";
import Time "mo:core/Time";
import Int "mo:core/Int";
import Runtime "mo:core/Runtime";Define the ICRC-1 Actor Interface
定义ICRC-1 Actor接口
motoko
persistent actor {
type Account = {
owner : Principal;
subaccount : ?Blob;
};
type TransferArg = {
from_subaccount : ?Blob;
to : Account;
amount : Nat;
fee : ?Nat;
memo : ?Blob;
created_at_time : ?Nat64;
};
type TransferError = {
#BadFee : { expected_fee : Nat };
#BadBurn : { min_burn_amount : Nat };
#InsufficientFunds : { balance : Nat };
#TooOld;
#CreatedInFuture : { ledger_time : Nat64 };
#Duplicate : { duplicate_of : Nat };
#TemporarilyUnavailable;
#GenericError : { error_code : Nat; message : Text };
};
type ApproveArg = {
from_subaccount : ?Blob;
spender : Account;
amount : Nat;
expected_allowance : ?Nat;
expires_at : ?Nat64;
fee : ?Nat;
memo : ?Blob;
created_at_time : ?Nat64;
};
type ApproveError = {
#BadFee : { expected_fee : Nat };
#InsufficientFunds : { balance : Nat };
#AllowanceChanged : { current_allowance : Nat };
#Expired : { ledger_time : Nat64 };
#TooOld;
#CreatedInFuture : { ledger_time : Nat64 };
#Duplicate : { duplicate_of : Nat };
#TemporarilyUnavailable;
#GenericError : { error_code : Nat; message : Text };
};
type TransferFromArg = {
spender_subaccount : ?Blob;
from : Account;
to : Account;
amount : Nat;
fee : ?Nat;
memo : ?Blob;
created_at_time : ?Nat64;
};
type TransferFromError = {
#BadFee : { expected_fee : Nat };
#BadBurn : { min_burn_amount : Nat };
#InsufficientFunds : { balance : Nat };
#InsufficientAllowance : { allowance : Nat };
#TooOld;
#CreatedInFuture : { ledger_time : Nat64 };
#Duplicate : { duplicate_of : Nat };
#TemporarilyUnavailable;
#GenericError : { error_code : Nat; message : Text };
};
// Remote ledger actor reference (ICP ledger shown; swap canister ID for other tokens)
transient let icpLedger = actor ("ryjl3-tyaaa-aaaaa-aaaba-cai") : actor {
icrc1_balance_of : shared query (Account) -> async Nat;
icrc1_transfer : shared (TransferArg) -> async { #Ok : Nat; #Err : TransferError };
icrc2_approve : shared (ApproveArg) -> async { #Ok : Nat; #Err : ApproveError };
icrc2_transfer_from : shared (TransferFromArg) -> async { #Ok : Nat; #Err : TransferFromError };
icrc1_fee : shared query () -> async Nat;
icrc1_decimals : shared query () -> async Nat8;
};
// Check balance
public func getBalance(who : Principal) : async Nat {
await icpLedger.icrc1_balance_of({
owner = who;
subaccount = null;
})
};
// Transfer tokens (this canister sends from its own account)
// WARNING: Add access control in production — this allows any caller to transfer tokens
public func sendTokens(to : Principal, amount : Nat) : async Nat {
let now = Nat64.fromNat(Int.abs(Time.now()));
let result = await icpLedger.icrc1_transfer({
from_subaccount = null;
to = { owner = to; subaccount = null };
amount = amount;
fee = ?10000; // ICP fee: 10000 e8s
memo = null;
created_at_time = ?now;
});
switch (result) {
case (#Ok(blockIndex)) { blockIndex };
case (#Err(#InsufficientFunds({ balance }))) {
Runtime.trap("Insufficient funds. Balance: " # Nat.toText(balance))
};
case (#Err(#BadFee({ expected_fee }))) {
Runtime.trap("Wrong fee. Expected: " # Nat.toText(expected_fee))
};
case (#Err(_)) { Runtime.trap("Transfer failed") };
}
};
// ICRC-2: Approve a spender
public shared ({ caller }) func approveSpender(spender : Principal, amount : Nat) : async Nat {
// caller is captured at function entry in Motoko -- safe across await
let now = Nat64.fromNat(Int.abs(Time.now()));
let result = await icpLedger.icrc2_approve({
from_subaccount = null;
spender = { owner = spender; subaccount = null };
amount = amount;
expected_allowance = null;
expires_at = null;
fee = ?10000;
memo = null;
created_at_time = ?now;
});
switch (result) {
case (#Ok(blockIndex)) { blockIndex };
case (#Err(_)) { Runtime.trap("Approve failed") };
}
};
// ICRC-2: Transfer from another account (requires prior approval)
// WARNING: Add access control in production — this allows any caller to transfer tokens
public func transferFrom(from : Principal, to : Principal, amount : Nat) : async Nat {
let now = Nat64.fromNat(Int.abs(Time.now()));
let result = await icpLedger.icrc2_transfer_from({
spender_subaccount = null;
from = { owner = from; subaccount = null };
to = { owner = to; subaccount = null };
amount = amount;
fee = ?10000;
memo = null;
created_at_time = ?now;
});
switch (result) {
case (#Ok(blockIndex)) { blockIndex };
case (#Err(#InsufficientAllowance({ allowance }))) {
Runtime.trap("Insufficient allowance: " # Nat.toText(allowance))
};
case (#Err(_)) { Runtime.trap("TransferFrom failed") };
}
};
}motoko
persistent actor {
type Account = {
owner : Principal;
subaccount : ?Blob;
};
type TransferArg = {
from_subaccount : ?Blob;
to : Account;
amount : Nat;
fee : ?Nat;
memo : ?Blob;
created_at_time : ?Nat64;
};
type TransferError = {
#BadFee : { expected_fee : Nat };
#BadBurn : { min_burn_amount : Nat };
#InsufficientFunds : { balance : Nat };
#TooOld;
#CreatedInFuture : { ledger_time : Nat64 };
#Duplicate : { duplicate_of : Nat };
#TemporarilyUnavailable;
#GenericError : { error_code : Nat; message : Text };
};
type ApproveArg = {
from_subaccount : ?Blob;
spender : Account;
amount : Nat;
expected_allowance : ?Nat;
expires_at : ?Nat64;
fee : ?Nat;
memo : ?Blob;
created_at_time : ?Nat64;
};
type ApproveError = {
#BadFee : { expected_fee : Nat };
#InsufficientFunds : { balance : Nat };
#AllowanceChanged : { current_allowance : Nat };
#Expired : { ledger_time : Nat64 };
#TooOld;
#CreatedInFuture : { ledger_time : Nat64 };
#Duplicate : { duplicate_of : Nat };
#TemporarilyUnavailable;
#GenericError : { error_code : Nat; message : Text };
};
type TransferFromArg = {
spender_subaccount : ?Blob;
from : Account;
to : Account;
amount : Nat;
fee : ?Nat;
memo : ?Blob;
created_at_time : ?Nat64;
};
type TransferFromError = {
#BadFee : { expected_fee : Nat };
#BadBurn : { min_burn_amount : Nat };
#InsufficientFunds : { balance : Nat };
#InsufficientAllowance : { allowance : Nat };
#TooOld;
#CreatedInFuture : { ledger_time : Nat64 };
#Duplicate : { duplicate_of : Nat };
#TemporarilyUnavailable;
#GenericError : { error_code : Nat; message : Text };
};
// 远程账本Actor引用(示例为ICP账本;如需操作其他代币,替换容器ID即可)
transient let icpLedger = actor ("ryjl3-tyaaa-aaaaa-aaaba-cai") : actor {
icrc1_balance_of : shared query (Account) -> async Nat;
icrc1_transfer : shared (TransferArg) -> async { #Ok : Nat; #Err : TransferError };
icrc2_approve : shared (ApproveArg) -> async { #Ok : Nat; #Err : ApproveError };
icrc2_transfer_from : shared (TransferFromArg) -> async { #Ok : Nat; #Err : TransferFromError };
icrc1_fee : shared query () -> async Nat;
icrc1_decimals : shared query () -> async Nat8;
};
// 查询余额
public func getBalance(who : Principal) : async Nat {
await icpLedger.icrc1_balance_of({
owner = who;
subaccount = null;
})
};
// 转账代币(当前容器从自身账户发起转账)
// 注意:生产环境中请添加访问控制——当前代码允许任何调用者发起转账
public func sendTokens(to : Principal, amount : Nat) : async Nat {
let now = Nat64.fromNat(Int.abs(Time.now()));
let result = await icpLedger.icrc1_transfer({
from_subaccount = null;
to = { owner = to; subaccount = null };
amount = amount;
fee = ?10000; // ICP手续费:10000 e8s
memo = null;
created_at_time = ?now;
});
switch (result) {
case (#Ok(blockIndex)) { blockIndex };
case (#Err(#InsufficientFunds({ balance }))) {
Runtime.trap("余额不足。当前余额:" # Nat.toText(balance))
};
case (#Err(#BadFee({ expected_fee }))) {
Runtime.trap("手续费错误。预期手续费:" # Nat.toText(expected_fee))
};
case (#Err(_)) { Runtime.trap("转账失败") };
}
};
// ICRC-2:授权代付方
public shared ({ caller }) func approveSpender(spender : Principal, amount : Nat) : async Nat {
// Motoko会在函数入口捕获caller——跨await调用依然安全
let now = Nat64.fromNat(Int.abs(Time.now()));
let result = await icpLedger.icrc2_approve({
from_subaccount = null;
spender = { owner = spender; subaccount = null };
amount = amount;
expected_allowance = null;
expires_at = null;
fee = ?10000;
memo = null;
created_at_time = ?now;
});
switch (result) {
case (#Ok(blockIndex)) { blockIndex };
case (#Err(_)) { Runtime.trap("授权失败") };
}
};
// ICRC-2:从其他账户转账(需提前完成授权)
// 注意:生产环境中请添加访问控制——当前代码允许任何调用者发起转账
public func transferFrom(from : Principal, to : Principal, amount : Nat) : async Nat {
let now = Nat64.fromNat(Int.abs(Time.now()));
let result = await icpLedger.icrc2_transfer_from({
spender_subaccount = null;
from = { owner = from; subaccount = null };
to = { owner = to; subaccount = null };
amount = amount;
fee = ?10000;
memo = null;
created_at_time = ?now;
});
switch (result) {
case (#Ok(blockIndex)) { blockIndex };
case (#Err(#InsufficientAllowance({ allowance }))) {
Runtime.trap("授权额度不足:" # Nat.toText(allowance))
};
case (#Err(_)) { Runtime.trap("代付转账失败") };
}
};
}Rust
Rust
Cargo.toml Dependencies
Cargo.toml依赖配置
toml
[package]
name = "icrc_ledger_backend"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
ic-cdk = "0.19"
candid = "0.10"
icrc-ledger-types = "0.1"
serde = { version = "1", features = ["derive"] }toml
[package]
name = "icrc_ledger_backend"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
ic-cdk = "0.19"
candid = "0.10"
icrc-ledger-types = "0.1"
serde = { version = "1", features = ["derive"] }Complete Implementation
完整实现代码
rust
use candid::{Nat, Principal};
use icrc_ledger_types::icrc1::account::Account;
use icrc_ledger_types::icrc1::transfer::{TransferArg, TransferError};
use icrc_ledger_types::icrc2::approve::{ApproveArgs, ApproveError};
use icrc_ledger_types::icrc2::transfer_from::{TransferFromArgs, TransferFromError};
use ic_cdk::update;
use ic_cdk::call::Call;
const ICP_LEDGER: &str = "ryjl3-tyaaa-aaaaa-aaaba-cai";
const ICP_FEE: u64 = 10_000; // 10000 e8s
fn ledger_id() -> Principal {
Principal::from_text(ICP_LEDGER).unwrap()
}
// Check balance
#[update]
async fn get_balance(who: Principal) -> Nat {
let account = Account {
owner: who,
subaccount: None,
};
let (balance,): (Nat,) = Call::unbounded_wait(ledger_id(), "icrc1_balance_of")
.with_arg(account)
.await
.expect("Failed to call icrc1_balance_of")
.candid_tuple()
.expect("Failed to decode response");
balance
}
// Transfer tokens from this canister's account
// WARNING: Add access control in production — this allows any caller to transfer tokens
#[update]
async fn send_tokens(to: Principal, amount: Nat) -> Result<Nat, String> {
let transfer_arg = TransferArg {
from_subaccount: None,
to: Account {
owner: to,
subaccount: None,
},
amount,
fee: Some(Nat::from(ICP_FEE)),
memo: None,
created_at_time: Some(ic_cdk::api::time()),
};
let (result,): (Result<Nat, TransferError>,) = Call::unbounded_wait(ledger_id(), "icrc1_transfer")
.with_arg(transfer_arg)
.await
.map_err(|e| format!("Call failed: {:?}", e))?
.candid_tuple()
.map_err(|e| format!("Decode failed: {:?}", e))?;
match result {
Ok(block_index) => Ok(block_index),
Err(TransferError::InsufficientFunds { balance }) => {
Err(format!("Insufficient funds. Balance: {}", balance))
}
Err(TransferError::BadFee { expected_fee }) => {
Err(format!("Wrong fee. Expected: {}", expected_fee))
}
Err(e) => Err(format!("Transfer error: {:?}", e)),
}
}
// ICRC-2: Approve a spender
#[update]
async fn approve_spender(spender: Principal, amount: Nat) -> Result<Nat, String> {
let args = ApproveArgs {
from_subaccount: None,
spender: Account {
owner: spender,
subaccount: None,
},
amount,
expected_allowance: None,
expires_at: None,
fee: Some(Nat::from(ICP_FEE)),
memo: None,
created_at_time: Some(ic_cdk::api::time()),
};
let (result,): (Result<Nat, ApproveError>,) = Call::unbounded_wait(ledger_id(), "icrc2_approve")
.with_arg(args)
.await
.map_err(|e| format!("Call failed: {:?}", e))?
.candid_tuple()
.map_err(|e| format!("Decode failed: {:?}", e))?;
result.map_err(|e| format!("Approve error: {:?}", e))
}
// ICRC-2: Transfer from another account (requires prior approval)
// WARNING: Add access control in production — this allows any caller to transfer tokens
#[update]
async fn transfer_from(from: Principal, to: Principal, amount: Nat) -> Result<Nat, String> {
let args = TransferFromArgs {
spender_subaccount: None,
from: Account {
owner: from,
subaccount: None,
},
to: Account {
owner: to,
subaccount: None,
},
amount,
fee: Some(Nat::from(ICP_FEE)),
memo: None,
created_at_time: Some(ic_cdk::api::time()),
};
let (result,): (Result<Nat, TransferFromError>,) = Call::unbounded_wait(ledger_id(), "icrc2_transfer_from")
.with_arg(args)
.await
.map_err(|e| format!("Call failed: {:?}", e))?
.candid_tuple()
.map_err(|e| format!("Decode failed: {:?}", e))?;
result.map_err(|e| format!("TransferFrom error: {:?}", e))
}rust
use candid::{Nat, Principal};
use icrc_ledger_types::icrc1::account::Account;
use icrc_ledger_types::icrc1::transfer::{TransferArg, TransferError};
use icrc_ledger_types::icrc2::approve::{ApproveArgs, ApproveError};
use icrc_ledger_types::icrc2::transfer_from::{TransferFromArgs, TransferFromError};
use ic_cdk::update;
use ic_cdk::call::Call;
const ICP_LEDGER: &str = "ryjl3-tyaaa-aaaaa-aaaba-cai";
const ICP_FEE: u64 = 10_000; // 10000 e8s
fn ledger_id() -> Principal {
Principal::from_text(ICP_LEDGER).unwrap()
}
// 查询余额
#[update]
async fn get_balance(who: Principal) -> Nat {
let account = Account {
owner: who,
subaccount: None,
};
let (balance,): (Nat,) = Call::unbounded_wait(ledger_id(), "icrc1_balance_of")
.with_arg(account)
.await
.expect("调用icrc1_balance_of失败")
.candid_tuple()
.expect("解码响应失败");
balance
}
// 从当前容器账户转账代币
// 注意:生产环境中请添加访问控制——当前代码允许任何调用者发起转账
#[update]
async fn send_tokens(to: Principal, amount: Nat) -> Result<Nat, String> {
let transfer_arg = TransferArg {
from_subaccount: None,
to: Account {
owner: to,
subaccount: None,
},
amount,
fee: Some(Nat::from(ICP_FEE)),
memo: None,
created_at_time: Some(ic_cdk::api::time()),
};
let (result,): (Result<Nat, TransferError>,) = Call::unbounded_wait(ledger_id(), "icrc1_transfer")
.with_arg(transfer_arg)
.await
.map_err(|e| format!("调用失败:{:?}", e))?
.candid_tuple()
.map_err(|e| format!("解码失败:{:?}", e))?;
match result {
Ok(block_index) => Ok(block_index),
Err(TransferError::InsufficientFunds { balance }) => {
Err(format!("余额不足。当前余额:{}", balance))
}
Err(TransferError::BadFee { expected_fee }) => {
Err(format!("手续费错误。预期手续费:{}", expected_fee))
}
Err(e) => Err(format!("转账错误:{:?}", e)),
}
}
// ICRC-2:授权代付方
#[update]
async fn approve_spender(spender: Principal, amount: Nat) -> Result<Nat, String> {
let args = ApproveArgs {
from_subaccount: None,
spender: Account {
owner: spender,
subaccount: None,
},
amount,
expected_allowance: None,
expires_at: None,
fee: Some(Nat::from(ICP_FEE)),
memo: None,
created_at_time: Some(ic_cdk::api::time()),
};
let (result,): (Result<Nat, ApproveError>,) = Call::unbounded_wait(ledger_id(), "icrc2_approve")
.with_arg(args)
.await
.map_err(|e| format!("调用失败:{:?}", e))?
.candid_tuple()
.map_err(|e| format!("解码失败:{:?}", e))?;
result.map_err(|e| format!("授权错误:{:?}", e))
}
// ICRC-2:从其他账户转账(需提前完成授权)
// 注意:生产环境中请添加访问控制——当前代码允许任何调用者发起转账
#[update]
async fn transfer_from(from: Principal, to: Principal, amount: Nat) -> Result<Nat, String> {
let args = TransferFromArgs {
spender_subaccount: None,
from: Account {
owner: from,
subaccount: None,
},
to: Account {
owner: to,
subaccount: None,
},
amount,
fee: Some(Nat::from(ICP_FEE)),
memo: None,
created_at_time: Some(ic_cdk::api::time()),
};
let (result,): (Result<Nat, TransferFromError>,) = Call::unbounded_wait(ledger_id(), "icrc2_transfer_from")
.with_arg(args)
.await
.map_err(|e| format!("调用失败:{:?}", e))?
.candid_tuple()
.map_err(|e| format!("解码失败:{:?}", e))?;
result.map_err(|e| format!("代付转账错误:{:?}", e))
}Deploy & Test
部署与测试
Deploy a Local ICRC-1 Ledger for Testing
部署本地ICRC-1测试账本
Add to :
icp.yamlPin the release version before deploying: get the latest release tag from https://github.com/dfinity/ic/releases?q=%22ledger-suite-icrc%22&expanded=false, then substitute it for in both URLs below.
<RELEASE_TAG>yaml
canisters:
icrc1_ledger:
name: icrc1_ledger
recipe:
type: custom
candid: "https://github.com/dfinity/ic/releases/download/<RELEASE_TAG>/ledger.did"
wasm: "https://github.com/dfinity/ic/releases/download/<RELEASE_TAG>/ic-icrc1-ledger.wasm.gz"
config:
init_arg_file: "icrc1_ledger_init.args"Create (replace with the output of ):
icrc1_ledger_init.argsYOUR_PRINCIPALicp identity principalPitfall: Shell substitutions likewill NOT expand inside this file. You must paste the literal principal string.$(icp identity principal)
(variant { Init = record {
token_symbol = "TEST";
token_name = "Test Token";
minting_account = record { owner = principal "YOUR_PRINCIPAL" };
transfer_fee = 10_000 : nat;
metadata = vec {};
initial_balances = vec {
record {
record { owner = principal "YOUR_PRINCIPAL" };
100_000_000_000 : nat;
};
};
archive_options = record {
num_blocks_to_archive = 1000 : nat64;
trigger_threshold = 2000 : nat64;
controller_id = principal "YOUR_PRINCIPAL";
};
feature_flags = opt record { icrc2 = true };
}})Deploy:
bash
undefined在中添加以下配置:
icp.yaml部署前请固定版本号:从https://github.com/dfinity/ic/releases?q=%22ledger-suite-icrc%22&expanded=false获取最新版本标签,然后替换下方两个URL中的`<RELEASE_TAG>`。
yaml
canisters:
icrc1_ledger:
name: icrc1_ledger
recipe:
type: custom
candid: "https://github.com/dfinity/ic/releases/download/<RELEASE_TAG>/ledger.did"
wasm: "https://github.com/dfinity/ic/releases/download/<RELEASE_TAG>/ic-icrc1-ledger.wasm.gz"
config:
init_arg_file: "icrc1_ledger_init.args"创建文件(将替换为命令的输出):
icrc1_ledger_init.argsYOUR_PRINCIPALicp identity principal注意:类似的Shell替换表达式在此文件中不会被展开。你必须粘贴字面量的principal字符串。$(icp identity principal)
(variant { Init = record {
token_symbol = "TEST";
token_name = "Test Token";
minting_account = record { owner = principal "YOUR_PRINCIPAL" };
transfer_fee = 10_000 : nat;
metadata = vec {};
initial_balances = vec {
record {
record { owner = principal "YOUR_PRINCIPAL" };
100_000_000_000 : nat;
};
};
archive_options = record {
num_blocks_to_archive = 1000 : nat64;
trigger_threshold = 2000 : nat64;
controller_id = principal "YOUR_PRINCIPAL";
};
feature_flags = opt record { icrc2 = true };
}})执行部署:
bash
undefinedStart local replica
启动本地节点
icp network start -d
icp network start -d
Deploy the ledger
部署账本
icp deploy icrc1_ledger
icp deploy icrc1_ledger
Verify it deployed
验证部署结果
icp canister id icrc1_ledger
undefinedicp canister id icrc1_ledger
undefinedInteract with Mainnet Ledgers
与主网账本交互
bash
undefinedbash
undefinedCheck ICP balance
查询ICP余额
icp canister call ryjl3-tyaaa-aaaaa-aaaba-cai icrc1_balance_of
"(record { owner = principal "$(icp identity principal)"; subaccount = null })"
-e ic
"(record { owner = principal "$(icp identity principal)"; subaccount = null })"
-e ic
icp canister call ryjl3-tyaaa-aaaaa-aaaba-cai icrc1_balance_of
"(record { owner = principal "$(icp identity principal)"; subaccount = null })"
-e ic
"(record { owner = principal "$(icp identity principal)"; subaccount = null })"
-e ic
Check token metadata
查询代币元数据
icp canister call ryjl3-tyaaa-aaaaa-aaaba-cai icrc1_metadata '()' -e ic
icp canister call ryjl3-tyaaa-aaaaa-aaaba-cai icrc1_metadata '()' -e ic
Check fee
查询手续费
icp canister call ryjl3-tyaaa-aaaaa-aaaba-cai icrc1_fee '()' -e ic
icp canister call ryjl3-tyaaa-aaaaa-aaaba-cai icrc1_fee '()' -e ic
Transfer ICP (amount in e8s: 100000000 = 1 ICP)
转账ICP(金额单位为e8s:100000000 = 1 ICP)
icp canister call ryjl3-tyaaa-aaaaa-aaaba-cai icrc1_transfer
"(record { to = record { owner = principal "TARGET_PRINCIPAL_HERE"; subaccount = null }; amount = 100_000_000 : nat; fee = opt (10_000 : nat); memo = null; from_subaccount = null; created_at_time = null; })" -e ic
"(record { to = record { owner = principal "TARGET_PRINCIPAL_HERE"; subaccount = null }; amount = 100_000_000 : nat; fee = opt (10_000 : nat); memo = null; from_subaccount = null; created_at_time = null; })" -e ic
undefinedicp canister call ryjl3-tyaaa-aaaaa-aaaba-cai icrc1_transfer
"(record { to = record { owner = principal "TARGET_PRINCIPAL_HERE"; subaccount = null }; amount = 100_000_000 : nat; fee = opt (10_000 : nat); memo = null; from_subaccount = null; created_at_time = null; })" -e ic
"(record { to = record { owner = principal "TARGET_PRINCIPAL_HERE"; subaccount = null }; amount = 100_000_000 : nat; fee = opt (10_000 : nat); memo = null; from_subaccount = null; created_at_time = null; })" -e ic
undefinedVerify It Works
验证功能
Local Ledger Verification
本地账本验证
bash
undefinedbash
undefined1. Check your balance (should show initial minted amount)
1. 查询自身余额(应显示初始铸造的金额)
icp canister call icrc1_ledger icrc1_balance_of
"(record { owner = principal "$(icp identity principal)"; subaccount = null })"
"(record { owner = principal "$(icp identity principal)"; subaccount = null })"
icp canister call icrc1_ledger icrc1_balance_of
"(record { owner = principal "$(icp identity principal)"; subaccount = null })"
"(record { owner = principal "$(icp identity principal)"; subaccount = null })"
Expected: (100_000_000_000 : nat)
预期结果:(100_000_000_000 : nat)
2. Check fee
2. 查询手续费
icp canister call icrc1_ledger icrc1_fee '()'
icp canister call icrc1_ledger icrc1_fee '()'
Expected: (10_000 : nat)
预期结果:(10_000 : nat)
3. Check decimals
3. 查询小数位数
icp canister call icrc1_ledger icrc1_decimals '()'
icp canister call icrc1_ledger icrc1_decimals '()'
Expected: (8 : nat8)
预期结果:(8 : nat8)
4. Check symbol
4. 查询代币符号
icp canister call icrc1_ledger icrc1_symbol '()'
icp canister call icrc1_ledger icrc1_symbol '()'
Expected: ("TEST")
预期结果:("TEST")
5. Transfer to another identity
5. 转账至另一个身份
icp identity new test-recipient --storage plaintext 2>/dev/null
RECIPIENT=$(icp identity principal --identity test-recipient)
icp canister call icrc1_ledger icrc1_transfer
"(record { to = record { owner = principal "$RECIPIENT"; subaccount = null }; amount = 1_000_000 : nat; fee = opt (10_000 : nat); memo = null; from_subaccount = null; created_at_time = null; })"
"(record { to = record { owner = principal "$RECIPIENT"; subaccount = null }; amount = 1_000_000 : nat; fee = opt (10_000 : nat); memo = null; from_subaccount = null; created_at_time = null; })"
icp identity new test-recipient --storage plaintext 2>/dev/null
RECIPIENT=$(icp identity principal --identity test-recipient)
icp canister call icrc1_ledger icrc1_transfer
"(record { to = record { owner = principal "$RECIPIENT"; subaccount = null }; amount = 1_000_000 : nat; fee = opt (10_000 : nat); memo = null; from_subaccount = null; created_at_time = null; })"
"(record { to = record { owner = principal "$RECIPIENT"; subaccount = null }; amount = 1_000_000 : nat; fee = opt (10_000 : nat); memo = null; from_subaccount = null; created_at_time = null; })"
Expected: (variant { Ok = 0 : nat })
预期结果:(variant { Ok = 0 : nat })
6. Verify recipient balance
6. 验证接收方余额
icp canister call icrc1_ledger icrc1_balance_of
"(record { owner = principal "$RECIPIENT"; subaccount = null })"
"(record { owner = principal "$RECIPIENT"; subaccount = null })"
icp canister call icrc1_ledger icrc1_balance_of
"(record { owner = principal "$RECIPIENT"; subaccount = null })"
"(record { owner = principal "$RECIPIENT"; subaccount = null })"
Expected: (1_000_000 : nat)
预期结果:(1_000_000 : nat)
undefinedundefinedMainnet Verification
主网验证
bash
undefinedbash
undefinedVerify ICP ledger is reachable
验证ICP账本可达
icp canister call ryjl3-tyaaa-aaaaa-aaaba-cai icrc1_symbol '()' -e ic
icp canister call ryjl3-tyaaa-aaaaa-aaaba-cai icrc1_symbol '()' -e ic
Expected: ("ICP")
预期结果:("ICP")
Verify ckBTC ledger is reachable
验证ckBTC账本可达
icp canister call mxzaz-hqaaa-aaaar-qaada-cai icrc1_symbol '()' -e ic
icp canister call mxzaz-hqaaa-aaaar-qaada-cai icrc1_symbol '()' -e ic
Expected: ("ckBTC")
预期结果:("ckBTC")
Verify ckETH ledger is reachable
验证ckETH账本可达
icp canister call ss2fx-dyaaa-aaaar-qacoq-cai icrc1_symbol '()' -e ic
icp canister call ss2fx-dyaaa-aaaar-qacoq-cai icrc1_symbol '()' -e ic
Expected: ("ckETH")
预期结果:("ckETH")
undefinedundefined