Ethereum
solidity
SmartContract
仮想通貨
ブロックチェーン

ブロックチェーンで動くフリマアプリを作ってみた

はじめに

image.png
 実行結果がブロックチェーンに保存されるプログラムをスマートコントラクトといいます.ブロックチェーンの内容は改ざんすることができないので,スマートコントラクトを使えば,中央管理者のいないサービスを作ることができます.
 そこで(勉強も兼ねてなんですが汗)スマートコントラクトでフリマアプリを作ってみました.もし実現すれば,中央管理者がいないので手数料が格安になります.国境も関係なく,仮想通貨を持っている人ならだれでも参加できます.仮想通貨を利用したシェアリングエコノミーって夢を感じませんか?(^ー^* )ちなみに売上げの5%はユニセフに寄付,残りは全て出品者に渡すので,オーナーの利益はゼロという賢者仕様です.
 プラットフォームはBitcoinの次くらいに有名な仮想通貨,イーサリアムです.Remix-Solidity IDEで開発しました.Solidityはスマートコントラクトの開発で現在最も利用されている言語です.Remixはその総合開発環境のひとつで,ブラウザで動くため環境構築がいりません.簡単です.
 この記事で紹介するスマートコントラクトは,私が未熟故,まだ使い勝手やセキュリティ面で至らぬところがかなりあると思いますが,これから改善していく予定です...お気づきの点がありましたら,コメント頂けますと幸いです.m(__)m

 スマートコントラクトをRopstenテストネットワークに公開しました.ウェブユーザーインターフェースを作成しました.使用にはMetaMaskのインストールが必要です.MetaMaskについてはこちらをご覧下さい.
 まだプロトタイプ丸出しなんですが,これから勉強しつつ良くしていこうと思います.

使い方

 以下の順番で関数を実行すれば一通りの取引が完了します.需要があれば説明はおいおい詳しくしていこうと思います.

1.アカウント作成

function registerAccount(string _name, string _email) public {
        require(!accounts[msg.sender].resistered); // 未登録のethアドレスか確認する

        accounts[msg.sender].name = _name;   // 名前
        accounts[msg.sender].email = _email; // emailアドレス
        accounts[msg.sender].resistered = true;
    }
引数 説明
_name アカウント名
_email アカウントのemailアドレス

 このスマートコントラクトの利用にはアカウント登録が必要としました.各ユーザーに対して以下の情報を記録します.

// アカウント情報
    struct account {
        string name;          // 名前
        string email;         // emailアドレス
        uint numTransactions; // 取引回数
        int reputations;      // 取引評価, 大きい値ほど良いユーザー
        bool resistered;      // アカウント未登録:false, 登録済み:true
    }
    mapping(address => account) public accounts;

2.出品

function sell(string _name, string _description, uint _price) public onlyUser {
        items[numItems].sellerAddr = msg.sender;            // 出品者のethアドレス
        items[numItems].seller = accounts[msg.sender].name; // 出品者名
        items[numItems].name = _name;               // 商品名
        items[numItems].description = _description; // 商品説明
        items[numItems].price = _price;             // 商品価格
        numItems++;
    }
引数 説明
_name 商品名
_description 商品説明
_price 商品価格

 この関数を実行すると商品番号(numItems)が出品順に与えられます.各商品は以下の情報を持っています.

struct item {
        address sellerAddr;  // 出品者のethアドレス
        address buyerAddr;   // 購入者のethアドレス
        string seller;       // 出品者名
        string name;         // 商品名
        string description;  // 商品説明
        uint price;          // 価格
        bool payment;        // false:未支払い, true:支払済み
        bool shipment;       // false:未発送, true:発送済み
        bool receivement;    // false:未受取り, true:受取済み
        bool sellerReputate; // 出品者の評価, false:未評価, true:評価済み
        bool buyerReputate;  // 購入者の評価, false:未評価, true:評価済み
        bool stopSell;       // false:出品中, true:出品取消し
    }
    mapping(uint => item) public items;

