diff --git a/setup.py b/setup.py index fcef7e4281..dcbf99083d 100644 --- a/setup.py +++ b/setup.py @@ -123,7 +123,7 @@ # ** Dependencies maintained by ApeWorX ** "eip712>=0.2.1,<0.3", "ethpm-types>=0.5.3,<0.6", - "evm-trace>=0.1.0a22", + "evm-trace>=0.1.0a23", ], entry_points={ "console_scripts": ["ape=ape._cli:cli"], diff --git a/src/ape/api/compiler.py b/src/ape/api/compiler.py index 383f37f463..11515f83c5 100644 --- a/src/ape/api/compiler.py +++ b/src/ape/api/compiler.py @@ -189,18 +189,11 @@ def _create_contract_from_call( evm_frame = EvmTraceFrame(**frame.raw) data = create_call_node_data(evm_frame) calldata = data.get("calldata", HexBytes("")) - - if "address" not in data: + if not (address := (data.get("address", frame.contract_address) or None)): return None, calldata - # NOTE: Handling when providers give us odd address values. - # NOTE: `or ""` because sometimes the address key exists and is None. - raw_addr = HexBytes(data.get("address") or "").hex().replace("0x", "") - zeroes = max(40 - len(raw_addr), 0) * "0" - addr = f"0x{zeroes}{raw_addr}" - try: - address = self.provider.network.ecosystem.decode_address(addr) + address = self.provider.network.ecosystem.decode_address(address) except Exception: return None, calldata diff --git a/src/ape/api/providers.py b/src/ape/api/providers.py index 14ce3eff48..8685f34238 100644 --- a/src/ape/api/providers.py +++ b/src/ape/api/providers.py @@ -1085,11 +1085,10 @@ def _send_call_as_txn( ) -> bytes: account = self.account_manager.test_accounts[0] receipt = account.call(txn, **kwargs) - call_tree = receipt.call_tree - if not call_tree: + if not (call_tree := receipt.call_tree): return self._send_call(txn, **kwargs) - # Grab raw retrurndata before enrichment + # Grab raw returndata before enrichment returndata = call_tree.outputs if (track_gas or track_coverage) and show_gas and not show_trace: @@ -1454,6 +1453,9 @@ def send_transaction(self, txn: TransactionAPI) -> ReceiptAPI: ), ) + # NOTE: Ensure to cache even the failed receipts. + self.chain_manager.history.append(receipt) + if receipt.failed: txn_dict = receipt.transaction.dict() txn_params = cast(TxParams, txn_dict) @@ -1467,7 +1469,6 @@ def send_transaction(self, txn: TransactionAPI) -> ReceiptAPI: raise vm_err from err logger.info(f"Confirmed {receipt.txn_hash} (total fees paid = {receipt.total_fees_paid})") - self.chain_manager.history.append(receipt) return receipt def _create_call_tree_node( diff --git a/src/ape/api/transactions.py b/src/ape/api/transactions.py index 94e764598c..12e824adad 100644 --- a/src/ape/api/transactions.py +++ b/src/ape/api/transactions.py @@ -547,9 +547,8 @@ def track_coverage(self): return tracker = self._test_runner.coverage_tracker - if self.provider.supports_tracing: - traceback = self.source_traceback - if traceback is not None and len(traceback) > 0 and self._test_runner is not None: + if self.provider.supports_tracing and (traceback := self.source_traceback): + if len(traceback) > 0: tracker.cover(traceback) elif method := self.method_called: diff --git a/src/ape/managers/chain.py b/src/ape/managers/chain.py index d2817349eb..1527305c4b 100644 --- a/src/ape/managers/chain.py +++ b/src/ape/managers/chain.py @@ -622,20 +622,34 @@ def __getitem_str(self, account_or_hash: str) -> Union[AccountHistory, ReceiptAP :class:`~ape.api.transactions.ReceiptAPI`: The receipt. """ - try: - address = self.provider.network.ecosystem.decode_address(account_or_hash) - return self._get_account_history(address) - except Exception as err: - # Use Transaction hash + def _get_receipt() -> Optional[ReceiptAPI]: try: return self._get_receipt(account_or_hash) except Exception: - pass + return None + + try: + address = self.provider.network.ecosystem.decode_address(account_or_hash) + history = self._get_account_history(address) + if len(history) > 0: + return history + + except Exception as err: + # Try to treat as transaction hash. + if receipt := _get_receipt(): + return receipt raise ChainError( f"'{account_or_hash}' is not a known address or transaction hash." ) from err + # No account history found. Check for transaction hash. + if receipt := _get_receipt(): + return receipt + + # Nothing found. Return empty history + return history + def _get_receipt(self, txn_hash: str) -> ReceiptAPI: receipt = self._hash_to_receipt_map.get(txn_hash) if not receipt: @@ -1697,4 +1711,8 @@ def get_receipt(self, transaction_hash: str) -> ReceiptAPI: Returns: :class:`~ape.apt.transactions.ReceiptAPI` """ - return self.chain_manager.history[transaction_hash] + receipt = self.chain_manager.history[transaction_hash] + if not isinstance(receipt, ReceiptAPI): + raise ChainError(f"No receipt found with hash '{transaction_hash}'.") + + return receipt diff --git a/src/ape/managers/converters.py b/src/ape/managers/converters.py index fa4a5430ce..89ca408b4a 100644 --- a/src/ape/managers/converters.py +++ b/src/ape/managers/converters.py @@ -347,8 +347,12 @@ def convert(self, value: Any, type: Union[Type, Tuple, List]) -> Any: try: return converter.convert(value) except Exception as err: - raise ConversionError( - f"Failed to convert '{value}' using '{converter.__class__.__name__}'." - ) from err + try: + error_value = f" '{value}' " + except Exception: + error_value = " " + + message = f"Failed to convert{error_value}using '{converter.__class__.__name__}'." + raise ConversionError(message) from err raise ConversionError(f"No conversion registered to handle '{value}'.") diff --git a/src/ape/managers/project/manager.py b/src/ape/managers/project/manager.py index af52333a91..275f24fe27 100644 --- a/src/ape/managers/project/manager.py +++ b/src/ape/managers/project/manager.py @@ -771,11 +771,10 @@ def track_deployment(self, contract: ContractInstance): destination.write_text(artifact.json()) def _create_contract_source(self, contract_type: ContractType) -> Optional[ContractSource]: - if not contract_type.source_id: + if not (source_id := contract_type.source_id): return None - src = self._lookup_source(contract_type.source_id) - if not src: + if not (src := self._lookup_source(source_id)): return None try: diff --git a/src/ape/pytest/contextmanagers.py b/src/ape/pytest/contextmanagers.py index 1e267fbaff..9bf4a6cc88 100644 --- a/src/ape/pytest/contextmanagers.py +++ b/src/ape/pytest/contextmanagers.py @@ -50,7 +50,14 @@ def _check_dev_message(self, exception: ContractLogicError): raise AssertionError(str(err)) from err if dev_message is None: - raise AssertionError("Could not find the source of the revert.") + err_message = "Could not find the source of the revert." + + # Attempt to show source traceback so the user can see the real failure. + if (info := self.revert_info) and (ex := info.value): + if tb := ex.source_traceback: + err_message = f"{err_message}\n{tb}" + + raise AssertionError(err_message) if not ( (self.dev_message.match(dev_message) is not None) @@ -156,6 +163,11 @@ def __exit__(self, exc_type: Type, exc_value: Exception, traceback) -> bool: f"However, an exception of type {type(exc_value)} occurred: {exc_value}." ) from exc_value + # Set the exception on the returned info. + # This allows the user to make further assertions on the exception. + if self.revert_info is not None: + self.revert_info.value = exc_value + if self.dev_message is not None: self._check_dev_message(exc_value) @@ -166,11 +178,6 @@ def __exit__(self, exc_type: Type, exc_value: Exception, traceback) -> bool: # Is a custom error type. self._check_custom_error(exc_value) - # Set the exception on the returned info. - # This allows the user to make further assertions on the exception. - if self.revert_info is not None: - self.revert_info.value = exc_value - # Returning True causes the expected exception not to get raised # and the test to pass return True diff --git a/src/ape/pytest/coverage.py b/src/ape/pytest/coverage.py index 08ed351eb7..9d1a2855b6 100644 --- a/src/ape/pytest/coverage.py +++ b/src/ape/pytest/coverage.py @@ -76,7 +76,6 @@ def cover( self, src_path: Path, pcs: Iterable[int], inc_fn_hits: bool = True ) -> Tuple[Set[int], List[str]]: source_id = str(get_relative_path(src_path.absolute(), self.base_path)) - if source_id not in self.report.sources: # The source is not tracked for coverage. return set(), [] @@ -87,8 +86,7 @@ def cover( if pc < 0: continue - source_coverage = self.report.get_source_coverage(source_id) - if not source_coverage: + if not (source_coverage := self.report.get_source_coverage(source_id)): continue for contract in source_coverage.contracts: diff --git a/src/ape/pytest/fixtures.py b/src/ape/pytest/fixtures.py index c16dc22719..0f9ede3b70 100644 --- a/src/ape/pytest/fixtures.py +++ b/src/ape/pytest/fixtures.py @@ -184,17 +184,14 @@ def capture(self, transaction_hash: str): if not receipt: return - contract_address = receipt.receiver or receipt.contract_address - if not contract_address: + if not (contract_address := (receipt.receiver or receipt.contract_address)): return - contract_type = self.chain_manager.contracts.get(contract_address) - if not contract_type: + if not (contract_type := self.chain_manager.contracts.get(contract_address)): # Not an invoke-transaction or a known address return - source_id = contract_type.source_id or None - if not source_id: + if not (source_id := (contract_type.source_id or None)): # Not a local or known contract type. return diff --git a/src/ape/utils/trace.py b/src/ape/utils/trace.py index 239fc94069..922a56acf5 100644 --- a/src/ape/utils/trace.py +++ b/src/ape/utils/trace.py @@ -261,18 +261,38 @@ def _parse_verbose_coverage(coverage: "CoverageReport", statement: bool = True) # It is impossible to really track. continue - row = ( - ( - fn.name, - fn.full_name, - f"{fn.lines_valid}", - f"{fn.miss_count}", - f"{round(fn.line_rate * 100, 2)}%", + if fn.name == "__builtin__": + # Create a row per unique type. + builtins = {x.tag for x in fn.statements if x.tag} + for builtin in builtins: + name_chars = [ + c + for c in builtin.lower().strip().replace(" ", "_") + if c.isalpha() or c == "_" + ] + name = f"__{''.join(name_chars).replace('dev_', '')}__" + miss = ( + 0 + if any(s.hit_count > 0 for s in fn.statements if s.tag == builtin) + else 1 + ) + rows.append( + tuple((name, name, "1", f"{miss}", "0.0%" if miss else "100.0%")) + ) + + else: + row = ( + ( + fn.name, + fn.full_name, + f"{fn.lines_valid}", + f"{fn.miss_count}", + f"{round(fn.line_rate * 100, 2)}%", + ) + if statement + else (fn.name, fn.full_name, "✓" if fn.hit_count > 0 else "x") ) - if statement - else (fn.name, fn.full_name, "✓" if fn.hit_count > 0 else "x") - ) - rows.append(row) + rows.append(row) # Handle cases where normal names are duplicated. # Use full names in this case. diff --git a/src/ape_accounts/accounts.py b/src/ape_accounts/accounts.py index 9ceb2e7fb6..04efccc8f2 100644 --- a/src/ape_accounts/accounts.py +++ b/src/ape_accounts/accounts.py @@ -50,7 +50,18 @@ class KeyfileAccount(AccountAPI): __cached_key: Optional[HexBytes] = None def __repr__(self): - return f"<{self.__class__.__name__} address={self.address} alias={self.alias}>" + # NOTE: Prevent errors from preventing repr from working. + try: + address_str = f" address={self.address} " + except Exception: + address_str = "" + + try: + alias_str = f" alias={self.alias} " + except Exception: + alias_str = "" + + return f"<{self.__class__.__name__}{address_str}{alias_str}>" @property def alias(self) -> str: diff --git a/src/ape_ethereum/ecosystem.py b/src/ape_ethereum/ecosystem.py index 32c08ecb83..32b9ed7c8a 100644 --- a/src/ape_ethereum/ecosystem.py +++ b/src/ape_ethereum/ecosystem.py @@ -209,10 +209,7 @@ def default_transaction_type(self) -> TransactionType: @classmethod def decode_address(cls, raw_address: RawAddress) -> AddressType: - raw: Union[str, HexBytes] = ( - HexBytes(raw_address) if isinstance(raw_address, int) else raw_address - ) - return to_checksum_address(raw) + return to_checksum_address(HexBytes(raw_address)[-20:].rjust(20, b"\x00")) @classmethod def encode_address(cls, address: AddressType) -> RawAddress: diff --git a/src/ape_geth/provider.py b/src/ape_geth/provider.py index 227319d10d..7844272ff9 100644 --- a/src/ape_geth/provider.py +++ b/src/ape_geth/provider.py @@ -16,6 +16,7 @@ from evm_trace import CallType, ParityTraceList from evm_trace import TraceFrame as EvmTraceFrame from evm_trace import ( + create_trace_frames, get_calltree_from_geth_call_trace, get_calltree_from_geth_trace, get_calltree_from_parity_trace, @@ -350,8 +351,8 @@ def get_transaction_trace(self, txn_hash: str) -> Iterator[TraceFrame]: frames = self._stream_request( "debug_traceTransaction", [txn_hash, {"enableMemory": True}], "result.structLogs.item" ) - for frame in frames: - yield self._create_trace_frame(EvmTraceFrame(**frame)) + for frame in create_trace_frames(frames): + yield self._create_trace_frame(frame) def _get_transaction_trace_using_call_tracer(self, txn_hash: str) -> Dict: return self._make_request( @@ -624,7 +625,7 @@ def send_call(self, txn: TransactionAPI, **kwargs: Any) -> bytes: def _trace_call(self, arguments: List[Any]) -> Tuple[Dict, Iterator[EvmTraceFrame]]: result = self._make_request("debug_traceCall", arguments) trace_data = result.get("structLogs", []) - return result, (EvmTraceFrame(**f) for f in trace_data) + return result, create_trace_frames(trace_data) def _eth_call(self, arguments: List) -> bytes: try: