Skip to content

Commit

Permalink
feat: block API (#279)
Browse files Browse the repository at this point in the history
  • Loading branch information
antazoey authored Dec 3, 2021
1 parent 7e22890 commit 42aa439
Show file tree
Hide file tree
Showing 16 changed files with 167 additions and 17 deletions.
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@
"IPython>=7.25",
"pytest>=6.0,<7.0",
"rich>=10.14,<11",
"typing-extensions ; python_version<'3.8'",
"web3[tester]>=5.25.0,<6.0.0",
],
entry_points={
Expand Down
6 changes: 6 additions & 0 deletions src/ape/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
from .explorers import ExplorerAPI
from .networks import EcosystemAPI, NetworkAPI, ProviderContextManager, create_network_type
from .providers import (
BlockAPI,
BlockConsensusAPI,
BlockGasAPI,
ProviderAPI,
ReceiptAPI,
TestProviderAPI,
Expand All @@ -21,6 +24,9 @@
"AccountContainerAPI",
"Address",
"AddressAPI",
"BlockAPI",
"BlockConsensusAPI",
"BlockGasAPI",
"ConfigDict",
"ConfigEnum",
"ConfigItem",
Expand Down
3 changes: 2 additions & 1 deletion src/ape/api/networks.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

from .contracts import ContractLog
from .explorers import ExplorerAPI
from .providers import ProviderAPI, ReceiptAPI, TransactionAPI, TransactionType
from .providers import BlockAPI, ProviderAPI, ReceiptAPI, TransactionAPI, TransactionType


@abstractdataclass
Expand All @@ -35,6 +35,7 @@ class EcosystemAPI:

transaction_types: Dict["TransactionType", Type["TransactionAPI"]]
receipt_class: Type["ReceiptAPI"]
block_class: Type["BlockAPI"]

_default_network: str = "development"

Expand Down
87 changes: 83 additions & 4 deletions src/ape/api/providers.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
from enum import Enum, IntEnum
from pathlib import Path
from typing import Iterator, List, Optional
from typing import Dict, Iterator, List, Optional

from dataclassy import as_dict
from eth_typing import HexStr
from eth_utils import add_0x_prefix
from hexbytes import HexBytes
from web3 import Web3

from ape.exceptions import ProviderError
from ape.logging import logger
from ape.types import TransactionSignature
from ape.types import BlockID, TransactionSignature

from . import networks
from .base import abstractdataclass, abstractmethod
Expand Down Expand Up @@ -133,6 +135,45 @@ def decode(cls, data: dict) -> "ReceiptAPI":
...


@abstractdataclass
class BlockGasAPI:
gas_limit: int
gas_used: int
base_fee: Optional[int] = None

@classmethod
@abstractmethod
def decode(cls, data: Dict) -> "BlockGasAPI":
...


@abstractdataclass
class BlockConsensusAPI:
difficulty: Optional[int] = None
total_difficulty: Optional[int] = None

@classmethod
@abstractmethod
def decode(cls, data: Dict) -> "BlockConsensusAPI":
...


@abstractdataclass
class BlockAPI:
gas_data: BlockGasAPI
consensus_data: BlockConsensusAPI
hash: HexBytes
number: int
parent_hash: HexBytes
size: int
timestamp: float

@classmethod
@abstractmethod
def decode(cls, data: Dict) -> "BlockAPI":
...


@abstractdataclass
class ProviderAPI:
"""
Expand Down Expand Up @@ -192,6 +233,10 @@ def priority_fee(self) -> int:
def base_fee(self) -> int:
raise NotImplementedError("base_fee is not implemented by this provider")

@abstractmethod
def get_block(self, block_id: BlockID) -> BlockAPI:
...

@abstractmethod
def send_call(self, txn: TransactionAPI) -> bytes: # Return value of function
...
Expand Down Expand Up @@ -271,8 +316,42 @@ def priority_fee(self) -> int:

@property
def base_fee(self) -> int:
block = self._web3.eth.get_block("latest")
return block.baseFeePerGas # type: ignore
"""
Returns the current base fee from the latest block.
NOTE: If your chain does not support base_fees (EIP-1559),
this method will raise a not-implemented error.
"""
block = self.get_block("latest")

if block.gas_data.base_fee is None:
# Non-EIP-1559 chains or we time-travelled pre-London fork.
raise NotImplementedError("base_fee is not implemented by this provider.")

return block.gas_data.base_fee

def get_block(self, block_id: BlockID) -> BlockAPI:
"""
Returns a block for the given ID.
Args:
block_id: The ID of the block to get. Set as
"latest" to get the latest block,
"earliest" to get the earliest block,
"pending" to get the pending block,
or pass in a block number or hash.
Returns:
The block for the given block ID.
"""
if isinstance(block_id, str):
block_id = HexStr(block_id)

if block_id.isnumeric():
block_id = add_0x_prefix(block_id)

block_data = self._web3.eth.get_block(block_id)
return self.network.ecosystem.block_class.decode(block_data) # type: ignore

def get_nonce(self, address: str) -> int:
"""
Expand Down
2 changes: 1 addition & 1 deletion src/ape/managers/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from dataclassy import dataclass

from ape.api.config import ConfigDict, ConfigItem
from ape.api import ConfigDict, ConfigItem
from ape.exceptions import ConfigError
from ape.plugins import PluginManager
from ape.utils import load_config
Expand Down
2 changes: 1 addition & 1 deletion src/ape/plugins/config.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from typing import Type

from ape.api.config import ConfigItem
from ape.api import ConfigItem

from .pluggy_patch import PluginType, hookspec

Expand Down
13 changes: 13 additions & 0 deletions src/ape/types/__init__.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,25 @@
import sys
from typing import Union

from eth_typing import ChecksumAddress as AddressType
from hexbytes import HexBytes

from .contract import ABI, Bytecode, Checksum, Compiler, ContractType, Source
from .manifest import PackageManifest, PackageMeta
from .signatures import MessageSignature, SignableMessage, TransactionSignature

# We can remove this once we stop supporting python3.7.
if sys.version_info >= (3, 8):
from typing import Literal
else:
from typing_extensions import Literal

BlockID = Union[str, int, HexBytes, Literal["earliest", "latest", "pending"]]

__all__ = [
"ABI",
"AddressType",
"BlockID",
"Bytecode",
"Checksum",
"Compiler",
Expand Down
2 changes: 1 addition & 1 deletion src/ape_compile/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from ape import plugins
from ape.api.config import ConfigEnum, ConfigItem
from ape.api import ConfigEnum, ConfigItem


class EvmVersion(ConfigEnum):
Expand Down
41 changes: 40 additions & 1 deletion src/ape_ethereum/ecosystem.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Any, Optional
from typing import Any, Dict, Optional

from eth_abi import decode_abi as abi_decode
from eth_abi import encode_abi as abi_encode
Expand All @@ -13,6 +13,9 @@
from hexbytes import HexBytes

from ape.api import (
BlockAPI,
BlockConsensusAPI,
BlockGasAPI,
ContractLog,
EcosystemAPI,
ReceiptAPI,
Expand Down Expand Up @@ -158,12 +161,48 @@ def decode(cls, data: dict) -> ReceiptAPI:
)


class BlockGasFee(BlockGasAPI):
@classmethod
def decode(cls, data: Dict) -> BlockGasAPI:
return BlockGasFee( # type: ignore
gas_limit=data["gasLimit"],
gas_used=data["gasUsed"],
base_fee=data.get("baseFeePerGas"),
)


class BlockConsensus(BlockConsensusAPI):
difficulty: Optional[int] = None
total_difficulty: Optional[int] = None

@classmethod
def decode(cls, data: Dict) -> BlockConsensusAPI:
return cls(
difficulty=data.get("difficulty"), total_difficulty=data.get("totalDifficulty")
) # type: ignore


class Block(BlockAPI):
@classmethod
def decode(cls, data: Dict) -> BlockAPI:
return cls( # type: ignore
gas_data=BlockGasFee.decode(data),
consensus_data=BlockConsensus.decode(data),
number=data["number"],
size=data.get("size"),
timestamp=data.get("timestamp"),
hash=data.get("hash"),
parent_hash=data.get("hash"),
)


class Ethereum(EcosystemAPI):
transaction_types = {
TransactionType.STATIC: StaticFeeTransaction,
TransactionType.DYNAMIC: DynamicFeeTransaction,
}
receipt_class = Receipt
block_class = Block

def encode_calldata(self, abi: ABI, *args) -> bytes:
if abi.inputs:
Expand Down
3 changes: 1 addition & 2 deletions src/ape_geth/providers.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,7 @@
from web3.middleware import geth_poa_middleware
from web3.types import NodeInfo

from ape.api import ReceiptAPI, TransactionAPI, UpstreamProvider, Web3Provider
from ape.api.config import ConfigItem
from ape.api import ConfigItem, ReceiptAPI, TransactionAPI, UpstreamProvider, Web3Provider
from ape.exceptions import ContractLogicError, ProviderError, TransactionError, VirtualMachineError
from ape.logging import logger
from ape.utils import extract_nested_value, gas_estimation_error_message, generate_dev_accounts
Expand Down
2 changes: 1 addition & 1 deletion src/ape_plugins/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from ape import plugins
from ape.api.config import ConfigDict
from ape.api import ConfigDict


@plugins.register(plugins.Config)
Expand Down
2 changes: 1 addition & 1 deletion src/ape_test/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from ape import plugins
from ape.api.config import ConfigItem
from ape.api import ConfigItem

from .accounts import TestAccount, TestAccountContainer
from .providers import LocalNetwork
Expand Down
2 changes: 1 addition & 1 deletion tests/functional/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@

from ape.api import (
AccountContainerAPI,
ConfigItem,
EcosystemAPI,
NetworkAPI,
ProviderAPI,
ReceiptAPI,
TransactionAPI,
TransactionStatusEnum,
)
from ape.api.config import ConfigItem
from ape.exceptions import ContractLogicError

TEST_ADDRESS = "0x0A78AAAAA2122100000b9046f0A085AB2E111113"
Expand Down
4 changes: 2 additions & 2 deletions tests/integration/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@


@pytest.fixture
def in_test_network():
def eth_tester_provider():
with networks.parse_network_choice("::test"):
yield
yield networks.active_provider
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from ape import accounts, convert


def test_transfer(in_test_network):
def test_transfer(eth_tester_provider):
sender = accounts.test_accounts[0]
receiver = accounts.test_accounts[1]
initial_balance = receiver.balance
Expand Down
12 changes: 12 additions & 0 deletions tests/integration/test_networks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import pytest
from eth_typing import HexStr


@pytest.mark.parametrize("block_id", ("latest", 0, "0", "0x0", HexStr("0x0")))
def test_get_block(eth_tester_provider, block_id):
latest_block = eth_tester_provider.get_block(block_id)

# Each parameter is the same as requesting the first block.
assert latest_block.number == 0
assert latest_block.gas_data.base_fee == 1000000000
assert latest_block.gas_data.gas_used == 0

0 comments on commit 42aa439

Please sign in to comment.