From faf534315eb6ffa48009cc16cab930efdbbb52ac Mon Sep 17 00:00:00 2001 From: gabedonnan <47415809+gabedonnan@users.noreply.github.com> Date: Mon, 5 Feb 2024 17:55:14 +0000 Subject: [PATCH] Added contract ABI call data generating with ContractABI, renamed exceptions --- pythereum/__init__.py | 18 +++++---- pythereum/abi.py | 80 +++++++++++++++++++++++++++++++++++++++ pythereum/builders.py | 6 +-- pythereum/dclasses.py | 10 ++--- pythereum/exceptions.py | 22 ++++++----- pythereum/gas_managers.py | 10 ++--- pythereum/rpc.py | 30 +++++++-------- pythereum/utils.py | 6 +-- tests/test_builders.py | 12 +++--- tests/test_decoders.py | 14 +++---- tests/test_exceptions.py | 56 +++++++++++++++------------ 11 files changed, 179 insertions(+), 85 deletions(-) create mode 100644 pythereum/abi.py diff --git a/pythereum/__init__.py b/pythereum/__init__.py index cbd7f2d..0cfff2e 100644 --- a/pythereum/__init__.py +++ b/pythereum/__init__.py @@ -39,14 +39,14 @@ ) from pythereum.exceptions import ( - ERPCRequestException, - ERPCInvalidReturnException, - ERPCDecoderException, - ERPCEncoderException, - ERPCSubscriptionException, - ERPCBuilderException, - ERPCManagerException, - ERPCGenericException, + PythereumRequestException, + PythereumInvalidReturnException, + PythereumDecoderException, + PythereumEncoderException, + PythereumSubscriptionException, + PythereumBuilderException, + PythereumManagerException, + PythereumGenericException, ) from pythereum.gas_managers import GasManager @@ -58,3 +58,5 @@ recover_raw_transaction, convert_eth, ) + +from pythereum.abi import ContractABI diff --git a/pythereum/abi.py b/pythereum/abi.py new file mode 100644 index 0000000..0d790ee --- /dev/null +++ b/pythereum/abi.py @@ -0,0 +1,80 @@ +# MIT License +# Copyright (C) 2023 Gabriel "gabedonnan" Donnan +# Further copyright info available at the end of the file + +import json + +from re import compile +from functools import partial +from eth_abi import encode +from eth_utils import function_signature_to_4byte_selector +from .exceptions import PythereumABIException + + +snake_case = compile(r'(? list[tuple]: + """ + Gets the ABI argument names and types for a specified function. + Will attempt to convert name from snake_case to PascalCase if it is not found + :param name: The name of the function you would like to call + :return: a list of tuples of form [(param_name, param_type), ...] + """ + if name in self._functions: + return self._functions[name] + elif (pascal_name := name.replace("_", " ").title().replace(" ", "")) in self._functions: + return self._functions[pascal_name] + else: + raise PythereumABIException(f"Neither {name} nor {pascal_name} is defined in this ABI") + + def _encode_call(self, name: str, *args) -> str: + required_args: list[tuple] = self._functions[name] + + if len(args) != len(required_args): + raise PythereumABIException(f"Incorrect arguments, required arguments are {self.get_args(name)}") + + arg_types = [arg[1] for arg in required_args] + function_signature = name + "(" + ",".join(arg_types) + ")" + return "0x" + function_signature_to_4byte_selector(function_signature).hex() + encode(arg_types, args).hex() + + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. diff --git a/pythereum/builders.py b/pythereum/builders.py index dcf9810..6b44f27 100644 --- a/pythereum/builders.py +++ b/pythereum/builders.py @@ -13,7 +13,7 @@ from pythereum.common import HexStr from pythereum.dclasses import Bundle, MEVBundle -from pythereum.exceptions import ERPCBuilderException, ERPCRequestException +from pythereum.exceptions import PythereumBuilderException, PythereumRequestException from pythereum.rpc import parse_results @@ -338,7 +338,7 @@ async def _send_message( builder.url, json=constructed_json, headers=header_data ) as resp: if resp.status != 200: - raise ERPCRequestException( + raise PythereumRequestException( resp.status, f"Invalid BuilderRPC request for url {builder.url} of form " f"(method={method}, params={params})", @@ -346,7 +346,7 @@ async def _send_message( msg = await resp.json() else: - raise ERPCBuilderException( + raise PythereumBuilderException( "BuilderRPC session not started. Either context manage this class or call BuilderRPC.start_session()" ) diff --git a/pythereum/dclasses.py b/pythereum/dclasses.py index aa23432..06b8acf 100644 --- a/pythereum/dclasses.py +++ b/pythereum/dclasses.py @@ -8,7 +8,7 @@ from dataclasses import dataclass, field from dataclasses_json import dataclass_json, LetterCase, config from pythereum.common import HexStr -from pythereum.exceptions import ERPCDecoderException, ERPCEncoderException +from pythereum.exceptions import PythereumDecoderException, PythereumEncoderException def hex_int_decoder(hex_string: str | None) -> int | None: @@ -17,7 +17,7 @@ def hex_int_decoder(hex_string: str | None) -> int | None: elif re.match(r"^(0[xX])?[A-Fa-f0-9]+$", hex_string): return int(hex_string, 16) else: - raise ERPCDecoderException( + raise PythereumDecoderException( f'{type(hex_string)} "{hex_string}" is an invalid input to decoder "hex_int_decoder"' ) @@ -26,7 +26,7 @@ def hex_int_encoder(int_val: int | None) -> str | None: if int_val is None: return None elif not isinstance(int_val, int): - raise ERPCEncoderException( + raise PythereumEncoderException( f'{type(int_val)} {int_val} is an invalid input to encoder "hex_int_encoder"' ) return hex(int_val) @@ -40,7 +40,7 @@ def hex_decoder(hex_string: str | None) -> HexStr | None: elif hex_string == "0x": return None else: - raise ERPCDecoderException( + raise PythereumDecoderException( f'{type(hex_string)} "{hex_string}" is an invalid input to decoder "hex_decoder"' ) @@ -52,7 +52,7 @@ def hex_encoder(hex_obj: HexStr | None) -> str | None: if hex_obj is None: return None elif not isinstance(hex_obj, HexStr): - raise ERPCEncoderException( + raise PythereumEncoderException( f'{type(hex_obj)} {hex_obj} is an invalid input to encoder "hex_encoder"' ) return str(hex_obj) diff --git a/pythereum/exceptions.py b/pythereum/exceptions.py index 057dcbd..0783e69 100644 --- a/pythereum/exceptions.py +++ b/pythereum/exceptions.py @@ -3,7 +3,7 @@ # Further copyright info available at the end of the file -class ERPCBaseException(Exception): +class PythereumBaseException(Exception): """ Base exception class for Ethereum RPC interactions. """ @@ -13,7 +13,7 @@ def __init__(self, message: str): super().__init__(self.message) -class ERPCRequestException(ERPCBaseException): +class PythereumRequestException(PythereumBaseException): """ Raised when an error is returned from the Ethereum RPC. """ @@ -24,48 +24,52 @@ def __init__(self, code: int, message: str = "Generic ERPC Error"): super().__init__(full_message) -class ERPCInvalidReturnException(ERPCBaseException): +class PythereumInvalidReturnException(PythereumBaseException): """ Raised when the Ethereum RPC returns a value which is incorrectly formatted. """ -class ERPCDecoderException(ERPCBaseException): +class PythereumDecoderException(PythereumBaseException): """ Raised when invalid data is input to a decoder and an error is thrown. """ -class ERPCEncoderException(ERPCBaseException): +class PythereumEncoderException(PythereumBaseException): """ Raised when invalid data is input to an encoder and an error is thrown. """ -class ERPCSubscriptionException(ERPCBaseException): +class PythereumSubscriptionException(PythereumBaseException): """ Raised when a subscription request is rejected by a host or for other generic subscription errors. """ -class ERPCBuilderException(ERPCBaseException): +class PythereumBuilderException(PythereumBaseException): """ Raised for exceptions related to builders and the BuilderRPC """ -class ERPCManagerException(ERPCBaseException): +class PythereumManagerException(PythereumBaseException): """ Raised for exceptions related to manager classes such as nonce managers or gas managers """ -class ERPCGenericException(ERPCBaseException): +class PythereumGenericException(PythereumBaseException): """ Raised for exceptions which do not fall into any of the above categories, things like utility functions will use it """ +class PythereumABIException(Exception): + ... + + # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights diff --git a/pythereum/gas_managers.py b/pythereum/gas_managers.py index 0a6dd0e..e5e2103 100644 --- a/pythereum/gas_managers.py +++ b/pythereum/gas_managers.py @@ -5,7 +5,7 @@ import statistics from contextlib import asynccontextmanager -from pythereum.exceptions import ERPCManagerException, ERPCInvalidReturnException +from pythereum.exceptions import PythereumManagerException, PythereumInvalidReturnException from pythereum.common import EthDenomination, GasStrategy, BlockTag from pythereum.rpc import EthRPC from pythereum.dclasses import TransactionFull, Transaction @@ -58,7 +58,7 @@ async def _get_latest_receipts( transactions = latest_block.transactions self.latest_transactions = transactions if len(transactions) == 0: - raise ERPCInvalidReturnException( + raise PythereumInvalidReturnException( f"Invalid vlue: {transactions} returned from _get_latest_receipts" ) return transactions @@ -98,7 +98,7 @@ async def suggest( case GasStrategy.custom: res = self.custom_pricing(prices) case _: - raise ERPCManagerException(f"Invalid strategy of type {strategy} used") + raise PythereumManagerException(f"Invalid strategy of type {strategy} used") return round(res) async def fill_transaction( @@ -158,7 +158,7 @@ async def fill_transaction( def custom_pricing(self, prices): # Override this function when subclassing for custom pricing implementation - raise ERPCManagerException("Custom pricing strategy not defined for this class") + raise PythereumManagerException("Custom pricing strategy not defined for this class") class InformedGasManager: @@ -209,7 +209,7 @@ async def _set_initial_price(self): transactions = latest_block.transactions self.latest_transactions = transactions if len(transactions) == 0: - raise ERPCInvalidReturnException( + raise PythereumInvalidReturnException( f"Invalid vlue: {transactions} returned from _get_latest_receipts" ) for key, attribute in zip( diff --git a/pythereum/rpc.py b/pythereum/rpc.py index d6d1167..ffeffc2 100644 --- a/pythereum/rpc.py +++ b/pythereum/rpc.py @@ -10,10 +10,10 @@ import websockets from pythereum.socket_pool import WebsocketPool from pythereum.exceptions import ( - ERPCRequestException, - ERPCInvalidReturnException, - ERPCSubscriptionException, - ERPCManagerException, + PythereumRequestException, + PythereumInvalidReturnException, + PythereumSubscriptionException, + PythereumManagerException, ) from pythereum.common import ( HexStr, @@ -56,9 +56,9 @@ def parse_results( errmsg = res["error"]["message"] + ( "" if builder is None else f" for builder {builder}" ) - raise ERPCRequestException(res["error"]["code"], errmsg) + raise PythereumRequestException(res["error"]["code"], errmsg) else: - raise ERPCInvalidReturnException( + raise PythereumInvalidReturnException( f"Invalid return value from RPC, return format: {res}" ) @@ -158,7 +158,7 @@ async def __aenter__(self): else: self._close_pool = False else: - raise ERPCManagerException( + raise PythereumManagerException( "NonceManager was never given EthRPC or RPC Url instance" ) return self @@ -212,7 +212,7 @@ def __init__( :param pool_size: The number of websocket connections opened for the WebsocketPool :param use_socket_pool: Whether the socket pool should be used or AIOHTTP requests :param connection_max_payload_size: The maximum payload size a websocket can send or recv in one message - :param connection_timeout: The maximum time in seconds to wait for a response from the websocket before timing out (default 20s ) + :param connection_timeout: The maximum time in seconds to wait for a response from the websocket before timeout """ self._id = 0 if use_socket_pool: @@ -412,7 +412,7 @@ async def _send_message_aio(self, built_msg: str) -> dict: headers={"Content-Type": "application/json"}, ) as resp: if resp.status != 200: - raise ERPCRequestException( + raise PythereumRequestException( resp.status, f"Bad EthRPC aiohttp request for url {self._http_url} of form {built_msg}", ) @@ -447,7 +447,7 @@ async def subscribe(self, method: SubscriptionType, max_message_num: int = -1) - yield sub finally: if subscription_id == "": - raise ERPCSubscriptionException( + raise PythereumSubscriptionException( f"Subscription of type {method.value} rejected by destination." ) await self._unsubscribe(subscription_id, ws) @@ -1244,7 +1244,7 @@ async def get_filter_changes( case list(): return [HexStr(result) for result in msg] case _: - raise ERPCInvalidReturnException( + raise PythereumInvalidReturnException( f"Unexpected return of form {msg} in get_filter_changes" ) @@ -1272,7 +1272,7 @@ async def get_filter_logs( ): return [Log.from_dict(result) for result in msg] case _: - raise ERPCInvalidReturnException( + raise PythereumInvalidReturnException( f"Unexpected return of form {msg} in get_filter_changes" ) @@ -1314,7 +1314,7 @@ async def get_logs( ): return [Log.from_dict(result) for result in msg] case _: - raise ERPCInvalidReturnException( + raise PythereumInvalidReturnException( f"Unexpected return of form {msg} in get_filter_changes" ) @@ -1346,7 +1346,7 @@ async def sha3( case list(): return [HexStr(result) for result in msg] case _: - raise ERPCInvalidReturnException( + raise PythereumInvalidReturnException( f"Unexpected return of form {msg} in sha3" ) @@ -1474,4 +1474,4 @@ async def send_raw( # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. \ No newline at end of file +# SOFTWARE. diff --git a/pythereum/utils.py b/pythereum/utils.py index 0a16248..d0c9f23 100644 --- a/pythereum/utils.py +++ b/pythereum/utils.py @@ -1,6 +1,6 @@ from Crypto.Hash import keccak -from pythereum import ERPCGenericException +from pythereum import PythereumGenericException from pythereum.common import HexStr, EthDenomination from pythereum.dclasses import TransactionFull from eth_account._utils.legacy_transactions import ( @@ -77,7 +77,7 @@ def convert_eth( if hasattr(EthDenomination, convert_from.lower()): convert_from = EthDenomination[convert_from.lower()] else: - raise ERPCGenericException( + raise PythereumGenericException( "convert_from value string is not a member of EthDenomination" ) @@ -85,7 +85,7 @@ def convert_eth( if hasattr(EthDenomination, convert_to.lower()): convert_to = EthDenomination[convert_to.lower()] else: - raise ERPCGenericException( + raise PythereumGenericException( "convert_to value string is not a member of EthDenomination" ) diff --git a/tests/test_builders.py b/tests/test_builders.py index b4c685b..ff37af4 100644 --- a/tests/test_builders.py +++ b/tests/test_builders.py @@ -7,7 +7,7 @@ import pythereum as pye from eth_account import Account -from pythereum.exceptions import ERPCRequestException +from pythereum.exceptions import PythereumRequestException """ Each of the following tests sends invalid requests to the given endpoint @@ -24,7 +24,7 @@ async def test_titan_builder(): async with pye.BuilderRPC(pye.TitanBuilder(), Account.create().key) as brpc: try: print(await brpc.send_private_transaction(None)) - except ERPCRequestException as e: + except PythereumRequestException as e: assert ( str(e) == "Error -32000: no transaction found for builder https://rpc.titanbuilder.xyz" @@ -43,7 +43,7 @@ async def test_0x69_builder(): async with pye.BuilderRPC(pye.Builder0x69()) as brpc: try: await brpc.send_private_transaction(None) - except ERPCRequestException as e: + except PythereumRequestException as e: assert str(e) == ( "Error -32602: invalid argument 0: json: cannot unmarshal non-string into Go value of " "type hexutil.Bytes for builder https://builder0x69.io/" @@ -56,7 +56,7 @@ async def test_flashbots_builder(): async with pye.BuilderRPC(pye.FlashbotsBuilder(), Account.create().key) as brpc: try: await brpc.send_private_transaction(None) - except ERPCRequestException as e: + except PythereumRequestException as e: assert str(e) == ( "Error 403: Invalid BuilderRPC request for url https://relay.flashbots.net of form (" "method=eth_sendPrivateRawTransaction, params=[{'tx': None, 'preferences': None}])" @@ -69,7 +69,7 @@ async def test_loki_builder(): async with pye.BuilderRPC(pye.LokiBuilder()) as brpc: try: await brpc.send_private_transaction(None) - except ERPCRequestException as e: + except PythereumRequestException as e: assert str(e) == ( "Error -32603: Timeout for builder https://rpc.lokibuilder.xyz/" "\nPlease consult your endpoint's documentation for info on error codes." @@ -83,7 +83,7 @@ async def test_all_builders(): ) as brpc: try: await brpc.send_private_transaction(None) - except ERPCRequestException as e: + except PythereumRequestException as e: assert str(e) in ( "Error 400: Invalid BuilderRPC request " "for url https://rpc.beaverbuild.org/ of form (method=eth_sendPrivateRawTransaction, params=[None])" diff --git a/tests/test_decoders.py b/tests/test_decoders.py index fcbaedd..3e4db25 100644 --- a/tests/test_decoders.py +++ b/tests/test_decoders.py @@ -23,8 +23,8 @@ log_decoder, transaction_decoder, access_decoder, - ERPCDecoderException, - ERPCEncoderException, + PythereumDecoderException, + PythereumEncoderException, HexStr, Log, Access, @@ -39,7 +39,7 @@ def test_hex_int_decoder(): assert hex_int_decoder("0") == 0 assert hex_int_decoder("0xaaaa") == 43690 assert hex_int_decoder(None) is None - with pytest.raises(ERPCDecoderException) as info: + with pytest.raises(PythereumDecoderException) as info: hex_int_decoder("zzzzz") @@ -47,7 +47,7 @@ def test_hex_int_encoder(): assert hex_int_encoder(0) == "0x0" assert hex_int_encoder(43690) == "0xaaaa" assert hex_int_encoder(None) is None - with pytest.raises(ERPCEncoderException) as info: + with pytest.raises(PythereumEncoderException) as info: hex_int_encoder("zzzzz") @@ -56,7 +56,7 @@ def test_hex_decoder(): assert hex_decoder("0") == HexStr("0x0") assert hex_decoder("0xaaaa") == HexStr("0xaaaa") assert hex_decoder(None) is None - with pytest.raises(ERPCDecoderException) as info: + with pytest.raises(PythereumDecoderException) as info: hex_decoder("zzzzz") @@ -75,7 +75,7 @@ def test_hex_list_decoder(): HexStr("0xaaaa"), ] assert hex_list_decoder(None) is None - with pytest.raises(ERPCDecoderException) as info: + with pytest.raises(PythereumDecoderException) as info: hex_list_decoder(["zzzzz"]) @@ -103,7 +103,7 @@ def test_hex_list_list_decoder(): [HexStr("0xaaaa")], ] assert hex_list_list_decoder(None) is None - with pytest.raises(ERPCDecoderException) as info: + with pytest.raises(PythereumDecoderException) as info: hex_list_list_decoder([["zzzzz"]]) diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index d85da7d..1a68aae 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -3,22 +3,23 @@ # Further copyright info available at the end of the file from pythereum.exceptions import ( - ERPCRequestException, - ERPCInvalidReturnException, - ERPCDecoderException, - ERPCEncoderException, - ERPCSubscriptionException, - ERPCBuilderException, - ERPCManagerException, - ERPCGenericException, + PythereumRequestException, + PythereumInvalidReturnException, + PythereumDecoderException, + PythereumEncoderException, + PythereumSubscriptionException, + PythereumBuilderException, + PythereumManagerException, + PythereumGenericException, + PythereumABIException ) # Test for ERPCRequestException -def test_ERPCRequestException(): +def test_PythereumRequestException(): code = 404 message = "Not Found" - exception = ERPCRequestException(code, message) + exception = PythereumRequestException(code, message) assert ( str(exception) == f"Error {code}: {message}\nPlease consult your endpoint's documentation for info on error codes." @@ -27,54 +28,61 @@ def test_ERPCRequestException(): # Test for ERPCInvalidReturnException -def test_ERPCInvalidReturnException(): +def test_PythereumInvalidReturnException(): message = "Invalid Return" - exception = ERPCInvalidReturnException(message) + exception = PythereumInvalidReturnException(message) assert str(exception) == message # Test for ERPCDecoderException -def test_ERPCDecoderException(): +def test_PythereumDecoderException(): message = "Decoder Error" - exception = ERPCDecoderException(message) + exception = PythereumDecoderException(message) assert str(exception) == message # Test for ERPCEncoderException -def test_ERPCEncoderException(): +def test_PythereumEncoderException(): message = "Encoder Error" - exception = ERPCEncoderException(message) + exception = PythereumEncoderException(message) assert str(exception) == message # Test for ERPCSubscriptionException -def test_ERPCSubscriptionException(): +def test_PythereumSubscriptionException(): message = "Subscription Error" - exception = ERPCSubscriptionException(message) + exception = PythereumSubscriptionException(message) assert str(exception) == message -def test_ERPCBuilderException(): +def test_PythereumBuilderException(): message = "Builder Error" - exception = ERPCBuilderException(message) + exception = PythereumBuilderException(message) assert str(exception) == message -def test_ERPCManagerException(): +def test_PythereumManagerException(): message = "Manager Error" - exception = ERPCManagerException(message) + exception = PythereumManagerException(message) assert str(exception) == message -def test_ERPCGenericException(): +def test_PythereumGenericException(): message = "Generic Error" - exception = ERPCGenericException(message) + exception = PythereumGenericException(message) + + assert str(exception) == message + + +def test_PythereumABIException(): + message = "ABI Error" + exception = PythereumABIException(message) assert str(exception) == message