Работа с транзакциями: 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
npx hardhat nodeimport {
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:
const fees = await publicClient.estimateFeesPerGas();
// fees.maxFeePerGas, fees.maxPriorityFeePerGasЕсли ты знаешь ethers.js
| ethers v6 | viem |
|---|---|
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 === 1 | waitForTransactionReceipt / status === "success" |
receipt.gasPrice | receipt.effectiveGasPrice |
Частые ошибки
maxFeePerGas < текущий base fee → транзакция сидит в мемпуле или отклоняется. Бери запас: часто baseFee * 2n + tip.
Nonce вручную и параллельные tx → два send с одним nonce = конфликт. Либо полагайся на авто-nonce viem, либо веди счётчик сам (см. ручной nonce).
Путаешь gas и комиссию в ETH → gas — лимит единиц. Сколько спишут в ETH, видно только после gasUsed * effectiveGasPrice.
Что дальше
- Мультивызовы: multicall — пачка read без отдельных RPC на каждый вызов
- viem + wagmi — те же tx из React-хуков
- EIP-1559 — maxFeePerGas vs gasPrice — разбор комиссий на ethers.js v6