緒言
はじめに
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.sol
はtruffle compile
でMetacoin.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.json
のaddress
プロパティに格納されます。コントラクトを生成する際に必要になるので、確認してください。また、truffleを止めるとデプロイしたコントラクトは失われるため、次回以降も再度デプロイする必要があるので注意してください。
{
"networks": {
"4447": {
"events": {},
"links": {
"ConvertLib": "0x345ca3e014aaf5dca488057592ee47305d9b3e10"
},
"address": "0xf25186b5081ff5ce73482ad761db0eb0d25abfbf",
"transactionHash": "0xc48822d7e8b51eb73e1676184f9de5fd966b11025f002fc613192f1c1759caa8"
}
}
}
Web3.jsでデプロイ
Web3.jsでデプロイする場合は、deploy
メソッドを使用します。deploy
メソッドには引数として、コントラクトのバイトコードを渡します。少々面倒なのは、コントラクトに依存関係がある場合 (コントラクト内でimport
やusing
を使っている場合) は、デプロイ順序があることです。たとえば、Metacoin
コントラクトはConvertLib
ライブラリに依存しているので、ConvertLib
ライブラリを先にデプロイする必要があります。また、コンパイル済みのMetacoin.json
のbytecode
を見てみると以下のようになっています。
"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メソッドの違い
コントラクトのデプロイやメソッドの実行するには、call
かsend
メソッドを使用します。両者の違いはコントラクトの状態を変化させるかどうかです。metacoin
コントラクトであれば、デプロイやbalance
の状態を変化させるsendCoin
メソッドは、send
メソッドで実行します。一方、getBalance
メソッドはbalance
の値を返すだけで、コントラクトの状態を変化させません。つまり、pure
なメソッドです。そのため、call
メソッドで呼び出せます。ちなみに、状態を変化させるsend
メソッドの方がgasコストは高くなるので注意してください。参考に、solidityのコントラクトを掲載しておきます。
// メソッド部分以外は省略しています
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
で呼び出します。
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のクラスでカプセル化してしまっているので、利用するコードも載せておきます。
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
は便利。