Skip to content

Commit

Permalink
refactor: trace api
Browse files Browse the repository at this point in the history
  • Loading branch information
antazoey committed Jan 15, 2024
1 parent 94a2705 commit 99235f5
Show file tree
Hide file tree
Showing 32 changed files with 1,317 additions and 1,134 deletions.
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@
"eth-account>=0.10.0,<0.11",
"eth-typing>=3.5.2,<4",
"eth-utils>=2.3.1,<3",
"hexbytes", # Peer
"py-geth>=3.13.0,<4",
"web3[tester]>=6.12.0,<7",
# ** Dependencies maintained by ApeWorX **
Expand Down
2 changes: 2 additions & 0 deletions src/ape/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from .projects import DependencyAPI, ProjectAPI
from .providers import BlockAPI, ProviderAPI, SubprocessProvider, TestProviderAPI, UpstreamProvider
from .query import QueryAPI, QueryType
from .trace import TraceAPI
from .transactions import ReceiptAPI, TransactionAPI

__all__ = [
Expand Down Expand Up @@ -49,6 +50,7 @@
"TestAccountAPI",
"TestAccountContainerAPI",
"TestProviderAPI",
"TraceAPI",
"TransactionAPI",
"UpstreamProvider",
]
31 changes: 5 additions & 26 deletions src/ape/api/compiler.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
from functools import cached_property
from pathlib import Path
from typing import Dict, Iterator, List, Optional, Sequence, Set, Tuple
from typing import Dict, List, Optional, Sequence, Set

from eth_pydantic_types import HexBytes
from ethpm_types import ContractType
from ethpm_types.source import Content, ContractSource
from evm_trace.geth import TraceFrame as EvmTraceFrame
from evm_trace.geth import create_call_node_data
from packaging.version import Version

from ape.api.config import PluginConfig
from ape.api.trace import TraceAPI
from ape.exceptions import APINotImplementedError, ContractLogicError
from ape.types.coverage import ContractSourceCoverage
from ape.types.trace import SourceTraceback, TraceFrame
from ape.types.trace import SourceTraceback
from ape.utils import BaseInterfaceModel, abstractmethod, raises_not_implemented


Expand Down Expand Up @@ -199,7 +198,7 @@ def enrich_error(self, err: ContractLogicError) -> ContractLogicError:

@raises_not_implemented
def trace_source( # type: ignore[empty-body]
self, contract_type: ContractType, trace: Iterator[TraceFrame], calldata: HexBytes
self, contract_type: ContractType, trace: TraceAPI, calldata: HexBytes
) -> SourceTraceback:
"""
Get a source-traceback for the given contract type.
Expand All @@ -208,7 +207,7 @@ def trace_source( # type: ignore[empty-body]
Args:
contract_type (``ContractType``): A contract type that was created by this compiler.
trace (Iterator[:class:`~ape.types.trace.TraceFrame`]): The resulting frames from
trace (Iterator:class:`~ape.types.trace.TraceFrame`]): The resulting frames from
executing a function defined in the given contract type.
calldata (``HexBytes``): Calldata passed to the top-level call.
Expand All @@ -232,26 +231,6 @@ def flatten_contract(self, path: Path, **kwargs) -> Content: # type: ignore[emp
``ethpm_types.source.Content``: The flattened contract content.
"""

def _create_contract_from_call(
self, frame: TraceFrame
) -> Tuple[Optional[ContractSource], HexBytes]:
evm_frame = EvmTraceFrame(**frame.raw)
data = create_call_node_data(evm_frame)
calldata = data.get("calldata", HexBytes(""))
if not (address := (data.get("address", frame.contract_address) or None)):
return None, calldata

try:
address = self.provider.network.ecosystem.decode_address(address)
except Exception:
return None, calldata

if address not in self.chain_manager.contracts:
return None, calldata

called_contract = self.chain_manager.contracts[address]
return self.project_manager._create_contract_source(called_contract), calldata

