Truffle, OpenZeppelin, Ganache を使用してERC721を作成する
まえおき
タイトル通りTruffle, OpenZeppelin を使用してERC721を作成してみます。参考にしたのはERC721トークンの実装とRinkebyデプロイという記事。これをちょびっとだけ改造したものになります。 新規プロジェクト作成〜ERC721発行までの手順とかを毎回調べるのは苦痛のなのでまとめておきます。 バージョンなどの変更等で気づいたこととかあったら更新するかもしれません。(気づいたら教えてください) なおTruffle, OpenZeppelinの説明はしません。 - Truffleを入れる。 - そもそもSolidityとかNode.jsが入っていない人はそれを入れましょう - そもそもTruffleって何やねんって人はTruffleチュートリアルをやってみるといいかも。(幸いDocBase上にはTruffleチュートリアルをやった際のまとめがあるため、それを参考してみてね) - Truffle チュートリアル
環境
$ truffle version Truffle v5.0.26 (core: 5.0.26) Solidity v0.5.0 (solc-js) Node v10.13.0 Web3.js v1.0.0-beta.37
全体の大まかな流れ
コントラクト作成までの大まかな流れは以下の通り。 1. ディレクトリ作成 2. 新しいTruffleプロジェクトを作る 3. OpenZeppleinインストール 4. コントラクトを作る 5. コンパイルを通す 5. マイグレーションファイルを作成する 6. テスト作成 7. テストを通す 7. デプロイ
今回はデプロイ後新規でNFTも発行してみます。
今回作成するスマートコントラクト概要
- 名前はMochimochiNFT、単位はMTC
- デプロイと同時に1MTC発行
- 誰でも追加発行、譲渡可能
ディレクトリ作成
$ mkdir erc721-test $ cd erc721-test/
Truffleプロジェクト作成
Truffleプロジェクトを新規作成
$ truffle init
作成すると以下の様なものが作成される。
$ tree |── contracts │ └── Migrations.sol |── migrations │ └── 1_initial_migration.js |── test └── truffle-config.js
OpenZeppleinインストール
npm使ってOpenZeppleinをインストール。
$ npm init -y $ npm install openzeppelin-solidity
すると以下の様なディレクトリが作成される。見た通り、npmで入れるとOpenZeppelinライブラリは全てnode_modules
の中に入ることになる。
$ tree . ├── contracts │ └── Migrations.sol ├── migrations │ └── 1_initial_migration.js ├── node_modules │ └── openzeppelin-solidity │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── build │ │ └── contracts │ │ ├── Address.json │ │ ├── AllowanceCrowdsale.json │ │ ├── Arrays.json │ │ ├── CappedCrowdsale.json │ │ ├── CapperRole.json │ │ ├── ConditionalEscrow.json │ │ ├── Counters.json │ │ ├── Crowdsale.json │ │ ├── ECDSA.json │ │ ├── ERC165.json │ │ ├── ERC165Checker.json │ │ ├── ERC1820Implementer.json │ │ ├── ERC20.json │ │ ├── ERC20Burnable.json │ │ ├── ERC20Capped.json │ │ ├── ERC20Detailed.json │ │ ├── ERC20Metadata.json │ │ ├── ERC20Migrator.json │ │ ├── ERC20Mintable.json │ │ ├── ERC20Pausable.json │ │ ├── ERC20Snapshot.json │ │ ├── ERC721.json │ │ ├── ERC721Burnable.json │ │ ├── ERC721Enumerable.json │ │ ├── ERC721Full.json │ │ ├── ERC721Holder.json │ │ ├── ERC721Metadata.json │ │ ├── ERC721MetadataMintable.json │ │ ├── ERC721Mintable.json │ │ ├── ERC721Pausable.json │ │ ├── ERC777.json │ │ ├── Escrow.json │ │ ├── FinalizableCrowdsale.json │ │ ├── IERC165.json │ │ ├── IERC1820Implementer.json │ │ ├── IERC1820Registry.json │ │ ├── IERC20.json │ │ ├── IERC721.json │ │ ├── IERC721Enumerable.json │ │ ├── IERC721Full.json │ │ ├── IERC721Metadata.json │ │ ├── IERC721Receiver.json │ │ ├── IERC777.json │ │ ├── IERC777Recipient.json │ │ ├── IERC777Sender.json │ │ ├── IncreasingPriceCrowdsale.json │ │ ├── IndividuallyCappedCrowdsale.json │ │ ├── Math.json │ │ ├── MerkleProof.json │ │ ├── MintedCrowdsale.json │ │ ├── MinterRole.json │ │ ├── Ownable.json │ │ ├── Pausable.json │ │ ├── PausableCrowdsale.json │ │ ├── PauserRole.json │ │ ├── PaymentSplitter.json │ │ ├── PostDeliveryCrowdsale.json │ │ ├── PullPayment.json │ │ ├── ReentrancyGuard.json │ │ ├── RefundEscrow.json │ │ ├── RefundableCrowdsale.json │ │ ├── RefundablePostDeliveryCrowdsale.json │ │ ├── Roles.json │ │ ├── SafeERC20.json │ │ ├── SafeMath.json │ │ ├── Secondary.json │ │ ├── SignatureBouncer.json │ │ ├── SignedSafeMath.json │ │ ├── SignerRole.json │ │ ├── TimedCrowdsale.json │ │ ├── TokenTimelock.json │ │ ├── TokenVesting.json │ │ ├── WhitelistAdminRole.json │ │ ├── WhitelistCrowdsale.json │ │ ├── WhitelistedRole.json │ │ └── __unstable__TokenVault.json │ ├── contracts │ │ ├── ARCHITECTURE.md │ │ ├── access │ │ │ ├── README.md │ │ │ ├── Roles.sol │ │ │ └── roles │ │ │ ├── CapperRole.sol │ │ │ ├── MinterRole.sol │ │ │ ├── PauserRole.sol │ │ │ ├── SignerRole.sol │ │ │ ├── WhitelistAdminRole.sol │ │ │ └── WhitelistedRole.sol │ │ ├── crowdsale │ │ │ ├── Crowdsale.sol │ │ │ ├── README.md │ │ │ ├── distribution │ │ │ │ ├── FinalizableCrowdsale.sol │ │ │ │ ├── PostDeliveryCrowdsale.sol │ │ │ │ ├── RefundableCrowdsale.sol │ │ │ │ └── RefundablePostDeliveryCrowdsale.sol │ │ │ ├── emission │ │ │ │ ├── AllowanceCrowdsale.sol │ │ │ │ └── MintedCrowdsale.sol │ │ │ ├── price │ │ │ │ └── IncreasingPriceCrowdsale.sol │ │ │ └── validation │ │ │ ├── CappedCrowdsale.sol │ │ │ ├── IndividuallyCappedCrowdsale.sol │ │ │ ├── PausableCrowdsale.sol │ │ │ ├── TimedCrowdsale.sol │ │ │ └── WhitelistCrowdsale.sol │ │ ├── cryptography │ │ │ ├── ECDSA.sol │ │ │ ├── MerkleProof.sol │ │ │ └── README.md │ │ ├── drafts │ │ │ ├── Counters.sol │ │ │ ├── ERC1046 │ │ │ │ └── ERC20Metadata.sol │ │ │ ├── ERC20Migrator.sol │ │ │ ├── ERC20Snapshot.sol │ │ │ ├── README.md │ │ │ ├── SignatureBouncer.sol │ │ │ ├── SignedSafeMath.sol │ │ │ └── TokenVesting.sol │ │ ├── introspection │ │ │ ├── ERC165.sol │ │ │ ├── ERC165Checker.sol │ │ │ ├── ERC1820Implementer.sol │ │ │ ├── IERC165.sol │ │ │ ├── IERC1820Implementer.sol │ │ │ ├── IERC1820Registry.sol │ │ │ └── README.md │ │ ├── lifecycle │ │ │ └── Pausable.sol │ │ ├── math │ │ │ ├── Math.sol │ │ │ ├── README.md │ │ │ └── SafeMath.sol │ │ ├── ownership │ │ │ ├── Ownable.sol │ │ │ ├── README.md │ │ │ └── Secondary.sol │ │ ├── payment │ │ │ ├── PaymentSplitter.sol │ │ │ ├── PullPayment.sol │ │ │ ├── README.md │ │ │ └── escrow │ │ │ ├── ConditionalEscrow.sol │ │ │ ├── Escrow.sol │ │ │ ├── README.md │ │ │ └── RefundEscrow.sol │ │ ├── token │ │ │ ├── ERC20 │ │ │ │ ├── ERC20.sol │ │ │ │ ├── ERC20Burnable.sol │ │ │ │ ├── ERC20Capped.sol │ │ │ │ ├── ERC20Detailed.sol │ │ │ │ ├── ERC20Mintable.sol │ │ │ │ ├── ERC20Pausable.sol │ │ │ │ ├── IERC20.sol │ │ │ │ ├── README.md │ │ │ │ ├── SafeERC20.sol │ │ │ │ └── TokenTimelock.sol │ │ │ ├── ERC721 │ │ │ │ ├── ERC721.sol │ │ │ │ ├── ERC721Burnable.sol │ │ │ │ ├── ERC721Enumerable.sol │ │ │ │ ├── ERC721Full.sol │ │ │ │ ├── ERC721Holder.sol │ │ │ │ ├── ERC721Metadata.sol │ │ │ │ ├── ERC721MetadataMintable.sol │ │ │ │ ├── ERC721Mintable.sol │ │ │ │ ├── ERC721Pausable.sol │ │ │ │ ├── IERC721.sol │ │ │ │ ├── IERC721Enumerable.sol │ │ │ │ ├── IERC721Full.sol │ │ │ │ ├── IERC721Metadata.sol │ │ │ │ ├── IERC721Receiver.sol │ │ │ │ └── README.md │ │ │ └── ERC777 │ │ │ ├── ERC777.sol │ │ │ ├── IERC777.sol │ │ │ ├── IERC777Recipient.sol │ │ │ ├── IERC777Sender.sol │ │ │ └── README.md │ │ └── utils │ │ ├── Address.sol │ │ ├── Arrays.sol │ │ ├── README.md │ │ └── ReentrancyGuard.sol │ ├── package.json │ └── test │ └── behaviors │ └── access │ └── roles │ └── PublicRole.behavior.js ├── package-lock.json ├── package.json ├── test └── truffle-config.js
コントラクト作成
コントラクトを作成していく。フォルダーはcontracts/
ディレクトリ下にAsset.sol
ファイルを作成する。
まず、コード内容は以下の通り
pragma solidity ^0.5.0; import "../node_modules/openzeppelin-solidity/contracts/token/ERC721/ERC721Full.sol"; contract Test is ERC721Full { // (1) constructor( string memory name, // (2) string memory symbol, // (2) uint tokenId, // (2) string memory tokenURI // (2) ) ERC721Full(name, symbol) public { // (3) _mint(msg.sender, tokenId); // (4) _setTokenURI(tokenId, tokenURI); //(5) } function mint(uint256 tokenId) public{ // (6) _mint(msg.sender, tokenId); // (7) } }
解説
今回はERC721Ful.sol
を継承させます。ERC721と言っても色々ライブラリがありますが、とりあえず全部載せであるこれを入れておきます。
(1) contract
contract
内に処理内容を書いていく。オブジェクト指向言語での「クラス」的なもの。以下に基本的な構文などは書いてある。
- Solidity Dcoument v0.5.10
- 基本的な記法
(2) 各パラメータ
(3) ERC721Full(name, symbol) public {
今回のスマコンでは ERC721Full
関数がデプロイ時に実行される。挙動はERC721Full(name, symbol) public {
と書いてあるのでERC721.sol
を見てみる。
contract ERC721Full is ERC721, ERC721Enumerable, ERC721Metadata { constructor (string memory name, string memory symbol) public ERC721Metadata(name, symbol) { // solhint-disable-previous-line no-empty-blocks }
ここでは引数として受け取ったname
, symbol
を ERC721Metadata.sol
に渡す。
bytes4 private constant _INTERFACE_ID_ERC721_METADATA = 0x5b5e139f; /** * @dev Constructor function */ constructor (string memory name, string memory symbol) public { _name = name; _symbol = symbol; // register the supported interfaces to conform to ERC721 via ERC165 _registerInterface(_INTERFACE_ID_ERC721_METADATA); }
同じように引数として受け取ったname
, symbol
を_name
, _symbol
に渡す。 その後 _registerInterface
を実行する。 これはERC165.sol
に書かれているので見てみます。
bytes4 private constant _INTERFACE_ID_ERC165 = 0x01ffc9a7; mapping(bytes4 => bool) private _supportedInterfaces; constructor () internal { // Derived contracts need only register support for their own interfaces, // we register support for ERC165 itself here _registerInterface(_INTERFACE_ID_ERC165); } ・・・・・・・・・・・・・ function _registerInterface(bytes4 interfaceId) internal { require(interfaceId != 0xffffffff, "ERC165: invalid interface id"); _supportedInterfaces[interfaceId] = true; }
該当箇所だけ抜き取ったのが以下のコード。 constructor
内の _registerInterface
が実行されます。
contract ERC165 is IERC165 { /* * bytes4(keccak256('supportsInterface(bytes4)')) == 0x01ffc9a7 */ bytes4 private constant _INTERFACE_ID_ERC165 = 0x01ffc9a7; /** * @dev Mapping of interface ids to whether or not it's supported. */ mapping(bytes4 => bool) private _supportedInterfaces; constructor () internal { // Derived contracts need only register support for their own interfaces, // we register support for ERC165 itself here _registerInterface(_INTERFACE_ID_ERC165); } /** * @dev See `IERC165.supportsInterface`. * * Time complexity O(1), guaranteed to always use less than 30 000 gas. */ function supportsInterface(bytes4 interfaceId) external view returns (bool) { return _supportedInterfaces[interfaceId]; } /** * @dev Registers the contract as an implementer of the interface defined by * `interfaceId`. Support of the actual ERC165 interface is automatic and * registering its interface id is not required. * * See `IERC165.supportsInterface`. * * Requirements: * * - `interfaceId` cannot be the ERC165 invalid interface (`0xffffffff`). */ function _registerInterface(bytes4 interfaceId) internal { require(interfaceId != 0xffffffff, "ERC165: invalid interface id"); _supportedInterfaces[interfaceId] = true; } }
ちなみに変数の先頭によく使われている_
は何なのか調べてみると同じように疑問に思っている人を見かけたので貼っておく。どうも関係修飾子を使う際に使われるものらしい。暗黙のルール的な感じです。
What does _(underscore) do?
ただしmodifier
を書く際は最後に_;
が必ず必要になります。
modifier
って何やねんてんって方は以下の記事をどうぞ
(4) _mint(msg.sender, tokenId);
_mint
が新規NFTの発行のための関数になっている。ERC721Enumerable.sol
に記述されているのが以下のもの。
function _mint(address to, uint256 tokenId) internal { super._mint(to, tokenId); _addTokenToOwnerEnumeration(to, tokenId); _addTokenToAllTokensEnumeration(tokenId); }
第一引数にaddress
第二引数tokenId
を入れることで、それぞれをNFTに紐づけている。今回はmsg.sender
(ここではコントラクトをデプロイした人)と渡すuint256
型の引数がtokenId
になります。
super
は多重継承問題をよしなにしてくれるメソッド。詳しくはSolidityの多重継承やダイヤモンド継承の挙動や優先順位についてを見てください。
_addTokenToOwnerEnumeration(to, tokenId);
と _addTokenToAllTokensEnumeration(tokenId);
はそれぞれ以下の通り。
function _addTokenToOwnerEnumeration(address to, uint256 tokenId) private { _ownedTokensIndex[tokenId] = _ownedTokens[to].length; _ownedTokens[to].push(tokenId); } /** * @dev Private function to add a token to this extension's token tracking data structures. * @param tokenId uint256 ID of the token to be added to the tokens list */ function _addTokenToAllTokensEnumeration(uint256 tokenId) private { _allTokensIndex[tokenId] = _allTokens.length; _allTokens.push(tokenId); }
(5) _setTokenURI(tokenId, tokenURI);
最後にERC721Metadata.sol
の _setTokenURI
を呼び出し tokenId
と tokenURI
を関連づける。
_setTokenURI
は以下の通り
function _setTokenURI(uint256 tokenId, string memory uri) internal { require(_exists(tokenId), "ERC721Metadata: URI set of nonexistent token"); _tokenURIs[tokenId] = uri; }
_exists
に関しては以下のようになっています。
function _exists(uint256 tokenId) internal view returns (bool) { address owner = _tokenOwner[tokenId]; return owner != address(0); }
(6) function mint(uint256 tokenId) public{
ここでは新規NFT発行の関数を作成している。最初の宣言時に function
を宣言。引数にはuint256
型を指定。実際に発行する時の関数名がmint
なので名前も mint
関数とした。
(7) _mint(msg.sender, tokenId);
(4) でも実行した_mint
を呼び出している。msg.sender
とすることで、この関数を呼び出した人がaddressと紐付くことになる。
コンパイル
truffle compile
を叩くだけ。成功するまで愚直に修正しましょう。
テスト作成
test/
ディレクトリ配下に Asset.js
を作成。テスト内容は超適当。とりあえずシンボル名と名前があっているかの確認だけ。テストに関しては詳しく別記事を書きます。(知らんけど)
const Asset = artifacts.require('../contracts/Assets.sol'); contract('Asset', function ([creator, ...accounts]) { const name = "MochiMochiNFT"; const tokenId = 1; it("...is going to confirm the token name and tokenId.", async () => { const assetInstance = await Asset.deployed(); let tokenName = await assetInstance.name(); let owner = await assetInstance.ownerOf(tokenId); assert.equal(tokenName, name, "Name isn't the same."); assert.equal(creator, owner, "Account isn't the same."); }); });
コントラクトデプロイ後、トークン名とtokeId
に紐づいたアカウントアドレスを取得。
その後、指定したトークン名(Simple Asset)とtokenId
に紐づいたアドレスがトークン作成者のアカウントアドレスを一致しているかを検証。
truffle test
で実行。特に問題なければ成功するはず。ちなみにディレクトリ指定も出来ます。
マイグレーションの作成
コントラクト作成後、マイグレーションを作成します。
ここでは2_deploy_assets.js
を migrattions
配下に作成。ここではERC721トークンに必要なパラメーターを定義する。コードは以下の通り。
const Asset = artifacts.require("../contracts/Asset.sol") module.exports = async(deployer, network, accounts) => { const name = "MochiMochiNFT"; const symbol = "MTC"; const tokenId = 1; const tokenURI = "https://ipfs.io/ipfs/QmYMYdJqSrTNB3iwXzmYfGdjAymuXvHAJuum9zzT7RV7N1"; await deployer.deploy( Asset, name, symbol, tokenId, tokenURI ); };
デプロイ
まずはEthereumネットワーク繋げる必要があるので、それらの設定が書かれているファイルtruffle-config.js
を開き、以下のようにする。実際には途中に書いてあるコメントを元に戻して、port番号を変えるだけ。
また今回はGanacheを利用する。これはmetamaskとも接続済みという前提で進める。(この記事は前回のTruffleチュートリアル後に書いているため)
module.exports = { networks: { development: { host: "127.0.0.1", // Localhost (default: none) port: 7545, // Standard Ethereum port (default: none) network_id: "*", // Any network (default: none) }, }, mocha: { }, compilers: { solc: { } } }
それが出来たらGanacheを立ち上げる。その後、追加のNFT発行等もコンソール画面で色々やりたいので、Truffleコンソール画面を立ち上げる。
Truffle console
と打つとターミナルにtruffle(development)>
と表示されるようになる。
では早速 migrate
と打ってみる。実行結果が以下の通り。(ここではすでにmigrateをしてしまっていたので、--reset
をしているが初めての場合は気にしなくていい。)
truffle(development)> truffle(development)> migrate --reset Compiling your contracts... =========================== > Everything is up to date, there is nothing to compile. Starting migrations... ====================== > Network name: 'development' > Network id: 5777 > Block gas limit: 0x6691b7 1_initial_migration.js ====================== Replacing 'Migrations' ---------------------- > transaction hash: 0xe7058b58cdc9bb62b4f344d167adfd6fa16c9ca0fa49e382e305913f5c60e5ec > Blocks: 0 Seconds: 0 > contract address: 0x9C8390a018Eb0CA1Eda560284b5BA8b458092024 > block number: 29 > block timestamp: 1563933879 > account: 0xf7654E6fC8Ca3e7dB6C615D49C3e184A8A39E3DB > balance: 99.54674476 > gas used: 261393 > gas price: 20 gwei > value sent: 0 ETH > total cost: 0.00522786 ETH > Saving migration to chain. > Saving artifacts ------------------------------------- > Total cost: 0.00522786 ETH 2_deploy_asset.js ================= Replacing 'Asset' ----------------- > transaction hash: 0xad054c38523eb8e8f181d3c7f777f3d1e47da018e82a4eaf492546a951dec239 > Blocks: 0 Seconds: 0 > contract address: 0x7c107079Aaa4cb108685a3413f604f86f1d9f554 > block number: 31 > block timestamp: 1563933880 > account: 0xf7654E6fC8Ca3e7dB6C615D49C3e184A8A39E3DB > balance: 99.48850862 > gas used: 2869784 > gas price: 20 gwei > value sent: 0 ETH > total cost: 0.05739568 ETH > Saving migration to chain. > Saving artifacts ------------------------------------- > Total cost: 0.05739568 ETH Summary ======= > Total deployments: 2 > Final cost: 0.06262354 ETH
これはいわゆるレシートですね。今回のトランザクションハッシュはこれで、コントラクトアドレスはこれですよ。デプロイにいくらのgas がかかって、いくらのgas peiceでやって、トータルでこれだけのコストが掛かりましたよなどですね。
Metamskで確認
早速作ったNFTをMetamaskで見てみます。ネットワークはGanchaと接続した状態にしておきます。
左のメニューバーを開いて下にある「トークンを追加」を押す。その後トークンを追加という画面になるので、カスタムトークンを選択し、「Token Contract Address」に先ほどデプロイ時のレシート的なものに出ていたContract Address
の値(2_deploy_asset.js
の方)を入れて「次へ」を押す。
すると自動で該当のトークン表
示されるので、「トークンを追加」を押すとMetamaskに追加されている。
追加発行
今回はデプロイと同時に発行を行ったが、せっかく追加発行できる関数を入れたので、それを使ってみる。
ちなみに使えるコマンド等はtab
キー二回連打で以下のように表示される。
Array Boolean Date Error EvalError Function Infinity JSON Math NaN Number Object RangeError ReferenceError RegExp String SyntaxError TypeError URIError decodeURI decodeURIComponent encodeURI encodeURIComponent eval isFinite isNaN parseFloat parseInt undefined Address ArrayBuffer Asset Atomics BigInt BigInt64Array BigUint64Array Buffer Counters DTRACE_HTTP_CLIENT_REQUEST DTRACE_HTTP_CLIENT_RESPONSE DTRACE_HTTP_SERVER_REQUEST DTRACE_HTTP_SERVER_RESPONSE DTRACE_NET_SERVER_CONNECTION DTRACE_NET_STREAM_END DataView ERC165 ERC721 ERC721Enumerable ERC721Full ERC721Metadata Float32Array Float64Array GLOBAL IERC165 IERC721 IERC721Enumerable IERC721Metadata IERC721Receiver Int16Array Int32Array Int8Array Intl Map Migrations Promise Proxy Reflect SafeMath Set SharedArrayBuffer Symbol URL URLSearchParams Uint16Array Uint32Array Uint8Array Uint8ClampedArray WeakMap WeakSet WebAssembly _ _error assert async_hooks buffer child_process clearImmediate clearInterval clearTimeout cluster console crypto dgram dns domain escape events fs global http http2 https inspector module net os path perf_hooks process punycode querystring readline repl require root setImmediate setInterval setTimeout stream string_decoder tls trace_events tty unescape url util v8 vm web3 zlib __defineGetter__ __defineSetter__ __lookupGetter__ __lookupSetter__ __proto__ constructor hasOwnProperty isPrototypeOf propertyIsEnumerable toLocaleString toString valueOf
この中でも一番大事なのがweb3
というもの。これはEthereum JavaScript APIで、ローカル・リモートのEthereumノードとやり取りできるJavascriptライブラリのこと。実際にデプロイしたコントラクトとのやり取りはこいつを使う場合が多い。なので仲良くなっておきましょう。詳細はgithubを見てください。
早速発行しようと行きたいところですが、まずは自分の残高を確認ターミナル上で確認しておきましょう。まず以下のコマンドでアカウントを取得します。
truffle(development)> accounts = await web3.eth.getAccounts()
ここで大事な点はawait
です。古いweb3バージョンだとweb3.eth.getAccounts()
だけでも良いらしいのですが、最新バージョンだとこいつが使えます。JSの教養が乏しいので詳しくは説明できませんが、await
なしだとPromise
型で値が返ってくるのでこいつで何とかしないといけない。(よくわかっていなけどasync/await 入門(JavaScript)を読めば良いと思う)
例えばawait
有り無しで作った時の型の違いを見てみる。
truffle(development)> accounts = web3.eth.getAccounts() undefined truffle(develop)> accounts [ '0xf7654E6fC8Ca3e7dB6C615D49C3e184A8A39E3DB', '0x6a2951c7cD868e43936e401B4F1b956Ea07DFD08', '0xff380d7114c2B95872b0D953c4111c44Cd21a2dF', '0xC0C53371A19A1bc7C97A4212A248A3995D3dEfBc', '0xDdABA55D62658477483ECE4352803804048A45E7', '0xFfBa02dC6ff91e4AbEEEE8Bf8043524f84FC6bFb', '0x855221E7d5522e1Af6Cabf33Ba22027C727d6AE0', '0x01200E489974A2Ad5B7e96dae3c770f73371baCA', '0xaE788e6fdD86AF932882FDcF50963003b18f0655', '0x05f9FA1707CbB3dE4f1A7DE5d6A5AA66bEb203e0' ] truffle(development)> accounts.toString() '[object Object]' truffle(development)> accounts1 = web3.eth.getAccounts() [ '0xf7654E6fC8Ca3e7dB6C615D49C3e184A8A39E3DB', '0x6a2951c7cD868e43936e401B4F1b956Ea07DFD08', '0xff380d7114c2B95872b0D953c4111c44Cd21a2dF', '0xC0C53371A19A1bc7C97A4212A248A3995D3dEfBc', '0xDdABA55D62658477483ECE4352803804048A45E7', '0xFfBa02dC6ff91e4AbEEEE8Bf8043524f84FC6bFb', '0x855221E7d5522e1Af6Cabf33Ba22027C727d6AE0', '0x01200E489974A2Ad5B7e96dae3c770f73371baCA', '0xaE788e6fdD86AF932882FDcF50963003b18f0655', '0x05f9FA1707CbB3dE4f1A7DE5d6A5AA66bEb203e0' ] truffle(development)> accounts1.toString() '[object Promise]'
となる。最初undefined
と返ってくるので焦ったけど、ちゃんと入っている。実際にmint
関数を動かすためにはPromise
型では都合が悪いので、忘れずawait
をします。
一旦instance
というコントラクトオブジェクト(ETHネットワークを通信するオブジェクト)を作成し、アカウントの0番目の量を見てみます。
今回はデプロイしたらNFTが発行されるので、先頭のアカウントが発行したため1つ保持しているはずです。
truffle(development)> instance = await Asset.deployed() undefined truffle(development)> instance.balanceOf(account[0]) <BN: 1>
truffle(development)> instance.mint(0) { tx: '0x6f3caf87af645fdb2051a376cb7389e2b2ffb3a2677ff50ee042a1a364c0ef80', receipt: { transactionHash: '0x6f3caf87af645fdb2051a376cb7389e2b2ffb3a2677ff50ee042a1a364c0ef80', transactionIndex: 0, blockHash: '0x42563adf041c53598b8a5b6788e32f9746d914e5dead6edf2c7fb93084f93954', blockNumber: 39, from: '0xf7654e6fc8ca3e7db6c615d49c3e184a8a39e3db', to: '0x7c107079aaa4cb108685a3413f604f86f1d9f554', gasUsed: 111297, cumulativeGasUsed: 111297, contractAddress: null, logs: [ [Object] ], status: true, logsBloom: ・・・・途中省略・・・ '0x42563adf041c53598b8a5b6788e32f9746d914e5dead6edf2c7fb93084f93954', blockNumber: 39, address: '0x7c107079Aaa4cb108685a3413f604f86f1d9f554', type: 'mined', id: 'log_9ba02281', event: 'Transfer', args: [Result] } ] }
無事できたら残高が増えているか確認してみます。
truffle(development)> instance.balanceOf(account[0]) <BN: 2>
ちゃんと増えているので成功ですね。もちろんMetamaskでも同じように確認できるはず。
送信
作ったはいいけど、このNFTは誰にも送信できません。ということで、送信できるようにしてみましょう。
以下完成コードです。
pragma solidity ^0.5.0; import "../node_modules/openzeppelin-solidity/contracts/token/ERC721/ERC721Full.sol"; contract Asset is ERC721Full { constructor( string memory name, string memory symbol, uint tokenId, string memory tokenURI ) ERC721Full(name, symbol) public { _mint(msg.sender, tokenId); _setTokenURI(tokenId, tokenURI); } function mint(uint256 tokenId) public{ _mint(msg.sender, tokenId); } function transfer(address to, uint tokenId) public { _transferFrom(msg.sender, to, tokenId); } }
変更点は最後のtransfer
関数です。これはERC721.sol
内にある_transferFrom(msg.sender, to, tokenId);
を呼び出します。引数には送信先のアドレスとtokenID
を渡します。送り元はコントラクトを呼び出したアドレスになります。
では早速送信してみましょう。先ほどまでで下準備はできているので、単純にtransfer
関数を読んで送信先と TokenID
を指定するだけです。
truffle(develop)> instance.transfer(account[1], 0) { tx: '0xfb67245af52b2762ddd6fe7b142bf086a08db860b06c21a763ea38089e359962', receipt: { transactionHash: '0xfb67245af52b2762ddd6fe7b142bf086a08db860b06c21a763ea38089e359962', transactionIndex: 0, blockHash: '0x119fa44f6ac8374e94ded70ca2e1a14cfbfed6be14483cea85e829d33c3868cf', blockNumber: 6, from: '0x752a8c5261321fe0a9f1878e120149bac533b212', to: '0x75f3e2e19acecb1867a7e025530bd2224383137b', gasUsed: 84538, cumulativeGasUsed: 84538, contractAddress: null, logs: [ [Object] ], status: true, logsBloom: '0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000040000000000000000000000000000000000001000000008000000000000000000000000000000000000000000000000020000000000000000000800000020000000000000000010000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000002000000000000000000000000000000000000000000000040000020100000000000000000000000000000000000000000000000000000000000100000', v: '0x1b', r: '0x70690692409a293b16d9b26fe09907c866143b8f817b858c9ad7d57ec1583bb4', s: '0x45de03a717b20f2715fcb8caa6b4b1c246f11a1006be7a8e3ee5a1da35635bba', rawLogs: [ [Object] ] }, logs: [ { logIndex: 0, transactionIndex: 0, transactionHash: '0xfb67245af52b2762ddd6fe7b142bf086a08db860b06c21a763ea38089e359962', blockHash: '0x119fa44f6ac8374e94ded70ca2e1a14cfbfed6be14483cea85e829d33c3868cf', blockNumber: 6, address: '0x75F3e2E19aCecb1867a7E025530BD2224383137B', type: 'mined', id: 'log_e2acc8ac', event: 'Transfer', args: [Result] } ] } truffle(develop)> instance.balanceOf(account[0]) <BN: 1> truffle(develop)> instance.balanceOf(account[1]) <BN: 1>
これでオリジナルトークンの発行と送信ができるようになりました。めでたしめでたし。
まとめ
内容は単純だけど、いざ自分で調べてやってみるとしょうもないところで躓いてなかなか思うように進まない。
P.S. ところでPromiseってなんです???
参考文献
- ERC721トークンの実装とRinkebyデプロイ
- async/await 入門(JavaScript)
- What does _(underscore) do?
- 【Solidity基礎】modifier修飾子について
- web3.eth.accounts[0]がundefinedなあなたへ
- Ethereum入門
- JavaScript API
- Promiseについて0から勉強してみた
- Truffleで簡単なContractの作成からテストまで
- OpenZeppelinを活用してセキュアのコントラクトを書く
- Solidity
- openzeppelin-solidityでERC721トークンを発行しよう