FlutterでTodo管理dAppを作る

# やること
[web3dart](https://pub.dev/packages/web3dart) / [Truffle](https://trufflesuite.com/) / [Ganache](https://trufflesuite.com/ganache/) を使って簡易的なtodo flutterアプリを作成します。
スマートコントラクトのイベントをsubscribeする実装サンプルがあんまりなかったためそこも含めてやってみました。
Web3初心者のため、間違っていることを言っている可能性がありますのでご容赦ください。
また動作確認はAndroidのみで行っています。

# 作るもの
![dapp.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/95489/071d19de-800d-ff46-d53a-0e6cb03d3dce.gif)


# 概要
全体像を把握できてなかったので整理しました。
![arch.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/95489/b1deb784-bac4-6557-a557-1147b7b86c8c.png)

より詳細は[NTT Dataのコラム](https://www.intellilink.co.jp/column/fintech/2016/061700.aspx)に乗っている図がわかりやすいです。


## [Truffle](https://trufflesuite.com/)
- `Solidity`で書いたスマートコントラクトをコンパイルしてローカルのチェーンネットワークにデプロイするためのツールです。コンパイルされたときに生成されたjsonファイルをFlutterプロジェクトに取り込んで`web3dart`からチェーンネットワークに接続できるようにします。

## [web3dart](https://pub.dev/packages/web3dart)
- イーサリアムネットワークとのやりとりをwrapしてくれるライブラリです。

## JSON RPC
- イーサリアムネットワークと`https/http`でAPIのやり取りをする際に`RPC(Remote Procedure Call)`を実現するプロトコルの一つです。別のマシンにあるメソッドを直接叩くようなインタフェースが特徴です。他に比較されるAPI設計としてはREST APIなどがありますがこちらはプロコトルではなくあくまでも設計思想です。

## WebSocket
- 双方向通信プロトコルです。スマートコントラクトのイベントが実行された際に結果を受け取るために利用します。

## [Ganache](https://trufflesuite.com/ganache/)
- イーサリアムネットワークをローカル環境で構築するツールです。他には[geth](https://geth.ethereum.org/)や[Hardhat](https://hardhat.org/)などがあるようです。

## スマートコントラクト
- ネット上で検索するとスマートコントラクトとは、「特定の条件が満たされた場合に、決められた処理が自動的に実行される契約履行の管理自動化」とでてきますが、Web2.0におけるAPIサーバーみたいな役割と捉えるとわかりやすいと思います。

## EVM
- スマートコントラクトの展開と実行を管理するコンポーネントです。`Solidity`のような高級言語で書かれたコードをバイトコードに変換したりステートをもっておりそれを更新したりする役割を担います。ただこのEVMは実行速度が遅いとかそもそも`Solidity`のようなイーサリアム専用の独自言語じゃないと動かないというようなデメリットがあり、`Etherium2.0`では`eWASM`という`Web Assembly`ベースの新しいVMに置き換わるそうです。

## Blockchain
- ブロックチェーンはWeb2.0におけるデータベースという認識です。

# ローカル環境の構築
- スマートコントラクトはWeb2.0におけるAPIサーバーだと考えると理解しやすいのですが、まずはスマートコントラクトが動く環境を構築し、そこにコントラクトをデプロイすることでアプリからの呼び出しが可能になります。
今回はこのローカル環境の構築に[Ganache](https://trufflesuite.com/ganache/)というツールを使います。インストール後に起動しておきます。
- コントラクトの`Ganache`へのデプロイには[Truffle](https://trufflesuite.com/)というツールを使います。`truffle init`でプロジェクトを新規作成しておきます。
- iOS/Androidの実機から接続する場合は、Ganacheのホストをlocalhostではなく自身のPCのローカルIPにして下さい。ターミナルで`ifconfig`すれば確認できると思います。Ganache上からは「設定(歯車マーク)」 -> 「Server」から変更ができます
![ganache.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/95489/6e6d0905-9af8-16ad-73ed-d8f94e9ef339.png)



# スマートコントラクトの作成
- `truffle init`で生成された`contracts/`配下に以下のコードを作成します。

```solidity:todo_contract.sol
// TodoContract.sol
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.9.0;

contract TodoContract {
  uint256 public totalTasksCount = 0;

  struct Task {
    uint256 id;
    string name;
    bool isComplete;
  }

  mapping(uint256 => Task) public todos;

  event TaskCreated(uint256 id, string name);
  event TaskUpdated(uint256 id, string name);
  event TaskIsCompleteToggled(uint256 id, string name, bool isComplete);
  event TaskDeleted(uint256 id);

  function createTask(string memory _name) public {
    uint256 id = totalTasksCount;
    todos[id] = Task(totalTasksCount, _name, false);
    totalTasksCount++;
    emit TaskCreated(id, _name);
  }

  function updateTask(uint256 _id, string memory _name) public {
    Task memory currentTask = todos[_id];
    todos[_id] = Task(_id, _name, currentTask.isComplete);
    emit TaskUpdated(_id, _name);
  }

  function toggleTaskIsComplete(uint256 _id) public {
    Task memory currentTask = todos[_id];
    todos[_id] = Task(_id, currentTask.name, !currentTask.isComplete);
    emit TaskIsCompleteToggled(
      _id,
      currentTask.name,
      !currentTask.isComplete
    );
  }

  function deleteTask(uint256 _id) public {
    delete todos[_id];
    emit TaskDeleted(_id);
  }
}
```

# スマートコントラクトのdeploy
- コンパイルして`Ganache`にアップロードします。
```bash
truffle compile
truffle migrate
```

# Flutterのセットアップ
- 前項で`TodoContract.json` というファイルが生成されるのでこれを `TodoContract.abi.json`にリネームしてFlutterの `lib/` の任意の場所に置きます
- ビルドランナーを走らせてコードを自動生成します
```bash
flutter pub run build_runner build --delete-conflicting-outputs
```
- `TodoContract.g.dart` というファイルがジェネレートされるので基本的にアプリからはこのクラスのメソッドを呼び出して使う形になります
- 詳細は[公式](https://pub.dev/packages/web3dart#smart-contracts)を参照してください。


# Contractインスタンスの取得
- 生成されたファイルに`TodoContract` というクラスがあるのでこのインスタンスを取得します
```dart
final abiStringFile = await rootBundle.loadString('lib/TodoContract.abi.json');
final jsonAbi = jsonDecode(abiStringFile);
final contractAddress = EthereumAddress.fromHex(jsonAbi["networks"]["5777"]["address"]);
_todoContract = TodoContract(address: contractAddress, client: _client);
```

# 各メソッドの実行
- 以下のような形でメソッド呼びだしをすることが可能で、これによってJSON RPC経由でトランザクションが送信されます
- 秘密鍵はGanacheの「Accounts」で表示された行の横にある鍵マークをタップすると取得できます(ローカル環境のものなので公開してます)
<img width="600" alt="ファイル名" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/95489/9b633a5c-6b69-dbbe-3f4f-633d6ce54a51.png">

```dart
final _credentials = EthPrivateKey.fromHex(dotenv.env['PRIVATE_KEY']!);
_todoContract.createTask(name, credentials: _credentials));
_todoContract.updateTask(todo.id, todo.name, credentials: _credentials));
_todoContract.toggleTaskIsComplete(id, credentials: _credentials));
_todoContract.deleteTask(id, credentials: _credentials));
```

# イベントの結果通知を受け取る
- スマートコントラクト上で定義したイベントをアプリ側で受け取れるようにします。イベントストリームも先ほど生成された Contractクラスの中に含まれているのでそれを使います。
- イベントストリームの取得に`toBlock` / `fromBlock`という引数がありますが、これは指定したブロック間に発生したEventを受け取るようにするためのものです。なにも指定しないと過去に発生したイベントが全て通知されるので、今回のように今後発生するEventのみを購読したい場合は `toBlock`に `BlockNum.genesis()`を指定します。
API docs: [eth_getLogs](https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_getlogs)
- listenすると`Subscription` クラスが返却されるので、画面遷移等でEventを受け取る必要なくなった際に`dispose`するのを忘れないようにしてください。
```dart
final subscription = _todoContract
  .taskCreatedEvents(toBlock: const BlockNum.genesis())
  .listen((event) {
    // handle event
   });
```

# すべてのTodoを取得する
- トータルのTodoの数を取得したのちに、ループを回して指定したindexのTodoを取得するというコードになっています。
  - 一回で全件とれないのかと思ったのですが、どうやらSolidityのmappingはkey-valueのペアをループする仕組みがないのと、`private` `internal`のみでしか返り値として指定できないそうです。不便。。 もしかしたら配列で返すようなgetterをContract側に生やした方がいいかもしれません。
- 最後に`where`で空文字を弾いているのですが、これはdeleteしたデータも取得できてしまい、そのデータのnameが空になっているからです。なぜなのかまだ追えてません。
```dart
  void _getAllTodos() async {
    final count = await _todoContract.totalTasksCount();
    final allTodos = [for (var i = 0; i < count.toInt(); i++) await _getTodo(i)]
        .where((element) => element.name.isNotEmpty)
        .toList();
    );
  }

  Future<Todo> _getTodo(int index) async {
    final masterTodo = await _todoContract.todos(BigInt.from(index));
    return Todo(
        id: masterTodo.id,
        name: masterTodo.name,
        completed: masterTodo.isComplete);
  }
```

# 感想
## 非構造化されたバイトデータしか扱えないのが辛い
例えばですが、本当だったら`updateTodo()`という関数の引数にはユーザーが定義した`Todo`オブジェクトを渡したいですが、それができません。
`updateTodo(string name, int id)`みたいにする必要があります。
返り値やユーザー定義のオブジェクトのネストみたいな場合も同様です。
これはちゃんとデータモデリングをしていたり規模が大きいアプリになると辛いのかなと思います。
あとは上にある通りmappingを一回で全件取得できないとかも辛いなと思いました。

## グローバルステートの排他制御
クライアント側でstateの排他制御は必要ないのか？という疑問が思い浮かんで調べてみました。
結論としてはEVMがトランザクションを１個流しで処理している（厳密には並行処理）ので大丈夫なようですが、このため処理速度が遅いというデメリットもあるようようです。（理解が間違っていたらすいません）
[Concurrency and Parallelism in Smart Contracts, Part 1](https://medium.com/coinmonks/concurrency-and-parallelism-in-smart-contracts-part-1-10e8f6ecfa12)
一方で[Aptos](https://aptoslabs.com/)や[Sui](https://sui.io/)で採用されている`Move言語`はユーザー定義の型が扱えるかつステートアクセスへのアトミック性が言語レベルで担保されていて、そのため並列処理が可能になっていて速いとのことです。とても気になったので時間があるときに触ってみたいです。
[Why Move? Move vs Solidity](https://docs.sui.io/learn/why-move#move-vs-solidity)

## 最後に
意味不明だったスマートコントラクトやEVM等の概念とdappとの関係性理解がすごく深まりました。
アプリからの利用という側面を考えたときにまだまだ発展途上という印象ですが、これからどのように進化していくのかとても楽しみです！
次回はこのコードをベースに WalletConnect x Metamaskでウォレット連携をやってみようと思います。

https://github.com/aya2453/flutter-todo-dapp