Eliminate dependencies on all ethereum packages except py_ecc

The rationale for this patch is similar to that of commit a7126ed:

- Many packages were imported for the sake of just a few trivial conversion
  functions.  These functions have been copied into the local pyethereum.utils.

- rlp has been locally copied and its import statements modified to import
  the locally-copied functions.  Unneeded classes and functions have been
  removed.

- As a result, dependencies on the following external packages have been
  eliminated:

  + rlp
  + eth-hash
  + eth-utils
  + eth-typing
  + toolz
  + cytoolz
  + setuptools
This commit is contained in:
The MMGen Project 2019-03-25 09:38:49 +00:00
commit 66d0f76635
Signed by: mmgen
GPG key ID: 3F8B1861E32B7DA2
22 changed files with 1389 additions and 258 deletions

View file

@ -21,7 +21,7 @@ altcoins.eth.contract: Ethereum contract and token classes for the MMGen suite
"""
from decimal import Decimal
import rlp
from . import rlp
from mmgen.globalvars import g
from mmgen.common import *
@ -110,7 +110,7 @@ class Token(MMGenObject): # ERC20
def txsign(self,tx_in,key,from_addr,chain_id=None):
try: from ethereum.transactions import Transaction
except: from mmgen.altcoins.eth.pyethereum.transactions import Transaction
except: from .pyethereum.transactions import Transaction
if chain_id is None:
chain_id_method = ('parity_chainId','eth_chainId')['eth_chainId' in g.rpch.caps]

View file

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2015 Vitalik Buterin, Heiko Hees
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.

View file

@ -1,12 +1,13 @@
#
# Adapted from: # https://github.com/ethereum/pyethereum/blob/master/ethereum/transactions.py
#
import rlp
from rlp.sedes import big_endian_int,binary
from mmgen.altcoins.eth.pyethereum.utils import (
str_to_bytes,encode_hex,ascii_chr,big_endian_to_int,TT256,mk_contract_address,
from .. import rlp
from ..rlp.sedes import big_endian_int,binary
from .utils import (
str_to_bytes,encode_hex,ascii_chr,big_endian_to_int,
TT256,mk_contract_address,
ecsign,ecrecover_to_pub,normalize_key )
import mmgen.altcoins.eth.pyethereum.utils as utils
from . import utils
class InvalidTransaction(Exception): pass
class opcodes(object):

View file

@ -2,12 +2,9 @@
# Adapted from: https://github.com/ethereum/pyethereum/blob/master/ethereum/utils.py
#
from py_ecc.secp256k1 import privtopub, ecdsa_raw_sign, ecdsa_raw_recover
import rlp
from rlp.sedes import big_endian_int, BigEndianInt, Binary
from eth_utils import encode_hex as encode_hex_0x
from eth_utils import decode_hex, int_to_big_endian, big_endian_to_int
from rlp.utils import ALL_BYTES
from py_ecc.secp256k1 import privtopub,ecdsa_raw_sign,ecdsa_raw_recover
from .. import rlp
from ..rlp.sedes import Binary
from mmgen.globalvars import g
try:
@ -19,6 +16,60 @@ except:
def sha3_256(x):
return keccak_256(x).digest()
import struct
ALL_BYTES = tuple( struct.pack('B', i) for i in range(256) )
# from eth_utils:
# Type ignored for `codecs.decode()` due to lack of mypy support for 'hex' encoding
# https://github.com/python/typeshed/issues/300
from typing import AnyStr,Any,Callable,TypeVar
import codecs
import functools
T = TypeVar("T")
TVal = TypeVar("TVal")
TKey = TypeVar("TKey")
def apply_to_return_value(callback: Callable[..., T]) -> Callable[..., Callable[..., T]]:
def outer(fn):
# We would need to type annotate *args and **kwargs but doing so segfaults
# the PyPy builds. We ignore instead.
@functools.wraps(fn)
def inner(*args, **kwargs) -> T: # type: ignore
return callback(fn(*args, **kwargs))
return inner
return outer
to_list = apply_to_return_value(list)
to_set = apply_to_return_value(set)
to_dict = apply_to_return_value(dict)
to_tuple = apply_to_return_value(tuple)
to_list = apply_to_return_value(list)
def encode_hex_0x(value: AnyStr) -> str:
if not is_string(value):
raise TypeError("Value must be an instance of str or unicode")
binary_hex = codecs.encode(value, "hex") # type: ignore
return '0x' + binary_hex.decode("ascii")
def decode_hex(value: str) -> bytes:
if not isinstance(value,str):
raise TypeError("Value must be an instance of str")
return codecs.decode(remove_0x_prefix(value), "hex") # type: ignore
def is_bytes(value: Any) -> bool:
return isinstance(value, (bytes,bytearray))
def int_to_big_endian(value: int) -> bytes:
return value.to_bytes((value.bit_length() + 7) // 8 or 1, "big")
def big_endian_to_int(value: bytes) -> int:
return int.from_bytes(value, "big")
# end from eth_utils
class Memoize:
def __init__(self, fn):
@ -30,9 +81,6 @@ class Memoize:
return self.memo[args]
TT256 = 2 ** 256
TT256M1 = 2 ** 256 - 1
TT255 = 2 ** 255
SECP256K1P = 2**256 - 4294968273
def is_numeric(x): return isinstance(x, int)
@ -46,24 +94,11 @@ def to_string(value):
if isinstance(value, int):
return bytes(str(value), 'utf-8')
def int_to_bytes(value):
if isinstance(value, bytes):
return value
return int_to_big_endian(value)
def to_string_for_regexp(value):
return str(to_string(value), 'utf-8')
unicode = str
def bytearray_to_bytestr(value):
return bytes(value)
def encode_int32(v):
return v.to_bytes(32, byteorder='big')
def bytes_to_int(value):
return int.from_bytes(value, byteorder='big')
def str_to_bytes(value):
if isinstance(value, bytearray):
value = bytes(value)
@ -102,39 +137,6 @@ def mk_metropolis_contract_address(sender, initcode):
return sha3(normalize_address(sender) + initcode)[12:]
def safe_ord(value):
if isinstance(value, int):
return value
else:
return ord(value)
# decorator
def flatten(li):
o = []
for l in li:
o.extend(l)
return o
def bytearray_to_int(arr):
o = 0
for a in arr:
o = (o << 8) + a
return o
def int_to_32bytearray(i):
o = [0] * 32
for x in range(32):
o[31 - x] = i & 0xff
i >>= 8
return o
# sha3_count = [0]
def sha3(seed):
return sha3_256(to_string(seed))
@ -183,36 +185,6 @@ def normalize_key(key):
return o
def zpad(x, l):
""" Left zero pad value `x` at least to length `l`.
>>> zpad('', 1)
'\x00'
>>> zpad('\xca\xfe', 4)
'\x00\x00\xca\xfe'
>>> zpad('\xff', 1)
'\xff'
>>> zpad('\xca\xfe', 2)
'\xca\xfe'
"""
return b'\x00' * max(0, l - len(x)) + x
def rzpad(value, total_length):
""" Right zero pad value `x` at least to length `l`.
>>> zpad('', 1)
'\x00'
>>> zpad('\xca\xfe', 4)
'\xca\xfe\x00\x00'
>>> zpad('\xff', 1)
'\xff'
>>> zpad('\xca\xfe', 2)
'\xca\xfe'
"""
return value + b'\x00' * max(0, total_length - len(value))
def int_to_addr(x):
o = [b''] * 20
for i in range(20):
@ -221,153 +193,10 @@ def int_to_addr(x):
return b''.join(o)
def coerce_addr_to_bin(x):
if is_numeric(x):
return encode_hex(zpad(big_endian_int.serialize(x), 20))
elif len(x) == 40 or len(x) == 0:
return decode_hex(x)
else:
return zpad(x, 20)[-20:]
def coerce_addr_to_hex(x):
if is_numeric(x):
return encode_hex(zpad(big_endian_int.serialize(x), 20))
elif len(x) == 40 or len(x) == 0:
return x
else:
return encode_hex(zpad(x, 20)[-20:])
def coerce_to_int(x):
if is_numeric(x):
return x
elif len(x) == 40:
return big_endian_to_int(decode_hex(x))
else:
return big_endian_to_int(x)
def coerce_to_bytes(x):
if is_numeric(x):
return big_endian_int.serialize(x)
elif len(x) == 40:
return decode_hex(x)
else:
return x
def parse_int_or_hex(s):
if is_numeric(s):
return s
elif s[:2] in (b'0x', '0x'):
s = to_string(s)
tail = (b'0' if len(s) % 2 else b'') + s[2:]
return big_endian_to_int(decode_hex(tail))
else:
return int(s)
def ceil32(x):
return x if x % 32 == 0 else x + 32 - (x % 32)
def to_signed(i):
return i if i < TT255 else i - TT256
def sha3rlp(x):
return sha3(rlp.encode(x))
# Format encoders/decoders for bin, addr, int
def decode_bin(v):
"""decodes a bytearray from serialization"""
if not is_string(v):
raise Exception("Value must be binary, not RLP array")
return v
def decode_addr(v):
"""decodes an address from serialization"""
if len(v) not in [0, 20]:
raise Exception("Serialized addresses must be empty or 20 bytes long!")
return encode_hex(v)
def decode_int(v):
"""decodes and integer from serialization"""
if len(v) > 0 and (v[0] == b'\x00' or v[0] == 0):
raise Exception("No leading zero bytes allowed for integers")
return big_endian_to_int(v)
def decode_int256(v):
return big_endian_to_int(v)
def encode_bin(v):
"""encodes a bytearray into serialization"""
return v
def encode_root(v):
"""encodes a trie root into serialization"""
return v
def encode_int(v):
"""encodes an integer into serialization"""
if not is_numeric(v) or v < 0 or v >= TT256:
raise Exception("Integer invalid or out of range: %r" % v)
return int_to_big_endian(v)
def encode_int256(v):
return zpad(int_to_big_endian(v), 256)
def scan_bin(v):
if v[:2] in ('0x', b'0x'):
return decode_hex(v[2:])
else:
return decode_hex(v)
def scan_int(v):
if v[:2] in ('0x', b'0x'):
return big_endian_to_int(decode_hex(v[2:]))
else:
return int(v)
def int_to_hex(x):
o = encode_hex(encode_int(x))
return '0x' + (o[1:] if (len(o) > 0 and o[0] == b'0') else o)
def remove_0x_head(s):
def remove_0x_prefix(s):
return s[2:] if s[:2] in (b'0x', '0x') else s
def parse_as_bin(s):
return decode_hex(s[2:] if s[:2] == '0x' else s)
def parse_as_int(s):
return s if is_numeric(s) else int(
'0' + s[2:], 16) if s[:2] == '0x' else int(s)
def dump_state(trie):
res = ''
for k, v in list(trie.to_dict().items()):
res += '%r:%r\n' % (encode_hex(k), encode_hex(v))
return res
class Denoms():
def __init__(self):
@ -385,13 +214,4 @@ class Denoms():
self.ether = 10 ** 18
self.turing = 2 ** 256 - 1
denoms = Denoms()
address = Binary.fixed_length(20, allow_empty=True)
int20 = BigEndianInt(20)
int32 = BigEndianInt(32)
int256 = BigEndianInt(256)
hash32 = Binary.fixed_length(32)
trie_root = Binary.fixed_length(32, allow_empty=True)

View file

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2015 Jnnk, Vitalik Buterin
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.

View file

@ -0,0 +1,14 @@
from . import sedes # noqa: F401
from .codec import ( # noqa: F401
encode,
decode,
infer_sedes,
)
from .exceptions import ( # noqa: F401
RLPException,
EncodingError,
DecodingError,
SerializationError,
DeserializationError,
)
from .sedes import Serializable # noqa: F401

View file

@ -0,0 +1,10 @@
import abc
class Atomic(metaclass=abc.ABCMeta):
"""ABC for objects that can be RLP encoded as is."""
pass
Atomic.register(bytes)
Atomic.register(bytearray)

View file

@ -0,0 +1,277 @@
import collections
from ..pyethereum.utils import big_endian_to_int,int_to_big_endian,is_bytes,ALL_BYTES
from .atomic import Atomic
from .exceptions import EncodingError, DecodingError
from .sedes.binary import Binary as BinaryClass
from .sedes import big_endian_int, binary, boolean, text
from .sedes.lists import List, is_sedes, is_sequence
from .sedes.serializable import Serializable
def encode(obj, sedes=None, infer_serializer=True, cache=True):
"""Encode a Python object in RLP format.
By default, the object is serialized in a suitable way first (using
:func:`rlp.infer_sedes`) and then encoded. Serialization can be explicitly
suppressed by setting `infer_serializer` to ``False`` and not passing an
alternative as `sedes`.
If `obj` has an attribute :attr:`_cached_rlp` (as, notably,
:class:`rlp.Serializable`) and its value is not `None`, this value is
returned bypassing serialization and encoding, unless `sedes` is given (as
the cache is assumed to refer to the standard serialization which can be
replaced by specifying `sedes`).
If `obj` is a :class:`rlp.Serializable` and `cache` is true, the result of
the encoding will be stored in :attr:`_cached_rlp` if it is empty.
:param sedes: an object implementing a function ``serialize(obj)`` which will be used to
serialize ``obj`` before encoding, or ``None`` to use the infered one (if any)
:param infer_serializer: if ``True`` an appropriate serializer will be selected using
:func:`rlp.infer_sedes` to serialize `obj` before encoding
:param cache: cache the return value in `obj._cached_rlp` if possible
(default `True`)
:returns: the RLP encoded item
:raises: :exc:`rlp.EncodingError` in the rather unlikely case that the item is too big to
encode (will not happen)
:raises: :exc:`rlp.SerializationError` if the serialization fails
"""
if isinstance(obj, Serializable):
cached_rlp = obj._cached_rlp
if sedes is None and cached_rlp:
return cached_rlp
else:
really_cache = (
cache and
sedes is None
)
else:
really_cache = False
if sedes:
item = sedes.serialize(obj)
elif infer_serializer:
item = infer_sedes(obj).serialize(obj)
else:
item = obj
result = encode_raw(item)
if really_cache:
obj._cached_rlp = result
return result
def encode_raw(item):
"""RLP encode (a nested sequence of) :class:`Atomic`s."""
if isinstance(item, Atomic):
if len(item) == 1 and item[0] < 128:
return item
payload = item
prefix_offset = 128 # string
elif not isinstance(item, str) and isinstance(item, collections.Sequence):
payload = b''.join(encode_raw(x) for x in item)
prefix_offset = 192 # list
else:
msg = 'Cannot encode object of type {0}'.format(type(item).__name__)
raise EncodingError(msg, item)
try:
prefix = length_prefix(len(payload), prefix_offset)
except ValueError:
raise EncodingError('Item too big to encode', item)
return prefix + payload
LONG_LENGTH = 256**8
def length_prefix(length, offset):
"""Construct the prefix to lists or strings denoting their length.
:param length: the length of the item in bytes
:param offset: ``0x80`` when encoding raw bytes, ``0xc0`` when encoding a
list
"""
if length < 56:
return ALL_BYTES[offset + length]
elif length < LONG_LENGTH:
length_string = int_to_big_endian(length)
return ALL_BYTES[offset + 56 - 1 + len(length_string)] + length_string
else:
raise ValueError('Length greater than 256**8')
SHORT_STRING = 128 + 56
def consume_length_prefix(rlp, start):
"""Read a length prefix from an RLP string.
:param rlp: the rlp byte string to read from
:param start: the position at which to start reading
:returns: a tuple ``(prefix, type, length, end)``, where ``type`` is either ``str``
or ``list`` depending on the type of the following payload,
``length`` is the length of the payload in bytes, and ``end`` is
the position of the first payload byte in the rlp string
"""
b0 = rlp[start]
if b0 < 128: # single byte
return (b'', bytes, 1, start)
elif b0 < SHORT_STRING: # short string
if b0 - 128 == 1 and rlp[start + 1] < 128:
raise DecodingError('Encoded as short string although single byte was possible', rlp)
return (rlp[start:start + 1], bytes, b0 - 128, start + 1)
elif b0 < 192: # long string
ll = b0 - 183 # - (128 + 56 - 1)
if rlp[start + 1:start + 2] == b'\x00':
raise DecodingError('Length starts with zero bytes', rlp)
len_prefix = rlp[start + 1:start + 1 + ll]
l = big_endian_to_int(len_prefix) # noqa: E741
if l < 56:
raise DecodingError('Long string prefix used for short string', rlp)
return (rlp[start:start + 1] + len_prefix, bytes, l, start + 1 + ll)
elif b0 < 192 + 56: # short list
return (rlp[start:start + 1], list, b0 - 192, start + 1)
else: # long list
ll = b0 - 192 - 56 + 1
if rlp[start + 1:start + 2] == b'\x00':
raise DecodingError('Length starts with zero bytes', rlp)
len_prefix = rlp[start + 1:start + 1 + ll]
l = big_endian_to_int(len_prefix) # noqa: E741
if l < 56:
raise DecodingError('Long list prefix used for short list', rlp)
return (rlp[start:start + 1] + len_prefix, list, l, start + 1 + ll)
def consume_payload(rlp, prefix, start, type_, length):
"""Read the payload of an item from an RLP string.
:param rlp: the rlp string to read from
:param type_: the type of the payload (``bytes`` or ``list``)
:param start: the position at which to start reading
:param length: the length of the payload in bytes
:returns: a tuple ``(item, per_item_rlp, end)``, where ``item`` is
the read item, per_item_rlp is a list containing the RLP
encoding of each item and ``end`` is the position of the
first unprocessed byte
"""
if type_ is bytes:
item = rlp[start: start + length]
return (item, [prefix + item], start + length)
elif type_ is list:
items = []
per_item_rlp = []
list_rlp = prefix
next_item_start = start
end = next_item_start + length
while next_item_start < end:
p, t, l, s = consume_length_prefix(rlp, next_item_start)
item, item_rlp, next_item_start = consume_payload(rlp, p, s, t, l)
per_item_rlp.append(item_rlp)
# When the item returned above is a single element, item_rlp will also contain a
# single element, but when it's a list, the first element will be the RLP of the
# whole List, which is what we want here.
list_rlp += item_rlp[0]
items.append(item)
per_item_rlp.insert(0, list_rlp)
if next_item_start > end:
raise DecodingError('List length prefix announced a too small '
'length', rlp)
return (items, per_item_rlp, next_item_start)
else:
raise TypeError('Type must be either list or bytes')
def consume_item(rlp, start):
"""Read an item from an RLP string.
:param rlp: the rlp string to read from
:param start: the position at which to start reading
:returns: a tuple ``(item, per_item_rlp, end)``, where ``item`` is
the read item, per_item_rlp is a list containing the RLP
encoding of each item and ``end`` is the position of the
first unprocessed byte
"""
p, t, l, s = consume_length_prefix(rlp, start)
return consume_payload(rlp, p, s, t, l)
def decode(rlp, sedes=None, strict=True, recursive_cache=False, **kwargs):
"""Decode an RLP encoded object.
If the deserialized result `obj` has an attribute :attr:`_cached_rlp` (e.g. if `sedes` is a
subclass of :class:`rlp.Serializable`) it will be set to `rlp`, which will improve performance
on subsequent :func:`rlp.encode` calls. Bear in mind however that `obj` needs to make sure that
this value is updated whenever one of its fields changes or prevent such changes entirely
(:class:`rlp.sedes.Serializable` does the latter).
:param sedes: an object implementing a function ``deserialize(code)`` which will be applied
after decoding, or ``None`` if no deserialization should be performed
:param \*\*kwargs: additional keyword arguments that will be passed to the deserializer
:param strict: if false inputs that are longer than necessary don't cause an exception
:returns: the decoded and maybe deserialized Python object
:raises: :exc:`rlp.DecodingError` if the input string does not end after the root item and
`strict` is true
:raises: :exc:`rlp.DeserializationError` if the deserialization fails
"""
if not is_bytes(rlp):
raise DecodingError('Can only decode RLP bytes, got type %s' % type(rlp).__name__, rlp)
try:
item, per_item_rlp, end = consume_item(rlp, 0)
except IndexError:
raise DecodingError('RLP string too short', rlp)
if end != len(rlp) and strict:
msg = 'RLP string ends with {} superfluous bytes'.format(len(rlp) - end)
raise DecodingError(msg, rlp)
if sedes:
obj = sedes.deserialize(item, **kwargs)
if is_sequence(obj) or hasattr(obj, '_cached_rlp'):
_apply_rlp_cache(obj, per_item_rlp, recursive_cache)
return obj
else:
return item
def _apply_rlp_cache(obj, split_rlp, recursive):
item_rlp = split_rlp.pop(0)
if isinstance(obj, (int, bool, str, bytes, bytearray)):
return
elif hasattr(obj, '_cached_rlp'):
obj._cached_rlp = item_rlp
if not recursive:
return
for sub in obj:
if isinstance(sub, (int, bool, str, bytes, bytearray)):
split_rlp.pop(0)
else:
sub_rlp = split_rlp.pop(0)
_apply_rlp_cache(sub, sub_rlp, recursive)
def infer_sedes(obj):
"""Try to find a sedes objects suitable for a given Python object.
The sedes objects considered are `obj`'s class, `big_endian_int` and
`binary`. If `obj` is a sequence, a :class:`rlp.sedes.List` will be
constructed recursively.
:param obj: the python object for which to find a sedes object
:raises: :exc:`TypeError` if no appropriate sedes could be found
"""
if is_sedes(obj.__class__):
return obj.__class__
elif not isinstance(obj, bool) and isinstance(obj, int) and obj >= 0:
return big_endian_int
elif BinaryClass.is_valid_type(obj):
return binary
elif not isinstance(obj, str) and isinstance(obj, collections.Sequence):
return List(map(infer_sedes, obj))
elif isinstance(obj, bool):
return boolean
elif isinstance(obj, str):
return text
msg = 'Did not find sedes handling type {}'.format(type(obj).__name__)
raise TypeError(msg)

View file

@ -0,0 +1,144 @@
class RLPException(Exception):
"""Base class for exceptions raised by this package."""
pass
class EncodingError(RLPException):
"""Exception raised if encoding fails.
:ivar obj: the object that could not be encoded
"""
def __init__(self, message, obj):
super(EncodingError, self).__init__(message)
self.obj = obj
class DecodingError(RLPException):
"""Exception raised if decoding fails.
:ivar rlp: the RLP string that could not be decoded
"""
def __init__(self, message, rlp):
super(DecodingError, self).__init__(message)
self.rlp = rlp
class SerializationError(RLPException):
"""Exception raised if serialization fails.
:ivar obj: the object that could not be serialized
"""
def __init__(self, message, obj):
super(SerializationError, self).__init__(message)
self.obj = obj
class ListSerializationError(SerializationError):
"""Exception raised if serialization by a :class:`sedes.List` fails.
:ivar element_exception: the exception that occurred during the serialization of one of the
elements, or `None` if the error is unrelated to a specific element
:ivar index: the index in the list that produced the error or `None` if the error is unrelated
to a specific element
"""
def __init__(self, message=None, obj=None, element_exception=None, index=None):
if message is None:
assert index is not None
assert element_exception is not None
message = ('Serialization failed because of element at index {} '
'("{}")'.format(index, str(element_exception)))
super(ListSerializationError, self).__init__(message, obj)
self.index = index
self.element_exception = element_exception
class ObjectSerializationError(SerializationError):
"""Exception raised if serialization of a :class:`sedes.Serializable` object fails.
:ivar sedes: the :class:`sedes.Serializable` that failed
:ivar list_exception: exception raised by the underlying list sedes, or `None` if no such
exception has been raised
:ivar field: name of the field of the object that produced the error, or `None` if no field
responsible for the error
"""
def __init__(self, message=None, obj=None, sedes=None, list_exception=None):
if message is None:
assert list_exception is not None
if list_exception.element_exception is None:
field = None
message = ('Serialization failed because of underlying list '
'("{}")'.format(str(list_exception)))
else:
assert sedes is not None
field = sedes._meta.field_names[list_exception.index]
message = ('Serialization failed because of field {} '
'("{}")'.format(field, str(list_exception.element_exception)))
else:
field = None
super(ObjectSerializationError, self).__init__(message, obj)
self.field = field
self.list_exception = list_exception
class DeserializationError(RLPException):
"""Exception raised if deserialization fails.
:ivar serial: the decoded RLP string that could not be deserialized
"""
def __init__(self, message, serial):
super(DeserializationError, self).__init__(message)
self.serial = serial
class ListDeserializationError(DeserializationError):
"""Exception raised if deserialization by a :class:`sedes.List` fails.
:ivar element_exception: the exception that occurred during the deserialization of one of the
elements, or `None` if the error is unrelated to a specific element
:ivar index: the index in the list that produced the error or `None` if the error is unrelated
to a specific element
"""
def __init__(self, message=None, serial=None, element_exception=None, index=None):
if not message:
assert index is not None
assert element_exception is not None
message = ('Deserialization failed because of element at index {} '
'("{}")'.format(index, str(element_exception)))
super(ListDeserializationError, self).__init__(message, serial)
self.index = index
self.element_exception = element_exception
class ObjectDeserializationError(DeserializationError):
"""Exception raised if deserialization by a :class:`sedes.Serializable` fails.
:ivar sedes: the :class:`sedes.Serializable` that failed
:ivar list_exception: exception raised by the underlying list sedes, or `None` if no such
exception has been raised
:ivar field: name of the field of the object that produced the error, or `None` if no field
responsible for the error
"""
def __init__(self, message=None, serial=None, sedes=None, list_exception=None):
if not message:
assert list_exception is not None
if list_exception.element_exception is None:
field = None
message = ('Deserialization failed because of underlying list '
'("{}")'.format(str(list_exception)))
else:
assert sedes is not None
field = sedes._meta.field_names[list_exception.index]
message = ('Deserialization failed because of field {} '
'("{}")'.format(field, str(list_exception.element_exception)))
super(ObjectDeserializationError, self).__init__(message, serial)
self.sedes = sedes
self.list_exception = list_exception
self.field = field

View file

@ -0,0 +1,7 @@
from . import raw # noqa: F401
from .binary import Binary, binary # noqa: F401
from .boolean import Boolean, boolean # noqa: F401
from .big_endian_int import BigEndianInt, big_endian_int # noqa: F401
from .lists import List # noqa: F401
from .text import Text, text # noqa: #401
from .serializable import Serializable # noqa: F401

View file

@ -0,0 +1,46 @@
from ...pyethereum.utils import int_to_big_endian,big_endian_to_int
from ..exceptions import DeserializationError,SerializationError
class BigEndianInt(object):
"""A sedes for big endian integers.
:param l: the size of the serialized representation in bytes or `None` to
use the shortest possible one
"""
def __init__(self, l=None):
self.l = l
def serialize(self, obj):
if isinstance(obj, bool) or not isinstance(obj, int):
raise SerializationError('Can only serialize integers', obj)
if self.l is not None and obj >= 256**self.l:
raise SerializationError('Integer too large (does not fit in {} '
'bytes)'.format(self.l), obj)
if obj < 0:
raise SerializationError('Cannot serialize negative integers', obj)
if obj == 0:
s = b''
else:
s = int_to_big_endian(obj)
if self.l is not None:
return b'\x00' * max(0, self.l - len(s)) + s
else:
return s
def deserialize(self, serial):
if self.l is not None and len(serial) != self.l:
raise DeserializationError('Invalid serialization (wrong size)',
serial)
if self.l is None and len(serial) > 0 and serial[0:1] == b'\x00':
raise DeserializationError('Invalid serialization (not minimal '
'length)', serial)
serial = serial or b'\x00'
return big_endian_to_int(serial)
big_endian_int = BigEndianInt()

View file

@ -0,0 +1,55 @@
from ..exceptions import SerializationError,DeserializationError
from ..atomic import Atomic
class Binary(object):
"""A sedes object for binary data of certain length.
:param min_length: the minimal length in bytes or `None` for no lower limit
:param max_length: the maximal length in bytes or `None` for no upper limit
:param allow_empty: if true, empty strings are considered valid even if
a minimum length is required otherwise
"""
def __init__(self, min_length=None, max_length=None, allow_empty=False):
self.min_length = min_length or 0
if max_length is None:
self.max_length = float('inf')
else:
self.max_length = max_length
self.allow_empty = allow_empty
@classmethod
def fixed_length(cls, l, allow_empty=False):
"""Create a sedes for binary data with exactly `l` bytes."""
return cls(l, l, allow_empty=allow_empty)
@classmethod
def is_valid_type(cls, obj):
return isinstance(obj, (bytes, bytearray))
def is_valid_length(self, l):
return any((self.min_length <= l <= self.max_length,
self.allow_empty and l == 0))
def serialize(self, obj):
if not Binary.is_valid_type(obj):
raise SerializationError('Object is not a serializable ({})'.format(type(obj)), obj)
if not self.is_valid_length(len(obj)):
raise SerializationError('Object has invalid length', obj)
return obj
def deserialize(self, serial):
if not isinstance(serial, Atomic):
m = 'Objects of type {} cannot be deserialized'
raise DeserializationError(m.format(type(serial).__name__), serial)
if self.is_valid_length(len(serial)):
return serial
else:
raise DeserializationError('{} has invalid length'.format(type(serial)), serial)
binary = Binary()

View file

@ -0,0 +1,30 @@
from ..exceptions import DeserializationError,SerializationError
class Boolean:
"""A sedes for booleans
"""
def serialize(self, obj):
if not isinstance(obj, bool):
raise SerializationError('Can only serialize integers', obj)
if obj is False:
return b''
elif obj is True:
return b'\x01'
else:
raise Exception("Invariant: no other options for boolean values")
def deserialize(self, serial):
if serial == b'':
return False
elif serial == b'\x01':
return True
else:
raise DeserializationError(
'Invalid serialized boolean. Must be either 0x01 or 0x00',
serial
)
boolean = Boolean()

View file

@ -0,0 +1,92 @@
"""
Module for sedes objects that use lists as serialization format.
"""
from collections import Sequence
from ...pyethereum.utils import to_list,to_tuple
from ..exceptions import (
SerializationError,
ListSerializationError,
DeserializationError,
ListDeserializationError,
)
from .binary import (
Binary as BinaryClass,
)
def is_sedes(obj):
"""Check if `obj` is a sedes object.
A sedes object is characterized by having the methods `serialize(obj)` and
`deserialize(serial)`.
"""
return hasattr(obj, 'serialize') and hasattr(obj, 'deserialize')
def is_sequence(obj):
"""Check if `obj` is a sequence, but not a string or bytes."""
return isinstance(obj, Sequence) and not (
isinstance(obj, str) or BinaryClass.is_valid_type(obj))
class List(list):
"""A sedes for lists, implemented as a list of other sedes objects.
:param strict: If true (de)serializing lists that have a length not
matching the sedes length will result in an error. If false
(de)serialization will stop as soon as either one of the
lists runs out of elements.
"""
def __init__(self, elements=None, strict=True):
super(List, self).__init__()
self.strict = strict
if elements:
for e in elements:
if is_sedes(e):
self.append(e)
elif isinstance(e, Sequence):
self.append(List(e))
else:
raise TypeError(
'Instances of List must only contain sedes objects or '
'nested sequences thereof.'
)
@to_list
def serialize(self, obj):
if not is_sequence(obj):
raise ListSerializationError('Can only serialize sequences', obj)
if self.strict and len(self) != len(obj):
raise ListSerializationError(
'Serializing list length (%d) does not match sedes (%d)' % (
len(obj), len(self)),
obj)
for index, (element, sedes) in enumerate(zip(obj, self)):
try:
yield sedes.serialize(element)
except SerializationError as e:
raise ListSerializationError(obj=obj, element_exception=e, index=index)
@to_tuple
def deserialize(self, serial):
if not is_sequence(serial):
raise ListDeserializationError('Can only deserialize sequences', serial)
if self.strict and len(serial) != len(self):
raise ListDeserializationError(
'Deserializing list length (%d) does not match sedes (%d)' % (
len(serial), len(self)),
serial)
for idx, (sedes, element) in enumerate(zip(self, serial)):
try:
yield sedes.deserialize(element)
except DeserializationError as e:
raise ListDeserializationError(serial=serial, element_exception=e, index=idx)

View file

@ -0,0 +1,29 @@
"""
A sedes that does nothing. Thus, everything that can be directly encoded by RLP
is serializable. This sedes can be used as a placeholder when deserializing
larger structures.
"""
from collections import Sequence
from ..exceptions import SerializationError
from ..atomic import Atomic
def serializable(obj):
if isinstance(obj, Atomic):
return True
elif not isinstance(obj, str) and isinstance(obj, Sequence):
return all(map(serializable, obj))
else:
return False
def serialize(obj):
if not serializable(obj):
raise SerializationError('Can only serialize nested lists of strings',
obj)
return obj
def deserialize(serial):
return serial

View file

@ -0,0 +1,484 @@
import abc
import collections
import copy
import enum
import re
from ...pyethereum.utils import to_dict,to_set,to_tuple
from ..exceptions import (
ListSerializationError,
ObjectSerializationError,
ListDeserializationError,
ObjectDeserializationError,
)
from .lists import (
List,
)
class MetaBase:
fields = None
field_names = None
field_attrs = None
sedes = None
def _get_duplicates(values):
counts = collections.Counter(values)
return tuple(
item
for item, num in counts.items()
if num > 1
)
def validate_args_and_kwargs(args, kwargs, arg_names, allow_missing=False):
duplicate_arg_names = _get_duplicates(arg_names)
if duplicate_arg_names:
raise TypeError("Duplicate argument names: {0}".format(sorted(duplicate_arg_names)))
needed_kwargs = arg_names[len(args):]
used_kwargs = set(arg_names[:len(args)])
duplicate_kwargs = used_kwargs.intersection(kwargs.keys())
if duplicate_kwargs:
raise TypeError("Duplicate kwargs: {0}".format(sorted(duplicate_kwargs)))
unknown_kwargs = set(kwargs.keys()).difference(arg_names)
if unknown_kwargs:
raise TypeError("Unknown kwargs: {0}".format(sorted(unknown_kwargs)))
missing_kwargs = set(needed_kwargs).difference(kwargs.keys())
if not allow_missing and missing_kwargs:
raise TypeError("Missing kwargs: {0}".format(sorted(missing_kwargs)))
@to_tuple
def merge_kwargs_to_args(args, kwargs, arg_names, allow_missing=False):
validate_args_and_kwargs(args, kwargs, arg_names, allow_missing=allow_missing)
needed_kwargs = arg_names[len(args):]
yield from args
for arg_name in needed_kwargs:
yield kwargs[arg_name]
@to_dict
def merge_args_to_kwargs(args, kwargs, arg_names, allow_missing=False):
validate_args_and_kwargs(args, kwargs, arg_names, allow_missing=allow_missing)
yield from kwargs.items()
for value, name in zip(args, arg_names):
yield name, value
def _eq(left, right):
"""
Equality comparison that allows for equality between tuple and list types
with equivalent elements.
"""
if isinstance(left, (tuple, list)) and isinstance(right, (tuple, list)):
return len(left) == len(right) and all(_eq(*pair) for pair in zip(left, right))
else:
return left == right
class ChangesetState(enum.Enum):
INITIALIZED = 'INITIALIZED'
OPEN = 'OPEN'
CLOSED = 'CLOSED'
class ChangesetField:
field = None
def __init__(self, field):
self.field = field
def __get__(self, instance, type=None):
if instance is None:
return self
elif instance.__state__ is not ChangesetState.OPEN:
raise AttributeError("Changeset is not active. Attribute access not allowed")
else:
try:
return instance.__diff__[self.field]
except KeyError:
return getattr(instance.__original__, self.field)
def __set__(self, instance, value):
if instance.__state__ is not ChangesetState.OPEN:
raise AttributeError("Changeset is not active. Attribute access not allowed")
instance.__diff__[self.field] = value
class BaseChangeset:
# reference to the original Serializable instance.
__original__ = None
# the state of this fieldset. Initialized -> Open -> Closed
__state__ = None
# the field changes that have been made in this change
__diff__ = None
def __init__(self, obj, changes=None):
self.__original__ = obj
self.__state__ = ChangesetState.INITIALIZED
self.__diff__ = changes or {}
def commit(self):
obj = self.build_rlp()
self.close()
return obj
def build_rlp(self):
if self.__state__ == ChangesetState.OPEN:
field_kwargs = {
name: self.__diff__.get(name, self.__original__[name])
for name
in self.__original__._meta.field_names
}
return type(self.__original__)(**field_kwargs)
else:
raise ValueError("Cannot open Changeset which is not in the OPEN state")
def open(self):
if self.__state__ == ChangesetState.INITIALIZED:
self.__state__ = ChangesetState.OPEN
else:
raise ValueError("Cannot open Changeset which is not in the INITIALIZED state")
def close(self):
if self.__state__ == ChangesetState.OPEN:
self.__state__ = ChangesetState.CLOSED
else:
raise ValueError("Cannot open Changeset which is not in the INITIALIZED state")
def __enter__(self):
if self.__state__ == ChangesetState.INITIALIZED:
self.open()
return self
else:
raise ValueError("Cannot open Changeset which is not in the INITIALIZED state")
def __exit__(self, exc_type, exc_value, traceback):
if self.__state__ == ChangesetState.OPEN:
self.close()
def Changeset(obj, changes):
namespace = {
name: ChangesetField(name)
for name
in obj._meta.field_names
}
cls = type(
"{0}Changeset".format(obj.__class__.__name__),
(BaseChangeset,),
namespace,
)
return cls(obj, changes)
class BaseSerializable(collections.Sequence):
def __init__(self, *args, **kwargs):
if kwargs:
field_values = merge_kwargs_to_args(args, kwargs, self._meta.field_names)
else:
field_values = args
if len(field_values) != len(self._meta.field_names):
raise TypeError(
'Argument count mismatch. expected {0} - got {1} - missing {2}'.format(
len(self._meta.field_names),
len(field_values),
','.join(self._meta.field_names[len(field_values):]),
)
)
for value, attr in zip(field_values, self._meta.field_attrs):
setattr(self, attr, make_immutable(value))
_cached_rlp = None
def as_dict(self):
return dict(
(field, value)
for field, value
in zip(self._meta.field_names, self)
)
def __iter__(self):
for attr in self._meta.field_attrs:
yield getattr(self, attr)
def __getitem__(self, idx):
if isinstance(idx, int):
attr = self._meta.field_attrs[idx]
return getattr(self, attr)
elif isinstance(idx, slice):
field_slice = self._meta.field_attrs[idx]
return tuple(getattr(self, field) for field in field_slice)
elif isinstance(idx, str):
return getattr(self, idx)
else:
raise IndexError("Unsupported type for __getitem__: {0}".format(type(idx)))
def __len__(self):
return len(self._meta.fields)
def __eq__(self, other):
return isinstance(other, Serializable) and hash(self) == hash(other)
def __getstate__(self):
state = self.__dict__.copy()
# The hash() builtin is not stable across processes
# (https://docs.python.org/3/reference/datamodel.html#object.__hash__), so we do this here
# to ensure pickled instances don't carry the cached hash() as that may cause issues like
# https://github.com/ethereum/py-evm/issues/1318
state['_hash_cache'] = None
return state
_hash_cache = None
def __hash__(self):
if self._hash_cache is None:
self._hash_cache = hash(tuple(self))
return self._hash_cache
@classmethod
def serialize(cls, obj):
try:
return cls._meta.sedes.serialize(obj)
except ListSerializationError as e:
raise ObjectSerializationError(obj=obj, sedes=cls, list_exception=e)
@classmethod
def deserialize(cls, serial, **extra_kwargs):
try:
values = cls._meta.sedes.deserialize(serial)
except ListDeserializationError as e:
raise ObjectDeserializationError(serial=serial, sedes=cls, list_exception=e)
args_as_kwargs = merge_args_to_kwargs(values, {}, cls._meta.field_names)
return cls(**args_as_kwargs, **extra_kwargs)
def copy(self, *args, **kwargs):
missing_overrides = set(
self._meta.field_names
).difference(
kwargs.keys()
).difference(
self._meta.field_names[:len(args)]
)
unchanged_kwargs = {
key: copy.deepcopy(value)
for key, value
in self.as_dict().items()
if key in missing_overrides
}
combined_kwargs = dict(**unchanged_kwargs, **kwargs)
all_kwargs = merge_args_to_kwargs(args, combined_kwargs, self._meta.field_names)
return type(self)(**all_kwargs)
def __copy__(self):
return self.copy()
def __deepcopy__(self, *args):
return self.copy()
_in_mutable_context = False
def build_changeset(self, *args, **kwargs):
args_as_kwargs = merge_args_to_kwargs(
args,
kwargs,
self._meta.field_names,
allow_missing=True,
)
return Changeset(self, changes=args_as_kwargs)
def make_immutable(value):
if isinstance(value, list):
return tuple(make_immutable(item) for item in value)
else:
return value
@to_tuple
def _mk_field_attrs(field_names, extra_namespace):
namespace = set(field_names).union(extra_namespace)
for field in field_names:
while True:
field = '_' + field
if field not in namespace:
namespace.add(field)
yield field
break
def _mk_field_property(field, attr):
def field_fn_getter(self):
return getattr(self, attr)
def field_fn_setter(self, value):
if not self._in_mutable_context:
raise AttributeError("can't set attribute")
setattr(self, attr, value)
return property(field_fn_getter, field_fn_setter)
IDENTIFIER_REGEX = re.compile(r"^[^\d\W]\w*\Z", re.UNICODE)
def _is_valid_identifier(value):
# Source: https://stackoverflow.com/questions/5474008/regular-expression-to-confirm-whether-a-string-is-a-valid-identifier-in-python # noqa: E501
if not isinstance(value, str):
return False
return bool(IDENTIFIER_REGEX.match(value))
@to_set
def _get_class_namespace(cls):
if hasattr(cls, '__dict__'):
yield from cls.__dict__.keys()
if hasattr(cls, '__slots__'):
yield from cls.__slots__
class SerializableBase(abc.ABCMeta):
def __new__(cls, name, bases, attrs):
super_new = super(SerializableBase, cls).__new__
serializable_bases = tuple(b for b in bases if isinstance(b, SerializableBase))
has_multiple_serializable_parents = len(serializable_bases) > 1
is_serializable_subclass = any(serializable_bases)
declares_fields = 'fields' in attrs
if not is_serializable_subclass:
# If this is the original creation of the `Serializable` class,
# just create the class.
return super_new(cls, name, bases, attrs)
elif not declares_fields:
if has_multiple_serializable_parents:
raise TypeError(
"Cannot create subclass from multiple parent `Serializable` "
"classes without explicit `fields` declaration."
)
else:
# This is just a vanilla subclass of a `Serializable` parent class.
parent_serializable = serializable_bases[0]
if hasattr(parent_serializable, '_meta'):
fields = parent_serializable._meta.fields
else:
# This is a subclass of `Serializable` which has no
# `fields`, likely intended for further subclassing.
fields = ()
else:
# ensure that the `fields` property is a tuple of tuples to ensure
# immutability.
fields = tuple(tuple(field) for field in attrs.pop('fields'))
# split the fields into names and sedes
if fields:
field_names, sedes = zip(*fields)
else:
field_names, sedes = (), ()
# check that field names are unique
duplicate_field_names = _get_duplicates(field_names)
if duplicate_field_names:
raise TypeError(
"The following fields are duplicated in the `fields` "
"declaration: "
"{0}".format(",".join(sorted(duplicate_field_names)))
)
# check that field names are valid identifiers
invalid_field_names = {
field_name
for field_name
in field_names
if not _is_valid_identifier(field_name)
}
if invalid_field_names:
raise TypeError(
"The following field names are not valid python identifiers: {0}".format(
",".join("`{0}`".format(item) for item in sorted(invalid_field_names))
)
)
# extract all of the fields from parent `Serializable` classes.
parent_field_names = {
field_name
for base in serializable_bases if hasattr(base, '_meta')
for field_name in base._meta.field_names
}
# check that all fields from parent serializable classes are
# represented on this class.
missing_fields = parent_field_names.difference(field_names)
if missing_fields:
raise TypeError(
"Subclasses of `Serializable` **must** contain a full superset "
"of the fields defined in their parent classes. The following "
"fields are missing: "
"{0}".format(",".join(sorted(missing_fields)))
)
# the actual field values are stored in separate *private* attributes.
# This computes attribute names that don't conflict with other
# attributes already present on the class.
reserved_namespace = set(attrs.keys()).union(
attr
for base in bases
for parent_cls in base.__mro__
for attr in _get_class_namespace(parent_cls)
)
field_attrs = _mk_field_attrs(field_names, reserved_namespace)
# construct the Meta object to store field information for the class
meta_namespace = {
'fields': fields,
'field_attrs': field_attrs,
'field_names': field_names,
'sedes': List(sedes),
}
meta_base = attrs.pop('_meta', MetaBase)
meta = type(
'Meta',
(meta_base,),
meta_namespace,
)
attrs['_meta'] = meta
# construct `property` attributes for read only access to the fields.
field_props = tuple(
(field, _mk_field_property(field, attr))
for field, attr
in zip(meta.field_names, meta.field_attrs)
)
return super_new(
cls,
name,
bases,
dict(
field_props +
tuple(attrs.items())
),
)
class Serializable(BaseSerializable, metaclass=SerializableBase):
"""
The base class for serializable objects.
"""
pass

View file

@ -0,0 +1,64 @@
from ..exceptions import SerializationError,DeserializationError
from ..atomic import Atomic
class Text:
"""A sedes object for encoded text data of certain length.
:param min_length: the minimal length in encoded characters or `None` for no lower limit
:param max_length: the maximal length in encoded characters or `None` for no upper limit
:param allow_empty: if true, empty strings are considered valid even if
a minimum length is required otherwise
"""
def __init__(self, min_length=None, max_length=None, allow_empty=False, encoding='utf8'):
self.min_length = min_length or 0
if max_length is None:
self.max_length = float('inf')
else:
self.max_length = max_length
self.allow_empty = allow_empty
self.encoding = encoding
@classmethod
def fixed_length(cls, l, allow_empty=False):
"""Create a sedes for text data with exactly `l` encoded characters."""
return cls(l, l, allow_empty=allow_empty)
@classmethod
def is_valid_type(cls, obj):
return isinstance(obj, str)
def is_valid_length(self, l):
return any((
self.min_length <= l <= self.max_length,
self.allow_empty and l == 0
))
def serialize(self, obj):
if not self.is_valid_type(obj):
raise SerializationError('Object is not a serializable ({})'.format(type(obj)), obj)
if not self.is_valid_length(len(obj)):
raise SerializationError('Object has invalid length', obj)
return obj.encode(self.encoding)
def deserialize(self, serial):
if not isinstance(serial, Atomic):
m = 'Objects of type {} cannot be deserialized'
raise DeserializationError(m.format(type(serial).__name__), serial)
try:
text_value = serial.decode(self.encoding)
except UnicodeDecodeError as err:
raise DeserializationError(str(err), serial)
if self.is_valid_length(len(text_value)):
return text_value
else:
raise DeserializationError('{} has invalid length'.format(type(serial)), serial)
text = Text()

View file

@ -25,7 +25,7 @@ from mmgen.common import *
from mmgen.obj import ETHAmt,TwMMGenID,TwComment,TwLabel
from mmgen.tw import TrackingWallet,TwAddrList,TwUnspentOutputs
from mmgen.addr import AddrData
from mmgen.altcoins.eth.contract import Token
from .contract import Token
class EthereumTrackingWallet(TrackingWallet):

View file

@ -92,9 +92,9 @@ class EthereumMMGenTX(MMGenTX):
if self.check_sigs():
try: from ethereum.transactions import Transaction
except: from mmgen.altcoins.eth.pyethereum.transactions import Transaction
except: from .pyethereum.transactions import Transaction
import rlp
from . import rlp
etx = rlp.decode(bytes.fromhex(self.hex),Transaction)
d = etx.to_dict() # ==> hex values have '0x' prefix, 0 is '0x'
for k in ('sender','to','data'):
@ -288,12 +288,12 @@ class EthereumMMGenTX(MMGenTX):
'data': bytes.fromhex(d['data'])}
try: from ethereum.transactions import Transaction
except: from mmgen.altcoins.eth.pyethereum.transactions import Transaction
except: from .pyethereum.transactions import Transaction
etx = Transaction(**d_in).sign(wif,d['chainId'])
assert etx.sender.hex() == d['from'],(
'Sender address recovered from signature does not match true sender')
import rlp
from . import rlp
self.hex = rlp.encode(etx).hex()
self.coin_txid = CoinTxID(etx.hash.hex())
if d['data']:
@ -410,7 +410,7 @@ class EthereumTokenMMGenTX(EthereumMMGenTX):
if self.outputs[0].is_chg:
send_acct_tbal = '0'
else:
from mmgen.altcoins.eth.contract import Token
from .contract import Token
send_acct_tbal = Token(g.token).balance(self.inputs[0].addr) - self.outputs[0].amt
return m.format(ETHAmt(change_amt).hl(),g.coin,ETHAmt(send_acct_tbal).hl(),g.dcoin)
@ -431,7 +431,7 @@ class EthereumTokenMMGenTX(EthereumMMGenTX):
def make_txobj(self):
super(EthereumTokenMMGenTX,self).make_txobj()
from mmgen.altcoins.eth.contract import Token
from .contract import Token
t = Token(g.token)
o = t.txcreate( self.inputs[0].addr,
self.outputs[0].addr,
@ -444,7 +444,7 @@ class EthereumTokenMMGenTX(EthereumMMGenTX):
def check_txfile_hex_data(self):
d = super(EthereumTokenMMGenTX,self).check_txfile_hex_data()
o = self.txobj
from mmgen.altcoins.eth.contract import Token
from .contract import Token
if self.check_sigs(): # online, from rlp
rpc_init()
o['token_addr'] = TokenAddr(o['to'])
@ -462,7 +462,7 @@ class EthereumTokenMMGenTX(EthereumMMGenTX):
r=super(EthereumTokenMMGenTX,self).format_view_body(*args,**kwargs))
def do_sign(self,d,wif,tx_num_str):
from mmgen.altcoins.eth.contract import Token
from .contract import Token
d = self.txobj
t = Token(d['token_addr'],decimals=d['decimals'])
tx_in = t.txcreate(d['from'],d['to'],d['amt'],self.start_gas,d['gasPrice'],nonce=d['nonce'])

View file

@ -205,13 +205,13 @@ t_alts=(
"$gentest_py --coin=ltc 2:ext $rounds"
"$gentest_py --coin=ltc --type=compressed 2:ext $rounds"
# "$gentest_py --coin=ltc --type=segwit 2:ext $rounds" # pycoin generates old-style LTC Segwit addrs
"$gentest_py --coin=etc 2:ext $rounds"
"$gentest_py --coin=eth 2:ext $rounds"
# "$gentest_py --coin=etc 2:ext $rounds" # no pythereum
# "$gentest_py --coin=eth 2:ext $rounds"
"$gentest_py --coin=zec 2:ext $rounds"
"$gentest_py --coin=zec --type=zcash_z 2:ext $rounds_mid"
"$gentest_py --all 2:pycoin $rounds"
"$gentest_py --all 2:pyethereum $rounds"
# "$gentest_py --all 2:pyethereum $rounds"
"$gentest_py --all 2:keyconv $rounds_mid"
"$gentest_py --all 2:zcash_mini $rounds_mid")
if [ "$MINGW" ]; then

View file

@ -152,10 +152,26 @@ setup(
'mmgen.altcoins.eth.obj',
'mmgen.altcoins.eth.tx',
'mmgen.altcoins.eth.tw',
'mmgen/altcoins/eth/pyethereum/LICENSE',
'mmgen.altcoins.eth.pyethereum.__init__',
'mmgen.altcoins.eth.pyethereum.transactions',
'mmgen.altcoins.eth.pyethereum.utils',
'mmgen/altcoins/eth/rlp/LICENSE',
'mmgen/altcoins/eth/rlp/__init__',
'mmgen/altcoins/eth/rlp/atomic',
'mmgen/altcoins/eth/rlp/codec',
'mmgen/altcoins/eth/rlp/exceptions',
'mmgen/altcoins/eth/rlp/sedes/__init__',
'mmgen/altcoins/eth/rlp/sedes/big_endian_int',
'mmgen/altcoins/eth/rlp/sedes/binary',
'mmgen/altcoins/eth/rlp/sedes/boolean',
'mmgen/altcoins/eth/rlp/sedes/lists',
'mmgen/altcoins/eth/rlp/sedes/raw',
'mmgen/altcoins/eth/rlp/sedes/serializable',
'mmgen/altcoins/eth/rlp/sedes/text',
'mmgen.main',
'mmgen.main_addrgen',
'mmgen.main_addrimport',

View file

@ -373,7 +373,7 @@ class TestSuiteEthdev(TestSuiteBase,TestSuiteShared):
args = ['98831F3A:E:1,123.456']
return self.txcreate(args=args,menu=menu,acct='1',non_mmgen_inputs=1)
def txsign1(self): return self.txsign()
def txsign1(self): return self.txsign(add_args=['--use-internal-keccak-module'])
def txsign1_ni(self): return self.txsign(ni=True)
def txsend1(self): return self.txsend()
def bal1(self): return self.bal(n='1')