Appearance
L1-15: Build Your Own ETH CLI(独自のETH CLIを構築)
1. 課題
Ethereum コマンドラインツール(CLI)をゼロから構築します。JSON-RPC プロトコルを介して Ethereum ノードと直接対話します。コア機能:残高照会、ブロック情報の取得、トランザクション詳細の照会、現在のガス価格の取得。ethers.js や viem などの高レベルライブラリを使用せず、JSON-RPC リクエストを直接送信します。
2. なぜ重要か
cast(Foundry)、ethers.js、viem などのツールは多くの低レベル詳細をカプセル化しています。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 |