json-rpc-engine と eth-json-rpc-middleware を使って web3.js の provider を作る
昔は web3.js の provider を作るというと web3-provider-engine を使うのが一般的でした
いつの間にかこのモジュールは積極的にメンテナンスされなくなっていて、今は json-rpc-engine と eth-json-rpc-middleware を使うのが主流みたいなので、この2つを使って web3.js の provider を作ってみます
リポジトリ
それぞれのモジュールの役割
json-rpc-engine
json-rpc-engine は MetaMask や web3.js などといった Ethereum の文脈を持たないモジュールです。純粋に JSON RPC を処理するモジュールを実装するために必要な機能を持ったモジュールです。
処理の内容はミドルウェアとして機能を拡張しながら追加することができます。
const { createAsyncMiddleware } = require('json-rpc-engine');
let engine = new RpcEngine();
engine.push(
createAsyncMiddleware(async (req, res, next) => {
res.result = 42;
next();
}),
);
また createScaffoldMiddleware
は非常に便利なミドルウェアを作るための関数です。これは JSON PRC のメソッド名に対応するミドルウェアを key-value で渡すことで、あるメソッドに対応する処理を宣言的に書くことができます。
eth-json-rpc-middleware
eth-json-rpc-middleware は json-rpc-engine を使って web3.js の provider を作成するための関数やミドルウェアが揃っています。
例えば https://github.com/MetaMask/eth-json-rpc-middleware/blob/main/src/fetch.ts は、JSON RPC のリクエストを指定した rpcUrl にプロキシするためのモジュールです。web3.js の provider は eth_blockNumber
などのメソッドは、Ethereum のノードに問い合わせる必要があります。このミドルウェアを使うことでこの処理を実現可能です。
また https://github.com/MetaMask/eth-json-rpc-middleware/blob/main/src/wallet.ts を使うと、provider を用いた署名処理を簡単に実装することができます。createWalletMiddleware
の引数に getAccounts
の実装を渡すことで、JSON RPC の eth_accounts
メソッド呼び出しに対応する実装が可能になります。
createFetchMiddleware を使ってみる
createFetchMiddleware
を使って、各種 read 系のメソッドを呼び出せる provider を実装してみます。
コードの全容は次の通りです。
function createSampleProvider(rpcUrl: string) {
const engine = new JsonRpcEngine();
engine.push(createFetchMiddleware({ rpcUrl }));
return providerFromEngine(engine);
}
async function main(): Promise<void> {
const rpcUrl = process.env.RINKEBY_URL;
if (!rpcUrl) {
throw new Error("RINKEBY_URL is undefiend");
}
const sampleProvider = createSampleProvider(rpcUrl);
const web3 = new Web3(sampleProvider as any);
console.log(await web3.eth.getBlockNumber());
console.log(await web3.eth.net.getId());
}
engine.push(createFetchMiddleware({ rpcUrl }));
を実装するだけで web3.eth.getBlockNumber
や web3.eth.net.getId
を呼び出せるようになりました。
eth_accounts
を実装する
createWalletMiddleware を使って 先程のコード例に加えて、eth_accounts
を実装してみます。
function createSampleProvider(rpcUrl: string, privateKey: string) {
const engine = new JsonRpcEngine();
engine.push(
createWalletMiddleware({
getAccounts: async () => {
const publicKey = publicKeyCreate(
Buffer.from(privateKey.substring(2), "hex"),
false
).slice(1);
const address = create("keccak256")
.update(Buffer.from(publicKey))
.digest()
.slice(12, 32);
return Promise.resolve([`0x${address.toString("hex")}`]);
},
})
);
engine.push(createFetchMiddleware({ rpcUrl }));
return providerFromEngine(engine);
}
async function main(): Promise<void> {
const rpcUrl = process.env.RINKEBY_URL;
const privateKey = process.env.PRIVATE_KEY;
if (!rpcUrl) {
throw new Error("RINKEBY_URL is undefiend");
}
if (!privateKey) {
throw new Error("PRIVATE_KEY is undefiend");
}
const sampleProvider = createSampleProvider(rpcUrl, privateKey);
const web3 = new Web3(sampleProvider as any);
console.log(await web3.eth.getBlockNumber());
console.log(await web3.eth.getAccounts());
}
createWalletMiddleware
の引数の getAccounts
を実装する必要があります。
このメソッドを実装すると web3.eth.getAccounts
を呼び出せるようになります。
processTransaction
はだいたいこんな感じのコードを書けば動きます
gasLimit が 21000 固定だったりしてコントラクトの呼び出しはできないので、あくまで PoC 用のコードです
import fetch from "node-fetch";
import { JsonRpcEngine } from "json-rpc-engine";
import {
createFetchMiddleware,
createWalletMiddleware,
providerFromEngine,
createFetchConfigFromReq,
} from "eth-json-rpc-middleware";
import { publicKeyCreate } from "secp256k1";
import create from "keccak";
import { TransactionFactory } from "@ethereumjs/tx";
import Common from "@ethereumjs/common";
import Web3 from "web3";
async function query(rpcUrl: string, method: string, params: string[]) {
const { fetchUrl, fetchParams } = createFetchConfigFromReq({
rpcUrl,
req: {
method,
params,
id: 42,
},
});
const response = await fetch(fetchUrl, fetchParams);
console.log({ response });
const body = await response.json();
if (body.error) {
return Promise.reject(body.error);
}
return body.result;
}
function createSampleProvider(rpcUrl: string, privateKey: string) {
const engine = new JsonRpcEngine();
engine.push(
createWalletMiddleware({
getAccounts: async () => {
const publicKey = publicKeyCreate(
Buffer.from(privateKey.substring(2), "hex"),
false
).slice(1);
const address = create("keccak256")
.update(Buffer.from(publicKey))
.digest()
.slice(12, 32);
return Promise.resolve([`0x${address.toString("hex")}`]);
},
async processTransaction(txParams, req) {
console.log({ txParams, params: req.params });
const common = new Common({ chain: "rinkeby" });
const gasPrice = await query(rpcUrl, "eth_gasPrice", []);
const gas = await query(rpcUrl, "eth_estimateGas", [txParams] as any);
const nonce = await query(rpcUrl, "eth_getTransactionCount", [
txParams.from,
"pending",
]);
const tx = TransactionFactory.fromTxData(
{ gasPrice, gas, nonce, gasLimit: 21000, ...txParams } as any,
{
common,
}
);
console.log({ tx });
const signedTx = tx.sign(Buffer.from(privateKey.substring(2), "hex"));
const data = `0x${signedTx.serialize().toString("hex")}`;
const hash = await query(rpcUrl, "eth_sendRawTransaction", [data]);
console.log({ hash });
return `${hash}` as any;
},
})
);
engine.push(createFetchMiddleware({ rpcUrl }));
return providerFromEngine(engine);
}
本番用にちゃんとコードを書くなら
- gasLimit を設定する middleware を書く
- nonce を設定する middleware を書く
- gasPrice を設定する middleware を書く
などが必要になってきます
processTransaction
の中で nonce の設定をやっちゃうより、middleware に切り出すほうがテストしやすい構造になるので、この方針のほうが筋が良いはず