Edited at

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にデプロイする