【Solidity】event の半分は優しさで出来ている話

## はじめに

イーサリアムのスマートコントラクト開発で利用される **Solidity** において、イベントの仕様はとてもシンプルです。

> イベントは **event** をつけて宣言します.
> **event TransferToken( address indexed from, address indexed to, uint id );**

> イベントは **emit** をつけて発行します.
**emit TransferToken( msg.sender, reciever, tokenId );**

> 発行されたイベントはトランザクションのレシートに記録されます.
> **indexed** のついた引数はフィルタリングに利用でき、特定のイベントを捕捉できます.

> めでたし、めでたし.

ネット上の情報や、書籍等での説明はだいたこんな感じです。わかったような気にはなりますが、「発行後のイベントを どうやって処理したらいいの？」という風にモヤモヤします。

この記事では、**DApps** 開発では避けて通れないのに、**Solidity** の解説においては多くを語られない、そんな **event** についてお話ししてみたいと思います。

## Solidity はバックエンド向け、 event はフロントエンド向け

バックエンドという言葉があります。
エンジニア視点で見ると「**裏方 ＝ ユーザーの目につかないところで データの管理や操作を行うモノ**」というイメージです。

**DApps** 開発におけるバックエンドといえば、イーサリアムブロックチェーンとなります。そして、ブロックチェーン上において、ユーザーの資産やデータを管理するスマートコントラクトも、バックエンドといえるでしょう。

一方、フロントエンドという言葉もあります。
「**窓口 ＝ ユーザーと直接やりとりして 裏方とのあいだを取り持つモノ**」というイメージです。「顧客」を意味する「クライアント」という言葉が、同じ意味で用いられることもあります。

**Solidity** の **event** は バックエンドであるスマートコントラクトから、フロントエンドであるクライアントへ向けたメッセージの役割を担います。**event** を具体的に説明するには、イベントを受け取るクライアント側の前提がないと始まりません。

では、**DApps** 開発におけるクライアントとはなんでしょう？

**DApps** が **Web** 向けのサービスを想定しているのであれば、**Chrome** 等のウェブブラウザとなりそうです。**PC** 向けのサービスであれば、**Windows** や **Mac** プログラムになりそうですし、スマホ向けであれば、**iOS** や **Android** アプリとなるでしょう。サービスの提供先に応じてクライアントの選定が変わってきますし、当然、開発環境も大きく変わります。

**Solidity** の解説において **event** の発行後について多くを言及されないのは、バックエンドとフロントエンドというスタンスの違いがあるからだと思われます。それに、イベント発行後の説明をしようとなると、クライアント側での実装の話が必要となってきて、収拾がつかなくなりそうです。

「そこまで紙面を割くことはできない」というのが解説者さんの本音なのだと思います。

## イベントはクライアント本位で考える
では、発行後のイベントについて考えてみましょう。

スマートコントラクトはユーザーの現在の残高や、トークン（資産）の現在の所有者といった、「現在の状況」を扱います。一方で、「昨日受け取った **ETH** の合計」や「誰が誰にどんなトークンを送った」というような「状況の変化」を追いかけるのは得意ではありません。

**DApps** を開発する上で、取引履歴や状況変化の通知は、ユーザーの満足度を上げるためにも手厚く実装したいところです。そして、この状況の変化をクライアント側で検出するための仕組みが、**Solidity** の **event** となります。

例えば、トークンの送信に対応した **DApp** を開発するとしましょう。

この **DApp** はユーザー間でトークンを送信しあえるのですが、コスト削減を理由に、下記の仕様でＧＯサインがでてしまったと仮定します。

> ・送信者がトークンを送ったら、そのまま黙ってホーム画面へ戻ってよい
> （※トランザクションの結果はイーサスキャンで見てくれ！すまん！）
> ・トークン情報はトークン画面へ遷移した時に読み込めばよい
> （※トークンの情報を知りたければトークン画面を開いてくれ！お願い！）

さて、運悪くこの **DApp** を利用してトークンを送ったＡさんは、次のように思うはずです。
「送信完了のポップすらでないけど、本当に送れてるの…？」

同じく、この **DApp** を利用してトークンを受け取った（ことに気づいていない）Ｂさんは、次のように思うはずです。
「Ａさんからのトークン、なかなか来ないな…」（※ホーム画面を眺めながら）

トークンの送信自体が無事に完了していたとしても、その過程の説明が不足していると、ユーザーを不安にさせてしまいます。この場合、Ａさんに対する「送信が完了しました」通知、Ｂさんに対する「トークンが届きました」通知ぐらいは実装しておきたいところです。

