Posted at

Web3.js 0.X.X系統はもう古い! 1.0.0-betaを使ってみた

More than 1 year has passed since last update.


緒言


はじめに

Web3.jsは、etereumと連携して動作するDapp (Distributed Application) を作成するためのJavaScript APIです。記事執筆現在では、0.20がリリースされていますが、1.0.0系統もbeta版として開発されています。1.0.0系統はPromiseを用いた実装になっており、コールバックヘルが起きにくく、await/asyncも使えるので嬉しい限りです。しかも、型定義ファイルも含まれているようなので、TypeScriptやVS Codeでの開発もしやすいです。今から始めるなら、新しい方がいい!! しかし、ほとんどのブログ記事では0.X.X系統で書かれているので、少々情報が不足しています (未完成ながら公式ドキュメントはあります)。せっかくなので、Web3.js 1.0.0に触れた記録を残しておきたいと思います。


環境

今回はtruffleのローカルネットワーク上でmetacoinボックスに含まれるMetacoinコントラクトを操作します。Metacoin.soltruffle compileMetacoin.jsonにコンパイル済みであることを前提に進めます。

なお、Solidityやtruffle等々の話は割愛します。また、コード例はJavaScriptではなく、TypeScriptを用いています (TS最高!!)。完全に同じものではないですが、動くサンプルを見たい方は、こちらを使ってください。


Web3のインスタンス生成

事前にDappからアクセスできるように、ethereumのローカルネットワークを起動しておいてください (truffle developコマンドをコンソールで実行)。そのうえで、web3に接続先のURIを渡してインスタンス生成します (truffleのローカルネットの既定のポートは9545です)。なお、1.0.0系統からはws (websocket)プロトコルでアクセス可能です。

import Web3 from "web3";

const web3 = new Web3("ws://localhost:9545");


コントラクトのデプロイ


truffleでデプロイ

Solidityで作成したコントラクトは、ネットワーク上にデプロイする必要があります (余談ですが、本番環境にデプロイしたコントラクトは永久に削除できないので注意!!)。ローカルネットワークへのデプロイはtruffleで行った方が楽です。metacoinボックスでは設定がされているので簡単なコマンドで実行できます。

truffle develop  # 起動済みの場合は不要

compile # コンパイル
migrate # ローカルネットワークへデプロイ

ちなみに、デプロイされたコントラクトのアドレスはMetaCoin.jsonaddressプロパティに格納されます。コントラクトを生成する際に必要になるので、確認してください。また、truffleを止めるとデプロイしたコントラクトは失われるため、次回以降も再度デプロイする必要があるので注意してください。


MetaCoin.json

{

"networks": {
"4447": {
"events": {},
"links": {
"ConvertLib": "0x345ca3e014aaf5dca488057592ee47305d9b3e10"
},
"address": "0xf25186b5081ff5ce73482ad761db0eb0d25abfbf",
"transactionHash": "0xc48822d7e8b51eb73e1676184f9de5fd966b11025f002fc613192f1c1759caa8"
}
}
}


Web3.jsでデプロイ

Web3.jsでデプロイする場合は、deployメソッドを使用します。deployメソッドには引数として、コントラクトのバイトコードを渡します。少々面倒なのは、コントラクトに依存関係がある場合 (コントラクト内でimportusingを使っている場合) は、デプロイ順序があることです。たとえば、MetacoinコントラクトはConvertLibライブラリに依存しているので、ConvertLibライブラリを先にデプロイする必要があります。また、コンパイル済みのMetacoin.jsonbytecodeを見てみると以下のようになっています。

