Skip to content

Commit

Permalink
fix!: handle missing size with computed lookup (#1980)
Browse files Browse the repository at this point in the history
  • Loading branch information
antazoey committed May 1, 2024
1 parent a371d04 commit 7d5b484
Show file tree
Hide file tree
Showing 4 changed files with 125 additions and 8 deletions.
68 changes: 67 additions & 1 deletion src/ape/api/providers.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,17 +57,41 @@ class BlockAPI(BaseInterfaceModel):
# NOTE: All fields in this class (and it's subclasses) should not be `Optional`
# except the edge cases noted below

"""
The number of transactions in the block.
"""
num_transactions: int = 0

"""
The block hash identifier.
"""
hash: Optional[Any] = None # NOTE: pending block does not have a hash

"""
The block number identifier.
"""
number: Optional[int] = None # NOTE: pending block does not have a number

"""
The preceeding block's hash.
"""
parent_hash: Any = Field(
EMPTY_BYTES32, alias="parentHash"
) # NOTE: genesis block has no parent hash
size: int

"""
The timestamp the block was produced.
NOTE: The pending block uses the current timestamp.
"""
timestamp: int

_size: Optional[int] = None

@property
def datetime(self) -> datetime.datetime:
"""
The block timestamp as a datetime object.
"""
return datetime.datetime.fromtimestamp(self.timestamp, tz=datetime.timezone.utc)

@model_validator(mode="before")
Expand All @@ -77,12 +101,54 @@ def convert_parent_hash(cls, data):
data["parentHash"] = parent_hash
return data

@model_validator(mode="wrap")
@classmethod
def validate_size(cls, values, handler):
"""
A validator for handling non-computed size.
Saves it to a private member on this class and
gets returned in computed field "size".
"""

if not hasattr(values, "pop"):
# Handle weird AttributeDict missing pop method.
# https://github.com/ethereum/web3.py/issues/3326
values = {**values}

size = values.pop("size", None)
model = handler(values)
if size is not None:
model._size = size

return model

@computed_field() # type: ignore[misc]
@cached_property
def transactions(self) -> List[TransactionAPI]:
"""
All transactions in a block.
"""
query = BlockTransactionQuery(columns=["*"], block_id=self.hash)
return cast(List[TransactionAPI], list(self.query_manager.query(query)))

@computed_field() # type: ignore[misc]
@cached_property
def size(self) -> int:
"""
The size of the block in gas. Most of the time,
this field is passed to the model at validation time,
but occassionally it is missing (like in `eth_subscribe:newHeads`),
in which case it gets calculated if and only if the user
requests it (or during serialization of this model to disk).
"""

if self._size is not None:
# The size was provided with the rest of the model
# (normal).
return self._size

raise APINotImplementedError()


class ProviderAPI(BaseInterfaceModel):
"""
Expand Down
24 changes: 19 additions & 5 deletions src/ape_ethereum/ecosystem.py
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,7 @@ class Block(BlockAPI):
base_fee: int = Field(0, alias="baseFeePerGas")
difficulty: int = 0
total_difficulty: int = Field(0, alias="totalDifficulty")
uncles: List[HexBytes] = []

# Type re-declares.
hash: Optional[HexBytes] = None
Expand All @@ -316,6 +317,24 @@ class Block(BlockAPI):
def validate_ints(cls, value):
return to_int(value) if value else 0

@computed_field() # type: ignore[misc]
@property
def size(self) -> int:
if self._size is not None:
# The size was provided with the rest of the model
# (normal).
return self._size

# Try to get it from the provider.
if provider := self.network_manager.active_provider:
block = provider.get_block(self.number)
size = block._size
if size is not None and size > -1:
self._size = size
return size

raise APINotImplementedError()


class Ethereum(EcosystemAPI):
# NOTE: `default_transaction_type` should be overridden
Expand Down Expand Up @@ -554,11 +573,6 @@ def decode_block(self, data: Dict) -> BlockAPI:
if "transactions" in data:
data["num_transactions"] = len(data["transactions"])

if "size" not in data:
# NOTE: Due to an issue with `eth_subscribe:newHeads` on Infura
# https://github.com/ApeWorX/ape-infura/issues/72
data["size"] = -1 # HACK: use an unrealistic sentinel value

return Block.model_validate(data)

def _python_type_for_abi_type(self, abi_type: ABIType) -> Union[Type, Sequence]:
Expand Down
39 changes: 38 additions & 1 deletion tests/functional/test_block.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,21 @@
import pytest
from eth_pydantic_types import HexBytes

from ape_ethereum.ecosystem import Block


@pytest.fixture
def block(chain):
return chain.blocks.head


def test_block(eth_tester_provider, vyper_contract_instance):
data = eth_tester_provider.web3.eth.get_block("latest")
actual = Block.model_validate(data)
assert actual.hash == data["hash"]
assert actual.number == data["number"]


def test_block_dict(block):
actual = block.model_dump()
expected = {
Expand All @@ -21,6 +31,7 @@ def test_block_dict(block):
"timestamp": block.timestamp,
"totalDifficulty": 0,
"transactions": [],
"uncles": [],
}
assert actual == expected

Expand All @@ -33,6 +44,32 @@ def test_block_json(block):
'"num_transactions":0,"number":0,'
f'"parentHash":"{block.parent_hash.hex()}",'
f'"size":{block.size},"timestamp":{block.timestamp},'
f'"totalDifficulty":0,"transactions":[]}}'
f'"totalDifficulty":0,"transactions":[],"uncles":[]}}'
)
assert actual == expected


def test_block_calculate_size(block):
original = block.model_dump(by_alias=True)
size = original.pop("size")

# Show size works normally (validated when passed in as a field).
assert size > 0
assert block.size == size

# Show we can also calculate size if it is missing.
actual = block.model_validate(original) # re-init without size.
assert actual.size == size

original["size"] = 123
new_block = Block.model_validate(original)
assert new_block.size == 123 # Show no clashing.
assert actual.size == size # Show this hasn't changed.


def test_block_uncles(block):
data = block.model_dump(by_alias=True)
uncles = [HexBytes("0xb983ecae1ed260dd08d108653912a9138bdce56c78aa7d78ee4fca70c2c8767b")]
data["uncles"] = uncles
actual = Block.model_validate(data)
assert actual.uncles == uncles
2 changes: 1 addition & 1 deletion tests/functional/test_query.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@ def test_basic_query(chain, eth_tester_provider):
"num_transactions",
"number",
"parent_hash",
"size",
"timestamp",
"total_difficulty",
"uncles",
]


Expand Down

0 comments on commit 7d5b484

Please sign in to comment.