Node.js
Ethereum
solidity
web3.js
DApps

Token Marketという、イーサリアム を使ったERC721ベースのDApps(分散型アプリケーション)を作りました

Token Marketという、イーサリアム を使ったERC721ベースのDApps(分散型アプリケーション)を作りました

プラットフォーム上で自由に発行したERC721トークンを、売買、burnができるものです。
ERC721の使い道を考えていた時に浮かんだアイディアです。

テストネットのropsten環境にデプロイし、独自ドメインも取得しました。

以下のurlです。
https://dtokenmarket.com

また、ソースコードを公開しています。

https://github.com/rjoshima/tokenMarket
フロント側は、学習コストと、保守性、シンプルさの観点からVue.jsを選びました。

使い方のチュートリアルもYoutubeで公開しました。

https://youtu.be/_IyErYJBKFc


工夫したところ

保守性や拡張性(今後のレンタル機能やオークション機能の追加など)の観点から、極力ブラックボックスを少なくし、ライブラリに頼らず全体的にシンプルなロジックを組む必要性があったため、
zeppelinのERC721のmintロジックやburnロジックを継承せずに、必要なとこだけを抜き出し、
売買ロジックと融合させました。

フロント側に関しても、シンプルにかつ、透明感やわかりやすさを意識し、モダンなマテリアルデザインのUIにしました。
また、カードUIを使用することで、cryptokittiesやその他のイケてるDappsっぽくするよう意識しました。
metamaskをインストールし、ropstenモードにしないと、機能しないので
ユーザーがropstenモードにするように、わかりやすく誘導するようにしました。
その一環で、使い方のチュートリアル動画を作って、ユーザーのサービスの学習コストを下げる工夫をしました。

フロント側のnode.jsに関して、async、awaitを使い、非同期処理をシンプルにしました。

トークン生成の際、現状、画像をブロックチェーンに書き込むのではなく、使用頻度と抽象度の高い画像をあらかじめいくつか用意して、ユーザにセレクトさせるようにしました。

ソースコードのメインの一部紹介

solidity