3.購入

function buy(uint _numItems) public payable onlyUser {
        require(_numItems <= numItems);      // 存在しない商品を購入しようとしていないか確認
        require(!items[_numItems].payment);  // 商品が売り切れていないか確認
        require(!items[_numItems].stopSell); // 出品取消しになっていないか確認
        require(items[_numItems].price == msg.value); // 入金金額が商品価格と一致しているか確認

        items[_numItems].buyerAddr = msg.sender; // 購入者のethアドレス
        items[_numItems].payment = true;  // 支払済み
        items[_numItems].stopSell = true; // 売れたので出品をストップする
    }
引数 説明
_numItems 商品番号

 購入商品は商品番号で指定します.購入したい商品の番号をいちいち調べないといけないので,これはクライアントアプリで改善したいなぁと思っています.

4.商品の発送

function ship(uint _numItems) public onlyUser {
        require(items[_numItems].sellerAddr == msg.sender); // コントラクトの呼び出しが出品者か確認する
        require(_numItems <= numItems);    // 存在しない商品を発送しようとしていないか確認
        require(items[_numItems].payment); // 入金済み商品か確認する

        items[_numItems].shipment = true;
    }
引数 説明
_numItems 商品番号

 ここで,発送に必要な情報(住所とか)どうやって連絡するんだ?ということになります.イーサリアムのアドレスと登録したアカウント情報からEmailアドレスが分かるので,一旦,出品者と購入者にEmailでやり取りしてもらうことにしました.

5.商品の受取り

function receive(uint _numItems) public payable onlyUser {
        require(items[_numItems].buyerAddr == msg.sender); // コントラクトの呼び出しが購入者か確認する
        require(_numItems <= numItems);     // 存在しない商品を受け取ろうとしていないか確認
        require(items[_numItems].shipment); // 発送済み商品か確認する

        items[_numItems].receivement = true;
        // 受取りが完了したら出品者とユニセフにethを送金する
        donation.transfer(items[_numItems].price * 1 / 20); // 売上の5%を寄付
        items[_numItems].sellerAddr.transfer(items[_numItems].price * 19 / 20); // 残りを出品者に送金する
    }
引数 説明
_numItems 商品番号

 商品の受取りが完了するまで,出品者へ売上は送金されません.商品受取り完了後,スマートコントラクトへデポジットされていたイーサリアムが自動的に送金されます.なお,売上の5%はユニセフに募金されます.

6.出品者,購入者の評価

function sellerEvaluate(uint _numItems, int _reputate) public onlyUser {
        require(items[_numItems].sellerAddr == msg.sender); // コントラクトの呼び出しが出品者か確認する
        require(_reputate >= -2 && _reputate <= 2); // 評価は-2 ~ +2の範囲で行う
        require(!items[_numItems].sellerReputate);  // 購入者の評価が完了をしていないことを確認

        accounts[items[_numItems].buyerAddr].numTransactions++;        // 購入者の取引回数の加算
        accounts[items[_numItems].buyerAddr].reputations += _reputate; // 購入者の評価の更新
        items[_numItems].sellerReputate = true; // 評価済みにする
    }
引数 説明
_numItems 商品番号
_reputate 評価値

 評価値は-2~+2の範囲で指定します.上記のコードは出品者が購入者を評価する関数ですが,逆の関数もあります.

ソースコード

スマートコントラクト

pragma solidity ^0.4.23;

