SolidityとReactとFirebase Hostingを使って爆速でDAppsをリリースする

今回の記事では

- Solidityを使ったDAppsを開発し、
- Ganacheとテストネット上にスマートコントラクトをデプロイし、
- React製のフロントエンドをFirebase Hostingを使ってデプロイし、
- ブラウザ上でテストネット上に存在するContractのDataを表示・更新する


というところまでの作業過程をまとめてみます。

## スマートコントラクトの開発

### truffle boxでのプロジェクトの準備
今回はTruffle Boxを使って、Reactのプロジェクトテンプレートから開発を始めます。
（なおTruffleとはブロックチェーンの開発環境とテスティングフレームワークのツールです）。
https://www.trufflesuite.com/boxes/react
下記コマンドを実行すると、Reactが含まれたスマートコントラクトのテンプレートがディレクトリに展開されます。

```bash
truffle unbox react
Starting unbox...
=================
✔ Preparing to download box
✔ Downloading
✔ cleaning up temporary files
✔ Setting up box
Unbox successful, sweet!
Commands:
Compile:truffle compile
Migrate:truffle migrate
Test contracts: truffle test
Test dapp:cd client && npm test
Run dev server: cd client && npm run start
Build for production: cd client && npm run build
```

展開が完了したら、次に `truffle develop` を実行します。このコマンドはインタラクティブなコンソールを立ち上げてくれ、かつ開発用のブロックチェーンを構築してくれます。

```bash

$ truffle develop
Truffle Develop started at http://127.0.0.1:8545/
Accounts:
(...)
Private Keys:
(...)
Mnemonic: (...)
⚠️Important ⚠️: This mnemonic was created for you by Truffle. It is not secure.
Ensure you do not use it on production blockchains, or else you risk losing funds.
Error: The network id specified in the truffle config (5777) does not match the one returned by the network (3).Ensure that both the network and the provider are properly configured.
at Object.detectAndSetNetworkId (/Users/wildmouse/.nodenv/versions/10.20.1/lib/node_modules/truffle/build/webpack:/packages/environment/environment.js:97:1)
at process._tickCallback (internal/process/next_tick.js:68:7)
Truffle v5.1.31 (core: 5.1.31)
Node v10.20.1
```

コマンドを実行すると、ネットワークIDが違うと怒られて終了してしまっているようです。
ネットワークIDとはEthereumにおけるmainnetやGoerli, Rinkebyなどのテストネットの識別子です。
truffleでは `truffle-config.js` に設定を記述しているので参照してみます。

```js

const path = require("path");
module.exports = {
  // See <http://truffleframework.com/docs/advanced/configuration>
  // to customize your Truffle configuration!
  contracts_build_directory: path.join(__dirname, "client/src/contracts"),
  networks: {
    develop: {
      port: 8545
    }
  }
};
```

このコードのnetworksの部分がネットワークに関する設定をしているようです。
公式ドキュメントを参照すると、今のコードと解離があるようなので、設定を更新し下記のようにします。

```js

const path = require("path");
module.exports = {
  (...)
  networks: {
    development: {
      host: "127.0.0.1",
      port: 8545,
      network_id: "*"
    }
  }
};
```

そもそも開発用のnetworkはキー名が `develop` ではなく `development` にだったようです。truffle box側のコードが古かったのでしょうね。
コードを更新した後にもう一度 `truffle develop` を実行すると、無事truffleのコンソール画面が表示されるので、成功したようです。

```bash

$ truffle develop
Truffle Develop started at http://127.0.0.1:9545/
(...)
truffle(develop)> 
```

reactのtruffle boxではcontract, migrations, testについてのサンプルファイルが用意されているので、試しにcompileとtestを実行して、成功することを確認します。

### Hello Worldを扱うContractを書く

プロジェクトの初期化が完了したら、次に簡単なContractを書いてみます。
今回はシンプルにHello, Worldをstateとして保持し、必要に応じて取得できるようなContractを作ります。
（なお、truffle unboxをしたときにサンプルで入っているSimpleStorageというContractは不要なので、このタイミングで削除しておきます。）

