Pour les développeurs / JSON-RPC

Envoyer une transaction

Envoyer est ordinaire. Lire le résultat ne l'est pas : un reçu existe avant le bloc, et un revert arrive sous forme de texte que votre bibliothèque ne peut pas décoder.

Trois choses qui peuvent être vraies

Sur la plupart des chaînes, « envoyée », « minée » et « finale » s'effondrent en une seule attente. Ici ce sont trois événements distincts, et savoir lequel vous testez fait l'essentiel de l'écriture d'un client correct :

  • Exécutée. eth_sendRawTransaction bloque jusqu'à ce que la transaction ait réellement tourné, donc un retour réussi signifie déjà exécutée - pas mise en file. Un revert est connu à ce point.
  • Préconfirmée. Dans la cible d'ordonnancement du mini-bloc, un reçu existe. Ses blockNumber et blockHash valent encore null.
  • Confirmée. À la cible d'ordonnancement du bloc EVM, le bloc se scelle et ces deux champs se remplissent.

Attendre un numéro de bloc, c'est attendre la mauvaise chose

Une bibliothèque qui sonde jusqu'à ce que receipt.blockNumber soit non nul attend l'étage de confirmation alors que la réponse qu'elle veut - est-ce que ça a réussi - était disponible à l'étage de préconfirmation. Ce n'est pas un bug, mais cela veut dire que vous attendez plus longtemps que nécessaire. Lisez status dès que le reçu existe.

La finalité est une quatrième chose, distincte : elle survient quand le lot contenant se règle sur Ethereum, et n'est pas du tout observable à travers ces champs. Les étiquettes de bloc n'aident pas non plus - safe et finalized se résolvent toutes deux vers latest.

Envoyer

Signez localement et envoyez en brut. Le nœud ne détient aucune clé, donc eth_sendTransaction et toutes les méthodes de signature répondent -32601 par conception.

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

Ou contre un fournisseur EIP-1193 brut, sans aucune bibliothèque. Notez qui signe : ici eth_sendTransaction est traité par le portefeuille, qui signe puis soumet la transaction brute à votre place - le nœud, lui, refuse cette méthode.

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" }],
});

Lire le reçu

Ici un reçu manquant est un problème, pas de la patience

Un null de eth_getTransactionReceipt signifie que le nœud n'a aucun reçu pour ce hash - et sur cette chaîne il en existe normalement un dans la cible d'ordonnancement du mini-bloc, donc un null après une seconde ou deux est un signal pour enquêter plutôt que pour continuer d'attendre.

C'est aussi ambigu par conception : un reçu évincé de la fenêtre répond null exactement comme un reçu qui n'a jamais existé. C'est pourquoi un long délai de reprise est la mauvaise forme ici - attendez trop entre deux tentatives et le reçu peut disparaître entre les deux.

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.

Avec viem, waitForTransactionReceipt fait cela pour vous - mais réglez pollingInterval bas et n'augmentez pas son délai d'expiration en supposant qu'attendre plus longtemps est plus sûr. Ici, ce ne l'est pas.

Deux champs de ce reçu ne veulent pas dire ce qu'ils veulent dire ailleurs. cumulativeGasUsed est le gas propre de cette transaction plutôt qu'un cumul courant du bloc, et le logIndex de chaque log est énuméré par transaction plutôt que par bloc - donc (blockNumber, logIndex) n'est pas une clé unique. Incluez le hash de la transaction si vous stockez des événements.

Quand ça revert

Votre bibliothèque ne peut pas décoder la raison

Un revert revient sous forme de texte de message - execution reverted: 0x… - et error.data n'est jamais renseigné. Toutes les bibliothèques décodent les erreurs personnalisées et les chaînes de revert depuis error.data, donc sur cette chaîne aucune ne le peut : viem comme ethers feront remonter le message brut au lieu d'une erreur nommée.

Les données de retour sont dans le message, vous pouvez donc les décoder vous-même :

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

Une dernière différence de transport bonne à connaître : le même revert est -32000 en HTTP et -32603 sur le WebSocket, parce que le socket réencapsule toute erreur qui n'est pas un envoi. Un client qui se branche sur le code doit savoir sur quel transport il se trouve.

Le gas

Fixez gasPrice explicitement à 1 wei. Il n'y a pas de marché des frais contre lequel estimer : la base fee est nulle, eth_gasPrice répond un wei, et un wei est aussi le minimum que la chaîne acceptera.

eth_estimateGas fonctionne et simule correctement, en ajoutant la marge habituelle - mais il ignore les paramètres gas, gasPrice, nonce et de bloc que vous lui envoyez, et simule toujours à la limite de 30 000 000 par bloc contre l'état le plus récent. Et ne lisez pas eth_feeHistory : il renvoie des constantes plutôt que des mesures.

Deux limites qui remontent en -32005

Une transaction brute de plus de 128 Kio est rejetée sur sa taille avant même d'être décodée, et la file d'admission retient 64 transactions en attente par émetteur. Les deux répondent -32005, qui signifie une limite plutôt qu'une erreur - temporisez et réessayez plutôt que d'y voir un échec.