Skip to content

Commit

Permalink
Merge pull request #54 from fjarri/expose-provider
Browse files Browse the repository at this point in the history
Move the compiler and the test provider to the main package
  • Loading branch information
fjarri authored Dec 31, 2023
2 parents 3309ebd + 693312c commit 5755e6f
Show file tree
Hide file tree
Showing 36 changed files with 1,079 additions and 504 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/lints.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ jobs:
run: curl -sSL https://raw.githubusercontent.com/pdm-project/pdm/main/install-pdm.py | python3
- name: Install dependencies
run: |
pdm sync -G lint
pdm sync -G lint,compiler,test-rpc-provider
- name: Run mypy
run: |
pdm run mypy pons
21 changes: 20 additions & 1 deletion docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,14 @@ Errors

.. autoclass:: pons.Unreachable

.. autoclass:: pons.ProtocolError

.. autoclass:: pons.TransactionFailed

.. autoclass:: pons._client.ProviderErrorCode
.. autoclass:: pons._provider.RPCError
:members:

.. autoclass:: pons._provider.RPCErrorCode
:members:

.. autoclass:: pons.ProviderError()
Expand Down Expand Up @@ -120,6 +125,20 @@ Contract ABI
:members:


Testing utilities
-----------------

``pons`` exposes several types useful for testing applications that connect to Ethereum RPC servers. Not intended for the production environment.

.. autoclass:: LocalProvider
:show-inheritance:
:members: disable_auto_mine_transactions, enable_auto_mine_transactions

.. autoclass:: HTTPProviderServer
:members:
:special-members: __call__


Secondary classes
-----------------

Expand Down
2 changes: 2 additions & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ Added
- Exposed ``ClientSession``, ``ConstructorCall``, ``MethodCall``, ``EventFilter``, ``BoundConstructor``, ``BoundConstructorCall``, ``BoundMethod``, ``BoundMethodCall``, ``BoundEvent``, ``BoundEventFilter`` from the top level. (PR_56_)
- Various methods that had a default ``Amount(0)`` for a parameter can now take ``None``. (PR_57_)
- Support for overloaded methods via ``MultiMethod``. (PR_59_)
- Expose ``HTTPProviderServer``, ``LocalProvider``, ``compile_contract_file`` that can be used for tests of Ethereum-using applications. These are gated behind optional features. (PR_54_)


Fixed
Expand All @@ -30,6 +31,7 @@ Fixed

.. _PR_51: https://github.com/fjarri/pons/pull/51
.. _PR_52: https://github.com/fjarri/pons/pull/52
.. _PR_54: https://github.com/fjarri/pons/pull/54
.. _PR_56: https://github.com/fjarri/pons/pull/56
.. _PR_57: https://github.com/fjarri/pons/pull/57
.. _PR_59: https://github.com/fjarri/pons/pull/59
Expand Down
17 changes: 8 additions & 9 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@ A quick usage example:
import pons
import eth_account

from tests.provider import EthereumTesterProvider
from tests.provider_server import ServerHandle
from pons import LocalProvider, HTTPProviderServer, Amount

# Run examples with our test server in the background

Expand All @@ -32,14 +31,14 @@ A quick usage example:
http_provider = None
pons.HTTPProvider = lambda uri: http_provider

# This variable will be set when the EthereumTesterProvider is created
# This variable will be set when the LocalProvider is created

root_account = None
root_signer = None
orig_Account_from_key = eth_account.Account.from_key

def mock_Account_from_key(private_key_hex):
if private_key_hex == "0x<your secret key>":
return root_account
return root_signer.account
else:
return orig_Account_from_key(private_key_hex)

Expand All @@ -60,14 +59,14 @@ A quick usage example:
# This function will start a test server and fill in some global variables

async def run_with_server(func):
global root_account
global root_signer
global http_provider

test_provider = EthereumTesterProvider()
root_account = test_provider.root_account
test_provider = LocalProvider(root_balance=Amount.ether(100))
root_signer = test_provider.root

