reactjs
React
solidity
geth
ethers.js

ethers.js(+ React)を試してみた

はじめに

前回の続きで、ethers.jsを試してみたいと思います。(ついでにReactと連携)
手探りで書いてますので、間違っている箇所があればご指摘頂ければなと思います。

前提

  • Mac
  • gethでのプライベートネット構築済み
  • npmインストール済み

前回の変更点

プロジェクト名の変更(前回作成したtry-gethをtry-ethに変更)

ターミナル
$ cd $HOME
$ mv try-geth try-eth && cd try-eth

ethers.jsを使ってみる

ethers.jsとは

クライアントサイドでetherを扱うJavaScriptライブラリ?

準備

初期化とethersのインストール

ターミナル
$ npm init -y
$ npm i ethers -S

ES6実行用にbabelをインストール

ターミナル
$ npm i babel-cli babel-preset-env -D
$ touch .babelrc
.babelrc
{
  "presets": ["env"]
}

ethers.js実行用のファイル作成

ターミナル
$ mkdir src src/lib && touch src/lib/ether-api.js

ブロックの数を取得

スクリプトを作成

ether-api.js
import Ethers from  "ethers";

const providers = Ethers.providers;
const network = providers.networks.ropsten;
const provider = new providers.JsonRpcProvider("http://localhost:8545", network);

async function main() {
  const blockNumber = await provider.getBlockNumber().then(function(blockNumber) {
    return blockNumber;
  }).catch(function(e) {
    throw new Error("failed getBlockNumber Function => " + e);
  });
  console.log(blockNumber);
}

main();

gethの起動

ターミナル
$ geth --rpc --rpcport 8545 --rpcapi "eth,net,web3,personal" --rpccorsdomain "*" --rpcaddr "0.0.0.0" --networkid "15" --nodiscover --datadir $HOME/try-eth/private_net/ console 2>> $HOME/try-eth/private_net/error.log

実行(別ウィンドウで)

ターミナル
$ babel-node ./src/lib/ether-api.js
>ブロック数

contractを試してみる

Solidityのcompilerをインストールとcontract用のファイルを作成

ターミナル
$ npm i solc -D

$ solcjs --help
Usage: /Users/{UserName}/try-eth/node_modules/.bin/solcjs [options] [input_file...]

オプション:
  --version         バージョンを表示                                      [真偽]
  --optimize        Enable bytecode optimizer.                            [真偽]
  --bin             Binary of the contracts in hex.                       [真偽]
  --abi             ABI of the contracts.                                 [真偽]
  --standard-json   Turn on Standard JSON Input / Output mode.            [真偽]
  --output-dir, -o  Output directory for the contracts.                 [文字列]
  --help            ヘルプを表示                                          [真偽]

$ solcjs --version
0.4.21+commit.dfe3193c.Emscripten.clang

$ mkdir src/contract && touch src/contract/HelloWorld.sol
HelloWorld.sol
pragma solidity ^0.4.19;

contract HelloWorld {
    function Hello() external pure returns (string) {
        return "Hello World";
    }
}

以下のコマンドでコンパイルすると、contractフォルダにabiファイルとbinファイルが生成されます

ターミナル
$ solcjs src/contract/HelloWorld.sol --abi --bin --output-dir ./src/contract

デプロイ

  • bin: "0x" + 先ほどのコンパイル後の.binファイルの中身
  • abi: 先ほどのコンパイル後の.abiファイルの中身
ターミナル
> bin = "0x6060604052341561000f57600080fd5b6101578061001e6000396000f300606060405260043610610041576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff168063bcdfe0d514610046575b600080fd5b341561005157600080fd5b6100596100d4565b6040518080602001828103825283818151815260200191508051906020019080838360005b8381101561009957808201518184015260208101905061007e565b50505050905090810190601f1680156100c65780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b6100dc610117565b6040805190810160405280600b81526020017f48656c6c6f20576f726c64000000000000000000000000000000000000000000815250905090565b6020604051908101604052806000815250905600a165627a7a723058208b70143c11ea4a2766be338da90ecb12082b81a2dd1b6e9ae9b09608c05097790029"

