Skip to content

Commit

Permalink
fix: handling gas-related fixes for remote nodes (#82)
Browse files Browse the repository at this point in the history
  • Loading branch information
antazoey authored Jan 12, 2024
1 parent c8213d4 commit a62550b
Show file tree
Hide file tree
Showing 4 changed files with 102 additions and 42 deletions.
18 changes: 9 additions & 9 deletions ape_foundry/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -221,14 +221,6 @@ def ws_uri(self) -> str:
# NOTE: Overriding `Web3Provider.ws_uri` implementation
return "ws" + self.uri[4:] # Remove `http` in default URI w/ `ws`

@property
def priority_fee(self) -> int:
return self.settings.priority_fee

@property
def gas_price(self) -> int:
return self.settings.gas_price

@property
def is_connected(self) -> bool:
if self._host in ("auto", None):
Expand All @@ -238,6 +230,15 @@ def is_connected(self) -> bool:
self._set_web3()
return self._web3 is not None

@property
def gas_price(self) -> int:
if self.process is not None:
# NOTE: Workaround for bug where RPC does not honor CLI flag.
return self.settings.gas_price

# Not managing node so must use RPC.
return self.web3.eth.gas_price

@cached_property
def _test_config(self) -> ApeTestConfig:
return cast(ApeTestConfig, self.config_manager.get_config("test"))
Expand Down Expand Up @@ -500,7 +501,6 @@ def send_transaction(self, txn: TransactionAPI) -> ReceiptAPI:

if sender and sender in self.unlocked_accounts:
# Allow for an unsigned transaction
sender = cast(AddressType, sender) # We know it's checksummed at this point.
txn = self.prepare_transaction(txn)
txn_dict = txn.model_dump(mode="json", by_alias=True)
if isinstance(txn_dict.get("type"), int):
Expand Down
30 changes: 20 additions & 10 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,11 @@ def networks():
return ape.networks


@pytest.fixture(scope="session")
def ethereum():
return ape.networks.ethereum


@pytest.fixture
def get_contract_type():
def fn(name: str) -> ContractType:
Expand Down Expand Up @@ -185,21 +190,26 @@ def not_owner(accounts):


@pytest.fixture(scope="session")
def local_network_api(networks):
return networks.ethereum.local
def local_network(ethereum):
return ethereum.local


@pytest.fixture(scope="session")
def mainnet_fork(ethereum):
return ethereum.mainnet_fork


@pytest.fixture
def connected_provider(name, networks, local_network_api):
with networks.ethereum.local.use_provider(name) as provider:
def connected_provider(name, ethereum, local_network):
with ethereum.local.use_provider(name) as provider:
yield provider


@pytest.fixture(scope="session")
def disconnected_provider(name, local_network_api):
def disconnected_provider(name, local_network):
return FoundryProvider(
name=name,
network=local_network_api,
network=local_network,
request_header={},
data_folder=Path("."),
provider_settings={},
Expand All @@ -212,8 +222,8 @@ def mainnet_fork_port():


@pytest.fixture
def mainnet_fork_provider(name, networks, mainnet_fork_port):
with networks.ethereum.mainnet_fork.use_provider(
def mainnet_fork_provider(name, mainnet_fork, mainnet_fork_port):
with mainnet_fork.use_provider(
name, provider_settings={"host": f"http://127.0.0.1:{mainnet_fork_port}"}
) as provider:
yield provider
Expand All @@ -225,8 +235,8 @@ def goerli_fork_port():


@pytest.fixture
def goerli_fork_provider(name, networks, goerli_fork_port):
with networks.ethereum.goerli_fork.use_provider(
def goerli_fork_provider(name, ethereum, goerli_fork_port):
with ethereum.goerli_fork.use_provider(
name, provider_settings={"host": f"http://127.0.0.1:{goerli_fork_port}"}
) as provider:
yield provider
Expand Down
34 changes: 27 additions & 7 deletions tests/test_fork_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,25 +18,21 @@ def mainnet_fork_contract_instance(owner, contract_container, mainnet_fork_provi

@pytest.mark.fork
def test_multiple_providers(
name, networks, connected_provider, mainnet_fork_port, goerli_fork_port
name, networks, ethereum, connected_provider, mainnet_fork_port, goerli_fork_port
):
default_host = "http://127.0.0.1:8545"
assert networks.active_provider.name == name
assert networks.active_provider.network.name == LOCAL_NETWORK_NAME
assert networks.active_provider.uri == default_host
mainnet_fork_host = f"http://127.0.0.1:{mainnet_fork_port}"

with networks.ethereum.mainnet_fork.use_provider(
name, provider_settings={"host": mainnet_fork_host}
):
with ethereum.mainnet_fork.use_provider(name, provider_settings={"host": mainnet_fork_host}):
assert networks.active_provider.name == name
assert networks.active_provider.network.name == "mainnet-fork"
assert networks.active_provider.uri == mainnet_fork_host
goerli_fork_host = f"http://127.0.0.1:{goerli_fork_port}"

with networks.ethereum.goerli_fork.use_provider(
name, provider_settings={"host": goerli_fork_host}
):
with ethereum.goerli_fork.use_provider(name, provider_settings={"host": goerli_fork_host}):
assert networks.active_provider.name == name
assert networks.active_provider.network.name == "goerli-fork"
assert networks.active_provider.uri == goerli_fork_host
Expand Down Expand Up @@ -203,3 +199,27 @@ def test_provider_settings(networks, network, port):

with provider_ctx as provider:
assert provider.fork_block_number == expected_block_number


@pytest.mark.fork
def test_contract_interaction(mainnet_fork_provider, owner, mainnet_fork_contract_instance, mocker):
# Spy on the estimate_gas RPC method.
estimate_gas_spy = mocker.spy(mainnet_fork_provider.web3.eth, "estimate_gas")

# Check what max gas is before transacting.
max_gas = mainnet_fork_provider.max_gas

# Invoke a method from a contract via transacting.
receipt = mainnet_fork_contract_instance.setNumber(102, sender=owner)

# Verify values from the receipt.
assert not receipt.failed
assert receipt.receiver == mainnet_fork_contract_instance.address
assert receipt.gas_used < receipt.gas_limit
assert receipt.gas_limit == max_gas

# Show contract state changed.
assert mainnet_fork_contract_instance.myNumber() == 102

# Verify the estimate gas RPC was not used (since we are using max_gas).
assert estimate_gas_spy.call_count == 0
62 changes: 46 additions & 16 deletions tests/test_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from ape.api.accounts import ImpersonatedAccount
from ape.exceptions import ContractLogicError, TransactionError
from ape.types import CallTreeNode, TraceFrame
from ape_ethereum.transactions import TransactionStatusEnum
from ape_ethereum.transactions import TransactionStatusEnum, TransactionType
from eth_pydantic_types import HashBytes32
from eth_utils import to_int
from evm_trace import CallType
Expand Down Expand Up @@ -107,8 +107,9 @@ def test_unlock_account(connected_provider, contract_a, accounts):
assert isinstance(acct, ImpersonatedAccount)

# Ensure can transact.
receipt = contract_a.methodWithoutArguments(sender=acct)
assert not receipt.failed
# NOTE: Using type 0 to avoid needing to set a balance.
receipt_0 = contract_a.methodWithoutArguments(sender=acct, type=0)
assert not receipt_0.failed


def test_get_transaction_trace(connected_provider, contract_instance, owner):
Expand Down Expand Up @@ -142,9 +143,27 @@ def test_request_timeout(connected_provider, config):
assert connected_provider.timeout == 30


def test_send_transaction(contract_instance, owner):
contract_instance.setNumber(10, sender=owner)
assert contract_instance.myNumber() == 10
def test_contract_interaction(connected_provider, owner, contract_instance, mocker):
# Spy on the estimate_gas RPC method.
estimate_gas_spy = mocker.spy(connected_provider.web3.eth, "estimate_gas")

# Check what max gas is before transacting.
max_gas = connected_provider.max_gas

# Invoke a method from a contract via transacting.
receipt = contract_instance.setNumber(102, sender=owner)

# Verify values from the receipt.
assert not receipt.failed
assert receipt.receiver == contract_instance.address
assert receipt.gas_used < receipt.gas_limit
assert receipt.gas_limit == max_gas

# Show contract state changed.
assert contract_instance.myNumber() == 102

# Verify the estimate gas RPC was not used (since we are using max_gas).
assert estimate_gas_spy.call_count == 0


def test_revert(sender, contract_instance):
Expand Down Expand Up @@ -230,10 +249,10 @@ def test_revert_error_using_impersonated_account(error_contract, accounts, conne


@pytest.mark.parametrize("host", ("https://example.com", "example.com"))
def test_host(temp_config, networks, host):
def test_host(temp_config, local_network, host):
data = {"foundry": {"host": host}}
with temp_config(data):
provider = networks.ethereum.local.get_provider("foundry")
provider = local_network.get_provider("foundry")
assert provider.uri == "https://example.com"


Expand Down Expand Up @@ -266,37 +285,37 @@ def test_get_virtual_machine_error_from_contract_logic_message_includes_base_err
assert actual.base_err == exception


def test_no_mining(temp_config, networks, connected_provider):
def test_no_mining(temp_config, local_network, connected_provider):
assert "--no-mining" not in connected_provider.build_command()
data = {"foundry": {"auto_mine": "false"}}
with temp_config(data):
provider = networks.ethereum.local.get_provider("foundry")
provider = local_network.get_provider("foundry")
cmd = provider.build_command()
assert "--no-mining" in cmd


def test_block_time(temp_config, networks, connected_provider):
def test_block_time(temp_config, local_network, connected_provider):
assert "--block-time" not in connected_provider.build_command()
data = {"foundry": {"block_time": 10}}
with temp_config(data):
provider = networks.ethereum.local.get_provider("foundry")
provider = local_network.get_provider("foundry")
cmd = provider.build_command()
assert "--block-time" in cmd
assert "10" in cmd


def test_remote_host(temp_config, networks, no_anvil_bin):
def test_remote_host(temp_config, local_network, no_anvil_bin):
data = {"foundry": {"host": "https://example.com"}}
with temp_config(data):
with pytest.raises(
FoundryProviderError,
match=r"Failed to connect to remote Anvil node at 'https://example.com'\.",
):
with networks.ethereum.local.use_provider("foundry"):
with local_network.use_provider("foundry"):
assert True


def test_remote_host_using_env_var(temp_config, networks, no_anvil_bin):
def test_remote_host_using_env_var(temp_config, local_network, no_anvil_bin):
original = os.environ.get("APE_FOUNDRY_HOST")
os.environ["APE_FOUNDRY_HOST"] = "https://example2.com"

Expand All @@ -305,7 +324,7 @@ def test_remote_host_using_env_var(temp_config, networks, no_anvil_bin):
FoundryProviderError,
match=r"Failed to connect to remote Anvil node at 'https://example2.com'\.",
):
with networks.ethereum.local.use_provider("foundry") as provider:
with local_network.use_provider("foundry") as provider:
# It shouldn't actually get to the line below,
# but in case it does, this is a helpful debug line.
assert provider.uri == os.environ["APE_FOUNDRY_HOST"], "env var not setting."
Expand Down Expand Up @@ -358,3 +377,14 @@ def test_send_transaction_when_no_error_and_receipt_fails(

finally:
connected_provider._web3 = start_web3


@pytest.mark.parametrize("tx_type", TransactionType)
def test_prepare_tx_with_max_gas(tx_type, connected_provider, ethereum, owner):
tx = ethereum.create_transaction(type=tx_type.value, sender=owner.address)
tx.gas_limit = None # Undo set from validator
assert tx.gas_limit is None, "Test setup failed - couldn't clear tx gas limit."

# NOTE: The local network by default uses max_gas.
actual = connected_provider.prepare_transaction(tx)
assert actual.gas_limit == connected_provider.max_gas

0 comments on commit a62550b

Please sign in to comment.