Skip to content

Commit

Permalink
Added contract ABI call data generating with ContractABI, renamed exc…
Browse files Browse the repository at this point in the history
…eptions
  • Loading branch information
gabedonnan committed Feb 5, 2024
1 parent 8d95b53 commit faf5343
Show file tree
Hide file tree
Showing 11 changed files with 179 additions and 85 deletions.
18 changes: 10 additions & 8 deletions pythereum/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -58,3 +58,5 @@
recover_raw_transaction,
convert_eth,
)

from pythereum.abi import ContractABI
80 changes: 80 additions & 0 deletions pythereum/abi.py
Original file line number Diff line number Diff line change
@@ -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'(?<!^)(?=[A-Z])')


class ContractABI:
"""
Takes an ethereum contract ABI as input, and generates functions from it.
This allows users to encode call data simply via abi_instance.abi_specified_function_name(abi_specified_args)
"""
def __init__(self, data: list[dict] | str, to_snake_case: bool = True):
if isinstance(data, str):
data = json.loads(data)

for func in data:
if "name" in func:
if to_snake_case:
setattr(self, snake_case.sub("_", func["name"]).lower(), partial(self._encode_call, func["name"]))
else:
setattr(self, func["name"], partial(self._encode_call, func["name"]))

self._functions = {
func["name"]: [
(inp["name"], inp["type"]) for inp in func["inputs"]
] for func in data if "name" in func
}

def get_args(self, name: str) -> 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.
6 changes: 3 additions & 3 deletions pythereum/builders.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -338,15 +338,15 @@ 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})",
)

msg = await resp.json()
else:
raise ERPCBuilderException(
raise PythereumBuilderException(
"BuilderRPC session not started. Either context manage this class or call BuilderRPC.start_session()"
)

Expand Down
10 changes: 5 additions & 5 deletions pythereum/dclasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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"'
)

Expand All @@ -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)
Expand All @@ -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"'
)

Expand All @@ -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)
Expand Down
22 changes: 13 additions & 9 deletions pythereum/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
"""
Expand All @@ -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.
"""
Expand All @@ -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
Expand Down
10 changes: 5 additions & 5 deletions pythereum/gas_managers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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(
Expand Down
Loading

0 comments on commit faf5343

Please sign in to comment.