contract KappaMarket {

    address owner;        // コントラクトオーナーのアドレス
    address donation;     // ユニセフのアドレス 
    uint public numItems; // 商品数

    // コンストラクタ
    constructor() public {
        owner = msg.sender;
        // ユニセフのアドレス
        // http://helpdesk.unicef.org.nz/knowledge_base/topics/donate-to-unicef-via-cryptocurrencies
        donation = 0xB9407f0033DcA85ac48126a53E1997fFdE04B746;
        numItems = 0;
    }

    // コントラクトの呼び出しがコントラクトのオーナーか確認する
    modifier onlyOwner {
        require(msg.sender == owner);
        _;
    }

    // コントラクトの呼び出しがアカウント情報登録済みユーザーか確認する
    modifier onlyUser {
        require(accounts[msg.sender].resistered);
        _;
    }

    // 商品情報
    struct item {
        address sellerAddr;  // 出品者のethアドレス
        address buyerAddr;   // 購入者のethアドレス
        string seller;       // 出品者名
        string name;         // 商品名
        string description;  // 商品説明
        uint price;          // 価格
        bool payment;        // false:未支払い, true:支払済み
        bool shipment;       // false:未発送, true:発送済み
        bool receivement;    // false:未受取り, true:受取済み
        bool sellerReputate; // 出品者の評価, false:未評価, true:評価済み
        bool buyerReputate;  // 購入者の評価, false:未評価, true:評価済み
        bool stopSell;       // false:出品中, true:出品取消し
    }

    // 商品情報は誰でも見ることができる
    mapping(uint => item) public items;

    // アカウント情報
    struct account {
        string name;          // 名前
        string email;         // emailアドレス
        uint numTransactions; // 取引回数
        int reputations;      // 取引評価, 大きい値ほど良いユーザー
        bool resistered;      // アカウント未登録:false, 登録済み:true
    }

    // アカウント情報も誰でも見ることができる(todo:見せちゃっていい?)
    mapping(address => account) public accounts;

    // アカウント情報を登録する関数
    function registerAccount(string _name, string _email) public {
        require(!accounts[msg.sender].resistered); // 未登録のethアドレスか確認する

        accounts[msg.sender].name = _name;   // 名前
        accounts[msg.sender].email = _email; // emailアドレス
        accounts[msg.sender].resistered = true;
    }

    // アカウント情報を修正する関数
    function modifyAccount(string _name, string _email) public onlyUser {
        accounts[msg.sender].name = _name;   // 名前
        accounts[msg.sender].email = _email; // emailアドレス
    }

    // 出品する関数
    function sell(string _name, string _description, uint _price) public onlyUser {
        items[numItems].sellerAddr = msg.sender;            // 出品者のethアドレス
        items[numItems].seller = accounts[msg.sender].name; // 出品者名
        items[numItems].name = _name;               // 商品名
        items[numItems].description = _description; // 商品説明
        items[numItems].price = _price;             // 商品価格
        numItems++;
    }

    // 出品内容を変更する関数
    function modifyItem(uint _numItems, string _name, string _description, uint _price) public onlyUser {
        require(items[_numItems].sellerAddr == msg.sender);  // コントラクトの呼び出しが出品者か確認する
        items[_numItems].seller = accounts[msg.sender].name; // 出品者名
        items[_numItems].name = _name;                       // 商品名
        items[_numItems].description = _description;         // 商品説明
        items[_numItems].price = _price;                     // 商品価格
    }

    // 購入する関数
    function buy(uint _numItems) public payable onlyUser {
        require(_numItems <= numItems);      // 存在しない商品を購入しようとしていないか確認
        require(!items[_numItems].payment);  // 商品が売り切れていないか確認
        require(!items[_numItems].stopSell); // 出品取消しになっていないか確認
        require(items[_numItems].price == msg.value); // 入金金額が商品価格と一致しているか確認

        items[_numItems].buyerAddr = msg.sender; // 購入者のethアドレス
        items[_numItems].payment = true;  // 支払済み
        items[_numItems].stopSell = true; // 売れたので出品をストップする
    }

    // 発送完了時に呼び出される関数
    function ship(uint _numItems) public onlyUser {
        require(items[_numItems].sellerAddr == msg.sender); // コントラクトの呼び出しが出品者か確認する
        require(_numItems <= numItems);    // 存在しない商品を発送しようとしていないか確認
        require(items[_numItems].payment); // 入金済み商品か確認する

        items[_numItems].shipment = true;
    }

    // 商品受取り時に呼び出される関数
    function receive(uint _numItems) public payable onlyUser {
        require(items[_numItems].buyerAddr == msg.sender); // コントラクトの呼び出しが購入者か確認する
        require(_numItems <= numItems);     // 存在しない商品を受け取ろうとしていないか確認
        require(items[_numItems].shipment); // 発送済み商品か確認する

        items[_numItems].receivement = true;
        // 受取りが完了したら出品者とユニセフにethを送金する
        donation.transfer(items[_numItems].price * 1 / 20); // 売上の5%を寄付
        items[_numItems].sellerAddr.transfer(items[_numItems].price * 19 / 20); // 残りを出品者に送金する
    }

    // 出品者が購入者を評価する関数
    function sellerEvaluate(uint _numItems, int _reputate) public onlyUser {
        require(items[_numItems].sellerAddr == msg.sender); // コントラクトの呼び出しが出品者か確認する
        require(_reputate >= -2 && _reputate <= 2); // 評価は-2 ~ +2の範囲で行う
        require(!items[_numItems].sellerReputate);  // 購入者の評価が完了をしていないことを確認

        accounts[items[_numItems].buyerAddr].numTransactions++;        // 購入者の取引回数の加算
        accounts[items[_numItems].buyerAddr].reputations += _reputate; // 購入者の評価の更新
        items[_numItems].sellerReputate = true; // 評価済みにする
    }

    // 購入者が出品者を評価する関数
    function buyerEvaluate(uint _numItems, int _reputate) public onlyUser {
        require(items[_numItems].buyerAddr == msg.sender); // コントラクトの呼び出しが購入者か確認する
        require(_reputate >= -2 && _reputate <= 2); // 評価は-2 ~ +2の範囲で行う
        require(!items[_numItems].buyerReputate);   // 購入者の評価が完了をしていないことを確認

        accounts[items[_numItems].sellerAddr].numTransactions++;        // 購入者の取引回数の加算
        accounts[items[_numItems].sellerAddr].reputations += _reputate; // 購入者の評価の更新
        items[_numItems].buyerReputate = true; // 評価済みにする
    }

    // 出品を取り消す関数(出品者)
    function sellerStop(uint _numItems) public onlyUser {
        require(items[_numItems].sellerAddr == msg.sender); // コントラクトの呼び出しが出品者か確認する
        require(_numItems <= numItems);     // 存在しない商品を削除しようとしていないか確認
        require(!items[_numItems].payment); // 出品中の商品か確認する

        items[_numItems].stopSell = true; // 出品の取消し
    }

    // 出品を取り消す関数(オーナー)
    function ownerStop(uint _numItems) public onlyOwner {
        require(items[_numItems].sellerAddr == msg.sender); // コントラクトの呼び出しが出品者か確認する
        require(_numItems <= numItems);     // 存在しない商品を削除しようとしていないか確認
        require(!items[_numItems].payment); // 出品中の商品か確認する

        items[_numItems].stopSell = true;
    }

    // 購入者へ返金する関数
    // 商品が届かなかった時に使用する
    function ownerRefund(uint _numItems) public payable onlyOwner {
        require(_numItems <= numItems);    // 存在しない商品を選択していないか確認
        require(items[_numItems].payment); // 入金済み商品か確認する

        items[_numItems].buyerAddr.transfer(items[_numItems].price); // 購入者へ返金
    }

    function sellerRefund(uint _numItems) public payable {
        require(_numItems <= numItems);    // 存在しない商品を選択していないか確認
        require(msg.sender == items[_numItems].sellerAddr); // コントラクトの呼び出しが出品者か確認する
        require(items[_numItems].payment); // 入金済み商品か確認する

        items[_numItems].buyerAddr.transfer(items[_numItems].price); // 購入者へ返金
    }

    // コントラクトを破棄して,残金をオーナーに送る関数
    // クラッキング対策
    function kill() public onlyOwner {
        selfdestruct(owner);
    }
}