createToken.sol
~上記省略~
contract createToken is ERC721 {
    using SafeMath for uint256;

    // Token name
    string internal name_ = "TokenMarket";

    // Token symbol
    string internal symbol_ = "TM";

    constructor(string _name, string _symbol) public {
        name_ = _name;
        symbol_ = _symbol;

    }

    struct Token {
        string name;
        string details;
        uint256 price;
        bool sale;
        uint256 sort;
        address author;
    }

    /* STORAGE */
    Token[] tokens;


    // Mapping from token ID to owner
    mapping (uint256 => address) internal tokenOwner;

    // Mapping from owner to number of owned token
    // mapping (address => uint256) internal ownedTokensCount;

  // Mapping from owner to list of owned token IDs
    mapping(address => uint256[]) internal ownedTokens;

    // Mapping from token ID to index of the owner tokens list
    mapping(uint256 => uint256) internal ownedTokensIndex;

    // Array with all token ids, used for enumeration
    uint256[] internal allTokens;

    // Mapping from token id to position in the allTokens array
    mapping(uint256 => uint256) internal allTokensIndex;


   // mapping (uint256 => address) public tokenIndexToApproved;

    /*** EVENTS ***/

    event Mint(address owner, uint256 tokenId);

    /*** INTERNAL FUNCTIONS ***/

    function _owns(address _claimant, uint256 _tokenId) internal view returns (bool) {
        return tokenOwner[_tokenId] == _claimant;
    }


    function mint(string _name, string _details, uint256 _price, bool _sale, uint256 _sort) external returns (uint256) {
        require(msg.sender != address(0));


        uint256 tokenId = tokens.push(Token({name: _name, details: _details, price: _price, sale: _sale, sort: _sort, author: msg.sender})) - 1;

        emit Mint(msg.sender, tokenId);
        _transfer(0, msg.sender, tokenId);

        allTokensIndex[tokenId] = allTokens.length;
        allTokens.push(tokenId);

        return tokenId;

    }


    function purchase(uint256 _tokenId) external payable {
        address oldOwner = tokenOwner[_tokenId];
        address newOwner = msg.sender;
        require(oldOwner != newOwner);
        require(tokens[_tokenId].price <= msg.value);
        require(tokens[_tokenId].sale == true);

        Token memory token = tokens[_tokenId];
        uint256 price = token.price;
        require(msg.value >= price);

        _transfer(oldOwner, newOwner, _tokenId);

        tokens[_tokenId].sale = false;

        if (price > 0) {
            newOwner.transfer(price);
        }

    }


    function _transfer(address _from, address _to, uint256 _tokenId) internal {

        tokenOwner[_tokenId] = _to;


        if (_from != address(0)) {
            uint256 tokenIndex = ownedTokensIndex[_tokenId];
            uint256 lastTokenIndex = ownedTokens[_from].length.sub(1);
            uint256 lastToken = ownedTokens[_from][lastTokenIndex];

            ownedTokens[_from][tokenIndex] = lastToken;
            ownedTokens[_from][lastTokenIndex] = 0;

            ownedTokens[_from].length--;
            ownedTokensIndex[_tokenId] = 0;
            ownedTokensIndex[lastToken] = tokenIndex;


        }

        uint256 length = ownedTokens[_to].length;
        ownedTokens[_to].push(_tokenId);
        ownedTokensIndex[_tokenId] = length;

        emit Transfer(_from, _to, _tokenId);

    }


    function totalSupply() public view returns (uint256) {
        return allTokens.length;

    }


    function burn(uint256 _tokenId) external {
        require(ownerOf(_tokenId) == msg.sender);

        // require(ownerOf(_tokenId) == msg.sender || msg.sender == ceoAddress);

        // ownedTokensCount[msg.sender] = ownedTokensCount[msg.sender].sub(1);

        uint256 tokenIndex = allTokensIndex[_tokenId];
        uint256 lastTokenIndex = allTokens.length.sub(1);
        uint256 lastToken = allTokens[lastTokenIndex];
        allTokens[tokenIndex] = lastToken;
        allTokens[lastTokenIndex] = 0;
        allTokens.length--;
        allTokensIndex[_tokenId] = 0;
        allTokensIndex[lastToken] = tokenIndex;



        uint256 ownedtokenIndex = ownedTokensIndex[_tokenId];
        uint256 ownedlastTokenIndex = ownedTokens[msg.sender].length.sub(1);
        uint256 ownedlastToken = ownedTokens[msg.sender][ownedlastTokenIndex];
        ownedTokens[msg.sender][ownedtokenIndex] = ownedlastToken;
        ownedTokens[msg.sender][ownedlastTokenIndex] = 0;
        ownedTokens[msg.sender].length--;

        ownedTokensIndex[_tokenId] = 0;
        ownedTokensIndex[lastToken] = ownedtokenIndex;

        tokenOwner[_tokenId] = address(0);

        emit Transfer(msg.sender, address(0), _tokenId);
    }
~以下省略~

Vue.js

Market.vue
~上記省略~
<script>
  import Web3 from 'web3'
  import contract from 'truffle-contract'
  import artifacts from '../../build/contracts/createToken.json'
  const CreateToken = contract(artifacts)
  export default {
    name: 'UserCollection',
    data () {
      return {
        tokens: [],
        ropsten: false,
        txSuccess: false,
        txUrl: "",
        loading: false,
        gasPrice: web3.toWei(2, "gwei"),
      }
    },
    async created () {

      try {
        web3 = await new Web3(web3.currentProvider)

        CreateToken.setProvider(web3.currentProvider)
        await web3.eth.getAccounts(async (err, accs) => {
          if (web3.currentProvider.publicConfigStore._state.networkVersion !== '3') {
            this.ropsten = true
            return
          } 
          this.account = accs[0];
          console.log(this.account)
          this.instance = await CreateToken.deployed()
          await this.updatetokens()
          await this.getGasPrice()

        })
      } catch (err) {
        console.log(err.message)
        this.ropsten = true
      }

    },
    methods: {
      async updatetokens() {
        try {
          this.tokens = []
          await this.instance.tokensOfAll().then(async (r) => {
            for (var i = 0; i < r.length; i++) {
              await this.gettoken(r[i]);
            }
          })
        } catch (err) {
          console.log(err.message);

        }
      },
      async gettoken(tokenId) {
        try {
          await this.instance.getToken(tokenId, { from: this.account }).then((r) => {
            let token = {}

            token["name"] = r[0];
            token["details"] = r[1];
            token["true_price"] = r[2];
            token["sell"] = r[3]
            token["owner"] = r[4] === this.account
            token["minted"] = r[5];
            token["price"] = parseFloat(web3.fromWei(r[2], "ether"))
            token["id"] = parseInt(tokenId);
            token["sort"] = r[6];
            this.tokens.push(token)
          })
        } catch (err) {
          console.log(err.message);


        }
      },
      async quitSoldToken(tokenId) {
        try {
          if (web3.currentProvider.publicConfigStore._state.networkVersion !== '3') {
            this.ropsten = true
            return
          } 
          this.loading = true;
          await this.instance.quitSoldToken(tokenId, false, { from: this.account, gas:3000000, gasPrice: this.gasPrice}).then(async (r) => {

            this.loading = false;
            const rawTxUrl = "https://ropsten.etherscan.io/tx/" + r.tx
            this.txUrl = "success! check it: <a href=" + rawTxUrl + " target='_blank'}>" + rawTxUrl + "</a>"

            this.txSuccess = true
            console.log(r)
            await this.updatetokens();
          })
        } catch (err) {
          this.loading = false;
          console.log(err.message);
          alert(err.message + " or Use ropsten net( You can't use this function), check it https://qiita.com/tmikada/items/cdc5a3871f655cb7b67d  ")

        }
      },
      async buyToken(tokenId, tokenPrice) {
        try {
          if (web3.currentProvider.publicConfigStore._state.networkVersion !== '3') {
            this.ropsten = true
            return
          } 
          this.loading = true;
          await this.instance.purchase(tokenId, { from: this.account, value: web3.toWei(tokenPrice, "wei"), gas:3000000, gasPrice: this.gasPrice}).then(async (r) => {
            this.loading = false;
            const rawTxUrl = "https://ropsten.etherscan.io/tx/" + r.tx
            this.txUrl = "success! check it: <a href=" + rawTxUrl + " target='_blank'}>" + rawTxUrl + "</a>"
            this.txSuccess = true
            console.log(r)
            await this.updatetokens();
          })
        } catch (err) {
          this.loading = false;
          console.log(err.message);
          alert(err.message + " or Use ropsten net( You can't use this function), check it https://qiita.com/tmikada/items/cdc5a3871f655cb7b67d  ")

        }
      },  

      async getGasPrice() {
        await web3.eth.getGasPrice((err, result) => {
          if (err) {
            console.log(err.message);
            alert(err.message)
          } else {        
            this.gasPrice = result.toString(10);  

          }
        })
      }
     }
    }

</script>
~以下省略~

大変だったところ

情報が少なすぎるので、一個一個自分で考えてロジックを作る必要があった(コピペがきかない)
不安定な開発ツール(updateが速い)を使わざるを得ないので、文字通り、意味不明がエラーが起きる。
その時は、一個一個、理詰めでfixするしかなく、結構時間的にコストがかかった。

次は

レンタルロジックや、オークションロジックを追加実装し、それに伴い、UI側も刷新して行こうと思っています。

まとめ

みなさんが自由にERC721トークンを発行することで、新たなERC721の使い道を見い出せればと思っています。
よかったら、フィードバックいただけたらと思います。
プルリクも待ってます。
コードも良かったら、スターお願い致します。。
また、DApps開発のお仕事もしたいと思っています。(現在freelance)
良かったら、メッセージ待っています。
DApps、ブロックチェーン盛り上げていきましょう。