NFT関連の自動テストのアーキテクチャ

# STYLYとNFT

もう去年の話になりますが、2022年10月15日にSTYLYではNFT関連の機能が実装され、OpenSeaを介して、XR作品の取引が出来るようになりました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/139643/7989996a-6a3d-b185-68e2-adde3a87cea2.png)

[![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/139643/ef588a7f-735a-67f4-dc08-b0906b8a89e5.png)](https://vimeo.com/758951801)

現在、NFTとして出品する機能は提供されていませんが、MetaMaskと連携を行い、Ethereumのwalletと連携する機能があります。これによりSTYLYのXR作品のNFTのコレクションを表示できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/139643/0cddec19-817b-5b32-a4df-c8f0ad370b67.png)

STYLYではこのような機能に関しても自動テストを書いています。ネット上にはあまり知見が書かれていない内容なので、これを紹介したいと思います。

# MetaMaskをどのようにシミュレートするか？

特に説明なく話していましたが、MetaMaskについて説明します。

[Chromeプラグインの説明](https://chrome.google.com/webstore/detail/metamask/nkbihfbeogaeaoehlefnkodbefgpgknn?hl=ja)によると、

> ブラウザにあるイーサリアムウォレット
> EthereumのID管理プラグイン MetaMaskは、Ethereumを用いる分散型アプリケーション(DApps)にアクセスしやすくするためのプラグインです。
>
> あらゆるウェブサイトのJavascript のコンテキストに対して、EthereumのWeb3 APIを提供し、 それによって分散型アプリがブロックチェーンのデータをブラウザから読むことができるようになります。

とあり、AndroidやiPhone用アプリもあります。いわゆるウォレットと呼ばれる、Ethereumという仮想通貨を入れておく財布アプリのようなものです。
今回、自動テストをすることを考えると、このMetamaskが行っているwallet登録と同じ動作をする必要があります。そこで、ナイーブに考えるとすると、

1. 自動テスト内でChromeを立ち上げる
2. ChromeにMetamask用のプラグインを導入する
3. Metamaskにテスト用のwallet情報(秘密鍵など)を登録する
4. Chromeを操作し、Galleryに対し、walletの登録作業を行う。

というフローになります。おそらく多くの方が考えているとおり、うまくいかないでしょう。
まず今回テストしたいのは、"wallet登録のAPI"です。そのようなサーバーサイドのAPIのテストに対し、このためだけに、Chromeをインストールし実行するのは非常にコスト高です。また、4.ではGalleryを操作しているので、APIサーバーに加えて、フロントエンド側のサーバーも建てる必要があります。最近では、Playwrightなど、E2Eテストの環境は整ってきましたが、個人的にはまだ動作が不安定な印象があります。そこに、加えてChromeプラグインをインストール、セットアップするのはとても大変だと思われます。
このように、wallet登録のAPIテストを行うにあたって、Chromeを実行し、Metamaskのプラグインをインストールし自動テストを実行するのは、現実的ではないと分かります。

# wallet登録とは何をしているのか？

前節では、MetaMaskのChromeプラグインを自動テスト内で動かすのは現実的ではない。という話を書きました。それを受けて、では、そもそもwallet登録とは何をしているのか？という話になります。
STYLYのwallet登録はおおむね以下のようなフローとなっています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/139643/9d69ad00-60ca-3df0-2133-7706cd050c5a.png)

