Для разработчиков / JSON-RPC

Отправка транзакции

Отправка обычна. Чтение результата - нет: квитанция существует раньше блока, а revert приходит текстом, который ваша библиотека декодировать не умеет.

Три вещи, которые могут быть истинны

В большинстве сетей "отправлено", "смайнено" и "финально" схлопываются в одно ожидание. Здесь это три отдельных события, и понимание того, какое из них вы проверяете, - большая часть написания корректного клиента:

  • Исполнено. eth_sendRawTransaction блокирует до тех пор, пока транзакция действительно не отработает, так что успешный возврат уже означает исполнено, а не поставлено в очередь. Revert известен уже в этот момент.
  • Предподтверждено. В пределах целевого интервала мини-блока квитанция уже существует. Её blockNumber и blockHash всё ещё null.
  • Подтверждено. На целевом интервале EVM-блока блок запечатывается, и эти два поля заполняются.

Ждать номер блока - значит ждать не того

Библиотека, которая опрашивает, пока receipt.blockNumber не станет непустым, ждёт яруса подтверждения, тогда как нужный ей ответ - получилось ли - был доступен уже на ярусе предподтверждения. Это не баг, но это значит, что вы ждёте дольше, чем нужно. Читайте status, как только квитанция появилась.

Финальность - четвёртая, отдельная вещь: она наступает, когда содержащий пакет рассчитывается на Ethereum, и через эти поля не наблюдается вовсе. Теги блоков тоже не помогут - safe и finalized оба разрешаются в latest.

Отправка

Подписывайте локально и отправляйте raw. Узел не хранит ключей, поэтому 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 здесь обрабатывается кошельком, который подписывает и затем сам отправляет за вас raw-транзакцию, - сам узел в этом методе отказывает.

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

Чтение квитанции

Отсутствующая квитанция здесь - проблема, а не повод для терпения

null от eth_getTransactionReceipt означает, что у узла нет квитанции для этого хеша, - а в этой сети она обычно появляется в пределах целевого интервала мини-блока, так что 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 - это -32000 по HTTP и -32603 по WebSocket, потому что сокет переупаковывает любую ошибку, кроме ошибок отправки. Клиенту, который ветвится по коду, нужно знать, на каком он транспорте.

Газ

Задавайте gasPrice явно равным 1 wei. Рынка комиссий, по которому можно было бы оценивать, нет: базовая комиссия нулевая, eth_gasPrice отвечает одним wei, и один wei - это же и минимум, который сеть примет.

eth_estimateGas работает и симулирует как надо, добавляя обычный запас, - но он игнорирует переданные ему gas, gasPrice, nonce и параметры блока, всегда симулируя при лимите блока 30 000 000 против последнего состояния. И не читайте eth_feeHistory: он возвращает константы, а не измерения.

Два ограничения, которые всплывают как -32005

Raw-транзакция больше 128 КиБ отвергается по размеру ещё до декодирования, а очередь приёма держит 64 ожидающих транзакции на отправителя. Оба отвечают -32005, что означает ограничение, а не ошибку, - отступите и повторите, а не считайте это отказом.