では、この **DApp** に通知機能を加えるとしたらどうしたらよいでしょう？
イベントによる通知の実装の流れは、おおよそ下記のようになると思います。

> 1.コントラクトにてトークンの送信完了イベントを定義する（※例えば冒頭に挙げた **TransferToken** イベント）
> 2.コントラクトにて、トークンの送信が正常終了したら「送信完了イベント」を発行する（※ **TransferToken** の **emit**）
> 3.ブロックチェーン上にて、トランザクションのレシートに「送信完了イベント」が記録される
> 4.クライアントにて、定期的にブロックをチェックし、ユーザーに関わる「送信完了イベント」がないかを監視する
> 5.クライアントにて、ユーザーに関わる「送信完了」イベントを見つけたら、適切な表示を行う
> （※ユーザーが送信者であれば「送信が完了しました」通知、ユーザーが受信者であれば「トークンが届きました」通知を出す）

これならば、トークンの送信完了後にクライアント上で通知が行われるため、Ａさんの不安は解消され、Ｂさんはホーム画面でトークンの取得に気づいたはずです。

処理の結果が同じだったとしても、その過程を丁寧に通知する DApp と、そうでない DApp では、お客さんに与える安心感に大きな差が出ることでしょう。

「どのような通知があれば利用者が 安心＆満足してくれるか？」

クライアント側の実装、とくにイベントの設計において、お客さんへの優しさは欠かせません。


## イベント実装の具体例
サンプル **DApp** により、イベントの流れを具体的に見てみましょう。このサンプルのサービス内容はミニゲーム、クライアントは **iOS** アプリを想定します。

### サービス内容
サービス内容は「**みんなで カモったり カモられたりする ゲーム**」です。

ゲーム内では、参加するプレイヤーのうち１人がカモにされ、周りのプレイヤーから所持ゴールドを盗まれます。逆に、カモられているプレイヤーは、最後にカモったプレイヤーを容疑者として通報することで示談金を得られます。抜け目なくカモり、仮にカモられても示談金でやり返し、所持ゴールドを高めるのがゲームの目的です。

<details><summary>具体的なルール(タップで開閉します)</summary><div>

```
・プレイヤーの中から「カモ」と「容疑者」が、それぞれ１人づつ認定される
・ゲームに参加するにはETHを送金してゴールドを購入する必要がある
・ゴールドを購入したプレイヤーは、新たなカモに認定される
・カモ以外のプレイヤーはカモの所持ゴールドの「５％」をカモれる（盗める）
・一番最後にカモった（盗みを働いた）プレイヤーは、新たな容疑者に認定される
・カモは容疑者を通報することで、相手の所持ゴールドの「３０％」を示談金として分捕れる
・通報されたプレイヤーはペナルティとして、新たなカモに認定される
・通報したプレイヤーは逆恨みされ、新たな容疑者に認定される
```
</div></details>

### ゲームに必要なデータ
ゲームを構成するためにスマートコントラクト側で管理しないといけない情報としては下記となります。

```
// 管理データ
mapping( address => bool ) internal valids;   // プレイヤーの有効性
mapping( address => uint ) internal golds;    // プレイヤーの所持ゴールド
address internal targetPlayer;                // 現在のカモ
address internal suspectPlayer;               // 現在の容疑者
```

これらの情報がブロックチェーン上で保持され、ゲームの流れによって更新されていきます。クライアント側ではこれらの情報をスマートコントラクトから読み込んで、画面へ表示します。

ですが、上記のデータだけでは「誰が誰をカモった」等の状況説明ができません。そのための情報はイベントにより通知することになります。

### イベント設計と定義
さて、このゲームのフローにおいてどのようなイベントが必要になるでしょうか？
別の言い方をすると、どのような通知があるとプレイヤーが楽しんでくれそうでしょうか？

まず、自分がカモった時にはいくら盗めたか通知して欲しいですよね。それに、カモられた時は被害額を知りたいです。また、他人同士のカモりカモられも見られた方が、ワイワイやっている感じがして楽しそうです。

この通知に使うイベントを **Steal** として定義しましょう。

```
event Steal( address indexed player, address indexed target, uint ammount );
```

同様に、通報された時の情報も欲しいので、**Report** イベントも用意しましょう。

```
event Report( address indexed player, address indexed suspect, uint ammount );
```

あとは、ゲームに参加（**ETH** を送ってゴールドを取得）した際に、いくら入手したかのお知らせを出さないと不親切そうです。
このイベントを **BuyGold** としましょう。

```
event BuyGold( address indexed player, uint ammount );
```

加えて、ゴールドを入手したユーザーはカモにされるので、良くも悪くも目立たせてドキドキしてもらいましょう。
このイベントを **Target** としましょう。

