メインコンテンツまでスキップ

lesson-3_DEXのトークンを管理する機能を実装しよう

DEX 内のトークンを管理するモジュールを実装しよう

このレッスンでは、ユーザーが取引のためにDEXへ入金したトークンを管理するBalanceBookモジュールを実装していきます。

BalanceBookモジュールが持つ機能は、以下になります。

  • ユーザーとDEXに入金されたトークンのデータを紐付けて保存する
  • 取引・出金時にトークン残高が十分か確認をする

それでは、実装をしていきましょう。

まずは、Faucetキャニスターを実装した時と同様にtypes.moファイルを作成します。場所は、サンプルプロジェクトのmain.moファイルと同じ階層になります。

touch src/icp_basic_dex_backend/types.mo

[types.mo]

module {
public type Token = Principal;
}

続いて、BalanceBookモジュールを実装するファイルを作成します。こちらも、サンプルプロジェクトのmain.moファイルと同じ階層に作成します。

touch src/icp_basic_dex_backend/balance_book.mo

作成したbalance_book.moファイルに、まずはライブラリのインポート文とデータを保存する変数を記述します。

[balance_book.mo]

import Iter "mo:base/Iter";
import Principal "mo:base/Principal";
import HashMap "mo:base/HashMap";

import T "types";

module {
public class BalanceBook() {

// ユーザーとトークンの種類・量をマッピング
var balance_book = HashMap.HashMap<Principal, HashMap.HashMap<T.Token, Nat>>(10, Principal.equal, Principal.hash);
};
};

ユーザーとトークンを紐づけて保存するbalance_bookは、HashMapの入れ子になっています。少し複雑ですが、Faucetキャニスターを実装した際のデータ構造と比較すると、配列部分がHashMapになった構造をしています。以下のようなイメージとなります。

{
user1 : {
{tokenA : 100},
{tokenB : 100},
},
user2 : {
{tokenA : 200},
},
user3 : {
{tokenB : 50},
{tokenC : 100},
},
}
var balance_book = HashMap.HashMap<Principal, HashMap.HashMap<T.Token, Nat>>(10, Principal.equal, Principal.hash);

続いて、関数を定義していきます。以下のように関数をbalance_book変数の下に追加してください。

[balance_book.mo]

module {
public class BalanceBook() {

// ユーザーとトークンの種類・量をマッピング
var balance_book = HashMap.HashMap<Principal, HashMap.HashMap<T.Token, Nat>>(10, Principal.equal, Principal.hash);

+ // ユーザーに紐づいたトークンと残高を取得
+ public func get(user : Principal) : ?HashMap.HashMap<T.Token, Nat> {
+ return balance_book.get(user);
+ };
+
+ // ユーザーの預け入れを記録する
+ public func addToken(user : Principal, token : T.Token, amount : Nat) {
+ // ユーザーのデータがあるかどうか
+ switch (balance_book.get(user)) {
+ case null {
+ var new_data = HashMap.HashMap<Principal, Nat>(2, Principal.equal, Principal.hash);
+ new_data.put(token, amount);
+ balance_book.put(user, new_data);
+ };
+ case (?token_balance) {
+ // トークンが記録されているかどうか
+ switch (token_balance.get(token)) {
+ case null {
+ token_balance.put(token, amount);
+ };
+ case (?balance) {
+ token_balance.put(token, balance + amount);
+ };
+ };
+ };
+ };
+ };
+
+ // DEXからトークンを引き出す際にコールされる
+ // トークンがあれば更新された残高を返し、なければ`null`を返す
+ public func removeToken(user : Principal, token : T.Token, amount : Nat) : ?Nat {
+ // ユーザーのデータがあるかどうか
+ switch (balance_book.get(user)) {
+ case null return (null);
+ case (?token_balance) {
+ // トークンが記録されているかどうか
+ switch (token_balance.get(token)) {
+ case null return (null);
+ case (?balance) {
+ if (balance < amount) return (null);
+
+ // 残高と引き出す量が等しい時はトークンのデータごと削除
+ if (balance == amount) {
+ token_balance.delete(token);
+ // 残高の方が多い時は差し引いた分を再度保存
+ } else {
+ token_balance.put(token, balance - amount);
+ };
+ return ?(balance - amount);
+ };
+ };
+ };
+ };
+ };
+
+ // ユーザーが`balance_book`内に`amount`分のトークンを保有しているかを確認する
+ public func hasEnoughBalance(user : Principal, token : T.Token, amount : Nat) : Bool {
+ // ユーザーデータがあるかどうか
+ switch (balance_book.get(user)) {
+ case null return (false);
+ case (?token_balance) {
+ // トークンが記録されているかどうか
+ switch (token_balance.get(token)) {
+ case null return (false);
+ case (?balance) {
+ // `amount`以上残高ありで`true`、なしで`false`を返す
+ return (balance >= amount);
+ };
+ };
+ };
+ };
+ };
};
};

4つのpublic関数を定義しました。順番に見ていきましょう。

最初のget関数は、ユーザーがDEX内に預けているトークンの情報を取得して返す関数です。ポイントは戻り値?HashMap.HashMap<T.Token, Nat>です。**?**をつけた型はOption型と呼ばれ、この場合、何かしらのエラーとなった場合にはnullを返し、それ以外の場合は指定した型の値を返すことができるようになります。

public func get(user : Principal) : ?HashMap.HashMap<T.Token, Nat>

HashMapが持つget関数自体がOption型を返します。トークンの情報が取得できた場合はトークンPrincipalとトークン量のマップを返し、取得できなかった場合はそのままnullを返します。

return balance_book.get(user);

2つ目のaddToken関数は、入金を記録する関数になります。引数にユーザー Principal、トークンキャニスター Principal、トークン量を受け取ります。ユーザーデータが既に記録されているかどうか、トークンデータが既に記録されているかどうがで場合分けを行いbalance_bookのデータを更新します。

3つ目のremoveToken関数は、出金の際balance_book内のデータを更新する関数になります。ポイントは戻り値?Natです。最初に定義したget関数同様に、Option型を返します。

public func removeToken(user : Principal, token : T.Token, amount : Nat) : ?Nat

ユーザーがトークンを引き出す際、残高が0または不足する場合にはエラーとしてnullを返します。問題がなければ、出金後の残高を返します。出金後にトークン残高が0になるようであれば、トークンデータをbalance_bookから削除します。

// 残高と引き出す量が等しい時はトークンのデータごと削除
if (balance == amount) {
token_balance.delete(token);
// 残高の方が多い時は差し引いた分を再度保存
} else {
token_balance.put(token, balance - amount);
}

最後のhasEnoughBalance関数は、DEX内に入金されているユーザーのトークン量が十分かどうかを確認したい際にコールされる関数です。

public func hasEnoughBalance(user : Principal, token : T.Token, amount : Nat) : Bool

戻り値はBool型で、引数amount分のトークンがあればtrueを返し、なければfalseを返します。

ここまでで、DEX内に入金されたトークンを管理する機能ができました!

🙋‍♂️ 質問する

ここまでの作業で何かわからないことがある場合は、Discordの#icpで質問をしてください。

ヘルプをするときのフローが円滑になるので、エラーレポートには下記の4点を記載してください ✨

1. 質問が関連しているセクション番号とレッスン番号
2. 何をしようとしていたか
3. エラー文をコピー&ペースト
4. エラー画面のスクリーンショット

次のレッスンに進んで、DEXに入金・出金をする機能を実装していきましょう!