Skip to content
On this page

L1-15: Build Your Own ETH CLI(独自のETH CLIを構築)

1. 課題

Ethereum コマンドラインツール(CLI)をゼロから構築します。JSON-RPC プロトコルを介して Ethereum ノードと直接対話します。コア機能:残高照会、ブロック情報の取得、トランザクション詳細の照会、現在のガス価格の取得。ethers.js や viem などの高レベルライブラリを使用せず、JSON-RPC リクエストを直接送信します。

2. なぜ重要か

cast(Foundry)、ethers.jsviem などのツールは多くの低レベル詳細をカプセル化しています。CLI をゼロから構築することで、開発者は以下を深く理解できます:

  • JSON-RPC プロトコル:すべての Ethereum ノードは eth_* メソッドを通じてデータを公開します。このプロトコルの理解は dApp やオンチェーンの問題をデバッグするための必須知識です
  • 16進数エンコーディング:Ethereum の戻り値は hex エンコーディング(例:0xde0b6b3a7640000 = 1 ETH)を使用します。hex <-> decimal 変換の理解は基本スキルです
  • 秘密鍵からアドレスへの導出:ECDSA (secp256k1) 公開鍵導出 -> keccak256 ハッシュ -> 末尾20バイトを取得 = アドレス
  • トランザクション構築:nonce、gasPrice、gasLimit、value、data などのフィールドの意味を理解する

これはパーソナルチャレンジ(Personal Challenge)です。目標は本番用ツールを作ることではなく、自らの手で実装することで基盤原理を理解することです。

3. 解決策

アーキテクチャ設計

eth-cli.js
  |
  ├── rpcCall(method, params)      → POST JSON-RPC リクエスト
  ├── cmdBalance(address)          → eth_getBalance
  ├── cmdBlock(tag)                → eth_getBlockByNumber
  ├── cmdTx(hash)                  → eth_getTransactionByHash
  ├── cmdGas()                     → eth_gasPrice
  └── privateKeyToAddress(key)     → 秘密鍵→アドレス導出

コア実装

JSON-RPC 呼び出しラッパー:

javascript
const RPC_URL = process.env.ETH_RPC_URL || "http://localhost:8545";
let requestId = 1;

async function rpcCall(method, params = []) {
  const response = await axios.post(RPC_URL, {
    jsonrpc: "2.0",
    method: method,
    params: params,
    id: requestId++,
  });
  if (response.data.error) throw new Error(response.data.error.message);
  return response.data.result;
}

残高照会:

javascript
async function cmdBalance(address) {
  const balanceWei = await rpcCall("eth_getBalance", [address, "latest"]);
  const weiDecimal = hexToDecimal(balanceWei);
  console.log(`Wei:    ${weiDecimal}`);
  console.log(`Ether:  ${weiToEther(balanceWei)} ETH`);
}

ガス価格照会:

javascript
async function cmdGas() {
  const gasPrice = await rpcCall("eth_gasPrice", []);
  console.log(`Gas Price: ${hexToDecimal(gasPrice)} wei`);
  console.log(`           ${Number(BigInt(gasPrice)) / 1e9} gwei`);
}

サポートコマンド

コマンド説明JSON-RPC メソッド
balance <address>ETH 残高照会eth_getBalance
block [tag|number]ブロック情報照会eth_getBlockByNumber
tx <hash>トランザクション詳細照会eth_getTransactionByHash
gasガス価格照会eth_gasPrice
addr <privateKey>アドレス導出(教育用)ローカル計算

4. 遭遇した落とし穴

4.1 Hex エンコードされた数値の変換

JSON-RPC は数値(残高、ガス、ブロック番号)を hex エンコード文字列(例:0xde0b6b3a7640000)として返します。直接 parseInt(hex) するとオーバーフローします。Ethereum の wei 値は JavaScript の Number.MAX_SAFE_INTEGER を頻繁に超えます。必ず BigInt を使用する必要があります。

4.2 ブロックパラメータの柔軟性

eth_getBlockByNumber"latest""earliest""pending"(文字列タグ)および hex エンコードされたブロック番号(例:0xbc614e)を受け付けます。10進数の数値を渡す場合は、まず hex に変換する必要があります:"0x" + BigInt(tag).toString(16)

4.3 秘密鍵からアドレスへの簡略化された実装

この CLI では SHA-256(keccak256 ではなく)を使用して秘密鍵からアドレスへの導出を簡略化しています。これは Node.js のネイティブ crypto モジュールが keccak256 をサポートしていないためです。実際の Ethereum アドレスは keccak256(publicKey) の末尾20バイトを取得して導出されます。本番環境では ethers.js または viem を使用すべきです。

4.4 トランザクション送信機能なし

安全上の理由から、この CLI にはトランザクション送信機能は含まれていません。トランザクションの送信には、ガスの見積もり、トランザクションへの署名(RLP エンコーディング)、生トランザクションの送信が必要です。これらの操作は秘密鍵の管理を伴うため、CLI でのデモには適していません。

5. 落とし穴の原因

5.1

JavaScript の Number 型は IEEE 754 倍精度浮動小数点数であり、最大安全整数は 2^53 - 1 です。1 ETH = 10^18 wei であるため、少額の ETH 残高でもこの制限をはるかに超えます。BigInt(ES2020+)が唯一の正しい処理方法です。

5.2

JSON-RPC 仕様では、ブロック番号パラメータに hex エンコーディングの使用が要求されます。生の文字列 "12345678" を渡すと、ノードはそれをブロックタグとして解釈します(存在しないためエラーになるか null を返します)。

5.3

Node.js のネイティブ crypto モジュールは SHA-256 をサポートしていますが、keccak256(Ethereum が使用するハッシュアルゴリズム)はサポートしていません。正しい keccak256 の実装には ethers.keccak256() または @noble/hashes ライブラリが必要です。

6. 落とし穴の解決方法

javascript
// 正しい方法:BigInt を使用して hex 値を処理
function hexToDecimal(hex) {
  return BigInt(hex).toString();
}

// 正しい方法:ブロックパラメータを正規化
function normalizeBlockTag(tag) {
  if (/^\d+$/.test(tag)) {
    return "0x" + BigInt(tag).toString(16);
  }
  return tag; // "latest", "earliest", "pending"
}

// 注意:この関数はデモ用に SHA-256 を使用
// 実際のアドレスには keccak256 が必要
function privateKeyToAddress(privateKeyHex) {
  // 本番環境では ethers.js または viem を使用
}

7. 技術的ポイント

ポイント説明
JSON-RPC プロトコル{jsonrpc:"2.0", method, params, id} POST リクエスト
BigInt 処理すべての Ethereum 数値(wei, gas)は BigInt を使用
Hex エンコーディング0x プレフィックス + 16進数、BigInt(hex).toString() で10進数に変換
wei/ETH 変換1 ETH = 10^18 wei、1e18 で割って Ether 値を取得
EIP-1559新しいトランザクションは gasPrice の代わりに maxFeePerGas + maxPriorityFeePerGas を使用
RPC エンドポイントサブスクリプションには WebSocket、リクエスト/レスポンスには HTTP

Built with AiAda