```
event Target( address player, uint gold );
```

以上、４つのイベントがあれば、クライアント側でゲームを盛り上げることができそうです。

通知の具体的なイメージとしては、ゲームに参加するとまず、「**BuyGold:ゴールドを入手しました**」と表示され、続いて「**Target:気をつけろ！カモられるぞ！**」と表示します。その後は、自分含めた全てのプレイヤーの「**Steal:カモった／カモられた**」と「**Report:通報した／通報された**」が次々と表示されていくことになります。

メモ：
このサンプルでは、イベントをプレイヤー別にフィルタリングできるよう、**address** に **indexed** をつけて宣言しています。が、**Target** イベントのアドレスには **indexed** がついていません。これは、**Target** イベントは全プレイヤーに対して必ず通知されることを想定しており、フィルタリングは不要と考えたからです（ようするにどうでもよいこだわり分です）。

### Solidityでの実装

ゲームの肝である、「カモる」、「通報する」、「入金する」ための処理を、「**steal** 関数」、「**report** 関数」、「**ETH** の入金を受け取るフォールバック関数」として用意しましょう。

<details><summary>steal 関数(タップで開閉します)</summary><div>

```
//--------------------------------
// 盗む（※カモをカモる）
//--------------------------------
function steal() external{
  // 未参加、自身がカモなら失敗
  require( valids[msg.sender], "you need to join the game" );
  require( msg.sender != targetPlayer, "you can not steal your own gold" );

  // 最後に盗みを働いたプレイヤーが容疑者となる
  suspectPlayer = msg.sender;

  // カモの所持ゴールドを５％盗む（※端数入り揚げ）
  uint ammount = (golds[targetPlayer]+19) / 20;
  golds[suspectPlayer] += ammount;
  golds[targetPlayer] -= ammount;
  emit Steal( suspectPlayer, targetPlayer, ammount );
}
```
</div></details>

<details><summary>report 関数(タップで開閉します)</summary><div>

```
//--------------------------------
// 通報する（※カモがカモる）
//--------------------------------
function report() external{
  // 未参加、自身がカモでなければ失敗
  require( valids[msg.sender], "you need to join the game" );
  require( msg.sender == targetPlayer, "you can not report your own crime" );

  // 容疑者がカモになり、逆恨みされた通報者が容疑者となる
  targetPlayer = suspectPlayer;
  suspectPlayer = msg.sender;

  // 示談金としてカモ（旧容疑者）が所持ゴールドの３０％を支払う（※端数入り揚げ）
  uint ammount = (3*golds[targetPlayer]+9) / 10; 
  golds[suspectPlayer] += ammount;
  golds[targetPlayer] -= ammount;
  emit Report( suspectPlayer, targetPlayer, ammount );
}
```
</div></details>

<details><summary>入金(フォールバック)関数(タップで開閉します)</summary><div>

```
//-------------------------------------------
// ETHを送ったら参加 ＆ ゴールドを得る ＆ 狙われる
//-------------------------------------------
function () external payable {
  // 参加費は 1 gwei 以上
  if( !valids[msg.sender] ){
      require( msg.value >= 1000000000, "please send 1 gwei or more, to join the game" );
    valids[msg.sender] = true;
  }

  // ゴールドの入手
  golds[msg.sender] += msg.value;
  emit BuyGold( msg.sender, msg.value );

  // 早速狙われる
  targetPlayer = msg.sender;
  emit Target( targetPlayer, golds[msg.sender] );
}
```
</div></details>

これらの関数が正常に終了すると、それぞれに対応するイベントが **emit** にて発行され、クライアント側で検出できるようになります。

また、クライアントが管理データを参照できるように、**status** 関数も用意しましょう。クライアントがイベントを捕捉した際、ゲームの状況を画面へ反映させるために、この関数が呼ばれる想定です。

<details><summary>status 関数(タップで開閉します)</summary><div>

```
//--------------------------------
// ゲームの状況
//--------------------------------
function status() external view returns( uint retGold, address retTarget, uint retTargetGold, address retSupsect, uint retSuspectGold ){
  // 未参加なら失敗
  require( valids[msg.sender], "you need to join the game" );

  retGold = golds[msg.sender];            // プレイヤーの所持ゴールド
  retTarget = targetPlayer;               // カモのアドレス
  retTargetGold = golds[targetPlayer];    // カモの所持金
  retSupsect = suspectPlayer;             // 容疑者のアドレス
  retSuspectGold = golds[suspectPlayer];  // 容疑者の所持金
}
```
</div></details>

