DAppsの勉強として分散型Tweetサイト、DTwitterを作りました

作ってみました、初めてのDApps。

初めてということなので、一番簡単そうな分散型のTweet投稿サイトを作ってみました。

もちろん、MetaMaskを使用しています。

<img width="1439" alt="スクリーンショット 2018-04-05 20.34.59.png" src="https://qiita-image-store.s3.amazonaws.com/0/129706/0fccb878-914b-e9b8-7233-8054b7648747.png">


[Decentralized Twitter](https://dtwitter.herokuapp.com/)

#1. 技術スタック

- Vue.js 2.5.13
- solidity 0.4.18
- Truffle 4.0.4
- zeppelin-solidity 1.8.0
- MetaMask

アプリケーションの雛形は[truffleの公式ページ](http://truffleframework.com/boxes/)にある[vue-box](https://github.com/standup75/vue-box)を使いました。
DAppsの開発にはReactが使われることが多いですが、個人的にVue.jsの方が好きなので、今回はこちらを使いました。

#2. こだわったところ
##2.1 ERC721

学習の一環として開発しようと思ったので、TweetはたんにStorageに格納するのではなく、ERC721として、トークン化しました。

```solidity:DTweetToken.sol
pragma solidity ^0.4.16;

import 'zeppelin-solidity/contracts/token/ERC721/ERC721Token.sol';

contract DTweetToken is ERC721Token {

  /* DATA TYPE */
  struct DTweet {
    string title;
    string content;
    bool publishing;
    address mintedBy;
    uint64 mintedAt;
  }

  /* STORAGE */
  DTweet[] DTweets;

  event Mint(address owner, uint256 tokenId);

  /* CONSTRUCTOR */
  function DTweetToken(string _name, string _symbol) public ERC721Token(_name, _symbol) {}

  /* ERC721 IMPLEMENTATION */
  function mint(string _title, string _content, bool _publishing) external returns (uint256) {
    require(msg.sender != address(0));
    DTweet memory dTweet = DTweet({
        title: _title,
        content: _content,
        publishing: _publishing,
        mintedBy: msg.sender,
        mintedAt: uint64(now)
      });
      uint256 tokenId = DTweets.push(dTweet) - 1;
      super._mint(msg.sender, tokenId);

      Mint(msg.sender, tokenId);
      return tokenId;
  }

  function burn(uint256 _tokenId) public {
    super._burn(ownerOf(_tokenId), _tokenId);
    if (DTweets.length != 0) {
      delete DTweets[_tokenId];
    }
  }

  function getDTweet(uint256 _tokenId) external view returns (string title, string content, bool publishing, address mintedBy, uint64 mintedAt) {
    DTweet memory dTweet = DTweets[_tokenId];

    title = dTweet.title;
    content = dTweet.content;
    publishing = dTweet.publishing;
    mintedBy = dTweet.mintedBy;
    mintedAt = dTweet.mintedAt;
  }

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

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

mint()でトークンを発行し、burn()でトークンを削除できるようにしてあります。
zeppelin-solidityのERC721Token.solを継承してやると比較的簡単に作れます。
（これでERC721って合っているのでしょうか？間違っていたらご指摘お願いいたしますm(_ _)m）

getAllDTweetsOfOwner()やgetAllDTweets() といったデータの全件取得の関数も始めは独自に実装しようと、試行錯誤していたのですが、継承元のmappingを利用すれば、実装できました。

##2.2 currentProvider
今回はRopstenのネットワークにスマートコントラクトをデプロイしていますが、MetaMaskのネットワークがRopstenに設定されているかどうかを判定して、メッセージを表示しています。
始め、created()が呼ばれてすぐ、web3 = new Web3(web3.currentProvider)の直後にweb3.currentProvider.publicConfigStore._state.networkVersion !== '3'でRopstenかどうかを判定していましたが、そうすると、MetaMaskのネットワークを変更しても動的にメッセージ内容が更新されなかったです。
そこで、以下のようにweb3.eth.getAccounts((err, accs)の直後に判定すると、MetaMaskのネットワークを変更すると、動的にメッセージが変更されるようになりました。


```vue:DTweet.vue

〜〜（省略）〜〜

<script>
import Web3 from 'web3'
import contract from 'truffle-contract'
import artifacts from '../../build/contracts/DTweetToken.json'
const DTweetToken = contract(artifacts)

export default {
  name: 'DTweet',
  data() {
    return {
      dtweets: [],
      is_network: true,
      message: null,
      tx_hash: null,
      tx_url: null,
      network: null,
      contractAddress: null,
      account: null,
      title: null,
      content: null
    }
  },
  created() {
    if (typeof web3 !== 'undefined') {
      console.warn("Using web3 detected from external source. If you find that your accounts don't appear or you have 0 Fluyd, ensure you've configured that source properly. If using MetaMask, see the following link. Feel free to delete this warning. :) http://truffleframework.com/tutorials/truffle-and-metamask")
      // Use Mist/MetaMask's provider
      web3 = new Web3(web3.currentProvider)
      // ここでnetworkVersionをチェックしても、メッセージが動的に変化せず...
      // if (web3.currentProvider.publicConfigStore._state.networkVersion !== '3') {
      //  this.is_network = false
      //} else {
      //  this.is_network = true
      //}
    } else {
      console.warn("No web3 detected. Falling back to http://127.0.0.1:8545. You should remove this fallback when you deploy live, as it's inherently insecure. Consider switching to Metamask for development. More info here: http://truffleframework.com/tutorials/truffle-and-metamask")
      // fallback - use your fallback strategy (local node / hosted node + in-dapp id mgmt / fail)
      web3 = new Web3(new Web3.providers.HttpProvider("http://127.0.0.1:8545"))
    }

    DTweetToken.setProvider(web3.currentProvider)
    web3.eth.getAccounts((err, accs) => {
      // このタイミングでcrrentProviderのnetwork idを調べると、UIが動的に更新されるみたい
      if (web3.currentProvider.publicConfigStore._state.networkVersion !== '3') {
        this.is_network = false
      } else {
        this.is_network = true
      }
      if (err != null) {
        console.log(err)
        this.message = "There was an error fetching your accounts. Do you have Metamask, Mist installed or an Ethereum node running? If not, you might want to look into that"
        return
      }
      if (accs.length == 0) {
        this.message = "Couldn't get any accounts! Make sure your Ethereum client is configured correctly."
        return
      }
      this.account = accs[0];
      DTweetToken.deployed()
        .then((instance) => instance.address)
        .then((address) => {
          this.contractAddress = address
          this.updateDTweet();
        })
    })
  },
  methods: {
    createDTweet() {
      this.message = "Transaction started";
      return DTweetToken.deployed()
        .then((instance) => instance.mint(this.title, this.content, true, { from: this.account }))
        .then((r) => {
          this.tx_hash = r.tx
          this.tx_url = 'https://ropsten.etherscan.io/tx/' + r.tx
          this.message = "Transaction result"
          var dtweet = {
            "id": null,
            "title": null,
            "content": null,
            "mintedBy": null
          }
          dtweet.id = this.dtweets.length + 1
          dtweet.title = this.title
          dtweet.content = this.content
          dtweet.mintedBy = this.account

          this.dtweets.push(dtweet)
          this.title = null
          this.content = null
        })
        .catch((e) => {
          console.error(e)
          this.message = "Transaction failed"
        })
    },
    updateDTweet() {
      DTweetToken.deployed().then((instance) => instance.getAllDTweetsOfOwner(this.account, { from: this.account })).then((r) => {
        for (var i = 0; i < r.length; i++) {
          this.getDTweet(r[i]);
        }
      })
    },
    getDTweet(tokenId) {
      DTweetToken.deployed().then((instance) => instance.getDTweet(tokenId, { from: this.account })).then((r) => {
        var dtweet = {
          "id": null,
          "title": null,
          "content": null,
          "mintedBy": null
        }
        dtweet.id = tokenId
        dtweet.title = r[0].toString()
        dtweet.content = r[1].toString()
        this.dtweets.push(dtweet)
      })
    },
    deleteDTweet(tokenId){
      DTweetToken.deployed().then((instance) => instance.burn(tokenId, { from: this.account })).then((r) => {
        this.tx_hash = r.tx
        this.tx_url = 'https://ropsten.etherscan.io/tx/' + r.tx
        this.dtweets = []
        this.updateDTweet();
      })
    }
  }
}
</script>
```

#3. まとめ
今回、初めてDAppsを作ってみました。truffle boxを使うとMetaMaskとの連携が簡単にできて、開発が非常に効率的になりました。（drizzleも触ってみたい）

気になった点はTweetをCreateしたあとのUIです。トランザクションが承認されるまで1分間くらいかかるので、その間のUIがイケてないなと感じました。（改善の余地あり）

今回はERC721を単純な形で使いましたが、今後は別のコントラクトや別の規格も含めたトークンの設計やスマートコントラクトの設計について学んでみます。

ソースコードはGitHubにあります。プルリク、イシュー大歓迎です！
https://github.com/shiki-tak/Decentralized_Twitter

#4. 参考
[OpenZeppelinのERC721が更新されたので見てみる](https://qiita.com/marutake/items/671719864e1fbbc757f1)

[Solidity 言語における詰まりどころメモ](https://qiita.com/kyrieleison/items/7f37b5349fc6c7c220cd)

[vue-cliで生成したVue.js 2.0 なアプリケーションをherokuにデプロイする](http://heimusu.hatenablog.com/entry/2017/02/27/220000)