```
    pragma solidity ^0.5.16;
    contract Greeter {
        string public greeting = "Hello, World!";
        function updateGreeting(string calldata _greeting) external {
            greeting = _greeting;
        }
    }
```

Contractが書けたら、次にunit testを追加します。
Solidity上でのUnit testと、JavaScript上から呼び出された場合のUnit Testの両方を記述します。

```
    pragma solidity >=0.5.16 <0.7.0;
    import "truffle/Assert.sol";
    import "truffle/DeployedAddresses.sol";
    import "../contracts/Greeter.sol";
    contract TestGreeter {
        Greeter greeter;
        function beforeEach() public {
            greeter = Greeter(DeployedAddresses.Greeter());
        }
        function testGreet() public {
            string memory expected = "Hello, World!";
            Assert.equal(greeter.greeting(), expected, "Greeter message should equals to expected.");
        }
        function testUpdateGreeting() public {
            greeter.updateGreeting("Hello, Solidity!");
            string memory expected = "Hello, Solidity!";
            Assert.equal(greeter.greeting(), expected, "Greeter should be updated");
        }
    }
```

```javascript

    const Greeter = artifacts.require("./Greeter.sol");
    contract("Greeter", (accounts) => {
        it("should return greeting", async () => {
            const greeterInstance = await Greeter.deployed();
            const greeting = await greeterInstance.greeting();
            assert.equal(greeting, "Hello, World!", "Greeting should match to expected.");
        })
    })
```

テストが書けたら、truffle consoleの `test` コマンドを使って、想定通りの動作になっていることを確認します。

```bash
    truffle(develop)> test
    Using network 'develop'.
    Compiling your contracts...
    ===========================
    > Compiling ./contracts/Greeter.sol
    > Compiling ./test/TestGreeter.sol
    > Artifacts written to /var/folders/fp/vgz1s6qs2nngrmvvryfx7v380000gn/T/test-2020619-21793-powi9y.7ulx
    > Compiled successfully using:
        solc: 0.5.16+commit.9c3226ce.Emscripten.clang
        TestGreeter
            1) "before each" hook: beforeEach for "testGreet"
        Contract: Greeter
            2) should return greeting
                > No events were emitted
        0 passing (7s)
        2 failing
        1) TestGreeter
            "before each" hook: beforeEach for "testGreet":
                Error: Returned error: VM Exception while processing transaction: revert
        2) Contract: Greeter
            should return greeting:
                Error: Greeter has not been deployed to detected network (network/artifact mismatch)
```

テストが失敗してしまいました。
エラーを見ると、Greeter Contractがネットワーク上にデプロイされていないとのことです。
新しく作られたコントラクトをネットワークにコントラクトをデプロイするためには、migrationファイルを追加する必要があるため、新規に追加します。

```javascript

    var Greeter = artifacts.require("./Greeter.sol");
    module.exports = function(deployer) {
        deployer.deploy(Greeter);
    };
```

migrationファイルが追加できたら再度テストを実行します。今度は成功しましたね。

```bash

    truffle(develop)> test
    Using network 'develop'.
    Compiling your contracts...
    ===========================
    > Compiling ./contracts/Greeter.sol
    > Compiling ./test/TestGreeter.sol
    > Artifacts written to /var/folders/fp/vgz1s6qs2nngrmvvryfx7v380000gn/T/test-2020619-21793-i78bel.3kjkp
    > Compiled successfully using:
        solc: 0.5.16+commit.9c3226ce.Emscripten.clang
        TestGreeter
            ✓ testGreet (82ms)
                ✓ testUpdateGreeting (80ms)
        Contract: Greeter
            ✓ should return greeting (134ms)
        3 passing (7s)
```

## スマートコントラクトのデプロイ

### 開発用ネットワークへスマートコントラクトをデプロイする

コントラクト自体の開発はこれで完了とし、次に開発用のネットワークへスマートコントラクトをデプロイします。
とは言ってもこれは簡単で、 `truffle migrate` コマンドを実行することで完了します。

