Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ape 0.8.2 #2

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 49 additions & 63 deletions ape_alchemy/provider.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,16 @@
import os
import random
import time
from typing import Any, Dict, List, Optional, cast

from ape.api import PluginConfig, ReceiptAPI, TransactionAPI, UpstreamProvider
from ape.exceptions import (
APINotImplementedError,
ContractLogicError,
ProviderError,
VirtualMachineError,
)
from collections.abc import Iterable
from typing import Any, Optional, cast

from ape.api import ReceiptAPI, TraceAPI, TransactionAPI, UpstreamProvider, PluginConfig
from ape.exceptions import ContractLogicError, ProviderError, VirtualMachineError
from ape.logging import logger
from ape.types import CallTreeNode
from ape_ethereum.provider import Web3Provider
from ape_ethereum.trace import TransactionTrace
from eth_pydantic_types import HexBytes
from eth_typing import HexStr
from evm_trace import (
ParityTraceList,
get_calltree_from_geth_call_trace,
get_calltree_from_parity_trace,
)
from requests import HTTPError
from web3 import HTTPProvider, Web3
from web3.exceptions import ContractLogicError as Web3ContractLogicError
from web3.gas_strategies.rpc import rpc_gas_price_strategy
Expand Down Expand Up @@ -76,15 +66,16 @@ class Alchemy(Web3Provider, UpstreamProvider):
A mapping of (ecosystem_name, network_name) -> URI
"""

network_uris: Dict[tuple, str] = {}
network_uris: dict[tuple, str] = {}

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
alchemy_config = cast(AlchemyConfig, self.config_manager.get_config("alchemy"))
self.concurrency = alchemy_config.concurrency
self.block_page_size = alchemy_config.block_page_size
# overwrite for testing
self.block_page_size = 5000
self.block_page_size = 1_000_000
self.block_page_size = 2000
self.concurrency = 1

@property
Expand Down Expand Up @@ -161,34 +152,15 @@ def connect(self):
def disconnect(self):
self._web3 = None

def _get_prestate_trace(self, txn_hash: str) -> Dict:
return self._debug_trace_transaction(txn_hash, "prestateTracer")

def get_call_tree(self, txn_hash: str) -> CallTreeNode:
try:
return self._get_calltree_using_parity_style(txn_hash)
except Exception as err:
try:
return self._get_calltree_using_call_tracer(txn_hash)
except Exception:
pass

raise APINotImplementedError() from err

def _get_calltree_using_parity_style(self, txn_hash: str) -> CallTreeNode:
raw_trace_list = self._make_request("trace_transaction", [txn_hash])
trace_list = ParityTraceList.model_validate(raw_trace_list)
evm_call = get_calltree_from_parity_trace(trace_list)
return self._create_call_tree_node(evm_call)

def _get_calltree_using_call_tracer(self, txn_hash: str) -> CallTreeNode:
# Create trace frames using geth-style call tracer
calls = self._debug_trace_transaction(txn_hash, "callTracer")
evm_call = get_calltree_from_geth_call_trace(calls)
return self._create_call_tree_node(evm_call, txn_hash=txn_hash)
def _get_prestate_trace(self, transaction_hash: str) -> dict:
return self.make_request(
"debug_traceTransaction", [transaction_hash, {"tracer": "prestateTracer"}]
)

def _debug_trace_transaction(self, txn_hash: str, tracer: str) -> Dict:
return self._make_request("debug_traceTransaction", [txn_hash, {"tracer": tracer}])
def get_transaction_trace(self, transaction_hash: str, **kwargs) -> TraceAPI:
if "debug_trace_transaction_parameters" not in kwargs:
kwargs["debug_trace_transaction_parameters"] = {}
return TransactionTrace(transaction_hash=transaction_hash, **kwargs)

def get_virtual_machine_error(self, exception: Exception, **kwargs) -> VirtualMachineError:
txn = kwargs.get("txn")
Expand Down Expand Up @@ -223,16 +195,17 @@ def get_virtual_machine_error(self, exception: Exception, **kwargs) -> VirtualMa

return VirtualMachineError(message=message, txn=txn)

def _make_request(
def make_request(
self,
endpoint: str,
parameters: Optional[List] = None,
parameters: Optional[Iterable] = None,
min_retry_delay: Optional[int] = None,
retry_backoff_factor: Optional[int] = None,
max_retry_delay: Optional[int] = None,
max_retries: Optional[int] = None,
retry_jitter: Optional[int] = None,
) -> Any:
print(f"{parameters=}")
alchemy_config = cast(AlchemyConfig, self.config_manager.get_config("alchemy"))
min_retry_delay = (
min_retry_delay if min_retry_delay is not None else alchemy_config.min_retry_delay
Expand All @@ -249,25 +222,38 @@ def _make_request(
retry_jitter = retry_jitter if retry_jitter is not None else alchemy_config.retry_jitter
for attempt in range(max_retries):
try:
return super()._make_request(endpoint, parameters)
except HTTPError as err:
# safely get response date
response_data = err.response.json() if err.response else {}

# check if we have an error message, otherwise throw an error
if "error" not in response_data:
raise AlchemyProviderError(str(err)) from err

return super().make_request(endpoint, parameters)
except ProviderError as err:
print(f"{err=}")
# safely get error message
error_data = response_data["error"]
message = (
error_data.get("message", str(error_data))
if isinstance(error_data, dict)
else error_data
)
message = str(err)

# handle known error messages and continue
if any(
if "this block range should work:" in message:
# extract block from error message: this block range should work: [0xef9020, 0xf0e791]
# extract pieces within the square brackets
block_range_match = re.search(r"\[(0x[0-9a-fA-F]+),\s*(0x[0-9a-fA-F]+)\]", message)
if block_range_match:
block_start_hex = block_range_match.group(1)
block_start = int(block_start_hex, 16) # Convert from hex string
block_end_hex = block_range_match.group(2)
block_end = int(block_end_hex, 16)
block_range_int = block_start, block_end
block_range_hex = f"{block_start:x}", f"{block_end:x}"
print(f"Block range int: {block_range_int}")
print(f"Block range hex: {block_range_hex}")
else:
raise AlchemyProviderError("No valid block range found in the message.")
final_block = parameters["toBlock"]
new_params = parameters.copy()
new_params["toBlock"] = block_end_hex
results = []
results = super().make_request(endpoint, new_params)
for d in dir(results):
if not d.startswith("_"):
print(f"{d=}")
raise AlchemyProviderError("Testing.")
elif any(
error in message
for error in ["exceeded its compute units", "Too Many Requests for url"]
):
Expand Down
5 changes: 2 additions & 3 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,14 +76,14 @@
url="https://github.com/ApeWorX/ape-alchemy",
include_package_data=True,
install_requires=[
"eth-ape>=0.7.0,<0.8",
"eth-ape>=0.8.1,<0.9",
"eth-pydantic-types", # Use same version as eth-ape
"ethpm-types", # Use same version as eth-ape
"evm-trace", # Use same version as eth-ape
"web3", # Use same version as eth-ape
"requests",
],
python_requires=">=3.8,<4",
python_requires=">=3.9,<4",
extras_require=extras_require,
py_modules=["ape_alchemy"],
license="Apache-2.0",
Expand All @@ -99,7 +99,6 @@
"Operating System :: MacOS",
"Operating System :: POSIX",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
Expand Down
28 changes: 28 additions & 0 deletions tests/test_backoff.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import pytest
from requests import HTTPError
from unittest.mock import MagicMock
from ape_alchemy.provider import AlchemyProviderError

@pytest.fixture
def alchemy_instance(mock_web3, alchemy_provider):
alchemy_provider._web3 = mock_web3

# Mock the _make_request method to always raise the AlchemyProviderError
# mock_make_request = MagicMock(side_effect=AlchemyProviderError("Rate limit exceeded"))
# alchemy_provider._make_request = mock_make_request
return alchemy_provider

def test_exponential_backoff(alchemy_instance, mocker):
error_response = MagicMock()
error_response.json.return_value = {"error": {"message": "exceeded its compute units"}}
error = HTTPError(response=error_response)

# Mock the _make_request method to always raise the HTTPError
mock_make_request = mocker.patch.object(alchemy_instance, '_make_request', side_effect=error)

with pytest.raises(AlchemyProviderError, match="Rate limit exceeded after 3 attempts."):
alchemy_instance._make_request("some_endpoint", [])

assert mock_make_request.call_count == alchemy_instance.config.max_retries

# Additional tests to cover other aspects of exponential backoff can be added here.
18 changes: 7 additions & 11 deletions tests/test_providers.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import re

import pytest
from ape.exceptions import APINotImplementedError, ContractLogicError
from ape.exceptions import ContractLogicError
from ape.types import LogFilter
from hexbytes import HexBytes
from web3.exceptions import ContractLogicError as Web3ContractLogicError
Expand Down Expand Up @@ -115,8 +115,8 @@ def test_when_no_api_key_raises_error(missing_token, alchemy_provider):
"Must set one of "
"$WEB3_ALCHEMY_PROJECT_ID, "
"$WEB3_ALCHEMY_API_KEY, "
"$WEB3_ETHEREUM_GOERLI_ALCHEMY_PROJECT_ID, "
"$WEB3_ETHEREUM_GOERLI_ALCHEMY_API_KEY."
"$WEB3_ETHEREUM_SEPOLIA_ALCHEMY_PROJECT_ID, "
"$WEB3_ETHEREUM_SEPOLIA_ALCHEMY_API_KEY."
),
):
alchemy_provider.connect()
Expand Down Expand Up @@ -160,11 +160,6 @@ def test_estimate_gas_would_revert_no_message(token, alchemy_provider, mock_web3
alchemy_provider.estimate_gas_cost(transaction)


def test_feature_not_available(alchemy_provider, txn_hash):
with pytest.raises(APINotImplementedError):
list(alchemy_provider.get_transaction_trace(txn_hash))


def test_get_contract_logs(networks, alchemy_provider, mock_web3, block, log_filter):
mock_web3.eth.get_block.return_value = block
alchemy_provider._web3 = mock_web3
Expand All @@ -175,11 +170,12 @@ def test_get_contract_logs(networks, alchemy_provider, mock_web3, block, log_fil
assert actual == []


def test_get_call_tree(networks, alchemy_provider, mock_web3, parity_trace, receipt):
def test_get_transaction_trace(networks, alchemy_provider, mock_web3, parity_trace, receipt):
mock_web3.provider.make_request.return_value = [parity_trace]
mock_web3.eth.wait_for_transaction_receipt.return_value = receipt
alchemy_provider._web3 = mock_web3
networks.active_provider = alchemy_provider
actual = repr(alchemy_provider.get_call_tree(TXN_HASH))
expected = r"0xC17f2C69aE2E66FD87367E3260412EEfF637F70E\.0x96d373e5\(\) \[1401584 gas\]"
trace = alchemy_provider.get_transaction_trace(TXN_HASH)
actual = repr(trace.get_calltree())
expected = r"CALL: 0xC17f2C69aE2E66FD87367E3260412EEfF637F70E\.<0x96d373e5\> \[1401584 gas\]"
assert re.match(expected, actual)
Loading