diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 818a71f..70f78fe 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -24,7 +24,7 @@ repos: rev: v1.7.1 hooks: - id: mypy - additional_dependencies: [types-setuptools, pydantic] + additional_dependencies: [types-setuptools, pydantic, types-requests] - repo: https://github.com/executablebooks/mdformat rev: 0.7.17 diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..86ab3eb --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "python.linting.mypyEnabled": true, + "python.linting.enabled": true +} \ No newline at end of file diff --git a/ape_subsquid/archive.py b/ape_subsquid/archive.py new file mode 100644 index 0000000..3c9773b --- /dev/null +++ b/ape_subsquid/archive.py @@ -0,0 +1,97 @@ +from typing import NotRequired, Optional, TypedDict + +from requests import Session + + +class BlockFieldSelection(TypedDict, total=False): + number: bool + hash: bool + parentHash: bool + timestamp: bool + transactionsRoot: bool + receiptsRoot: bool + stateRoot: bool + logsBloom: bool + sha3Uncles: bool + extraData: bool + miner: bool + nonce: bool + mixHash: bool + size: bool + gasLimit: bool + gasUsed: bool + difficulty: bool + totalDifficulty: bool + baseFeePerGas: bool + + +class FieldSelection(TypedDict, total=False): + block: BlockFieldSelection + # transaction: TxFieldSelection + # log: LogFieldSelection + # trace: TraceFieldSelection + + +class Query(TypedDict): + fromBlock: int + toBlock: NotRequired[int] + includeAllBlocks: NotRequired[bool] + fields: NotRequired[FieldSelection] + transactions: NotRequired[list[dict]] + # logs: NotRequired[list[LogRequest]] + # traces: NotRequired[list[TraceRequest]] + + +class BlockHeader(TypedDict): + number: int + hash: str + parentHash: str + size: int + sha3Uncles: str + miner: str + stateRoot: str + transactionsRoot: str + receiptsRoot: str + logsBloom: str + difficulty: str + totalDifficulty: str + gasLimit: str + gasUsed: str + timestamp: float + extraData: str + mixHash: str + nonce: str + baseFeePerGas: Optional[str] + + +class Log(TypedDict): + pass + + +class Transaction(TypedDict): + pass + + +class Trace(TypedDict): + pass + + +class Block(TypedDict): + header: BlockHeader + logs: NotRequired[list[Log]] + transactions: NotRequired[list[Transaction]] + traces: NotRequired[list[Trace]] + + +class Archive: + _session = Session() + + def get_worker(self, start_block: int) -> str: + url = f"https://v2.archive.subsquid.io/network/ethereum-mainnet/{start_block}/worker" + response = self._session.get(url) + return response.text + + def query(self, query: Query) -> list[Block]: + worker_url = self.get_worker(query["fromBlock"]) + response = self._session.post(worker_url, json=query) + return response.json() diff --git a/ape_subsquid/query.py b/ape_subsquid/query.py index b643737..cf5d8ab 100644 --- a/ape_subsquid/query.py +++ b/ape_subsquid/query.py @@ -11,9 +11,30 @@ ) from ape.exceptions import QueryEngineError from ape.utils import singledispatchmethod +from ape_ethereum import ecosystem + +from ape_subsquid.archive import Archive, Block + + +def map_block(block: Block) -> ecosystem.Block: + return ecosystem.Block( + number=block["header"]["number"], + hash=block["header"]["hash"], + parentHash=block["header"]["parentHash"], + size=block["header"]["size"], + timestamp=int(block["header"]["timestamp"]), + num_transactions=len(block.get("transactions", [])), + gasLimit=block["header"]["gasLimit"], + gasUsed=block["header"]["gasUsed"], + baseFeePerGas=block["header"]["baseFeePerGas"], + difficulty=block["header"]["difficulty"], + totalDifficulty=block["header"]["totalDifficulty"], + ) class SubsquidQueryEngine(QueryAPI): + _archive = Archive() + @singledispatchmethod def estimate_query(self, query: QueryType) -> Optional[int]: return None @@ -51,8 +72,40 @@ def perform_query(self, query: QueryType) -> Iterator: ) @perform_query.register - def perform_block_query(self, query: BlockQuery): - return None + def perform_block_query(self, query: BlockQuery) -> Iterator[ecosystem.Block]: + from_block = query.start_block + while True: + data = self._archive.query( + { + "fromBlock": from_block, + "toBlock": query.stop_block, + "fields": { + "block": { + "number": True, + "hash": True, + "parentHash": True, + "size": True, + "timestamp": True, + "gasLimit": True, + "gasUsed": True, + "baseFeePerGas": True, + "difficulty": True, + "totalDifficulty": True, + }, + }, + "includeAllBlocks": True, + "transactions": [{}], + } + ) + + for block in data: + yield map_block(block) + + last_block = data[-1] + if last_block["header"]["number"] == query.stop_block: + break + + from_block = last_block["header"]["number"] + 1 @perform_query.register def perform_block_transaction_query(self, query: BlockTransactionQuery):