chainlib-eth

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

commit a2168a50e3b9ea56e9921fd4a485465d3d967d1d
parent 9548ed5d1b58050375ea392191f3587ffdf27332
Author: lash <dev@holbrook.no>
Date:   Mon,  9 May 2022 10:00:29 +0000

WIP implement generic tx and block

Diffstat:
MCHANGELOG | 2++
Mchainlib/eth/block.py | 4++--
Achainlib/eth/src.py | 42++++++++++++++++++++++++++++++++++++++++++
Mchainlib/eth/tx.py | 214++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------
Msetup.cfg | 2+-
Mtests/test_block.py | 51+++++++++++++++++++++++++++++++++++++++++++++++++++
Mtests/test_tx.py | 20++++++++++++++++++++
7 files changed, 258 insertions(+), 77 deletions(-)

diff --git a/CHANGELOG b/CHANGELOG @@ -1,3 +1,5 @@ +- 0.2.0 + * Implement chainlib generic tx, block and tx result objects - 0.1.3 * Add block author field - 0.1.2 diff --git a/chainlib/eth/block.py b/chainlib/eth/block.py @@ -87,6 +87,7 @@ class Block(BaseBlock): tx_generator = Tx def __init__(self, src): + super(Block, self).__init__(src) self.hash = src['hash'] try: self.number = int(strip_0x(src['number']), 16) @@ -101,7 +102,7 @@ class Block(BaseBlock): self.author = src['author'] - def get_tx(self, tx_hash): + def tx_index_by_hash(self, tx_hash): i = 0 idx = -1 tx_hash = add_0x(tx_hash) @@ -118,4 +119,3 @@ class Block(BaseBlock): if idx == -1: raise AttributeError('tx {} not found in block {}'.format(tx_hash, self.hash)) return idx - diff --git a/chainlib/eth/src.py b/chainlib/eth/src.py @@ -0,0 +1,42 @@ +# external imports +from potaahto.symbols import snake_and_camel +from hexathon import ( + uniform, + strip_0x, + ) + +# local imports +from chainlib.src import ( + Src as BaseSrc, + SrcItem, + ) + + +class Src(BaseSrc): + + @classmethod + def src_normalize(self, v): + src = snake_and_camel(v) + if isinstance(src.get('v'), str): + try: + src['v'] = int(src['v']) + except ValueError: + src['v'] = int(src['v'], 16) + return src + + + def normal(self, v, typ=SrcItem.AUTO): + if typ == SrcItem.SRC: + return self.src_normalize(v) + + if typ == SrcItem.HASH: + v = strip_0x(v, pad=False) + v = uniform(v, compact_value=True) + elif typ == SrcItem.ADDRESS: + v = strip_0x(v, pad=False) + v = uniform(v, compact_value=True) + elif typ == SrcItem.PAYLOAD: + v = strip_0x(v, pad=False, allow_empty=True) + v = uniform(v, compact_value=False, allow_empty=True) + + return v diff --git a/chainlib/eth/tx.py b/chainlib/eth/tx.py @@ -10,6 +10,7 @@ from hexathon import ( strip_0x, add_0x, compact, + to_int as hex_to_int, ) from rlp import decode as rlp_decode from rlp import encode as rlp_encode @@ -22,12 +23,16 @@ from potaahto.symbols import snake_and_camel from chainlib.hash import keccak256_hex_to_hex from chainlib.status import Status from chainlib.jsonrpc import JSONRPCRequest -from chainlib.tx import Tx as BaseTx +from chainlib.tx import ( + Tx as BaseTx, + TxResult as BaseTxResult, + ) from chainlib.eth.nonce import ( nonce as nonce_query, nonce_confirmed as nonce_query_confirmed, ) from chainlib.block import BlockSpec +from chainlib.src import SrcItem # local imports from .address import to_checksum @@ -39,6 +44,7 @@ from .constant import ( ) from .contract import ABIContractEncoder from .jsonrpc import to_blockheight_param +from .src import Src logg = logging.getLogger(__name__) @@ -510,7 +516,7 @@ class TxFactory: return o -class Tx(BaseTx): +class Tx(BaseTx, Src): """Wraps transaction data, transaction receipt data and block data, enforces local standardization of fields, and provides useful output formats for viewing transaction contents. If block is applied, the transaction data or transaction hash must exist in its transactions array. @@ -527,103 +533,163 @@ class Tx(BaseTx): #:todo: divide up constructor method """ - def __init__(self, src, block=None, rcpt=None, strict=False): - self.__rcpt_block_hash = None + def __init__(self, src, block=None, result=None, strict=False, rcpt=None): + if result == None: + result = rcpt + + # backwards compat + self.gas_price = None + self.gas_limit = None + self.contract = None + self.v = None + self.r = None + self.s = None + + super(Tx, self).__init__(src, block=block, result=result, strict=strict) + #self.__rcpt_block_hash = None + + #src = self.src_normalize(src) + #self.index = -1 + #tx_hash = add_0x(src['hash']) +# self.hash = strip_0x(tx_hash) +# if block != None: +# self.apply_block(block) +# try: +# self.value = int(strip_0x(src['value']), 16) +# except TypeError: +# self.value = int(src['value']) +# try: +# self.nonce = int(strip_0x(src['nonce']), 16) +# except TypeError: +# self.nonce = int(src['nonce']) +# address_from = strip_0x(src['from']) +# try: +# self.gas_price = int(strip_0x(src['gasPrice']), 16) +# except TypeError: +# self.gas_price = int(src['gasPrice']) +# try: +# self.gas_limit = int(strip_0x(src['gas']), 16) +# except TypeError: +# self.gas_limit = int(src['gas']) +# self.outputs = [to_checksum(address_from)] + +# self.fee_limit = self.gas_limit +# self.fee_price = self.gas_price + +# try: +# inpt = src['input'] +# except KeyError: +# inpt = src['data'] +# src['input'] = src['data'] + +# if inpt != '0x': +# inpt = strip_0x(inpt) +# else: +# inpt = '' +# self.payload = inpt + +# to = src['to'] +# if to == None: +# to = ZERO_ADDRESS +# self.inputs = [to_checksum(strip_0x(to))] + +# self.block = block +# try: +# self.wire = src['raw'] +# except KeyError: +# logg.debug('no inline raw tx src, and no raw rendering implemented, field will be "None"') + +# self.status = Status.PENDING +# self.logs = None + + #self.tx_rcpt_src = None + #if rcpt != None: + # self.apply_receipt(rcpt, strict=strict) + #self.outputs = [to_checksum(address_from)] + +# self.v = src.get('v') +# self.r = src.get('r') +# self.s = src.get('s') + +# self.wire = None + +# self.tx_src = src + + + + def apply_src(self, src): + try: + inpt = src['input'] + except KeyError: + inpt = src['data'] + src['input'] = src['data'] + + src = super(Tx, self).apply_src(src) + + self.hash = self.normal(src['hash'], SrcItem.HASH) - src = self.src_normalize(src) - self.index = -1 - tx_hash = add_0x(src['hash']) - self.hash = strip_0x(tx_hash) - if block != None: - self.apply_block(block) try: - self.value = int(strip_0x(src['value']), 16) + self.value = hex_to_int(src['value']) except TypeError: self.value = int(src['value']) + try: - self.nonce = int(strip_0x(src['nonce']), 16) + self.nonce = hex_to_int(src['nonce']) except TypeError: self.nonce = int(src['nonce']) - address_from = strip_0x(src['from']) + try: - self.gas_price = int(strip_0x(src['gasPrice']), 16) + self.fee_limit = hex_to_int(src['gas']) except TypeError: - self.gas_price = int(src['gasPrice']) + self.fee_limit = int(src['gas']) + try: - self.gas_limit = int(strip_0x(src['gas']), 16) + self.fee_price = hex_to_int(src['gas_price']) except TypeError: - self.gas_limit = int(src['gas']) - self.outputs = [to_checksum(address_from)] - self.contract = None - - self.fee_limit = self.gas_limit - self.fee_price = self.gas_price + self.fee_price = int(src['gas_price']) - try: - inpt = src['input'] - except KeyError: - inpt = src['data'] - src['input'] = src['data'] + self.gas_price = self.fee_price + self.gas_limit = self.fee_limit - if inpt != '0x': - inpt = strip_0x(inpt) - else: - inpt = '' - self.payload = inpt + address_from = self.normal(src['from'], SrcItem.ADDRESS) + self.outputs = [to_checksum(address_from)] to = src['to'] if to == None: to = ZERO_ADDRESS self.inputs = [to_checksum(strip_0x(to))] - self.block = block + self.payload = self.normal(src['input'], SrcItem.PAYLOAD) + try: - self.wire = src['raw'] + self.set_wire(src['raw']) except KeyError: logg.debug('no inline raw tx src, and no raw rendering implemented, field will be "None"') - self.status = Status.PENDING - self.logs = None - - self.tx_rcpt_src = None - if rcpt != None: - self.apply_receipt(rcpt, strict=strict) - self.v = src.get('v') self.r = src.get('r') self.s = src.get('s') - self.wire = None - - self.tx_src = src - - - def src(self): - """Retrieve normalized representation source used to construct transaction object. - - :rtype: dict - :returns: Transaction representation - """ - return self.tx_src - + self.status = Status.PENDING - @classmethod - def src_normalize(self, src): - """Normalizes transaction representation source data. - :param src: Transaction representation - :type src: dict - :rtype: dict - :returns: Transaction representation, normalized - """ - src = snake_and_camel(src) - - if isinstance(src.get('v'), str): - try: - src['v'] = int(src['v']) - except ValueError: - src['v'] = int(src['v'], 16) - return src +# @classmethod +# def src_normalize(self, src): +# """Normalizes transaction representation source data. +# +# :param src: Transaction representation +# :type src: dict +# :rtype: dict +# :returns: Transaction representation, normalized +# """ +# src = snake_and_camel(src) +# +# if isinstance(src.get('v'), str): +# try: +# src['v'] = int(src['v']) +# except ValueError: +# src['v'] = int(src['v'], 16) +# return src def as_dict(self): @@ -688,7 +754,7 @@ class Tx(BaseTx): except TypeError: self.gas_used = int(rcpt['gasUsed']) - self.__rcpt_block_hash = rcpt['block_hash'] + #self.__rcpt_block_hash = rcpt['block_hash'] def apply_block(self, block): @@ -697,9 +763,9 @@ class Tx(BaseTx): :param block: Block object :type block: chainlib.block.Block """ - if self.__rcpt_block_hash != None: - if block.hash != self.__rcpt_block_hash: - raise ValueError('block hash {} does not match already applied receipt block hash {}'.format(block.hash, self.__rcpt_block_hash)) + #if self.__rcpt_block_hash != None: + # if block.hash != self.__rcpt_block_hash: + # raise ValueError('block hash {} does not match already applied receipt block hash {}'.format(block.hash, self.__rcpt_block_hash)) self.index = block.get_tx(self.hash) self.block = block diff --git a/setup.cfg b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = chainlib-eth -version = 0.1.3 +version = 0.2.0 description = Ethereum implementation of the chainlib interface author = Louis Holbrook author_email = dev@holbrook.no diff --git a/tests/test_block.py b/tests/test_block.py @@ -1,12 +1,63 @@ # standard imports import unittest +import os +import datetime +import logging # local imports from chainlib.eth.jsonrpc import to_blockheight_param +from chainlib.eth.block import Block + +logging.basicConfig(level=logging.DEBUG) class TestBlock(unittest.TestCase): + + def test_block(self): + tx_one_src = { + 'hash': os.urandom(32).hex(), + 'from': os.urandom(20).hex(), + 'to': os.urandom(20).hex(), + 'value': 13, + 'data': '0xdeadbeef', + 'nonce': 666, + 'gasPrice': 100, + 'gas': 21000, + } + + tx_two_src_hash = os.urandom(32).hex() + + block_hash = os.urandom(32).hex() + block_author = os.urandom(20).hex() + block_time = datetime.datetime.utcnow().timestamp() + block_src = { + 'number': 42, + 'hash': block_hash, + 'author': block_author, + 'transactions': [ + tx_one_src, + tx_two_src_hash, + ], + 'timestamp': block_time, + } + block = Block(block_src) + + self.assertEqual(block.number, 42) + self.assertEqual(block.hash, block_hash) + self.assertEqual(block.author, block_author) + self.assertEqual(block.timestamp, int(block_time)) + + tx_index = block.tx_index_by_hash(tx_one_src['hash']) + self.assertEqual(tx_index, 0) + + tx_retrieved = block.tx_by_index(tx_index) + self.assertEqual(tx_retrieved.hash, tx_one_src['hash']) + + tx_index = block.tx_index_by_hash(tx_two_src_hash) + self.assertEqual(tx_index, 1) + + def test_blockheight_param(self): self.assertEqual(to_blockheight_param('latest'), 'latest') self.assertEqual(to_blockheight_param(0), 'latest') diff --git a/tests/test_tx.py b/tests/test_tx.py @@ -39,6 +39,26 @@ logg = logging.getLogger() class TxTestCase(EthTesterCase): + def test_tx_basic(self): + tx_src = { + 'hash': os.urandom(32).hex(), + 'from': os.urandom(20).hex(), + 'to': os.urandom(20).hex(), + 'value': 13, + 'data': '0xdeadbeef', + 'nonce': 666, + 'gasPrice': 100, + 'gas': 21000, + } + + tx = Tx(tx_src) + + self.assertEqual(tx.hash, tx_src['hash']) + self.assertTrue(is_same_address(tx.outputs[0], tx_src['from'])) + self.assertTrue(is_same_address(tx.inputs[0], tx_src['to'])) + self.assertEqual(tx.value, tx_src['value']) + self.assertEqual(tx.nonce, tx_src['nonce']) + def test_tx_reciprocal(self): nonce_oracle = RPCNonceOracle(self.accounts[0], self.rpc)