開発者へ / JSON-RPC

トランザクションを送る

送ることは普通です。結果を読むことは普通ではありません。レシートはブロックより先に存在し、revert はあなたのライブラリがデコードできないテキストとして届きます。

同時に真でありうる三つのこと

たいていのチェーンでは「送った」「採掘された」「確定した」が一つの待ちに潰れます。ここでは それが三つの別々の出来事であり、自分がどれを検査しているかを知ることが、正しい クライアントを書くことのほとんどです。

  • 実行済み。eth_sendRawTransaction は トランザクションが実際に走り終わるまでブロックするので、正常に戻った時点ですでに 実行済みを意味します。キューに入っただけではありません。revert はこの時点で判明します。
  • 事前確認済み。ミニブロックのスケジューリング目標のうちに、レシートが 存在します。その blockNumber と blockHash はまだ null です。
  • 確認済み。EVM ブロックのスケジューリング目標でブロックが封印され、 その二つのフィールドが埋まります。

ブロック番号を待つのは、間違ったものを待つことです

receipt.blockNumber が null でなくなるまでポーリングするライブラリは、 欲しい答え、すなわち成功したかどうかが事前確認の段ですでに得られていたのに、確認の段を 待っています。バグではありませんが、必要より長く待っていることになります。レシートが 存在したらすぐ status を読んでください。

ファイナリティは四つ目の、別のものです。それを含むバッチが Ethereum 上で決済されたときに 起こり、これらのフィールドからはまったく観測できません。ブロックのタグも助けになりません。safe と finalized はどちらも latest に解決されます。

送る

ローカルで署名し、生のまま送ってください。ノードは鍵を一つも持たないので、eth_sendTransaction とすべての署名メソッドは設計上 -32601 を返します。

typescript
import { createWalletClient, custom } from "viem";
import { pickleChain } from "./pickle";

const wallet = createWalletClient({ chain: pickleChain, transport: custom(window.ethereum) });

const hash = await wallet.sendTransaction({
  to: recipient,
  value: 10n ** 16n,
  // Explicit, because there is no fee market to estimate from: the base fee is
  // zero and the minimum accepted price is one wei.
  gasPrice: 1n,
});

あるいは、ライブラリをまったく使わずに素の EIP-1193 プロバイダーに対して。どちらの側が 署名するかに注意してください。ここでの eth_sendTransaction はウォレットが処理し、署名してから生のトランザクションを代わりに提出します。 ノード自身はこのメソッドを拒否します。

javascript
const [account] = await window.ethereum.request({ method: "eth_requestAccounts" });

const hash = await window.ethereum.request({
  method: "eth_sendTransaction",
  params: [{ from: account, to: recipient, value: "0x2386f26fc10000", gasPrice: "0x1" }],
});

レシートを読む

ここでレシートがないのは、辛抱の問題ではなく問題です

eth_getTransactionReceipt からの null は、そのハッシュに 対するレシートをノードが持っていないという意味です。このチェーンでは通常、ミニブロックの スケジューリング目標のうちに一つ存在するので、1 秒か 2 秒たっての null は、待ち続ける 合図ではなく調べる合図です。

これは設計上あいまいでもあります。ウィンドウから追い出されたレシートは、一度も 存在しなかったレシートとまったく同じように null を返します。だから長い バックオフはここでは形が違うのです。試行の間を空けすぎると、その二回の間にレシートが 消えることがあります。

javascript
// Poll early and tightly: the receipt exists almost immediately, and a long
// backoff risks the window evicting it between two attempts.
async function receiptFor(hash, tries = 40) {
  for (let i = 0; i < tries; i += 1) {
    const receipt = await call("eth_getTransactionReceipt", [hash]);
    if (receipt) return receipt;
    await new Promise((r) => setTimeout(r, 100));
  }
  throw new Error("no receipt after 4s - check the node, not the transaction");
}

const receipt = await receiptFor(hash);

// "0x1" succeeded, "0x0" reverted. Both are known before the block seals.
if (receipt.status !== "0x1") throw new Error("transaction reverted");

// Only if you actually need the block: blockNumber is null until the EVM seal.

viem なら waitForTransactionReceipt がこれをやってくれます。ただし pollingInterval は低く設定し、長く待つほうが安全だという前提でタイムアウトを 伸ばさないでください。ここではそうではありません。

そのレシートの二つのフィールドは、ほかの場所での意味とは違います。cumulativeGasUsed はブロックの累計ではなくこのトランザクション自身のガスであり、 各ログの logIndex はブロックごとではなくトランザクションごとに採番されます。 つまり (blockNumber, logIndex) は一意なキーではありません。イベントを 保存するなら、トランザクションハッシュを含めてください。

revert したとき

あなたのライブラリは理由をデコードできません

revert はメッセージのテキストとして返り、execution reverted: 0x… の形を 取ります。そして error.data は決して埋められません。どのライブラリも カスタムエラーと revert の文字列を error.data からデコードするので、この チェーンではどれもできません。viem も ethers も、名前の付いたエラーではなく生の メッセージを浮かび上がらせます。

戻りデータはメッセージの中にあるので、自分でデコードできます。

typescript
import { decodeErrorResult } from "viem";

try {
  await client.simulateContract({ /* … */ });
} catch (err) {
  const hex = String(err.message).match(/0x[0-9a-fA-F]+/)?.[0];
  if (hex && hex.length > 2) {
    // Decode against your own ABI's error definitions.
    const decoded = decodeErrorResult({ abi: myAbi, data: hex as `0x${string}` });
    console.error(decoded.errorName, decoded.args);
  }
}

もう一つ知っておくとよいトランスポートの違いがあります。同じ revert が HTTP では -32000、WebSocket では -32603 になります。ソケットが送信以外の すべてのエラーを包み直すからです。コードで分岐するクライアントは、自分がどちらの トランスポート上にいるかを知っている必要があります。

ガス

gasPrice は明示的に 1 wei に設定してください。見積もりの 対象になる手数料市場はありません。base fee はゼロ、eth_gasPrice は 1 wei を 返し、1 wei はチェーンが受け付ける最小でもあります。

eth_estimateGas は動き、いつもの余裕を足して正しくシミュレートします。ただし 送られた gas、gasPrice、nonce、ブロックの各 パラメータを無視し、常にブロック上限の 30,000,000 で最新の状態に対してシミュレートします。 そして eth_feeHistory は読まないでください。それは測定ではなく定数を返します。

-32005 として表に出る二つの制限

128 KiB を超える生のトランザクションは、デコードされる前にサイズで拒否されます。また 受付キューは送信者ごとに 64 件の待機トランザクションを保持します。どちらも -32005 を返し、これは間違いではなく制限を意味します。失敗と見なすのではなく、バックオフして再試行してください。