"bytecode": "0x6060604052341561000f57600080fd5b6127106000803273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020819055506103c5806100636000396000f300606060405260043610610057576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680637bd703e81461005c57806390b98a11146100a9578063f8b2cb4f14610103575b600080fd5b341561006757600080fd5b610093600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610150565b6040518082815260200191505060405180910390f35b34156100b457600080fd5b6100e9600480803573ffffffffffffffffffffffffffffffffffffffff169060200190919080359060200190919050506101f8565b604051808215151515815260200191505060405180910390f35b341561010e57600080fd5b61013a600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610351565b6040518082815260200191505060405180910390f35b600073__ConvertLib____________________________6396e4ee3d61017584610351565b60026000604051602001526040518363ffffffff167c0100000000000000000000000000000000000000000000000000000000028152600401808381526020018281526020019250505060206040518083038186803b15156101d657600080fd5b6102c65a03f415156101e757600080fd5b505050604051805190509050919050565b6000816000803373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020541015610249576000905061034b565b816000803373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008282540392505081905550816000808573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600082825401925050819055508273ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef846040518082815260200191505060405180910390a3600190505b92915050565b60008060008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000205490509190505600a165627a7a72305820ec40ff9d451a379618c92e1c10efe2fac972c74825e3def2225c6ffd724d26bc0029"

横に長いですが、よく見てみるとバイトコード内に__ConvertLib____________________________という部分が含まれています。この部分をConvertLibライブラリのバイトコードで置き換えなくてはなりません。正確には、ConvertLibライブラリのバイトコードのHexプレフィックス (0x) を削除した値で置き換えます (こちらのページが参考になります)。

それでは実際にデプロイしてみます。ソースコードの細かい解説はコメントとして埋め込んでおくので、そちらを参照してください。

// コンパイル済みのSolidityをインポート

import CompiledMetaCoin from "../build/contracts/MetaCoin.json";
import CompiledConverLib from "../build/contracts/ConvertLib.json";
import Web3 from "web3";

const web3 = new Web3("ws://localhost:9545");
var lib: Contract;
var metacoinContract: Contract;
var accounts: string[];

function async deploy() {
// web3.eth.getAccountsメソッドでアカウントのアドレスをstring配列として取得できる
// truffleでは既定で10アカウント作成されている
accounts = await web3.eth.getAccounts();

// ConvertLibライブラリを先にデプロイ
// Contractをインスタンス生成。1つ目の引数にはabiを渡す。
// デプロイ済みの場合は、2つ目の引数としてコントラクトのアドレスを渡せばOK
lib = await new web3.eth.Contract(CompiledConverLib.abi)
// デプロイの設定。dataプロパティにはコンパイル済みのバイトコードを指定
// コントラクトのコンストラクターが引数を取る場合はargumentsプロパティに配列として指定する
.deploy({ data: CompiledConverLib.bytecode, arguments: [] })
// コントラクトをブロックチェーンに組み込む (デプロイ)
.send({
from: accounts[0],
gas: 100000,
gasPrice: 100000
});

metacoinContract = await new web3.eth.Contract(CompiledMetaCoin.abi);
// __ConvertLib____________________________をConvertLibのバイトコードに置換
// bytecodeの0x部分は削除しておく必要がある。
const metaCoinBytecode = CompiledMetaCoin.bytecode.replace(/_+ConvertLib_+/g, CompiledConverLib.bytecode.replace("0x", ""));

metacoinContract = await metacoin.deploy({ data: metaCoinBytecode, arguments: [] })
.send({
from: accounts[0],
gas: 2000000,
gasPrice: 2000000
});

}


Promise

繰り返しになりますがWeb3.js 1.0.0からはPromiseベースの実装になっています。await/asyncを使用しない場合は、thenメソッドで処理をつなげていくことになります。たとえば、アドレスの取得であれば以下のようになります。

web3.eth.getAccounts()

.then((accounts) => {
console.log(accounts);
});


sendメソッドとcallメソッドの違い

コントラクトのデプロイやメソッドの実行するには、callsendメソッドを使用します。両者の違いはコントラクトの状態を変化させるかどうかです。metacoinコントラクトであれば、デプロイやbalanceの状態を変化させるsendCoinメソッドは、sendメソッドで実行します。一方、getBalanceメソッドはbalanceの値を返すだけで、コントラクトの状態を変化させません。つまり、pureなメソッドです。そのため、callメソッドで呼び出せます。ちなみに、状態を変化させるsendメソッドの方がgasコストは高くなるので注意してください。参考に、solidityのコントラクトを掲載しておきます。


