この記事は Clojure Advent Calendar 2018 15日目の記事です。
ClojureScript による DApp 開発を紹介します。
具体的には Ethereum DApp のフロントエンドを ClojureScript で開発する方法についての解説です。スマートコントラクト自体の開発についてはあまり詳細には解説しません。想定読者はブロックチェーンに興味のある Clojure エンジニア、もしくは DApp フロントエンド開発に疲れた人です。
DApp とは
DApp/DApps: Decentralized Application とは、ブロックチェーンをプラットフォームとして動作する分散型アプリケーションのことです。仮想通貨で知られるブロックチェーンを基盤とすることで、分散性、耐改竄性、監査性などの強みを持ち、安全に「価値」の個人間取引を行うアプリケーションが開発できます。有名な DApp としては CryptoKitties があります。CryptoKitties では仮想通貨を介して Kitty という「価値」を取引することが出来ます。
その他 DApps の例: State of the ÐApps
Ethereum スマートコントラクト
Ethereum は仮想通貨 Ether のプラットフォームであると同時に DApp の開発基盤として利用で出来るブロックチェーンプラットフォームです。その実体はノードと呼ばれるサーバ群がインターネット上に分散し、ブロックチェーンネットワークを構築しています。
Bitcoin では仮想通貨の取引しか行えませんが、Ethereum では各ノードが EVM(Ehtereum Virtual Machine) と呼ばれる仮想マシンを持ち、スマートコントラクトと呼ばれる任意の(チューリング完全な)プログラムをデプロイして実行することが可能です。Ethereum DApp はこのスマートコントラクトをバックエンドとするアプリケーションです。
スマートコントラクトは Contract という単位でバイトコードとしてブロックチェーンにデプロイされます。Contract はそれぞれ状態を持ち、外部から状態の参照と、状態の更新(トランザクション)が行えます。トランザクションの発行にはコスト(Gas と呼ばれる)を仮想通貨で支払う必要があり、無用なトランザクションを抑える仕組みになっています。コントラクトには各ノードが持つ RPC エンドポイント経由でアクセス出来るので、これを使って DApp フロントエンドの開発が可能となります。
スマートコントラクト開発言語には現在 Solidity, Vyper, LLL などが存在し、それぞれ異なった特徴を持ちますが、Javascript-Like な文法を持つ Solidity がデファクトスタンダードと言えます。今回も Solidity を使用します。
DApp のエコシステム
DApp 開発は当初、環境構築の難しさが参入の障壁となっていました。その障壁を取り除くため、Ethereum 企業 ConsenSys を中心に、多くのエコシステムが生み出されました。それらを活用することで簡単に、高機能な DApp 開発が可能となりました。
ただ、現状では様々な開発レイヤーで多くのエコシステムを組み合わせる必要があり、どの様なものを使うのか知っておく必要があります。
以下、今回の開発で使ったものを紹介します。エコシステム間には依存関係も存在するため、バージョンにセンシティブなものは使用バージョンを記します。
1. MetaMask
使用バージョン: v5.2.0
Ethereum 上で仮想通貨の取引を行ったり、スマートコントラクトを実行するためには自分自身のアドレスを管理するウォレットアプリケーションが必要です。MetaMask はブラウザアドオンであり、最も手軽に使える Ethereum ウォレットです。現状 Chrom, Firefox 向けにリリースされています。(スマホアプリも出そう)
2. Infura
ブロックチェーンにアクセスするためにはネットワークに参加しているノードにアクセスする必要があります。Infura は無料でアクセス可能なノードを提供する SaaS であり、Ethereum メインネット、テストネットに対応しています。また、IPFS ノードも提供しています。
3. Rinkeby テストネットワーク
Ethereum には本番環境として使われるメインネット以外に、無料で使えるパブリックなテストネットがいくつか存在します。Ethereum はトランザクションの発行に Ether が必要となりますが、テストネットではそのテストネットでのみ流通する無料の Ether を使うことが出来ます。テストネットは複数存在しますが、新しいコンセンサスアルゴリズムを試しているテストネットなども存在します。今回の開発では Rinkeby というテストネットワークを利用しました。
5. Web3.js
使用バージョン: v0.19.0
JavaScript からスマートコントラクトにアクセスするための npm ライブラリであり、DApp フロントエンド開発の中心です。MetaMask, uPort などのウォレットアプリは Web3 のインスタンスをブラウザに提供するので、それを使うことも出来ます。
現在、v0.x 系と v.1.0 が存在し、機能が大きく追加・整理されているため 1.0 の使用が推奨されていますが、まだベータ版のため v0.x も広く使われています。今回は ClojureScript から使う上で制約があったため v0.x 系を使いました。
6. Truffle
使用バージョン: v4.1.14
スマートコントラクト開発フレームワークです。プロジェクトの作成からテスト実行、ネットワークへのデプロイまでをサポートし、多くの Ethereum DApp で使われています。
7. Ganache
使用バージョン(CLI): v6.1.6
ローカルに Ethereum ネットワークを立ち上げられるツールです。GUI 版と CUI 版が存在しますが、いずれも細かい設定なしに起動すれば直ぐに開発用ブロックチェーンネットワークが手に入ります。
ちなみに、Truffle, Ganache のお菓子シリーズには他にフロントエンド開発用の Drizzle というライブラリが存在しますが、今回は ClojureScript を使うので使いません。
8. IPFS
ipfs-api 使用バージョン: v18.1.1
以前の記事で紹介した、惑星間ファイルシステムと呼ばれる分散ファイルシステムです。アップロードされたファイルをハッシュで管理するため、大きいデータを扱えないスマートコントラクトと組み合わせて使われることが多いです。
JavaScript からのアクセスには ipfs-api というライブラリを使いますが、最近確認したら js-ipfs-http-client という名前に変更されていました。
9. uPort
uport-connect 使用バージョン: v0.7.5
Consensys スピンアウトが開発する Ethereum ベースの個人認証サービス・スマホアプリです。ユーザがアプリ上で個人情報を登録すると、個人情報がブロックチェーン上のアドレスと紐付けられます。個人情報が必要な外部サービスは個人情報取得リクエストを出し、ユーザはそれを個別に許可することで個人情報を外部サービスに提供できます。また、これによりブロックチェーン上のアドレスが個人と紐付くため、改竄不可能な個人の履歴をブロックチェーンへ書き込むことが出来ます。例えば、改竄不可能な証明書を発行してもらえたり、学歴や職歴をブロックチェーンに記録したり出来ます。また単純にウォレットアプリとしても利用可能です。
JavaScript からの呼び出し(個人情報リクエスト)には uport-connect というライブラリを使います。
DApp の構成
今回開発した DApp の本番構成は、紹介したエコシステムを使って下記の通りです。特筆すべきは、SPA 自体の配信も IPFS で行っている点ですが、その話は以前の記事で紹介しています。
ClojureScript による DApp 開発方法
では、実際に作ったリポジトリを基に開発方法を説明します。
以下が今回開発したプロジェクトで、スマートフォンから撮影した写真・動画をブロックチェーンにアップロードすることでその存在証明と所有権の取引が行える DApp です。
プロジェクト構成
スマートコントラクトプロジェクトと ClojureScript プロジェクトがマージされてしまっていますが、それぞれ分割して構成を説明します。
Truffle(スマートコントラクト)プロジェクト部分
スマートコントラクトプロジェクトは Truffle により作成しています。Solidity ソースファイル ScoopMaeket.sol がスマートコントラクトの実装です。踏み込みませんが ERC721 という分割不可能なトークンをベースにしています。
.
├── contracts ;; スマートコントラクトソースコード (Solidity)
│ ├── Migrations.sol ;; マイグレーション用コード
│ └── ScoopMarket.sol ;; アプリ実装の Solidity ソースファイル
├── migrations ;; スマートコントラクトデプロイスクリプト
│ ├── 1_initial_migration.js ;; マイグレーションスクリプト
│ └── 2_scoop_market.js ;; デプロイスクリプト
├── test ;; スマートコントラクトテストソースコード
│ ├── testScoopMarket_mint.js ;; テストは JavaScript でも Solidity でも書ける
│ ├── testScoopMarket_stop.js ;;
│ └── testScoopMarket_trade.js ;;
├── truffle.js ;; truffle 設定ファイル
├── package.json ;; Solidity 用ライブラリは npm パッケージとして追加可能
└── yarn.lock ;;
Leiningen(ClojureScript)プロジェクト部分
ClojureScript プロジェクトは re-frame テンプレートにより作成しましたが、web3, uport-connect, ipfs-api などの初期化順や設定値を管理するために integrant を組み合わせています。このあたりの話は以前の記事でも紹介しましたのでそちらも参照して下さい。(ややプロジェクト構成は異なる)
.
├── dev ;;
│ └── src ;;
│ └── user.cljs ;; dev profile での REPL 名前空間。dev-config も定義
├── project.clj ;;
├── resources ;;
│ └── public ;;
│ ├── css ;;
│ │ └── site.css ;;
│ └── index.html ;;
├── src ;;
│ └── scoopmarket ;;
│ ├── config.cljs ;; integrant の config を定義
│ ├── core.cljs ;; エントリーポイント
│ ├── macro.cljc ;; integrant リーダーなどを定義
│ ├── module ;; 初期化順や依存関係の単位で intgrant のモジュールを定義
│ │ ├── app.cljs ;; React(reagent) ビューのマウントを行うモジュール
│ │ ├── events.cljs ;; re-frame イベントハンドラの登録モジュール
│ │ ├── ipfs.cljs ;; ipfs モジュール
│ │ ├── routes.cljs ;; ルート管理モジュール
│ │ ├── subs.cljs ;; re-frame サブスクリプションハンドラの登録モジュール
│ │ ├── uport.cljs ;; uPort モジュール
│ │ └── web3.cljs ;; web3 モジュール
│ ├── views ;; reagent view
│ │ ├── market.cljs ;;
│ │ ├── mypage.cljs ;;
│ │ └── verify.cljs ;;
│ └── views.cljs ;;
└── test ;;
└── scoopmarket ;;
├── core_test.cljs ;;
└── runner.cljs ;;
下記がこの SPA のシステム設定です。integrant を組み合わせることで、システムを一つのマップとして定義でき、SPA の構成と初期化手順、設定値を全てここに集約しています。特に DApp 開発においてはエコシステムに関する設定値が多いため、この様に全体の設定を宣言的に定義できることで開発をシンプルにすることが出来ます。
(defn system-conf []
{:scoopmarket.module/web3 ;; web3.js の初期化設定
{:network-id 4
:contract-json ;; Clojure マクロによってコントラクト定義ファイルを
(json "build/contracts/ScoopMarket.json") ;; コンパイル時に読み込んでパースしている
:dev false}
:scoopmarket.module/uport ;; uport-connect の初期化設定
{:app-name "ScoopMarket"
:client-id "2ongzbaHaEopuxDdxrCvU1XZqWt16oir144"
:network "rinkeby"
:signing-key "f5dc5848640a565994f9889d9ddda443a2fcf4c3d87aef3a74c54c4bcadc8ebd"}
:scoopmarket.module/ipfs ;; ipfs-api の初期化設定
{:protocol "https"
:host "ipfs.infura.io"
:port 5001
:endpoint "https://ipfs.infura.io/ipfs/"}
:scoopmarket.module/events {}
:scoopmarket.module/subs {}
:scoopmarket.module/routes ;; SPA のルーティング定義
{:routes ["/" {"" :mypage
["verify/" [#"\d+" :id]] :verify
"market" :market}]
:subs (ig/ref :scoopmarket.module/subs)
:events (ig/ref :scoopmarket.module/events)}
:scoopmarket.module/app
{:initial-db {:active-page {:panel :none}
:sidebar-opened false
:loading? true
:ipfs (ig/ref :scoopmarket.module/ipfs)
:web3 (ig/ref :scoopmarket.module/web3)
:uport (ig/ref :scoopmarket.module/uport)}
:dev false
:routes (ig/ref :scoopmarket.module/routes)}})
開発手順
では、このプロジェクトを使った実際の開発手順を解説します。Windows でも出来ますが、OS は macOS Mojave 10.14.1 で動作確認しています。
開発時の環境構成は下記の通りで、Ethereum ノードも IPFS ノードもローカルで起動して開発します。
1. 各種インストール
Clojure 開発環境は構築されている前提とします。
まずはスマートコントラクト開発用に下記のツールをインストールして下さい。
1-1. MetaMask
MetaMask は Google Chrome, Firefox のアドオンです。今回は Chrome を使うこととして、下記のマーケットプレイスから取得してください。
https://chrome.google.com/webstore/detail/metamask/nkbihfbeogaeaoehlefnkodbefgpgknn
インストール後、セットアップが必要です。下記記事などを参考にセットアップして下さい。
https://hinemoto1231.com/investment/metamask
1-2. Truffle
npm からインストール可能です。
% npm install -g truffle@4.1.14
1-3. Ganache
CUI 版の ganache-cli は npm からインストール可能です。
% npm install -g ganache-cli@6.1.6
1-4. IPFS
Go 実装の go-ipfs を使います。下記サイトの通りインストールしてください。
https://docs.ipfs.io/introduction/install/
% tar xvfz go-ipfs.tar.gz
% cd go-ipfs
% ./install.sh
インストール後、ローカルにノードを立てるための初期化をして下さい。またブラウザから接続するために Access-Control-Allow の設定も必要です。
% ipfs init
% ipfs config --json API.HTTPHeaders.Access-Control-Allow-Origin '["*"]'
% ipfs config --json API.HTTPHeaders.Access-Control-Allow-Methods '["GET", "POST"]'
1-5. geth
ローカルネットワーク上で送金を行うために、Ethereum ノードの go 実装で、クライアントとしても使える geth: go-ethereum をインストールします。
https://github.com/ethereum/go-ethereum/wiki/Installation-Instructions-for-Mac
% brew tap ethereum/ethereum
% brew install ethereum
1-6. uPort
uPort ID というアプリをスマートフォンにインストールして下さい。(本番デプロイする人のみ)
https://itunes.apple.com/us/app/uport-id/id1123434510?mt=8
インストール後、アプリのインストレーションに従ってセットアップして下さい。
以上で開発ツールのインストールは完了です。
2. 開発環境起動
2-1. ganache-cli
スマートコントラクト開発のために ganache-cli でローカルノードを起動します。
% ganache-cli -i 1533140371286
2-2. IPFS
ローカルノードを立ち上げます。
% ipfs daemon
2-3. geth
geth クライアントで Ganache で立ち上げたブロックチェーンに接続します。
% geth attach http://localhost:8545
Welcome to the Geth JavaScript console!
instance: EthereumJS TestRPC/v2.1.5/ethereum-js
coinbase: 0xeb5c45016bb9a55ec2ff143eb8a4efa50e8422b5
at block: 0 (Mon, 10 Dec 2018 12:23:09 PST)
modules: eth:1.0 evm:1.0 net:1.0 personal:1.0 rpc:1.0 web3:1.0
>
これで、スマートコントラクトの開発が可能となりました。
3. スマートコントラクト開発
まずは、yarn で依存ライブラリをダウンロードします。
% git clone https://github.com/223kazuki/ScoopMarket
% cd ScoopMarket
% yarn
コンパイル
% truffle compile
テスト
% truffle test
ローカルネットワークへのデプロイ
% truffle migrate --reset
デプロイに成功すると build/contracts/
以下にコントラクトの定義ファイルが生成されます。
% ls build/contracts
AddressUtils.json Escrow.json
ERC165.json Migrations.json
ERC721.json Ownable.json
ERC721Basic.json Pausable.json
ERC721BasicToken.json PullPayment.json
ERC721Enumerable.json SafeMath.json
ERC721Metadata.json ScoopMarket.json
ERC721Receiver.json SupportsInterfaceWithLookup.json
ERC721Token.json
これらの定義ファイルにはスマートコントラクトの ABI(Application Binary Interface) というインターフェース情報、 デプロイ先アドレスなどが含まれており、フロントエンドからスマートコントラクトにアクセスするために必要となります。
4. フロントエンド開発
肝心の ClojureScript によるフロントエンド開発方法です。Figwheel を起動します。
% lein do clean, dev
起動後、ブラウザから http://localhost:3449 にアクセスすると Figwheel に接続し cljs-repl が開きます。Figwheel はソースコードを監視し、変更があれば自動でコンパイルしてブラウザにプッシュします。
cljs-repl に直接コードを入力してブラウザに反映させることも出来ます。
dev:cljs.user=> (js/alert "Success!")
コンパイル時には前手順で生成されたコントラクト定義ファイルが Clojure マクロにより自動で読み込まれるため、コントラクトの変更内容も動的に画面に反映させることができます。
ブラウザでの動作確認には MetaMask を使います。ローカルノードは 8545 ポートで起動しているため、接続先を "Localhost 8545" に変更します。トランザクションの発行には Ether が必要なため、MetaMask のアドレスに送金する必要があります。MetaMask のアドレスをコピーし、geth console から下記コマンドを実行して下さい。
> eth.sendTransaction({from:eth.coinbase, to:"0x2722C8aa201e384F3a2397A2f52Afb9AdCc78722", value: web3.toWei(10, "ether")})
0x2722C8aa201e384F3a2397A2f52Afb9AdCc78722
の部分は自分の MetaMask アドレスに書き換えて下さい。これで MetaMask によりスマートコントラクト操作が行えます。
今回の DApp では "New Scoop" ボタンを押して写真をアップロードし、情報を入力して "Mint" ボタンを押すと MetaMask が起動し、トランザクション発行が出来ます。
手順 3, 4 を繰り返すことで動的に DApps を開発することが出来ます。
5. 本番デプロイ
手順の最後は本番デプロイです。本番と言いつつ今回の DApp の最終デプロイ先はテストネットです。MetaMask などで Rinkeby の Mnemonic と自分のアドレスを用意して下さい。
テストネットでもデプロイには Ether が必要となります。Rinkeby の Ether は下記サイトから無料で取得できます。
https://faucet.rinkeby.io/
自分のアドレスを Twitter でツイートし、そのツイートのアドレスを投稿すれば自動で Ether が送金されます。
Infura 経由でデプロイするので、infura のアクセストークンが必要となります。Infura サイトから "GET STARTED FREE" でサインアップして取得して下さい。
https://infura.io/
Mnemonic と Infura アクセストークンが用意出来たら、下記を実行してスマートコントラクトを Rinkeby にデプロイして下さい。
% export MNEMONIC="portion undo stumble ..."
% export INFURA_ACCESS_TOKEN="mig...."
% truffle migrate --reset --network=rinkeby
デプロイに成功するとコントラクト定義ファイルが更新されるので、フロントエンドもビルドしてデプロイします。下記スクリプトを実行して下さい。
% ./deploy_ipfs.sh
SPA のデプロイ先は IPFS です。まずローカルの IPFS ノードにのみ反映されますが、しばらくするとインターネット経由(パブリックな IPFS ノード)でも確認出来るようになります。
- http://localhost:8080/ipfs/QmQEwZsE6qeLjHeTtrcZKiaWiRkHdh5ALssuh6TF3kvNFP
- https://gateway.ipfs.io/ipfs/QmQEwZsE6qeLjHeTtrcZKiaWiRkHdh5ALssuh6TF3kvNFP
駆け足でしたが、以上が ClojureScript による DApp 開発手順でした。
ClojureScript によるスマートコントラクトアクセス
最後に、ClojureScript によるスマートコントラクトアクセス部分を解説します。これには下記の 2 ライブラリを使っています。
- cljs-web3 v0.19.0-0-11
- re-frame-web3-fx v1.0.5
cljs-web3 は web3.js の cljs ラッパーライブラリで、今回依存する web3.js のバージョンは 0.19.0 です。
re-frame-web3-fx は cljs-web3 呼び出しを、re-frame の副作用として登録してくれるライブラリです。re-frame の副作用についてはドキュメントを参照して下さい。
Web3 初期化
cljs-web3, re-frame-web3-fx を使うためには Web3 関連インスタンスを初期化する必要があります。下記が Web3 を初期化している箇所であり、integrant のモジュールの init メソッドとして実装しています。
(defmethod ig/init-key :scoopmarket.module/web3 [_ {:keys [:contract-json :network-id :dev]}]
(let [{:keys [:abi :networks] :as contract} contract-json
network-id-key (keyword (str network-id))
address (-> networks network-id-key :address)
web3 {:network-id network-id
:dev dev
:contract contract
:contract-address address}]
(if-let [web3-instance (aget js/window "web3")] ;; web3 インスタンスの取得
(assoc web3
:web3-instance web3-instance
:contract-instance (web3-eth/contract-at web3-instance abi address) ;; contract インスタンスの生成
:my-address (aget web3-instance "eth" "defaultAccount") ;; 自分のアドレスの取得
:is-rinkeby? (or (some-> web3-instance
(aget "currentProvider")
(aget "publicConfigStore")
(aget "_state")
(aget "networkVersion")
(= (str network-id)))
dev)) ;; 返り値は app モジュールで app-db に init-state として書き込む。
web3)))
web3.js によりスマートコントラクトにアクセスするためには 2 つのインスタンスが必要となります。接続先情報や秘密鍵を管理する web3 インスタンスと、スマートコントラクトのインターフェースを管理する contract インスタンスです。
web3 インスタンスは MetaMask や uPort が生成し、ブラウザに挿入してくれるものを使います。((aget js/window "web3")
)
contract インスタンスは web3 インスタンスと、接続するスマートコントラクトの ABI(Application Binary Interface) と呼ばれるインターフェース定義、コントラクトのアドレスから生成します。((web3-eth/contract-at web3-instance abi address)
)
ABI とコントラクトアドレスは Truffle によりスマートコントラクトをデプロイした際に生成される json ファイルに書かれているため、これをシステム設定から受け取っています。(config.cljs の (json "build/contracts/ScoopMarket.json")
)生成したインスタンスは re-frame の app-db に書き込み、イベントハンドラから使えるようにします。
スマートコントラクトアクセス
re-frame-web3-fx を使います。district0x.re-frame.web3-fx
を require すると副作用ハンドラが登録されて、イベントハンドラの副作用としてスマートコントラクトにアクセスが出来ます。
下記はスマートコントラクトの状態参照処理です。app-db からインスタンスを取り出し、:web3/call
という副作用を呼び出します。
(require [district0x.re-frame.web3-fx])
(re-frame/reg-event-fx
::fetch-scoops
(fn-traced [{:keys [:db]} [_ web3]]
{:web3/call {:web3 (:web3-instance web3) ;; web3, contract インスタンスを渡して副作用呼び出し。
:fns [{:instance (:contract-instance web3) ;; contract インスタンス
:fn :scoops-of ;; 呼び出すスマートコントラクト関数名
:args [(:my-address web3)] ;; 引数
:on-success [::fetch-scoops-success web3] ;; 成功時のコールバックハンドラ
:on-error [::api-failure]}]}})) ;; 失敗時のコールバックハンドラ
;; 成功時のコールバックハンドラ
(re-frame/reg-event-db
::fetch-scoops-success
(fn-traced [db [_ web3 scoops]]
(let [ids (map #(let [id (js-invoke % "toNumber")] ;; レスポンス中の数値は int に変換する必要あり
(re-frame/dispatch [::fetch-scoop web3 :scoops id])
id) scoops)]
(-> db
(dissoc :loading?)
(assoc :scoops (select-keys (:scoops db)
(map #(keyword (str %)) ids)))))))
注意としてはスマートコントラクトのレスポンスは json でなくタプルとしてしか受け取れないため、期待するデータ構造にマッピングする必要があります。また、数値は BigNumber というクラスのインスタンスで返ってくるため toNumber で int に変換する必要がもあります。
次にスマートコントラクトの状態を更新するトランザクション発行処理です。ここでは今回のシステム構成で問題があり、re-frame-web3-fx を直接使うことが出来ませんでした。MetaMask で生成された web3 インスタンスを使えば特に問題は起こらないのですが、uPort が生成した web3 によりトランザクションを発行しようとすると失敗してしまうという問題です。これは uPort が生成する web3 の接続先である infura が、re-frame-web3-fx がトランザクション発行時に呼び出すイベントログ監視機能をサポートしていないという問題に起因しており、cljs-web3 を直接呼び出してイベントログ監視機能をスキップすれば解決できます。そのため、トランザクション発行は副作用として呼び出せませんでしたが、cljs-web3 を直接使うことで下記の通りトランザクションを発行できます。
(re-frame/reg-event-db
::mint
(fn-traced [db [_ web3 mint-cost scoop]]
(let [{:keys [:image-hash :name :for-sale? :price]} scoop
price (if-not (empty? price)
(js/parseInt price) 0)]
(web3-eth/contract-call (:contract-instance web3) ;; cljs-web3 関数の直接呼び出し
:mint (or name "") price (or for-sale? false) image-hash ;; スマートコントラクト関数名と引数
{:gas 4700000 ;; トランザクションパラメータ
:gas-price 100000000000
:value mint-cost}
(fn [err tx-hash]
(if err
(js/console.log err)
(web3/wait-for-mined web3 tx-hash ;; この部分が問題で既存のトランザクション監視が失敗するため、setTimeout でトランザクション状態を定期監視する方法にしている
#(js/console.log "pending")
#(re-frame/dispatch [::mint-success web3 %]))))))
(assoc db :loading? {:message "Minting..."})))
re-frame-web3-fx が登録する他の副作用としてはイベントログ監視 :web3/watch-events
などがありますが、こちらも上記と同様の問題で uPort で接続した際には失敗してしまいます。ちなみに MetaMask も接続先は infura らしいですが、MetaMask 自体がイベント監視機能を polyfill している様子です。
(re-frame/reg-event-fx
::watch-minted
(fn [{:keys [:db]} [_ web3]]
{:web3/watch-events {:events [{:id :minted-watcher ;; イベントログを監視する副作用
:event :Minted ;; 監視対象のイベントログ名
:instance (get-in db [:web3 :contract-instance])
:block-filter-opts {:from-block "latest"
:to-block "latest"}
:on-success [::minted web3]
:on-error [::api-failure]}]}}))
cljs-web3 が依存する web3.js は v0.19.0 であるため、使えない機能がいくつか存在します。本当は v1.x 系を使いたいのですが、web3.js は依然活発な開発中であるため、cljs-web3 の対応はしばらく先のことになりそうです。
知見
では、開発で得た知見をいくつか紹介します。
- スマートコントラクト関係
- 脆弱性が簡単に作り込めてしまうため、セキュリティ対策とテストが非常に重要。
- 基本的には再デプロイ出来ず、コストもかかるため、やっぱりテストが超重要。(ロケットを打ち上げるようなものと言われる)
- デプロイ可能なバイナリのサイズにシビアで、複雑な業務ロジックを乗せることは出来ない。(もしくはコントラクトの分割が必要)
- 再デプロイやコントラクト分割を実現するためにはデザインパターンを習得する必要がある。
- Clojure で書きたかった。(LLL は Lisp...)
- どんなプログラムが向いているかは難しい問題で、正直仮想通貨以上のユースケースはない気がしている。
- エコシステム関係
- とにかく使うものが多すぎる。
- 更に、対応する web3.js のバージョンや機能の違いに起因する不整合が存在。
- 活発な開発が行われており、時間が解決してくれるのを期待している。
- web3.js
- v0.x 系と v1.x 系の違いが大きい。v1.0 早くリリースされて欲しい。
- スマートコントラクトの RPC アクセス自体に REST ほどの柔軟性がなく簡単に扱えるという程ではない。
- ClojureScript で開発したことによる利点
- 依存するエコシステムとライブラリが多いため DApp フロントエンド開発の難易度はそもそも高いが、integrant を採用したことでライブラリ間の依存と設定を宣言的に管理できた。
- 依存ライブラリが巨大なため、Figwheel でブラウザリロードなしに開発を進められたのは大きい。
- Clojure マクロにより、巨大なコントラクト定義ファイルをコンパイル時にパース出来たのが気持ちよかった。
- スマートコントラクト開発を動的な開発の中に組み込むことで、単純に ClojureScript 開発であること以上の高速化をもたらせた。
まとめ
以上、ClojureScript による Ethereum DApp 開発を紹介しました。
Ethereum DApp 開発は現状、シンプルとは言えない状況ですが、Clojure による宣言的で動的な開発を取り入れることである程度管理を簡単にすることが出来ます。仮想通貨ブームは下火になりつつありますが、ブロックチェーンは幻滅期を超えて適切な場所、適切な使われ方で普及していくものと信じています。この機会にブロックチェーンに触れてみるのはいかがでしょうか。
参考
- How to create decentralised apps with Clojurescript re-frame and Ethereum
- Truffle Document
- ClojureScript による SPA のモジュール分割
- re-frame+integrant による ClojureScript SPA 開発
(おまけ)Etherum 学習リソース
- EthereumDev.io
- Comprehensive Solidity tutorials
- Ethereum Development Walkthrough (5 part series) -- Hackernoon
- CryptoZombies
- Ethernaut Security game by Open Zeppelin
- Visual Overview of Ethereum in 116 slides
- Dan Finlay on How Ethereum Works
- Verify contract
- Solidity by Example -- Solidity Docs
- Style Guide -- Solidity Docs
- Ethereum Wiki Main Page