Add TransactionManager to handle stalled tx#767
Conversation
|
|
||
| # forget gas records for nonces that have already been mined (nonce | ||
| # `latest_nonce` itself may still be pending, so keep it) | ||
| self._nonce_to_tx_params = { |
There was a problem hiding this comment.
Optional: complicated one-liner, can be exracted into _update_nonce... func
| self.priority_fee_per_gas = self._bump(self.priority_fee_per_gas) | ||
| self._cap() | ||
|
|
||
| def _cap(self) -> None: |
There was a problem hiding this comment.
move private funcs in the end of the class
| max_fee_per_gas=self.max_fee_per_gas, | ||
| ) | ||
|
|
||
| def tx_params(self) -> TxParams: |
There was a problem hiding this comment.
Optional: Use @Property or smth like get_tx_params
| gas_manager = build_gas_manager() | ||
| tx_params = await gas_manager.get_high_priority_tx_params() | ||
| tx = await vault_contract.functions.multicall(calls).transact(tx_params) | ||
| tx_receipt = await tx_manager.transact( |
There was a problem hiding this comment.
estimate_gas will pass, but tx can fail if there is a lock
| return None | ||
|
|
||
| @staticmethod | ||
| async def _wait_for_receipt(tx_hash: HexBytes) -> TxReceipt | None: |
There was a problem hiding this comment.
catch from claude:
TimeExhausted handling is inconsistent and partly defeats the
design (transaction.py:194-201)
_wait_for_receipt doesn't catch TimeExhausted, so timeouts raise
instead of returning None. The PR's "stop and let next run replace
it" story depends on returning None. Behavior now diverges by call
site: exits/validators/withdrawals swallow it (except Exception),
but harvest/execution.py:23 (catches only ValueError,
ContractLogicError) and meta_vault/reward_splitter (no try)
propagate it as a task-level crash. Fix centrally: catch
TimeExhausted in _wait_for_receipt, return None, log "pending, will
replace next run."
| # an earlier transaction is stuck at latest_nonce - replace it instead of | ||
| # queuing a new one behind it (skip the default-gas attempts) | ||
| logger.info('Found pending transaction at nonce %d, replacing it', latest_nonce) | ||
| tx_hash = await self._submit_high_priority(tx_function, tx_params, latest_nonce) |
There was a problem hiding this comment.
catch from claude: Looks like node can revert tx with same params
At-cap replacement can't bump → "replacement underpriced" wedge
(transaction.py:47-56,152-171)
Once a pending tx's fee_per_gas is at max_fee_per_gas, bump()→_cap()
leaves it unchanged, so the replacement is broadcast with identical
maxFeePerGas and rejected (-32000 underpriced ValueError). That
error isn't caught in submit_high_priority, so it surfaces as a
generic failure and the nonce stays stuck until base fee drops. The
docstring's "so it converges" is misleading. Detect this case and
log a clear warning.
Why the node rejects it
EIP-1559 replacement rules in geth/Nethermind require a same-nonce
replacement to raise both maxFeePerGas and maxPriorityFeePerGas by
at least the configured price bump (default 10%). A 0% increase is
rejected with replacement transaction underpriced — a ValueError,
JSON-RPC code -32000
Summary
Routes all wallet transactions through a single
TransactionManager.transact(...)path and removestransaction_gas_wrapper. The manager owns submit + receipt-wait under a lock (one in-flight tx at a time), reuses the lowest unconfirmed nonce, and replaces a stuck pending tx with bumped fees instead of an in-call retry loop.What changed
transact(tx_function, tx_params=None, high_priority=False) -> TxReceipt | None: serializes submit and the receipt wait so exactly one wallet tx is in flight at a time; returns the receipt (orNoneon revert).high_priority=True(e.g. validator registration) skips the default-gas attempts and submits high-priority fees straight away.high_priority=Falsekeeps the existing default-gas attempts loop, escalating to high-priority fees onFeeTooLow.execution_transaction_timeoutthe task retries on its next run, where a pending tx is detected and its fees bumped (max(high_priority, bump(prev)), capped atmax_fee_per_gas).transaction_gas_wrapper; migrated every caller (validators register/fund/consolidate, withdrawals, exits, harvest, reward splitter, meta vault) plus thetx_aggregate/deposit_to_sub_vaultswrappers, which now returnTxReceipt | None.Behavior note
Because the manager now owns the receipt wait, a receipt timeout that previously propagated out of a caller is now caught by the caller's existing
except Exceptionand returnsNone— still retried on the next task run.