Closed6

json-rpc-engine と eth-json-rpc-middleware を使って web3.js の provider を作る

odanodan

それぞれのモジュールの役割

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 メソッド呼び出しに対応する実装が可能になります。

odanodan

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.getBlockNumberweb3.eth.net.getId を呼び出せるようになりました。

odanodan

createWalletMiddleware を使って eth_accounts を実装する

先程のコード例に加えて、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 を呼び出せるようになります。

odanodan

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);
}
odanodan

本番用にちゃんとコードを書くなら

  • gasLimit を設定する middleware を書く
  • nonce を設定する middleware を書く
  • gasPrice を設定する middleware を書く

などが必要になってきます

processTransaction の中で nonce の設定をやっちゃうより、middleware に切り出すほうがテストしやすい構造になるので、この方針のほうが筋が良いはず

このスクラップは2022/02/07にクローズされました