@raises_not_implemented
def init_coverage_profile(
self, source_coverage: ContractSourceCoverage, contract_source: ContractSource
Expand Down
12 changes: 7 additions & 5 deletions src/ape/api/networks.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
SignatureError,
)
from ape.logging import logger
from ape.types import AddressType, AutoGasLimit, CallTreeNode, ContractLog, GasLimit, RawAddress
from ape.types import AddressType, AutoGasLimit, ContractLog, GasLimit, RawAddress
from ape.utils import (
DEFAULT_TRANSACTION_ACCEPTANCE_TIMEOUT,
BaseInterfaceModel,
Expand All @@ -52,6 +52,7 @@
if TYPE_CHECKING:
from .explorers import ExplorerAPI
from .providers import BlockAPI, ProviderAPI, UpstreamProvider
from .trace import TraceAPI
from .transactions import ReceiptAPI, TransactionAPI


Expand Down Expand Up @@ -556,18 +557,19 @@ def get_method_selector(self, abi: MethodABI) -> HexBytes:

return HexBytes(keccak(text=abi.selector)[:4])

def enrich_calltree(self, call: CallTreeNode, **kwargs) -> CallTreeNode:
def enrich_trace(self, trace: "TraceAPI", **kwargs) -> "TraceAPI":
"""
Enhance the data in the call tree using information about the ecosystem.
Args:
call (:class:`~ape.types.trace.CallTreeNode`): The call tree node to enrich.
kwargs: Additional kwargs to help with enrichment.
call (:class:`~ape.api.trace.TraceAPI`): The trace to enrich.
kwargs: Additional kwargs to control enrichment, defined at the
plugin level.
Returns:
:class:`~ape.types.trace.CallTreeNode`
"""
return call
return trace

@raises_not_implemented
def get_python_types( # type: ignore[empty-body]
Expand Down
41 changes: 13 additions & 28 deletions src/ape/api/providers.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from ape.api.config import PluginConfig
from ape.api.networks import NetworkAPI
from ape.api.query import BlockTransactionQuery
from ape.api.trace import TraceAPI
from ape.api.transactions import ReceiptAPI, TransactionAPI
from ape.exceptions import (
APINotImplementedError,
Expand All @@ -30,16 +31,7 @@
VirtualMachineError,
)
from ape.logging import LogLevel, logger
from ape.types import (
AddressType,
BlockID,
CallTreeNode,
ContractCode,
ContractLog,
LogFilter,
SnapshotID,
TraceFrame,
)
from ape.types import AddressType, BlockID, ContractCode, ContractLog, LogFilter, SnapshotID
from ape.utils import (
EMPTY_BYTES32,
BaseInterfaceModel,
Expand Down Expand Up @@ -254,6 +246,14 @@ def network_choice(self) -> str:

return f"{self.network.choice}:{self.name}"

@abstractmethod
def make_request(self, rpc: str, parameters: Optional[List] = None) -> Any:
"""
Make a raw RPC request to the provider.
Advanced featues such as tracing may utilize this to by-pass unnecessary
class-serializations.
"""

def get_storage_at(self, *args, **kwargs) -> HexBytes:
warnings.warn(
"'provider.get_storage_at()' is deprecated. Use 'provider.get_storage()'.",
Expand Down Expand Up @@ -634,17 +634,15 @@ def unlock_account(self, address: AddressType) -> bool: # type: ignore[empty-bo
"""

@raises_not_implemented
def get_transaction_trace( # type: ignore[empty-body]
self, txn_hash: str
) -> Iterator[TraceFrame]:
def get_transaction_trace(self, transaction_hash: str) -> TraceAPI: # type: ignore[empty-body]
"""
Provide a detailed description of opcodes.
Args:
txn_hash (str): The hash of a transaction to trace.
transaction_hash (str): The hash of a transaction to trace.
Returns:
Iterator(:class:`~ape.type.trace.TraceFrame`): Transaction execution trace.
:class:`~ape.api.trace.TraceAPI`: A transaction trace.
"""

@raises_not_implemented
Expand Down Expand Up @@ -717,19 +715,6 @@ def poll_logs( # type: ignore[empty-body]
Iterator[:class:`~ape.types.ContractLog`]
"""

@raises_not_implemented
def get_call_tree(self, txn_hash: str) -> CallTreeNode: # type: ignore[empty-body]
"""
Create a tree structure of calls for a transaction.
Args:
txn_hash (str): The hash of a transaction to trace.
Returns:
:class:`~ape.types.trace.CallTreeNode`: Transaction execution
call-tree objects.
"""

def prepare_transaction(self, txn: TransactionAPI) -> TransactionAPI:
"""
Set default values on the transaction.
Expand Down
60 changes: 60 additions & 0 deletions src/ape/api/trace.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import sys
from abc import abstractmethod
from typing import IO, Any, Dict, List, Optional, Sequence

from ape.types import ContractFunctionPath
from ape.types.trace import GasReport
from ape.utils.basemodel import BaseInterfaceModel


class TraceAPI(BaseInterfaceModel):
"""
The class returned from
:meth:`~ape.api.providers.ProviderAPI.get_transaction_trace`.
"""

@abstractmethod
def show(self, verbose: bool = False, file: IO[str] = sys.stdout):
"""
Show the enriched trace.
"""

@abstractmethod
def get_gas_report(
self, exclude: Optional[Sequence["ContractFunctionPath"]] = None
) -> GasReport:
"""
Get the gas report.
"""

@abstractmethod
def show_gas_report(self, verbose: bool = False, file: IO[str] = sys.stdout):
"""
Show the gas report.
"""

@property
@abstractmethod
def return_value(self) -> Any:
"""
The return value deduced from the trace.
"""

@property
@abstractmethod
def revert_message(self) -> Optional[str]:
"""
The revert message deduced from the trace.
"""

@abstractmethod
def get_raw_frames(self) -> List[Dict]:
"""
Get raw trace frames for deeper analysis.
"""

@abstractmethod
def get_raw_calltree(self) -> Dict:
"""
Get a raw calltree for deeper analysis.
"""
35 changes: 10 additions & 25 deletions src/ape/api/transactions.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
AutoGasLimit,
ContractLogContainer,
SourceTraceback,
TraceFrame,
TransactionSignature,
)
from ape.utils import (
Expand All @@ -38,6 +37,7 @@

if TYPE_CHECKING:
from ape.api.providers import BlockAPI
from ape.api.trace import TraceAPI
from ape.contracts import ContractEvent


Expand Down Expand Up @@ -156,7 +156,7 @@ def receipt(self) -> Optional["ReceiptAPI"]:
return None

@property
def trace(self) -> Iterator[TraceFrame]:
def trace(self) -> "TraceAPI":
"""
The transaction trace. Only works if this transaction was published
and you are using a provider that support tracing.
Expand Down Expand Up @@ -290,10 +290,6 @@ def confirm_transaction(cls, value):
def validate_txn_hash(cls, value):
return HexBytes(value).hex()

@property
def call_tree(self) -> Optional[Any]:
return None

@property
def failed(self) -> bool:
"""
Expand Down Expand Up @@ -323,11 +319,11 @@ def ran_out_of_gas(self) -> bool:
"""

@property
def trace(self) -> Iterator[TraceFrame]:
def trace(self) -> "TraceAPI":
"""
The trace of the transaction, if available from your provider.
The :class:`~ape.api.trace.TraceAPI` of the transaction.
"""
return self.provider.get_transaction_trace(txn_hash=self.txn_hash)
return self.provider.get_transaction_trace(self.txn_hash)

@property
def _explorer(self) -> Optional[ExplorerAPI]:
Expand Down Expand Up @@ -462,21 +458,10 @@ def return_value(self) -> Any:
since this is not available from the receipt object.
"""

if not (call_tree := self.call_tree) or not (method_abi := self.method_called):
return None

if isinstance(call_tree.outputs, (str, HexBytes, int)):
output = self.provider.network.ecosystem.decode_returndata(
method_abi, HexBytes(call_tree.outputs)
)
else:
# Already enriched.
output = call_tree.outputs

if isinstance(output, tuple) and len(output) < 2:
output = output[0] if len(output) == 1 else None
if (trace := self.trace) and (ret_val := trace.return_value):
return ret_val[0] if isinstance(ret_val, tuple) and len(ret_val) == 1 else ret_val

return output
return None

@property
@raises_not_implemented
Expand Down Expand Up @@ -523,9 +508,9 @@ def track_gas(self):
if not address or not self._test_runner:
return

if self.provider.supports_tracing and (call_tree := self.call_tree):
if self.provider.supports_tracing and (trace := self.trace):
tracker = self._test_runner.gas_tracker
tracker.append_gas(call_tree.enrich(in_line=False), address)
tracker.append_gas(trace, address)

elif (
(contract_type := self.chain_manager.contracts.get(address))
Expand Down
Loading

0 comments on commit 99235f5

Please sign in to comment.