```bash

truffle(develop)> migrate --reset
Compiling your contracts...
===========================
> Everything is up to date, there is nothing to compile.
Starting migrations...
======================
> Network name:'develop'
> Network id:5777
> Block gas limit: 6721975 (0x6691b7)
1_initial_migration.js
======================
Deploying 'Migrations'
(...)
2_deploy_contracts.js
=====================
Deploying 'Greeter'
(...)
Summary
=======
> Total deployments: 2
> Final cost:0.0079031 ETH
```

デプロイが完了しました。便宜上、フロントエンドでの動作確認は後ほど行います。

### テストネットへスマートコントラクトをデプロイする
開発用ネットワークへのデプロイが完了したので、次はテストネットへのデプロイを行います。今回はGoerliテストネットへデプロイすることにします。
テストネットへのデプロイは開発用のものと違い、いくつか追加で作業を行う必要があります。

#### Infuraの利用
InfuraはマネージドのEthereumノードを管理してくれるもので、これを使うことでローカルでEthereum Clientを立てることなくEthereumとの通信を行うことができるようになります。
利用するにはGUIでアカウント登録を行い、新規プロジェクトを作成すればOKです。
作成したプロジェクトのプロジェクトIDは後ほど利用することになります。

#### HDWallet Providerの利用
truffle configでHDWalletを使うために、npmあるいはyarnで `@truffle/hdwallet-provider` をインストールする必要があります。

```bash

yarn add -D @truffle/hdwallet-provider
```

インストールできたら、truffle-config.jsを更新します。

```javascript

    const path = require("path");
    module.exports = {
        (...)
        networks: {
            (...)
            goerli: {
                provider: () => {
                    const mnemonic = process.env["MNEMONIC"]
                    return new HDWalletProvider(
                        mnemonic,
                        "https://goerli.infura.io/v3/[infura_project_id]"
                    )
                },
                network_id: "5"
            }
        }
    };
```

#### FaucetでのEtherの受け取り
デプロイには手数料としてEtherを支払う必要がありますが、テストネットではFaucetを使って幾らかのEtherを自由なタイミングで発行することができます。
Goerliでは https://goerli-faucet.slock.it/ でEtherの発行をすることができます。
以上の準備ができれば、truffleのコンソール上でmigrateを実行すればテストネットへのデプロイを行うことができます。
なおmigrateを行うときには、デプロイ対象のネットワークとしてテストネットを指定する必要があります。

```bash

truffle(develop)> migrate --network goerli
Starting migrations...
======================
> Network name:'goerli'
> Network id:5
> Block gas limit: 8000000 (0x7a1200)
1_initial_migration.js
======================
Deploying 'Migrations'
(...)
2_deploy_contracts.js
=====================
Deploying 'Greeter'
(...)
Summary
=======
> Total deployments: 2
> Final cost:0.0079031 ETH
```

## フロントエンドの開発

スマートコントラクト自体のデプロイは完了したので、次にフロントエンド側の開発を行います。
こちらも今回の内容としてはシンプルで、Greetingを取得し、必要に応じて更新できる画面を開発するだけに留めておきます。
初期化完了後にGreetingを表示し、ユーザーが新しいGreetingを入力・更新できるコードはサンプルコードを更新し、下記のようにしました。

```react
    import React, {Component} from "react";
    import GreeterContract from "./contracts/Greeter.json"
    import getWeb3 from "./getWeb3";
    import "./App.css";
    class App extends Component {
        state = {greeting: "", newGreeting: "", web3: null, accounts: null, contract: null};
        componentDidMount = async () => {
            try {
                const web3 = await getWeb3();
                const accounts = await web3.eth.getAccounts();
                const networkId = await web3.eth.net.getId();
                const deployedNetwork = GreeterContract.networks[networkId];
                const instance = new web3.eth.Contract(
                    GreeterContract.abi,
                    deployedNetwork && deployedNetwork.address,
                );
                this.setState({web3, accounts, contract: instance}, this.getGreeting);
            } catch (error) {
                alert(`Failed to load web3, accounts, or contract. Check console for details.`,);
                console.error(error);
            }
        };
        getGreeting = async () => {
            const {contract} = this.state;
            const response = await contract.methods.greeting().call();
            this.setState({greeting: response});
        };
        updateGreeting = async (greeting) => {
            const {accounts, contract} = this.state;
            contract.methods.updateGreeting(greeting).send({from: accounts[0]})
                .catch(error => console.error(error));
        }
        render() {
            if (!this.state.web3) {
                return <div>Loading Web3, accounts, and contract...</div>;
            }
            return (
                <div className="App">
                    <div>{this.state.greeting}</div>
                    <input
                        onChange={e => this.setState({newGreeting: e.target.value})}
                        value={this.state.newGreeting}
                    />
                    <button onClick={this.state.updateGreeting}>Update Greeting</button>
                </div>
            );
        }
    }
    export default App;
```

