китаев.tech

Работа с nonce вручную

Как вручную задавать nonce в Ethereum-транзакциях через ethers v6 и безопасно отправлять несколько pending-транзакций подряд.

Когда nonce нужен вручную

Nonce — это порядковый номер транзакции от конкретного EOA.

Обычно кошелёк и RPC подставляют его автоматически. Вручную nonce нужен, когда вы отправляете несколько транзакций подряд, заменяете pending-транзакцию или строите собственную очередь отправки.


Отправляем несколько транзакций с ручным nonce

Запустите локальную сеть Hardhat в отдельном терминале:

bash
npx hardhat node

Затем выполните TypeScript-код:

typescript
import { ethers } from "ethers";

const provider = new ethers.JsonRpcProvider("http://127.0.0.1:8545");

// Первый тестовый аккаунт Hardhat. Не используйте этот ключ в mainnet.
const sender = new ethers.Wallet(
  "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80",
  provider,
);

const recipients = [
  "0x70997970C51812dc3A010C7d01b50e0d17dc79C8",
  "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC",
  "0x90F79bf6EB2c4f870365E785982E1f101E93b906",
];

try {
  await provider.send("evm_setAutomine", [false]);

  const startNonce = await provider.getTransactionCount(sender.address, "pending");

  console.log("Sender:", sender.address);
  console.log("Start nonce:", startNonce);

  const txs = await Promise.all(
    recipients.map((recipient, index) => {
      return sender.sendTransaction({
        to: recipient,
        value: ethers.parseEther("0.01"),
        nonce: startNonce + index,
      });
    }),
  );

  for (const tx of txs) {
    console.log("Pending tx:", tx.hash);
    console.log("Nonce:", tx.nonce);
  }

  const pendingNonce = await provider.getTransactionCount(sender.address, "pending");
  const latestNonceBeforeMining = await provider.getTransactionCount(sender.address, "latest");

  console.log("Latest nonce before mining:", latestNonceBeforeMining);
  console.log("Pending nonce before mining:", pendingNonce);

  await provider.send("evm_mine", []);

  for (const tx of txs) {
    const receipt = await tx.wait();

    if (!receipt || receipt.status !== 1) {
      throw new Error(`Транзакция ${tx.hash} не была успешно выполнена`);
    }

    console.log("Confirmed tx:", tx.hash);
    console.log("Block:", receipt.blockNumber);
    console.log("Gas used:", receipt.gasUsed.toString());
  }

  const latestNonceAfterMining = await provider.getTransactionCount(sender.address, "latest");

  console.log("Latest nonce after mining:", latestNonceAfterMining);
} finally {
  await provider.send("evm_setAutomine", [true]);
}

Код отправляет три pending-транзакции от одного адреса и вручную назначает им последовательные nonce.


Как это работает

provider.getTransactionCount(sender.address, "pending") — возвращает следующий свободный nonce с учётом pending-транзакций.

startNonce + index — выдаёт каждой транзакции отдельный nonce: N, N + 1, N + 2.

Promise.all() — отправляет транзакции параллельно. Без ручного nonce несколько запросов могли бы получить один и тот же nonce.

evm_setAutomine — выключает автоматический майнинг в Hardhat, чтобы транзакции остались pending до ручного блока.

latest — считает только подтверждённые транзакции.

pending — считает подтверждённые и уже отправленные pending-транзакции.


Почему порядок важен

Ethereum выполняет транзакции одного EOA строго по nonce.

text
nonce 10 → выполнится первой
nonce 11 → ждёт nonce 10
nonce 12 → ждёт nonce 11

Если транзакция с nonce 10 зависла, транзакции 11 и 12 тоже будут ждать, даже если у них высокая комиссия.


latest vs pending

Для отправки новых транзакций почти всегда нужен pending.

typescript
const latestNonce = await provider.getTransactionCount(address, "latest");
const pendingNonce = await provider.getTransactionCount(address, "pending");

latestNonce не видит mempool. Если у адреса уже есть pending-транзакция, latest может вернуть старый nonce и вы случайно создадите замену вместо новой транзакции.

pendingNonce учитывает очередь, которую знает ваша RPC-нода.


Что делать с ошибками nonce

nonce too low означает, что транзакция использует nonce, который уже выполнен или уже занят другой транзакцией.

replacement transaction underpriced означает, что вы отправили транзакцию с тем же nonce, но комиссия недостаточно выше для замены.

nonce too high означает, что в очереди есть пропуск: сеть ждёт транзакции с более ранним nonce.

text
Есть nonce 7 и 9
Нет nonce 8

Транзакция 9 будет ждать, пока появится или выполнится nonce 8

Частые ошибки

Брать nonce через latest для параллельной отправки → используйте pending, иначе можно случайно выдать одинаковый nonce.

Оставлять пропуск в nonce → все следующие транзакции от этого адреса будут ждать.

Управлять nonce из нескольких процессов без синхронизации → два воркера могут выдать один nonce и заменить транзакции друг друга.


Что дальше

Материалы китаev.tech публикуются в образовательных целях и не являются инвестиционной рекомендацией. Примеры кода и описания протоколов — для обучения; использование в продакшне на ваш собственный риск. Дисклеймер и политика конфиденциальности.