hawk, camphora, avocado and so on..

美術展、博物展、神社、旅行、読書、IT関連などの雑感を書いていきます。

ERC-721 トークンを発行する DApp を作ってみました(その2)

アボカドです。前回の記事では、捨てたものをブロックチェーンに記録するアプリの概要を紹介しました。
ERC-721 トークンを発行する DApp を作ってみました(その1) - hawk, camphora, avocado and so on..

今回は、このアプリの実際の作り方や、ハマった部分などを書いていきます。

おおまかな作り方

  • 事前準備
    • Node をインストール
    • MetaMask ウォレットをインストール
  • スマートコントラクトを作る
    • npm で Truffle をインストールして、任意のフォルダを init
    • npm で OpenZeppelin ライブラリをインストール
    • OpenZeppelin ライブラリの ERC721Token を継承して、独自の ERC-721 トークンのスマートコントラクトを実装
    • Remix を使って、実装したスマートコントラクトの動作確認
    • Infura.io にアカウントを作って、プロジェクトを作成し、API キーを取得
    • デプロイに使う MetaMask ウォレットのニーモニックを控える
    • Truffle のデプロイ先のプロバイダーを設定して(ここで Infura.io の API キー、エンドポイント、MetaMask ウォレットのニーモニックを使う)、テストネットにスマートコントラクトをデプロイ
  • Web フロントエンドを作る
    • npm で React をインストール
    • React のサンプル実装 create-react-app をベースにして、UI を作成
    • ipfs-api を使って、分散ストレージ IPFS にファイルや文字列を書き込む
    • Web3.js を使って、デプロイしたスマートコントラクトを操作
    • Ganache をインストールして Ethereum プライベートネットを立ち上げて、動作確認
    • Firebase でプロジェクトを作って、作った Web フロントエンドを公開

実際やったこと&ソースコード紹介

Windows 10 の環境を使いました。…が、Linux のほうがいろいろスムーズかもしれません。

Truffle のプロジェクトを作る

まず Node をインストールします(バージョン 8.11.3 LTS をインストールしました)。多くのライブラリが、パッケージ管理システム npm でインストールできるようになっています。

その後、Truffle をインストールして、任意のフォルダをひとつ作って初期化します。

> npm install -g truffle
truffle@4.1.13
> mkdir myTruffleProject
> cd  myTruffleProject
> truffle init

ERC-721 トークンを実装する

Solidity 言語の模範的な実装を提供している OpenZeppelin ライブラリをインストールします。この中に、ERC-721 トークンのベースとなるコードが含まれるので、今回はこれを継承して独自のトークンを作ります。ERC-721 は、固有のプロパティを持つ代替不可能トークン (Non-Fungible Token, NFT) の規格です。

> npm install zeppelin-solidity --save
zeppelin-solidity@1.11.0

まず、truffle プロジェクトの contracts フォルダ内に .sol ファイルを作成し、下記のように、必要なコントラクトを import & 継承してコントラクトを定義します。ERC721Token.sol は ERC-721 トークンの機能の多くを実装しているコード、ERC721Holder.sol は ERC-721 のコントラクト自身がトークンを受け取れるようにする機能を実装しているコード、Ownable.sol は管理者のみ操作できる処理を書きやすくするものです。なお、今回つくったコントラクトの名前は「DiscardedRelicToken」(破棄された遺産トークン)としました。コンストラクタで、トークンの名称と単位の名称を設定します。ちなみにこれらは他のトークンとかぶっても問題ないです(エラー等は出ません)。

pragma solidity ^0.4.24;

import "zeppelin-solidity/contracts/token/ERC721/ERC721Token.sol";
import "zeppelin-solidity/contracts/token/ERC721/ERC721Holder.sol";
import "zeppelin-solidity/contracts/ownership/Ownable.sol";

contract DiscardedRelicToken is ERC721Token, ERC721Holder, Ownable {
  // コンストラクタで、トークンの名称と単位名称を設定
  constructor() ERC721Token("DiscardedRelic", "DRLC") public {
  }

  // 必要な関数を実装する
}


今回、このトークンの持つ固有のプロパティとして、次の構造体を定義しました。imageIpfsHash は捨てたものの写真の IPFS ハッシュ値(分散ストレージ IPFS にファイルをアップロードしたときに得られる、ファイルの格納場所を示す値)、metaDataIpfsHash はそのメタデータの IPFS ハッシュ値、owner はトークンの所有者のアドレスです。それから、discardedRelics は、発行したすべてのトークンを格納する配列です。

  struct DiscardedRelic {
    string imageIpfsHash;
    string metaDataIpfsHash;
    address owner;
  }

  DiscardedRelic[] public discardedRelics;

今回作るのは mintable(鋳造可能な)トークンです。ユーザーはコントラクトに対して送金することで、自分が所有者のトークンを発行できます。これを実現するための、2 つの重要な関数 mint() と burn() を実装します。継承している OpenZeppelin のコントラクトには関数 _mint() と _burn() が書かれており、内部で所有者とトークン ID を渡してこれらの関数を実行することで、トークンの管理が楽になります。なお、burn() はトークンを削除する関数ですが、必要がなければ実装しなくてよいと思います。createFee についてはこの下で述べます。

  function mint(string _imageIpfsHash, string _metaDataIpfsHash) public payable {
    require(msg.value == createFee);
    require(owner != address(0));
    DiscardedRelic memory _discardedRelic = DiscardedRelic({imageIpfsHash: _imageIpfsHash, metaDataIpfsHash: _metaDataIpfsHash, owner: msg.sender});
    uint256 newTokenId = discardedRelics.push(_discardedRelic) - 1;
    _mint(msg.sender, newTokenId);
  }

  function burn(uint256 _tokenId) external onlyOwnerOf(_tokenId) {
    _burn(msg.sender, _tokenId);
  }

上で出てくる変数 createFee は、トークンを発行する際の手数料です(型は uint128)。今回は初期値を 0 ether として宣言しましたが、下記、コントラクトをデプロイしたユーザーのみが実行できる関数で、この手数料は変更できるようにしてあります。

  uint128 private createFee = 0 ether;

  function setCreateFee(uint128 _fee) public onlyOwner {
    createFee = _fee;
  }

  function getCreateFee() external view returns (uint128) {
    return createFee;
  }


そのほか、必要になりそうな関数を定義しました。このあたりは、実現したいアプリによって異なると思います。
ownedTokens や allTokens は、継承しているコントラクトで管理している変数であり、これらを使うことで実装量を減らせます。

  function getDiscardedRelic(uint256 _tokenId) public view returns (string, string, address) {
    return  (discardedRelics[_tokenId].imageIpfsHash, discardedRelics[_tokenId].metaDataIpfsHash, ownerOf(_tokenId));
  }

  function tokensOf(address _owner) external view returns (uint256[]) {
    return ownedTokens[_owner];
  }

  function getAllTokens() external view returns (uint256[]) {
      return allTokens;
  }

  function updateDiscardedRelic(uint256 _tokenId, string _metaDataIpfsHash) public {
    require(ownerOf(_tokenId) == msg.sender);
    discardedRelics[_tokenId].metaDataIpfsHash = _metaDataIpfsHash;
  }


今回はここまで。
次の記事では、「スマートコントラクトを作る」の続きを書きたいと思います。
動作確認と、テストネットへのデプロイについて説明する予定です。

🥑