ブロックチェーンで動くフリマアプリを作ってみる～フロントエンド編～

#ブロックチェーンフリマアプリのウェブページ作ってみました
![image.png](https://qiita-image-store.s3.amazonaws.com/0/158180/890d9029-c24f-a669-8f6a-d3585dfb8899.png)
https://yutaizumi.github.io/KappaMarket/html/main.html

[前回](https://qiita.com/Yu-Nie/items/e7472d0ae6a1b7408fe3)は名簿を作成するスマートコントラクトをテストネットワークに公開し，ウェブサイトからアクセスしました．今回はブロックチェーンで動くフリマアプリのウェブインターフェースを作ってみました．~~スマートフォンは未対応です．~~（2018/11/24）スマートフォンからも見られるようになりました．利用にはメタマスクとIPFSが必要です．いづれもネットで調べるとたくさん記事が出てくるので，インストールや使用方法は割愛させて下さい．この記事ではウェブインターフェースの使い方とソースコードをご紹介したいと思います．
ウェブ系のプログラミングは初心者で，四苦八苦しながら作っているので，ぜひ温かい目で見守って下さい(笑)コメント，ご質問等大歓迎です．よろしくお願いします！m(__)m

2018/11/19
デザインを少し変えてみました．
![image.png](https://qiita-image-store.s3.amazonaws.com/0/158180/30ed9c8a-2bb4-9aa2-110e-5d5189b0a0e5.png)

関連する過去の記事
[ブロックチェーンで動くフリマアプリを作ってみる～スマートコントラクト編～](https://qiita.com/Yu-Nie/items/b11b3a784661a03bbb6a)
[ブロックチェーンにアクセスできるウェブサイトを作ってみる](https://qiita.com/Yu-Nie/items/e7472d0ae6a1b7408fe3)

ソースコードはgithubにアップロードしています．
https://github.com/YutaIzumi/KappaMarket

#基本的な使い方
メタマスクを起動しRopstenテストネットワークに接続した状態で以下のページを開きます．
http://kappaichiba.html.xdomain.jp/html/main.html
##ユーザー登録
アプリの使用にはユーザー登録が必要としました．ユーザーの評価や連絡先を保存する必要があると考えたからです．未登録のユーザー（イーサリアムアドレス）は出品や購入ができない仕様になってます．
![image.png](https://qiita-image-store.s3.amazonaws.com/0/158180/69f7c76f-a4fe-34fe-4493-58f569306759.png)
「会員登録」をクリックして下さい．
![image.png](https://qiita-image-store.s3.amazonaws.com/0/158180/7ec120c2-8f24-1368-6ce0-3ab15904de55.png)
ユーザー名とEメールアドレスを入力し，「登録ボタン」をクリックします．
![image.png](https://qiita-image-store.s3.amazonaws.com/0/158180/1fb83b7a-f81f-c731-c2c5-36b93d343f49.png)
メタマスクが起動しますので，「確認」をクリックしてください．しばらく待つと登録内容がブロックチェーンに書き込まれて，登録が完了します．
![image.png](https://qiita-image-store.s3.amazonaws.com/0/158180/16f0b8a0-2a86-b7fd-d3c9-dad6d83564ea.png)
念のためちゃんと登録されているか確認します．「会員情報を見る」をクリックします．
![image.png](https://qiita-image-store.s3.amazonaws.com/0/158180/818c7bd8-b3f5-c022-1f15-1afb2841f420.png)
自分のウォレットアドレスを入力し，「アカウント情報表示ボタン」をクリックすると，下にアカウント情報が表示されます．
##購入
![image.png](https://qiita-image-store.s3.amazonaws.com/0/158180/414c87a6-3144-950b-9e78-45b60cb61bb0.png)
出品中の商品の画像をクリックします．商品の詳細を表示するページに移動します．
![image.png](https://qiita-image-store.s3.amazonaws.com/0/158180/1176e88e-1457-7f84-36f7-3261b9e8ab2e.png)
「こちらから購入できますボタン」をクリックしてください．メタマスクが起動し「確認」をクリックすると購入できます．
![image.png](https://qiita-image-store.s3.amazonaws.com/0/158180/4437ce69-88da-e70e-4176-1a453a8e7b91.png)
しばらく待ってからページを再読み込みして下さい．出品中⇒売切れに変化します．売切れの商品はもう購入できなくなります（コントラクトの実行が通りません）．購入が確定したら購入者は出品者のEメールアドレスに連絡して，住所など発送に必要な情報を伝えます．Eメールアドレスは「会員情報を見る」から検索できます．ちなみに，この時点では出品者に代金は振り込まれません．一旦コントラクトで預かっています．購入者から受取連絡があった時点で，自動的に出品者に代金が振り込まれます．
![image.png](https://qiita-image-store.s3.amazonaws.com/0/158180/3279f1e5-ddc9-fbc9-d2c9-7ed2a3e5eb73.png)
「購入した商品一覧」をクリックしてください．これまでに購入した商品の商品説明と取引の進捗が表示されます．受取連絡や出品者の評価はこのページで行います．なお，受取連絡は出品者の発送が完了してからしかできません．取引の進捗は全てブロックチェーンに書き込まれます．
##出品
テストとして家の柿の木になっていた柿を出品してみます．
![image.png](https://qiita-image-store.s3.amazonaws.com/0/158180/3415daf4-a212-9436-8713-5edfd82dc677.png)
![image.png](https://qiita-image-store.s3.amazonaws.com/0/158180/e1f63f0e-8482-52a5-c8e3-25aa18f9cc76.png)
「出品する」をクリックします．商品情報を入力し「出品ボタン」をクリックします．商品の画像はブロックチェーンには保存できないので，IPFSに保存したファイルのハッシュ値を入力します．IPFSは分散型ストレージです．私も技術内容は良く理解できていませんが(笑)，BitTorrent等ファイル共有ソフトに近いものだと認識しています．ブロックチェーンの参考書に載っていたので使ってみました．使い方は以下が参考になると思います．
[IPFSの環境構築の手順と基本的なコマンドを学ぶ](http://block-chain.jp/blockchain_study_salon_dojo/how-to-use-ipfs/)
![image.png](https://qiita-image-store.s3.amazonaws.com/0/158180/2a751367-2086-f740-06a1-15524573bf2c.png)
商品情報がブロックチェーンに取り込まれたら，メインページを確認してみましょう．ちゃんと柿が表示されました！画像ファイルがIPFSのネットワークに広まるまで時間が掛かるみたいで，画像はすぐに表示されないかもしれません．
![image.png](https://qiita-image-store.s3.amazonaws.com/0/158180/6a94d28b-21bb-cbb9-cded-ea9b0efbd247.png)
「出品した商品一覧」に移動します．この柿を誰かが購入してくれたら，購入者からの連絡を待つか，自分から購入者に連絡して，発送に必要な情報を取得し商品を発送します．
使い方のフローをまとめるとこんな感じです．

１．会員登録
２．出品 or 購入
３．商品の発送に必要な情報を交換する．
　　連絡先は「会員情報を見る」から検索する．
４．商品の発送連絡
５．商品の受取連絡（ここで出品者に代金が支払われる．）
６．出品者，購入者の評価

出品者がやるべき作業は出品者しか行えない（出品者のアドレス以外ではコントラクトが通らない）様になってます．購入者の場合も同様です．

#おわりに
今回ゼロから作ってみてかなり勉強になりました．まだ改良できる点がたくさんありますので，これから良くしていこうと思います．
最後にちょっとだけ，思想的な話になりますが，私がブロックチェーンに興味を持つのは少し思うところがありまして．．．私たちは資本主義は所与のシステム，大前提だと考えがちなんですが，実は過渡的なものなんじゃないか，と最近思います．テクノロジーを短期間で高度化し，便利な生活を実現する，単なる１つの方法なんじゃないかと．格差とか環境破壊とかデメリットも大きいですし，豊かさがサチュレーションした段階で新しい経済システムに徐々に以降する準備が必要であると思っています．まだ確信めいたものは無いのですが，ブロックチェーンはそれを実現する１つのシーズになるのではないでしょうか？．．．続く(笑)

#ソースコード
##スマートコントラクト
```java:kappaMarket.sol
pragma solidity ^0.4.25;

contract KappaMarket {

    address owner;        // コントラクトオーナーのアドレス
    address donation;     // ユニセフのアドレス 
    uint public numItems; // 商品数
    bool public stopped;  // trueの場合Circuit Breakerが発動し，全てのコントラクトが使用不可能になる

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

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

    // Circuit Breaker
    modifier isStopped {
        require(!stopped);
        _;
    }
    
    // Circuit Breakerを発動，停止する関数
    function toggleCircuit(bool _stopped) public onlyOwner {
        stopped = _stopped;
    }

    // コントラクトの呼び出しがアカウント情報登録済みユーザーか確認
    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;

    // 商品画像の在り処
    // 商品画像はgoogleドライブかIPFSに保存する
    struct image {
        string googleDocID; // ファイルのid
        string ipfsHash;    // ファイルのハッシュ
    }
    mapping(uint => image) public images;

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

    // 各ユーザーが出品した商品の番号を記録する配列
    mapping(address => uint[]) public sellItems;

    // 各ユーザーが購入した商品の番号を記録する配列
    mapping(address => uint[]) public buyItems;
    
    // 返金する際に参照するフラグ
    mapping(uint => bool) public refundFlags; // 返金すると，falseからtrueに変わる

    // アカウント情報を登録する関数
    function registerAccount(string _name, string _email) public isStopped {
        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 isStopped {
        accounts[msg.sender].name = _name;   // 名前
        accounts[msg.sender].email = _email; // emailアドレス
    }

    // 出品する関数
    function sell(string _name, string _description, uint _price, string _googleDocID, string _ipfsHash) public onlyUser isStopped {
        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;                     // 商品価格
        images[numItems].googleDocID = _googleDocID;        // ファイルのid
        images[numItems].ipfsHash = _ipfsHash;              // ファイルのハッシュ
        accounts[msg.sender].numSell++;                     // 出品した商品数の更新
        sellItems[msg.sender].push(numItems);               // 各ユーザーが購入した商品の番号を記録
        numItems++;
    }

    // 出品内容を変更する関数
    function modifyItem(uint _numItems, string _name, string _description, uint _price, string _googleDocID, string _IPFSHash) public onlyUser isStopped {
        require(items[_numItems].sellerAddr == msg.sender);  // コントラクトの呼び出しが出品者か確認
        require(!items[_numItems].payment);                  // 購入されていない商品か確認
        require(!items[_numItems].stopSell);                 // 出品中の商品か確認

        items[_numItems].seller = accounts[msg.sender].name; // 出品者名
        items[_numItems].name = _name;                       // 商品名
        items[_numItems].description = _description;         // 商品説明
        items[_numItems].price = _price;                     // 商品価格
        images[numItems].googleDocID = _googleDocID;         // ファイルのid
        images[numItems].ipfsHash = _IPFSHash;               // ファイルのハッシュ
    }

    // 購入する関数
    function buy(uint _numItems) public payable onlyUser isStopped {
        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;        // 売れたので出品をストップする
        accounts[msg.sender].numBuy++;           // 購入した商品数の更新
        buyItems[msg.sender].push(_numItems);    // 各ユーザーが購入した商品の番号を記録
    }

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

        items[_numItems].shipment = true;  // 発送済みにする
    }

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

        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 isStopped {
        require(items[_numItems].buyerAddr == msg.sender); // コントラクトの呼び出しが購入者か確認
        require(_numItems < numItems);                     // 存在する商品か確認
        require(_reputate >= -2 && _reputate <= 2);        // 評価は-2 ~ +2の範囲で行う
        require(!items[_numItems].sellerReputate);         // 購入者の評価が完了をしていないことを確認

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

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

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

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

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

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

        items[_numItems].stopSell = true;
    }

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

        refundFlags[_numItems] = true; // 返金済みにする
        items[_numItems].buyerAddr.transfer(items[_numItems].price); // 購入者へ返金
    }

    function sellerRefund(uint _numItems) public payable onlyUser isStopped {
        require(_numItems < numItems);                      // 存在する商品か確認
        require(msg.sender == items[_numItems].sellerAddr); // コントラクトの呼び出しが出品者か確認
        require(items[_numItems].payment);                  // 入金済み商品か確認
        require(!items[_numItems].receivement);             // 出品者が代金を受取る前か確認
        require(!refundFlags[_numItems]);                   // 既に返金していないか確認

        refundFlags[_numItems] = true; // 返金済みにする
        items[_numItems].buyerAddr.transfer(items[_numItems].price); // 購入者へ返金
    }

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

##ウェブインターフェース
JavaScriptで実装しています．ブラウザのデベロッパーツールでご確認下さい．
githubにアップロードしました．
https://github.com/YutaIzumi/KappaMarket