diff --git a/atxm/utils.py b/atxm/utils.py index 8576e36..b444068 100644 --- a/atxm/utils.py +++ b/atxm/utils.py @@ -37,7 +37,7 @@ def _log_gas_weather(base_fee: Wei, tip: Wei) -> None: log.info(f"Gas conditions: base {base_fee_gwei} gwei | tip {tip_gwei} gwei") -def __get_receipt_from_txhash(w3: Web3, txhash: TxHash) -> Optional[TxReceipt]: +def _get_receipt_from_txhash(w3: Web3, txhash: TxHash) -> Optional[TxReceipt]: try: receipt = w3.eth.get_transaction_receipt(txhash) except TransactionNotFound: @@ -61,7 +61,7 @@ def _get_receipt(w3: Web3, pending_tx: PendingTx) -> Optional[TxReceipt]: log.error(f"[error] Transaction {pending_tx.txhash.hex()} not found") return - receipt = __get_receipt_from_txhash(w3=w3, txhash=txdata["hash"]) + receipt = _get_receipt_from_txhash(w3=w3, txhash=txdata["hash"]) if not receipt: return @@ -86,7 +86,7 @@ def _get_receipt(w3: Web3, pending_tx: PendingTx) -> Optional[TxReceipt]: def _get_confirmations(w3: Web3, tx: Union[PendingTx, FinalizedTx]) -> int: current_block = w3.eth.block_number - tx_receipt = __get_receipt_from_txhash(w3=w3, txhash=tx.txhash) + tx_receipt = _get_receipt_from_txhash(w3=w3, txhash=tx.txhash) if not tx_receipt: log.info(f"Transaction {tx.txhash.hex()} is pending or unconfirmed") return 0 diff --git a/tests/test_faults.py b/tests/test_faults.py index 91707d3..0ffd3ef 100644 --- a/tests/test_faults.py +++ b/tests/test_faults.py @@ -1,58 +1,107 @@ -import pytest -import pytest_twisted -from twisted.internet import reactor -from twisted.internet.task import deferLater from web3.exceptions import TransactionNotFound +from web3.types import TxReceipt -from atxm.exceptions import Fault +from atxm.exceptions import Fault, TransactionFaulted +from atxm.strategies import AsyncTxStrategy from atxm.tx import FaultedTx +from atxm.utils import _get_receipt_from_txhash -@pytest.fixture -def mock_eth_get_transaction(mocker, w3): - return mocker.patch.object( - w3.eth, - "get_transaction", - side_effect=TransactionNotFound - ) - +def _broadcast_tx(machine, eip1559_transaction, account, mocker): + fault_hook = mocker.Mock() -@pytest_twisted.inlineCallbacks -def test_timeout( - machine, clock, eip1559_transaction, account, interval, - mock_wake_sleep, mocker, mock_eth_get_transaction -): - hook = mocker.Mock() atx = machine.queue_transaction( params=eip1559_transaction, signer=account, - on_fault=hook, + on_fault=fault_hook, ) - machine.start() - while not machine.pending: - yield clock.advance(interval) - machine.stop() - assert not machine.running + # broadcast tx + machine._cycle() assert machine.pending == atx assert atx.final is False assert atx.fault is None - atx.created -= 9999999999 - machine.start() - while machine.pending: - yield clock.advance(interval) - machine.stop() + return atx, fault_hook + + +def _verify_tx_faulted(machine, atx, fault_hook, expected_fault: Fault): + while fault_hook.call_count == 0: + # ensure tx processed + machine._cycle() assert atx.final is False - assert isinstance(atx.fault, Fault) assert isinstance(atx, FaultedTx) + assert isinstance(atx.fault, Fault) + assert atx.fault == expected_fault # check async tx advanced through the state machine assert atx not in machine.queued + assert machine.pending is None assert atx.final is False - yield deferLater(reactor, 0.2, lambda: None) - assert hook.call_count == 1 + assert fault_hook.call_count == 1 + fault_hook.assert_called_with(atx) + + +def test_revert( + chain, + w3, + machine, + clock, + eip1559_transaction, + account, + interval, + mock_wake_sleep, + mocker, +): + atx, fault_hook = _broadcast_tx(machine, eip1559_transaction, account, mocker) + + assert machine.pending + + chain.mine(1) + + # force receipt to symbolize a revert of the tx + receipt = _get_receipt_from_txhash(w3, atx.txhash) + revert_receipt = dict(receipt) + revert_receipt["status"] = 0 + + mocker.patch.object( + w3.eth, "get_transaction_receipt", return_value=TxReceipt(revert_receipt) + ) + + _verify_tx_faulted(machine, atx, fault_hook, expected_fault=Fault.REVERT) + + +def test_strategy_fault( + w3, machine, clock, eip1559_transaction, account, interval, mock_wake_sleep, mocker +): + faulty_strategy = mocker.Mock(spec=AsyncTxStrategy) + machine._strategies.insert(0, faulty_strategy) # add first + + atx, fault_hook = _broadcast_tx(machine, eip1559_transaction, account, mocker) + + faulty_message = "mocked fault" + faulty_strategy.execute.side_effect = TransactionFaulted( + tx=atx, fault=Fault.ERROR, message=faulty_message + ) + + mocker.patch.object( + w3.eth, "get_transaction_receipt", side_effect=TransactionNotFound + ) + + _verify_tx_faulted(machine, atx, fault_hook, expected_fault=Fault.ERROR) + assert atx.error == faulty_message + + +def test_timeout_strategy_fault( + w3, machine, clock, eip1559_transaction, account, interval, mock_wake_sleep, mocker +): + atx, fault_hook = _broadcast_tx(machine, eip1559_transaction, account, mocker) + + atx.created -= 9999999999 + mocker.patch.object(w3.eth, "get_transaction", side_effect=TransactionNotFound) + + _verify_tx_faulted(machine, atx, fault_hook, expected_fault=Fault.TIMEOUT)