ウェブユーザーインターフェース

<!DOCTYPE html>
<html>

<title>Kappaマーケット</title>
<head>
<meta charset="UTF-8">
<script language="javascript" type="text/javascript" src="web3.min.js"></script>
<script language="javascript" type="text/javascript" src="FrontEndAbi.js"></script>
</head>

<body>
    <p><b>アカウント登録</b><br>
    ユーザー名:<br>
    <input name="UserName" id="UserName" type="text" value="" /><br>
    Eメールアドレス:<br>
    <input name="UserEmail" id="UserEmail" type="email" value="" /><br>
    <button onclick="registerAccount()">登録</button><br></p>
    <hr>

    <p><b>出品</b><br>
    商品名:<br>
    <input name="ItemName" id="ItemName" type="text" value="" /><br>
    説明:<br>
    <textarea name="Description" id="Description" type="text" rows="4" cols="40" value=""></textarea><br>
    価格(wei):<br>
    <input name="Price" id="Price" type="number" value="" /><br>
    <button onclick="sell()">出品</button><br></p>
    <hr>

    <p><b>購入</b><br>
    商品番号:<br>
    <input name="NumItem" id="NumItem" type="number" value="" /><br>
    価格(wei):<br>
    <input name="BuyPrice" id="BuyPrice" type="number" value="" /><br>
    <button onclick="buy()">購入</button><br></p>
    <hr>

    <p><b>発送連絡</b><br>
    商品番号:<br>
    <input name="Ship" id="Ship" type="number" value="" /><br>
    <button onclick="ship()">発送連絡</button><br></p>
    <hr>

    <p><b>受取連絡</b><br>
    商品番号:<br>
    <input name="Receive" id="Receive" type="number" value="" /><br>
    <button onclick="receive()">受取連絡</button><br></p>
    <hr>

    <p><b>購入者を評価</b><br>
    商品番号:<br>
    <input name="Item-a" id="Item-a" type="number" value="" /><br>
    評価値:<br>
    <input name="SellerEvaluate" id="SellerEvaluate" type="number" value="" /><br>
    <button onclick="sellerEvaluate()">購入者を評価</button><br></p>
    <hr>

    <p><b>出品者を評価</b><br>
    商品番号:<br>
    <input name="Item-b" id="Item-b" type="number" value="" /><br>
    評価値:<br>
    <input name="BuyerEvaluate" id="BuyerEvaluate" type="number" value="" /><br>
    <button onclick="buyerEvaluate()">出品者を評価</button><br></p>
    <hr>

    <p><b>商品表示</b><br>
    商品番号:<br>
    <input name="showItem" id="showItem" type="number" value="" /><br>
    <button onclick="showItem()">商品表示</button><br></p>
    <ul id="Item"></ul>
    <hr>

    <p><b>アカウント情報表示</b><br>
    アドレス:<br>
    <input name="Address" id="Address" type="text" value="" /><br>
    <button onclick="showAccount()">アカウント情報表示</button><br></p>
    <ul id="Account"></ul>
