今回はOpen Zeppelinというライブラリを使用してERC721のサンプルトークン作成を行います。
本記事とほぼ同じタイトルのこちらの記事を参考にしました🙇
https://qiita.com/shora_kujira16/items/6efd834e6a3a3eb8a98c
本記事ではより初心者レベルでやっていきます。
ちなみに以下の検証はMacOS 10.12.6で実施しました。
プロジェクトの作成
まずはプロジェクトを初期化します。
Macの適当なところにディレクトリを作成し、そこにトリュフ、NodeJSのコマンドを使ってプロジェクトを初期化していきます。
NodeJSのインストールについては https://nodejs.org/ja/download/ を参照してください。
トリュフのインストールについては https://truffleframework.com/docs/truffle/getting-started/installation を参照してください。
# ディレクトリの作成
$ mkdir erc721 & cd erc721
# トリュフプロジェクトの初期化(事前にtruffleのインストールが必要)
$ truffle init
# NodeJSの初期化(事前にnodejsのインストールが必要)
$ npm init
# openzeppelinライブラリのインストール
$ npm install openzeppelin-solidity
+ openzeppelin-solidity@1.12.0
added 1 package in 1.676s
Open Zeppelinとは
ここで簡単にOpen Zeppelinについて。
Open ZeppelinとはZeppelin社が立ち上げたオープンソースのEthereum Solidityライブラリです。
数多くの実際のプロジェクトで使用されており検証済みのコードになっています。
https://openzeppelin.org/
Open ZeppelinにはERC20やERC721の雛形となるコードが用意されており、それらを使用すると検証済みで安全なコードを書きやすくなります。
例えば今回のERC721はnode_modulesフォルダの下を辿っていくと下記のようにERC721関連のsolidityのコードがあります。
#コードの作成
ERC721のコードを書いて行きます。
上記で作成したプロジェクトフォルダ(erc721)を適当な統合開発環境で開きます。
ここではVisualStudio Codeで進めます。
開くとこんな感じです。
build・・・コンパイル済みのファイル
contracts・・・solidityのコード
migrations・・・デプロイ用のコード
node_modules・・・ライブラリ
test・・・テスト用のコード
package.json・・・nodejsのプロジェクト設定ファイル
truffle.js・・・truffleのプロジェクト設定ファイル
コードは冒頭の記事中のソースを参考にしましたが、コンパイル時にパスが見つからないとエラーが出てきたのでライブラリを一個ずつ追記しました(回避策がわかる人教えてください)。
https://qiita.com/shora_kujira16/items/6efd834e6a3a3eb8a98c
pragma solidity ^0.4.23;
import "node_modules/openzeppelin-solidity/contracts/token/ERC721/ERC721.sol";
import "node_modules/openzeppelin-solidity/contracts/token/ERC721/ERC721Token.sol";
import "node_modules/openzeppelin-solidity/contracts/token/ERC721/ERC721Basic.sol";
import "node_modules/openzeppelin-solidity/contracts/token/ERC721/ERC721Receiver.sol";
import "node_modules/openzeppelin-solidity/contracts/token/ERC721/ERC721BasicToken.sol";
import "node_modules/openzeppelin-solidity/contracts/introspection/SupportsInterfaceWithLookup.sol";
import "node_modules/openzeppelin-solidity/contracts/introspection/ERC165.sol";
import "node_modules/openzeppelin-solidity/contracts/math/SafeMath.sol";
import "node_modules/openzeppelin-solidity/contracts/AddressUtils.sol";
contract MyToken is ERC721Token {
uint256 internal nextTokenId = 0;
constructor() public ERC721Token("MyToken", "MTKN") {}
function mint() external {
uint256 tokenId = nextTokenId;
nextTokenId = nextTokenId.add(1);
super._mint(msg.sender, tokenId);
}
function setTokenURI(uint256 _tokenId, string _message) external {
super._setTokenURI(_tokenId, _message);
}
function burn(uint256 _tokenId) external {
super._burn(msg.sender, _tokenId);
}
}
作りとしてはERC721Tokenというコントラクトを継承したMyTokenというコントラクトを作成しています。
Open ZeppelinのERC721の仕様としては最低限、mint、setTokenURI, burnのメソッドを実装する必要があるようです。
デプロイ用コードの作成
デプロイをするためにコードを作成します。
var MyToken = artifacts.require("./MyToken.sol");
module.exports = function(deployer) {
deployer.deploy(MyToken);
};
デプロイ前の準備
コードが書けたら実際にデプロイしてトークンの発行などをやっていきます。
その前に事前準備としてイーサリアムノードを立ち上げます。
ここでは簡易的に実験するためにGanacheを使いました。
Ganacheの画面でRPCサーバーのアドレスを確認します。(ここでは http://localhost:8545)
アドレスを確認したらプロジェクトのtruffle.jsの中身を以下に書き換えます。
module.exports = {
networks: {
development: {
host: "localhost",
port: 8545,
network_id: "*" // Match any network id
}
}
};
プログラムのコンパイルとデプロイ
ここまでできたら次はコンパイルとデプロイをしていきます。
コマンドを2つ叩くだけです。
# コンパイルします。
$ truffle compile
# デプロイします。
$ truffle migration
コンパイルはローカルのbuildというフォルダに何やらJSONファイルがいくつか作成されます。
migrationで実際のイーサリアムノード(ここではGanache)へデプロイされます。
エラーが出なければ次に進みます。
ここからはトリュフのコンソールというモードで試していきます。
これを使うとGanacheと対話的にコマンドを打って動きが確認できます。
$ truffle console
こんな風にプロンプトが出てくると思います。
truffle(development)>
まずはトークンのインスタンスを取得します。
truffle(development)> myToken = MyToken.at(MyToken.address)
こんな感じの結果が返って来ればOKです。
TruffleContract {
constructor:
{ [Function: TruffleContract]
_static_methods:
{ setProvider: [Function: setProvider],
new: [Function: new],
...
また上記の結果の中にabiという欄があります。これは使えるコマンドの一覧が出ています。
abi:
[ { constant: true,
inputs: [Array],
name: 'supportsInterface',
outputs: [Array],
payable: false,
stateMutability: 'view',
type: 'function' },
{ constant: true,
inputs: [],
name: 'name',
outputs: [Array],
payable: false,
stateMutability: 'view',
type: 'function' },
...
さらにこんな感じでアドレスが表示されます。
これが多分コントラクトのアドレスです。
allEvents: [Function: bound ],
address: '0x6176e9ec8ab713e3ab4ca415d25f57eea52e3cd6',
transactionHash: null }
続いて基本情報を取得します。
name()
は名前が取得できます。
symbol()
はトークンのシンボルが取得できます。
truffle(development)> myToken.name()
'MyToken'
truffle(development)> myToken.symbol()
'MTKN'
トークンを発行してみます。
truffle(development)> myToken.mint()
{ tx: '0x4f19f527b2b1487f1bd1121204707caa4efbbdc7df20b5605de3d087299efe90',
receipt:
{ transactionHash: '0x4f19f527b2b1487f1bd1121204707caa4efbbdc7df20b5605de3d087299efe90',
transactionIndex: 0,
...
これでトークンが発行されました。
トランザクションハッシュが返されます。パブリックならetherscanなどで追えますね。
ちなみにトークンのIDはソースコードを辿っていくとわかりますが、今回は0で作成されています。
発行したトークンを確認してみます。
実はコンソールで接続するとaccountのゼロ番目(コインベース?)でログインしているようです。
この辺りどこかに説明とかないのかなぁ。。
// 発行されたトークンの全数を取得します。cの1がトークン数です。
truffle(development)> myToken.totalSupply()
BigNumber { s: 1, e: 0, c: [ 1 ] }
// アカウントのゼロ番目のアドレスをaccount0へ格納
truffle(development)> account0 = web3.eth.accounts[0]
'0x1c925b08abcd86a5a157d610a283ea54b7183577'
// account0の保持しているトークン数を取得します。cの1が数です。
truffle(development)> myToken.balanceOf(account0)
BigNumber { s: 1, e: 0, c: [ 1 ] }
// account0の保持しているトークンのうち0番目のトークンのトークンIDを取得します。cの0がトークンIDです。
truffle(development)> myToken.tokenOfOwnerByIndex(account0,0)
BigNumber { s: 1, e: 0, c: [ 0 ] }
この例だとたまたまトークンIDとインデックスが同じなのでわかりづらいが上のbalanceOf
で総数がわかり、インデックスでトークンIDを調べればユーザーの保持しているトークンIDリストを取得できます。
一気にトークンリストを取得する関数は標準では用意されていないので上記の手順で取得するか、自分で作成する必要があるようです。
もう一個トークンを発行してみます。
truffle(development)> myToken.mint()
{ tx: '0x083464cb943d2db1f37113c7670e8b5fcbeef8ea087d838e49b958e32a14e3d2',
receipt:
{ transactionHash: '0x083464cb943d2db1f37113c7670e8b5fcbeef8ea087d838e49b958e32a14e3d2',
transactionIndex: 0,
...
//全体の総数を確認する
truffle(development)> myToken.totalSupply()
BigNumber { s: 1, e: 0, c: [ 2 ] }
//指定したトークンIDが存在するか確認する
truffle(development)> myToken.exists(0)
true
//指定したトークンIDが存在するか確認する
truffle(development)> myToken.exists(1)
true
//指定したトークンIDが存在するか確認する(存在しない)
truffle(development)> myToken.exists(2)
false
ここまででトークンが2つ発行できたことがわかります。
ちなみにBigNumberをtoString()で見やすくしようとしましたがうまくできません。わかる人教えてください。
'[object Promise]'というのが返って来ます。
#Meta属性の設定
上記でトークンを2つ発行しましたが、トークンのIDが0と1で発行しただけでそれぞれのトークンの属性が何も設定されていません。Crypto Kittyとかであれば猫のイメージやDNAなどを属性として設定します。
現在のERC721ではそれらの属性は外部に保存する仕組みとなっています。
ブロックチェーン上のトークンにはURIを1つセットしてそのURIから実際の属性を取得するという仕組みです。
仕様はこちらのページに書いてあります。
https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md
のMeta Data Schemaのあたり。
こんな感じにするようです。
//トークンID 0のトークンにURIとしてtestをセット
truffle(development)> myToken.setTokenURI(0,'{"title": "MyToken","type": "object","properties": {"name": "Test!","description": "Test!","image": "https://example.com/image1.png","status": "Enabled","date": "2018/1/1 13:00:00"}}
')
{ tx: '0x5fdf78f05037bce6df8573a45666a401b070dfbdfc0afc926983a2bc9b63e959',
receipt:
{ transactionHash: '0x5fdf78f05037bce6df8573a45666a401b070dfbdfc0afc926983a2bc9b63e959',
transactionIndex: 0,
...
//セットしたURIを確認
truffle(development)> myToken.tokenURI(0)
'{"title": "MyToken","type": "object","properties": {"name": "Test!","description": "Test!","image": "https://example.com/image1.png","status": "Enabled","date": "2018/1/1 13:00:00"}}'
同様にトークン1にもセットします。
//jsonをセット
truffle(development)> myToken.setTokenURI(1,'{"title": "MyToken","type": "object","properties": {"name": "Test2!","description": "Test!","image": "https://example.com/image2.png","status": "Enabled","date": "2019/1/1 1:00:00"}}')
{ tx: '0x9fd137e334515104875a4bcc36ba269c4f9ed1d604e166e4ddda90f28a553c38',
receipt:
{ transactionHash: '0x9fd137e334515104875a4bcc36ba269c4f9ed1d604e166e4ddda90f28a553c38',
transactionIndex: 0,
...
//トークン1のuriが返ってくる
truffle(development)> myToken.tokenURI(1)
'{"title": "MyToken","type": "object","properties": {"name": "Test2!","description": "Test!","image": "https://example.com/image2.png","status": "Enabled","date": "2019/1/1 1:00:00"}}'
#トークンを送信してみる
ここまできたらあとはトークンの所有者を変更してみます。
//account0からaccount1にトークン0を送信
truffle(development)> myToken.transferFrom(account0, account1, 0)
{ tx: '0x077d78a9b1fb8f35b5a45725fb55334989c5237de215a66dbf4b26e68ae9dc11',
receipt:
{ transactionHash: '0x077d78a9b1fb8f35b5a45725fb55334989c5237de215a66dbf4b26e68ae9dc11',
transactionIndex: 0,
blockHash: '0x06d66cf6a55bdde2f7f32324d7dc0f8f87f330f63aa28636fabb4d54a59850b2',
blockNumber: 9,
...
//トークンの総量は変わらないことを確認
truffle(development)> myToken.totalSupply()
BigNumber { s: 1, e: 0, c: [ 2 ] }
//account0のトークン数は1であることを確認(2から1に減ってる)
truffle(development)> myToken.balanceOf(account0)
BigNumber { s: 1, e: 0, c: [ 1 ] }
//account1のトークン数は1であることを確認(0から1に増えている)
truffle(development)> myToken.balanceOf(account1)
BigNumber { s: 1, e: 0, c: [ 1 ] }
#Tips
今回の検証でいくつか試行錯誤したのでその時のTipsを書き留めておきます。
##デプロイに失敗した場合。
同じ環境で2回目のデプロイ時に失敗した。
$ truffle migration
Using network 'development'.
Error: Attempting to run transaction which calls a contract function, but recipient address 0x6ce82c497a87de933739e428233813c8f860fb2d is not a contract address
以下のようにbuildフォルダ内を一度削除、再度、コンパイル&デプロイするとうまくいった。
$ rm -r build/*