Para desenvolvedores / JSON-RPC

Enviar uma transação

Enviar é comum. Ler o resultado não é: um recibo existe antes do bloco, e um revert chega como um texto que sua biblioteca não consegue decodificar.

Três coisas que podem ser verdadeiras

Na maioria das cadeias, "enviada", "minerada" e "final" colapsam numa só espera. Aqui são três eventos distintos, e saber qual deles você está testando é a maior parte de escrever um cliente correto:

  • Executada. eth_sendRawTransaction bloqueia até a transação ter realmente rodado, portanto um retorno bem-sucedido já significa executada - e não enfileirada. Um revert é conhecido neste ponto.
  • Pré-confirmada. Dentro da meta de agendamento do minibloco, um recibo existe. Seus blockNumber e blockHash ainda valem null.
  • Confirmada. Na meta de agendamento do bloco EVM, o bloco sela e esses dois campos se preenchem.

Esperar por um número de bloco é esperar pela coisa errada

Uma biblioteca que sonda até que receipt.blockNumber não seja nulo espera pelo nível de confirmação quando a resposta que ela quer - isto deu certo - já estava disponível no nível de pré-confirmação. Isso não é um bug, mas significa que você está esperando mais do que precisa. Leia status assim que o recibo existir.

A finalidade é uma quarta coisa, separada: acontece quando o lote que a contém liquida na Ethereum, e não é de forma alguma observável através desses campos. As etiquetas de bloco também não ajudam - safe e finalized resolvem ambas para latest.

Enviar

Assine localmente e envie em bruto. O nó não detém chaves, então eth_sendTransaction e todos os métodos de assinatura respondem -32601 por desenho.

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 contra um provedor EIP-1193 bruto, sem biblioteca alguma. Note qual lado assina: aqui eth_sendTransaction é tratado pela carteira, que assina e em seguida submete a transação bruta por você - o nó em si recusa esse método.

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

Ler o recibo

Aqui um recibo que falta é um problema, não paciência

Um null vindo de eth_getTransactionReceipt significa que o nó não tem recibo para esse hash - e nesta cadeia normalmente existe um dentro da meta de agendamento do minibloco, portanto um null depois de um segundo ou dois é um sinal para investigar em vez de continuar esperando.

Também é ambíguo por desenho: um recibo que foi despejado da janela responde null exatamente como um que nunca existiu. É por isso que um recuo longo de sondagem é a forma errada aqui - espere demais entre as tentativas e o recibo pode desaparecer entre duas delas.

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.

Com viem, waitForTransactionReceipt faz isso por você - mas deixe pollingInterval baixo e não aumente o tempo limite dele supondo que esperar mais é mais seguro. Aqui não é.

Dois campos desse recibo não querem dizer o que dizem em outros lugares. cumulativeGasUsed é o gas próprio desta transação e não um total corrente do bloco, e o logIndex de cada log é enumerado por transação e não por bloco - portanto (blockNumber, logIndex) não é uma chave única. Inclua o hash da transação se você estiver armazenando eventos.

Quando dá revert

Sua biblioteca não consegue decodificar a razão

Um revert volta como texto de mensagem - execution reverted: 0x… - e error.data nunca é preenchido. Todas as bibliotecas decodificam os erros personalizados e as cadeias de revert a partir de error.data, portanto nesta cadeia nenhuma consegue: viem e ethers vão ambas expor a mensagem bruta em vez de um erro nomeado.

Os dados de retorno estão na mensagem, então você pode decodificá-los por conta própria:

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

Mais uma diferença de transporte que vale saber: o mesmo revert é -32000 sobre HTTP e -32603 sobre o WebSocket, porque o socket reencapsula todo erro que não seja de envio. Um cliente que se ramifica no código precisa saber em qual transporte ele está.

O gas

Defina gasPrice explicitamente como 1 wei. Não há mercado de taxas contra o qual estimar: a base fee é zero, eth_gasPrice responde um wei, e um wei é também o mínimo que a cadeia aceitará.

eth_estimateGas funciona e simula corretamente, acrescentando a folga habitual - mas ele ignora os parâmetros gas, gasPrice, nonce e de bloco que você lhe envia, simulando sempre no limite de 30.000.000 por bloco contra o estado mais recente. E não leia eth_feeHistory: ele devolve constantes em vez de medições.

Dois limites que aparecem como -32005

Uma transação bruta com mais de 128 KiB é rejeitada pelo tamanho antes mesmo de ser decodificada, e a fila de admissão retém 64 transações pendentes por remetente. As duas respondem -32005, o que significa um limite e não um erro - recue e tente de novo em vez de tratar isso como uma falha.