フロントエンドチーム小林です。
今回は、イーサリアムの規格であるERC1155を使って、コントラクトの実装からフロントエンドの実行まで一通りの動作を行なってみたいと思います。
ERC1155とは
ERC1115とは、一つのスマートコントラクトを使用して、一度に複数のトークンを表すことができるというものです。たくさんのトークンを必要とするプロジェクトでは、ガス代が多くかかってしまう問題があります。その場合、ERC1155を用いることで、大幅にガス代を節約することができます。
https://docs.openzeppelin.com/contracts/3.x/erc1155
作るもの
管理者が、Metamaskに接続し、アイテムをユーザーに送るボタンを押します。その後マイページのユーザーがMetamaskに接続し、もらったアイテムを表示するボタンを押すと、先ほど管理者が送ったアイテムが表示されます。送るアイテムは今回は固定で、GOLD が 50、SHILVER が 100, THORS_HAMMER が0、SWORD が 1、SHIELD が 1です。
実装
まずはスマートコントラクトの開発から行なっていきます。
スマートコントラクト開発環境の準備
フレームワークはHardhatを使います。
npm install --save-dev hardhat
npx hardhat
下記のように選択します
✔ What do you want to do? · Create a TypeScript project
✔ Hardhat project root: · /Users/${user}/erc1155_gettingstarted
✔ Do you want to add a .gitignore? (Y/n) · y
✔ Do you want to install this sample project's dependencies with npm (hardhat @nomicfoundation/hardhat-toolbox)? (Y/n) · y
Openzeppelinをインストールします。これはスマートコントラクトのベースとして使います。
npm install @openzeppelin/contracts
スマートコントラクトの実装
https://docs.openzeppelin.com/contracts/3.x/erc1155
にあるサンプルをそのまま実装します
contracts/myERC1155.sol
pragma solidity ^0.8.9;
import "@openzeppelin/contracts/token/ERC1155/ERC1155.sol";
contract GameItems is ERC1155 {
uint256 public constant GOLD = 0;
uint256 public constant SILVER = 1;
uint256 public constant THORS_HAMMER = 2;
uint256 public constant SWORD = 3;
uint256 public constant SHIELD = 4;
constructor() public ERC1155("https://game.example/api/item/{id}.json") {
_mint(msg.sender, GOLD, 10**18, "");
_mint(msg.sender, SILVER, 10**27, "");
_mint(msg.sender, THORS_HAMMER, 1, "");
_mint(msg.sender, SWORD, 10**9, "");
_mint(msg.sender, SHIELD, 10**9, "");
}
}
スマートコントラクトのデプロイ
スマートコントラクトを使うためには、デプロイが必要です。今回はテスト用のネットワークを使います。
デプロイ先チェーンの設定
イーサリアムのテスト用チェーンであるGoerliを使います。
ノードは、Alchemyを使います。新規登録し、ALCHEMY_API_KEYを取得します。GOELRI_PRIVATE_KEYには、自分の持っているアカウントの秘密鍵を記載します。
https://www.alchemy.com/
その情報を使って、hardhat.config.tsを下記のように記載します。
import { HardhatUserConfig } from "hardhat/config";
import "@nomicfoundation/hardhat-toolbox";
const ALCHEMY_API_KEY = "XXXXXXXXXXXXXXXXXXXX"
const GOELRI_PRIVATE_KEY = "XXXXXXXXXXXXXXXXXXXX"
const config: HardhatUserConfig = {
solidity: "0.8.9",
defaultNetwork: "goerli",
networks: {
goerli: {
url: `https://eth-goerli.g.alchemy.com/v2/${ALCHEMY_API_KEY}`,
accounts: [GOELRI_PRIVATE_KEY]
}
}
};
export default config;
デプロイ用スクリプトの作成
scripts/deploy.sh を下記のように変更します
import { ethers } from "hardhat";
async function main() {
const GameItems = await ethers.getContractFactory("GameItems");
const gameItems = await GameItems.deploy();
await gameItems.deployed();
console.log("gameItems deployed to:", gameItems.address);
}
// We recommend this pattern to be able to use async/await everywhere
// and properly handle errors.
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
デプロイ
下記コマンドで、goerliのネットワークにデプロイします。
npx hardhat compile
npx hardhat run scripts/deploy.ts --network goerli
デプロイ後、下記のように表示されます。0xXXXXXXXXXXXがコントラクトアドレスです。
あとで使うのでメモっておきます。
gameItems deployed to: 0xXXXXXXXXXXX
フロントエンドの作成
次は、できたコントラクトを実行するためのフロントエンドを作成していきます。
Vue.jsのインストール
フレームワークは、Vue.jsを使います。
下記コマンドで初期化します。
npm init vue@latest
App.vueの編集
App.vueを下記のように編集します。
管理者用の画面としてAdmin.vue、ユーザー用の画面としてMyPage.vueを作成します。
わかりやすさのために、両方を同じ画面に表示します。
<script setup lang="ts">
import Admin from './components/Admin.vue'
import MyPage from './components/MyPage.vue'
</script>
<template>
<Admin/>
<MyPage/>
</template>
管理者用画面の作成
components/Admin.vueを下記のように作成します。
<script setup lang="ts">
import { ethers } from 'ethers';
import gameitemsabi from "@/abi/gameitems.json";
import type { GameItems } from "@/abi/myERC1155.sol/index";
const contractAddress = "0xXXXXXXXXXXX";
const deployerAddress = "0xXXXXXXXXXXX";
const playerAddress = "0xXXXXXXXXXXX";
const connect = async() => {
const accouts = await window.ethereum.request({method: "eth_requestAccounts"})
console.log(accouts[0]);
};
const safeBatchTransferFrom = async () => {
const provider = new ethers.providers.Web3Provider(window.ethereum)
const signer = await provider.getSigner();
const gameItemsContract: GameItems = await new ethers.Contract(contractAddress, gameitemsabi, signer) as GameItems;
const res = await gameItemsContract.safeBatchTransferFrom(deployerAddress, playerAddress, [0, 1, 3, 4], [50, 100, 1, 1], "0x00").catch(err => {
console.error(err)
})
}
</script>
<template>
<div id="top">
<h1>管理者</h1>
<button @click="connect">Metamaskに接続する</button>
<button @click="safeBatchTransferFrom">アイテムをユーザーに送る</button>
</div>
</template>
<style scoped>
#top {
height: 50vh;
width: 20vw;
}
</style>
ただ、上記コードが動作するためには、いくつか準備が必要です。
ether.jsのインストール
スマートコントラクトをJavascriptから扱うためのライブラリはweb3.jsとether.jsがありますが、最近主流になっているether.jsを使用します。
npm i ethers
abiの作成
コントラクトを実行するためには、設計図となるabiの作成が必要です。
今回は自分で作成したコントラクトのため、それを用いてabiを作成します。
solc /Users/${user}/erc1155_gettingstarted/contracts/myERC1155.sol --base-path ./ --include-path node_modules --abi
abiがコンソールに出力されるので、frontend/vue-project/src/abi/gameitems.json として保存します
型の作成
Typescriptでコントラクトを実行するためには、型があった方が良いので作成します。
まずは、typechainをインストールします
npm install -g typechain @typechain/ethers-v5
次に下記コマンドで、型を生成します。
typechain --target=ethers-v5 frontend/vue-project/src/abi/gameitems.json
各種アドレスの設定
const contractAddress = "0xXXXXXXXXXXX";
const deployerAddress = "0xXXXXXXXXXXX";
const playerAddress = "0xXXXXXXXXXXX";
contractAddressには、コントラクトをデプロイした際にメモしたコントラクトアドレスを書きます。
contractAddressには、コントラクトをデプロイした際のGOELRI_PRIVATE_KEYを持つユーザーのウォレットアドレスを書きます。
playerAddressは、なんでも良いです。アイテムの送り先のウォレットアドレスを書きます。
管理者画面の実行
まずは、Metamaskに接続するボタンをクリックして、Metamaskに接続します。
その後、アイテムをユーザーに送るボタンをクリックすると、playerAddressにアイテムが送付されます。
ユーザー側画面の作成
MyPage.vueを下記のように編集します。
<script setup lang="ts">
import { ethers } from 'ethers';
import gameitemsabi from "@/abi/gameitems.json";
import type { GameItems } from "@/abi/myERC1155.sol/index";
import { ref } from "vue";
const contractAddress = "0xXXXXXXXXXXX";
const deployerAddress = "0xXXXXXXXXXXX";
const playerAddress = "0xXXXXXXXXXXX";
const myItems = ref<number[]>()
const gold = ref<number>()
const silver = ref<number>()
const thorsHammer = ref<number>()
const sword = ref<number>()
const shield = ref<number>()
const connect = async() => {
const accouts = await window.ethereum.request({method: "eth_requestAccounts"})
console.log(accouts[0]);
};
const balanceOfBatch = async () => {
const provider = new ethers.providers.Web3Provider(window.ethereum)
const signer = await provider.getSigner();
const gameItemsContract: GameItems = await new ethers.Contract(contractAddress, gameitemsabi, signer) as GameItems;
const res = await gameItemsContract.balanceOfBatch([playerAddress, playerAddress, playerAddress, playerAddress, playerAddress], [0, 1, 2, 3, 4]).catch(err => {
console.error(err)
})
if (res) {
const result = res.map(bignumber => Number(bignumber.toString()))
myItems.value = result;
gold.value = myItems.value[0]
silver.value = myItems.value[1]
thorsHammer.value = myItems.value[2]
sword.value = myItems.value[3]
shield.value = myItems.value[4]
}
}
</script>
<template>
<div class="mypage">
<h1>マイページ</h1>
<div>
<button @click="connect">Metamaskに接続する</button>
<button @click="balanceOfBatch">もらったアイテムを表示する</button>
</div>
<div class="row">
<img v-for="g in gold" :key="g" src="@/assets/gold.png"/>
</div>
<div class="row">
<img v-for="s in silver" :key="s" src="@/assets/silver.png"/>
</div>
<div class="row">
<img v-for="t in thorsHammer" :key="t" src="@/assets/thorsHammer.png"/>
</div>
<div class="row">
<img v-for="s in sword" :key="s" src="@/assets/sword.png"/>
</div>
<div class="row">
<img v-for="s in shield" :key="s" src="@/assets/shield.png"/>
</div>
</div>
</template>
<style>
.mypage {
display: flex;
flex-flow: column;
height: 50vh;
width: 50vw;
}
.row {
display: flex;
max-width: 100%;
flex-wrap: wrap;
}
img {
width: 100px;
height: auto;
}
</style>
管理者が、Metamaskに接続し、アイテムをユーザーに送るボタンを押します。その後マイページのユーザーがMetamaskに接続し、もらったアイテムを表示するボタンを押すと、先ほど管理者が送ったアイテムが表示されます。GOLD が 50、SHILVER が 100, THORS_HAMMER が0、SWORD が 1、SHIELD が 1となれば完成です。
まとめ
今回は、イーサリアムの規格であるERC1155を使って、コントラクトの実装からフロントエンドの実行まで一通りの動作を行なってみました。今回のように多くのアイテムを扱う場合は、GOLDのERC20を50,SHILVERのERC20を100・・・とそれぞれ用意して送る必要がありましたが、ERC1155を使うと大幅に手間を減らすことができ、1つのERC1155で表現できるのが、便利だと思いました。