【Sui】marimoNFTをSuiで実装するパターン - Kioskパターン
本記事は、以前執筆した以下の記事とは別の新しいパターンについて言及をしています
この記事を書いてから2ヶ月が経過し、少しではありますがSui, Moveについての知識が深まりました
いろいろと苦戦しながらも3つの実装パターンに辿り着きました。結果的に採用できそうなのは Dynamic Object Fields と Transfer to Objectを使うパターンだと思います
おそらくこれら以外のアプローチも存在します。1つはKioskです。Kioskも試してみたいなと思ったのですがまだキャッチアップできていないので、これは宿題とします
記事にも書いたように、 Kiosk
を使うパターンもありなのではないかと思っていたので、Kioskのドキュメントを読むことにしました
Kioskは、開発者ではなくとも、SuiでNFTを購入するユーザーにとっては聞いたことのある言葉だと思います。ただ、よく聞く言葉・概念ではあるけれども、NFTと何が違うんじゃい?と思っている方も多いのではないでしょうか
なんなら、NFTとは違って簡単に他のアドレスに転送することもできないし、転送するにはTradeportを経由しないといけなくて、Kioskを嫌いになっているかもしれません
かくいう筆者も、KioskいらねーNFTだけでいいよ、と思っていた時期もありました。ただよく調べてみると、これは画期的なModuleでした
Kiosk is 何
結局Kioskってなんなの? よく日本の駅にあるやつ...? (筆者は使った記憶ほぼなし)
とりあえずドキュメントを見てみましょう
KioskはSui上のコマースアプリケーションのための分散型システムです。 Kioskオブジェクトは、各パーティが所有する共有オブジェクトで構成され、アセットを保管し、オークションなどのカスタム取引機能を利用することができます。 高度に分散化されている一方で、Sui Kioskは一連の強力な保証を提供します。 Kioskの所有者は、購入の瞬間までアセットの所有権を保持します。 クリエイターは、カスタムポリシー(すべての取引に適用されるルールセット)を設定します(ロイヤリティの支払いや任意のアクションXなど)。 マーケットプレイスは、Kioskオブジェクトが発するイベントをインデックス化し、オンチェーンアセット取引のために単一のフィードを購読することができます。 実質的に、KioskはSuiフレームワークの一部であり、システムにネイティブで、誰でもすぐに利用できます。
ん....? パーティ? カスタムポリシー?
わからん....!!!
ドキュメントだけではよくわからなかったので、実際のコードも読んでみました。それでやっと概要が理解できた気がします
筆者なりにKioskを1行で表現するとしたら、
「SUI支払い専用のオリジナルマーケットプレイスを作るための便利なツール」
と答えるでしょう
なので、今回実装したいペット育成ゲームではKioskをそのまま利用するのは難しそうでした。なぜならペットを販売したり購入する要件はないからです
Kioskについての解説記事は別途書く予定ですが、今回はKioskからヒントを得た実装パターンを適用してみたいと思います
Kioskからヒントを得た実装パターン
以前書いた記事では、 Wrapped Object
, Dynamic Object Fields
, Transfer to Object
を活用しました
Kioskではこのうち Dynamic Object Fields
を利用した設計になっていたので、強いていえば 実装パターン2: Dynamic Object Fields がKioskに少し似ているパターンかもしれません
ただ実装パターン2には拡張性がありませんでした。なぜなら、1つの公園に、1匹のペットしか放せない仕様になっていたからです
本来はKioskのように、ペットオーナーは複数のKioskを持つことができ、各Kioskに複数のNFTを格納できるのが理想です。つまり、複数の公園を持ち、各公園に複数のペットを放つことができれば、いろんな要件に対応することができそうですよね
では実装したコードを見てみましょう(一部抜粋、 全コードはこちら)
module pet::park;
// === Structs ===
public struct Park has key, store {
id: UID,
owner: address,
pet_count: u32,
}
public struct ParkOwnerCap has key, store {
id: UID,
`for`: ID,
}
public struct PetKey has store, copy, drop { id: ID }
public fun new(ctx: &mut TxContext): (Park, ParkOwnerCap) {
let park = Park {
id: object::new(ctx),
owner: ctx.sender(),
pet_count: 0,
};
let cap = ParkOwnerCap {
id: object::new(ctx),
`for`: object::id(&park),
};
(park, cap)
}
public fun release<T: key + store>(
self: &mut Park, cap: &ParkOwnerCap, pet: T
) {
assert!(has_access(self, cap), 1);
self.pet_count = self.pet_count + 1;
dof::add(&mut self.id, PetKey { id: object::id(&pet) }, pet);
}
public fun retrieve<T: key + store>(
self: &mut Park, cap: &ParkOwnerCap, id: ID
): T {
assert!(has_access(self, cap), 1);
self.pet_count = self.pet_count - 1;
dof::remove(&mut self.id, PetKey { id })
}
public(package) fun borrow_mut<T: key + store>(
self: &mut Park, id: ID
): &mut T {
dof::borrow_mut(&mut self.id, PetKey { id })
}
module pet::pet;
// === Structs ===
public struct Pet has key, store {
id: UID,
cute: u64,
}
public fun create(ctx: &mut TxContext): Pet {
Pet {
id: object::new(ctx),
cute: 0,
}
}
public fun naderu_at_park(park: &mut Park, id: ID) {
let pet = park.borrow_mut(id);
naderu(pet);
}
public fun naderu(pet: &mut Pet) {
pet.cute = pet.cute + 1;
}
Kioskのコードを見たことがある人にとっては既視感があるかもしれません。そうです、 Kiosk
が Park
に、 KioskOwnerCap
が ParkOwnerCap
に対応している形で、Kioskパターンを参考にしています。またKioskでいう place
/take
(KioskにNFTを格納する、取り出す) が、 release
/retrieve
(公園にペットを放つ、戻す)になっていることにも気づいたかもしれません
これによって、複数の公園を作ることができますし、その公園で複数のペットを放したり戻したりできるようになりました
Kioskと異なるのは borrow_mut
です。 Kioskではこれは KioskOwnerCap
が必要な公開関数ですが、今回のパターンでは ParkOwnerCap
が不要した代わりに、 公開(パッケージ)関数としています
こうすることで、petモジュールにある naderu_at_park
関数を実装することができるようになっています。つまり、公園にいるペットに対して、 mutable
な操作ができるようにしているということですね
まとめ
Kioskはオリジナルのマーケットプレイスを作るためのモジュールのため、拡張性に優れており、今後dAppを作る上でとても参考になるコードベースだということがわかりました
今回実装したパターンを使って、簡単なペット育成ゲームを作ることができたら面白そうだなとも思いました
また、実はKioskには KioskExtension
という Kioskをさらに拡張するモジュールがあるようでした。もしかするとこれを使えば、今回の独自実装も不要になるかもしれないので、今後の宿題としたいと思います
Discussion