chainlib-eth

Ethereum implementation of the chainlib interface
Info | Log | Files | Refs | README | LICENSE

contract.py (17638B)


      1 # standard imports
      2 import enum
      3 import re
      4 import logging
      5 
      6 # external imports
      7 from hexathon import (
      8         strip_0x,
      9         add_0x,
     10         pad,
     11         same as same_hex,
     12         )
     13 
     14 # local imports
     15 from chainlib.hash import keccak256_string_to_hex
     16 from chainlib.block import BlockSpec
     17 from chainlib.jsonrpc import JSONRPCRequest
     18 from .address import to_checksum_address
     19 
     20 logg = logging.getLogger(__name__)
     21 
     22 
     23 re_method = r'^[a-zA-Z0-9_]+$'
     24 
     25 class ABIContractType(enum.Enum):
     26     """Data types used by ABI encoders
     27     """
     28     BYTES32 = 'bytes32'
     29     BYTES4 = 'bytes4'
     30     UINT256 = 'uint256'
     31     UINT128 = 'uint128'
     32     UINT64 = 'uint64'
     33     UINT32 = 'uint32'
     34     UINT16 = 'uint16'
     35     UINT8 = 'uint8'
     36     ADDRESS = 'address'
     37     STRING = 'string'
     38     BYTES = 'bytes'
     39     BOOLEAN = 'bool'
     40     TUPLE = 'tuple'
     41 
     42 dynamic_contract_types = [
     43     ABIContractType.STRING,
     44     ABIContractType.BYTES,
     45     ]
     46 
     47 pointer_contract_types = [
     48     ABIContractType.TUPLE,
     49         ] + dynamic_contract_types
     50 
     51 
     52 class ABIContract:
     53     """Base class for Ethereum smart contract encoder
     54     """
     55     def __init__(self):
     56         self.types = []
     57         self.contents = []
     58         self.dirty = False
     59 
     60 
     61     def add_type(self, v):
     62         self.types.append(v)
     63         self.dirty = True
     64 
     65 
     66 class ABIMethodEncoder(ABIContract):
     67     """Generate ABI method signatures from method signature string.
     68     """
     69     def __init__(self):
     70         super(ABIMethodEncoder, self).__init__()
     71         self.method_name = None
     72         self.method_contents = []
     73 
     74 
     75     def method(self, m):
     76         """Set method name.
     77 
     78         :param m: Method name
     79         :type m: str
     80         :raises ValueError: Invalid method name
     81         """
     82         if re.match(re_method, m) == None:
     83             raise ValueError('Invalid method {}, must match regular expression {}'.format(re_method))
     84         self.method_name = m
     85         self.__log_method()
     86 
     87 
     88     def get_method(self):
     89         """Return currently set method signature string.
     90 
     91         :rtype: str
     92         :returns: Method signature
     93         """
     94         contents = '(' + ','.join(self.method_contents) + ')'
     95         if self.method_name == None:
     96             return contents
     97         return self.method_name + contents
     98 
     99 
    100     def typ(self, v):
    101         """Add argument type to argument vector.
    102 
    103         Method name must be set before this is called.
    104 
    105         :param v: Type to add
    106         :type v: chainlib.eth.contract.ABIContractType
    107         :raises AttributeError: Type set before method name
    108         :raises TypeError: Invalid type
    109         """
    110         if isinstance(v, ABIContractEncoder):
    111             return self.typ_tuple(v)
    112         if not isinstance(v, ABIContractType):
    113             raise TypeError('method type not valid; expected {}, got {}'.format(type(ABIContractType).__name__, type(v).__name__))
    114         self.method_contents.append(v.value)
    115         self.__log_method()
    116 
    117 
    118     def typ_literal(self, v):
    119         if type(v).__name__ != 'str':
    120             raise ValueError('literal type must be string')
    121         self.method_contents.append(v)
    122         self.__log_method()
    123 
    124 
    125     def typ_tuple(self, v):
    126         if not isinstance(v, ABIContractEncoder):
    127             raise TypeError('tuple type not valid; expected {}, got {}'.format(type(ABIContractEncoder).__name__, type(v).__name__))
    128         r = v.get_method()
    129         self.method_contents.append(r)
    130         self.__log_method()
    131 
    132 
    133     def __log_method(self):
    134         logg.debug('method set to {}'.format(self.get_method()))
    135 
    136 
    137     def get_signature(self):
    138         """Generate topic signature from set topic.
    139 
    140         :rtype: str
    141         :returns: Topic signature, in hex
    142         """
    143         if self.method_name == None:
    144             return ''
    145         s = self.get_method()
    146         return keccak256_string_to_hex(s)
    147 
    148 
    149     def get_method_signature(self):
    150         s = self.get_signature()
    151         if s == '':
    152             return s
    153         return s[:8]
    154 
    155 
    156 
    157 class ABIContractDecoder(ABIContract):
    158     """Decode serialized ABI contract input data to corresponding python primitives.
    159     """
    160     
    161     def typ(self, v):
    162         """Add type to argument array to parse input against.
    163 
    164         :param v: Type
    165         :type v: chainlib.eth.contract.ABIContractType
    166         :raises TypeError: Invalid type
    167         """
    168         if not isinstance(v, ABIContractType):
    169             raise TypeError('method type not valid; expected {}, got {}'.format(type(ABIContractType).__name__, type(v).__name__))
    170         if v == ABIContractType.TUPLE:
    171             raise NotImplementedError('sorry, tuple decoding not yet implemented')
    172         self.add_type(v.value)
    173         self.__log_typ()
    174 
    175 
    176     def val(self, v):
    177         """Add value to value array.
    178 
    179         :param v: Value, in hex
    180         :type v: str
    181         """
    182         self.contents.append(v)
    183         logg.debug('content is now {}'.format(self.contents))
    184 
    185 
    186     def uint256(self, v):
    187         """Parse value as uint256.
    188 
    189         :param v: Value, in hex
    190         :type v: str
    191         :rtype: int
    192         :returns: Int value
    193         """
    194         return int(v, 16)
    195 
    196 
    197     def uintn(self, v, bitsize):
    198         # all uints no matter what size are returned to 256 bit boundary
    199         return self.uint256(v)
    200 
    201         l = len(v) * 8 * 2
    202         if bitsize % 8 > 0:
    203             raise ValueError('must be 8 multiple')
    204         elif bitsize > 256:
    205             raise ValueError('max 256 bits')
    206         elif l < bitsize:
    207             raise ValueError('input value length {} shorter than bitsize {}'.format(l, bitsize))
    208         return int(v[:int(bitsize/8)], 16)
    209 
    210 
    211     def bytes32(self, v):
    212         """Parse value as bytes32.
    213 
    214         :param v: Value, in hex
    215         :type v: str
    216         :rtype: str
    217         :returns: Value, in hex
    218         """
    219         return v
    220 
    221 
    222     def bool(self, v):
    223         """Parse value as bool.
    224 
    225         :param v: Value, in hex
    226         :type v: str
    227         :rtype: bool
    228         :returns: Value
    229         """
    230         return bool(self.uint256(v))
    231 
    232 
    233     def boolean(self, v):
    234         """Alias of chainlib.eth.contract.ABIContractDecoder.bool
    235         """
    236         return bool(self.uint256(v))
    237 
    238 
    239     def address(self, v):
    240         """Parse value as address.
    241 
    242         :param v: Value, in hex
    243         :type v: str
    244         :rtype: str
    245         :returns: Value. in hex
    246         """
    247         a = strip_0x(v)[64-40:]
    248         return to_checksum_address(a)
    249 
    250 
    251     def string(self, v):
    252         """Parse value as string.
    253 
    254         :param v: Value, in hex
    255         :type v: str
    256         :rtype: str
    257         :returns: Value
    258         """
    259         s = strip_0x(v)
    260         b = bytes.fromhex(s)
    261         cursor = 0
    262         offset = int.from_bytes(b[cursor:cursor+32], 'big')
    263         cursor += 32
    264         length = int.from_bytes(b[cursor:cursor+32], 'big')
    265         cursor += 32
    266         content = b[cursor:cursor+length]
    267         logg.debug('parsing string offset {} length {} content {}'.format(offset, length, content))
    268         return content.decode('utf-8')
    269 
    270 
    271     def __log_typ(self):
    272         logg.debug('types set to ({})'.format(','.join(self.types)))
    273 
    274 
    275     def decode(self):
    276         """Apply decoder on value array using argument type array.
    277 
    278         :rtype: list
    279         :returns: List of decoded values
    280         """
    281         r = []
    282         logg.debug('contents {}'.format(self.contents))
    283         for i in range(len(self.types)):
    284             m = None
    285             try:
    286                 m = getattr(self, self.types[i])
    287                 logg.debug('executing module {}'.format(m))
    288                 s = self.contents[i]
    289                 r.append(m(s))
    290             except AttributeError as e:
    291                 if len(self.types[i]) > 4 and self.types[i][:4] == 'uint':
    292                     m = getattr(self, 'uintn')
    293                     s = self.contents[i]
    294                     v = m(s, int(self.types[i][4:]))
    295                     r.append(v)
    296                 else:
    297                     raise e
    298 
    299         return r
    300 
    301 
    302     def get(self):
    303         """Alias of chainlib.eth.contract.ABIContractDecoder.decode
    304         """
    305         return self.decode()
    306 
    307 
    308     def __str__(self):
    309         return self.decode()
    310 
    311 
    312 class ABIContractLogDecoder(ABIMethodEncoder, ABIContractDecoder):
    313     """Decoder utils for log entries of an Ethereum network transaction receipt.
    314 
    315     Uses chainlib.eth.contract.ABIContractDecoder.decode to render output from template.
    316     """
    317     def __init__(self):
    318         super(ABIContractLogDecoder, self).__init__()
    319         self.method_name = None
    320         self.indexed_content = []
    321 
    322 
    323     def topic(self, event):
    324         """Set topic to match.
    325 
    326         :param event: Topic name
    327         :type event: str
    328         """
    329         self.method(event)
    330 
    331     def typ(self, v):
    332         """Add type to event argument array.
    333 
    334         :param v: Type
    335         :type v: chainlib.eth.contract.ABIContractType
    336         """
    337         super(ABIContractLogDecoder, self).typ(v)
    338         self.add_type(v.value)
    339 
    340 
    341 
    342     def apply(self, topics, data):
    343         """Set log entry data to parse.
    344 
    345         After set, self.decode can be used to render the output.
    346 
    347         :param topics: The topics array of the receipt, list of hex
    348         :type topics: list
    349         :param data: Non-indexed data, in hex
    350         :type data: str
    351         :raises ValueError: Topic of input does not match topic set in parser
    352         """
    353         t = self.get_signature()
    354         if not same_hex(topics[0], t):
    355             raise ValueError('topic mismatch')
    356         for i in range(len(topics) - 1):
    357             self.contents.append(topics[i+1])
    358         self.contents += data
    359 
    360 
    361     # Backwards compatibility
    362     def get_method_signature(self):
    363         logg.warning('ABIContractLogDecoder.get_method_signature() is deprecated. Use ABIContractLogDecoder.get_signature() instead')
    364         return self.get_signature()
    365               
    366 
    367 class ABIContractEncoder(ABIMethodEncoder):
    368 
    369     def __log_latest(self, v):
    370         l = len(self.types) - 1 
    371         logg.debug('Encoder added {} -> {} ({})'.format(v, self.contents[l], self.types[l].value))
    372 
    373 
    374     def uint256(self, v):
    375         """Encode value to uint256 and add to input value vector.
    376 
    377         :param v: Integer value
    378         :type v: int
    379         """
    380         v = int(v)
    381         b = v.to_bytes(32, 'big')
    382         self.contents.append(b.hex())
    383         self.add_type(ABIContractType.UINT256)
    384         self.__log_latest(v)
    385 
    386 
    387     def uintn(self, v, bitsize):
    388         """Encode value to uint256 and add to input value vector.
    389 
    390         :param v: Integer value
    391         :type v: int
    392         """
    393         if bitsize % 8 > 0:
    394             raise ValueError('must be 8 multiple')
    395         elif bitsize > 256:
    396             raise ValueError('max 256 bits')
    397 
    398         # encodings of all uint types are padded to word boundary
    399         return self.uint256(v)
    400 
    401         v = int(v)
    402         b = v.to_bytes(int(bitsize / 8), 'big')
    403         self.contents.append(b.hex())
    404         typ = getattr(ABIContractType, 'UINT' + str(bitsize))
    405         self.add_type(typ)
    406         self.__log_latest(v)
    407 
    408 
    409     def bool(self, v):
    410         """Alias of chainlib.eth.contract.ABIContractEncoder.boolean.
    411         """
    412         return self.boolean(v)
    413 
    414 
    415     def boolean(self, v):
    416         """Encode value to boolean and add to input value vector.
    417 
    418         :param v: Trueish or falsish value
    419         :type v: any
    420         :rtype: See chainlib.eth.contract.ABIContractEncoder.uint256
    421         :returns: See chainlib.eth.contract.ABIContractEncoder.uint256
    422         """
    423         if bool(v):
    424             return self.uint256(1)
    425         return self.uint256(0)
    426 
    427 
    428     def address(self, v):
    429         """Encode value to address and add to input value vector.
    430 
    431         :param v: Ethereum address, in hex
    432         :type v: str
    433         """
    434         self.bytes_fixed(32, v, exact=20)
    435         self.add_type(ABIContractType.ADDRESS)
    436         self.__log_latest(v)
    437 
    438 
    439     def bytes32(self, v):
    440         """Encode value to bytes32 and add to input value vector.
    441 
    442         :param v: Bytes, in hex
    443         :type v: str
    444         """
    445         self.bytes_fixed(32, v)
    446         self.add_type(ABIContractType.BYTES32)
    447         self.__log_latest(v)
    448 
    449 
    450     def bytes4(self, v):
    451         """Encode value to bytes4 and add to input value vector.
    452 
    453         :param v: Bytes, in hex
    454         :type v: str
    455         """
    456         self.bytes_fixed(4, v)
    457         self.add_type(ABIContractType.BYTES4)
    458         self.__log_latest(v)
    459 
    460 
    461     def tuple(self, v):
    462         if type(v).__name__ != 'ABIContractEncoder':
    463             raise ValueError('Type for tuple must be ABIContractEncoder')
    464         r = v.get_contents()
    465         self.bytes_fixed(int(len(r) / 2), r)
    466         self.add_type(ABIContractType.TUPLE)
    467         self.__log_latest(v)
    468 
    469 
    470     def string(self, v):
    471         """Encode value to string and add to input value vector.
    472 
    473         :param v: String input
    474         :type v: str
    475         """
    476         b = v.encode('utf-8')
    477         return self._bytes(b, pad=True)
    478 
    479 
    480     def bytes(self, v):
    481         b = bytes.fromhex(v)
    482         return self._bytes(b, pad=True)
    483         
    484 
    485     def _bytes(self, v, pad=False):
    486         l = len(v)
    487         contents = l.to_bytes(32, 'big')
    488         contents += v
    489         padlen = 32 - (l % 32)
    490         if pad:
    491             contents += padlen * b'\x00'
    492         self.bytes_fixed(len(contents), contents)
    493         self.add_type(ABIContractType.STRING)
    494         self.__log_latest(v)
    495         return contents
    496 
    497 
    498     def bytes_fixed(self, mx, v, exact=0, enforce_word=False):
    499         """Add arbirary length byte data to value vector.
    500 
    501         :param mx: Max length of input data.
    502         :type mx: int
    503         :param v: Byte input, hex or bytes
    504         :type v: str | bytes
    505         :param exact: Fail parsing if input does not translate to given byte length.
    506         :type exact: int
    507         :raises ValueError: Input length or input format mismatch.
    508         """
    509         typ = type(v).__name__
    510         if typ == 'str':
    511             v = strip_0x(v)
    512             l = len(v)
    513             if mx == 0:
    514                 mx = l
    515             if exact > 0 and l != exact * 2:
    516                 raise ValueError('value wrong size; expected {}, got {})'.format(mx, l))
    517             if enforce_word and mx % 32 > 0:
    518                 raise ValueError('value size {} does not match word boundary'.format(mx))
    519             if l > mx * 2:
    520                 raise ValueError('value too long ({})'.format(l))
    521             v = pad(v, mx)
    522         elif typ == 'bytes':
    523             l = len(v)
    524             if mx == 0:
    525                 mx = l
    526             if exact > 0 and l != exact:
    527                 raise ValueError('value wrong size; expected {}, got {})'.format(mx, l))
    528             if enforce_word and mx % 32 > 0:
    529                 raise ValueError('value size {} does not match word boundary'.format(mx))
    530             b = bytearray(mx)
    531             b[mx-l:] = v
    532             v = pad(b.hex(), mx)
    533         else:
    534             raise ValueError('invalid input {}'.format(typ))
    535         self.contents.append(v.ljust(64, '0'))
    536 
    537 
    538     def get_contents(self):
    539         """Encode value array.
    540 
    541         :rtype: str
    542         :returns: ABI encoded values, in hex
    543         """
    544         direct_contents = ''
    545         pointer_contents = ''
    546         l = len(self.types)
    547         pointer_cursor = 32 * l
    548         for i in range(l):
    549             if self.types[i] in pointer_contract_types:
    550                 content_length = len(self.contents[i])
    551                 pointer_contents += self.contents[i]
    552                 direct_contents += pointer_cursor.to_bytes(32, 'big').hex()
    553                 pointer_cursor += int(content_length / 2)
    554             else:
    555                 direct_contents += self.contents[i]
    556         s = ''.join(direct_contents + pointer_contents)
    557         for i in range(0, len(s), 64):
    558             l = len(s) - i
    559             if l > 64:
    560                 l = 64
    561             logg.debug('code word {} {}'.format(int(i / 64), s[i:i+64]))
    562         self.dirty = False
    563         return s
    564 
    565 
    566     def get(self):
    567         """Alias of chainlib.eth.contract.ABIContractEncoder.encode
    568         """
    569         return self.encode()
    570 
    571 
    572     def encode(self):
    573         """Encode method and value array.
    574 
    575         The data generated by this method is the literal data used as input to contract calls or transactions.
    576 
    577         :rtype: str
    578         :returns: ABI encoded contract input data, in hex
    579         """
    580         m = self.get_method_signature()
    581         c = self.get_contents()
    582         return m + c
    583 
    584 
    585     def __str__(self):
    586         return self.encode()
    587 
    588 
    589 
    590 def abi_decode_single(typ, v):
    591     """Convenience function to decode a single ABI encoded value against a given type.
    592 
    593     :param typ: Type to parse value as
    594     :type typ: chainlib.eth.contract.ABIContractEncoder
    595     :param v: Value to parse, in hex
    596     :type v: str
    597     """
    598     d = ABIContractDecoder()
    599     d.typ(typ)
    600     d.val(v)
    601     r = d.decode()
    602     return r[0]
    603 
    604 
    605 def code(address, block_spec=BlockSpec.LATEST, id_generator=None):
    606     """Generate json-rpc query to retrieve code stored at an Ethereum address.
    607 
    608     :param address: Address to use for query, in hex
    609     :type address: str
    610     :param block_spec: Block height spec
    611     :type block_spec: chainlib.block.BlockSpec
    612     :param id_generator: json-rpc id generator
    613     :type id_generator: chainlib.jsonrpc.JSONRPCIdGenerator
    614     :rtype: dict
    615     :returns: rpc query object
    616     """
    617     block_height = None
    618     if block_spec == BlockSpec.LATEST:
    619         block_height = 'latest'
    620     elif block_spec == BlockSpec.PENDING:
    621         block_height = 'pending'
    622     else:
    623         block_height = int(block_spec)
    624         block_height = block_height.to_bytes(8, byteorder='big')
    625         block_height = add_0x(block_height.hex())
    626     j = JSONRPCRequest(id_generator)
    627     o = j.template()
    628     o['method'] = 'eth_getCode'
    629     o['params'].append(address)
    630     o['params'].append(block_height)
    631     return j.finalize(o)