diff --git a/CHANGELOG b/CHANGELOG index 99a2ec2..b2124cf 100644 --- 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 index 4628312..b8fa997 100644 --- 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 new file mode 100644 index 0000000..9b6430b --- /dev/null +++ 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 index 502770f..efd7a08 100644 --- 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 - 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)] + # backwards compat + self.gas_price = None + self.gas_limit = None self.contract = None + self.v = None + self.r = None + self.s = None - self.fee_limit = self.gas_limit - self.fee_price = self.gas_price + 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'] - if inpt != '0x': - inpt = strip_0x(inpt) - else: - inpt = '' - self.payload = inpt + src = super(Tx, self).apply_src(src) + + self.hash = self.normal(src['hash'], SrcItem.HASH) + + try: + self.value = hex_to_int(src['value']) + except TypeError: + self.value = int(src['value']) + + try: + self.nonce = hex_to_int(src['nonce']) + except TypeError: + self.nonce = int(src['nonce']) + + try: + self.fee_limit = hex_to_int(src['gas']) + except TypeError: + self.fee_limit = int(src['gas']) + + try: + self.fee_price = hex_to_int(src['gas_price']) + except TypeError: + self.fee_price = int(src['gas_price']) + + self.gas_price = self.fee_price + self.gas_limit = self.fee_limit + + 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 + self.status = Status.PENDING - def src(self): - """Retrieve normalized representation source used to construct transaction object. - - :rtype: dict - :returns: Transaction representation - """ - return self.tx_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 +# @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 index 1452929..f63669f 100644 --- 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 index 19079e1..81871b2 100644 --- 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 index 8dc993a..83c4bf7 100644 --- 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)