WIP implement generic tx and block
This commit is contained in:
parent
9548ed5d1b
commit
a2168a50e3
@ -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
|
||||||
|
@ -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
42
chainlib/eth/src.py
Normal 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
|
@ -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
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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')
|
||||||
|
@ -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)
|
||||||
|
Loading…
Reference in New Issue
Block a user