китаев.tech

Работа с транзакциями: nonce, gas, EIP-1559 в viem

Соберёшь EIP-1559 транзакцию в viem: nonce, estimateGas, maxFeePerGas / maxPriorityFeePerGas и sendTransaction на Hardhat.

Те же понятия, другой синтаксис

Nonce, gas limit и комиссии EIP-1559 в viem те же, что в ethers. Меняются клиенты и имена методов: getTransactionCount, estimateGas, sendTransaction с maxFeePerGas / maxPriorityFeePerGas.

Концепты base fee и priority fee разобраны в EIP-1559 — maxFeePerGas vs gasPrice. Здесь — как задать их в viem и отправить ETH.


Отправляем EIP-1559 перевод на Hardhat

bash
npx hardhat node
typescript
import {
  createPublicClient,
  createWalletClient,
  http,
  parseEther,
  parseGwei,
  formatGwei,
  formatEther,
} from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { hardhat } from "viem/chains";

const account = privateKeyToAccount(
  // Первый тестовый ключ Hardhat. Не используй его в mainnet.
  "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80",
);

const publicClient = createPublicClient({
  chain: hardhat,
  transport: http("http://127.0.0.1:8545"),
});

const walletClient = createWalletClient({
  account,
  chain: hardhat,
  transport: http("http://127.0.0.1:8545"),
});

const recipient = "0x70997970C51812dc3A010C7d01b50e0d17dc79C8";
const value = parseEther("0.1");

const latestBlock = await publicClient.getBlock();
if (latestBlock.baseFeePerGas == null) {
  throw new Error("Сеть не поддерживает EIP-1559");
}

const maxPriorityFeePerGas = parseGwei("2");
const maxFeePerGas = latestBlock.baseFeePerGas * 2n + maxPriorityFeePerGas;

const nonce = await publicClient.getTransactionCount({
  address: account.address,
});

const gas = await publicClient.estimateGas({
  account: account.address,
  to: recipient,
  value,
  maxFeePerGas,
  maxPriorityFeePerGas,
});

console.log("Base fee:", formatGwei(latestBlock.baseFeePerGas), "gwei");
console.log("Max priority fee:", formatGwei(maxPriorityFeePerGas), "gwei");
console.log("Max fee:", formatGwei(maxFeePerGas), "gwei");
console.log("Nonce:", nonce);
console.log("Gas limit:", gas.toString());

const hash = await walletClient.sendTransaction({
  to: recipient,
  value,
  gas,
  nonce,
  maxFeePerGas,
  maxPriorityFeePerGas,
});

const receipt = await publicClient.waitForTransactionReceipt({ hash });
if (receipt.status !== "success") {
  throw new Error("Транзакция не подтвердилась");
}

const minedBlock = await publicClient.getBlock({
  blockNumber: receipt.blockNumber,
});
const effectiveGasPrice = receipt.effectiveGasPrice;
const actualFee = receipt.gasUsed * effectiveGasPrice;

console.log("Hash:", hash);
console.log("Block:", receipt.blockNumber);
console.log("Gas used:", receipt.gasUsed.toString());
console.log(
  "Effective gas price:",
  formatGwei(effectiveGasPrice),
  "gwei",
);
console.log("Actual fee:", formatEther(actualFee), "ETH");
console.log(
  "Mined base fee:",
  minedBlock.baseFeePerGas != null
    ? `${formatGwei(minedBlock.baseFeePerGas)} gwei`
    : "n/a",
);

Если gas / nonce / fees не указать, viem часто подставит их сам при sendTransaction. Явные значения нужны, когда контролируешь очередь (replacement), батчишь tx или отлаживаешь underpriced.


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

getTransactionCount({ address }) — nonce аккаунта (число подтверждённых tx). Для pending-очереди смотри blockTag: "pending".

estimateGas({ account, to, value, ... }) — симуляция: сколько gas units нужно. Это не цена в ETH. Верхняя оценка комиссии ≈ gas * maxFeePerGas.

maxPriorityFeePerGas — чаевые валидатору (tip). maxFeePerGas — потолок за gas: должен покрывать baseFee + tip, иначе tx не включат.

parseGwei("2") — удобнее, чем считать wei вручную. Аналог parseUnits("2", "gwei") в ethers.

sendTransaction({ gas, nonce, maxFeePerGas, maxPriorityFeePerGas }) — type-2 (EIP-1559), если заданы max-fee поля. Legacy gasPrice в post-London сетях обычно не нужен.

receipt.effectiveGasPrice — фактическая цена gas после майнинга. Реальная комиссия: gasUsed * effectiveGasPrice.

Альтернатива ручному расчёту fees:

typescript
const fees = await publicClient.estimateFeesPerGas();
// fees.maxFeePerGas, fees.maxPriorityFeePerGas

Если ты знаешь ethers.js

ethers v6viem
provider.getTransactionCount(addr)publicClient.getTransactionCount({ address })
signer.estimateGas(tx)publicClient.estimateGas({ account, ... })
provider.getFeeData()publicClient.estimateFeesPerGas()
parseUnits("2", "gwei")parseGwei("2")
signer.sendTransaction({ type: 2, maxFeePerGas, ... })walletClient.sendTransaction({ maxFeePerGas, ... })
tx.wait() / receipt.status === 1waitForTransactionReceipt / status === "success"
receipt.gasPricereceipt.effectiveGasPrice

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

maxFeePerGas < текущий base fee → транзакция сидит в мемпуле или отклоняется. Бери запас: часто baseFee * 2n + tip.

Nonce вручную и параллельные tx → два send с одним nonce = конфликт. Либо полагайся на авто-nonce viem, либо веди счётчик сам (см. ручной nonce).

Путаешь gas и комиссию в ETHgas — лимит единиц. Сколько спишут в ETH, видно только после gasUsed * effectiveGasPrice.


Что дальше

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