卒論から始まるBlockchain

卒論から始まるBlockchain

俺の屍を越えて行け

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発行
  • 誰でも追加発行、譲渡可能

ディレクトリ作成

  • ディレクトリを作ってそこでTruffleの新しいプロジェクトを作る(ここではerc721-testというディレクトリを作成)
$ 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) 各パラメータ

  • nameトークン名
  • symbolトークン単位
  • tokenId:アセット識別子
  • tokenURLトークンの詳細譲歩を示すURLファイルのパス

(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, symbolERC721Metadata.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って何やねんてんって方は以下の記事をどうぞ

【Solidity基礎】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 を呼び出し tokenIdtokenURI を関連づける。 _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.jsmigrattions配下に作成。ここでは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 の方)を入れて「次へ」を押す。 f:id:mochi-mochi-0397:20190803103128p:plain すると自動で該当のトークン表 示されるので、「トークンを追加」を押すとMetamaskに追加されている。

f:id:mochi-mochi-0397:20190803103234p:plain

追加発行

今回はデプロイと同時に発行を行ったが、せっかく追加発行できる関数を入れたので、それを使ってみる。 ちなみに使えるコマンド等は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ってなんです???

参考文献