> var abi = [{"constant":true,"inputs":[],"name":"Hello","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"pure","type":"function"}]

> contract = eth.contract(abi)

> Hello = contract.new({ from: eth.accounts[0], data: bin, gas: 1000000 })
{
  abi: [{
      constant: true,
      inputs: [],
      name: "Hello",
      outputs: [{...}],
      payable: false,
      stateMutability: "pure",
      type: "function"
  }],
  address: undefined,
  transactionHash: "0x52220fa124fe1a4187676c9c62dcfcf0def2e177d708d10af6276b8f42393d73"
}

マイニング後に、確認

ターミナル
> miner.start()
null

> miner.stop()
true

> Hello.Hello()
"Hello World"

ethers.jsでcontractを実行してみる

ether-api.js
import Ethers from  "ethers";

const abi = [{"constant":true,"inputs":[],"name":"Hello","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"pure","type":"function"}];
const contractAdress = "0x9d73f0f2cabf29a74ce748aea5403d7ca749a3cb";

const providers = Ethers.providers;
const network = providers.networks.ropsten;
const provider = new providers.JsonRpcProvider("http://localhost:8545", network);

const contract = new Ethers.Contract(contractAdress, abi, provider);

async function main() {
  const result = await contract.Hello().then(function(result) {
    return result;
  }).catch(function(e) {
    throw new Error("failed Hello Function => " + e);
  });
  console.log(result);
}

main();

実行

ターミナル
$ babel-node ./src/lib/ether-api.js
Hello World

Reactの導入

webpackを使用します。

パッケージのインストールとpackage.jsonにタスクを追加

以下を参考に、各パッケージのインストールとscriptsの追加

  • devDependenciesのインストール: npm i パッケージ名 -D
  • dependenciesのインストール: npm i パッケージ名 -S
package.json
{
  "name": "try-eth",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "private": true,
  "dependencies": {
    "ethers": "^3.0.7",
    "react": "^16.2.0",
    "react-dom": "^16.2.0"
  },
  "devDependencies": {
    "babel-cli": "^6.26.0",
    "babel-core": "^6.26.0",
    "babel-loader": "^7.1.4",
    "babel-plugin-transform-runtime": "^6.23.0",
    "babel-preset-env": "^1.6.1",
    "babel-preset-react": "^6.24.1",
    "solc": "^0.4.21",
    "webpack": "^4.2.0",
    "webpack-cli": "^2.0.13",
    "webpack-dev-server": "^3.1.1"
  },
  "scripts": {
    "build:dev": "webpack --mode development",
    "build:prd": "webpack --mode production",
    "start": "webpack-dev-server --mode development"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

準備

  • webpack.config.jsの作成
  • .babelrcの変更
webpack.config.js
const path = require('path');

module.exports = {
  entry: './src/index.jsx',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'bundle.js',
  },
  devServer: {
    contentBase: path.resolve(__dirname, 'dist'),
    host: '0.0.0.0',
    port: '8080',
    open: true,
  },
  module: {
    rules: [
      {
        test: /\.js(x)$/,
        use: [
          {
            loader: 'babel-loader',
          }
        ],
        exclude: /node_modules/
      }
    ]
  },
  resolve: {
    extensions: ['.js', '.jsx'],
  },
  plugins: []
};
.babelrc
{
  "presets": ["env", "react"],
  "plugins": [
    ["transform-runtime", {
      "polyfill": false,
      "regenerator": true
    }]
  ]
}

src配下のファイルを作成

以下を参考に各ファイルを作成

ターミナル
$ tree src
src
├── component
│   ├── app.jsx
│   ├── contract-item.jsx
│   └── contract-list.jsx
├── contract
│   ├── HelloWorld.sol
│   ├── src_contract_HelloWorld_sol_HelloWorld.abi
│   └── src_contract_HelloWorld_sol_HelloWorld.bin
├── data
│   └── constans.js
├── index.jsx
└── lib
    └── ether-api.js
index.jsx
import React from 'react';
import ReactDOM from 'react-dom';

import APP from './component/app';

ReactDOM.render(
  <APP />,
  document.getElementById('root')
);
app.jsx
import React from 'react';

import EtherAPI from '../lib/ether-api';
import Constans from '../data/constans';

import ContractList from './contract-list';

const eth = new EtherAPI();

function initState(contractLists) {
  const defaultState = {
    "contractLists": [],
    "counter": 0
  };

  for(let i in contractLists) {
    let contractItem = {
      "id": i,
      "name": contractLists[i].name,
      "funcName": contractLists[i].funcName,
      "adress": contractLists[i].adress,
      "abi": contractLists[i].abi,
      "result": "",
      "display": true
    }
    defaultState.contractLists.push(contractItem);
    defaultState.counter++;
  }

  return defaultState;
}

export default class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = initState(Constans.contractLists);
    this.handleClickContract = this.handleClickContract.bind(this);
  }

  async handleClickContract (index, funcName) {
    const result = await eth.execContract(this.state.contractLists[index].adress, this.state.contractLists[index].abi, funcName);
    const updateState = this.state;
    updateState.contractLists[index].result = result;
    this.setState({updateState});
  }

  render() {
    return (
    <React.Fragment>
      <ContractList contractLists={this.state.contractLists} handleClickContract={this.handleClickContract} />
    </React.Fragment>
    );
  }
}
contract-list.jsx
import React from 'react';

import ContractItem from './contract-item';

export default props => {
  const {contractLists, handleClickContract} = props;
  return (
    <ul>
      {contractLists.map(contractItem => {
        return <ContractItem {...contractItem} key={contractItem.id} handleClickContract={handleClickContract} />;
      })}
    </ul>
  );
}

handleClickContract関数に渡すfuncNameを元に、api側は実行する関数を分岐します

contract-item.jsx
import React from 'react';

export default props => {
  const {id, funcName, adress, result, handleClickContract} = props;
  return(
    <React.Fragment>
      <li>アドレス; {adress}</li>
      <li>実行結果 {result}</li>
      <button onClick={() => handleClickContract(id, funcName)}>コントラクトを実行する</button>
    </React.Fragment>
  );
}

各ネットワークの情報、コントラクトのリストを持つオブジェクト
(コントラクトの情報は、ファイルで持たせず動的に取得したほうがよいが、今回は簡易的な確認のためオブジェクトとして持たしてます)

今回は、すべて同じコントラクトのアドレスですが、別アドレスのコントラクトや同じコントラクトの別関数を実行できるようにしています。

constans.js
const Constans = {
  private_network: "http://localhost:8545",
  test_network: "",
  main_network: "",
  contractLists: [
    {
      name: "HelloContract",
      funcName: "Hello",
      adress: "0x9d73f0f2cabf29a74ce748aea5403d7ca749a3cb",
      abi: [{"constant":true,"inputs":[],"name":"Hello","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"pure","type":"function"}]
    },
    {
      name: "demoContract",
      funcName: "demo",
      adress: "0x9d73f0f2cabf29a74ce748aea5403d7ca749a3cb",
      abi: [{"constant":true,"inputs":[],"name":"Hello","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"pure","type":"function"}]
    },
    {
      name: "demoContract",
      funcName: "demo2",
      adress: "0x9d73f0f2cabf29a74ce748aea5403d7ca749a3cb",
      abi: [{"constant":true,"inputs":[],"name":"Hello","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"pure","type":"function"}]
    }
  ]
}

export default Constans;

(なるべくコンポーネントとEthersのロジックを分離をしたかったので)コンポーネント側で実行されるexecContract関数のfuncName引数を元に、実行するコントラクト関数をswitchで分岐しています。

また、Contractのインスタンス化する際に、第3引数にはwalletを受け取る可能性があります。(今回は、Etherのやり取りが発生しないContractのため、providerを渡しているが、もしやり取りが発生する場合はEtherAPIクラスにwalletインスタンスを返すメソッドを追加し、それをexecContract関数に渡せばよいかなと思います)

ether-api.js
import Ethers from  'ethers';

import Constans from '../data/constans';

const providers = Ethers.providers;
const network = providers.networks.ropsten;
const provider = new providers.JsonRpcProvider(Constans.private_network, network);

export default class EtherAPI {
  constructor(){
    this._provider = provider;
  }

  // コントラクトの実行
  async execContract(contractAdress, abi, funcName, provider = this._provider){
    const contract = new Ethers.Contract(contractAdress, abi, provider);
    let result = "null";
    switch (funcName) {
      case "Hello":
        result = await contract.Hello().then(function(result) {
          return result;
        }).catch(function(e) {
          throw new Error("failed " + funcName + " function => " + e);
        });
        break;
      case "demo":
        result = "exec from demo method";
        break;
      case "demo2":
        result = "exec from demo2 method";
        break;
      default:
        result = ""
    }
    return result;
  }

}

ローカルで確認

ターミナル
$ pwd
/Users/{UserName}/try-eth

$ mkdir dist && touch dist/index.html
index.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Try Ether</title>
</head>
<body>
    <div id="root"></div>
    <script src="bundle.js"></script>
</body>
</html>

gethを起動した状態でサーバーを起動

ターミナル
$ npm start

パッケージのバージョン

$ npm -v
5.6.0

$ npm ls --depth=0
try-eth@1.0.0
├── babel-cli@6.26.0
├── babel-core@6.26.0
├── babel-loader@7.1.4
├── babel-plugin-transform-runtime@6.23.0
├── babel-preset-env@1.6.1
├── babel-preset-react@6.24.1
├── ethers@3.0.7
├── react@16.2.0
├── react-dom@16.2.0
├── solc@0.4.21
├── webpack@4.2.0
├── webpack-cli@2.0.13
└── webpack-dev-server@3.1.1

おわり

今回は、文字列を返すだけの簡単なContractの実装でしたが、今後はより複雑なContractの実装及びWalletとの連携らへんを深めていきたいと思いました。

参考資料

https://docs.ethers.io/ethers.js/html/
https://qiita.com/SotaOishi/items/d6ab0f8ad180d3a7982f