ここで開発用ネットワークやテストネットとの接続を確認し、正しく動作していることを確認するために、 `yarn start` コマンドで一度フロントエンドサーバーを立ち上げて確認するのが良いでしょう。

## フロントエンドのデプロイ

動作が確認できたら、最後にFirebase Hostingを使ってフロントエンドをFirebaseにデプロイします。
Firebase Hostingの[Get started with Firebase Hosting](https://firebase.google.com/docs/hosting/quickstart)の記事の流れに従って進めていきます。
Firebase CLIがインストール済みであると仮定して、 `firebase init` でプロジェクトを初期化します。
firebase initではHostingを選択し、必要に応じて既存プロジェクトの利用あるいは新規プロジェクトの作成を選択します。
public directoryをclient/buildにしていることに注意してください。

```bash

$ firebase init

######## #### ########  ######## ########     ###     ######  ########
##        ##  ##     ## ##       ##     ##  ##   ##  ##       ##
######    ##  ########  ######   ########  #########  ######  ######
##        ##  ##    ##  ##       ##     ## ##     ##       ## ##
##       #### ##     ## ######## ########  ##     ##  ######  ########

You're about to initialize a Firebase project in this directory:
/Users/wildmouse/projects/greeter
? Which Firebase CLI features do you want to set up for this folder? Press Space to select features, then Enter to confirm your choices. Hosting: Configure and deploy Firebase Hosting sites
=== Project Setup
First, let's associate this project directory with a Firebase project.
You can create multiple project aliases by running firebase use --add,
but for now we'll just set up a default project.
? Please select an option: Use an existing project
? Select a default Firebase project for this directory: hello-smart-contract (hello-smart-contract)
iUsing project hello-smart-contract (hello-smart-contract)
=== Hosting Setup
Your public directory is the folder (relative to your project directory) that
will contain Hosting assets to be uploaded with firebase deploy. If youhave a build process for your assets, use your build's output directory.
? What do you want to use as your public directory? client/build
? Configure as a single-page app (rewrite all urls to /index.html)? Yes
✔Wrote client/build/index.html
iWriting configuration info to firebase.json...
iWriting project information to .firebaserc...
iWriting gitignore file to .gitignore...
✔Firebase initialization complete!
```

初期化が完了したら、今度はフロントエンドのbuildを行い、ビルドファイルをfirebaseにデプロイします。

```bash

$ cd client && yarn build
(...)
✨Done in 34.31s.
$ cd .. && firebase deploy 
firebase deploy
=== Deploying to 'hello-smart-contract'...
ideploying hosting
ihosting[hello-smart-contract]: beginning deploy...
ihosting[hello-smart-contract]: found 17 files in client/build
✔hosting[hello-smart-contract]: file upload complete
ihosting[hello-smart-contract]: finalizing version...
✔hosting[hello-smart-contract]: version finalized
ihosting[hello-smart-contract]: releasing new version...
✔hosting[hello-smart-contract]: release complete
✔Deploy complete!
Project Console: https://console.firebase.google.com/project/hello-smart-contract/overview
Hosting URL: https://hello-smart-contract.web.app
```

デプロイが完了したらHosting URLにアクセスして、Firebase上でDAppsが動作していること確認できたら、リリース完了です！