diff --git a/src/ape/managers/chain.py b/src/ape/managers/chain.py index fd27243fd3..6e30f51bfc 100644 --- a/src/ape/managers/chain.py +++ b/src/ape/managers/chain.py @@ -823,12 +823,30 @@ def __delitem__(self, address: AddressType): if address in self._local_contract_types: del self._local_contract_types[address] - if self._is_live_network: - if not self._contract_types_cache.is_dir(): - return + # Delete proxy. + if address in self._local_proxies: + info = self._local_proxies[address] + target = info.target + del self._local_proxies[address] - address_file = self._contract_types_cache / f"{address}.json" - address_file.unlink(missing_ok=True) + # Also delete target. + if target in self._local_contract_types: + del self._local_contract_types[target] + + if self._is_live_network: + if self._contract_types_cache.is_dir(): + address_file = self._contract_types_cache / f"{address}.json" + address_file.unlink(missing_ok=True) + + if self._proxy_info_cache.is_dir(): + disk_info = self._get_proxy_info_from_disk(address) + if disk_info: + target = disk_info.target + address_file = self._proxy_info_cache / f"{address}.json" + address_file.unlink() + + # Also delete the target. + self.__delitem__(target) def __contains__(self, address: AddressType) -> bool: return self.get(address) is not None diff --git a/src/ape_ethereum/proxies.py b/src/ape_ethereum/proxies.py index f485a4ea52..36ed36be19 100644 --- a/src/ape_ethereum/proxies.py +++ b/src/ape_ethereum/proxies.py @@ -7,9 +7,10 @@ from ape.api.networks import ProxyInfoAPI from ape.contracts import ContractContainer +MINIMAL_PROXY_TARGET_PLACEHOLDER = "bebebebebebebebebebebebebebebebebebebebe" MINIMAL_PROXY_BYTES = ( "0x3d602d80600a3d3981f3363d3d373d3d3d363d73" - "bebebebebebebebebebebebebebebebebebebebe5af43d82803e903d91602b57fd5bf3" + f"{MINIMAL_PROXY_TARGET_PLACEHOLDER}5af43d82803e903d91602b57fd5bf3" ) @@ -87,8 +88,10 @@ class ProxyInfo(ProxyInfoAPI): ) -def _make_minimal_proxy() -> ContractContainer: - bytecode = {"bytecode": MINIMAL_PROXY_BYTES} +def _make_minimal_proxy(address: str = MINIMAL_PROXY_TARGET_PLACEHOLDER) -> ContractContainer: + address = address.replace("0x", "") + code = MINIMAL_PROXY_BYTES.replace(MINIMAL_PROXY_TARGET_PLACEHOLDER, address) + bytecode = {"bytecode": code} contract_type = ContractType(abi=[], deploymentBytecode=bytecode) return ContractContainer(contract_type=contract_type) diff --git a/tests/functional/conftest.py b/tests/functional/conftest.py index 240263294e..e0a75bc790 100644 --- a/tests/functional/conftest.py +++ b/tests/functional/conftest.py @@ -15,6 +15,7 @@ from ape.logging import LogLevel from ape.logging import logger as _logger from ape.types import AddressType, ContractLog +from ape_ethereum.proxies import minimal_proxy as _minimal_proxy_container PROJECT_PATH = Path(__file__).parent CONTRACTS_FOLDER = PROJECT_PATH / "data" / "contracts" / "ethereum" / "local" @@ -503,3 +504,13 @@ def vyper_factory(owner, get_contract_type): def vyper_blueprint(owner, vyper_contract_container): receipt = owner.declare(vyper_contract_container) return receipt.contract_address + + +@pytest.fixture +def minimal_proxy_container(): + return _minimal_proxy_container + + +@pytest.fixture +def minimal_proxy(owner, minimal_proxy_container): + return owner.deploy(minimal_proxy_container) diff --git a/tests/functional/test_block_container.py b/tests/functional/test_block_container.py new file mode 100644 index 0000000000..d2f795e491 --- /dev/null +++ b/tests/functional/test_block_container.py @@ -0,0 +1,149 @@ +import time +from queue import Queue +from typing import List + +import pytest +from ethpm_types import HexBytes + +from ape.exceptions import ChainError + + +def test_iterate_blocks(chain_that_mined_5): + blocks = [b for b in chain_that_mined_5.blocks] + assert len(blocks) >= 5 # Because mined 5 blocks so is at least 5 + + iterator = blocks[0].number + for block in blocks: + assert block.number == iterator + iterator += 1 + + +def test_blocks_range(chain_that_mined_5): + # The number of the block before mining the 5 + start_block = len(chain_that_mined_5.blocks) - 5 + num_to_get = 3 # Expecting blocks [s, s+1, s+2] + blocks = [b for b in chain_that_mined_5.blocks.range(start_block, start_block + num_to_get)] + assert len(blocks) == num_to_get + + expected_number = start_block + prev_block_hash = ( + HexBytes("0x0000000000000000000000000000000000000000000000000000000000000000") + if start_block == 0 + else chain_that_mined_5.blocks[start_block - 1].hash + ) + for block in blocks: + assert block.number == expected_number + expected_number += 1 + assert block.parent_hash == prev_block_hash + prev_block_hash = block.hash + + +def test_blocks_range_too_high_stop(chain_that_mined_5): + num_blocks = len(chain_that_mined_5.blocks) + num_blocks_add_1 = num_blocks + 1 + expected = ( + rf"'stop={num_blocks_add_1}' cannot be greater than the chain length \({num_blocks}\)\. " + rf"Use 'poll_blocks\(\)' to wait for future blocks\." + ) + with pytest.raises(ChainError, match=expected): + # Have to run through generator to trigger code in definition. + list(chain_that_mined_5.blocks.range(num_blocks_add_1)) + + +def test_block_range_with_step(chain_that_mined_5): + blocks = [b for b in chain_that_mined_5.blocks.range(3, step=2)] + assert len(blocks) == 2 + assert blocks[0].number == 0 + assert blocks[1].number == 2 + + +def test_block_range_negative_start(chain_that_mined_5): + with pytest.raises(ValueError) as err: + _ = [b for b in chain_that_mined_5.blocks.range(-1, 3, step=2)] + + assert "ensure this value is greater than or equal to 0" in str(err.value) + + +def test_block_range_out_of_order(chain_that_mined_5): + with pytest.raises(ValueError) as err: + _ = [b for b in chain_that_mined_5.blocks.range(3, 1, step=2)] + + assert "stop_block: '0' cannot be less than start_block: '3'." in str(err.value) + + +def test_block_timestamp(chain): + chain.mine() + assert chain.blocks.head.timestamp == chain.blocks.head.datetime.timestamp() + + +def test_poll_blocks_stop_block_not_in_future(chain_that_mined_5): + bad_stop_block = chain_that_mined_5.blocks.height + + with pytest.raises(ValueError, match="'stop' argument must be in the future."): + _ = [x for x in chain_that_mined_5.blocks.poll_blocks(stop_block=bad_stop_block)] + + +def test_poll_blocks(chain_that_mined_5, eth_tester_provider, owner, PollDaemon): + blocks: Queue = Queue(maxsize=3) + poller = chain_that_mined_5.blocks.poll_blocks() + + with PollDaemon("blocks", poller, blocks.put, blocks.full): + # Sleep first to ensure listening before mining. + time.sleep(1) + eth_tester_provider.mine(3) + + assert blocks.full() + first = blocks.get().number + second = blocks.get().number + third = blocks.get().number + assert first == second - 1 + assert second == third - 1 + + +def test_poll_blocks_reorg(chain_that_mined_5, eth_tester_provider, owner, PollDaemon, caplog): + blocks: Queue = Queue(maxsize=6) + poller = chain_that_mined_5.blocks.poll_blocks() + + with PollDaemon("blocks", poller, blocks.put, blocks.full): + # Sleep first to ensure listening before mining. + time.sleep(1) + + snapshot = chain_that_mined_5.snapshot() + chain_that_mined_5.mine(2) + + # Wait to allow blocks before re-org to get yielded + time.sleep(5) + + # Simulate re-org by reverting to the snapshot + chain_that_mined_5.restore(snapshot) + + # Allow it time to trigger realizing there was a re-org + time.sleep(1) + chain_that_mined_5.mine(2) + time.sleep(1) + + chain_that_mined_5.mine(3) + + assert blocks.full() + + # Show that re-org was detected + expected_error = ( + "Chain has reorganized since returning the last block. " + "Try adjusting the required network confirmations." + ) + assert caplog.records, "Didn't detect re-org" + assert expected_error in caplog.records[-1].message + + # Show that there are duplicate blocks + block_numbers: List[int] = [blocks.get().number for _ in range(6)] + assert len(set(block_numbers)) < len(block_numbers) + + +def test_poll_blocks_timeout( + vyper_contract_instance, chain_that_mined_5, eth_tester_provider, owner, PollDaemon +): + poller = chain_that_mined_5.blocks.poll_blocks(new_block_timeout=1) + + with pytest.raises(ChainError, match=r"Timed out waiting for new block \(time_waited=1.\d+\)."): + with PollDaemon("blocks", poller, lambda x: None, lambda: False): + time.sleep(1.5) diff --git a/tests/functional/test_chain.py b/tests/functional/test_chain.py index c06ec0c521..3469748979 100644 --- a/tests/functional/test_chain.py +++ b/tests/functional/test_chain.py @@ -1,31 +1,8 @@ -import time from datetime import datetime, timedelta -from queue import Queue -from typing import List import pytest -from ethpm_types import ContractType, HexBytes -import ape -from ape.contracts import ContractInstance -from ape.exceptions import ( - APINotImplementedError, - ChainError, - ContractNotFoundError, - ConversionError, -) -from ape_ethereum.transactions import Receipt, TransactionStatusEnum -from tests.conftest import explorer_test, skip_if_plugin_installed - - -@pytest.fixture -def contract_0(project_with_contract): - return project_with_contract.ApeContract0 - - -@pytest.fixture -def contract_1(project_with_contract): - return project_with_contract.ApeContract1 +from ape.exceptions import APINotImplementedError, ChainError def test_snapshot_and_restore(chain, owner, receiver, vyper_contract_instance): @@ -71,6 +48,29 @@ def test_snapshot_and_restore(chain, owner, receiver, vyper_contract_instance): assert receiver.balance == initial_balance +def test_snapshot_and_restore_unknown_snapshot_id(chain): + _ = chain.snapshot() + chain.mine() + snapshot_id_2 = chain.snapshot() + chain.mine() + snapshot_id_3 = chain.snapshot() + chain.mine() + + # After restoring to the second ID, the third ID is now invalid. + chain.restore(snapshot_id_2) + + with pytest.raises(ChainError) as err: + chain.restore(snapshot_id_3) + + assert "Unknown snapshot ID" in str(err.value) + + +def test_snapshot_and_restore_no_snapshots(chain): + chain._snapshots = [] # Ensure empty (gets set in test setup) + with pytest.raises(ChainError, match="There are no snapshots to revert to."): + chain.restore() + + def test_isolate(chain, vyper_contract_instance, owner): number_at_start = 444 vyper_contract_instance.setNumber(number_at_start, sender=owner) @@ -113,156 +113,6 @@ def test_get_receipt_from_history(chain, vyper_contract_instance, owner): assert actual.receiver == expected.receiver -def test_snapshot_and_restore_unknown_snapshot_id(chain): - _ = chain.snapshot() - chain.mine() - snapshot_id_2 = chain.snapshot() - chain.mine() - snapshot_id_3 = chain.snapshot() - chain.mine() - - # After restoring to the second ID, the third ID is now invalid. - chain.restore(snapshot_id_2) - - with pytest.raises(ChainError) as err: - chain.restore(snapshot_id_3) - - assert "Unknown snapshot ID" in str(err.value) - - -def test_snapshot_and_restore_no_snapshots(chain): - chain._snapshots = [] # Ensure empty (gets set in test setup) - with pytest.raises(ChainError, match="There are no snapshots to revert to."): - chain.restore() - - -def test_history(sender, receiver, chain): - length_at_start = len(chain.history[sender].sessional) - receipt = sender.transfer(receiver, "1 wei") - transactions_from_cache = list(sender.history) - assert len(transactions_from_cache) == length_at_start + 1 - assert sender.history[-1] == receipt - assert sender.history[0:] == transactions_from_cache[0:] - assert sender.history[:-1] == transactions_from_cache[:-1] - - txn = transactions_from_cache[-1] - assert txn.sender == receipt.sender == sender - assert txn.receiver == receipt.receiver == receiver - - -@explorer_test -def test_history_caches_sender_over_address_key( - mocker, chain, eth_tester_provider, sender, vyper_contract_container, ethereum -): - # When getting receipts from the explorer for contracts, it includes transactions - # made to the contract. This test shows we cache by sender and not address key. - contract = sender.deploy(vyper_contract_container, 0) - network = ethereum.local - txn = ethereum.create_transaction( - receiver=contract.address, sender=sender.address, value=10000000000000000000000 - ) - known_receipt = Receipt( - block_number=10, - gas_price=11, - gas_used=12, - gas_limit=13, - status=TransactionStatusEnum.NO_ERROR.value, - txn_hash="0x98d2aee8617897b5983314de1d6ff44d1f014b09575b47a88267971beac97b2b", - transaction=txn, - ) - - # The receipt is already known and cached by the sender. - chain.history.append(known_receipt) - - # We ask for receipts from the contract, but it returns ones sent to the contract. - def get_txns_patch(address): - if address == contract.address: - yield from [known_receipt] - - mock_explorer = mocker.MagicMock() - mock_explorer.get_account_transactions.side_effect = get_txns_patch - network.__dict__["explorer"] = mock_explorer - eth_tester_provider.network = network - - # Previously, this would error because the receipt was cached with the wrong sender - try: - actual = [t for t in chain.history[contract.address].sessional] - - # Actual is 0 because the receipt was cached under the sender. - assert len(actual) == 0 - finally: - if "explorer" in network.__dict__: - del network.__dict__["explorer"] - - -def test_iterate_blocks(chain_that_mined_5): - blocks = [b for b in chain_that_mined_5.blocks] - assert len(blocks) >= 5 # Because mined 5 blocks so is at least 5 - - iterator = blocks[0].number - for block in blocks: - assert block.number == iterator - iterator += 1 - - -def test_blocks_range(chain_that_mined_5): - # The number of the block before mining the 5 - start_block = len(chain_that_mined_5.blocks) - 5 - num_to_get = 3 # Expecting blocks [s, s+1, s+2] - blocks = [b for b in chain_that_mined_5.blocks.range(start_block, start_block + num_to_get)] - assert len(blocks) == num_to_get - - expected_number = start_block - prev_block_hash = ( - HexBytes("0x0000000000000000000000000000000000000000000000000000000000000000") - if start_block == 0 - else chain_that_mined_5.blocks[start_block - 1].hash - ) - for block in blocks: - assert block.number == expected_number - expected_number += 1 - assert block.parent_hash == prev_block_hash - prev_block_hash = block.hash - - -def test_blocks_range_too_high_stop(chain_that_mined_5): - num_blocks = len(chain_that_mined_5.blocks) - num_blocks_add_1 = num_blocks + 1 - expected = ( - rf"'stop={num_blocks_add_1}' cannot be greater than the chain length \({num_blocks}\)\. " - rf"Use 'poll_blocks\(\)' to wait for future blocks\." - ) - with pytest.raises(ChainError, match=expected): - # Have to run through generator to trigger code in definition. - list(chain_that_mined_5.blocks.range(num_blocks_add_1)) - - -def test_block_range_with_step(chain_that_mined_5): - blocks = [b for b in chain_that_mined_5.blocks.range(3, step=2)] - assert len(blocks) == 2 - assert blocks[0].number == 0 - assert blocks[1].number == 2 - - -def test_block_range_negative_start(chain_that_mined_5): - with pytest.raises(ValueError) as err: - _ = [b for b in chain_that_mined_5.blocks.range(-1, 3, step=2)] - - assert "ensure this value is greater than or equal to 0" in str(err.value) - - -def test_block_range_out_of_order(chain_that_mined_5): - with pytest.raises(ValueError) as err: - _ = [b for b in chain_that_mined_5.blocks.range(3, 1, step=2)] - - assert "stop_block: '0' cannot be less than start_block: '3'." in str(err.value) - - -def test_block_timestamp(chain): - chain.mine() - assert chain.blocks.head.timestamp == chain.blocks.head.datetime.timestamp() - - def test_set_pending_timestamp(chain): start_timestamp = chain.pending_timestamp chain.pending_timestamp += 3600 @@ -288,390 +138,6 @@ def test_set_pending_timestamp_failure(chain): ) -def test_cache_deployment_live_network( - chain, - vyper_contract_instance, - vyper_contract_container, - remove_disk_writes_deployments, - dummy_live_network, -): - # Arrange - Ensure the contract is not cached anywhere - address = vyper_contract_instance.address - contract_name = vyper_contract_instance.contract_type.name - deployments = chain.contracts._deployments - contract_types = chain.contracts._local_contract_types - chain.contracts._local_contract_types = { - a: ct for a, ct in contract_types.items() if a != address - } - chain.contracts._deployments = {n: d for n, d in deployments.items() if n != contract_name} - - # Act - chain.contracts.cache_deployment(vyper_contract_instance) - - # Assert - actual_deployments = chain.contracts.get_deployments(vyper_contract_container) - actual_contract_type = chain.contracts._get_contract_type_from_disk(address) - expected = vyper_contract_instance.contract_type - assert len(actual_deployments) == 1 - assert actual_deployments[0].address == address - assert actual_deployments[0].txn_hash == vyper_contract_instance.txn_hash - assert chain.contracts.get(address) == expected - assert actual_contract_type == expected - - -def test_contract_caches_default_contract_type_when_used(solidity_contract_instance, chain, config): - address = solidity_contract_instance.address - contract_type = solidity_contract_instance.contract_type - - # Delete contract from local cache if it's there - if address in chain.contracts._local_contract_types: - del chain.contracts._local_contract_types[address] - - # Delete cache file if it exists - cache_file = chain.contracts._contract_types_cache / f"{address}.json" - if cache_file.is_file(): - cache_file.unlink() - - # Create a contract using the contract type when nothing is cached. - contract = ape.Contract(address, contract_type=contract_type) - assert isinstance(contract, ContractInstance) - - # Ensure we don't need the contract type when creating it the second time. - contract = ape.Contract(address) - assert isinstance(contract, ContractInstance) - - def test_set_balance(chain, owner): with pytest.raises(APINotImplementedError): chain.set_balance(owner, "1000 ETH") - - -def test_instance_at(chain, contract_instance): - contract = chain.contracts.instance_at(str(contract_instance.address)) - assert contract.contract_type == contract_instance.contract_type - - -def test_instance_at_unknown_hex_str(chain, contract_instance): - # Fails when decoding Ethereum address and NOT conversion error. - hex_str = "0x1402b10CA274cD76C441e16C844223F79D3566De12bb12b0aebFE41aDFAe302" - with pytest.raises(ValueError, match=f"Unknown address value '{hex_str}'."): - chain.contracts.instance_at(hex_str) - - -def test_instance_at_when_given_contract_type(chain, contract_instance): - contract = chain.contracts.instance_at( - str(contract_instance.address), contract_type=contract_instance.contract_type - ) - assert contract.contract_type == contract_instance.contract_type - - -def test_instance_at_when_given_name_as_contract_type(chain, contract_instance): - expected_match = "Expected type 'ContractType' for argument 'contract_type'." - with pytest.raises(TypeError, match=expected_match): - address = str(contract_instance.address) - bad_contract_type = contract_instance.contract_type.name - chain.contracts.instance_at(address, contract_type=bad_contract_type) - - -@explorer_test -def test_instance_at_uses_given_contract_type_when_retrieval_fails(mocker, chain, caplog): - # The manager always attempts retrieval so that default contact types can - # get cached. However, sometimes an explorer plugin may fail. If given a contract-type - # in that situation, we can use it and not fail and log the error instead. - expected_contract_type = ContractType(contractName="foo", sourceId="foo.bar") - new_address = "0x4a986a6dCA6dbf99bC3d17F8D71aFb0d60e740f8" - expected_fail_message = "LOOK_FOR_THIS_FAIL_MESSAGE" - existing_fn = chain.contracts.get - - def fn(addr, default=None): - if addr == new_address: - raise ValueError(expected_fail_message) - - return existing_fn(addr, default=default) - - chain.contracts.get = mocker.MagicMock() - chain.contracts.get.side_effect = fn - - actual = chain.contracts.instance_at(new_address, contract_type=expected_contract_type) - assert actual.contract_type == expected_contract_type - assert caplog.records[-1].message == expected_fail_message - - -@explorer_test -def test_instance_at_contract_type_not_found(chain): - new_address = "0x4a986a6dca6dbF99Bc3D17F8d71aFB0D60E740F9" - expected = ( - rf"Failed to get contract type for address '{new_address}'. " - r"Current provider 'ethereum:local:test' has no associated explorer plugin. " - "Try installing an explorer plugin using .*ape plugins install etherscan.*, " - r"or using a network with explorer support\." - ) - with pytest.raises(ContractNotFoundError, match=expected): - chain.contracts.instance_at(new_address) - - -@explorer_test -def test_contracts_getitem_contract_not_found(chain): - new_address = "0x4a986a6dca6dbF99Bc3D17F8d71aFB0D60E740F9" - expected = ( - rf"Failed to get contract type for address '{new_address}'. " - r"Current provider 'ethereum:local:test' has no associated explorer plugin. " - "Try installing an explorer plugin using .*ape plugins install etherscan.*, " - r"or using a network with explorer support\." - ) - with pytest.raises(IndexError, match=expected): - _ = chain.contracts[new_address] - - -def test_deployments_mapping_cache_location(chain): - # Arrange / Act - mapping_location = chain.contracts._deployments_mapping_cache - split_mapping_location = str(mapping_location).split("/") - - # Assert - assert split_mapping_location[-1] == "deployments_map.json" - assert split_mapping_location[-2] == "ethereum" - - -def test_deployments_when_offline(chain, networks_disconnected, vyper_contract_container): - """ - Ensure you don't get `ProviderNotConnectedError` here. - """ - assert chain.contracts.get_deployments(vyper_contract_container) == [] - - -def test_get_deployments_local(chain, owner, contract_0, contract_1): - # Arrange - chain.contracts._local_deployments_mapping = {} - chain.contracts._local_contract_types = {} - starting_contracts_list_0 = chain.contracts.get_deployments(contract_0) - starting_contracts_list_1 = chain.contracts.get_deployments(contract_1) - deployed_contract_0 = owner.deploy(contract_0) - deployed_contract_1 = owner.deploy(contract_1) - - # Act - contracts_list_0 = chain.contracts.get_deployments(contract_0) - contracts_list_1 = chain.contracts.get_deployments(contract_1) - - # Assert - for contract_list in (contracts_list_0, contracts_list_1): - assert type(contract_list[0]) is ContractInstance - - index_0 = len(contracts_list_0) - len(starting_contracts_list_0) - 1 - index_1 = len(contracts_list_1) - len(starting_contracts_list_1) - 1 - actual_address_0 = contracts_list_0[index_0].address - assert actual_address_0 == deployed_contract_0.address - actual_address_1 = contracts_list_1[index_1].address - assert actual_address_1 == deployed_contract_1.address - - -def test_get_deployments_live( - chain, owner, contract_0, contract_1, remove_disk_writes_deployments, dummy_live_network -): - deployed_contract_0 = owner.deploy(contract_0, required_confirmations=0) - deployed_contract_1 = owner.deploy(contract_1, required_confirmations=0) - - # Act - my_contracts_list_0 = chain.contracts.get_deployments(contract_0) - my_contracts_list_1 = chain.contracts.get_deployments(contract_1) - - # Assert - address_from_api_0 = my_contracts_list_0[-1].address - assert address_from_api_0 == deployed_contract_0.address - address_from_api_1 = my_contracts_list_1[-1].address - assert address_from_api_1 == deployed_contract_1.address - - -def test_get_deployments_live_migration( - chain, owner, contract_0, dummy_live_network, caplog, use_debug -): - contract = owner.deploy(contract_0, required_confirmations=0) - old_style_map = {"ethereum": {"goerli": {"ApeContract0": [contract.address]}}} - chain.contracts._write_deployments_mapping(old_style_map) - actual = chain.contracts.get_deployments(contract_0) - assert actual == [contract] - assert caplog.records[-1].message == "Migrating 'deployments_map.json'." - - -def test_get_multiple_deployments_live( - chain, owner, contract_0, contract_1, remove_disk_writes_deployments, dummy_live_network -): - starting_contracts_list_0 = chain.contracts.get_deployments(contract_0) - starting_contracts_list_1 = chain.contracts.get_deployments(contract_1) - initial_deployed_contract_0 = owner.deploy(contract_0, required_confirmations=0) - initial_deployed_contract_1 = owner.deploy(contract_1, required_confirmations=0) - owner.deploy(contract_0, required_confirmations=0) - owner.deploy(contract_1, required_confirmations=0) - final_deployed_contract_0 = owner.deploy(contract_0, required_confirmations=0) - final_deployed_contract_1 = owner.deploy(contract_1, required_confirmations=0) - contracts_list_0 = chain.contracts.get_deployments(contract_0) - contracts_list_1 = chain.contracts.get_deployments(contract_1) - contract_type_map = { - "ApeContract0": (initial_deployed_contract_0, final_deployed_contract_0), - "ApeContract1": (initial_deployed_contract_1, final_deployed_contract_1), - } - - assert len(contracts_list_0) == len(starting_contracts_list_0) + 3 - assert len(contracts_list_1) == len(starting_contracts_list_1) + 3 - - for ct_name, ls in zip(("ApeContract0", "ApeContract1"), (contracts_list_0, contracts_list_1)): - initial_ct, final_ct = contract_type_map[ct_name] - assert ls[len(ls) - 3].address == initial_ct.address - assert ls[-1].address == final_ct.address - - -def test_contract_cache_mapping_updated_on_many_deployments(owner, chain, contract_0, contract_1): - # Arrange / Act - initial_contracts = chain.contracts.get_deployments(contract_0) - expected_first_contract = owner.deploy(contract_0) - - owner.deploy(contract_0) - owner.deploy(contract_0) - expected_last_contract = owner.deploy(contract_0) - - actual_contracts = chain.contracts.get_deployments(contract_0) - first_index = len(initial_contracts) # next index before deploys from this test - actual_first_contract = actual_contracts[first_index].address - actual_last_contract = actual_contracts[-1].address - - # Assert - fail_msg = f"Check deployments: {', '.join([c.address for c in actual_contracts])}" - assert len(actual_contracts) - len(initial_contracts) == 4, fail_msg - assert actual_first_contract == expected_first_contract.address, fail_msg - assert actual_last_contract == expected_last_contract.address, fail_msg - - -def test_poll_blocks_stop_block_not_in_future(chain_that_mined_5): - bad_stop_block = chain_that_mined_5.blocks.height - - with pytest.raises(ValueError, match="'stop' argument must be in the future."): - _ = [x for x in chain_that_mined_5.blocks.poll_blocks(stop_block=bad_stop_block)] - - -def test_poll_blocks(chain_that_mined_5, eth_tester_provider, owner, PollDaemon): - blocks: Queue = Queue(maxsize=3) - poller = chain_that_mined_5.blocks.poll_blocks() - - with PollDaemon("blocks", poller, blocks.put, blocks.full): - # Sleep first to ensure listening before mining. - time.sleep(1) - eth_tester_provider.mine(3) - - assert blocks.full() - first = blocks.get().number - second = blocks.get().number - third = blocks.get().number - assert first == second - 1 - assert second == third - 1 - - -def test_poll_blocks_reorg(chain_that_mined_5, eth_tester_provider, owner, PollDaemon, caplog): - blocks: Queue = Queue(maxsize=6) - poller = chain_that_mined_5.blocks.poll_blocks() - - with PollDaemon("blocks", poller, blocks.put, blocks.full): - # Sleep first to ensure listening before mining. - time.sleep(1) - - snapshot = chain_that_mined_5.snapshot() - chain_that_mined_5.mine(2) - - # Wait to allow blocks before re-org to get yielded - time.sleep(5) - - # Simulate re-org by reverting to the snapshot - chain_that_mined_5.restore(snapshot) - - # Allow it time to trigger realizing there was a re-org - time.sleep(1) - chain_that_mined_5.mine(2) - time.sleep(1) - - chain_that_mined_5.mine(3) - - assert blocks.full() - - # Show that re-org was detected - expected_error = ( - "Chain has reorganized since returning the last block. " - "Try adjusting the required network confirmations." - ) - assert caplog.records, "Didn't detect re-org" - assert expected_error in caplog.records[-1].message - - # Show that there are duplicate blocks - block_numbers: List[int] = [blocks.get().number for _ in range(6)] - assert len(set(block_numbers)) < len(block_numbers) - - -def test_poll_blocks_timeout( - vyper_contract_instance, chain_that_mined_5, eth_tester_provider, owner, PollDaemon -): - poller = chain_that_mined_5.blocks.poll_blocks(new_block_timeout=1) - - with pytest.raises(ChainError, match=r"Timed out waiting for new block \(time_waited=1.\d+\)."): - with PollDaemon("blocks", poller, lambda x: None, lambda: False): - time.sleep(1.5) - - -def test_contracts_get_multiple(vyper_contract_instance, solidity_contract_instance, chain): - contract_map = chain.contracts.get_multiple( - (vyper_contract_instance.address, solidity_contract_instance.address) - ) - assert len(contract_map) == 2 - assert contract_map[vyper_contract_instance.address] == vyper_contract_instance.contract_type - assert ( - contract_map[solidity_contract_instance.address] == solidity_contract_instance.contract_type - ) - - -def test_contracts_get_multiple_no_addresses(chain, caplog): - contract_map = chain.contracts.get_multiple([]) - assert not contract_map - assert "WARNING" in caplog.records[-1].levelname - assert "No addresses provided." in caplog.records[-1].message - - -def test_contracts_get_all_include_non_contract_address(vyper_contract_instance, chain, owner): - actual = chain.contracts.get_multiple((vyper_contract_instance.address, owner.address)) - assert len(actual) == 1 - assert actual[vyper_contract_instance.address] == vyper_contract_instance.contract_type - - -@skip_if_plugin_installed("ens") -def test_contracts_get_multiple_attempts_to_convert(chain): - with pytest.raises(ConversionError): - chain.contracts.get_multiple(("test.eth",)) - - -def test_contracts_get_non_contract_address(chain, owner): - actual = chain.contracts.get(owner.address) - assert actual is None - - -def test_contracts_get_attempts_to_convert(chain): - with pytest.raises(ConversionError): - chain.contracts.get("test.eth") - - -def test_cache_non_checksum_address(chain, vyper_contract_instance): - """ - When caching a non-checksum address, it should use its checksum - form automatically. - """ - if vyper_contract_instance.address in chain.contracts: - del chain.contracts[vyper_contract_instance.address] - - lowered_address = vyper_contract_instance.address.lower() - chain.contracts[lowered_address] = vyper_contract_instance.contract_type - assert chain.contracts[vyper_contract_instance.address] == vyper_contract_instance.contract_type - - -def test_get_contract_receipt(chain, vyper_contract_instance): - address = vyper_contract_instance.address - receipt = chain.contracts.get_creation_receipt(address) - assert receipt.contract_address == address - - chain.mine() - receipt = chain.contracts.get_creation_receipt(address) - assert receipt.contract_address == address diff --git a/tests/functional/test_contracts_cache.py b/tests/functional/test_contracts_cache.py new file mode 100644 index 0000000000..989e338e75 --- /dev/null +++ b/tests/functional/test_contracts_cache.py @@ -0,0 +1,370 @@ +import pytest +from ethpm_types import ContractType + +from ape import Contract +from ape.contracts import ContractInstance +from ape.exceptions import ContractNotFoundError, ConversionError +from ape_ethereum.proxies import _make_minimal_proxy +from tests.conftest import explorer_test, skip_if_plugin_installed + + +@pytest.fixture +def contract_0(project_with_contract): + return project_with_contract.ApeContract0 + + +@pytest.fixture +def contract_1(project_with_contract): + return project_with_contract.ApeContract1 + + +def test_instance_at(chain, contract_instance): + contract = chain.contracts.instance_at(str(contract_instance.address)) + assert contract.contract_type == contract_instance.contract_type + + +def test_instance_at_unknown_hex_str(chain, contract_instance): + # Fails when decoding Ethereum address and NOT conversion error. + hex_str = "0x1402b10CA274cD76C441e16C844223F79D3566De12bb12b0aebFE41aDFAe302" + with pytest.raises(ValueError, match=f"Unknown address value '{hex_str}'."): + chain.contracts.instance_at(hex_str) + + +def test_instance_at_when_given_contract_type(chain, contract_instance): + contract = chain.contracts.instance_at( + str(contract_instance.address), contract_type=contract_instance.contract_type + ) + assert contract.contract_type == contract_instance.contract_type + + +def test_instance_at_when_given_name_as_contract_type(chain, contract_instance): + expected_match = "Expected type 'ContractType' for argument 'contract_type'." + with pytest.raises(TypeError, match=expected_match): + address = str(contract_instance.address) + bad_contract_type = contract_instance.contract_type.name + chain.contracts.instance_at(address, contract_type=bad_contract_type) + + +@explorer_test +def test_instance_at_uses_given_contract_type_when_retrieval_fails(mocker, chain, caplog): + # The manager always attempts retrieval so that default contact types can + # get cached. However, sometimes an explorer plugin may fail. If given a contract-type + # in that situation, we can use it and not fail and log the error instead. + expected_contract_type = ContractType(contractName="foo", sourceId="foo.bar") + new_address = "0x4a986a6dCA6dbf99bC3d17F8D71aFb0d60e740f8" + expected_fail_message = "LOOK_FOR_THIS_FAIL_MESSAGE" + existing_fn = chain.contracts.get + + def fn(addr, default=None): + if addr == new_address: + raise ValueError(expected_fail_message) + + return existing_fn(addr, default=default) + + chain.contracts.get = mocker.MagicMock() + chain.contracts.get.side_effect = fn + + actual = chain.contracts.instance_at(new_address, contract_type=expected_contract_type) + assert actual.contract_type == expected_contract_type + assert caplog.records[-1].message == expected_fail_message + + +@explorer_test +def test_instance_at_contract_type_not_found(chain, eth_tester_provider): + eth_tester_provider.network.__dict__["explorer"] = None + new_address = "0x4a986a6dca6dbF99Bc3D17F8d71aFB0D60E740F9" + expected = ( + rf"Failed to get contract type for address '{new_address}'. " + r"Current provider 'ethereum:local:test' has no associated explorer plugin. " + "Try installing an explorer plugin using .*ape plugins install etherscan.*, " + r"or using a network with explorer support\." + ) + with pytest.raises(ContractNotFoundError, match=expected): + chain.contracts.instance_at(new_address) + + +def test_cache_deployment_live_network( + chain, + vyper_contract_instance, + vyper_contract_container, + remove_disk_writes_deployments, + dummy_live_network, +): + # Arrange - Ensure the contract is not cached anywhere + address = vyper_contract_instance.address + contract_name = vyper_contract_instance.contract_type.name + deployments = chain.contracts._deployments + contract_types = chain.contracts._local_contract_types + chain.contracts._local_contract_types = { + a: ct for a, ct in contract_types.items() if a != address + } + chain.contracts._deployments = {n: d for n, d in deployments.items() if n != contract_name} + + # Act + chain.contracts.cache_deployment(vyper_contract_instance) + + # Assert + actual_deployments = chain.contracts.get_deployments(vyper_contract_container) + actual_contract_type = chain.contracts._get_contract_type_from_disk(address) + expected = vyper_contract_instance.contract_type + assert len(actual_deployments) == 1 + assert actual_deployments[0].address == address + assert actual_deployments[0].txn_hash == vyper_contract_instance.txn_hash + assert chain.contracts.get(address) == expected + assert actual_contract_type == expected + + +def test_cache_default_contract_type_when_used(solidity_contract_instance, chain, config): + address = solidity_contract_instance.address + contract_type = solidity_contract_instance.contract_type + + # Delete contract from local cache if it's there + if address in chain.contracts._local_contract_types: + del chain.contracts._local_contract_types[address] + + # Delete cache file if it exists + cache_file = chain.contracts._contract_types_cache / f"{address}.json" + if cache_file.is_file(): + cache_file.unlink() + + # Create a contract using the contract type when nothing is cached. + contract = Contract(address, contract_type=contract_type) + assert isinstance(contract, ContractInstance) + + # Ensure we don't need the contract type when creating it the second time. + contract = Contract(address) + assert isinstance(contract, ContractInstance) + + +@explorer_test +def test_contracts_getitem_contract_not_found(chain, eth_tester_provider): + eth_tester_provider.network.__dict__["explorer"] = None + new_address = "0x4a986a6dca6dbF99Bc3D17F8d71aFB0D60E740F9" + expected = ( + rf"Failed to get contract type for address '{new_address}'. " + r"Current provider 'ethereum:local:test' has no associated explorer plugin. " + "Try installing an explorer plugin using .*ape plugins install etherscan.*, " + r"or using a network with explorer support\." + ) + with pytest.raises(IndexError, match=expected): + _ = chain.contracts[new_address] + + +def test_deployments_mapping_cache_location(chain): + # Arrange / Act + mapping_location = chain.contracts._deployments_mapping_cache + split_mapping_location = str(mapping_location).split("/") + + # Assert + assert split_mapping_location[-1] == "deployments_map.json" + assert split_mapping_location[-2] == "ethereum" + + +def test_deployments_when_offline(chain, networks_disconnected, vyper_contract_container): + """ + Ensure you don't get `ProviderNotConnectedError` here. + """ + assert chain.contracts.get_deployments(vyper_contract_container) == [] + + +def test_get_deployments_local(chain, owner, contract_0, contract_1): + # Arrange + chain.contracts._local_deployments_mapping = {} + chain.contracts._local_contract_types = {} + starting_contracts_list_0 = chain.contracts.get_deployments(contract_0) + starting_contracts_list_1 = chain.contracts.get_deployments(contract_1) + deployed_contract_0 = owner.deploy(contract_0) + deployed_contract_1 = owner.deploy(contract_1) + + # Act + contracts_list_0 = chain.contracts.get_deployments(contract_0) + contracts_list_1 = chain.contracts.get_deployments(contract_1) + + # Assert + for contract_list in (contracts_list_0, contracts_list_1): + assert type(contract_list[0]) is ContractInstance + + index_0 = len(contracts_list_0) - len(starting_contracts_list_0) - 1 + index_1 = len(contracts_list_1) - len(starting_contracts_list_1) - 1 + actual_address_0 = contracts_list_0[index_0].address + assert actual_address_0 == deployed_contract_0.address + actual_address_1 = contracts_list_1[index_1].address + assert actual_address_1 == deployed_contract_1.address + + +def test_get_deployments_live( + chain, owner, contract_0, contract_1, remove_disk_writes_deployments, dummy_live_network +): + deployed_contract_0 = owner.deploy(contract_0, required_confirmations=0) + deployed_contract_1 = owner.deploy(contract_1, required_confirmations=0) + + # Act + my_contracts_list_0 = chain.contracts.get_deployments(contract_0) + my_contracts_list_1 = chain.contracts.get_deployments(contract_1) + + # Assert + address_from_api_0 = my_contracts_list_0[-1].address + assert address_from_api_0 == deployed_contract_0.address + address_from_api_1 = my_contracts_list_1[-1].address + assert address_from_api_1 == deployed_contract_1.address + + +def test_get_deployments_live_migration( + chain, owner, contract_0, dummy_live_network, caplog, use_debug +): + contract = owner.deploy(contract_0, required_confirmations=0) + old_style_map = {"ethereum": {"goerli": {"ApeContract0": [contract.address]}}} + chain.contracts._write_deployments_mapping(old_style_map) + actual = chain.contracts.get_deployments(contract_0) + assert actual == [contract] + assert caplog.records[-1].message == "Migrating 'deployments_map.json'." + + +def test_get_multiple_deployments_live( + chain, owner, contract_0, contract_1, remove_disk_writes_deployments, dummy_live_network +): + starting_contracts_list_0 = chain.contracts.get_deployments(contract_0) + starting_contracts_list_1 = chain.contracts.get_deployments(contract_1) + initial_deployed_contract_0 = owner.deploy(contract_0, required_confirmations=0) + initial_deployed_contract_1 = owner.deploy(contract_1, required_confirmations=0) + owner.deploy(contract_0, required_confirmations=0) + owner.deploy(contract_1, required_confirmations=0) + final_deployed_contract_0 = owner.deploy(contract_0, required_confirmations=0) + final_deployed_contract_1 = owner.deploy(contract_1, required_confirmations=0) + contracts_list_0 = chain.contracts.get_deployments(contract_0) + contracts_list_1 = chain.contracts.get_deployments(contract_1) + contract_type_map = { + "ApeContract0": (initial_deployed_contract_0, final_deployed_contract_0), + "ApeContract1": (initial_deployed_contract_1, final_deployed_contract_1), + } + + assert len(contracts_list_0) == len(starting_contracts_list_0) + 3 + assert len(contracts_list_1) == len(starting_contracts_list_1) + 3 + + for ct_name, ls in zip(("ApeContract0", "ApeContract1"), (contracts_list_0, contracts_list_1)): + initial_ct, final_ct = contract_type_map[ct_name] + assert ls[len(ls) - 3].address == initial_ct.address + assert ls[-1].address == final_ct.address + + +def test_cache_updates_per_deploy(owner, chain, contract_0, contract_1): + # Arrange / Act + initial_contracts = chain.contracts.get_deployments(contract_0) + expected_first_contract = owner.deploy(contract_0) + + owner.deploy(contract_0) + owner.deploy(contract_0) + expected_last_contract = owner.deploy(contract_0) + + actual_contracts = chain.contracts.get_deployments(contract_0) + first_index = len(initial_contracts) # next index before deploys from this test + actual_first_contract = actual_contracts[first_index].address + actual_last_contract = actual_contracts[-1].address + + # Assert + fail_msg = f"Check deployments: {', '.join([c.address for c in actual_contracts])}" + assert len(actual_contracts) - len(initial_contracts) == 4, fail_msg + assert actual_first_contract == expected_first_contract.address, fail_msg + assert actual_last_contract == expected_last_contract.address, fail_msg + + +def test_get_multiple(vyper_contract_instance, solidity_contract_instance, chain): + contract_map = chain.contracts.get_multiple( + (vyper_contract_instance.address, solidity_contract_instance.address) + ) + assert len(contract_map) == 2 + assert contract_map[vyper_contract_instance.address] == vyper_contract_instance.contract_type + assert ( + contract_map[solidity_contract_instance.address] == solidity_contract_instance.contract_type + ) + + +def test_get_multiple_no_addresses(chain, caplog): + contract_map = chain.contracts.get_multiple([]) + assert not contract_map + assert "WARNING" in caplog.records[-1].levelname + assert "No addresses provided." in caplog.records[-1].message + + +def test_get_all_include_non_contract_address(vyper_contract_instance, chain, owner): + actual = chain.contracts.get_multiple((vyper_contract_instance.address, owner.address)) + assert len(actual) == 1 + assert actual[vyper_contract_instance.address] == vyper_contract_instance.contract_type + + +@skip_if_plugin_installed("ens") +def test_get_multiple_attempts_to_convert(chain): + with pytest.raises(ConversionError): + chain.contracts.get_multiple(("test.eth",)) + + +def test_get_non_contract_address(chain, owner): + actual = chain.contracts.get(owner.address) + assert actual is None + + +def test_get_attempts_to_convert(chain): + with pytest.raises(ConversionError): + chain.contracts.get("test.eth") + + +def test_cache_non_checksum_address(chain, vyper_contract_instance): + """ + When caching a non-checksum address, it should use its checksum + form automatically. + """ + if vyper_contract_instance.address in chain.contracts: + del chain.contracts[vyper_contract_instance.address] + + lowered_address = vyper_contract_instance.address.lower() + chain.contracts[lowered_address] = vyper_contract_instance.contract_type + assert chain.contracts[vyper_contract_instance.address] == vyper_contract_instance.contract_type + + +def test_get_contract_receipt(chain, vyper_contract_instance): + address = vyper_contract_instance.address + receipt = chain.contracts.get_creation_receipt(address) + assert receipt.contract_address == address + + chain.mine() + receipt = chain.contracts.get_creation_receipt(address) + assert receipt.contract_address == address + + +def test_delete_contract(vyper_contract_instance, chain): + # Ensure we start with it cached. + if vyper_contract_instance.address not in chain.contracts: + chain.contracts[vyper_contract_instance.address] = vyper_contract_instance + + del chain.contracts[vyper_contract_instance.address] + assert vyper_contract_instance.address not in chain.contracts + + # Ensure we can't access it. + with pytest.raises(IndexError): + _ = chain.contracts[vyper_contract_instance.address] + + +def test_delete_proxy(vyper_contract_instance, chain, ethereum, owner): + address = vyper_contract_instance.address + container = _make_minimal_proxy(address=address.lower()) + proxy = container.deploy(sender=owner) + + # Ensure we start with both the proxy and the target contracts cached. + if proxy.address not in chain.contracts: + chain.contracts[proxy.address] = proxy + + proxy_info = ethereum.get_proxy_info(proxy.address) + chain.contracts.cache_proxy_info(proxy.address, proxy_info) + if proxy_info.target not in chain.contracts: + chain.contracts[proxy_info.target] = vyper_contract_instance + + del chain.contracts[proxy.address] + assert proxy.address not in chain.contracts + + # Ensure we can't access it. + with pytest.raises(IndexError): + _ = chain.contracts[proxy.address] + + # Ensure we can't access the target either. + with pytest.raises(IndexError): + _ = chain.contracts[proxy_info.target] diff --git a/tests/functional/test_history.py b/tests/functional/test_history.py new file mode 100644 index 0000000000..2bd10cb402 --- /dev/null +++ b/tests/functional/test_history.py @@ -0,0 +1,61 @@ +from ape_ethereum.transactions import Receipt, TransactionStatusEnum +from tests.conftest import explorer_test + + +def test_history(sender, receiver, chain): + length_at_start = len(chain.history[sender].sessional) + receipt = sender.transfer(receiver, "1 wei") + transactions_from_cache = list(sender.history) + assert len(transactions_from_cache) == length_at_start + 1 + assert sender.history[-1] == receipt + assert sender.history[0:] == transactions_from_cache[0:] + assert sender.history[:-1] == transactions_from_cache[:-1] + + txn = transactions_from_cache[-1] + assert txn.sender == receipt.sender == sender + assert txn.receiver == receipt.receiver == receiver + + +@explorer_test +def test_history_caches_sender_over_address_key( + mocker, chain, eth_tester_provider, sender, vyper_contract_container, ethereum +): + # When getting receipts from the explorer for contracts, it includes transactions + # made to the contract. This test shows we cache by sender and not address key. + contract = sender.deploy(vyper_contract_container, 0) + network = ethereum.local + txn = ethereum.create_transaction( + receiver=contract.address, sender=sender.address, value=10000000000000000000000 + ) + known_receipt = Receipt( + block_number=10, + gas_price=11, + gas_used=12, + gas_limit=13, + status=TransactionStatusEnum.NO_ERROR.value, + txn_hash="0x98d2aee8617897b5983314de1d6ff44d1f014b09575b47a88267971beac97b2b", + transaction=txn, + ) + + # The receipt is already known and cached by the sender. + chain.history.append(known_receipt) + + # We ask for receipts from the contract, but it returns ones sent to the contract. + def get_txns_patch(address): + if address == contract.address: + yield from [known_receipt] + + mock_explorer = mocker.MagicMock() + mock_explorer.get_account_transactions.side_effect = get_txns_patch + network.__dict__["explorer"] = mock_explorer + eth_tester_provider.network = network + + # Previously, this would error because the receipt was cached with the wrong sender + try: + actual = [t for t in chain.history[contract.address].sessional] + + # Actual is 0 because the receipt was cached under the sender. + assert len(actual) == 0 + finally: + if "explorer" in network.__dict__: + del network.__dict__["explorer"] diff --git a/tests/functional/test_proxy.py b/tests/functional/test_proxy.py index 8125bf9678..6d4302bd04 100644 --- a/tests/functional/test_proxy.py +++ b/tests/functional/test_proxy.py @@ -1,12 +1,8 @@ -import pytest - from ape_ethereum.proxies import ProxyType -from ape_ethereum.proxies import minimal_proxy as minimal_proxy_container - -@pytest.fixture -def minimal_proxy(owner): - return owner.deploy(minimal_proxy_container) +""" +NOTE: Most proxy tests are in `geth/test_proxy.py`. +""" def test_minimal_proxy(ethereum, minimal_proxy):