Skip to content

Commit c859d03

Browse files
Add TransactionManager to handle stalled tx (#767)
* Replace stuck pending transactions instead of queuing new nonce * Integrate transaction_gas_wrapper into TransactionManager * Review fix * Handle pending tx timeout and centralize estimate_gas * Add test for pending tx receipt timeout * Skip replacement when pending tx is at fee ceiling * Skip replacement when fee bump cannot clear node threshold * Add Fees unit tests and rename to_tx_params * Detect fee-too-low rejections via Web3RPCError message * Match Erigon replacement rejection message * Decrease EXECUTION_TRANSACTION_TIMEOUT default to 60 sec * Simplify default-gas escalation branch
1 parent 9c163d6 commit c859d03

14 files changed

Lines changed: 754 additions & 167 deletions

File tree

src/common/contracts.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,10 @@
1414
AsyncContractEvents,
1515
AsyncContractFunctions,
1616
)
17-
from web3.types import BlockNumber, ChecksumAddress, EventData, Wei
17+
from web3.types import BlockNumber, ChecksumAddress, EventData, TxReceipt, Wei
1818

1919
from src.common.clients import execution_client as default_execution_client
20-
from src.common.execution import transaction_gas_wrapper
20+
from src.common.transaction import tx_manager
2121
from src.common.typings import (
2222
ExitQueueMissingAssetsParams,
2323
HarvestParams,
@@ -411,10 +411,9 @@ async def aggregate(
411411
async def tx_aggregate(
412412
self,
413413
data: list[tuple[ChecksumAddress, HexStr]],
414-
) -> HexStr:
414+
) -> TxReceipt | None:
415415
tx_function = self.contract.functions.aggregate(data)
416-
tx_hash = await transaction_gas_wrapper(tx_function)
417-
return Web3.to_hex(tx_hash)
416+
return await tx_manager.transact(tx_function)
418417

419418

420419
class ValidatorsCheckerContract(ContractWrapper):

src/common/execution.py

Lines changed: 4 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,16 @@
1-
import asyncio
21
import logging
32
from urllib.parse import urlparse
43

5-
from hexbytes import HexBytes
64
from sw_utils import GasManager, InterruptHandler
75
from web3 import Web3
8-
from web3.contract.async_contract import AsyncContractFunction
9-
from web3.types import TxParams, Wei
6+
from web3.types import Wei
107

118
from src.common.clients import execution_client
129
from src.common.metrics import metrics
1310
from src.common.tasks import BaseTask
1411
from src.common.wallet import wallet
1512
from src.config.networks import HOODI
16-
from src.config.settings import ATTEMPTS_WITH_DEFAULT_GAS, settings
13+
from src.config.settings import settings
1714

1815
logger = logging.getLogger(__name__)
1916

@@ -49,40 +46,10 @@ async def get_wallet_balance() -> Wei:
4946
return await execution_client.eth.get_balance(wallet.address)
5047

5148

52-
async def transaction_gas_wrapper(
53-
tx_function: AsyncContractFunction, tx_params: TxParams | None = None
54-
) -> HexBytes:
55-
"""Handles periods with high gas in the network."""
56-
if not tx_params:
57-
tx_params = {}
58-
59-
# trying to submit with basic gas
60-
attempts_with_default_gas = ATTEMPTS_WITH_DEFAULT_GAS
61-
62-
# Alchemy does not support eth_maxPriorityFeePerGas for Hoodi
63-
if settings.network == HOODI and _is_alchemy_used():
64-
attempts_with_default_gas = 0
65-
66-
for i in range(attempts_with_default_gas):
67-
try:
68-
return await tx_function.transact(tx_params)
69-
except ValueError as e:
70-
# Handle only FeeTooLow error
71-
if not _is_fee_too_low_error(e):
72-
raise e
73-
if i < attempts_with_default_gas - 1: # skip last sleep
74-
await asyncio.sleep(settings.network_config.SECONDS_PER_BLOCK)
75-
76-
# use high priority fee
77-
gas_manager = build_gas_manager()
78-
tx_params = tx_params | await gas_manager.get_high_priority_tx_params()
79-
return await tx_function.transact(tx_params)
80-
81-
8249
async def check_gas_price(high_priority: bool = False) -> bool:
8350
gas_manager = build_gas_manager()
8451
# Alchemy does not support eth_maxPriorityFeePerGas for Hoodi, skip
85-
if settings.network == HOODI and _is_alchemy_used():
52+
if settings.network == HOODI and is_alchemy_used():
8653
return True
8754

8855
return await gas_manager.check_gas_price(high_priority)
@@ -99,14 +66,7 @@ def build_gas_manager() -> GasManager:
9966
)
10067

10168

102-
def _is_fee_too_low_error(e: ValueError) -> bool:
103-
code = None
104-
if e.args and isinstance(e.args[0], dict):
105-
code = e.args[0].get('code')
106-
return code == -32010
107-
108-
109-
def _is_alchemy_used() -> bool:
69+
def is_alchemy_used() -> bool:
11070
for endpoint in settings.execution_endpoints:
11171
domain = urlparse(endpoint).netloc
11272
if domain.lower().endswith(ALCHEMY_DOMAIN):

0 commit comments

Comments
 (0)