async with trio.open_nursery() as nursery:
handle = ServerHandle(test_provider)
handle = HTTPProviderServer(test_provider)
http_provider = handle.http_provider
await nursery.start(handle)
await func()
Expand Down
23 changes: 4 additions & 19 deletions pdm.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

19 changes: 18 additions & 1 deletion pons/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
RemoteError,
TransactionFailed,
)
from ._compiler import EVMVersion, compile_contract_file
from ._contract import (
BoundConstructor,
BoundConstructorCall,
Expand Down Expand Up @@ -45,7 +46,16 @@
FallbackStrategyFactory,
PriorityFallback,
)
from ._provider import JSON, HTTPProvider, Unreachable
from ._http_provider_server import HTTPProviderServer
from ._local_provider import LocalProvider
from ._provider import (
JSON,
HTTPProvider,
ProtocolError,
RPCError,
RPCErrorCode,
Unreachable,
)
from ._signer import AccountSigner, Signer

__all__ = [
Expand Down Expand Up @@ -74,8 +84,10 @@
"DeployedContract",
"Either",
"Error",
"LocalProvider",
"Event",
"EventFilter",
"EVMVersion",
"Fallback",
"FallbackProvider",
"FallbackStrategy",
Expand All @@ -87,12 +99,17 @@
"MultiMethod",
"Mutability",
"PriorityFallback",
"ProtocolError",
"ProviderError",
"Receive",
"RemoteError",
"RPCError",
"RPCErrorCode",
"HTTPProviderServer",
"Signer",
"TransactionFailed",
"TxHash",
"Unreachable",
"abi",
"compile_contract_file",
]
42 changes: 10 additions & 32 deletions pons/_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,11 +63,12 @@
)
from ._provider import (
JSON,
InvalidResponse,
Provider,
ProviderSession,
ResponseDict,
RPCError,
UnexpectedResponse,
RPCErrorCode,
)
from ._signer import Signer

Expand Down Expand Up @@ -108,36 +109,13 @@ class TransactionFailed(RemoteError):
"""


class ProviderErrorCode(Enum):
"""Known RPC error codes returned by providers."""

# This is our placeholder value, shouldn't be encountered in a remote server response
UNKNOWN_REASON = 0
"""An error code whose description is not present in this enum."""

SERVER_ERROR = -32000
"""Reserved for implementation-defined server-errors. See the message for details."""

EXECUTION_ERROR = 3
"""Contract transaction failed during execution. See the data for details."""

@classmethod
def from_int(cls, val: int) -> "ProviderErrorCode":
try:
return cls(val)
except ValueError:
return cls.UNKNOWN_REASON


class ProviderError(RemoteError):
"""A general problem with fulfilling the request at the provider's side."""

Code = ProviderErrorCode

raw_code: int
"""The error code returned by the server."""

code: ProviderErrorCode
code: RPCErrorCode
"""The parsed error code."""

message: str
Expand All @@ -149,11 +127,11 @@ class ProviderError(RemoteError):
@classmethod
def from_rpc_error(cls, exc: RPCError) -> "ProviderError":
data = rpc_decode_data(exc.data) if exc.data else None
parsed_code = ProviderErrorCode.from_int(exc.code)
parsed_code = RPCErrorCode.from_int(exc.code)
return cls(exc.code, parsed_code, exc.message, data)

