貸し借りを管理するEthereumスマートコントラクトをReact+Reduxで作ってみた


この記事は Ethereum Advent Calendar 2017 の8ブロック目の記事です。

昨日の記事は id:y-nakajo さんの「ContractのEventの仕組み」でした。Ethereumのeventは以前ためして上手くいかなかったので記事を参考にして再挑戦したいところです。

はじめに

Ethereumについての情報収集は夏ごろから始めていたのですが,そろそろ実用的なスマートコントラクトを作って知見を集める必要が出てきました。

題材として「カメラのレンズの貸し借りをブロックチェーンで管理したい!」という要望があったので,ReactとWeb3を使ってスマートコントラクトアプリを開発してみました。

アプリの概要

開発したアプリには上記のURLからアクセスできます。コントラクトの情報を見るにはMetaMaskが必要です。Ropstenネットワークにのみデプロイしています。常識的な範囲内ならコントラクトを自由に動かしてもらって構いません。「MetaMaskとかRopstenって何?」という方にはアドベントカレンダーの1日目の記事「Ethereum 開発者向けコミュニティを作ったよ」がおすすめです。

コントラクトのソースコードはEtherscanにデプロイ済みです。改善できるところがあったら教えてください!

貸し出し物品の追加と貸し出しリクエストの追加

Itemsのところには貸し出し可能な物品名とシリアル番号が載っています。レンタルしたい物品があったら,レンタルするための対価(ETH払い)と貸し出し期間を記入して,貸し出しのリクエストを出すことができます。リクエストを出した時点で対価となるETHが自分のアカウントからコントラクトに預けられます。 addItem が貸し出し可能な物品の追加, addRequest が貸し出しのリクエストです。

function addItem(string _name, string _serialNumber) public returns (uint) {
    Item memory newItem = Item({
      owner: msg.sender,
      name: _name,
      serialNumber: _serialNumber,
      state: ItemState.Idle
    });
    return items.push(newItem) - 1;
}

function addRequest(uint _itemId, string _start, string _end) public payable returns (uint) {
    Request memory newRequest = Request({
       client: msg.sender,
       itemId: _itemId,
       fee: msg.value,
       start: _start,
       end: _end,
       state: RequestState.Pending
    });
    return requests.push(newRequest) - 1;
}

貸し出しリクエストの承認

リクエストが出されたら,その物品の持ち主はリクエストを承認することができます。リクエストを承認したら,リクエストを出した人が払った対価(ETH)を受け取れます。Solidityのドキュメントにも書かれている Withdrawal pattern を使いました。

function acceptRequest(uint _requestId) public {
    Request storage req = requests[_requestId];
    require(req.state == RequestState.Pending);
    Item storage item = items[req.itemId];
    require(item.owner == msg.sender);
    require(item.state == ItemState.Idle);
    item.state = ItemState.Busy;
    req.state = RequestState.Accepted;
    msg.sender.transfer(req.fee);
}

物品の返却

貸出中の物品はItems欄からは見えなくなり他の人は貸し出しリクエストを追加できなくなります。物品が返却されたら持ち主はそれを承認して,再び貸し出し可能な状態に戻します。

function acceptReturning(uint _requestId) public {
    Request storage req = requests[_requestId];
    require(req.state == RequestState.Accepted);
    Item storage item = items[req.itemId];
    require(item.owner == msg.sender);
    require(item.state == ItemState.Busy);
    item.state = ItemState.Idle;
    req.state = RequestState.Finished;
}

貸し出しが終わったらリクエストの情報がHistoryに移動します。

貸し出しリクエストのキャンセル

貸し出しリクエストを出した人は,そのリクエストが持ち主に承認される前に限り,リクエストをキャンセルすることができます。リクエストをキャンセルすると,レンタルのために払った(正確には「コントラクトに一時的に預けた」)対価がコントラクトから返却されます。

function cancelRequest(uint _requestId) public {
    Request storage req = requests[_requestId];
    require(req.client == msg.sender);
    require(req.state == RequestState.Pending);
    req.state = RequestState.Canceled;
    msg.sender.transfer(req.fee);
}

アプリケーションを作ってみて気づいたこと

