Skip to content

Commit

Permalink
fix: .return_value bugs with tuples, structs, and length (#2244)
Browse files Browse the repository at this point in the history
  • Loading branch information
antazoey authored Aug 26, 2024
1 parent 637ffe2 commit a2ed551
Show file tree
Hide file tree
Showing 5 changed files with 217 additions and 33 deletions.
6 changes: 3 additions & 3 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,11 +133,11 @@
"trie>=3.0.1,<4", # Peer: stricter pin needed for uv support.
"web3[tester]>=6.17.2,<7",
# ** Dependencies maintained by ApeWorX **
"eip712>=0.2.7,<0.3",
"eip712>=0.2.10,<0.3",
"ethpm-types>=0.6.17,<0.7",
"eth_pydantic_types>=0.1.0,<0.2",
"eth_pydantic_types>=0.1.3,<0.2",
"evmchains>=0.0.10,<0.1",
"evm-trace>=0.2.0,<0.3",
"evm-trace>=0.2.3,<0.3",
],
entry_points={
"console_scripts": ["ape=ape._cli:cli"],
Expand Down
1 change: 0 additions & 1 deletion src/ape/api/transactions.py
Original file line number Diff line number Diff line change
Expand Up @@ -486,7 +486,6 @@ def return_value(self) -> Any:
Obtain the final return value of the call. Requires tracing to function,
since this is not available from the receipt object.
"""

if trace := self.trace:
ret_val = trace.return_value
return ret_val[0] if isinstance(ret_val, tuple) and len(ret_val) == 1 else ret_val
Expand Down
52 changes: 24 additions & 28 deletions src/ape_ethereum/trace.py
Original file line number Diff line number Diff line change
Expand Up @@ -225,24 +225,10 @@ def return_value(self) -> Any:
# for realistic contract interactions.
return self._return_value_from_enriched_calltree

elif abi := self.root_method_abi:
if self.call_trace_approach is TraceApproach.GETH_STRUCT_LOG_PARSE:
return_data = self._return_data_from_trace_frames
if return_data is not None:
try:
return self._ecosystem.decode_returndata(abi, return_data)
except Exception as err:
logger.debug(f"Failed decoding return data from trace frames. Error: {err}")
# Use enrichment method. It is slow but it'll at least work.

else:
# Barely enrich a calltree for performance reasons
# (likely not a need to enrich the whole thing).
calltree = self.get_raw_calltree()
enriched_calltree = self._ecosystem._enrich_returndata(calltree, abi)
return self._get_return_value_from_calltree(enriched_calltree)

return self._return_value_from_enriched_calltree
# Barely enrich a calltree for performance reasons
# (likely not a need to enrich the whole thing).
calltree = self.get_raw_calltree()
return self._get_return_value_from_calltree(calltree)

@cached_property
def _return_value_from_enriched_calltree(self) -> Any:
Expand All @@ -254,21 +240,19 @@ def _return_value_from_enriched_calltree(self) -> Any:

return self._get_return_value_from_calltree(calltree)

def _get_return_value_from_calltree(self, calltree: dict) -> Any:
# If enriching too much, Ethereum places regular values in a key
# named "unenriched_return_values".
if "unenriched_return_values" in calltree:
return calltree["unenriched_return_values"]

def _get_return_value_from_calltree(self, calltree: dict) -> tuple[Optional[Any], ...]:
num_outputs = 1
if raw_return_data := calltree.get("returndata"):
if abi := self.root_method_abi:
if abi := self._get_abi(calltree):
# Ensure we return a tuple with the correct length, even if fails.
num_outputs = len(abi.outputs)
try:
return self._ecosystem.decode_returndata(abi, HexBytes(raw_return_data))
except Exception as err:
logger.debug(f"Failed decoding raw returndata. Error: {err}")
return raw_return_data
logger.debug(f"Failed decoding raw returndata: {raw_return_data}. Error: {err}")
return tuple([None for _ in range(num_outputs)])

return None
return tuple([None for _ in range(num_outputs)])

@cached_property
def revert_message(self) -> Optional[str]:
Expand Down Expand Up @@ -446,6 +430,18 @@ def _debug_trace_transaction_struct_logs_to_call(self) -> CallTreeNode:
def _get_tree(self, verbose: bool = False) -> Tree:
return parse_rich_tree(self.enriched_calltree, verbose=verbose)

def _get_abi(self, call: dict) -> Optional[MethodABI]:
if not (addr := call.get("address")):
return self.root_method_abi
if not (calldata := call.get("calldata")):
return self.root_method_abi
if not (contract_type := self.chain_manager.contracts.get(addr)):
return self.root_method_abi
if not (calldata[:10] in contract_type.methods):
return self.root_method_abi

return contract_type.methods[calldata[:10]]


class TransactionTrace(Trace):
transaction_hash: str
Expand Down
19 changes: 18 additions & 1 deletion src/ape_test/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -456,4 +456,21 @@ class EthTesterTransactionTrace(TransactionTrace):
def return_value(self) -> Any:
# perf: skip trying anything else, because eth-tester doesn't
# yet implement any tracing RPCs.
return self._return_value_from_enriched_calltree
init_kwargs = self._get_tx_calltree_kwargs()
receipt = self.chain_manager.get_receipt(self.transaction_hash)
init_kwargs["gas_cost"] = receipt.gas_used

if not (abi := self.root_method_abi):
return (None,)

num_return = len(self.root_method_abi.outputs)

# Figure out the 'returndata' using 'eth_call' RPC.
tx = receipt.transaction.model_copy(update={"nonce": None})
try:
returndata = self.provider.send_call(tx, block_id=receipt.block_number)
except ContractLogicError:
# Unable to get the return value because even as a call, it fails.
return tuple([None for _ in range(num_return)])

return self._ecosystem.decode_returndata(abi, returndata)
Loading

0 comments on commit a2ed551

Please sign in to comment.