見出し画像

フルオンチェーンIsekai Battleで使えそうなコントラクト開発 その1

以前、フルオンチェーンIsekai Battleの個人的な楽しみ方を紹介しました。

フルオンチェーンBCGなので今までのゲームとは違いコントラクトを作成して自分が欲しい機能を実現させることが出来ます。
今後のバトルモードの実装後の武器・防具の使いみちとして、ゲームと言えば武器防具の交換とかの機能があったら面白いかも。という興味とコントラクトの勉強のために、実装をしていきたいと思います。

実現させたいこと

Isekai Battleでは攻撃・防御でパーティーを編成するので、所有しているキャラNFT次第では余ってしまう武器・防具が出てくる可能性があります。
武器に関しては、自分で攻撃するタイミングが出てくると思うので、その時に必要な武器がほしい時があることが想定されます。
フラグメントNFTを奪い合うゲームなので誰かと協力することなく、自ら探索で手に入れた武器やOpenSeaなどのマーケットで売買したものを使用すると思いますが、ギルドを作って複数人で協力して戦うのも面白いのかもしれないので、その時に使えるコントラクトとして作ろうと思います。

今回はその1 としてコントラクトの作成とUnitTestを通してどの様に動作するかの確認を行っていきます。

基本機能

  • ギルドの様に特定のチーム内で武器や防具を交換できる

  • 武器、防具にはレベルがあるので、同じレベルのみ交換できる

  • なるべくガス代を掛けずに実現させる

  • 自分の必要な武器、防具以外の余っているやつのみ交換可能

実装

作成物

  1. 交換用のコントラクト

  2. ローカルで動かすために、Isekai Battleの武器・防具が採用しているERC-1155のNFTコントラクト

  3. UnitTest

コントラクトの準備と設定

デプロイと交換対象のコントラクト登録

サンプル用のERC-1155のコントラクトと交換用のコントラクトをデプロイ

const Item = await ethers.getContractFactory("WeaponItem");
const item = await Item.deploy();

const ItemExchange = await ethers.getContractFactory("ItemExchange");
const itemExchange = await ItemExchange.deploy();

交換するコントラクトを交換用コントラクトに登録

await itemExchange.setWeaponNFT(item.address);

サンプル用のERC-1155のNFT登録

今回は武器のみを設定

await item.addItemConfig(1, "Sword Lv1");
await item.addItemConfig(2, "Sword Lv2");
await item.addItemConfig(3, "Spear Lv1");
await item.addItemConfig(4, "Spear Lv2");

ここでは、ID:1に剣のLv1、ID:2に剣のLv2、ID:3に槍のLv1、ID:4に槍のLv2を登録しているので、それぞれのユーザーはこのNFTを探索等で所有していくイメージです。

各々の武器のNFTをmintして所有の状態を作る

const addressList = await ethers.getSigners();
const userAddress1 = addressList[1];
await item.connect(userAddress1).mint(1, 10);
await item.connect(userAddress1).mint(2, 10);
await item.connect(userAddress1).mint(3, 10);
await item.connect(userAddress1).mint(4, 10);

const userAddress2 = addressList[2];
await item.connect(userAddress2).mint(1, 5);
await item.connect(userAddress2).mint(2, 5);
await item.connect(userAddress2).mint(3, 5);
await item.connect(userAddress2).mint(4, 5);

2ユーザー間での交換を行う必要があるため、ユーザー1は各4つのNFTを10個mintして、ユーザー2は各4つのNFTを5個mintしておきます。

特定メンバーのみアクセスできる仕組みとしてMerkleTreeを登録

const leaves = [userAddress1.address, userAddress2.address].map((x) => keccak256(x));
const tree = new MerkleTree(leaves, keccak256, { sortPairs: true });
const rootTree = tree.getHexRoot();
await itemExchange.setMerkleRoot(rootTree);

ユーザー1とユーザー2がギルドに所属していると仮定してMerkleTreeを作成してそのアドレスと交換用のコントラクトに登録します。
コントラクト内では、このMerkleRootを使用してギルドメンバーかどうかのチェックを行います。
ギルドメンバーは適宜増減することを想定しているので今回はMerkleTreeを使用しています。
大規模な人数にならないとは思うので単純にアクセスできるアドレスの登録・削除でもいいと思いますが、NFTプロジェクトでMerkleTreeをよく使うと思うので、個人的な勉強として今回は使用しています。

武器のグループ登録

武器や防具にはレベルがあり、1:1で交換するのは同じレベルでのみ行いたいので、ERC-1155のNFTのIDのレベルが同じものを同じグループとして登録します。

await itemExchange.addWeaponGroupingConfigs({ groups: [1,3]});
await itemExchange.addWeaponGroupingConfigs({ groups: [2,4]});

サンプル用のNFTはIDが1と3がLv1。IDの2と4がLv2なので、それぞれを同じグループとして交換用のコントラクトに登録します。

利用ユーザーの交換設定

ここまでで必要なコントラクトの準備や設定が終わりました。
次は各ユーザーの設定を見ていきます。
交換を利用するユーザーは自分が余っている武器・防具の設定を行う必要があるため、コントラクトにその設定を行います。

const addressList = await ethers.getSigners();
const userAddress1 = addressList[1];

await item.connect(userAddress1).setApprovalForAll(itemExchange.address, true);

const userAddressHexProof1 = hexProofs[0];
await itemExchange.connect(userAddress1).setWeaponConfig(1, 4, userAddressHexProof1);

ユーザー1が交換用コントラクトに武器・防具のコントラクトの転送の権限を許可します。
そして武器や防具の最低限自分が必要な個数を設定します。設定時にギルドメンバーの検証を行うためのMerkleTreeの検証に必要な情報を取得してコントラクトの関数を呼び出します。
ここではID:1(剣のLv1)は4つは必要。という設定を行っています。
これでユーザー1の剣のLv1は4個よりも多いものは余りとして交換可能な状態になります。

交換の実行と動作検証

交換

交換は別のユーザーが行うので、今回はユーザー2が交換の関数を実行します。

const userAddress2 = addressList[2];
await item.connect(userAddress2).setApprovalForAll(itemExchange.address, true);

const userAddressHexProof2 = hexProofs[1];
await itemExchange.connect(userAddress2).exchangeWeapon(userAddress1.address, 1, 2, 3, userAddressHexProof2);

ユーザー2も自分のNFTを転送することになるので、交換用コントラクトに武器・防具のコントラクトの転送の権限を許可します。
実際の交換を行うために、メンバー検証のための情報取得して関数を呼び出します。
今回は相手がユーザー1なので、ユーザー1のアドレスと交換したいIDと個数、自分の交換するIDを設定して関数を呼び出します。
ここでは、ユーザー1の剣のLv1と自分(ユーザー2)の槍のLv1を2個交換するようにしています。

動作検証

武器・防具は事前にmintしておいたので、交換前の状態でユーザー1は剣のLv1を10個、槍のLv1を10個所有。ユーザー2は剣のLv1を5個、槍のLv1を5個所有していました。
この交換を行うことで、ユーザー1の剣のLv1は8個、槍のLv1は12個になります。ユーザー2は剣のLv1が7個、槍のLv1は3個になるはずです。

expect(await item.balanceOf(userAddress1.address, 1)).to.equal(8);
expect(await item.balanceOf(userAddress2.address, 1)).to.equal(7);

expect(await item.balanceOf(userAddress1.address, 3)).to.equal(12);
expect(await item.balanceOf(userAddress2.address, 3)).to.equal(3);

このような形でユーザー1とユーザー2の武器の所有数を確認します。
これでUnitTestが通ることが確認できたので、交換は成功しています。

まとめ

この様にコントラクトの基本的な動きとして、UnitTestで動作確認することが出来ました。
ここから細かく下記のような確認をUnitTestをどんどん追加して行こうと思っています。

  • ギルドメンバー以外での設定や実行が出来ない確認

  • 交換対象のグループ外を指定して交換を実行した場合の確認

  • 最低限自分が所有しておきたい数未満の時に交換が出来ない

今回の確認で基本的なやりたいことはコントラクトで実装できそうです。

フルオンチェーンNFTなので、どこかに余りを預けてその範囲は交換しても良い。みたいな形を取ると、交換用コントラクトに転送するガス代。自分が交換されたものを使う場合に交換用コントラクトから転送するガス代が発生してしまうため、NFT自体は所有したまま余り分だけを自由に転送できる仕組みでガス代のコストを下げています。
この場合交換を実行する側が交換するNFTの転送のガス代を負担する事にはなりますが、自分が必要な時の交換なのでそこのガス代は必要経費と思ってもらいます。

まだまだ細かいUnitTestを作成して品質を上げていきますが、UnitTestで実行していることをFEで実装したら機能としては完成します。

開発の流れとしてはUnitTest、ローカルテスト、テストネット、メインネット。のような形で動かせていけそうです。
Isekai Battleに関しては、テストネットにデプロイしているのでここで実際に試せるのは楽しそうです!

この記事が気に入ったらサポートをしてみませんか?