dfxコマンドを用いたICRC-1/ICRC-2トークンの操作
はじめに
ICP Chain上には、Chain-Key Cryptographyという暗号技術によりBTCと1:1に対応づいている『ckBTC』、ETHと1:1に対応づいている『ckETH』、その他Ethereum上のERC-20トークンと1:1に対応づいている『ckUSDC』や『ckUSDT』などのトークンがあります。
これらのトークンはICPのICRC-1/ICRC-2と呼ばれるトークン規格に準拠していますので、種類によらず同じように取り扱うことができます。
本記事では、ICRC-1/ICRC-2に準拠したトークンを支払い等に利用するための基本的な取り扱い方法について、dfx canisterコマンドを用いて解説します。
FrontendやBackendのプログラムから取り扱う方法は改めて記事を書こうと思いますが、まずはdfx canisterコマンドを使って基本を押さえておくとよいでしょう。
1. ローカル実行環境の準備
ICPのMainnetで試すと実際にコストがかかってしまいますので、開発者向けのローカル実行環境上にテスト用ckUSDCトークンを用意して検証します。
(1) プロジェクトの作成
dfx newコマンドでプロジェクトを用意します。
本記事の範囲では、BackendやFrontendは開発しませんので、開発言語等は任意で構いません。
$ dfx new --frontend=react --type rust icptest
$ cd icptest
(2) dfx.jsonの修正
テスト用トークンのLedger Canisterを追加します。
本記事では、テスト用トークンとして『ckUSDC』相当をローカル実行環境に用意します。
{
"canisters": {
︙
"ckUSDC_ledger_canister": {
"type": "custom",
"candid": "https://github.com/dfinity/ic/releases/download/ledger-suite-icrc-2025-06-19/ledger.did",
"wasm": "https://github.com/dfinity/ic/releases/download/ledger-suite-icrc-2025-06-19/ic-icrc1-ledger.wasm.gz",
"specified_id": "xevnm-gaaaa-aaaar-qafnq-cai",
"remote": {
"id": {
"ic": "xevnm-gaaaa-aaaar-qafnq-cai"
}
},
"init_arg": "(variant { Init = record { decimals = opt 6; token_symbol = \"ckUSDC\"; token_name = \"ckUSDC\"; minting_account = record { owner = principal \"sv3dd-oaaaa-aaaar-qacoa-cai\"; }; transfer_fee = 10_000; metadata = vec {}; feature_flags = opt record { icrc2 = true }; initial_balances = vec { record { record { owner = principal \"2vxsx-fae\"; }; 1_000_000_000_000; }; }; archive_options = record { num_blocks_to_archive = 1000; trigger_threshold = 2000; max_message_size_bytes = null; cycles_for_archive_creation = opt 100_000_000_000_000; node_max_memory_size_bytes = opt 3_221_225_472; controller_id = principal \"2vxsx-fae\"; } } })"
}
}
︙
}
-
"candid"、"wasm"は2025/7/20時点のモジュールです。
以下で最新モジュールのバージョンを確認してください。
https://github.com/dfinity/ic/releases?q="ledger-suite-icrc" -
"specified_id"でローカル実行環境のCanister IdをckUSDCと同じ値にします。 -
"remote"を指定することで、Mainnetへのデプロイを抑止します。 -
"init_arg"でCanisterの初期設定ができます。-
dfx deploy --argumentで初期化する手間が省けますが、dfx.jsonには、シェル上でのコマンド実行と違って変数が使用できないため、initial_balances、controller_idのPrincipal値を2vxsx-fae(anonymous)とします。(ローカル実行環境限定であれば問題なし) -
"minting_account"に2vxsx-fae(anonymous)は指定できないらしいので、Mainnetと同じsv3dd-oaaaa-aaaar-qacoa-caiとします。(mintしない場合は何でもよい)
-
(3) ローカル実行環境の起動・デプロイ
dfx startコマンドでローカル実行環境を起動して、dfx deployコマンドでデプロイします。
$ dfx start --clean --background
$ dfx deploy
︙
Deployed canisters.
URLs:
︙
Backend canister via Candid interface:
ckUSDC_ledger_canister: http://127.0.0.1:4943/?canisterId=uzt4z-lp777-77774-qaabq-cai&id=xevnm-gaaaa-aaaar-qafnq-cai
※本記事ではdfx canisterコマンドを使用した方法を解説しますが、Candid interfaceでも開発環境のLedger Canisterを操作することはできます。
2. 残高照会
初期状態では、dfx.jsonの"init_arg" > "initial_balances"値により、anonymous (2vxsx-fae) に 1_000_000_000_000 (ckUSDCはdecimals=6なので、1,000,000,000,000 ÷
以下のコマンドを実行して残高照会してみましょう。残高照会はどのidentityからでも行えますので、--identityオプションは指定不要です。
$ dfx canister call ckUSDC_ledger_canister icrc1_balance_of "(record { owner = principal \"2vxsx-fae\" })"
(1_000_000_000_000 : nat)
3. トークン所有者による送金
トークン所有者による送金について解説します。
ckUSDC Ledger Canisterのicrc1_transfer()メソッドを呼び出します。
所有者であるanonymous (User A)から指定したPrincipal (User B)に、100_000_000 (100 ckUSDC) を送金するコマンドの例を以下に示します。
$ dfx canister --identity anonymous call ckUSDC_ledger_canister icrc1_transfer "(record { to = record { owner = principal \"<PRINCIPAL>\" }; amount = 100_000_000 })"
(variant { Ok = 1 : nat })
-
--identity <IDENTITY>オプションによりdfx canisterコマンドを実行するidentityを指定できます。 -
<PRINCIPAL>の部分には受け取り側(User B)のPrincipalを指定してください。
dfx identity new <IDENTITY>コマンドでidentityを作成した場合、
dfx identity --identity <IDENTITY> get-principalコマンドでPrincipalを確認できます。 - トランザクション手数料として所有者側(User A)に
10_000(0.01 ckUSDC) がかかります。
4. 受け取り側による集金
『3. トークン所有者による送金』では、所有者からのアクションによってトークンを送る仕組みでしたが、受け取り側がサービスを提供したタイミングで集金したいというケースも考えられます。
この場合、あらかじめトークン所有者の方で、指定数量までの送金許可(icrc2_approve()メソッド)を実行してもらっておき、受け取り側のアクションによって送信 (icrc2_transfer_from()メソッド)を実行するという流れになります。
User A → User Bに1_000_000 (1 ckUSDC)を支払う流れを解説します。
(1) icrc2_approve()
まずトークンの所有者 (User A)が受け取り側 (User B)に指定した数量までのトークン送信を許可します。
dfxコマンドの実行例を以下に示します。
$ dfx canister call --identity anonymous ckUSDC_ledger_canister icrc2_approve "(record { amount = 1_000_000; spender = record { owner = principal \"<PRINCIPAL>\" } })"
(variant { Ok = 2 : nat })
-
<PRINCIPAL>の部分には受け取り側 (User B)のPrincipalを指定してください。 - トランザクション手数料として所有者側(User A)に
10_000(0.01 ckUSDC) がかかります。 -
icrc2_approve()メソッドには、引数expires_atで有効期限を設定することもできます。
(2) icrc2_allowance()
必須ではありませんが、受け取り側 (User B)では集金を行う前に、集金できることを事前に確認しておくとよいでしょう。
dfxコマンドの実行例を以下に示します。
$ dfx canister call ckUSDC_ledger_canister icrc2_allowance "(record { account = record { owner=principal \"2vxsx-fae\" }; spender = record { owner = principal \"<PRINCIPAL>\" } })"
(record { allowance = 1_000_000 : nat; expires_at = null })
-
<PRINCIPAL>の部分には受け取り側 (User B)のPrincipalを指定してください。
(3) icrc2_transfer_from()
受け取り側 (User B)からのアクションにより集金を行います。トランザクション手数料と合わせて許可された 1_000_000 (1 ckUSDC)となるようにコマンドを実行します。
$ dfx canister --identity <IDENTITY> call ckUSDC_ledger_canister icrc2_transfer_from "(record { amount = 990_000; from = record { owner = principal \"2vxsx-fae\" }; to = record { owner = principal \"<PRINCIPAL>\" } })"
(variant { Ok = 3 : nat })
-
<IDENTITY>の部分は受け取り側 (User B)のidentityを指定してください。 -
<PRINCIPAL>の部分には受け取り側 (User B)のPrincipalを指定してください。
参考情報
ICRC仕様
ICRC-1 standard specification
ICRC-2
Chain-Key Tokens
ckUSDC以外のckXXXXトークンは、以下の情報が参考になります。
開発するアプリケーションで対応するトークンに合わせてdfx.jsonに定義を追加するとよいでしょう。
| Symbol | Ledger Canister Id | Decimal | Fee |
|---|---|---|---|
| ckBTC | mxzaz-hqaaa-aaaar-qaada-cai | 8 | 10 |
| ckETH | ss2fx-dyaaa-aaaar-qacoq-cai | 18 | 2_000_000_000_000 |
| ckUSDC | xevnm-gaaaa-aaaar-qafnq-cai | 6 | 10_000 |
| ckUSDT | cngnf-vqaaa-aaaar-qag4q-cai | 6 | 10_000 |
ICP Chain上にはckXXXX以外にも様々なトークンがありますが、ICRC-1/ICRC-2規格に準拠していれば、基本的に同様に扱うことができます。
dfx.jsonドキュメント
ICRC-1 ledger local setup
How to make fake ckUSDT CKBTC locally for testing
Discussion