WIP implement generic tx and block

This commit is contained in:
lash 2022-05-09 10:00:29 +00:00
parent 9548ed5d1b
commit a2168a50e3
Signed by: lash
GPG Key ID: 21D2E7BB88C2A746
7 changed files with 261 additions and 80 deletions

View File

@ -1,3 +1,5 @@
- 0.2.0
* Implement chainlib generic tx, block and tx result objects
- 0.1.3 - 0.1.3
* Add block author field * Add block author field
- 0.1.2 - 0.1.2

View File

@ -87,6 +87,7 @@ class Block(BaseBlock):
tx_generator = Tx tx_generator = Tx
def __init__(self, src): def __init__(self, src):
super(Block, self).__init__(src)
self.hash = src['hash'] self.hash = src['hash']
try: try:
self.number = int(strip_0x(src['number']), 16) self.number = int(strip_0x(src['number']), 16)
@ -101,7 +102,7 @@ class Block(BaseBlock):
self.author = src['author'] self.author = src['author']
def get_tx(self, tx_hash): def tx_index_by_hash(self, tx_hash):
i = 0 i = 0
idx = -1 idx = -1
tx_hash = add_0x(tx_hash) tx_hash = add_0x(tx_hash)
@ -118,4 +119,3 @@ class Block(BaseBlock):
if idx == -1: if idx == -1:
raise AttributeError('tx {} not found in block {}'.format(tx_hash, self.hash)) raise AttributeError('tx {} not found in block {}'.format(tx_hash, self.hash))
return idx return idx

42
chainlib/eth/src.py Normal file
View File

@ -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

View File

@ -10,6 +10,7 @@ from hexathon import (
strip_0x, strip_0x,
add_0x, add_0x,
compact, compact,
to_int as hex_to_int,
) )
from rlp import decode as rlp_decode from rlp import decode as rlp_decode
from rlp import encode as rlp_encode 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.hash import keccak256_hex_to_hex
from chainlib.status import Status from chainlib.status import Status
from chainlib.jsonrpc import JSONRPCRequest 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 ( from chainlib.eth.nonce import (
nonce as nonce_query, nonce as nonce_query,
nonce_confirmed as nonce_query_confirmed, nonce_confirmed as nonce_query_confirmed,
) )
from chainlib.block import BlockSpec from chainlib.block import BlockSpec
from chainlib.src import SrcItem
# local imports # local imports
from .address import to_checksum from .address import to_checksum
@ -39,6 +44,7 @@ from .constant import (
) )
from .contract import ABIContractEncoder from .contract import ABIContractEncoder
from .jsonrpc import to_blockheight_param from .jsonrpc import to_blockheight_param
from .src import Src
logg = logging.getLogger(__name__) logg = logging.getLogger(__name__)
@ -510,7 +516,7 @@ class TxFactory:
return o 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. """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. 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 #:todo: divide up constructor method
""" """
def __init__(self, src, block=None, rcpt=None, strict=False): def __init__(self, src, block=None, result=None, strict=False, rcpt=None):
self.__rcpt_block_hash = None if result == None:
result = rcpt
src = self.src_normalize(src) # backwards compat
self.index = -1 self.gas_price = None
tx_hash = add_0x(src['hash']) self.gas_limit = None
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.contract = None self.contract = None
self.v = None
self.r = None
self.s = None
self.fee_limit = self.gas_limit super(Tx, self).__init__(src, block=block, result=result, strict=strict)
self.fee_price = self.gas_price #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: try:
inpt = src['input'] inpt = src['input']
except KeyError: except KeyError:
inpt = src['data'] inpt = src['data']
src['input'] = src['data'] src['input'] = src['data']
if inpt != '0x': src = super(Tx, self).apply_src(src)
inpt = strip_0x(inpt)
else: self.hash = self.normal(src['hash'], SrcItem.HASH)
inpt = ''
self.payload = inpt 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'] to = src['to']
if to == None: if to == None:
to = ZERO_ADDRESS to = ZERO_ADDRESS
self.inputs = [to_checksum(strip_0x(to))] self.inputs = [to_checksum(strip_0x(to))]
self.block = block self.payload = self.normal(src['input'], SrcItem.PAYLOAD)
try: try:
self.wire = src['raw'] self.set_wire(src['raw'])
except KeyError: except KeyError:
logg.debug('no inline raw tx src, and no raw rendering implemented, field will be "None"') 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.v = src.get('v')
self.r = src.get('r') self.r = src.get('r')
self.s = src.get('s') self.s = src.get('s')
self.wire = None self.status = Status.PENDING
self.tx_src = src
def src(self): # @classmethod
"""Retrieve normalized representation source used to construct transaction object. # def src_normalize(self, src):
# """Normalizes transaction representation source data.
:rtype: dict #
:returns: Transaction representation # :param src: Transaction representation
""" # :type src: dict
return self.tx_src # :rtype: dict
# :returns: Transaction representation, normalized
# """
@classmethod # src = snake_and_camel(src)
def src_normalize(self, src): #
"""Normalizes transaction representation source data. # if isinstance(src.get('v'), str):
# try:
:param src: Transaction representation # src['v'] = int(src['v'])
:type src: dict # except ValueError:
:rtype: dict # src['v'] = int(src['v'], 16)
:returns: Transaction representation, normalized # return src
"""
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): def as_dict(self):
@ -688,7 +754,7 @@ class Tx(BaseTx):
except TypeError: except TypeError:
self.gas_used = int(rcpt['gasUsed']) 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): def apply_block(self, block):
@ -697,9 +763,9 @@ class Tx(BaseTx):
:param block: Block object :param block: Block object
:type block: chainlib.block.Block :type block: chainlib.block.Block
""" """
if self.__rcpt_block_hash != None: #if self.__rcpt_block_hash != None:
if block.hash != self.__rcpt_block_hash: # 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)) # 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.index = block.get_tx(self.hash)
self.block = block self.block = block

View File

@ -1,6 +1,6 @@
[metadata] [metadata]
name = chainlib-eth name = chainlib-eth
version = 0.1.3 version = 0.2.0
description = Ethereum implementation of the chainlib interface description = Ethereum implementation of the chainlib interface
author = Louis Holbrook author = Louis Holbrook
author_email = dev@holbrook.no author_email = dev@holbrook.no

View File

@ -1,12 +1,63 @@
# standard imports # standard imports
import unittest import unittest
import os
import datetime
import logging
# local imports # local imports
from chainlib.eth.jsonrpc import to_blockheight_param from chainlib.eth.jsonrpc import to_blockheight_param
from chainlib.eth.block import Block
logging.basicConfig(level=logging.DEBUG)
class TestBlock(unittest.TestCase): 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): def test_blockheight_param(self):
self.assertEqual(to_blockheight_param('latest'), 'latest') self.assertEqual(to_blockheight_param('latest'), 'latest')
self.assertEqual(to_blockheight_param(0), 'latest') self.assertEqual(to_blockheight_param(0), 'latest')

View File

@ -39,6 +39,26 @@ logg = logging.getLogger()
class TxTestCase(EthTesterCase): 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): def test_tx_reciprocal(self):
nonce_oracle = RPCNonceOracle(self.accounts[0], self.rpc) nonce_oracle = RPCNonceOracle(self.accounts[0], self.rpc)