Skip to content

Commit f04c451

Browse files
authored
perf: optimize getting virtual machine errors (#115)
1 parent f4f8332 commit f04c451

File tree

3 files changed

+64
-46
lines changed

3 files changed

+64
-46
lines changed

ape_foundry/provider.py

Lines changed: 45 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -546,83 +546,82 @@ def get_transaction_trace(self, transaction_hash: str, **kwargs) -> TraceAPI:
546546
return _get_transaction_trace(transaction_hash, **kwargs)
547547

548548
def get_virtual_machine_error(self, exception: Exception, **kwargs) -> VirtualMachineError:
549-
if not len(exception.args):
549+
if not exception.args:
550550
return VirtualMachineError(base_err=exception, **kwargs)
551551

552552
err_data = exception.args[0]
553553

554+
# Determine message based on the type of error data
554555
if isinstance(err_data, dict):
555556
message = str(err_data.get("message", f"{err_data}"))
556-
elif isinstance(err_data, str):
557-
message = err_data
558-
elif msg := getattr(exception, "message", ""):
559-
message = msg
560557
else:
561-
message = ""
558+
message = err_data if isinstance(err_data, str) else getattr(exception, "message", "")
562559

563560
if not message:
564561
return VirtualMachineError(base_err=exception, **kwargs)
565562

566-
def _handle_execution_reverted(
567-
exception: Exception, revert_message: Optional[str] = None, **kwargs
568-
):
569-
if revert_message in ("", "0x", None):
570-
revert_message = TransactionError.DEFAULT_MESSAGE
571-
572-
sub_err = ContractLogicError(
573-
base_err=exception, revert_message=revert_message, **kwargs
574-
)
575-
enriched = self.compiler_manager.enrich_error(sub_err)
576-
577-
# Show call trace if available
578-
if enriched.txn:
579-
# Unlikely scenario where a transaction is on the error even though a receipt
580-
# exists.
581-
if isinstance(enriched.txn, TransactionAPI) and enriched.txn.receipt:
582-
enriched.txn.receipt.show_trace()
583-
elif isinstance(enriched.txn, ReceiptAPI):
584-
enriched.txn.show_trace()
585-
586-
return enriched
587-
588-
# Handle `ContactLogicError` similarly to other providers in `ape`.
589-
# by stripping off the unnecessary prefix that foundry has on reverts.
563+
# Handle specific cases based on message content
590564
foundry_prefix = (
591565
"Error: VM Exception while processing transaction: reverted with reason string "
592566
)
567+
568+
# Handle Foundry error prefix
593569
if message.startswith(foundry_prefix):
594570
message = message.replace(foundry_prefix, "").strip("'")
595-
return _handle_execution_reverted(exception, message, **kwargs)
571+
return self._handle_execution_reverted(exception, message, **kwargs)
596572

597-
elif "Transaction reverted without a reason string" in message:
598-
return _handle_execution_reverted(exception, **kwargs)
573+
# Handle various cases of transaction reverts
574+
if "Transaction reverted without a reason string" in message:
575+
return self._handle_execution_reverted(exception, **kwargs)
599576

600-
elif message.lower() == "execution reverted":
577+
if message.lower() == "execution reverted":
601578
message = TransactionError.DEFAULT_MESSAGE
602-
if isinstance(exception, Web3ContractLogicError) and (
603-
msg := self._extract_custom_error(**kwargs)
604-
):
605-
exception.message = msg
579+
if isinstance(exception, Web3ContractLogicError):
580+
if custom_msg := self._extract_custom_error(**kwargs):
581+
exception.message = custom_msg
582+
return self._handle_execution_reverted(exception, revert_message=message, **kwargs)
606583

607-
return _handle_execution_reverted(exception, revert_message=message, **kwargs)
608-
609-
elif message == "Transaction ran out of gas" or "OutOfGas" in message:
584+
if "Transaction ran out of gas" in message or "OutOfGas" in message:
610585
return OutOfGasError(base_err=exception, **kwargs)
611586

612-
elif message.startswith("execution reverted: "):
587+
if message.startswith("execution reverted: "):
613588
message = (
614589
message.replace("execution reverted: ", "").strip()
615590
or TransactionError.DEFAULT_MESSAGE
616591
)
617-
return _handle_execution_reverted(exception, revert_message=message, **kwargs)
592+
return self._handle_execution_reverted(exception, revert_message=message, **kwargs)
618593

619-
elif isinstance(exception, ContractCustomError):
620-
# Is raw hex (custom exception)
594+
# Handle custom errors
595+
if isinstance(exception, ContractCustomError):
621596
message = TransactionError.DEFAULT_MESSAGE if message in ("", None, "0x") else message
622-
return _handle_execution_reverted(exception, revert_message=message, **kwargs)
597+
return self._handle_execution_reverted(exception, revert_message=message, **kwargs)
623598

624599
return VirtualMachineError(message, **kwargs)
625600

601+
# The type ignore is because are using **kwargs rather than repeating.
602+
def _handle_execution_reverted( # type: ignore[override]
603+
self, exception: Exception, revert_message: Optional[str] = None, **kwargs
604+
):
605+
# Assign default message if revert_message is invalid
606+
if revert_message == "0x":
607+
revert_message = TransactionError.DEFAULT_MESSAGE
608+
else:
609+
revert_message = revert_message or TransactionError.DEFAULT_MESSAGE
610+
611+
# Create and enrich the error
612+
sub_err = ContractLogicError(base_err=exception, revert_message=revert_message, **kwargs)
613+
enriched = self.compiler_manager.enrich_error(sub_err)
614+
615+
# Show call trace if available
616+
txn = enriched.txn
617+
if txn and hasattr(txn, "show_trace"):
618+
if isinstance(txn, TransactionAPI) and txn.receipt:
619+
txn.receipt.show_trace()
620+
elif isinstance(txn, ReceiptAPI):
621+
txn.show_trace()
622+
623+
return enriched
624+
626625
# Abstracted for easier testing conditions.
627626
def _extract_custom_error(self, **kwargs) -> str:
628627
# Check for custom error.

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
"pytest-xdist", # Multi-process runner
99
"pytest-cov", # Coverage analyzer plugin
1010
"pytest-mock", # For creating mocks
11+
"pytest-benchmark", # For performance tests
1112
"hypothesis>=6.2.0,<7.0", # Strategy-based fuzzer
1213
"ape-alchemy", # For running fork tests
1314
"ape-polygon", # For running polygon fork tests

tests/test_performance.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
from ape.api import ReceiptAPI
2+
3+
4+
def test_contract_transaction_revert(benchmark, connected_provider, owner, contract_instance):
5+
tx = benchmark.pedantic(
6+
lambda *args, **kwargs: contract_instance.setNumber(*args, **kwargs),
7+
args=(5,),
8+
kwargs={"sender": owner, "raise_on_revert": False},
9+
rounds=5,
10+
warmup_rounds=1,
11+
)
12+
assert isinstance(tx, ReceiptAPI) # Sanity check.
13+
stats = benchmark.stats
14+
median = stats.get("median")
15+
16+
# Was seeing 0.44419266798649915.
17+
# Seeing 0.2634877339878585 as of https://github.com/ApeWorX/ape-foundry/pull/115
18+
assert median < 3.5

0 commit comments

Comments
 (0)