Skip to content

Commit

Permalink
fix: issue with logs and traces (#25)
Browse files Browse the repository at this point in the history
  • Loading branch information
antazoey authored Aug 17, 2022
1 parent afc700b commit b80ca22
Show file tree
Hide file tree
Showing 3 changed files with 176 additions and 78 deletions.
7 changes: 3 additions & 4 deletions ape_alchemy/providers.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
from web3.exceptions import ContractLogicError as Web3ContractLogicError
from web3.gas_strategies.rpc import rpc_gas_price_strategy
from web3.middleware import geth_poa_middleware
from web3.types import RPCEndpoint

_ETH_ENVIRONMENT_VARIABLE_NAMES = ("WEB3_ALCHEMY_PROJECT_ID", "WEB3_ALCHEMY_API_KEY")
_ARB_ENVIRONMENT_VARIABLE_NAMES = (
Expand Down Expand Up @@ -103,7 +102,7 @@ def get_transaction_trace(self, txn_hash: str) -> Iterator[TraceFrame]:

def get_call_tree(self, txn_hash: str) -> CallTreeNode:
receipt = self.get_transaction(txn_hash)
raw_trace_list = self._make_request("trace_transaction", [txn_hash]).get("result", [])
raw_trace_list = self._make_request("trace_transaction", [txn_hash])
trace_list = ParityTraceList.parse_obj(raw_trace_list)
return get_calltree_from_parity_trace(trace_list, gas_cost=receipt.gas_used)

Expand Down Expand Up @@ -139,9 +138,9 @@ def get_virtual_machine_error(self, exception: Exception) -> VirtualMachineError

return VirtualMachineError(message=message)

def _make_request(self, rpc: str, args: list) -> Any:
def _make_request(self, endpoint: str, parameters: list) -> Any:
try:
return self.web3.provider.make_request(RPCEndpoint(rpc), args)
return super()._make_request(endpoint, parameters)
except HTTPError as err:
response_data = err.response.json()
if "error" not in response_data:
Expand Down
36 changes: 9 additions & 27 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
from pathlib import Path

import ape
import pytest
from ape.api import EcosystemAPI, NetworkAPI, TransactionAPI
from ape.api.config import PluginConfig
from ape.api import TransactionAPI
from requests import HTTPError, Response
from web3 import Web3

Expand All @@ -20,6 +18,11 @@
)


@pytest.fixture
def networks():
return ape.networks


@pytest.fixture
def missing_token(mocker):
mock = mocker.patch("os.environ.get")
Expand All @@ -34,20 +37,6 @@ def token(mocker):
return mock


@pytest.fixture
def mock_network(mocker):
mock = mocker.MagicMock(spec=NetworkAPI)
mock.name = "MOCK_NETWORK"
mock.ecosystem = mocker.MagicMock(spec=EcosystemAPI)
mock.ecosystem.name = "ethereum"
return mock


@pytest.fixture
def mock_config(mocker):
return mocker.MagicMock(spec=PluginConfig)


@pytest.fixture
def mock_web3(mocker):
mock = mocker.MagicMock(spec=Web3)
Expand Down Expand Up @@ -81,12 +70,5 @@ def feature_not_available_http_error(mocker, request):


@pytest.fixture
def alchemy_provider(mock_network, mock_config) -> AlchemyEthereumProvider:
return AlchemyEthereumProvider(
name="alchemy",
network=mock_network,
config=mock_config,
request_header={},
data_folder=Path("."),
provider_settings={},
)
def alchemy_provider(networks) -> AlchemyEthereumProvider:
return networks.get_provider_from_choice("ethereum:rinkeby:alchemy")
211 changes: 164 additions & 47 deletions tests/test_providers.py
Original file line number Diff line number Diff line change
@@ -1,69 +1,186 @@
import re

import pytest
from ape.exceptions import ContractLogicError
from ape.types import LogFilter
from hexbytes import HexBytes
from web3.exceptions import ContractLogicError as Web3ContractLogicError

from ape_alchemy.providers import AlchemyFeatureNotAvailable, MissingProjectKeyError

TXN_HASH = "0x3cef4aaa52b97b6b61aa32b3afcecb0d14f7862ca80fdc76504c37a9374645c4"


@pytest.fixture
def parity_trace():
return {
"action": {
"from": "0x5cab1e5286529370880776461c53a0e47d74fb63",
"callType": "call",
"gas": "0x17e6f0",
"input": "0x96d373e5",
"to": "0xc17f2c69ae2e66fd87367e3260412eeff637f70e",
"value": "0x0",
},
"blockHash": "0xa7e0792b07687130af6042d9e295e7a96d83a34f40fe01074348cac5c5dd0699",
"blockNumber": 15104985,
"result": {"gasUsed": "0x1562f0", "output": "0x"},
"subtraces": 1,
"traceAddress": [],
"transactionHash": TXN_HASH,
"transactionPosition": 259,
"type": "call",
}


@pytest.fixture
def log_filter():
return LogFilter(
address=["0xF7F78379391C5dF2Db5B66616d18fF92edB82022"],
fromBlock="0x3",
toBlock="0x3",
topics=[
"0x1a7c56fae0af54ebae73bc4699b9de9835e7bb86b050dff7e80695b633f17abd",
[
"0x0000000000000000000000000000000000000000000000000000000000000000",
"0x0000000000000000000000000000000000000000000000000000000000000001",
],
],
)


@pytest.fixture
def block():
return {
"transactions": [],
"hash": HexBytes("0xae1960ba0513948a507b652def457305d490d24bc0dd131d8d02be56564a3ee2"),
"number": 0,
"parentHash": HexBytes(
"0x0000000000000000000000000000000000000000000000000000000000000000"
),
"size": 517,
"timestamp": 1660338772,
"gasLimit": 30029122,
"gasUsed": 0,
"baseFeePerGas": 1000000000,
"difficulty": 131072,
"totalDifficulty": 131072,
}


@pytest.fixture
def receipt():
return {
"blockNumber": 15329094,
"data": b"0xa9059cbb00000000000000000000000016b308eb4591d9b4e34034ca2ff992d224b9927200000000000000000000000000000000000000000000000000000000030a32c0", # noqa: E501
"gasLimit": 79396,
"gasPrice": 14200000000,
"gasUsed": 65625,
"logs": [
{
"blockHash": HexBytes(
"0x141a61b8c738c0f1508728116049a0d4a6ff41ee1180d956148880f32ae99215"
),
"address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"logIndex": 213,
"data": HexBytes(
"0x00000000000000000000000000000000000000000000000000000000030a32c0"
),
"removed": False,
"topics": [
HexBytes("0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"),
HexBytes("0x000000000000000000000000958f973513f723f2cb9b47abe5e903695ab93e36"),
HexBytes("0x00000000000000000000000016b308eb4591d9b4e34034ca2ff992d224b99272"),
],
"blockNumber": 15329094,
"transactionIndex": 132,
"transactionHash": HexBytes(
"0x9e4be62c1a16caacaccd9d8c7706b75dc17a957ec6c5dea418a137a5c3a197c5"
),
}
],
"nonce": 16,
"receiver": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"sender": "0x958f973513F723f2cB9b47AbE5e903695aB93e36",
"status": 1,
"hash": TXN_HASH,
"value": 0,
}


def test_when_no_api_key_raises_error(missing_token, alchemy_provider):
with pytest.raises(
MissingProjectKeyError,
match=re.escape("Must set one of $WEB3_ALCHEMY_PROJECT_ID, $WEB3_ALCHEMY_API_KEY."),
):
alchemy_provider.connect()

class TestAlchemyEthereumProvider:
def test_when_no_api_key_raises_error(self, missing_token, alchemy_provider):
with pytest.raises(MissingProjectKeyError) as err:
alchemy_provider.connect()

expected = "Must set one of $WEB3_ALCHEMY_PROJECT_ID, $WEB3_ALCHEMY_API_KEY."
assert expected in str(err.value)
def test_send_transaction_reverts(token, alchemy_provider, mock_web3, mock_transaction):
expected_revert_message = "EXPECTED REVERT MESSAGE"
mock_web3.eth.send_raw_transaction.side_effect = Web3ContractLogicError(
f"execution reverted : {expected_revert_message}"
)
alchemy_provider._web3 = mock_web3

def test_send_transaction_reverts(self, token, alchemy_provider, mock_web3, mock_transaction):
expected_revert_message = "EXPECTED REVERT MESSAGE"
mock_web3.eth.send_raw_transaction.side_effect = Web3ContractLogicError(
f"execution reverted : {expected_revert_message}"
)
alchemy_provider._web3 = mock_web3
with pytest.raises(ContractLogicError, match=expected_revert_message):
alchemy_provider.send_transaction(mock_transaction)

with pytest.raises(ContractLogicError) as err:
alchemy_provider.send_transaction(mock_transaction)

assert err.value.revert_message == expected_revert_message
def test_send_transaction_reverts_no_message(token, alchemy_provider, mock_web3, mock_transaction):
mock_web3.eth.send_raw_transaction.side_effect = Web3ContractLogicError("execution reverted")
alchemy_provider._web3 = mock_web3

def test_send_transaction_reverts_no_message(
self, token, alchemy_provider, mock_web3, mock_transaction
):
mock_web3.eth.send_raw_transaction.side_effect = Web3ContractLogicError(
"execution reverted"
)
alchemy_provider._web3 = mock_web3
with pytest.raises(ContractLogicError, match="Transaction failed."):
alchemy_provider.send_transaction(mock_transaction)

with pytest.raises(ContractLogicError):
alchemy_provider.send_transaction(mock_transaction)

def test_estimate_gas_would_revert(self, token, alchemy_provider, mock_web3, mock_transaction):
expected_revert_message = "EXPECTED REVERT MESSAGE"
mock_web3.eth.estimate_gas.side_effect = Web3ContractLogicError(
f"execution reverted : {expected_revert_message}"
)
alchemy_provider._web3 = mock_web3
def test_estimate_gas_would_revert(token, alchemy_provider, mock_web3, mock_transaction):
expected_revert_message = "EXPECTED REVERT MESSAGE"
mock_web3.eth.estimate_gas.side_effect = Web3ContractLogicError(
f"execution reverted : {expected_revert_message}"
)
alchemy_provider._web3 = mock_web3

with pytest.raises(ContractLogicError) as err:
alchemy_provider.estimate_gas_cost(mock_transaction)
with pytest.raises(ContractLogicError, match=expected_revert_message):
alchemy_provider.estimate_gas_cost(mock_transaction)

assert err.value.revert_message == expected_revert_message

def test_estimate_gas_would_revert_no_message(
self, token, alchemy_provider, mock_web3, mock_transaction
):
mock_web3.eth.estimate_gas.side_effect = Web3ContractLogicError("execution reverted")
alchemy_provider._web3 = mock_web3
def test_estimate_gas_would_revert_no_message(token, alchemy_provider, mock_web3, mock_transaction):
mock_web3.eth.estimate_gas.side_effect = Web3ContractLogicError("execution reverted")
alchemy_provider._web3 = mock_web3

with pytest.raises(ContractLogicError, match="Transaction failed."):
alchemy_provider.estimate_gas_cost(mock_transaction)

with pytest.raises(ContractLogicError):
alchemy_provider.estimate_gas_cost(mock_transaction)

def test_feature_not_available(
self, token, alchemy_provider, mock_web3, txn_hash, feature_not_available_http_error
def test_feature_not_available(
alchemy_provider, mock_web3, txn_hash, feature_not_available_http_error
):
mock_web3.provider.make_request.side_effect = feature_not_available_http_error
alchemy_provider._web3 = mock_web3

with pytest.raises(
AlchemyFeatureNotAvailable, match=feature_not_available_http_error.response.fixture_param
):
mock_web3.provider.make_request.side_effect = feature_not_available_http_error
alchemy_provider._web3 = mock_web3
_ = [t for t in 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
networks.active_provider = alchemy_provider
actual = [x for x in alchemy_provider.get_contract_logs(log_filter)]

# Fails when improper response handling of logs (is part of bug fix)
assert actual == []

with pytest.raises(AlchemyFeatureNotAvailable) as err:
_ = [t for t in alchemy_provider.get_transaction_trace(txn_hash)]

assert str(err.value) == feature_not_available_http_error.response.fixture_param
def test_get_call_tree(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 = "CALL: 0xC17f2C69aE2E66FD87367E3260412EEfF637F70E.<0x96d373e5> [1401584 gas]"
assert actual == expected

0 comments on commit b80ca22

Please sign in to comment.