Compare commits

...

8 Commits

Author SHA1 Message Date
lash 23f977482c
Correct method to property calls 2022-05-09 19:21:45 +00:00
lash 20dfb641ff
Remove commented code 2022-05-09 18:49:04 +00:00
lash 972535f1f9
Complete test and refactor for generic tx, result, block objects 2022-05-09 18:46:10 +00:00
lash a2168a50e3
WIP implement generic tx and block 2022-05-09 10:00:29 +00:00
lash 9548ed5d1b
Add author property in block 2022-05-07 11:31:06 +00:00
lash e499770d6d
Upgrade deps 2022-05-04 18:15:33 +00:00
lash e49fae9717
Upgrade deps, bump vesrion 2022-04-28 15:44:04 +00:00
lash 84c4a82abb
Bump dep 2022-04-28 15:43:18 +00:00
9 changed files with 262 additions and 139 deletions

View File

@ -1,3 +1,9 @@
- 0.2.0
* Implement chainlib generic tx, block and tx result objects
- 0.1.3
* Add block author field
- 0.1.2
* Upgrade chainlib dep
- 0.1.1
* Add fee_limit, fee_price alias to Tx object
- 0.1.0:

View File

@ -10,6 +10,7 @@ from hexathon import (
# local imports
from chainlib.eth.tx import Tx
from .src import Src
def block_latest(id_generator=None):
@ -76,7 +77,7 @@ def syncing(id_generator=None):
return j.finalize(o)
class Block(BaseBlock):
class Block(BaseBlock, Src):
"""Encapsulates an Ethereum block
:param src: Block representation data
@ -87,7 +88,9 @@ class Block(BaseBlock):
tx_generator = Tx
def __init__(self, src):
self.hash = src['hash']
super(Block, self).__init__(src)
import sys
self.set_hash(src['hash'])
try:
self.number = int(strip_0x(src['number']), 16)
except TypeError:
@ -98,9 +101,10 @@ class Block(BaseBlock):
self.timestamp = int(strip_0x(src['timestamp']), 16)
except TypeError:
self.timestamp = int(src['timestamp'])
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)
@ -117,4 +121,3 @@ class Block(BaseBlock):
if idx == -1:
raise AttributeError('tx {} not found in block {}'.format(tx_hash, self.hash))
return idx

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

@ -0,0 +1,48 @@
# standard imports
import logging
# 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,
)
logg = logging.getLogger(__name__)
class Src(BaseSrc):
@classmethod
def src_normalize(self, v):
src = snake_and_camel(v)
logg.debug('normalize has {}'.format(src))
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,8 @@ from hexathon import (
strip_0x,
add_0x,
compact,
to_int as hex_to_int,
same as hex_same,
)
from rlp import decode as rlp_decode
from rlp import encode as rlp_encode
@ -22,12 +24,17 @@ 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.eth.address import is_same_address
from chainlib.block import BlockSpec
from chainlib.src import SrcItem
# local imports
from .address import to_checksum
@ -39,6 +46,7 @@ from .constant import (
)
from .contract import ABIContractEncoder
from .jsonrpc import to_blockheight_param
from .src import Src
logg = logging.getLogger(__name__)
@ -510,7 +518,51 @@ class TxFactory:
return o
class Tx(BaseTx):
class TxResult(BaseTxResult, Src):
def apply_src(self, v):
self.contract = None
super(TxResult, self).apply_src(v)
self.set_hash(v['transaction_hash'])
try:
status_number = int(v['status'], 16)
except TypeError:
status_number = int(v['status'])
except KeyError as e:
if strict:
raise(e)
logg.debug('setting "success" status on missing status property for {}'.format(self.hash))
status_number = 1
if v['block_number'] == None:
self.status = Status.PENDING
else:
if status_number == 1:
self.status = Status.SUCCESS
elif status_number == 0:
self.status = Status.ERROR
try:
self.tx_index = hex_to_int(v['transaction_index'])
except TypeError:
self.tx_index = int(v['transaction_index'])
self.block_hash = v['block_hash']
# TODO: replace with rpc receipt/transaction translator when available
contract_address = v.get('contract_address')
if contract_address != None:
self.contract = contract_address
self.logs = v['logs']
try:
self.fee_cost = hex_to_int(v['gas_used'])
except TypeError:
self.fee_cost = int(v['gas_used'])
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,114 +579,88 @@ class Tx(BaseTx):
#:todo: divide up constructor method
"""
def __init__(self, src, block=None, rcpt=None, strict=False):
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)]
def __init__(self, src, block=None, result=None, strict=False, rcpt=None):
# 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)
if result == None and rcpt != None:
self.apply_receipt(rcpt)
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)
hsh = self.normal(src['hash'], SrcItem.HASH)
self.set_hash(hsh)
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
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
#self.status = Status.PENDING
def as_dict(self):
return self.src()
def rcpt_src(self):
return self.tx_rcpt_src
return self.src
def apply_receipt(self, rcpt, strict=False):
result = TxResult(rcpt)
self.apply_result(result)
def apply_result(self, result, strict=False):
"""Apply receipt data to transaction object.
Effect is the same as passing a receipt at construction.
@ -642,53 +668,14 @@ class Tx(BaseTx):
:param rcpt: Receipt data
:type rcpt: dict
"""
rcpt = self.src_normalize(rcpt)
logg.debug('rcpt {}'.format(rcpt))
self.tx_rcpt_src = rcpt
if not hex_same(result.hash, self.hash):
raise ValueError('result hash {} does not match transaction hash {}'.format(result.hash, self.hash))
tx_hash = add_0x(rcpt['transaction_hash'])
if rcpt['transaction_hash'] != add_0x(self.hash):
raise ValueError('rcpt hash {} does not match transaction hash {}'.format(rcpt['transaction_hash'], self.hash))
block_hash = add_0x(rcpt['block_hash'])
if self.block != None:
if block_hash != add_0x(self.block.hash):
raise ValueError('rcpt block hash {} does not match transaction block hash {}'.format(rcpt['block_hash'], self.block.hash))
if not hex_same(result.block_hash, self.block.hash):
raise ValueError('result block hash {} does not match transaction block hash {}'.format(result.block_hash, self.block.hash))
try:
status_number = int(rcpt['status'], 16)
except TypeError:
status_number = int(rcpt['status'])
except KeyError as e:
if strict:
raise(e)
logg.debug('setting "success" status on missing status property for {}'.format(self.hash))
status_number = 1
if rcpt['block_number'] == None:
self.status = Status.PENDING
else:
if status_number == 1:
self.status = Status.SUCCESS
elif status_number == 0:
self.status = Status.ERROR
try:
self.tx_index = int(rcpt['transaction_index'], 16)
except TypeError:
self.tx_index = int(rcpt['transaction_index'])
# TODO: replace with rpc receipt/transaction translator when available
contract_address = rcpt.get('contractAddress')
if contract_address == None:
contract_address = rcpt.get('contract_address')
if contract_address != None:
self.contract = contract_address
self.logs = rcpt['logs']
try:
self.gas_used = int(rcpt['gasUsed'], 16)
except TypeError:
self.gas_used = int(rcpt['gasUsed'])
self.__rcpt_block_hash = rcpt['block_hash']
super(Tx, self).apply_result(result)
def apply_block(self, block):
@ -697,9 +684,6 @@ 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))
self.index = block.get_tx(self.hash)
self.block = block
@ -795,3 +779,5 @@ tx_index {}
)
return s

