Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

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

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

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

スクリーンショット 2018-04-05 20.34.59.png

Decentralized Twitter

1. 技術スタック

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

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

2. こだわったところ

2.1 ERC721

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

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のネットワークを変更すると、動的にメッセージが変更されるようになりました。

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が更新されたので見てみる

Solidity 言語における詰まりどころメモ

vue-cliで生成したVue.js 2.0 なアプリケーションをherokuにデプロイする

shiki_tak
元理論物理学屋。重力理論が専門でした。 現在の主な使用技術は Blockchain | Rust | Java | HBase | Golang | https://scrapbox.io/shiki-tak/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away