Metacoin.sol

// メソッド部分以外は省略しています

contract MetaCoin {
function sendCoin(address receiver, uint amount) public returns(bool sufficient) {
if (balances[msg.sender] < amount) return false;
balances[msg.sender] -= amount;
balances[receiver] += amount;
Transfer(msg.sender, receiver, amount);
return true;
}

function getBalance(address addr) public view returns(uint) {
return balances[addr];
}
}


なお、sendメソッドなどの引数として渡しているオブジェクトのfromプロパティは実行するアカウントのアドレスを、gasプロパティはガスを、gasPriceプロパティはガスの価格を表します。本番環境で実行する場合、コストに直結する部分のため、公式ドキュメントを見ておくことをお勧めします。


コントラクトのメソッド呼び出し

本節では、truffleでコントラクトをデプロイしている前提で話を進めていきます。コントラクトのメソッドは、インスタンス.methods.メソッド名で呼び出すことになります。先述したように、コントラクトの状態変化を伴うメソッド (ここではsendCoin) はsendで、状態変化しないメソッド (getBalance) はcallで呼び出します。


CoinOperator.ts

import Web3 from "web3";

import metaCoin from "../build/contracts/MetaCoin.json";
import { Contract } from "web3/types";

export default class CoinOperator {
private readonly web3: Web3;
private readonly metaCoinContract: Contract;

public constructor() {
this.web3 = new Web3("ws://localhost:9545");
/* Notice: `metaCoin.networks["4447"].address` return the address when truffle deployed the contract.
* 4447 is default network id of truffle develop.
* for more detail, see `../buid/contract/MetaCoin.json`.
* */

this.metaCoinContract = new this.web3.eth.Contract(metaCoin.abi, metaCoin.networks["4447"].address);
}

/**
* return all accounts. truffle has 10 accounts by default.
*/

public getAllAccounts() {
return this.web3.eth.getAccounts();
}

/**
* get the balance of methacoin.
* @param address address to get the balance of methacoin.
*/

public getBalance(address: string): Promise<number> {
return this.metaCoinContract.methods.getBalance(address)
.call({ from: address });
}

/**
* get the balance of methacoin in eth.
* @param address address to get the balance of methacoin.
*/

public getBalanceInEth(address: string): Promise<number> {
return this.metaCoinContract.methods.getBalanceInEth(address)
.call({ from: address });
}

public sendCoin(fromAddress: string, toAddress: string, amount: number): Promise<boolean> {
return this.metaCoinContract.methods.sendCoin(toAddress, amount)
.send({ from: fromAddress });
}
}



コントラクトの生成

前節でも少々触れましたが、デプロイ済みのコントラクトの場合、this.metaCoinContract = new this.web3.eth.Contract(metaCoin.abi, "0xf25186b5081ff5ce73482ad761db0eb0d25abfbf")のように第2引数にコントラクトのアドレスを指定して使用します(サンプルコードでは、truffleでコンパイルしたアドレスをJSONファイルから取得しています)。


CoinOperatorクラスの使用

メソッドをTypeScriptのクラスでカプセル化してしまっているので、利用するコードも載せておきます。


index.ts

import ConinOperator from "./ConinOperator";

const coinOperator = new ConinOperator();

window.onload = async () => {
const accounts = await web3.eth.getAccounts();
// balanceの取得
const balance = await coinOperator.getBalance(accounts[0]);
console.log("balance:" + balance.toString());
}



最後に

大したことはやっていないコードですが、Web3.js 1.0.0はまだ情報が少ないのでお役に立てれば幸いです。本筋と関係ありませんが、やっぱりawait/asyncは便利。