Ускорение и отмена TX через replace-by-fee
Как заменить pending-транзакцию в Ethereum: тот же nonce, тот же отправитель и более высокая комиссия через ethers v6.
Что такое replace-by-fee
Replace-by-fee — это замена pending-транзакции новой транзакцией с тем же nonce и более высокой комиссией.
Так можно ускорить зависшую транзакцию или отменить её, пока она ещё не попала в блок.
Отменяем pending-транзакцию
Запустите локальную сеть Hardhat в отдельном терминале:
npx hardhat nodeЗатем выполните 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,
);
// Второй тестовый аккаунт Hardhat.
const recipient = "0x70997970C51812dc3A010C7d01b50e0d17dc79C8";
async function getBaseFeePerGas() {
const block = await provider.getBlock("latest");
if (block?.baseFeePerGas == null) {
throw new Error("Сеть не поддерживает EIP-1559");
}
return block.baseFeePerGas;
}
try {
await provider.send("evm_setAutomine", [false]);
const nonce = await provider.getTransactionCount(sender.address, "pending");
const baseFeePerGas = await getBaseFeePerGas();
const originalMaxPriorityFeePerGas = ethers.parseUnits("1", "gwei");
const originalMaxFeePerGas = baseFeePerGas * 2n + originalMaxPriorityFeePerGas;
const originalTx = await sender.sendTransaction({
to: recipient,
value: ethers.parseEther("1"),
nonce,
type: 2,
maxFeePerGas: originalMaxFeePerGas,
maxPriorityFeePerGas: originalMaxPriorityFeePerGas,
});
console.log("Original hash:", originalTx.hash);
console.log("Original nonce:", originalTx.nonce);
console.log("Original max fee:", ethers.formatUnits(originalMaxFeePerGas, "gwei"), "gwei");
console.log("Original receipt:", await provider.getTransactionReceipt(originalTx.hash));
const cancelMaxPriorityFeePerGas = ethers.parseUnits("3", "gwei");
const cancelMaxFeePerGas = baseFeePerGas * 4n + cancelMaxPriorityFeePerGas;
const cancelTx = await sender.sendTransaction({
to: sender.address,
value: 0n,
nonce,
type: 2,
maxFeePerGas: cancelMaxFeePerGas,
maxPriorityFeePerGas: cancelMaxPriorityFeePerGas,
});
console.log("Cancel hash:", cancelTx.hash);
console.log("Cancel nonce:", cancelTx.nonce);
console.log("Cancel max fee:", ethers.formatUnits(cancelMaxFeePerGas, "gwei"), "gwei");
await provider.send("evm_mine", []);
const cancelReceipt = await cancelTx.wait();
const originalReceipt = await provider.getTransactionReceipt(originalTx.hash);
if (!cancelReceipt || cancelReceipt.status !== 1) {
throw new Error("Cancel-транзакция не была успешно выполнена");
}
console.log("Cancel included in block:", cancelReceipt.blockNumber);
console.log("Cancel gas used:", cancelReceipt.gasUsed.toString());
console.log("Original receipt after replacement:", originalReceipt);
} finally {
await provider.send("evm_setAutomine", [true]);
}Код создаёт pending-перевод 1 ETH, а затем заменяет его self-transfer транзакцией с тем же nonce. В блок попадёт только новая транзакция.
Как это работает
evm_setAutomine — выключает автоматический майнинг в Hardhat, чтобы первая транзакция осталась pending.
provider.getTransactionCount(sender.address, "pending") — берёт следующий nonce с учётом уже pending-транзакций.
nonce — ключ к замене. Новая транзакция должна иметь тот же nonce и того же отправителя.
maxFeePerGas и maxPriorityFeePerGas — должны быть выше, чем в старой pending-транзакции.
to: sender.address и value: 0n — делают отмену: вместо исходного перевода отправитель отправляет ноль ETH самому себе.
provider.getTransactionReceipt(originalTx.hash) — после замены не возвращает receipt, потому что старая транзакция не попала в блок.
Ускорение vs отмена
Ускорение и отмена используют один механизм:
тот же from
тот же nonce
более высокая комиссияРазница только в содержимом новой транзакции.
| Цель | Что отправить вместо старой TX |
|---|---|
| Ускорить | ту же операцию: тот же to, value, data |
| Отменить | self-transfer: to = from, value = 0 |
Если новая транзакция попадёт в блок, старая станет недействительной. Две транзакции с одним nonce от одного адреса не могут выполниться обе.
Почему нужна более высокая комиссия
RPC-нода и валидаторы не обязаны принимать замену с такой же ценой gas.
Обычно новая транзакция должна заметно повысить:
maxFeePerGas
maxPriorityFeePerGasЕсли поднять только maxFeePerGas, но оставить низкий maxPriorityFeePerGas, валидатору может быть всё равно невыгодно включать транзакцию. Для ускорения важны именно чаевые.
Когда replace-by-fee уже не поможет
Replace-by-fee работает только для pending-транзакций.
Если старая транзакция уже получила receipt, её нельзя отменить. Можно отправить новую обратную транзакцию, но она не откатит уже выполненное действие.
pending → можно заменить
confirmed → нельзя заменить
failed → нельзя заменить, gas уже потраченЧастые ошибки
Менять nonce → это создаст новую транзакцию в очереди, а не заменит старую.
Поднимать только gasLimit → replace-by-fee требует более высокую цену gas, а не больший лимит.
Пытаться отменить confirmed-транзакцию → после receipt транзакция уже в блоке, replace-by-fee не работает.
Что дальше
- Работа с nonce вручную — управлять очередью транзакций от одного адреса
- EIP-1559 — maxFeePerGas vs gasPrice — понять, какие поля комиссии нужно повышать
- Статусы транзакции — pending, confirmed, failed — проверить, можно ли ещё заменить транзакцию