❤️

FlutterでTodo管理dAppを作る

2022/10/25に公開約7,200字

やること

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

作るもの

altテキスト

概要

全体像を把握できてなかったので整理しました。
より詳細はNTT Dataのコラムに乗っている図がわかりやすいです。

altテキスト

Truffle

  • Solidityで書いたスマートコントラクトをコンパイルしてローカルのチェーンネットワークにデプロイするためのツールです。コンパイルされたときに生成されたjsonファイルをFlutterプロジェクトに取り込んでweb3dartからチェーンネットワークに接続できるようにします。

web3dart

  • イーサリアムネットワークとのやりとりをwrapしてくれるライブラリです。

JSON RPC

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

WebSocket

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

Ganache

  • イーサリアムネットワークをローカル環境で構築するツールです。他にはgethHardhatなどがあるようです。

スマートコントラクト

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

EVM

  • Ethereum Virtual Machineの略で「イーサリアム仮想マシン」です。Solidityのような高級言語で書かれたコードをバイトコードに変換したりステートの保持や更新をする役割を担います。ただこのEVMは実行速度が遅い、Solidityのようなイーサリアム専用の独自言語じゃないと動かないなどのデメリットがあり、Ethereum2.0ではeWASMというWeb Assemblyベースの新しいVMに置き換わるそうです。

Blockchain

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

ローカル環境の構築

  • スマートコントラクトはWeb2.0におけるAPIサーバーだと考えると理解しやすいのですが、まずはスマートコントラクトが動く環境を構築し、そこにコントラクトをデプロイすることでアプリからの呼び出しが可能になります。
    今回はこのローカル環境の構築にGanacheというツールを使います。インストール後に起動しておきます。
  • コントラクトのGanacheへのデプロイにはTruffleというツールを使います。truffle initでプロジェクトを新規作成しておきます。
  • iOS/Androidの実機から接続する場合は、Ganacheのホストをlocalhostではなく自身のPCのローカルIPにして下さい。ターミナルでifconfigすれば確認できると思います。Ganache上からは「設定(歯車マーク)」 -> 「Server」から変更ができます
    altテキスト

スマートコントラクトの作成

  • truffle initで生成されたcontracts/配下に以下のコードを作成します。
// 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にアップロードします。
truffle compile
truffle migrate

Flutterのセットアップ

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

Contractインスタンスの取得

  • 生成されたファイルにTodoContract というクラスがあるのでこのインスタンスを取得します。
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」で表示された行の横にある鍵マークをタップすると取得できます。(ローカル環境のものなので公開してます)
    altテキスト
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のみを購読したい場合は toBlockBlockNum.genesis()を指定します。
    API docs: eth_getLogs
  • listenするとSubscription クラスが返却されるので、画面遷移等でEventを受け取る必要なくなった際にdisposeするのを忘れないようにしてください。
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が空になっているからです。なぜなのかまだ追えてません。
  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)みたいにする必要があります。
返り値やユーザー定義のオブジェクトのネストみたいな場合も同様です。
これはちゃんとデータモデリングをしていたり規模が大きいアプリになると辛くなってきそうです。

グローバルステートの排他制御

クライアント側でstateの排他制御は必要ないのか?という疑問が思い浮かんで調べてみました。
結論としてはEVMがトランザクションを1個流しで処理している(厳密には並行処理)ので大丈夫なようですが、このため処理速度が遅いというデメリットもあるとのことです。
Concurrency and Parallelism in Smart Contracts, Part 1
一方でAptosSuiで採用されているMove言語はユーザー定義の型が扱えるかつステートアクセスへのアトミック性が言語レベルで担保されていて、そのため並列処理が可能になっていて速いとのことです。とても気になったので時間があるときに触ってみたいです。
Why Move? Move vs Solidity

最後に

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

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

Discussion

ログインするとコメントできます