ある署名用のデータを用意し、それをMetamaskに対し、データの署名を求めます。[これはmetamaskのpersonal_signという機能を使って実現できます。](https://docs.metamask.io/wallet/how-to/sign-data/)そうすることでMetamaskから署名データが返却されます。ブラウザでは、このデータと署名データをAPIへ送ります。API側では、データと署名データの組から、署名に利用したwalletアドレスを抽出します。これは、[web3.jsであれば、web3.eth.accounts.recover](https://web3js.readthedocs.io/en/v1.10.0/web3-eth-accounts.html#recover)や[ethers.jsであれば、ethers.utils.verifyMessage](https://docs.ethers.org/v5/api/utils/signing-key/#utils-verifyMessage)で実装可能です。そして、最後に抽出したwalletアドレスをDBへ保存する。というフローになります。
とどのつまり、

1. ethereum walletを作成
2. ethereum walletで署名する

ということが出来れば、MetaMaskと似た動作をシミュレーションすることが可能であると考えられます。

# PHPとWeb3の現状

[STYLYのAPIはPHPで実装されています。](https://qiita.com/kotauchisunsun/items/7eb07e83758cd6a546ce)そのため、Ethereum周りのテストをするにおいても、PHPで利用できるライブラリがある方が嬉しいです。しかし、Ethereumのエコシステムはjavascript周りが強く、web3.jsやethers.jsなどの方が有名です。もう1年ほど前の記憶なので、このあたりの記憶が曖昧なのですが、PHPで利用できるライブラリは

- [etherum-php](https://github.com/digitaldonkey/ethereum-php)
- [web3](https://github.com/web3-php/web3)
- [web3.php](https://github.com/web3p/web3.php)

の3種ぐらいしか見当たりませんでした。ethereum-phpは最終更新が2021年11月と少し古く、web3、web3.phpはどれもドキュメントが少なく動作が判然せず、学習コストが高そうである。という問題がありました。そのため、 **PHPのネイティブでEthereum周りの機能を利用するのはかなり難易度が高そうである。** という認識になりました。

# gethとの連携

今回、スマートコントラクトやweb3の周りをやるにあたって、自分の蔵書を読み直しました。

[スマートコントラクト本格入門―FinTechとブロックチェーンが作り出す近未来がわかる](https://amzn.to/3MjXm68)
[![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/139643/19531ced-fcc2-1436-f7bd-4d9d582caa0b.png)](https://amzn.to/3MjXm68)


[はじめてのブロックチェーン・アプリケーション Ethereumによるスマートコントラクト開発入門](https://amzn.to/3QhYgRO)
[![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/139643/38f49ed5-8e91-597d-b552-f2f6a6be3e9e.png)](https://amzn.to/3QhYgRO)

以上の2冊を読みましたが、かなり古めの本です。というのも、2017年頃にスマートコントラクト周りに興味があり、本を買ったまま読まずに積んでいた本になります。これらを読んでいると、概ね[geth](https://geth.ethereum.org/)というGo言語で実装されたEthereum系のツールを使っていました。そのため、このgeth周りにヒントは無いか？と思い、調査を開始しました。

## gethのJSON-RPC server

gethのドキュメントをよく読んでいくと、[JSON-RPC Srever](https://geth.ethereum.org/docs/interacting-with-geth/rpc)というセクションが見つかります。これは、特定のjsonをREST APIを通じてリクエストすることでgethの機能を利用することが出来ます。
その中に、[personal Namespace](https://geth.ethereum.org/docs/interacting-with-geth/rpc/ns-personal)という章があります。この機能を利用すると、REST APIを通じて、Walletの作成や、署名というものが可能になります。(ただ、そろそろdeprecatedのようですが・・・)
このJSON-RPC機能はデフォルトではオンになっていないため、自分でgethの起動オプションを調整してやる必要があります。STYLYの開発環境では以下の様な設定にしています。

```bash
geth --http --http.addr 0.0.0.0 --http.api personal,eth,debug --nodiscover --dev --http.vhosts '*'
```

例えば、Walletの作成は、[persnal_newAccount](https://geth.ethereum.org/docs/interacting-with-geth/rpc/ns-personal#personal-newaccount)を使います。パスワードを設定して呼び出し、返り値としてWalletのアドレスが手に入ります。

```bash
curl -X POST -H "Content-Type: application/json" --data '{"jsonrpc":"2.0","method": "personal_newAccount", "params": ["password"],"id":1111}' localhost:8550 -v
```

署名に関しては、[personal_sign](https://geth.ethereum.org/docs/interacting-with-geth/rpc/ns-personal#personal-sign)を用いて、署名したいデータ(message)、アカウントのアドレス、パスワードを指定して呼び出し、署名が返ってきます。

```bash
curl -X POST -H "Content-Type: application/json" --data '{"jsonrpc":"2.0","method": "personal_sign", "params": ["message","account","password"],"id":1111}' localhost:8550 -v
```

このようにREST API経由でgethのwallet作成機能、署名が利用できます。これであれば、PHP側から機能の利用が可能となります。

## PHPとgethのテストプロセスでの融合

これまでの内容で、REST APIでgethの機能が呼び出すことが可能になりました。PHPではGuzzleなどのHTTPリクエストが出来るライブラリを用いて、リクエストすればよいことが分かります。しかし、新たな問題があります。それは、テスト実行前にgethをhttpサーバーとして立ち上げる必要があります。また、CIにおいてgethをどのようにインストールするのか？という問題もあります。STYLYではどのようにこの辺りを解決しているか。というとdocker composeです。[以前の記事](https://qiita.com/kotauchisunsun/items/7eb07e83758cd6a546ce)にも書きましたが、STYLYのAPIの開発環境では、docker composeで複数のコンテナを起動しています。そして、gethは公式のDockerイメージが公開されています。

https://hub.docker.com/r/ethereum/client-go

なぜgethを選んだのか。というとこのあたりのDocker周りのエコシステムも整備されているので、既存のSTYLYとの技術スタックとの相性も良く、CIで安定的にテストが実行できています。

## gethのバグ？

しかし、gethに絶妙なバグ？があって、STYLYで使っているテスト用の署名の関数は以下の様になっています。

```php
    public function sign(string $message, string $account, string $password): string
    {
        usleep(100000);
        return $this->callJsonRpc("personal_sign", [$message, $account, $password]);
    }
```

このように微妙に0.1秒待つような実装になっています。
これはどういうことか。というと、データを署名したいとき、まず初めに、gethに対してpersonal_newAccountを用いて、walletの作成を依頼します。その返り値からwalletのアドレスを取り出し、今度は、personal_signを用いて、署名を依頼します。そうすると、**walletが存在しない旨のエラーメッセージが発生しました。**
これは予測でしかないのですが、gethのwallet情報はDBではなく、ただのファイルに書いているようです。そのため、高速にリクエストすると、ファイルシステムの反映が追い付かず、署名の時にwallet情報が無い扱いになってしまってエラーになっているようでした。したがって、0.1秒スリープを入れることで、ファイルシステムへの反映を十分に待って、リクエストをするようにしていました。　最新のgethだと発生しないかもしれませんが、少なくとも私が実装していた2022年10月ごろ(v1.10系)にはそういったバグが発生していました。
 おそらくですが、こういう風なテストの用途で高速にリクエストをするユーザーが少なかったがために見落とされてたのではないかなーと思います。

 # 「うちのシステムは特殊だから」を言い訳にしない。

　このNFT関連のシステムは非常にハードでした。はっきりと言えば門外漢の分野であり、そのテストに関連うする知見はほぼ世の中に無いようなシステムです。そんなシステムをどうやってテストすればよいのか？というのは、課題でした。
　MetaMaskでどのようにログインを実装するか。と言ったことは、チュートリアルなどもあり、比較的難易度の低い内容ではあります。しかし、その中身がどうなっているのか。というところは、さらに深く実装を知る必要があり、またその内容をどのように自動テストに組み込むか。というのは、かなり難易度の高い実装となるでしょう。

「うちのシステムは特殊だから自動テスト出来ない」

と思いがちですが、なんとか頭をひねればなんとかなるものです。こんなに特殊なシステムでもなんとかなるものなので、思考停止せず自動テストに取り組むと良いと思います。

# 宣伝
PsychicVRLabではUnityエンジニア・サーバーサイドエンジニアを募集しています！！ご応募お待ちしています！！

https://www.wantedly.com/projects/1360135

https://www.wantedly.com/projects/1360123