Skip to content

Commit

Permalink
refactor!: TraceAPI (#1864)
Browse files Browse the repository at this point in the history
  • Loading branch information
antazoey committed Apr 19, 2024
1 parent adbe761 commit 0881b5e
Show file tree
Hide file tree
Showing 40 changed files with 1,749 additions and 1,349 deletions.
25 changes: 13 additions & 12 deletions docs/userguides/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,8 @@ Similar to `pytest.raises()`, you can use `ape.reverts()` to assert that contrac
From our earlier example we can see this in action:

```python
import ape

def test_authorization(my_contract, owner, not_owner):
my_contract.set_owner(sender=owner)
assert owner == my_contract.owner()
Expand All @@ -328,6 +330,9 @@ If the message in the `ContractLogicError` raised by the transaction failure is
You may also supply an `re.Pattern` object to assert on a message pattern, rather than on an exact match.

```python
import ape
import re

# Matches explicitly "foo" or "bar"
with ape.reverts(re.compile(r"^(foo|bar)$")):
...
Expand Down Expand Up @@ -358,6 +363,8 @@ def check_value(_value: uint256) -> bool:
We can explicitly cause a transaction revert and check the failed line by supplying an expected `dev_message`:

```python
import ape

def test_authorization(my_contract, owner):
with ape.reverts(dev_message="dev: invalid value"):
my_contract.check_value(sender=owner)
Expand All @@ -376,6 +383,8 @@ Because `dev_message` relies on transaction tracing to function, you must use a
You may also supply an `re.Pattern` object to assert on a dev message pattern, rather than on an exact match.

```python
import ape

# Matches explictly "dev: foo" or "dev: bar"
with ape.reverts(dev_message=re.compile(r"^dev: (foo|bar)$")):
...
Expand Down Expand Up @@ -511,12 +520,10 @@ To run an entire test using a specific network / provider combination, use the `
```python
import pytest


@pytest.mark.use_network("fantom:local:test")
def test_my_fantom_test(chain):
assert chain.provider.network.ecosystem.name == "fantom"


@pytest.mark.use_network("ethereum:local:test")
def test_my_ethereum_test(chain):
assert chain.provider.network.ecosystem.name == "ethereum"
Expand Down Expand Up @@ -544,13 +551,11 @@ This is useful if certain fixtures must run in certain networks.
```python
import pytest


@pytest.fixture
def stark_contract(networks, project):
with networks.parse_network_choice("starknet:local"):
yield project.MyStarknetContract.deploy()


def test_starknet_thing(stark_contract, stark_account):
# Uses the starknet connection via the stark_contract fixture
receipt = stark_contract.my_method(sender=stark_account)
Expand All @@ -565,10 +570,11 @@ Thus, you can enter and exit a provider's context as much as you need in tests.
## Gas Reporting

To include a gas report at the end of your tests, you can use the `--gas` flag.
**NOTE**: This feature requires using a provider with tracing support, such as [ape-hardhat](https://github.com/ApeWorX/ape-hardhat).
**NOTE**: This feature works best when using a provider with tracing support, such as [ape-foundry](https://github.com/ApeWorX/ape-foundry).
When not using a provider with adequate tracing support, such as `EthTester`, gas reporting is limited to receipt-level data.

```bash
ape test --network ethereum:local:hardhat --gas
ape test --network ethereum:local:foundry --gas
```

At the end of test suite, you will see tables such as:
Expand All @@ -583,12 +589,6 @@ At the end of test suite, you will see tables such as:
changeOnStatus 2 23827 45739 34783 34783
getSecret 1 24564 24564 24564 24564

Transferring ETH Gas

Method Times called Min. Max. Mean Median
───────────────────────────────────────────────────────
to:test0 2 2400 9100 5750 5750

TestContract Gas

Method Times called Min. Max. Mean Median
Expand Down Expand Up @@ -649,6 +649,7 @@ ape test --coverage
```

**NOTE**: Some types of coverage require using a provider that supports transaction tracing, such as `ape-hardhat` or `ape-foundry`.
Without using a provider with adequate tracing support, coverage is limited to receipt-level data.

Afterwards, you should see a coverage report looking something like:

Expand Down
114 changes: 114 additions & 0 deletions docs/userguides/trace.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
# Traces

A transaction's trace frames are the individual steps the transaction took.
Using traces, Ape is able to offer features like:

1. Showing a pretty call-tree from a transaction receipt
2. Gas reporting in `ape test`
3. Coverage tools in `ape test`

Some network providers, such as Alchemy and Foundry, implement `debug_traceTransaction` and Parity's `trace_transaction` affording tracing capabilities in Ape.
**WARN**: Without RPCs for obtaining traces, some features such as gas-reporting and coverage are limited.

To see a transaction trace, use the [show_trace()](../methoddocs/api.html#ape.api.transactions.ReceiptAPI.show_trace) method on a receipt API object.

Here is an example using `show_trace()` in Python code to print out a transaction's trace.
**NOTE**: This code runs assuming you are connected to `ethereum:mainnet` using a provider with tracing RPCs.
To learn more about networks in Ape, see the [networks guide](./networks.html).

```python
from ape import chain

tx = chain.provider.get_receipt('0xb7d7f1d5ce7743e821d3026647df486f517946ef1342a1ae93c96e4a8016eab7')

# Show the steps the transaction took.
tx.show_trace()
```

You should see a (less-abridged) trace like:

```
Call trace for '0xb7d7f1d5ce7743e821d3026647df486f517946ef1342a1ae93c96e4a8016eab7'
tx.origin=0x5668EAd1eDB8E2a4d724C8fb9cB5fFEabEB422dc
DSProxy.execute(_target=LoanShifterTaker, _data=0x35..0000) -> "" [1421947 gas]
└── (delegate) LoanShifterTaker.moveLoan(
_exchangeData=[
0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE,
ZERO_ADDRESS,
...
# Abridged because is super long #
...
│ └── LendingRateOracle.getMarketBorrowRate(_asset=DAI) ->
│ 35000000000000000000000000 [1164 gas]
├── DSProxy.authority() -> DSGuard [1291 gas]
├── DSGuard.forbid(src=LoanShifterReceiver, dst=DSProxy, sig=0x1c..0000) [5253 gas]
└── DefisaverLogger.Log(
_contract=DSProxy,
_caller=tx.origin,
_logName="LoanShifter",
_data=0x00..0000
) [6057 gas]
```

Similarly, you can use the provider directly to get a trace.
This is useful if you want to interact with the trace or change some parameters for creating the trace.

```python
from ape import chain

# Change the `debug_traceTransaction` parameter dictionary
trace = chain.provider.get_transaction_trace(
"0x...", debug_trace_transaction_parameters={"enableMemory": False}
)

# You can still print the pretty call-trace (as we did in the example above)
print(trace)

# Interact with low-level logs for deeper analysis.
struct_logs = trace.get_raw_frames()
```

## Tracing Calls

Some network providers trace calls in addition to transactions.
EVM-based providers best achieve this by implementing the `debug_traceCall` RPC.

If you want to see the trace of call when making the call, use the `show_trace=` flag:

```python
token.balanceOf(account, show_trace=True)
```

**WARN**: If your provider does not properly support call-tracing (e.g. doesn't implement `debug_traceCall`), traces are limited to the top-level call.

Ape traces calls automatically when using `--gas` or `--coverage` in tests to build reports.
Learn more about testing in Ape in the [testing guide](./testing.html) and in the following sections.

## Gas Reports

To view the gas report of a transaction receipt, use the [ReceiptAPI.show_gas_report()](../methoddocs/api.html?highlight=receiptapi#ape.api.transactions.ReceiptAPI.show_gas_report) method:

```python
from ape import networks

txn_hash = "0x053cba5c12172654d894f66d5670bab6215517a94189a9ffc09bc40a589ec04d"
receipt = networks.provider.get_receipt(txn_hash)
receipt.show_gas_report()
```

It outputs tables of contracts and methods with gas usages that look like this:

```
DAI Gas
Method Times called Min. Max. Mean Median
────────────────────────────────────────────────────────────────
balanceOf 4 1302 13028 1302 1302
allowance 2 1377 1377 1337 1337
│ approve 1 22414 22414 22414 22414
│ burn 1 11946 11946 11946 11946
│ mint 1 25845 25845 25845 25845
```
119 changes: 3 additions & 116 deletions docs/userguides/transactions.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,122 +176,9 @@ ethereum:
## Traces
If you are using a provider that is able to fetch transaction traces, such as the [ape-hardhat](https://github.com/ApeWorX/ape-hardhat) provider, you can call the [`ReceiptAPI.show_trace()`](../methoddocs/api.html?highlight=receiptapi#ape.api.transactions.ReceiptAPI.show_trace) method.

```python
from ape import accounts, project
owner = accounts.load("acct")
contract = project.Contract.deploy(sender=owner)
receipt = contract.methodWithoutArguments()
receipt.show_trace()
```

**NOTE**: If your provider does not support traces, you will see a `NotImplementedError` saying that the method is not supported.

The trace might look something like:

```bash
Call trace for '0x43abb1fdadfdae68f84ce8cd2582af6ab02412f686ee2544aa998db662a5ef50'
txn.origin=0x1e59ce931B4CFea3fe4B875411e280e173cB7A9C
ContractA.methodWithoutArguments() -> 0x00..7a9c [469604 gas]
├── SYMBOL.supercluster(x=234444) -> [
│ [23523523235235, 11111111111, 234444],
│ [
│ 345345347789999991,
│ 99999998888882,
│ 345457847457457458457457457
│ ],
│ [234444, 92222229999998888882, 3454],
│ [
│ 111145345347789999991,
│ 333399998888882,
│ 234545457847457457458457457457
│ ]
│ ] [461506 gas]
├── SYMBOL.methodB1(lolol="ice-cream", dynamo=345457847457457458457457457) [402067 gas]
│ ├── ContractC.getSomeList() -> [
│ │ 3425311345134513461345134534531452345,
│ │ 111344445534535353,
│ │ 993453434534534534534977788884443333
│ │ ] [370103 gas]
│ └── ContractC.methodC1(
│ windows95="simpler",
│ jamaica=345457847457457458457457457,
│ cardinal=ContractA
│ ) [363869 gas]
├── SYMBOL.callMe(blue=tx.origin) -> tx.origin [233432 gas]
├── SYMBOL.methodB2(trombone=tx.origin) [231951 gas]
│ ├── ContractC.paperwork(ContractA) -> (
│ │ os="simpler",
│ │ country=345457847457457458457457457,
│ │ wings=ContractA
│ │ ) [227360 gas]
│ ├── ContractC.methodC1(windows95="simpler", jamaica=0, cardinal=ContractC) [222263 gas]
│ ├── ContractC.methodC2() [147236 gas]
│ └── ContractC.methodC2() [122016 gas]
├── ContractC.addressToValue(tx.origin) -> 0 [100305 gas]
├── SYMBOL.bandPractice(tx.origin) -> 0 [94270 gas]
├── SYMBOL.methodB1(lolol="lemondrop", dynamo=0) [92321 gas]
│ ├── ContractC.getSomeList() -> [
│ │ 3425311345134513461345134534531452345,
│ │ 111344445534535353,
│ │ 993453434534534534534977788884443333
│ │ ] [86501 gas]
│ └── ContractC.methodC1(windows95="simpler", jamaica=0, cardinal=ContractA) [82729 gas]
└── SYMBOL.methodB1(lolol="snitches_get_stiches", dynamo=111) [55252 gas]
├── ContractC.getSomeList() -> [
│ 3425311345134513461345134534531452345,
│ 111344445534535353,
│ 993453434534534534534977788884443333
│ ] [52079 gas]
└── ContractC.methodC1(windows95="simpler", jamaica=111, cardinal=ContractA) [48306 gas]
```

Additionally, you can view the traces of other transactions on your network.

```python
from ape import networks
txn_hash = "0x053cba5c12172654d894f66d5670bab6215517a94189a9ffc09bc40a589ec04d"
receipt = networks.provider.get_receipt(txn_hash)
receipt.show_trace()
```

In Ape, you can also show the trace for a call.
Use the `show_trace=` kwarg on a contract call and Ape will display the trace before returning the data.

```python
token.balanceOf(account, show_trace=True)
```

**NOTE**: This may not work on all providers, but it should work on common ones such as `ape-hardhat` or `ape-node`.

## Gas Reports

To view the gas report of a transaction receipt, use the [`ReceiptAPI.show_gas_report()`](../methoddocs/api.html?highlight=receiptapi#ape.api.transactions.ReceiptAPI.show_gas_report) method:

```python
from ape import networks
txn_hash = "0x053cba5c12172654d894f66d5670bab6215517a94189a9ffc09bc40a589ec04d"
receipt = networks.provider.get_receipt(txn_hash)
receipt.show_gas_report()
```

It will output tables of contracts and methods with gas usages that look like this:

```bash
DAI Gas
Method Times called Min. Max. Mean Median
────────────────────────────────────────────────────────────────
balanceOf 4 1302 13028 1302 1302
allowance 2 1377 1377 1337 1337
│ approve 1 22414 22414 22414 22414
│ burn 1 11946 11946 11946 11946
│ mint 1 25845 25845 25845 25845
```
Transaction traces are the steps in the contract the transaction took.
Traces both power a myriad of features in Ape as well are themselves a tool for developers to use to debug transactions.
To learn more about traces, see the [traces userguide](./trace.md).
## Estimate Gas Cost
Expand Down
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@
"eth-account>=0.11.2,<0.12",
"eth-typing>=3.5.2,<4",
"eth-utils>=2.3.1,<3",
"hexbytes", # Peer
"py-geth>=4.4.0,<5",
"web3[tester]>=6.17.2,<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",
]
Loading

0 comments on commit 0881b5e

Please sign in to comment.