From 9f8eacee150fdbcced0510707c1b4124bfdce09d Mon Sep 17 00:00:00 2001 From: El De-dog-lo <3859395+fubuloubu@users.noreply.github.com> Date: Thu, 17 Aug 2023 10:29:37 -0500 Subject: [PATCH] feat(query): contract creation query support [APE-1296] (#1606) * feat(query): add support for finding contract creation receipt * chore: fix typing updates for mypy * feat: add new abstract function to ProviderAPI to get creation receipts * feat: add concrete implementation to Web3Provider for creation receipts * feat: add support for ContractCreationQuery to default query provider * refactor: source first contract creation receipt from query system * chore: upgrade pre-commit versions of mypy and mdformat * refactor: add additional note to exception of new feature --- .pre-commit-config.yaml | 4 +-- src/ape/api/providers.py | 68 +++++++++++++++++++++++++++++++++++++++ src/ape/api/query.py | 5 +++ src/ape/managers/chain.py | 51 ++++++++++++++--------------- src/ape/managers/query.py | 15 +++++++++ src/ape/utils/misc.py | 2 +- 6 files changed, 115 insertions(+), 30 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index fa218c2dc3..e5921fb902 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -21,7 +21,7 @@ repos: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.991 + rev: v1.4.1 hooks: - id: mypy additional_dependencies: [ @@ -34,7 +34,7 @@ repos: ] - repo: https://github.com/executablebooks/mdformat - rev: 0.7.14 + rev: 0.7.16 hooks: - id: mdformat additional_dependencies: [mdformat-gfm, mdformat-frontmatter, mdformat-pyproject] diff --git a/src/ape/api/providers.py b/src/ape/api/providers.py index ec6fd42df5..4ccbf604b3 100644 --- a/src/ape/api/providers.py +++ b/src/ape/api/providers.py @@ -388,6 +388,27 @@ def get_transactions_by_account_nonce( # type: ignore[empty-body] Iterator[:class:`~ape.api.transactions.ReceiptAPI`] """ + @raises_not_implemented + def get_contract_creation_receipts( # type: ignore[empty-body] + self, + address: AddressType, + start_block: int = 0, + stop_block: int = -1, + contract_code: Optional[HexBytes] = None, + ) -> Iterator[ReceiptAPI]: + """ + Get all receipts where a contract address was created or re-created. + + Args: + address (``AddressType``): The address of the account. + start_block (int): The block number to start the search with. + stop_block (int): The block number to stop the search with. + contract_code (Optional[bytes]): The code of the contract at the stop block. + + Returns: + Iterator[:class:`~ape.api.transactions.ReceiptAPI`] + """ + @abstractmethod def send_transaction(self, txn: TransactionAPI) -> ReceiptAPI: """ @@ -1267,6 +1288,53 @@ def block_ranges(self, start=0, stop=None, page=None): stop_block = min(stop, start_block + page - 1) yield start_block, stop_block + def get_contract_creation_receipts( # type: ignore[empty-body] + self, + address: AddressType, + start_block: int = 0, + stop_block: Optional[int] = None, + contract_code: Optional[HexBytes] = None, + ) -> Iterator[ReceiptAPI]: + if stop_block is None: + stop_block = self.chain_manager.blocks.height + + if contract_code is None: + contract_code = HexBytes(self.get_code(address)) + + mid_block = (stop_block - start_block) // 2 + start_block + # NOTE: biased towards mid_block == start_block + + if start_block == mid_block: + for tx in self.chain_manager.blocks[mid_block].transactions: + if (receipt := tx.receipt) and receipt.contract_address == address: + yield receipt + + if mid_block + 1 <= stop_block: + yield from self.get_contract_creation_receipts( + address, + start_block=mid_block + 1, + stop_block=stop_block, + contract_code=contract_code, + ) + + # TODO: Handle when code is nonzero but doesn't match + # TODO: Handle when code is empty after it's not (re-init) + elif HexBytes(self.get_code(address, block_id=mid_block)) == contract_code: + yield from self.get_contract_creation_receipts( + address, + start_block=start_block, + stop_block=mid_block, + contract_code=contract_code, + ) + + elif mid_block + 1 <= stop_block: + yield from self.get_contract_creation_receipts( + address, + start_block=mid_block + 1, + stop_block=stop_block, + contract_code=contract_code, + ) + def get_contract_logs(self, log_filter: LogFilter) -> Iterator[ContractLog]: height = self.chain_manager.blocks.height start_block = log_filter.start_block diff --git a/src/ape/api/query.py b/src/ape/api/query.py index eb47372377..78263a629d 100644 --- a/src/ape/api/query.py +++ b/src/ape/api/query.py @@ -13,6 +13,7 @@ "BlockQuery", "BlockTransactionQuery", "AccountTransactionQuery", + "ContractCreationQuery", "ContractEventQuery", "ContractMethodQuery", ] @@ -149,6 +150,10 @@ def check_start_nonce_before_stop_nonce(cls, values: Dict) -> Dict: return values +class ContractCreationQuery(_BaseBlockQuery): + contract: AddressType + + class ContractEventQuery(_BaseBlockQuery): """ A ``QueryType`` that collects members from ``event`` over a range of diff --git a/src/ape/managers/chain.py b/src/ape/managers/chain.py index 18d8bcb3e0..d2817349eb 100644 --- a/src/ape/managers/chain.py +++ b/src/ape/managers/chain.py @@ -17,6 +17,7 @@ from ape.api.query import ( AccountTransactionQuery, BlockQuery, + ContractCreationQuery, extract_fields, validate_and_expand_columns, ) @@ -1348,37 +1349,33 @@ def get_creation_receipt( Returns: :class:`~ape.apt.transactions.ReceiptAPI` """ - if stop_block is None and (stop := self.chain_manager.blocks.head.number): - stop_block = stop - elif stop_block is None: - raise ChainError("Chain missing blocks.") - - mid_block = (stop_block - start_block) // 2 + start_block - # NOTE: biased towards mid_block == start_block - - if start_block == mid_block: - for tx in self.chain_manager.blocks[mid_block].transactions: - if (receipt := tx.receipt) and receipt.contract_address == address: - return receipt - - if mid_block + 1 <= stop_block: - return self.get_creation_receipt( - address, start_block=mid_block + 1, stop_block=stop_block + if stop_block is None: + stop_block = self.chain_manager.blocks.height + + # TODO: Refactor the name of this somehow to be clearer + creation_receipts = cast( + Iterator[ReceiptAPI], + self.query_manager.query( + ContractCreationQuery( + columns=["*"], + contract=address, + start_block=start_block, + stop_block=stop_block, ) - else: - raise ChainError(f"Failed to find a contract-creation receipt for '{address}'.") - - elif self.provider.get_code(address, block_id=mid_block): - return self.get_creation_receipt(address, start_block=start_block, stop_block=mid_block) + ), + ) - elif mid_block + 1 <= stop_block: - return self.get_creation_receipt( - address, start_block=mid_block + 1, stop_block=stop_block + try: + # Get the first contract receipt, which is the first time it appears + return next(creation_receipts) + except StopIteration: + raise ChainError( + f"Failed to find a contract-creation receipt for '{address}'. " + "Note that it may be the case that the backend used cannot detect contracts " + "deployed by other contracts, and you may receive better results by installing " + "a plugin that supports it, like Etherscan." ) - else: - raise ChainError(f"Failed to find a contract-creation receipt for '{address}'.") - class ReportManager(BaseManager): """ diff --git a/src/ape/managers/query.py b/src/ape/managers/query.py index 0794cb3281..efff6532c1 100644 --- a/src/ape/managers/query.py +++ b/src/ape/managers/query.py @@ -9,6 +9,7 @@ BaseInterfaceModel, BlockQuery, BlockTransactionQuery, + ContractCreationQuery, ContractEventQuery, ) from ape.contracts.base import ContractLog, LogFilter @@ -38,6 +39,12 @@ def estimate_block_transaction_query(self, query: BlockTransactionQuery) -> int: # NOTE: Very loose estimate of 1000ms per block for this query. return self.provider.get_block(query.block_id).num_transactions * 100 + @estimate_query.register + def estimate_contract_creation_query(self, query: ContractCreationQuery) -> int: + # NOTE: Extremely expensive query, involves binary search of all blocks in a chain + # Very loose estimate of 5s per transaction for this query. + return 5000 + @estimate_query.register def estimate_contract_events_query(self, query: ContractEventQuery) -> int: # NOTE: Very loose estimate of 100ms per block for this query. @@ -68,6 +75,14 @@ def perform_block_transaction_query( ) -> Iterator[TransactionAPI]: return self.provider.get_transactions_by_block(query.block_id) + @perform_query.register + def perform_contract_creation_query(self, query: ContractCreationQuery) -> Iterator[ReceiptAPI]: + yield from self.provider.get_contract_creation_receipts( + address=query.contract, + start_block=query.start_block, + stop_block=query.stop_block, + ) + @perform_query.register def perform_contract_events_query(self, query: ContractEventQuery) -> Iterator[ContractLog]: addresses = query.contract diff --git a/src/ape/utils/misc.py b/src/ape/utils/misc.py index e05c97e798..b0724e5261 100644 --- a/src/ape/utils/misc.py +++ b/src/ape/utils/misc.py @@ -21,7 +21,7 @@ from ape.utils.os import expand_environment_variables EMPTY_BYTES32 = HexBytes("0x0000000000000000000000000000000000000000000000000000000000000000") -ZERO_ADDRESS = cast(AddressType, "0x0000000000000000000000000000000000000000") +ZERO_ADDRESS: AddressType = cast(AddressType, "0x0000000000000000000000000000000000000000") DEFAULT_TRANSACTION_ACCEPTANCE_TIMEOUT = 120 DEFAULT_LOCAL_TRANSACTION_ACCEPTANCE_TIMEOUT = 20 DEFAULT_MAX_RETRIES_TX = 20