</body>

<script type="text/javascript">
    // メタマスクがインストールされているかのチェック
    if (typeof web3 !== 'undefined') {
        web3js = new Web3(web3.currentProvider);
    } else {
        alert("MetaMaskをインストールして下さい.");
    }

    // コントラクトを呼び出すアカウントのアドレス
    web3js.eth.getAccounts(function(err, accounts) {
        coinbase = accounts[0];
        console.log("coinbase is " + coinbase)
    });

    // コントラクトのアドレス
    const address = "0x3cdc9c33fba646d2c914964aa654aa349c1b9f40";

    // コントラクトのインスタンスを生成
    contract = new web3js.eth.Contract(abi, address);

    // アカウント登録
    function registerAccount() {
        var UserName = document.getElementById("UserName").value;
        var UserEmail = document.getElementById("UserEmail").value;

        return contract.methods.registerAccount(UserName, UserEmail)
        .send({ from: coinbase })
        .on("receipt", function(receipt){
            console.log("success");
        })
        .on("error", function(error){
                console.log("error"); 
        });
    }

    // 出品
    function sell() {
        var ItemName = document.getElementById("ItemName").value;
        var Description = document.getElementById("Description").value;
        var Price = document.getElementById("Price").value;

        return contract.methods.sell(ItemName, Description, Price)
        .send({ from: coinbase })
        .on("receipt", function(receipt){
            console.log("success");
        })
        .on("error", function(error){
                console.log("error"); 
        });
    }

    // 購入
    function buy() {
        var NumItem = document.getElementById("NumItem").value;
        var BuyPrice = document.getElementById("BuyPrice").value;

        return contract.methods.buy(NumItem)
        .send({ from: coinbase, value: BuyPrice })
        .on("receipt", function(receipt){
            console.log("success");
        })
        .on("error", function(error){
                console.log("error"); 
        });
    }

    // 発送連絡
    function ship() {
        var NumItem = document.getElementById("Ship").value;

        return contract.methods.ship(NumItem)
        .send({ from: coinbase })
        .on("receipt", function(receipt){
            console.log("success");
        })
        .on("error", function(error){
                console.log("error"); 
        });
    }

    // 受取連絡
    function receive() {
        var NumItem = document.getElementById("Receive").value;

        return contract.methods.receive(NumItem)
        .send({ from: coinbase })
        .on("receipt", function(receipt){
            console.log("success");
        })
        .on("error", function(error){
                console.log("error"); 
        });
    }

    // 購入者を評価
    function sellerEvaluate() {
        var NumItem = document.getElementById("Item-a").value;
        var SellerEvaluate = document.getElementById("SellerEvaluate").value;

        return contract.methods.sellerEvaluate(NumItem, SellerEvaluate)
        .send({ from: coinbase })
        .on("receipt", function(receipt){
            console.log("success");
        })
        .on("error", function(error){
                console.log("error"); 
        });
    }

    // 出品者を評価
    function buyerEvaluate() {
        var NumItem = document.getElementById("Item-b").value;
        var BuyerEvaluate = document.getElementById("BuyerEvaluate").value;

        return contract.methods.buyerEvaluate(NumItem, BuyerEvaluate)
        .send({ from: coinbase })
        .on("receipt", function(receipt){
            console.log("success");
        })
        .on("error", function(error){
                console.log("error"); 
        });
    }

    // 商品を表示する
    function showItem() {
        var NumItem = document.getElementById("showItem").value;

        sl = document.getElementById('Item');
        while(sl.lastChild) {
            sl.removeChild(sl.lastChild);
        }

        contract.methods.items(NumItem).call().then(function(item){
            for (var i = 0; i < Object.keys(item).length/2; i++) {
                var li = document.createElement('li');
                li.textContent = Object.keys(item)[i+Object.keys(item).length/2] + " : " + item[i];
                document.getElementById('Item').appendChild(li);
            }
        });
    }

    // アカウント情報を表示する
    function showAccount() {
        var Address = document.getElementById("Address").value;

        sl = document.getElementById('Account');
        while(sl.lastChild) {
            sl.removeChild(sl.lastChild);
        }

        contract.methods.accounts(Address).call().then(function(account){
            for (var i = 0; i < Object.keys(account).length/2; i++) {
                var li = document.createElement('li');
                li.textContent = Object.keys(account)[i+Object.keys(account).length/2] + " : " + account[i];
                document.getElementById('Account').appendChild(li);
            }
        });
    }

</script>
</html>

おわりに

 次はPCやスマートフォンから簡単操作できるクライアントアプリを作れる様になりたいなぁと思っています.
 以下の書籍を参考にしました.
・ブロックチェーンアプリケーション開発の教科書(マイナビ出版 (2018/2/1))
・堅牢なスマートコントラクト開発のためのブロックチェーン[技術]入門(技術評論社) (2017/10/27))