【ICP #3】DEX作る
UNCHAIN の課題備忘録
ICP の DEX
- ICP の安価なストレージを活かしデータの保存を全てオンチェーンで行う
- 従来のオーダーブック形式をとる DEX は、売買の注文をオフチェーンで行い取引の実行をオンチェーンで行う
2. 準備
2-1.機能
- II を使用したユーザ認証
- オリジナルトークンを faucet から受取
- DEX に対してトークンを入金・出金
- オーダーを作成し、取引を実行
2-2.環境チェック
dfx --version
node --version
2-3.プロジェクト作成
dfx new icp_basic_dex
フォルダ構成確認tree -L 1 -F -a icp_basic_dex
テストのため sample プロジェクト実行cd icp_basic_dex
プロジェクトをローカル環境へデプロイする前に、ローカルのキャニスター実行環境を起動する必要。キャニスター実行環境を立ち上げる。(ローカルのキャッシュ削除かつバックグラウンドで実行する)dfx start --clean --background
モジュールインストールnpm install
実行環境へキャニスター登録・ビルド・デプロイdfx deploy
サンプルプロジェクトでは 2 つのキャニスター(icp_basic_dex_backendicp_basic_dex_frontend)がデプロイ
開発サーバーを起動npm start
2-4.準備完了、キャニスター実行環境を停止
Ctrl+C でキャンセル。サーバーが停止しターミナル入力できるようになったら、dfx stop
3.DEX 基本機能実装
- オリジナルの fungible token を発行
- 規格にDIP20
- ERC-20 トークン標準を ICP でも利用できる
- オリジナルのトークンキャニスターをデプロイ
3-1. 設定ファイルにキャニスター書込
DIP20 のサブモジュールを取り込む
git submodule add https://github.com/Psychedelic/DIP20.git
- 設定ファイル
dfx.json
にキャニスター追加 - DFX を通じて開発者が実際にキャニスターとやりとりをする際に活用
トークンキャニスターのデプロイ
-
dfx start --clean --background
次にデプロイするユーザプリンシパル(識別子)を変数に登録。 -
export ROOT_PRINCIPAL=$(dfx identity get-principal)
-
dfx deploy GoldDIP20 --argument='("Token Gold Logo", "Token Gold", "TGLD", 8, 10_000_000_000_000_000, principal '\"$ROOT_PRINCIPAL\"', 0)'
キャニスターとやり取り -
dfx canister call GoldDIP20 getMetadata
他のトークンキャニスターも同様に。
4. キャニスター作成
DEX 上で扱うオリジナルのトークンをユーザー自身が取得できるよう、faucet 機能を付ける。Faucet の役割をするキャニスターを作成する。作成後、DIP20 キャニスターと Faucet キャニスターをデプロイして、DIP20 キャニスターの mint メソッドをコール ⇒ 一定量のトークンを保持させる。
motoko
- モジュール間で共通して使用したいユーザ定義の型や別のキャニスターのメソッドを呼びだすためのインターフェースを特定のファイルにまとめて定義し、インポートして使用する方法が一般的。
types.mo
に DIP20 キャニスターから呼び出したいメソッドや faucet キャニスターが使用するユーザ定義の型を記述(ROS でいう msg に近い形式か)main.mo
に faucet キャニスターのコードを記述する- hashMap は 3 つの引数を取る。第一引数に初期のサイズ、第二引数に HashMap の key 同士を比較するために使用する関数、第三引数に key をハッシュ化するために使用する関数を指定。Principal 型には equal と hash がそれぞれ定義されていて、ここではその関数を指定。
Faucet キャニスターの機能
-
ユーザにトークンを転送
-
トークンを渡したユーザのデータを保持(トークンの配布を一人のユーザに対し一度だけと制限するため)
-
トークンきゃにすたーの principal 受取、戻り値は faucet の型
-
配布可能かチェック
- ユーザが残高が配布する量より少ない場合エラー
- ユーザがトークンを受け取ってないかチェック
-
トークン付与の記録
- ユーザ principal とトークンを保存
テスト
test.sh
にシミュレーション書ける- default:DIP20 と Faucet キャニスターデプロイ
- user1, user2 がトークン取得
5. 入出金キャニスター
- dex 内にユーザが預けているトークンを取得して返す get 関数(トークン pricipal とトークン量のマップを返す、なかったら null)
- addToken 関数は入金を記録。balance_book のデータを更新。
- removeToken は出金を記録し balance_book 更新。戻り値
?Nat
で、最初に定義した get 関数と同様 Option 型を返す - 残高が 0、不足してたらエラー、問題なければ残高を返す
- hashEnoughBalance は dex 内に入金されてるユーザのトークン量が十分か確認するときにコールされる関数
- types.mo に DIP20 キャニスターからコールしたい関数・扱う型と、DEX が使用するユーザー定義の型を追加
5-2. main.mo, types.mo 修正し、入金、出金の機能実装
deposit 関数
- deposit カンスはユーザが DEX にトークン入金する際にコールされる
- トークンの転送処理が重要
- DEX 内(icp_basic_dex_backend キャニスター)にユーザのトークンを転送。ただし transfer メソッドではできない、トークン所有者はユーザのため、transferFrom を使用する。
- 転送前にユーザのトークン残高 allownce メソッドで取得。fee に満たない場合エラーを返す。
- 問題なければ transferFrom メソッドでユーザから DEX 内にトークンを転送。転送されるのは手数料を引いた分になる
- 転送官僚したら、balance_book モジュールの addToken を呼び、dex 内のトークンデータを更新
withdraw 関数
- ユーザが DEX からトークンを出金する際にコールされる
- DEX ないにユーザが保有するトークン量と引き出したい量を比較。不足ならエラー
- 問題なければ transfer で転送
- 転送完了したら、balance_book の更新
fetch_dif_fee は内部関数
- 転送を行うトークンの手終了取得
- getMetadata メソッドをコールし、データの取得可能.データ型は types.mo に定義
getBalance 関数
- 引数で渡されたユーザが dex 内で保有するトークンの量を返す
テスト
rm -rf .dfx
dfx start --clean
bash ./scripts/test.sh
6. オーダー作成機能
BalanceBook
モジュールをインポート。取引が成立するものがあれば取引を実行する、取引を実行する際に DEX 内のトークンデータを書き換える必要がある。そのために balancebook モジュールの関数をコールしたいのでインポート
6-1. var orderes
- オーダーはマップ構造で保存。各オーダーに割り振られる ID でオーダーの検索を簡素化する
6-2. getOrders, getOrder
保存されているオーダーを取得する関数。main.mo に定義した getBalance 関数同様に getOrder 関数は引数に渡されたオーダーの ID に応じてデータを返す
6-3. cancelOrder, addOrder
- addOrder 内で detectMatch 関数をコール(成立する関数があるか確認する関数)
- for 文で全てのオーダーを確認し取引が成立する if 文の条件に一致した場合 processTrade をコール。
6-4. ProcessTrade
-
実際にトークンの保有データを更新
userX
[ TGLD -100, TSLV + 100]
-
取引の内容で order_x の作成者のトークン残高を更新
userY
[ TSLV -100, TGLD + 100]
-
取引の内容で order_y のトークン残高を更新
-
取引が完了したオーダーを削除して終了
ここまででオーダーを作成して取引を行う部分が完了。
6-5. main.mo の編集 取引機能を完成
Exchange モジュールをmain.mo
からコールしてユーザが取引を実行できるようにする。そのためにはtypes.mo
を編集してオーダーに関するデータ型を追加する必要がある。
- withdraw 関数について出金を行う際に、ユーザーが出しているオーダーに対して DEX 内のトークンが不足した場合、オーダーを削除する