def __init__(
self, raw_code: int, code: ProviderErrorCode, message: str, data: Optional[bytes] = None
self, raw_code: int, code: RPCErrorCode, message: str, data: Optional[bytes] = None
):
super().__init__(raw_code, code, message, data)
self.raw_code = raw_code
Expand All @@ -163,7 +141,7 @@ def __init__(

def __str__(self) -> str:
# Substitute the known code if any, or report the raw integer value otherwise
code = self.raw_code if self.code == ProviderErrorCode.UNKNOWN_REASON else self.code.name
code = self.raw_code if self.code == RPCErrorCode.UNKNOWN_REASON else self.code.name
return f"Provider error ({code}): {self.message}" + (
f" (data: {self.data.hex()})" if self.data else ""
)
Expand All @@ -183,7 +161,7 @@ def _wrapper(func: Callable[Param, Awaitable[RetType]]) -> Callable[Param, Await
async def _wrapped(*args: Any, **kwargs: Any) -> RetType:
try:
result = await func(*args, **kwargs)
except (RPCDecodingError, UnexpectedResponse) as exc:
except (RPCDecodingError, InvalidResponse) as exc:
raise BadResponseFormat(f"{method_name}: {exc}") from exc
except RPCError as exc:
raise ProviderError.from_rpc_error(exc) from exc
Expand Down Expand Up @@ -292,9 +270,9 @@ def decode_contract_error(
) -> Union[ContractPanic, ContractLegacyError, ContractError, ProviderError]:
# A little wonky, but there's no better way to detect legacy errors without a message.
# Hopefully these are used very rarely.
if exc.code == ProviderErrorCode.SERVER_ERROR and exc.message == "execution reverted":
if exc.code == RPCErrorCode.SERVER_ERROR and exc.message == "execution reverted":
return ContractLegacyError("")
if exc.code == ProviderErrorCode.EXECUTION_ERROR:
if exc.code == RPCErrorCode.EXECUTION_ERROR:
try:
error, decoded_data = abi.resolve_error(exc.data or b"")
except UnknownError:
Expand Down Expand Up @@ -788,7 +766,7 @@ async def eth_get_filter_changes(

# TODO: this will go away with generalized RPC decoding.
if not isinstance(results, list):
raise UnexpectedResponse(f"Expected a list as a response, got {type(results).__name__}")
raise InvalidResponse(f"Expected a list as a response, got {type(results).__name__}")

if isinstance(filter_, BlockFilter):
return tuple(BlockHash.rpc_decode(elem) for elem in results)
Expand Down
67 changes: 67 additions & 0 deletions pons/_compiler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
from enum import Enum
from pathlib import Path
from typing import Dict, Mapping, Optional, Union

import solcx

from ._contract import CompiledContract


class EVMVersion(Enum):
"""
Supported EVM versions.
Some may not be available depending on the compiler version.
"""

HOMESTEAD = "homestead"
TANGERINE_WHISTLE = "tangerineWhistle"
SPURIOUS_DRAGON = "spuriousDragon"
BYZANTIUM = "byzantium"
CONSTANTINOPLE = "constantinople"
PETERSBURG = "petersburg"
ISTANBUL = "istanbul"
BERLIN = "berlin"
LONDON = "london"
PARIS = "paris"
SHANGHAI = "shanghai"


def compile_contract_file(
path: Union[str, Path],
*,
import_remappings: Mapping[str, Union[str, Path]] = {},
optimize: bool = False,
evm_version: Optional[EVMVersion] = None,
) -> Dict[str, CompiledContract]:
"""
Compiles the Solidity file at the given ``path`` and returns a dictionary of compiled contracts
keyed by the contract name.
Some ``evm_version`` values may not be available depending on the compiler version.
If ``evm_version`` is not given, the compiler default is used.
"""
path = Path(path).resolve()

compiled = solcx.compile_files(
[path],
output_values=["abi", "bin"],
evm_version=evm_version.value if evm_version else None,
import_remappings=dict(import_remappings),
optimize=optimize,
)

results = {}
for identifier, compiled_contract in compiled.items():
path, contract_name = identifier.split(":")

contract = CompiledContract.from_compiler_output(
json_abi=compiled_contract["abi"],
bytecode=bytes.fromhex(compiled_contract["bin"]),
)

# When `.sol` files are imported, all contracts are added to the flat namespace.
# So all the contract names are guaranteed to be different,
# otherwise the compilation fails.
results[contract_name] = contract

return results
1 change: 0 additions & 1 deletion pons/_entities.py
Original file line number Diff line number Diff line change
Expand Up @@ -608,7 +608,6 @@ def rpc_decode_data(val: Any) -> bytes:
raise RPCDecodingError(f"Could not convert encoded data to bytes: {exc}") from exc


# Only used intests, but I'll leave it here so that it could be matched with `rpc_encode_block()`
def rpc_decode_block(val: Any) -> Union[int, str]:
try:
Block(val) # check if it's one of the enum's values
Expand Down
Loading

0 comments on commit 5755e6f

Please sign in to comment.