View File

@ -1,7 +1,7 @@
funga-eth~=0.6.0
funga-eth~=0.6.1
pysha3==1.0.2
hexathon~=0.1.5
hexathon~=0.1.6
websocket-client==0.57.0
potaahto~=0.1.1
chainlib~=0.1.0
chainlib~=0.1.2
confini~=0.6.0

View File

@ -1,10 +1,10 @@
[metadata]
name = chainlib-eth
version = 0.1.1
version = 0.2.0
description = Ethereum implementation of the chainlib interface
author = Louis Holbrook
author_email = dev@holbrook.no
url = https://gitlab.com/chaintools/chainlib
url = https://gitlab.com/chaintool/chainlib-eth
keywords =
dlt
blockchain
@ -26,7 +26,7 @@ licence_files =
[options]
include_package_data = True
python_requires = >= 3.6
python_requires = >= 3.7
packages =
chainlib.eth
chainlib.eth.dialect

View File

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

View File

@ -1,6 +1,7 @@
# standard imports
import unittest
import datetime
import os
# external imports
from chainlib.stat import ChainStat
@ -19,6 +20,7 @@ class TestStat(unittest.TestCase):
'hash': None,
'transactions': [],
'number': 41,
'author': os.urandom(20).hex(),
})
d = datetime.datetime.utcnow()
@ -27,6 +29,7 @@ class TestStat(unittest.TestCase):
'hash': None,
'transactions': [],
'number': 42,
'author': os.urandom(20).hex(),
})
s.block_apply(block_a)
@ -39,6 +42,7 @@ class TestStat(unittest.TestCase):
'hash': None,
'transactions': [],
'number': 43,
'author': os.urandom(20).hex(),
})
s.block_apply(block_c)

View File

@ -30,6 +30,7 @@ from chainlib.eth.address import (
from hexathon import (
strip_0x,
add_0x,
same as hex_same,
)
from chainlib.eth.block import Block
@ -39,6 +40,27 @@ 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'])
self.assertTrue(hex_same(tx.payload, tx_src['data']))
def test_tx_reciprocal(self):
nonce_oracle = RPCNonceOracle(self.accounts[0], self.rpc)
@ -60,7 +82,7 @@ class TxTestCase(EthTesterCase):
o = transaction(tx_hash_hex)
tx_src = self.rpc.do(o)
tx = Tx(tx_src)
tx_bin = pack(tx.src(), self.chain_spec)
tx_bin = pack(tx.src, self.chain_spec)
def test_tx_pack(self):
@ -107,6 +129,7 @@ class TxTestCase(EthTesterCase):
'number': 42,
'timestamp': 13241324,
'transactions': [],
'author': os.urandom(20).hex(),
})
with self.assertRaises(AttributeError):
tx = Tx(tx_data, block=block)
@ -149,7 +172,9 @@ class TxTestCase(EthTesterCase):
'number': 42,
'timestamp': 13241324,
'transactions': [],
'author': os.urandom(20).hex(),
})
block.txs = [add_0x(tx_data['hash'])]
with self.assertRaises(ValueError):
tx = Tx(tx_data, rcpt=rcpt, block=block)