**status** 関数の返値は、クライアントの表示の都合によるものです。クライアント側が要求するのは、プレイーヤーの所持ゴールド、現在のカモと容疑者のプレイヤー、それぞれの所持ゴールドとなります。画面仕様等がかわれば、返値の内容はその都度修正されることになります。

これにて、**Solidity** 側の実装は終了です。
最終的なコードは[こちら](https://github.com/hakumai-iida/SolidityEventDemo/blob/master/sol/DemoEvent.sol)となります。


### 発行されたイベントの登録先

クライアントからスマートコントラクトの関数が呼ばれるとイベントが発行され、トラザクションのレシートに登録されます。この情報はイーサリアム上に保持され、クライアント側から参照することが可能となります。

保存内容は、イベント定義（イベント名と引数の型の羅列）のハッシュ値と、各引数の値を羅列したものとなります。**indexed** をつけた引数は **Topics** 枠に登録され、ついていない引数は **Data** 枠に登録されます。この時、イベント定義のハッシュ値が **Topics[0]** に登録され、イベント名によるフィルタリングに対応されます。また、**event** 定義で **indexed** で指定された引数は、**Topics[0]** の後ろから定義順に並ぶことになります。

例えば、**steal** イベントを[イーサスキャン](https://rinkeby.etherscan.io/tx/0x5d70cefd93b819119d44608ccb14390c49d2572c68fb14ba6d987ed39cda5f9f#eventlog)で見ると下記のような内容となります。

```
Topics
 [0] 0xc4521c77cd8558a7102424919c444912561b76d8973dfc5482ea96932e5a9a47
 [1] 0x00000000000000000000000093231c1b82547b20e85b70b87f1c98dcbd35d88a
 [2] 0x000000000000000000000000431e16c88dc8a449b93fb67d2bb08f1f6695348c

Data
 Hex 00000000000000000000000000000000000000000000000000000000170bb4c9
```

**indexed** で指定した **player** と **target** の引数は **Topics[1]** と **Topics[2]** として登録されています。一方で、**indexed** 指定のない **ammount** の引数は **Data** に登録されています。

さて、**indexed** をつけることで、この **Topics** 配列に追加するメリットはというと、指定の引数が添字で参照可能になるため、クライアント側でイベントのフィルタリングに利用できるという点です。

### クライアント側の実装
このサンプルでのクライアントは **iOS** での実装となります。
先にお話した通り、どのプラットフォームをターゲットにするかにより開発環境は大きく変わります。ここでは、**iOS** 特有の実装には深入りせず、クライアントがどのようにイベントの処理をするかの流れにしぼって説明します。

まず、イベント処理の基本は、新しく発行されるイベントがないかの監視なので、監視ループを開始します（※コードの詳細はわからなくても雰囲気をつかんでくだされば十分です）。

<details><summary>イベント監視ループの開始(タップで開閉します)</summary><div>

```swift
// イベントループの開始
let web = helper.getCurWeb3()!
let functionToCall: web3.Eventloop.EventLoopCall = onNewBlock
let monitoredProperty = web3.Eventloop.MonitoredProperty.init(
    name: "onNewBlock",
    queue: web.requestDispatcher.queue,
    calledFunction: functionToCall
)
web.eventLoop.monitoredProperties.append( monitoredProperty )
web.eventLoop.start( 1.0 )
```
</div></details>

イベントの監視ループでは一定間隔おきに新規ブロックが監視され、新しいブロックが検出されたら、そのブロック中のイベントををフィルタリングします。

<details><summary>イベントのフィルタリング(タップで開閉します)</summary><div>

```swift
var filter = EventFilter()
filter.fromBlock = .blockNumber(UInt64(blockNumber))
filter.toBlock = .blockNumber(UInt64(blockNumber))
        
// フィルター：BuyGold
if eventName == "BuyGold" {
    // 自身のアドレスでフィルタ
    filter.parameterFilters = [
        ([self.helper.getCurAddress()!] as [EventFilterable])
    ]
}

do{
    guard let result = try contract?.getIndexedEvents( eventName:eventName, filter:filter ) else {
                return
            }
```
</div></details>

サンプルコードでは、**BuyGold** はプレイヤーのアドレスでフィルタリングしています。これにより、プレイヤーが発行した **BuyGold** イベントだけが抽出され、プレイヤー自身のゴールド購入通知のみが表示されることになります。

一方で、**Steal** と **Report** イベントはイベント名のみでフィルタリングしています。結果として、全ての「カモられた」、「通報された」イベントが検出されることになりますが、検出後、イベントの引数のプレイヤーが自身であるかどうかにより、表示の割り振りを行なっています。

同様に、**Target** イベントもイベント名のみでフィルタリングを行い、検出後、自分がターゲットにされたのか否かで表示を割り振っています。

<details><summary>検出したイベントの通知(タップで開閉します)</summary><div>

```swift
if result.count > 0 {
    for event in result{
        switch event.eventName {
        case "BuyGold":
            addLog( "購入完了　\(event.decodedResult["1"]!) GOLDを手に入れた！" )
            break;

        case "Target":
            let addressTarget = (event.decodedResult["0"] as? EthereumAddress)!.address
            if( addressTarget != self.helper.getCurEthereumAddress() ){
                addLog( "カモ(\(addressTarget.prefix(10))...)発見！ \(event.decodedResult["1"]!) GOLD所持している！" )
            }else{
                addLog( "カモ認定された！ 気をつけろ！" )
            }
            break;
                        
        case "Steal":
            let addressPlayer = (event.decodedResult["0"] as? EthereumAddress)!.address
            let addressTarget = (event.decodedResult["1"] as? EthereumAddress)!.address
                        
            if( addressPlayer == self.helper.getCurEthereumAddress()! ){
                addLog( "カモ(\(addressTarget.prefix(10))...)から \(event.decodedResult["2"]!) GOLD 盗んだ！" )
            }else if( addressTarget == self.helper.getCurEthereumAddress()! ){
                addLog( "容疑者(\(addressPlayer.prefix(10))...)に \(event.decodedResult["2"]!) GOLD 盗まれた..." )
            }else{
                addLog( "カモ(\(addressTarget.prefix(10))...)が容疑者(\(addressPlayer.prefix(10))...)にカモられてるw" )
            }
            break;
                        
            case "Report":
                let addressPlayer = (event.decodedResult["0"] as? EthereumAddress)!.address
                let addressTarget = (event.decodedResult["1"] as? EthereumAddress)!.address
                        
                if( addressPlayer == self.helper.getCurEthereumAddress()! ){
                    addLog( "容疑者(\(addressTarget.prefix(10))...)から \(event.decodedResult["2"]!) GOLD の示談金を奪った！" )
                }else if( addressTarget == self.helper.getCurEthereumAddress()! ){
                    addLog( "カモ(\(addressPlayer.prefix(10))...)に \(event.decodedResult["2"]!) GOLD の示談金を奪われた..." )
                }else{
                    addLog( "カモ(\(addressPlayer.prefix(10))...)が容疑者(\(addressTarget.prefix(10))...)を通報してるw" )
                }
                break;
                        
        default:
            addLog( "エラー：知らないイベント　\(event.eventName)" )
            break;
        }
    }
}
```
</div></details>

上記の処理において、**Topics** のフィルタリングによりイベントを抽出し、**Data** 連想配列から詳細情報が取り出されて処理する流れを、イメージしていただけたことと思います。

これにて、クライアント側での実装も終了です。
最終的なサンプルプロジェクトは[こちら](https://github.com/hakumai-iida/SolidityEventDemo)となります。
*（※iOS アプリ上でのイーサリアムへの接続に関しては[こちらの記事](https://qiita.com/hakumai-iida/items/7167aeae5f9f96c92507)を参照ください）*

### ゲームの動作確認
複数台で並べてゲームを遊んでみた様子です。

プレイヤーＡの様子：新規参加でカモられている
![iPad6.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/596438/ab5de775-f715-0146-0219-f1098ec4e42e.png)

プレイヤーＢの様子：プレイヤーＡをカモっている
![iPhone7p.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/596438/14b3d47a-e958-816f-13a6-11e35f9651e0.png)

プレイヤーＣの様子：両者の様子を眺めつつ、姑息にカモっている
![iPhone6.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/596438/ead95d08-0e0a-6648-92f7-60f864d4322a.png)

画面下にでている時間付きのログが、イベントによって通知された情報です。各クライアントによる「カモる／通報する」行為の結果がイベントを発生させ、クライアント毎にフィルタリングされて新しい順に表示されているのが見て取れるかと思います。

このサンプル **DApp** の通知方法はシンプルなログの垂れ流しですが、イベント処理としては十分な機能が備わっています。あとは、絵的／音的な演出を作り込んでいくことで、ゲームとしての面白みを向上していけると思われます。

## まとめ
**Solidity** の仕様としては ほんの小さな **event** の存在ですが、クライアント側で果たす役割の大きさを具体的にイメージしていただけたのであれば幸いです。

**DApps** 開発において、クライアントの使い勝手は、ユーザーの満足度に大きく影響してきます。ユーザー本位のクライアントフローを検討し、優しさあふれるイベント設計を心がけたいものです。