クライアントサイドJavaScriptの知識だけで簡単に開発できる

Ethereumのことを詳しく知る前には「P2Pの通信処理とかトランザクションの署名とかの処理も書けないと開発できないのかなぁ…」と心配していたのですが,そのあたりの難しい処理はMetaMaskやMistなどのスマートコントラクトに対応したウォレットが肩代わりしてくれます。スマートコントラクトアプリケーションを開発したい人が必要なのはコントラクトのコードをSolidityやLLLで書くことと,そのコントラクトをコールするAPIを呼び出すことだけです。

正直に白状すると,このアプリケーションを開発するにあたっては,Ethereumに関連する処理を書くのにかかった時間よりも,フロントエンドの実装のほうが時間がかかりました。

セキュアなアプリケーションを簡単に書ける

送られてきたリクエストが正規のユーザーかどうかの検証が msg.sender を見るだけでできたり,assertrequirerevert などの関数でコントラクトの状態をrollbackすることができるなど,一般的なWebアプリケーションでは慎重に実装しないといけない処理が簡単に書けることに驚きました。

とはいえ,安全なコントラクトを作るために考慮しなければならないこともいろいろあります。以下のドキュメントが参考になります。

コントラクトの実行にかかる費用が高い

仮想子猫 の影響でgas priceが値上がりしていることもあり,このコントラクトをmainnetで実行した場合,貸し出しリクエストを追加すると手数料だけで300円くらい取られます。ちょっとこれは許容できないです。コントラクトのストレージを使用するような処理は非常に高価なのでどうにかする必要があります。

コントラクトのアップグレードが難しい

今のコントラクトではアップグレードに対応していないので,ロジックを更新したい場合には今までのコントラクトを破棄して新しいコントラクトをデプロイし直すしかありません。アップグレードに関してはいくつかノウハウも見つかるのですが,現状あまりスマートとは思えません…

async/await を使いたい

今回はコントラクトをコールするAPIの呼び出しにWeb3を使っているのですが,Web3はPromise形式に対応しておらず,普通に書くとコールバック地獄になります(「Promise コールバック地獄」などで検索してみよう!)。

コールバック地獄は遠慮したいので,Promiseでラップしています。以下のような感じです。

ethereum.js
export function getBlockNumber(): Promise<number> {
  return new Promise((resolve, reject) => {
    window.web3.eth.getBlockNumber((err, blockNumber) => {
      if (err) {
        reject(err);
        return;
      }
      resolve(blockNumber);
    });
  });
}

このようにPromiseでラップするコードを,使用しているAPI全てについて書いているのですが,さすがに面倒なのでどうにかしたいところです。Hi-EtherのSlackで知ったのですが,Web3の代替として ethers.js や truffle-contract といったものがあり,これらはPromiseに対応しているようです。いずれこれらのライブラリを使って書き直したいところです。

追記

amachinoさんから情報提供いただきました。Web3の1.0からはPromiseで扱えるようです。ただし,まだbetaなのでAPIに破壊的変更が加えられる可能性もあり注意が必要です。

トランザクション承認までの時間と UI/UX について

トランザクションは一瞬で承認されるわけではないので,物品の追加やリクエストの承認をしてから数十秒の待ち時間があります。送信したトランザクションがブロックチェーンに反映されるまで何も画面の表示が変わらないような UI だと「さっき間違って Submit じゃなくて Reject 押しちゃったかな?」という気分になってしまったり不安になるので,何らかのアクションをユーザーに示したいところです。

通常のWebアプリケーションやスマートフォンアプリでは,待ち時間はせいぜい数秒なので circular progress indicator や linear progress indicator を表示してお茶を濁すこともできますが,さすがに待ち時間が数十秒になってくるともう少しUIに工夫が必要だと思います。

Mistでトランザクションを送信したときの画面のように,トランザクション承認後の状態を半透明で表示するようなUIがよいかもしれません。

おわりに

習作として作ってみたスマートコントラクトアプリの紹介をしました。明日の記事はkentaroさんの「Slack上でERC20トークンを送りあってコミュニケーションする」です。昨日からHi-EtherのSlackが賑やかになっているので楽しみですね!