Add docstrings

This commit is contained in:
Louis Holbrook 2021-08-21 07:27:40 +00:00
parent 8374d83045
commit d13708d3ce
33 changed files with 1333 additions and 585 deletions

View File

@ -1,2 +1,3 @@
- 0.0.5-pending
* Receive all ethereum components from chainlib package
* Make settings configurable

View File

@ -1 +1 @@
include *requirements.txt LICENSE
include *requirements.txt LICENSE chainlib/eth/data/config/*

View File

@ -1,4 +1,4 @@
# third-party imports
# external imports
import sha3
from hexathon import (
strip_0x,
@ -11,3 +11,34 @@ from crypto_dev_signer.encoding import (
)
to_checksum = to_checksum_address
class AddressChecksum:
"""Address checksummer implementation.
Primarily for use with chainlib.cli.wallet.Wallet
"""
@classmethod
def valid(cls, v):
"""Check if address is a valid checksum address
:param v: Address value, in hex
:type v: str
:rtype: bool
:returns: True if valid checksum
"""
return is_checksum_address(v)
@classmethod
def sum(cls, v):
"""Create checksum from address
:param v: Address value, in hex
:type v: str
:raises ValueError: Invalid address
:rtype: str
:returns: Checksum address
"""
return to_checksum_address(v)

View File

@ -1,14 +1,19 @@
# third-party imports
# external imports
from chainlib.jsonrpc import JSONRPCRequest
from chainlib.eth.tx import Tx
from chainlib.block import Block as BaseBlock
from hexathon import (
add_0x,
strip_0x,
even,
)
# local imports
from chainlib.eth.tx import Tx
def block_latest(id_generator=None):
"""Implements chainlib.interface.ChainInterface method
"""
j = JSONRPCRequest(id_generator)
o = j.template()
o['method'] = 'eth_blockNumber'
@ -16,6 +21,8 @@ def block_latest(id_generator=None):
def block_by_hash(hsh, include_tx=True, id_generator=None):
"""Implements chainlib.interface.ChainInterface method
"""
j = JSONRPCRequest(id_generator)
o = j.template()
o['method'] = 'eth_getBlockByHash'
@ -25,6 +32,8 @@ def block_by_hash(hsh, include_tx=True, id_generator=None):
def block_by_number(n, include_tx=True, id_generator=None):
"""Implements chainlib.interface.ChainInterface method
"""
nhx = add_0x(even(hex(n)[2:]))
j = JSONRPCRequest(id_generator)
o = j.template()
@ -35,6 +44,15 @@ def block_by_number(n, include_tx=True, id_generator=None):
def transaction_count(block_hash, id_generator=None):
"""Generate json-rpc query to get transaction count of block
:param block_hash: Block hash, in hex
:type block_hash: str
:param id_generator: JSONRPC id generator
:type id_generator: JSONRPCIdGenerator
:rtype: dict
:returns: rpc query object
"""
j = JSONRPCRequest(id_generator)
o = j.template()
o['method'] = 'eth_getBlockTransactionCountByHash'
@ -42,7 +60,13 @@ def transaction_count(block_hash, id_generator=None):
return j.finalize(o)
class Block:
class Block(BaseBlock):
"""Encapsulates an Ethereum block
:param src: Block representation data
:type src: dict
:todo: Add hex to number parse to normalize
"""
def __init__(self, src):
self.hash = src['hash']
@ -58,22 +82,21 @@ class Block:
self.timestamp = int(src['timestamp'])
def src(self):
return self.block_src
def get_tx(self, tx_hash):
i = 0
idx = -1
tx_hash = add_0x(tx_hash)
for tx in self.txs:
tx_hash_block = None
try:
tx_hash_block = add_0x(tx['hash'])
except TypeError:
tx_hash_block = add_0x(tx)
if tx_hash_block == tx_hash:
idx = i
break
i += 1
if idx == -1:
raise AttributeError('tx {} not found in block {}'.format(tx_hash, self.hash))
return idx
def tx(self, i):
return Tx(self.txs[i], self)
def tx_src(self, i):
return self.txs[i]
def __str__(self):
return 'block {} {} ({} txs)'.format(self.number, self.hash, len(self.txs))
@staticmethod
def from_src(src):
return Block(src)

View File

@ -2,6 +2,13 @@ from chainlib.jsonrpc import JSONRPCRequest
def network_id(id_generator=None):
"""Generate json-rpc query to retrieve network id from node
:param id_generator: JSON-RPC id generator
:type id_generator: JSONRPCIdGenerator
:rtype: dict
:returns: rpc query object
"""
j = JSONRPCRequest(id_generator=id_generator)
o = j.template()
o['method'] = 'net_version'

44
chainlib/eth/cli.py Normal file
View File

@ -0,0 +1,44 @@
# standard imports
import os
# external imports
from chainlib.cli import (
ArgumentParser,
argflag_std_read,
argflag_std_write,
argflag_std_base,
Config as BaseConfig,
Wallet as BaseWallet,
Rpc as BaseRpc,
Flag,
)
from crypto_dev_signer.eth.signer import ReferenceSigner as EIP155Signer
# local imports
from chainlib.eth.address import AddressChecksum
from chainlib.eth.connection import EthHTTPConnection
script_dir = os.path.dirname(os.path.realpath(__file__))
class Wallet(BaseWallet):
"""Convenience constructor to set Ethereum defaults for chainlib cli Wallet object
:param checksummer: Address checksummer object
:type checksummer: Implementation of chainlib.eth.address.AddressChecksum
"""
def __init__(self, checksummer=AddressChecksum):
super(Wallet, self).__init__(EIP155Signer, checksummer=checksummer)
class Rpc(BaseRpc):
"""Convenience constructor to set Ethereum defaults for chainlib cli Rpc object
"""
def __init__(self, wallet=None):
super(Rpc, self).__init__(EthHTTPConnection, wallet=wallet)
class Config(BaseConfig):
"""Convenience constructor to set Ethereum defaults for the chainlib cli config object
"""
default_base_config_dir = os.path.join(script_dir, 'data', 'config')
default_fee_limit = 21000

View File

@ -43,8 +43,33 @@ logg = logging.getLogger(__name__)
class EthHTTPConnection(JSONRPCHTTPConnection):
"""HTTP Interface for Ethereum node JSON-RPC
:todo: support https
"""
def wait(self, tx_hash_hex, delay=0.5, timeout=0.0, error_parser=error_parser, id_generator=None):
"""Poll for confirmation of a transaction on network.
Returns the result of the transaction if it was successfully executed on the network, and raises RevertEthException if execution fails.
This is a blocking call.
:param tx_hash_hex: Transaction hash to wait for, hex
:type tx_hash_hex: str
:param delay: Polling interval
:type delay: float
:param timeout: Max time to wait for confirmation (0 = no timeout)
:type timeout: float
:param error_parser: json-rpc response error parser
:type error_parser: chainlib.jsonrpc.ErrorParser
:param id_generator: json-rpc id generator
:type id_generator: chainlib.jsonrpc.JSONRPCIdGenerator
:raises TimeoutError: Timeout reached
:raises chainlib.eth.error.RevertEthException: Transaction confirmed but failed
:rtype: dict
:returns: Transaction receipt
"""
t = datetime.datetime.utcnow()
i = 0
while True:
@ -59,13 +84,13 @@ class EthHTTPConnection(JSONRPCHTTPConnection):
)
req.add_header('Content-Type', 'application/json')
data = json.dumps(o)
logg.debug('(HTTP) poll receipt attempt {} {}'.format(i, data))
logg.debug('({}) poll receipt attempt {} {}'.format(str(self), i, data))
res = urlopen(req, data=data.encode('utf-8'))
r = json.load(res)
e = jsonrpc_result(r, error_parser)
if e != None:
logg.debug('(HTTP) poll receipt completed {}'.format(r))
logg.debug('({}) poll receipt completed {}'.format(str(self), r))
logg.debug('e {}'.format(strip_0x(e['status'])))
if strip_0x(e['status']) == '00':
raise RevertEthException(tx_hash_hex)
@ -80,7 +105,17 @@ class EthHTTPConnection(JSONRPCHTTPConnection):
i += 1
def __str__(self):
return 'ETH HTTP JSONRPC'
def check_rpc(self, id_generator=None):
"""Execute Ethereum specific json-rpc query to (superficially) check whether node is sane.
:param id_generator: json-rpc id generator
:type id_generator: chainlib.jsonrpc.JSONRPCIdGenerator
:raises Exception: Any exception indicates an invalid node
"""
j = JSONRPCRequest(id_generator)
req = j.template()
req['method'] = 'net_version'
@ -89,12 +124,29 @@ class EthHTTPConnection(JSONRPCHTTPConnection):
class EthUnixConnection(JSONRPCUnixConnection):
"""Unix socket implementation of Ethereum JSON-RPC
"""
def wait(self, tx_hash_hex, delay=0.5, timeout=0.0, error_parser=error_parser):
"""See EthHTTPConnection. Not yet implemented for unix socket.
"""
raise NotImplementedError('Not yet implemented for unix socket')
def sign_transaction_to_rlp(chain_spec, doer, tx):
"""Generate a signature query and execute it against a json-rpc signer backend.
Uses the `eth_signTransaction` json-rpc method, generated by chainlib.eth.sign.sign_transaction.
:param chain_spec: Chain spec to use for EIP155 signature.
:type chain_spec: chainlib.chain.ChainSpec
:param doer: Signer rpc backend
:type doer: chainlib.connection.RPCConnection implementing json-rpc
:param tx: Transaction object
:type tx: dict
:rtype: bytes
:returns: Ethereum signature
"""
txs = tx.serialize()
logg.debug('serializing {}'.format(txs))
# TODO: because some rpc servers may fail when chainId is included, we are forced to spend cpu here on this
@ -110,27 +162,66 @@ def sign_transaction_to_rlp(chain_spec, doer, tx):
def sign_message(doer, msg):
"""Sign arbitrary data using the Ethereum message signer protocol.
:param doer: Signer rpc backend
:type doer: chainlib.connection.RPCConnection with json-rpc
:param msg: Message to sign, in hex
:type msg: str
:rtype: str
:returns: Signature, hex
"""
o = sign_message(msg)
return doer(o)
class EthUnixSignerConnection(EthUnixConnection):
"""Connects rpc signer methods to Unix socket connection interface
"""
def sign_transaction_to_rlp(self, tx):
"""Sign transaction using unix socket rpc.
:param tx: Transaction object
:type tx: dict
:rtype: See chainlin.eth.connection.sign_transaction_to_rlp
:returns: See chainlin.eth.connection.sign_transaction_to_rlp
"""
return sign_transaction_to_rlp(self.chain_spec, self.do, tx)
def sign_message(self, tx):
return sign_message(self.do, tx)
def sign_message(self, msg):
"""Sign message using unix socket json-rpc.
:param msg: Message to sign, in hex
:type msg: str
:rtype: See chainlin.eth.connection.sign_message
:returns: See chainlin.eth.connection.sign_message
"""
return sign_message(self.do, msg)
class EthHTTPSignerConnection(EthHTTPConnection):
def sign_transaction_to_rlp(self, tx):
"""Sign transaction using http json-rpc.
:param tx: Transaction object
:type tx: dict
:rtype: See chainlin.eth.connection.sign_transaction_to_rlp
:returns: See chainlin.eth.connection.sign_transaction_to_rlp
"""
return sign_transaction_to_rlp(self.chain_spec, self.do, tx)
def sign_message(self, tx):
"""Sign message using http json-rpc.
:param msg: Message to sign, in hex
:type msg: str
:rtype: See chainlin.eth.connection.sign_message
:returns: See chainlin.eth.connection.sign_message
"""
return sign_message(self.do, tx)

View File

@ -2,4 +2,5 @@ ZERO_ADDRESS = '0x{:040x}'.format(0)
ZERO_CONTENT = '0x{:064x}'.format(0)
MINIMUM_FEE_UNITS = 21000
MINIMUM_FEE_PRICE = 1000000000
DEFAULT_FEE_LIMIT = 8000000
MAX_UINT = int('0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff', 16)

View File

@ -15,14 +15,14 @@ from chainlib.block import BlockSpec
from chainlib.jsonrpc import JSONRPCRequest
from .address import to_checksum_address
#logg = logging.getLogger(__name__)
logg = logging.getLogger()
logg = logging.getLogger(__name__)
re_method = r'^[a-zA-Z0-9_]+$'
class ABIContractType(enum.Enum):
"""Data types used by ABI encoders
"""
BYTES32 = 'bytes32'
BYTES4 = 'bytes4'
UINT256 = 'uint256'
@ -36,14 +36,16 @@ dynamic_contract_types = [
class ABIContract:
"""Base class for Ethereum smart contract encoder
"""
def __init__(self):
self.types = []
self.contents = []
class ABIMethodEncoder(ABIContract):
"""Generate ABI method signatures from method signature string.
"""
def __init__(self):
super(ABIMethodEncoder, self).__init__()
self.method_name = None
@ -51,6 +53,12 @@ class ABIMethodEncoder(ABIContract):
def method(self, m):
"""Set method name.
:param m: Method name
:type m: str
:raises ValueError: Invalid method name
"""
if re.match(re_method, m) == None:
raise ValueError('Invalid method {}, must match regular expression {}'.format(re_method))
self.method_name = m
@ -58,12 +66,26 @@ class ABIMethodEncoder(ABIContract):
def get_method(self):
"""Return currently set method signature string.
:rtype: str
:returns: Method signature
"""
if self.method_name == None:
return ''
return '{}({})'.format(self.method_name, ','.join(self.method_contents))
def typ(self, v):
"""Add argument type to argument vector.
Method name must be set before this is called.
:param v: Type to add
:type v: chainlib.eth.contract.ABIContractType
:raises AttributeError: Type set before method name
:raises TypeError: Invalid type
"""
if self.method_name == None:
raise AttributeError('method name must be set before adding types')
if not isinstance(v, ABIContractType):
@ -78,9 +100,16 @@ class ABIMethodEncoder(ABIContract):
class ABIContractDecoder(ABIContract):
"""Decode serialized ABI contract input data to corresponding python primitives.
"""
def typ(self, v):
"""Add type to argument array to parse input against.
:param v: Type
:type v: chainlib.eth.contract.ABIContractType
:raises TypeError: Invalid type
"""
if not isinstance(v, ABIContractType):
raise TypeError('method type not valid; expected {}, got {}'.format(type(ABIContractType).__name__, type(v).__name__))
self.types.append(v.value)
@ -88,32 +117,74 @@ class ABIContractDecoder(ABIContract):
def val(self, v):
"""Add value to value array.
:param v: Value, in hex
:type v: str
"""
self.contents.append(v)
logg.debug('content is now {}'.format(self.contents))
def uint256(self, v):
"""Parse value as uint256.
:param v: Value, in hex
:type v: str
:rtype: int
:returns: Int value
"""
return int(v, 16)
def bytes32(self, v):
"""Parse value as bytes32.
:param v: Value, in hex
:type v: str
:rtype: str
:returns: Value, in hex
"""
return v
def bool(self, v):
"""Parse value as bool.
:param v: Value, in hex
:type v: str
:rtype: bool
:returns: Value
"""
return bool(self.uint256(v))
def boolean(self, v):
"""Alias of chainlib.eth.contract.ABIContractDecoder.bool
"""
return bool(self.uint256(v))
def address(self, v):
"""Parse value as address.
:param v: Value, in hex
:type v: str
:rtype: str
:returns: Value. in hex
"""
a = strip_0x(v)[64-40:]
return to_checksum_address(a)
def string(self, v):
"""Parse value as string.
:param v: Value, in hex
:type v: str
:rtype: str
:returns: Value
"""
s = strip_0x(v)
b = bytes.fromhex(s)
cursor = 0
@ -131,18 +202,23 @@ class ABIContractDecoder(ABIContract):
def decode(self):
"""Apply decoder on value array using argument type array.
:rtype: list
:returns: List of decoded values
"""
r = []
logg.debug('contents {}'.format(self.contents))
for i in range(len(self.types)):
m = getattr(self, self.types[i])
s = self.contents[i]
logg.debug('{} {} {} {} {}'.format(i, m, self.types[i], self.contents[i], s))
#r.append(m(s.hex()))
r.append(m(s))
return r
def get(self):
"""Alias of chainlib.eth.contract.ABIContractDecoder.decode
"""
return self.decode()
@ -151,7 +227,10 @@ class ABIContractDecoder(ABIContract):
class ABIContractLogDecoder(ABIMethodEncoder, ABIContractDecoder):
"""Decoder utils for log entries of an Ethereum network transaction receipt.
Uses chainlib.eth.contract.ABIContractDecoder.decode to render output from template.
"""
def __init__(self):
super(ABIContractLogDecoder, self).__init__()
self.method_name = None
@ -159,20 +238,45 @@ class ABIContractLogDecoder(ABIMethodEncoder, ABIContractDecoder):
def topic(self, event):
"""Set topic to match.
:param event: Topic name
:type event: str
"""
self.method(event)
def get_method_signature(self):
"""Generate topic signature from set topic.
:rtype: str
:returns: Topic signature, in hex
"""
s = self.get_method()
return keccak256_string_to_hex(s)
def typ(self, v):
"""Add type to event argument array.
:param v: Type
:type v: chainlib.eth.contract.ABIContractType
"""
super(ABIContractLogDecoder, self).typ(v)
self.types.append(v.value)
def apply(self, topics, data):
"""Set log entry data to parse.
After set, self.decode can be used to render the output.
:param topics: The topics array of the receipt, list of hex
:type topics: list
:param data: Non-indexed data, in hex
:type data: str
:raises ValueError: Topic of input does not match topic set in parser
"""
t = self.get_method_signature()
if topics[0] != t:
raise ValueError('topic mismatch')
@ -189,6 +293,11 @@ class ABIContractEncoder(ABIMethodEncoder):
def uint256(self, v):
"""Encode value to uint256 and add to input value vector.
:param v: Integer value
:type v: int
"""
v = int(v)
b = v.to_bytes(32, 'big')
self.contents.append(b.hex())
@ -197,28 +306,52 @@ class ABIContractEncoder(ABIMethodEncoder):
def bool(self, v):
"""Alias of chainlib.eth.contract.ABIContractEncoder.boolean.
"""
return self.boolean(v)
def boolean(self, v):
"""Encode value to boolean and add to input value vector.
:param v: Trueish or falsish value
:type v: any
:rtype: See chainlib.eth.contract.ABIContractEncoder.uint256
:returns: See chainlib.eth.contract.ABIContractEncoder.uint256
"""
if bool(v):
return self.uint256(1)
return self.uint256(0)
def address(self, v):
"""Encode value to address and add to input value vector.
:param v: Ethereum address, in hex
:type v: str
"""
self.bytes_fixed(32, v, 20)
self.types.append(ABIContractType.ADDRESS)
self.__log_latest(v)
def bytes32(self, v):
"""Encode value to bytes32 and add to input value vector.
:param v: Bytes, in hex
:type v: str
"""
self.bytes_fixed(32, v)
self.types.append(ABIContractType.BYTES32)
self.__log_latest(v)
def bytes4(self, v):
"""Encode value to bytes4 and add to input value vector.
:param v: Bytes, in hex
:type v: str
"""
self.bytes_fixed(4, v)
self.types.append(ABIContractType.BYTES4)
self.__log_latest(v)
@ -226,6 +359,11 @@ class ABIContractEncoder(ABIMethodEncoder):
def string(self, v):
"""Encode value to string and add to input value vector.
:param v: String input
:type v: str
"""
b = v.encode('utf-8')
l = len(b)
contents = l.to_bytes(32, 'big')
@ -239,6 +377,16 @@ class ABIContractEncoder(ABIMethodEncoder):
def bytes_fixed(self, mx, v, exact=0):
"""Add arbirary length byte data to value vector.
:param mx: Max length of input data.
:type mx: int
:param v: Byte input, hex or bytes
:type v: str | bytes
:param exact: Fail parsing if input does not translate to given byte length.
:type exact: int
:raises ValueError: Input length or input format mismatch.
"""
typ = type(v).__name__
if typ == 'str':
v = strip_0x(v)
@ -260,8 +408,9 @@ class ABIContractEncoder(ABIMethodEncoder):
self.contents.append(v.ljust(64, '0'))
def get_method_signature(self):
"""Return abi encoded signature of currently set method.
"""
s = self.get_method()
if s == '':
return s
@ -269,6 +418,11 @@ class ABIContractEncoder(ABIMethodEncoder):
def get_contents(self):
"""Encode value array.
:rtype: str
:returns: ABI encoded values, in hex
"""
direct_contents = ''
pointer_contents = ''
l = len(self.types)
@ -291,10 +445,19 @@ class ABIContractEncoder(ABIMethodEncoder):
def get(self):
"""Alias of chainlib.eth.contract.ABIContractEncoder.encode
"""
return self.encode()
def encode(self):
"""Encode method and value array.
The data generated by this method is the literal data used as input to contract calls or transactions.
:rtype: str
:returns: ABI encoded contract input data, in hex
"""
m = self.get_method_signature()
c = self.get_contents()
return m + c
@ -306,6 +469,13 @@ class ABIContractEncoder(ABIMethodEncoder):
def abi_decode_single(typ, v):
"""Convenience function to decode a single ABI encoded value against a given type.
:param typ: Type to parse value as
:type typ: chainlib.eth.contract.ABIContractEncoder
:param v: Value to parse, in hex
:type v: str
"""
d = ABIContractDecoder()
d.typ(typ)
d.val(v)
@ -314,6 +484,17 @@ def abi_decode_single(typ, v):
def code(address, block_spec=BlockSpec.LATEST, id_generator=None):
"""Generate json-rpc query to retrieve code stored at an Ethereum address.
:param address: Address to use for query, in hex
:type address: str
:param block_spec: Block height spec
:type block_spec: chainlib.block.BlockSpec
:param id_generator: json-rpc id generator
:type id_generator: chainlib.jsonrpc.JSONRPCIdGenerator
:rtype: dict
:returns: rpc query object
"""
block_height = None
if block_spec == BlockSpec.LATEST:
block_height = 'latest'

View File

@ -0,0 +1,12 @@
[rpc]
http_provider = http://localhost:8545
http_authentication =
http_username =
http_password =
[chain]
spec = evm:ethereum:1
[wallet]
key_file =
passphrase =

View File

@ -1,23 +1,33 @@
# local imports
from chainlib.error import ExecutionError
class EthException(Exception):
"""Base class for all Ethereum related errors.
"""
pass
class RevertEthException(EthException, ExecutionError):
"""Raised when an rpc call or transaction reverts.
"""
pass
class NotFoundEthException(EthException):
"""Raised when rpc query is made against an identifier that is not known by the node.
"""
pass
class RequestMismatchException(EthException):
"""Raised when a request data parser is given unexpected input data.
"""
pass
class DefaultErrorParser:
"""Generate eth specific exception for the default json-rpc query error parser.
"""
def translate(self, error):
return EthException('default parser code {}'.format(error))

View File

@ -1,7 +1,7 @@
# standard imports
import logging
# third-party imports
# external imports
from hexathon import (
add_0x,
strip_0x,
@ -16,6 +16,8 @@ from chainlib.eth.tx import (
TxFormat,
raw,
)
from chainlib.eth.jsonrpc import to_blockheight_param
from chainlib.block import BlockSpec
from chainlib.eth.constant import (
MINIMUM_FEE_UNITS,
)
@ -24,22 +26,48 @@ logg = logging.getLogger(__name__)
def price(id_generator=None):
"""Generate json-rpc query to retrieve current network gas price guess from node.
:param id_generator: json-rpc id generator
:type id_generator: chainlib.connection.JSONRPCIdGenerator
:rtype: dict
:returns: rpc query object
"""
j = JSONRPCRequest(id_generator)
o = j.template()
o['method'] = 'eth_gasPrice'
return j.finalize(o)
def balance(address, id_generator=None):
def balance(address, id_generator=None, height=BlockSpec.LATEST):
"""Generate json-rpc query to retrieve gas balance of address.
:param address: Address to query balance for, in hex
:type address: str
:param id_generator: json-rpc id generator
:type id_generator: chainlib.connection.JSONRPCIdGenerator
:param height: Block height specifier
:type height: chainlib.block.BlockSpec
:rtype: dict
:returns: rpc query object
"""
j = JSONRPCRequest(id_generator)
o = j.template()
o['method'] = 'eth_getBalance'
o['params'].append(address)
o['params'].append('latest')
height = to_blockheight_param(height)
o['params'].append(height)
return j.finalize(o)
def parse_balance(balance):
"""Parse result of chainlib.eth.gas.balance rpc query
:param balance: rpc result value, in hex or int
:type balance: any
:rtype: int
:returns: Balance integer value
"""
try:
r = int(balance, 10)
except ValueError:
@ -48,10 +76,29 @@ def parse_balance(balance):
class Gas(TxFactory):
"""Gas transaction helper.
"""
def create(self, sender_address, recipient_address, value, tx_format=TxFormat.JSONRPC, id_generator=None):
def create(self, sender_address, recipient_address, value, data=None, tx_format=TxFormat.JSONRPC, id_generator=None):
"""Generate json-rpc query to execute gas transaction.
See parent class TxFactory for details on output format and general usage.
:param sender_address: Sender address, in hex
:type sender_address: str
:param recipient_address: Recipient address, in hex
:type recipient_address: str
:param value: Value of transaction, integer decimal value (wei)
:type value: int
:param data: Arbitrary input data, in hex. None means no data (vanilla gas transaction).
:type data: str
:param tx_format: Output format
:type tx_format: chainlib.eth.tx.TxFormat
"""
tx = self.template(sender_address, recipient_address, use_nonce=True)
tx['value'] = value
if data != None:
tx['data'] = data
txe = EIP155Transaction(tx, tx['nonce'], tx['chainId'])
tx_raw = self.signer.sign_transaction_to_rlp(txe)
tx_raw_hex = add_0x(tx_raw.hex())
@ -68,6 +115,17 @@ class Gas(TxFactory):
class RPCGasOracle:
"""JSON-RPC only gas parameter helper.
:param conn: RPC connection
:type conn: chainlib.connection.RPCConnection
:param code_callback: Callback method to evaluate gas usage for method and inputs.
:type code_callback: method taking abi encoded input data as single argument
:param min_price: Override gas price if less than given value
:type min_price: int
:param id_generator: json-rpc id generator
:type id_generator: chainlib.connection.JSONRPCIdGenerator
"""
def __init__(self, conn, code_callback=None, min_price=1, id_generator=None):
self.conn = conn
@ -76,7 +134,20 @@ class RPCGasOracle:
self.id_generator = id_generator
def get_gas(self, code=None):
def get_gas(self, code=None, input_data=None):
"""Retrieve gas parameters from node.
If code is given, the set code callback will be used to estimate gas usage.
If code is not given or code callback is not set, the chainlib.eth.constant.MINIMUM_FEE_UNITS constant will be used. This gas limit will only be enough gas for a gas transaction without input data.
:param code: EVM execution code to evaluate against, in hex
:type code: str
:param input_data: Contract input data, in hex
:type input_data: str
:rtype: tuple
:returns: Gas price in wei, and gas limit in gas units
"""
gas_price = 0
if self.conn != None:
o = price(id_generator=self.id_generator)
@ -93,13 +164,39 @@ class RPCGasOracle:
class RPCPureGasOracle(RPCGasOracle):
"""Convenience constructor for rpc gas oracle without minimum price.
:param conn: RPC connection
:type conn: chainlib.connection.RPCConnection
:param code_callback: Callback method to evaluate gas usage for method and inputs.
:type code_callback: method taking abi encoded input data as single argument
:param id_generator: json-rpc id generator
:type id_generator: chainlib.connection.JSONRPCIdGenerator
"""
def __init__(self, conn, code_callback=None, id_generator=None):
super(RPCPureGasOracle, self).__init__(conn, code_callback=code_callback, min_price=0, id_generator=id_generator)
class OverrideGasOracle(RPCGasOracle):
"""Gas parameter helper that can be selectively overridden.
If both price and limit are set, the conn parameter will not be used.
If either price or limit is set to None, the rpc in the conn value will be used to query the missing value.
If both are None, behaves the same as chainlib.eth.gas.RPCGasOracle.
:param price: Set exact gas price
:type price: int
:param limit: Set exact gas limit
:type limit: int
:param conn: RPC connection for fallback query
:type conn: chainlib.connection.RPCConnection
:param code_callback: Callback method to evaluate gas usage for method and inputs.
:type code_callback: method taking abi encoded input data as single argument
:param id_generator: json-rpc id generator
:type id_generator: chainlib.connection.JSONRPCIdGenerator
"""
def __init__(self, price=None, limit=None, conn=None, code_callback=None, id_generator=None):
self.conn = None
self.code_callback = None
@ -117,6 +214,8 @@ class OverrideGasOracle(RPCGasOracle):
def get_gas(self, code=None):
"""See chainlib.eth.gas.RPCGasOracle.
"""
r = None
fee_units = None
fee_price = None

View File

@ -14,3 +14,31 @@
#106 Timeout Should be used when an action timedout.
#107 Conflict Should be used when an action conflicts with another (ongoing?) action.
# external imports
from hexathon import add_0x
def to_blockheight_param(height):
"""Translate blockheight specifier to Ethereum json-rpc blockheight argument.
:param height: Height argument
:type height: any
:rtype: str
:returns: Argument value
"""
if height == None:
height = 'latest'
elif isinstance(height, str):
try:
height = int(height)
except ValueError:
pass
if isinstance(height, int):
if height == 0:
height = 'latest'
elif height < 0:
height = 'pending'
else:
height = add_0x(int(height).to_bytes(8, 'big').hex())
return height

View File

@ -3,12 +3,18 @@ import sha3
class LogBloom:
"""Helper for Ethereum receipt log bloom filters.
"""
def __init__(self):
self.content = bytearray(256)
def add(self, element):
"""Add topic element to filter.
:param element: Topic element
:type element: bytes
"""
if not isinstance(element, bytes):
raise ValueError('element must be bytes')
h = sha3.keccak_256()

View File

@ -1,4 +1,4 @@
# third-party imports
# external imports
from hexathon import (
add_0x,
strip_0x,
@ -8,17 +8,40 @@ from hexathon import (
from chainlib.jsonrpc import JSONRPCRequest
def nonce(address, id_generator=None):
def nonce(address, confirmed=False, id_generator=None):
"""Generate json-rpc query to retrieve next nonce of address from node.
:param address: Address to retrieve nonce for, in hex
:type address: str
:param id_generator: json-rpc id generator
:type id_generator: chainlib.connection.JSONRPCIdGenerator
:rtype: dict
:returns: rpc query object
"""
j = JSONRPCRequest(id_generator)
o = j.template()
o['method'] = 'eth_getTransactionCount'
o['params'].append(address)
o['params'].append('pending')
if confirmed:
o['params'].append('latest')
else:
o['params'].append('pending')
return j.finalize(o)
class NonceOracle:
def nonce_confirmed(address, id_generator=None):
return nonce(address, confirmed=True, id_generator=id_generator)
class NonceOracle:
"""Base class for the nonce parameter helpers.
:param address: Address to retireve nonce for, in hex
:type address: str
:param id_generator: json-rpc id generator
:type id_generator: chainlib.connection.JSONRPCIdGenerator
"""
def __init__(self, address, id_generator=None):
self.address = address
self.id_generator = id_generator
@ -26,23 +49,45 @@ class NonceOracle:
def get_nonce(self):
"""Load initial nonce value.
"""
raise NotImplementedError('Class must be extended')
def next_nonce(self):
"""Return next nonce value and advance.
:rtype: int
:returns: Next nonce for address.
"""
n = self.nonce
self.nonce += 1
return n
class RPCNonceOracle(NonceOracle):
"""JSON-RPC only nonce parameter helper.
:param address: Address to retireve nonce for, in hex
:type address: str
:param conn: RPC connection
:type conn: chainlib.connection.RPCConnection
:param id_generator: json-rpc id generator
:type id_generator: chainlib.connection.JSONRPCIdGenerator
"""
def __init__(self, address, conn, id_generator=None):
self.conn = conn
super(RPCNonceOracle, self).__init__(address, id_generator=id_generator)
def get_nonce(self):
"""Load and return nonce value from network.
Note! First call to next_nonce after calling get_nonce will return the same value!
:rtype: int
:returns: Initial nonce
"""
o = nonce(self.address, id_generator=self.id_generator)
r = self.conn.do(o)
n = strip_0x(r)
@ -50,14 +95,28 @@ class RPCNonceOracle(NonceOracle):
class OverrideNonceOracle(NonceOracle):
"""Manually set initial nonce value.
def __init__(self, address, nonce):
self.nonce = nonce
super(OverrideNonceOracle, self).__init__(address)
:param address: Address to retireve nonce for, in hex
:type address: str
:param nonce: Nonce value
:type nonce: int
:param id_generator: json-rpc id generator (not used)
:type id_generator: chainlib.connection.JSONRPCIdGenerator
"""
def __init__(self, address, nonce, id_generator=None):
self.initial_nonce = nonce
self.nonce = self.initial_nonce
super(OverrideNonceOracle, self).__init__(address, id_generator=id_generator)
def get_nonce(self):
return self.nonce
"""Returns initial nonce value set at object construction.
:rtype: int
:returns: Initial nonce value.
"""
return self.initial_nonce
DefaultNonceOracle = RPCNonceOracle

View File

@ -17,7 +17,7 @@ from chainlib.connection import (
from chainlib.eth.unittest.ethtester import create_tester_signer
from chainlib.eth.address import to_checksum_address
logg = logging.getLogger() #__name__)
logg = logging.getLogger(__name__)
@pytest.fixture(scope='function')
@ -37,13 +37,6 @@ def call_sender(
eth_accounts,
):
return eth_accounts[0]
#
#
#@pytest.fixture(scope='function')
#def eth_signer(
# init_eth_tester,
# ):
# return init_eth_tester
@pytest.fixture(scope='function')

View File

@ -1,30 +1,19 @@
#!python3
"""Token balance query script
.. moduleauthor:: Louis Holbrook <dev@holbrook.no>
.. pgp:: 0826EDA1702D1E87C6E2875121D2E7BB88C2A746
"""
# SPDX-License-Identifier: GPL-3.0-or-later
# standard imports
import os
import json
import argparse
import logging
# third-party imports
# external imports
from hexathon import (
add_0x,
strip_0x,
even,
)
import sha3
# local imports
from chainlib.eth.address import to_checksum
import chainlib.eth.cli
from chainlib.eth.address import AddressChecksum
from chainlib.jsonrpc import (
jsonrpc_result,
IntSequenceGenerator,
@ -35,53 +24,37 @@ from chainlib.eth.gas import (
balance,
)
from chainlib.chain import ChainSpec
from crypto_dev_signer.eth.signer import ReferenceSigner as EIP155Signer
logging.basicConfig(level=logging.WARNING)
logg = logging.getLogger()
default_eth_provider = os.environ.get('RPC_PROVIDER')
if default_eth_provider == None:
default_eth_provider = os.environ.get('ETH_PROVIDER', 'http://localhost:8545')
script_dir = os.path.dirname(os.path.realpath(__file__))
#config_dir = os.path.join(script_dir, '..', 'data', 'config')
argparser = argparse.ArgumentParser()
argparser.add_argument('-p', '--provider', dest='p', default=default_eth_provider, type=str, help='Web3 provider url (http only)')
argparser.add_argument('-i', '--chain-spec', dest='i', type=str, default='evm:ethereum:1', help='Chain specification string')
argparser.add_argument('-u', '--unsafe', dest='u', action='store_true', help='Auto-convert address to checksum adddress')
argparser.add_argument('--seq', action='store_true', help='Use sequential rpc ids')
argparser.add_argument('-v', action='store_true', help='Be verbose')
argparser.add_argument('-vv', action='store_true', help='Be more verbose')
argparser.add_argument('address', type=str, help='Account address')
arg_flags = chainlib.eth.cli.argflag_std_read
argparser = chainlib.eth.cli.ArgumentParser(arg_flags)
argparser.add_positional('address', type=str, help='Ethereum address of recipient')
args = argparser.parse_args()
#config = chainlib.eth.cli.Config.from_args(args, arg_flags, default_config_dir=config_dir)
config = chainlib.eth.cli.Config.from_args(args, arg_flags)
wallet = chainlib.eth.cli.Wallet()
wallet.from_config(config)
holder_address = args.address
if wallet.get_signer_address() == None and holder_address != None:
holder_address = wallet.from_address(holder_address)
if args.vv:
logg.setLevel(logging.DEBUG)
elif args.v:
logg.setLevel(logging.INFO)
rpc = chainlib.eth.cli.Rpc()
conn = rpc.connect_by_config(config)
rpc_id_generator = None
if args.seq:
rpc_id_generator = IntSequenceGenerator()
auth = None
if os.environ.get('RPC_AUTHENTICATION') == 'basic':
from chainlib.auth import BasicAuth
auth = BasicAuth(os.environ['RPC_USERNAME'], os.environ['RPC_PASSWORD'])
conn = EthHTTPConnection(args.p, auth=auth)
gas_oracle = OverrideGasOracle(conn)
address = to_checksum(args.address)
if not args.u and address != add_0x(args.address):
raise ValueError('invalid checksum address')
chain_spec = ChainSpec.from_chain_str(args.i)
chain_spec = ChainSpec.from_chain_str(config.get('CHAIN_SPEC'))
def main():
r = None
decimals = 18
o = balance(address, id_generator=rpc_id_generator)
o = balance(holder_address, id_generator=rpc.id_generator)
r = conn.do(o)
hx = strip_0x(r)

View File

@ -4,12 +4,13 @@
import sys
import os
import json
import argparse
#import argparse
import logging
import select
# local imports
from chainlib.eth.address import to_checksum
import chainlib.eth.cli
from chainlib.eth.address import AddressChecksum
from chainlib.eth.connection import EthHTTPConnection
from chainlib.eth.tx import count
from chainlib.chain import ChainSpec
@ -21,63 +22,28 @@ from hexathon import add_0x
logging.basicConfig(level=logging.WARNING)
logg = logging.getLogger()
default_eth_provider = os.environ.get('RPC_PROVIDER')
if default_eth_provider == None:
default_eth_provider = os.environ.get('ETH_PROVIDER', 'http://localhost:8545')
script_dir = os.path.dirname(os.path.realpath(__file__))
config_dir = os.path.join(script_dir, '..', 'data', 'config')
def stdin_arg():
h = select.select([sys.stdin], [], [], 0)
if len(h[0]) > 0:
v = h[0][0].read()
return v.rstrip()
return None
argparser = argparse.ArgumentParser()
argparser.add_argument('-p', '--provider', dest='p', default=default_eth_provider, type=str, help='Web3 provider url (http only)')
argparser.add_argument('-i', '--chain-spec', dest='i', type=str, default='evm:ethereum:1', help='Chain specification string')
argparser.add_argument('-y', '--key-file', dest='y', type=str, help='Ethereum keystore file to use for signing')
argparser.add_argument('--env-prefix', default=os.environ.get('CONFINI_ENV_PREFIX'), dest='env_prefix', type=str, help='environment prefix for variables to overwrite configuration')
argparser.add_argument('-u', '--unsafe', dest='u', action='store_true', help='Auto-convert address to checksum adddress')
argparser.add_argument('--seq', action='store_true', help='Use sequential rpc ids')
argparser.add_argument('-v', action='store_true', help='Be verbose')
argparser.add_argument('-vv', action='store_true', help='Be more verbose')
argparser.add_argument('address', nargs='?', type=str, default=stdin_arg(), help='Ethereum address of recipient')
arg_flags = chainlib.eth.cli.argflag_std_read
argparser = chainlib.eth.cli.ArgumentParser(arg_flags)
argparser.add_positional('address', type=str, help='Ethereum address of recipient')
args = argparser.parse_args()
config = chainlib.eth.cli.Config.from_args(args, arg_flags, default_config_dir=config_dir)
if args.address == None:
argparser.error('need first positional argument or value from stdin')
holder_address = args.address
wallet = chainlib.eth.cli.Wallet()
wallet.from_config(config)
if wallet.get_signer_address() == None and holder_address != None:
wallet.from_address(holder_address)
if args.vv:
logg.setLevel(logging.DEBUG)
elif args.v:
logg.setLevel(logging.INFO)
rpc = chainlib.eth.cli.Rpc(wallet=wallet)
conn = rpc.connect_by_config(config)
signer_address = None
keystore = DictKeystore()
if args.y != None:
logg.debug('loading keystore file {}'.format(args.y))
signer_address = keystore.import_keystore_file(args.y, passphrase)
logg.debug('now have key for signer address {}'.format(signer_address))
signer = EIP155Signer(keystore)
rpc_id_generator = None
if args.seq:
rpc_id_generator = IntSequenceGenerator()
auth = None
if os.environ.get('RPC_AUTHENTICATION') == 'basic':
from chainlib.auth import BasicAuth
auth = BasicAuth(os.environ['RPC_USERNAME'], os.environ['RPC_PASSWORD'])
rpc = EthHTTPConnection(args.p, auth=auth)
def main():
recipient = to_checksum(args.address)
if not args.u and recipient != add_0x(args.address):
raise ValueError('invalid checksum address')
o = count(recipient, id_generator=rpc_id_generator)
r = rpc.do(o)
o = count(holder_address, id_generator=rpc.id_generator)
r = conn.do(o)
count_result = None
try:
count_result = int(r, 16)

View File

@ -1,12 +1,3 @@
#!python3
"""Decode raw transaction
.. moduleauthor:: Louis Holbrook <dev@holbrook.no>
.. pgp:: 0826EDA1702D1E87C6E2875121D2E7BB88C2A746
"""
# SPDX-License-Identifier: GPL-3.0-or-later
# standard imports
@ -18,48 +9,29 @@ import logging
import select
# external imports
import chainlib.eth.cli
from chainlib.eth.tx import unpack
from chainlib.chain import ChainSpec
# local imports
from chainlib.eth.runnable.util import decode_for_puny_humans
logging.basicConfig(level=logging.WARNING)
logg = logging.getLogger()
def stdin_arg(t=0):
h = select.select([sys.stdin], [], [], t)
if len(h[0]) > 0:
v = h[0][0].read()
return v.rstrip()
return None
script_dir = os.path.dirname(os.path.realpath(__file__))
config_dir = os.path.join(script_dir, '..', 'data', 'config')
argparser = argparse.ArgumentParser()
argparser.add_argument('-i', '--chain-id', dest='i', default='evm:ethereum:1', type=str, help='Numeric network id')
argparser.add_argument('-v', action='store_true', help='Be verbose')
argparser.add_argument('-vv', action='store_true', help='Be more verbose')
argparser.add_argument('tx', type=str, nargs='?', default=stdin_arg(), help='hex-encoded signed raw transaction')
arg_flags = chainlib.eth.cli.Flag.VERBOSE | chainlib.eth.cli.Flag.CHAIN_SPEC | chainlib.eth.cli.Flag.ENV_PREFIX | chainlib.eth.cli.Flag.RAW
argparser = chainlib.eth.cli.ArgumentParser(arg_flags)
argparser.add_positional('tx_data', type=str, help='Transaction data to decode')
args = argparser.parse_args()
config = chainlib.eth.cli.Config.from_args(args, arg_flags, default_config_dir=config_dir)
if args.vv:
logg.setLevel(logging.DEBUG)
elif args.v:
logg.setLevel(logging.INFO)
argp = args.tx
logg.debug('txxxx {}'.format(args.tx))
if argp == None:
argp = stdin_arg(t=3)
if argp == None:
argparser.error('need first positional argument or value from stdin')
chain_spec = ChainSpec.from_chain_str(args.i)
chain_spec = ChainSpec.from_chain_str(config.get('CHAIN_SPEC'))
def main():
tx_raw = argp
decode_for_puny_humans(tx_raw, chain_spec, sys.stdout)
decode_for_puny_humans(args.tx_data, chain_spec, sys.stdout)
if __name__ == '__main__':
main()

View File

@ -1,12 +1,3 @@
#!python3
"""Gas transfer script
.. moduleauthor:: Louis Holbrook <dev@holbrook.no>
.. pgp:: 0826EDA1702D1E87C6E2875121D2E7BB88C2A746
"""
# SPDX-License-Identifier: GPL-3.0-or-later
# standard imports
@ -19,114 +10,53 @@ import logging
import urllib
# external imports
from crypto_dev_signer.eth.signer import ReferenceSigner as EIP155Signer
from crypto_dev_signer.keystore.dict import DictKeystore
from hexathon import (
add_0x,
strip_0x,
)
# local imports
from chainlib.eth.address import to_checksum
from chainlib.eth.address import to_checksum_address
from chainlib.eth.connection import EthHTTPConnection
from chainlib.jsonrpc import (
JSONRPCRequest,
IntSequenceGenerator,
)
from chainlib.eth.nonce import (
RPCNonceOracle,
OverrideNonceOracle,
)
from chainlib.eth.gas import (
RPCGasOracle,
OverrideGasOracle,
Gas,
)
from chainlib.eth.gas import Gas
from chainlib.eth.gas import balance as gas_balance
from chainlib.chain import ChainSpec
from chainlib.eth.runnable.util import decode_for_puny_humans
import chainlib.eth.cli
logging.basicConfig(level=logging.WARNING)
logg = logging.getLogger()
default_eth_provider = os.environ.get('RPC_PROVIDER')
if default_eth_provider == None:
default_eth_provider = os.environ.get('ETH_PROVIDER', 'http://localhost:8545')
argparser = argparse.ArgumentParser()
argparser.add_argument('-p', '--provider', dest='p', default=default_eth_provider, type=str, help='Web3 provider url (http only)')
argparser.add_argument('-w', action='store_true', help='Wait for the last transaction to be confirmed')
argparser.add_argument('-ww', action='store_true', help='Wait for every transaction to be confirmed')
argparser.add_argument('-i', '--chain-spec', dest='i', type=str, default='evm:ethereum:1', help='Chain specification string')
argparser.add_argument('-y', '--key-file', dest='y', type=str, help='Ethereum keystore file to use for signing')
argparser.add_argument('--env-prefix', default=os.environ.get('CONFINI_ENV_PREFIX'), dest='env_prefix', type=str, help='environment prefix for variables to overwrite configuration')
argparser.add_argument('--nonce', type=int, help='override nonce')
argparser.add_argument('--gas-price', dest='gas_price', type=int, help='override gas price')
argparser.add_argument('--gas-limit', dest='gas_limit', type=int, help='override gas limit')
argparser.add_argument('-u', '--unsafe', dest='u', action='store_true', help='Auto-convert address to checksum adddress')
argparser.add_argument('--seq', action='store_true', help='Use sequential rpc ids')
argparser.add_argument('-v', action='store_true', help='Be verbose')
argparser.add_argument('-vv', action='store_true', help='Be more verbose')
argparser.add_argument('-s', '--send', dest='s', action='store_true', help='Send to network')
argparser.add_argument('recipient', type=str, help='ethereum address of recipient')
argparser.add_argument('amount', type=int, help='gas value in wei')
arg_flags = chainlib.eth.cli.argflag_std_write | chainlib.eth.cli.Flag.WALLET
argparser = chainlib.eth.cli.ArgumentParser(arg_flags)
argparser.add_argument('--data', type=str, help='Transaction data')
argparser.add_positional('amount', type=int, help='Token amount to send')
args = argparser.parse_args()
if args.vv:
logg.setLevel(logging.DEBUG)
elif args.v:
logg.setLevel(logging.INFO)
extra_args = {
'data': None,
'amount': None,
}
#config = chainlib.eth.cli.Config.from_args(args, arg_flags, extra_args=extra_args, default_config_dir=config_dir)
config = chainlib.eth.cli.Config.from_args(args, arg_flags, extra_args=extra_args)
block_all = args.ww
block_last = args.w or block_all
passphrase_env = 'ETH_PASSPHRASE'
if args.env_prefix != None:
passphrase_env = args.env_prefix + '_' + passphrase_env
passphrase = os.environ.get(passphrase_env)
if passphrase == None:
logg.warning('no passphrase given')
passphrase=''
wallet = chainlib.eth.cli.Wallet()
wallet.from_config(config)
signer_address = None
keystore = DictKeystore()
if args.y != None:
logg.debug('loading keystore file {}'.format(args.y))
signer_address = keystore.import_keystore_file(args.y, password=passphrase)
logg.debug('now have key for signer address {}'.format(signer_address))
signer = EIP155Signer(keystore)
rpc = chainlib.eth.cli.Rpc(wallet=wallet)
conn = rpc.connect_by_config(config)
rpc_id_generator = None
if args.seq:
rpc_id_generator = IntSequenceGenerator()
chain_spec = ChainSpec.from_chain_str(config.get('CHAIN_SPEC'))
auth = None
if os.environ.get('RPC_AUTHENTICATION') == 'basic':
from chainlib.auth import BasicAuth
auth = BasicAuth(os.environ['RPC_USERNAME'], os.environ['RPC_PASSWORD'])
conn = EthHTTPConnection(args.p, auth=auth)
value = config.get('_AMOUNT')
nonce_oracle = None
if args.nonce != None:
nonce_oracle = OverrideNonceOracle(signer_address, args.nonce, id_generator=rpc_id_generator)
else:
nonce_oracle = RPCNonceOracle(signer_address, conn, id_generator=rpc_id_generator)
gas_oracle = None
if args.gas_price or args.gas_limit != None:
gas_oracle = OverrideGasOracle(price=args.gas_price, limit=args.gas_limit, conn=conn, id_generator=rpc_id_generator)
else:
gas_oracle = RPCGasOracle(conn, id_generator=rpc_id_generator)
chain_spec = ChainSpec.from_chain_str(args.i)
value = args.amount
send = args.s
g = Gas(chain_spec, signer=signer, gas_oracle=gas_oracle, nonce_oracle=nonce_oracle)
send = config.true('_RPC_SEND')
def balance(address, id_generator):
@ -137,29 +67,34 @@ def balance(address, id_generator):
def main():
recipient = to_checksum(args.recipient)
if not args.u and recipient != add_0x(args.recipient):
signer = rpc.get_signer()
signer_address = rpc.get_sender_address()
g = Gas(chain_spec, signer=signer, gas_oracle=rpc.get_gas_oracle(), nonce_oracle=rpc.get_nonce_oracle())
recipient = to_checksum_address(config.get('_RECIPIENT'))
if not config.true('_UNSAFE') and recipient != add_0x(config.get('_RECIPIENT')):
raise ValueError('invalid checksum address')
logg.info('gas transfer from {} to {} value {}'.format(signer_address, recipient, value))
if logg.isEnabledFor(logging.DEBUG):
try:
sender_balance = balance(signer_address, rpc_id_generator)
recipient_balance = balance(recipient, rpc_id_generator)
sender_balance = balance(signer_address, rpc.id_generator)
recipient_balance = balance(recipient, rpc.id_generator)
logg.debug('sender {} balance before: {}'.format(signer_address, sender_balance))
logg.debug('recipient {} balance before: {}'.format(recipient, recipient_balance))
except urllib.error.URLError:
pass
(tx_hash_hex, o) = g.create(signer_address, recipient, value, id_generator=rpc_id_generator)
(tx_hash_hex, o) = g.create(signer_address, recipient, value, data=config.get('_DATA'), id_generator=rpc.id_generator)
if send:
conn.do(o)
if block_last:
r = conn.wait(tx_hash_hex)
if logg.isEnabledFor(logging.DEBUG):
sender_balance = balance(signer_address, rpc_id_generator)
recipient_balance = balance(recipient, rpc_id_generator)
sender_balance = balance(signer_address, rpc.id_generator)
recipient_balance = balance(recipient, rpc.id_generator)
logg.debug('sender {} balance after: {}'.format(signer_address, sender_balance))
logg.debug('recipient {} balance after: {}'.format(recipient, recipient_balance))
if r['status'] == 0:
@ -167,12 +102,13 @@ def main():
sys.exit(1)
print(tx_hash_hex)
else:
if logg.isEnabledFor(logging.INFO):
#if logg.isEnabledFor(logging.INFO):
if config.true('_RAW'):
print(o['params'][0])
else:
io_str = io.StringIO()
decode_for_puny_humans(o['params'][0], chain_spec, io_str)
print(io_str.getvalue())
else:
print(o['params'][0])

View File

@ -1,12 +1,3 @@
#!python3
"""Data retrieval script
.. moduleauthor:: Louis Holbrook <dev@holbrook.no>
.. pgp:: 0826EDA1702D1E87C6E2875121D2E7BB88C2A746
"""
# SPDX-License-Identifier: GPL-3.0-or-later
# standard imports
@ -19,80 +10,56 @@ import enum
import select
# external imports
from potaahto.symbols import snake_and_camel
from hexathon import (
add_0x,
strip_0x,
)
import sha3
# local imports
from chainlib.eth.address import to_checksum
from chainlib.jsonrpc import (
JSONRPCRequest,
jsonrpc_result,
IntSequenceGenerator,
)
from chainlib.chain import ChainSpec
from chainlib.status import Status
# local imports
from chainlib.eth.connection import EthHTTPConnection
from chainlib.eth.tx import (
Tx,
pack,
)
from chainlib.eth.address import to_checksum_address
from chainlib.eth.block import Block
from chainlib.chain import ChainSpec
from chainlib.status import Status
from chainlib.eth.address import (
to_checksum_address,
is_checksum_address,
)
from chainlib.eth.block import (
Block,
block_by_hash,
)
from chainlib.eth.runnable.util import decode_for_puny_humans
from chainlib.eth.jsonrpc import to_blockheight_param
import chainlib.eth.cli
logging.basicConfig(level=logging.WARNING, format='%(asctime)s %(levelname)s %(filename)s:%(lineno)d %(message)s')
logg = logging.getLogger()
default_eth_provider = os.environ.get('RPC_PROVIDER')
if default_eth_provider == None:
default_eth_provider = os.environ.get('ETH_PROVIDER', 'http://localhost:8545')
script_dir = os.path.dirname(os.path.realpath(__file__))
config_dir = os.path.join(script_dir, '..', 'data', 'config')
def stdin_arg(t=0):
h = select.select([sys.stdin], [], [], t)
if len(h[0]) > 0:
v = h[0][0].read()
return v.rstrip()
return None
argparser = argparse.ArgumentParser('eth-get', description='display information about an Ethereum address or transaction', epilog='address/transaction can be provided as an argument or from standard input')
argparser.add_argument('-p', '--provider', dest='p', default=default_eth_provider, type=str, help='Web3 provider url (http only)')
argparser.add_argument('-i', '--chain-spec', dest='i', type=str, default='evm:ethereum:1', help='Chain specification string')
argparser.add_argument('--rlp', action='store_true', help='Display transaction as raw rlp')
argparser.add_argument('--seq', action='store_true', help='Use sequential rpc ids')
argparser.add_argument('-u', '--unsafe', dest='u', action='store_true', help='Auto-convert address to checksum adddress')
argparser.add_argument('-v', action='store_true', help='Be verbose')
argparser.add_argument('-vv', action='store_true', help='Be more verbose')
argparser.add_argument('item', nargs='?', default=stdin_arg(), type=str, help='Item to get information for (address og transaction)')
arg_flags = chainlib.eth.cli.argflag_std_read
argparser = chainlib.eth.cli.ArgumentParser(arg_flags)
argparser.add_positional('item', type=str, help='Address or transaction to retrieve data for')
args = argparser.parse_args()
config = chainlib.eth.cli.Config.from_args(args, arg_flags, default_config_dir=config_dir)
if args.vv:
logg.setLevel(logging.DEBUG)
elif args.v:
logg.setLevel(logging.INFO)
rpc = chainlib.eth.cli.Rpc()
conn = rpc.connect_by_config(config)
argp = args.item
if argp == None:
argp = stdin_arg(None)
if argsp == None:
argparser.error('need first positional argument or value from stdin')
rpc_id_generator = None
if args.seq:
rpc_id_generator = IntSequenceGenerator()
auth = None
if os.environ.get('RPC_AUTHENTICATION') == 'basic':
from chainlib.auth import BasicAuth
auth = BasicAuth(os.environ['RPC_USERNAME'], os.environ['RPC_PASSWORD'])
conn = EthHTTPConnection(args.p, auth=auth)
chain_spec = ChainSpec.from_chain_str(args.i)
chain_spec = ChainSpec.from_chain_str(config.get('CHAIN_SPEC'))
item = add_0x(args.item)
as_rlp = bool(args.rlp)
def get_transaction(conn, tx_hash, id_generator):
@ -106,7 +73,7 @@ def get_transaction(conn, tx_hash, id_generator):
logg.error('Transaction {} not found'.format(tx_hash))
sys.exit(1)
if as_rlp:
if config.true('_RAW'):
tx_src = Tx.src_normalize(tx_src)
return pack(tx_src, chain_spec).hex()
@ -125,16 +92,24 @@ def get_transaction(conn, tx_hash, id_generator):
tx = Tx(tx_src)
if rcpt != None:
tx.apply_receipt(rcpt)
rcpt = snake_and_camel(rcpt)
o = block_by_hash(rcpt['block_hash'])
r = conn.do(o)
block = Block(r)
tx.apply_block(block)
logg.debug('foo {}'.format(tx_src))
tx.generate_wire(chain_spec)
return tx
def get_address(conn, address, id_generator):
def get_address(conn, address, id_generator, height):
j = JSONRPCRequest(id_generator=id_generator)
o = j.template()
o['method'] = 'eth_getCode'
o['params'].append(address)
o['params'].append('latest')
height = to_blockheight_param(height)
o['params'].append(height)
o = j.finalize(o)
code = conn.do(o)
@ -146,11 +121,18 @@ def get_address(conn, address, id_generator):
def main():
address = item
r = None
if len(item) > 42:
r = get_transaction(conn, item, rpc_id_generator).to_human()
elif args.u or to_checksum_address(item):
r = get_address(conn, item, rpc_id_generator)
if len(address) > 42:
r = get_transaction(conn, address, rpc.id_generator)
if not config.true('_RAW'):
r = r.to_human()
else:
if config.get('_UNSAFE'):
address = to_checksum_address(address)
elif not is_checksum_address(address):
raise ValueError('invalid checksum address: {}'.format(address))
r = get_address(conn, address, rpc.id_generator, config.get('_HEIGHT'))
print(r)

View File

@ -1,12 +1,3 @@
#!python3
"""Token balance query script
.. moduleauthor:: Louis Holbrook <dev@holbrook.no>
.. pgp:: 0826EDA1702D1E87C6E2875121D2E7BB88C2A746
"""
# SPDX-License-Identifier: GPL-3.0-or-later
# standard imports
@ -17,19 +8,18 @@ import json
import argparse
import logging
# third-party imports
# external imports
from chainlib.chain import ChainSpec
from hexathon import (
add_0x,
strip_0x,
even,
)
import sha3
from crypto_dev_signer.eth.signer import ReferenceSigner as EIP155Signer
# local imports
from chainlib.eth.address import (
to_checksum_address,
is_checksum_address,
)
from chainlib.eth.address import AddressChecksum
from chainlib.eth.chain import network_id
from chainlib.eth.block import (
block_latest,
@ -43,79 +33,49 @@ from chainlib.eth.gas import (
balance,
price,
)
from chainlib.jsonrpc import (
IntSequenceGenerator,
)
from chainlib.chain import ChainSpec
import chainlib.eth.cli
BLOCK_SAMPLES = 10
logging.basicConfig(level=logging.WARNING)
logg = logging.getLogger()
default_eth_provider = os.environ.get('RPC_PROVIDER')
if default_eth_provider == None:
default_eth_provider = os.environ.get('ETH_PROVIDER', 'http://localhost:8545')
script_dir = os.path.dirname(os.path.realpath(__file__))
config_dir = os.path.join(script_dir, '..', 'data', 'config')
argparser = argparse.ArgumentParser()
argparser.add_argument('-p', '--provider', dest='p', default=default_eth_provider, type=str, help='Web3 provider url (http only)')
argparser.add_argument('-i', '--chain-spec', dest='i', type=str, default='evm:ethereum:1', help='Chain specification string')
argparser.add_argument('-H', '--human', dest='human', action='store_true', help='Use human-friendly formatting')
argparser.add_argument('-u', '--unsafe', dest='u', action='store_true', help='Auto-convert address to checksum adddress')
argparser.add_argument('-l', '--long', dest='l', action='store_true', help='Calculate averages through sampling of blocks and txs')
argparser.add_argument('-v', action='store_true', help='Be verbose')
argparser.add_argument('--seq', action='store_true', help='Use sequential rpc ids')
argparser.add_argument('-vv', action='store_true', help='Be more verbose')
argparser.add_argument('-y', '--key-file', dest='y', type=str, help='Include summary for keyfile')
argparser.add_argument('address', nargs='?', type=str, help='Include summary for address (conflicts with -y)')
arg_flags = chainlib.eth.cli.argflag_std_read
argparser = chainlib.eth.cli.ArgumentParser(arg_flags)
argparser.add_positional('address', type=str, help='Address to retrieve info for', required=False)
argparser.add_argument('--long', action='store_true', help='Calculate averages through sampling of blocks and txs')
args = argparser.parse_args()
config = chainlib.eth.cli.Config.from_args(args, arg_flags, extra_args={'long': None}, default_config_dir=config_dir)
if args.vv:
logg.setLevel(logging.DEBUG)
elif args.v:
logg.setLevel(logging.INFO)
holder_address = args.address
wallet = chainlib.eth.cli.Wallet()
wallet.from_config(config)
if wallet.get_signer_address() == None and holder_address != None:
wallet.from_address(holder_address)
signer = None
holder_address = None
if args.address != None:
if not args.u and not is_checksum_address(args.address):
raise ValueError('invalid checksum address {}'.format(args.address))
holder_address = add_0x(args.address)
elif args.y != None:
f = open(args.y, 'r')
o = json.load(f)
f.close()
holder_address = add_0x(to_checksum_address(o['address']))
rpc_id_generator = None
if args.seq:
rpc_id_generator = IntSequenceGenerator()
auth = None
if os.environ.get('RPC_AUTHENTICATION') == 'basic':
from chainlib.auth import BasicAuth
auth = BasicAuth(os.environ['RPC_USERNAME'], os.environ['RPC_PASSWORD'])
conn = EthHTTPConnection(args.p, auth=auth)
gas_oracle = OverrideGasOracle(conn)
rpc = chainlib.eth.cli.Rpc(wallet=wallet)
conn = rpc.connect_by_config(config)
token_symbol = 'eth'
chain_spec = ChainSpec.from_chain_str(args.i)
chain_spec = ChainSpec.from_chain_str(config.get('CHAIN_SPEC'))
human = args.human
human = not config.true('_RAW')
longmode = args.l
longmode = config.true('_LONG')
def main():
o = network_id(id_generator=rpc_id_generator)
o = network_id(id_generator=rpc.id_generator)
r = conn.do(o)
#if human:
# n = format(n, ',')
sys.stdout.write('Network id: {}\n'.format(r))
o = block_latest(id_generator=rpc_id_generator)
o = block_latest(id_generator=rpc.id_generator)
r = conn.do(o)
n = int(r, 16)
first_block_number = n
@ -123,7 +83,7 @@ def main():
n = format(n, ',')
sys.stdout.write('Block: {}\n'.format(n))
o = block_by_number(first_block_number, False, id_generator=rpc_id_generator)
o = block_by_number(first_block_number, False, id_generator=rpc.id_generator)
r = conn.do(o)
last_block = Block(r)
last_timestamp = last_block.timestamp
@ -132,7 +92,7 @@ def main():
aggr_time = 0.0
aggr_gas = 0
for i in range(BLOCK_SAMPLES):
o = block_by_number(first_block_number-i, False, id_generator=rpc_id_generator)
o = block_by_number(first_block_number-i, False, id_generator=rpc.id_generator)
r = conn.do(o)
block = Block(r)
aggr_time += last_block.timestamp - block.timestamp
@ -150,7 +110,7 @@ def main():
sys.stdout.write('Gaslimit: {}\n'.format(n))
sys.stdout.write('Blocktime: {}\n'.format(aggr_time / BLOCK_SAMPLES))
o = price(id_generator=rpc_id_generator)
o = price(id_generator=rpc.id_generator)
r = conn.do(o)
n = int(r, 16)
if human:

View File

@ -1,12 +1,3 @@
#!python3
"""Gas transfer script
.. moduleauthor:: Louis Holbrook <dev@holbrook.no>
.. pgp:: 0826EDA1702D1E87C6E2875121D2E7BB88C2A746
"""
# SPDX-License-Identifier: GPL-3.0-or-later
# standard imports
@ -19,6 +10,7 @@ import logging
import urllib
# external imports
import chainlib.eth.cli
from crypto_dev_signer.eth.signer import ReferenceSigner as EIP155Signer
from crypto_dev_signer.keystore.dict import DictKeystore
from hexathon import (
@ -45,129 +37,88 @@ from chainlib.eth.tx import (
TxFactory,
raw,
)
from chainlib.error import SignerMissingException
from chainlib.chain import ChainSpec
from chainlib.eth.runnable.util import decode_for_puny_humans
from chainlib.eth.jsonrpc import to_blockheight_param
logging.basicConfig(level=logging.WARNING)
logg = logging.getLogger()
default_eth_provider = os.environ.get('RPC_PROVIDER')
if default_eth_provider == None:
default_eth_provider = os.environ.get('ETH_PROVIDER', 'http://localhost:8545')
script_dir = os.path.dirname(os.path.realpath(__file__))
config_dir = os.path.join(script_dir, '..', 'data', 'config')
argparser = argparse.ArgumentParser()
argparser.add_argument('-p', '--provider', dest='p', default=default_eth_provider, type=str, help='Web3 provider url (http only)')
argparser.add_argument('-w', action='store_true', help='Wait for the last transaction to be confirmed')
argparser.add_argument('-ww', action='store_true', help='Wait for every transaction to be confirmed')
argparser.add_argument('-i', '--chain-spec', dest='i', type=str, default='evm:ethereum:1', help='Chain specification string')
argparser.add_argument('-y', '--key-file', dest='y', type=str, help='Ethereum keystore file to use for signing')
argparser.add_argument('-u', '--unsafe', dest='u', action='store_true', help='Auto-convert address to checksum adddress')
argparser.add_argument('--env-prefix', default=os.environ.get('CONFINI_ENV_PREFIX'), dest='env_prefix', type=str, help='environment prefix for variables to overwrite configuration')
argparser.add_argument('--nonce', type=int, help='override nonce')
argparser.add_argument('--gas-price', dest='gas_price', type=int, help='override gas price')
argparser.add_argument('--gas-limit', dest='gas_limit', type=int, help='override gas limit')
argparser.add_argument('-a', '--recipient', dest='a', type=str, help='recipient address (None for contract creation)')
argparser.add_argument('-value', type=int, help='gas value of transaction in wei')
argparser.add_argument('--seq', action='store_true', help='Use sequential rpc ids')
argparser.add_argument('-v', action='store_true', help='Be verbose')
argparser.add_argument('-vv', action='store_true', help='Be more verbose')
argparser.add_argument('-s', '--send', dest='s', action='store_true', help='Send to network')
argparser.add_argument('-l', '--local', dest='l', action='store_true', help='Local contract call')
argparser.add_argument('data', nargs='?', type=str, help='Transaction data')
arg_flags = chainlib.eth.cli.argflag_std_write | chainlib.eth.cli.Flag.EXEC
argparser = chainlib.eth.cli.ArgumentParser(arg_flags)
argparser.add_positional('data', type=str, help='Transaction data')
args = argparser.parse_args()
if args.vv:
logg.setLevel(logging.DEBUG)
elif args.v:
logg.setLevel(logging.INFO)
config = chainlib.eth.cli.Config.from_args(args, arg_flags, default_config_dir=config_dir)
block_all = args.ww
block_last = args.w or block_all
passphrase_env = 'ETH_PASSPHRASE'
if args.env_prefix != None:
passphrase_env = args.env_prefix + '_' + passphrase_env
passphrase = os.environ.get(passphrase_env)
if passphrase == None:
logg.warning('no passphrase given')
passphrase=''
wallet = chainlib.eth.cli.Wallet(EIP155Signer)
wallet.from_config(config)
signer_address = None
keystore = DictKeystore()
if args.y != None:
logg.debug('loading keystore file {}'.format(args.y))
signer_address = keystore.import_keystore_file(args.y, password=passphrase)
logg.debug('now have key for signer address {}'.format(signer_address))
signer = EIP155Signer(keystore)
rpc = chainlib.eth.cli.Rpc(wallet=wallet)
conn = rpc.connect_by_config(config)
rpc_id_generator = None
if args.seq:
rpc_id_generator = IntSequenceGenerator()
send = config.true('_RPC_SEND')
auth = None
if os.environ.get('RPC_AUTHENTICATION') == 'basic':
from chainlib.auth import BasicAuth
auth = BasicAuth(os.environ['RPC_USERNAME'], os.environ['RPC_PASSWORD'])
conn = EthHTTPConnection(args.p, auth=auth)
send = args.s
local = args.l
if local:
if config.get('_EXEC_ADDRESS') != None:
send = False
nonce_oracle = None
gas_oracle = None
if signer_address != None and not local:
if args.nonce != None:
nonce_oracle = OverrideNonceOracle(signer_address, args.nonce)
else:
nonce_oracle = RPCNonceOracle(signer_address, conn)
if args.gas_price or args.gas_limit != None:
gas_oracle = OverrideGasOracle(price=args.gas_price, limit=args.gas_limit, conn=conn, id_generator=rpc_id_generator)
else:
gas_oracle = RPCGasOracle(conn, id_generator=rpc_id_generator)
chain_spec = ChainSpec.from_chain_str(args.i)
value = args.value
g = TxFactory(chain_spec, signer=signer, gas_oracle=gas_oracle, nonce_oracle=nonce_oracle)
chain_spec = None
try:
chain_spec = ChainSpec.from_chain_str(config.get('CHAIN_SPEC'))
except AttributeError:
pass
def main():
recipient = None
if args.a != None:
recipient = add_0x(to_checksum(args.a))
if not args.u and recipient != add_0x(recipient):
signer_address = None
try:
signer = rpc.get_signer()
signer_address = rpc.get_signer_address()
except SignerMissingException:
pass
if config.get('_EXEC_ADDRESS') != None:
exec_address = add_0x(to_checksum(config.get('_EXEC_ADDRESS')))
if not args.u and exec_address != add_0x(exec_address):
raise ValueError('invalid checksum address')
if local:
j = JSONRPCRequest(id_generator=rpc_id_generator)
j = JSONRPCRequest(id_generator=rpc.id_generator)
o = j.template()
o['method'] = 'eth_call'
o['params'].append({
'to': recipient,
'to': exec_address,
'from': signer_address,
'value': '0x00',
'gas': add_0x(int.to_bytes(8000000, 8, byteorder='big').hex()), # TODO: better get of network gas limit
'gasPrice': '0x01',
'data': add_0x(args.data),
})
o['params'].append('latest')
height = to_blockheight_param(config.get('_HEIGHT'))
o['params'].append(height)
o = j.finalize(o)
r = conn.do(o)
print(strip_0x(r))
try:
print(strip_0x(r))
except ValueError:
sys.stderr.write('query returned an empty value\n')
sys.exit(1)
return
elif signer_address != None:
if chain_spec == None:
raise ValueError('chain spec must be specified')
g = TxFactory(chain_spec, signer=rpc.get_signer(), gas_oracle=rpc.get_gas_oracle(), nonce_oracle=rpc.get_nonce_oracle())
tx = g.template(signer_address, recipient, use_nonce=True)
if args.data != None:
tx = g.set_code(tx, add_0x(args.data))
(tx_hash_hex, o) = g.finalize(tx, id_generator=rpc_id_generator)
(tx_hash_hex, o) = g.finalize(tx, id_generator=rpc.id_generator)
if send:
r = conn.do(o)
@ -177,7 +128,7 @@ def main():
print(tx_hash_hex)
else:
o = raw(args.data, id_generator=rpc_id_generator)
o = raw(args.data, id_generator=rpc.id_generator)
if send:
r = conn.do(o)
print(r)

View File

@ -3,6 +3,17 @@ from chainlib.jsonrpc import JSONRPCRequest
def new_account(passphrase='', id_generator=None):
"""Generate json-rpc query to create new account in keystore.
Uses the personal_newAccount rpc call.
:param passphrase: Passphrase string
:type passphrase: str
:param id_generator: JSONRPC id generator
:type id_generator: JSONRPCIdGenerator
:rtype: dict
:returns: rpc query object
"""
j = JSONRPCRequest(id_generator)
o = j.template()
o['method'] = 'personal_newAccount'
@ -11,6 +22,17 @@ def new_account(passphrase='', id_generator=None):
def sign_transaction(payload, id_generator=None):
"""Generate json-rpc query to sign transaction using the node keystore.
The node must have the private key corresponding to the from-field in the transaction object.
:param payload: Transaction
:type payload: dict
:param id_generator: JSONRPC id generator
:type id_generator: JSONRPCIdGenerator
:rtype: dict
:returns: rpc query object
"""
j = JSONRPCRequest(id_generator)
o = j.template()
o['method'] = 'eth_signTransaction'
@ -19,6 +41,19 @@ def sign_transaction(payload, id_generator=None):
def sign_message(address, payload, id_generator=None):
"""Generate json-rpc query to sign an arbirary message using the node keystore.
The node must have the private key corresponding to the address parameter.
:param address: Address of key to sign with, in hex
:type address: str
:param payload: Arbirary message, in hex
:type payload: str
:param id_generator: JSONRPC id generator
:type id_generator: JSONRPCIdGenerator
:rtype: dict
:returns: rpc query object
"""
j = JSONRPCRequest(id_generator)
o = j.template()
o['method'] = 'eth_sign'

View File

@ -9,6 +9,7 @@ import sha3
from hexathon import (
strip_0x,
add_0x,
compact,
)
from rlp import decode as rlp_decode
from rlp import encode as rlp_encode
@ -16,25 +17,34 @@ from crypto_dev_signer.eth.transaction import EIP155Transaction
from crypto_dev_signer.encoding import public_key_to_address
from crypto_dev_signer.eth.encoding import chain_id_to_v
from potaahto.symbols import snake_and_camel
# local imports
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.eth.nonce import (
nonce as nonce_query,
nonce_confirmed as nonce_query_confirmed,
)
from chainlib.block import BlockSpec
# local imports
from .address import to_checksum
from .constant import (
MINIMUM_FEE_UNITS,
MINIMUM_FEE_PRICE,
ZERO_ADDRESS,
DEFAULT_FEE_LIMIT,
)
from .contract import ABIContractEncoder
from chainlib.jsonrpc import JSONRPCRequest
from .jsonrpc import to_blockheight_param
logg = logging.getLogger().getChild(__name__)
logg = logging.getLogger(__name__)
class TxFormat(enum.IntEnum):
"""Tx generator output formats
"""
DICT = 0x00
RAW = 0x01
RAW_SIGNED = 0x02
@ -56,24 +66,22 @@ field_debugs = [
's',
]
def count(address, confirmed=False, id_generator=None):
j = JSONRPCRequest(id_generator=id_generator)
o = j.template()
o['method'] = 'eth_getTransactionCount'
o['params'].append(address)
if confirmed:
o['params'].append('latest')
else:
o['params'].append('pending')
return j.finalize(o)
count_pending = count
def count_confirmed(address):
return count(address, True)
count = nonce_query
count_pending = nonce_query
count_confirmed = nonce_query_confirmed
def pack(tx_src, chain_spec):
"""Serialize wire format transaction from transaction representation.
:param tx_src: Transaction source.
:type tx_src: dict
:param chain_spec: Chain spec to calculate EIP155 v value
:type chain_spec: chainlib.chain.ChainSpec
:rtype: bytes
:returns: Serialized transaction
"""
if isinstance(tx_src, Tx):
tx_src = tx_src.as_dict()
tx_src = Tx.src_normalize(tx_src)
@ -96,6 +104,15 @@ def pack(tx_src, chain_spec):
def unpack(tx_raw_bytes, chain_spec):
"""Deserialize wire format transaction to transaction representation.
:param tx_raw_bytes: Serialized transaction
:type tx_raw_bytes: bytes
:param chain_spec: Chain spec to calculate EIP155 v value
:type chain_spec: chainlib.chain.ChainSpec
:rtype: dict
:returns: Transaction representation
"""
chain_id = chain_spec.chain_id()
tx = __unpack_raw(tx_raw_bytes, chain_id)
tx['nonce'] = int.from_bytes(tx['nonce'], 'big')
@ -106,6 +123,15 @@ def unpack(tx_raw_bytes, chain_spec):
def unpack_hex(tx_raw_bytes, chain_spec):
"""Deserialize wire format transaction to transaction representation, using hex values for all numeric value fields.
:param tx_raw_bytes: Serialized transaction
:type tx_raw_bytes: bytes
:param chain_spec: Chain spec to calculate EIP155 v value
:type chain_spec: chainlib.chain.ChainSpec
:rtype: dict
:returns: Transaction representation
"""
chain_id = chain_spec.chain_id()
tx = __unpack_raw(tx_raw_bytes, chain_id)
tx['nonce'] = add_0x(hex(tx['nonce']))
@ -193,6 +219,15 @@ def __unpack_raw(tx_raw_bytes, chain_id=1):
def transaction(hsh, id_generator=None):
"""Generate json-rpc query to retrieve transaction by hash from node.
:param hsh: Transaction hash, in hex
:type hsh: str
:param id_generator: json-rpc id generator
:type id_generator: JSONRPCIdGenerator
:rtype: dict
:returns: rpc query object
"""
j = JSONRPCRequest(id_generator=id_generator)
o = j.template()
o['method'] = 'eth_getTransactionByHash'
@ -201,6 +236,17 @@ def transaction(hsh, id_generator=None):
def transaction_by_block(hsh, idx, id_generator=None):
"""Generate json-rpc query to retrieve transaction by block hash and index.
:param hsh: Block hash, in hex
:type hsh: str
:param idx: Transaction index
:type idx: int
:param id_generator: json-rpc id generator
:type id_generator: JSONRPCIdGenerator
:rtype: dict
:returns: rpc query object
"""
j = JSONRPCRequest(id_generator=id_generator)
o = j.template()
o['method'] = 'eth_getTransactionByBlockHashAndIndex'
@ -210,6 +256,15 @@ def transaction_by_block(hsh, idx, id_generator=None):
def receipt(hsh, id_generator=None):
"""Generate json-rpc query to retrieve transaction receipt by transaction hash from node.
:param hsh: Transaction hash, in hex
:type hsh: str
:param id_generator: json-rpc id generator
:type id_generator: JSONRPCIdGenerator
:rtype: dict
:returns: rpc query object
"""
j = JSONRPCRequest(id_generator=id_generator)
o = j.template()
o['method'] = 'eth_getTransactionReceipt'
@ -218,6 +273,15 @@ def receipt(hsh, id_generator=None):
def raw(tx_raw_hex, id_generator=None):
"""Generator json-rpc query to send raw transaction to node.
:param hsh: Serialized transaction, in hex
:type hsh: str
:param id_generator: json-rpc id generator
:type id_generator: JSONRPCIdGenerator
:rtype: dict
:returns: rpc query object
"""
j = JSONRPCRequest(id_generator=id_generator)
o = j.template()
o['method'] = 'eth_sendRawTransaction'
@ -226,8 +290,23 @@ def raw(tx_raw_hex, id_generator=None):
class TxFactory:
"""Base class for generating and signing transactions or contract calls.
fee = 8000000
For transactions (state changes), a signer, gas oracle and nonce oracle needs to be supplied.
Gas oracle and nonce oracle may in some cases be needed for contract calls, if the node insists on counting gas for read-only operations.
:param chain_spec: Chain spec to use for signer.
:type chain_spec: chainlib.chain.ChainSpec
:param signer: Signer middleware.
:type param: Object implementing interface ofchainlib.eth.connection.sign_transaction_to_rlp.
:param gas_oracle: Backend to generate gas parameters
:type gas_oracle: Object implementing chainlib.eth.gas.GasOracle interface
:param nonce_oracle: Backend to generate gas parameters
:type nonce_oracle: Object implementing chainlib.eth.nonce.NonceOracle interface
"""
fee = DEFAULT_FEE_LIMIT
def __init__(self, chain_spec, signer=None, gas_oracle=None, nonce_oracle=None):
self.gas_oracle = gas_oracle
@ -237,6 +316,15 @@ class TxFactory:
def build_raw(self, tx):
"""Sign transaction data, returning the transaction hash and serialized transaction.
In most cases, chainlib.eth.tx.TxFactory.finalize should be used instead.
:param tx: Transaction representation
:type tx: dict
:rtype: tuple
:returns: Transaction hash (in hex), serialized transaction (in hex)
"""
if tx['to'] == None or tx['to'] == '':
tx['to'] = '0x'
txe = EIP155Transaction(tx, tx['nonce'], tx['chainId'])
@ -247,12 +335,34 @@ class TxFactory:
def build(self, tx, id_generator=None):
"""Sign transaction and wrap in raw transaction json-rpc query.
In most cases, chainlib.eth.tx.TxFactory.finalize should be used instead.
:param tx: Transaction representation
type tx: dict
:param id_generator: JSONRPC id generator
:type id_generator: JSONRPCIdGenerator
:rtype: tuple
:returns: Transaction hash (in hex), raw transaction rpc query object
"""
(tx_hash_hex, tx_raw_hex) = self.build_raw(tx)
o = raw(tx_raw_hex, id_generator=id_generator)
return (tx_hash_hex, o)
def template(self, sender, recipient, use_nonce=False):
"""Generate a base transaction template.
:param sender: Sender address, in hex
:type sender: str
:param receipient: Recipient address, in hex
:type recipient: str
:param use_nonce: Use and advance nonce in nonce generator.
:type use_nonce: bool
:rtype: dict
:returns: Transaction representation.
"""
gas_price = MINIMUM_FEE_PRICE
gas_limit = MINIMUM_FEE_UNITS
if self.gas_oracle != None:
@ -276,18 +386,35 @@ class TxFactory:
def normalize(self, tx):
"""Generate field name redundancies (camel-case, snake-case).
:param tx: Transaction representation
:type tx: dict
:rtype: dict:
:returns: Transaction representation with redudant field names
"""
txe = EIP155Transaction(tx, tx['nonce'], tx['chainId'])
txes = txe.serialize()
return {
'from': tx['from'],
'to': txes['to'],
'gasPrice': txes['gasPrice'],
'gas': txes['gas'],
'gasPrice': '0x' + compact(txes['gasPrice']),
'gas': '0x' + compact(txes['gas']),
'data': txes['data'],
}
def finalize(self, tx, tx_format=TxFormat.JSONRPC, id_generator=None):
"""Sign transaction and for specified output format.
:param tx: Transaction representation
:type tx: dict
:param tx_format: Transaction output format
:type tx_format: chainlib.eth.tx.TxFormat
:raises NotImplementedError: Unknown tx_format value
:rtype: varies
:returns: Transaction output in specified format.
"""
if tx_format == TxFormat.JSONRPC:
return self.build(tx, id_generator=id_generator)
elif tx_format == TxFormat.RLP_SIGNED:
@ -296,6 +423,17 @@ class TxFactory:
def set_code(self, tx, data, update_fee=True):
"""Apply input data to transaction.
:param tx: Transaction representation
:type tx: dict
:param data: Input data to apply, in hex
:type data: str
:param update_fee: Recalculate gas limit based on added input
:type update_fee: bool
:rtype: dict
:returns: Transaction representation
"""
tx['data'] = data
if update_fee:
tx['gas'] = TxFactory.fee
@ -307,6 +445,19 @@ class TxFactory:
def transact_noarg(self, method, contract_address, sender_address, tx_format=TxFormat.JSONRPC):
"""Convenience generator for contract transaction with no arguments.
:param method: Method name
:type method: str
:param contract_address: Contract address to transaction against, in hex
:type contract_address: str
:param sender_address: Transaction sender, in hex
:type sender_address: str
:param tx_format: Transaction output format
:type tx_format: chainlib.eth.tx.TxFormat
:rtype: varies
:returns: Transaction output in selected format
"""
enc = ABIContractEncoder()
enc.method(method)
data = enc.get()
@ -316,7 +467,22 @@ class TxFactory:
return tx
def call_noarg(self, method, contract_address, sender_address=ZERO_ADDRESS, id_generator=None):
def call_noarg(self, method, contract_address, sender_address=ZERO_ADDRESS, height=BlockSpec.LATEST, id_generator=None):
"""Convenience generator for contract (read-only) call with no arguments.
:param method: Method name
:type method: str
:param contract_address: Contract address to transaction against, in hex
:type contract_address: str
:param sender_address: Transaction sender, in hex
:type sender_address: str
:param height: Transaction height specifier
:type height: chainlib.block.BlockSpec
:param id_generator: json-rpc id generator
:type id_generator: JSONRPCIdGenerator
:rtype: varies
:returns: Transaction output in selected format
"""
j = JSONRPCRequest(id_generator)
o = j.template()
o['method'] = 'eth_call'
@ -326,34 +492,38 @@ class TxFactory:
tx = self.template(sender_address, contract_address)
tx = self.set_code(tx, data)
o['params'].append(self.normalize(tx))
o['params'].append('latest')
height = to_blockheight_param(height)
o['params'].append(height)
o = j.finalize(o)
return o
class Tx:
class Tx(BaseTx):
"""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 receipt is applied, the transaction hash in the receipt must match the hash in the transaction data.
:param src: Transaction representation
:type src: dict
:param block: Apply block object in which transaction in mined.
:type block: chainlib.block.Block
:param rcpt: Apply receipt data
:type rcpt: dict
#:todo: force tx type schema parser (whether expect hex or int etc)
#:todo: divide up constructor method
"""
# TODO: force tx type schema parser (whether expect hex or int etc)
def __init__(self, src, block=None, rcpt=None):
self.tx_src = self.src_normalize(src)
self.__rcpt_block_hash = None
src = self.src_normalize(src)
self.index = -1
tx_hash = add_0x(src['hash'])
if block != None:
i = 0
for tx in block.txs:
tx_hash_block = None
try:
tx_hash_block = tx['hash']
except TypeError:
tx_hash_block = add_0x(tx)
if tx_hash_block == tx_hash:
self.index = i
break
i += 1
if self.index == -1:
raise AttributeError('tx {} not found in block {}'.format(tx_hash, block.hash))
self.block = block
self.hash = strip_0x(tx_hash)
if block != None:
self.apply_block(block)
try:
self.value = int(strip_0x(src['value']), 16)
except TypeError:
@ -378,6 +548,7 @@ class Tx:
inpt = src['input']
except KeyError:
inpt = src['data']
src['input'] = src['data']
if inpt != '0x':
inpt = strip_0x(inpt)
@ -408,13 +579,27 @@ class Tx:
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):
@ -430,16 +615,40 @@ class Tx:
def apply_receipt(self, rcpt):
"""Apply receipt data to transaction object.
Effect is the same as passing a receipt at construction.
:param rcpt: Receipt data
:type rcpt: dict
"""
rcpt = self.src_normalize(rcpt)
logg.debug('rcpt {}'.format(rcpt))
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))
try:
status_number = int(rcpt['status'], 16)
except TypeError:
status_number = int(rcpt['status'])
if status_number == 1:
self.status = Status.SUCCESS
elif status_number == 0:
self.status = Status.ERROR
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:
@ -452,20 +661,43 @@ class Tx:
except TypeError:
self.gas_used = int(rcpt['gasUsed'])
self.__rcpt_block_hash = rcpt['block_hash']
def apply_block(self, block):
#block_src = self.src_normalize(block_src)
"""Apply block to transaction object.
: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
def generate_wire(self, chain_spec):
b = pack(self.src(), chain_spec)
self.wire = add_0x(b.hex())
"""Generate transaction wire format.
:param chain_spec: Chain spec to interpret EIP155 v value.
:type chain_spec: chainlib.chain.ChainSpec
:rtype: str
:returns: Wire format, in hex
"""
if self.wire == None:
b = pack(self.src(), chain_spec)
self.wire = add_0x(b.hex())
return self.wire
@staticmethod
def from_src(src, block=None):
return Tx(src, block=block)
def from_src(src, block=None, rcpt=None):
"""Creates a new Tx object.
Alias of constructor.
"""
return Tx(src, block=block, rcpt=rcpt)
def __str__(self):
@ -480,13 +712,18 @@ class Tx:
def to_human(self):
"""Human-readable string dump of transaction contents.
:rtype: str
:returns: Contents
"""
s = """hash {}
from {}
to {}
value {}
nonce {}
gasPrice {}
gasLimit {}
gas_price {}
gas_limit {}
input {}
""".format(
self.hash,
@ -500,13 +737,24 @@ input {}
)
if self.status != Status.PENDING:
s += """gasUsed {}
s += """gas_used {}
""".format(
self.gas_used,
)
s += 'status ' + self.status.name + '\n'
if self.block != None:
s += """block_number {}
block_hash {}
tx_index {}
""".format(
self.block.number,
self.block.hash,
self.tx_index,
)
if self.contract != None:
s += """contract {}
""".format(
@ -520,4 +768,3 @@ input {}
)
return s

View File

@ -17,6 +17,7 @@ address = add_0x(address_bytes.hex())
rpc_provider = os.environ.get('RPC_PROVIDER', 'http://localhost:8545')
rpc = EthHTTPConnection(rpc_provider)
o = balance(address)
print(o)
r = rpc.do(o)
clean_address = strip_0x(address)

View File

@ -3,22 +3,23 @@ import os
import sys
# local imports
from chainlib.jsonrpc import jsonrpc_template
from chainlib.jsonrpc import JSONRPCRequest
from chainlib.eth.connection import EthHTTPConnection
# set up node connection and execute rpc call
rpc_provider = os.environ.get('RPC_PROVIDER', 'http://localhost:8545')
rpc = EthHTTPConnection(rpc_provider)
conn = EthHTTPConnection(rpc_provider)
# check the connection
if not rpc.check():
if not conn.check():
sys.stderr.write('node {} not usable\n'.format(rpc_provider))
sys.exit(1)
# build and send rpc call
o = jsonrpc_template()
g = JSONRPCRequest()
o = g.template()
o['method'] = 'eth_blockNumber'
r = rpc.do(o)
r = conn.do(o)
# interpret result for humans
try:

View File

@ -0,0 +1,41 @@
# standard imports
import os
import sys
# local imports
from chainlib.jsonrpc import JSONRPCRequest
from chainlib.chain import ChainSpec
from chainlib.connection import (
JSONRPCHTTPConnection,
RPCConnection,
ConnType,
)
# set up node connection and execute rpc call
rpc_provider = os.environ.get('RPC_PROVIDER', 'http://localhost:8545')
RPCConnection.register_constructor(ConnType.HTTP, JSONRPCHTTPConnection)
tag = 'baz'
chain_spec = ChainSpec('foo', 'bar', 42, tag=tag)
RPCConnection.register_location(rpc_provider, chain_spec, tag='default')
conn = RPCConnection.connect(chain_spec, 'default')
# check the connection
if not conn.check():
sys.stderr.write('node {} not usable\n'.format(rpc_provider))
sys.exit(1)
# build and send rpc call
g = JSONRPCRequest()
o = g.template()
o['method'] = 'eth_blockNumber'
r = conn.do(o)
# interpret result for humans
try:
block_number = int(r, 10)
except ValueError:
block_number = int(r, 16)
print('block number {}'.format(block_number))

View File

@ -21,13 +21,21 @@ from chainlib.eth.tx import (
)
from chainlib.error import JSONRPCException
script_dir = os.path.dirname(os.path.realpath(__file__))
# eth transactions need an explicit chain parameter as part of their signature
chain_spec = ChainSpec.from_chain_str('evm:ethereum:1')
# create keystore and signer
keystore = DictKeystore()
signer = EIP155Signer(keystore)
sender_address = keystore.new()
# import private key for sender
sender_keystore_file = os.path.join(script_dir, '..', 'tests', 'testdata', 'keystore', 'UTC--2021-01-08T17-18-44.521011372Z--eb3907ecad74a0013c259d5874ae7f22dcbcc95c')
sender_address = keystore.import_keystore_file(sender_keystore_file)
# create a new address to use as recipient
recipient_address = keystore.new()
# set up node connection

View File

@ -1,6 +1,7 @@
crypto-dev-signer~=0.4.14b6
crypto-dev-signer>=0.4.14b7,<=0.4.14
pysha3==1.0.2
hexathon~=0.0.1a7
hexathon~=0.0.1a8
websocket-client==0.57.0
potaahto~=0.0.1a1
chainlib==0.0.5a1
chainlib==0.0.8a2
confini>=0.4.1a1,<0.5.0

View File

@ -1,6 +1,6 @@
[metadata]
name = chainlib-eth
version = 0.0.5a1
version = 0.0.8a2
description = Ethereum implementation of the chainlib interface
author = Louis Holbrook
author_email = dev@holbrook.no
@ -24,6 +24,7 @@ licence_files =
LICENSE.txt
[options]
include_package_data = True
python_requires = >= 3.6
packages =
chainlib.eth
@ -40,4 +41,5 @@ console_scripts =
eth-get = chainlib.eth.runnable.get:main
eth-decode = chainlib.eth.runnable.decode:main
eth-info = chainlib.eth.runnable.info:main
eth-nonce = chainlib.eth.runnable.count:main
eth = chainlib.eth.runnable.info:main

19
tests/test_block.py Normal file
View File

@ -0,0 +1,19 @@
# standard imports
import unittest
# local imports
from chainlib.eth.jsonrpc import to_blockheight_param
class TestBlock(unittest.TestCase):
def test_blockheight_param(self):
self.assertEqual(to_blockheight_param('latest'), 'latest')
self.assertEqual(to_blockheight_param(0), 'latest')
self.assertEqual(to_blockheight_param('pending'), 'pending')
self.assertEqual(to_blockheight_param(-1), 'pending')
self.assertEqual(to_blockheight_param(1), '0x0000000000000001')
if __name__ == '__main__':
unittest.main()

View File

@ -28,6 +28,7 @@ from hexathon import (
strip_0x,
add_0x,
)
from chainlib.eth.block import Block
logging.basicConfig(level=logging.DEBUG)
logg = logging.getLogger()
@ -35,6 +36,7 @@ logg = logging.getLogger()
class TxTestCase(EthTesterCase):
def test_tx_reciprocal(self):
nonce_oracle = RPCNonceOracle(self.accounts[0], self.rpc)
gas_oracle = RPCGasOracle(self.rpc)
@ -75,5 +77,70 @@ class TxTestCase(EthTesterCase):
logg.debug('r {}'.format(tx_signed_raw_bytes_recovered.hex()))
self.assertEqual(tx_signed_raw_bytes, tx_signed_raw_bytes_recovered)
def test_apply_block(self):
nonce_oracle = RPCNonceOracle(self.accounts[0], self.rpc)
gas_oracle = RPCGasOracle(self.rpc)
c = Gas(signer=self.signer, nonce_oracle=nonce_oracle, gas_oracle=gas_oracle, chain_spec=self.chain_spec)
(tx_hash_hex, o) = c.create(self.accounts[0], self.accounts[1], 1024, tx_format=TxFormat.RLP_SIGNED)
tx_data = unpack(bytes.fromhex(strip_0x(o)), self.chain_spec)
block_hash = os.urandom(32).hex()
block = Block({
'hash': block_hash,
'number': 42,
'timestamp': 13241324,
'transactions': [],
})
with self.assertRaises(AttributeError):
tx = Tx(tx_data, block=block)
tx_unknown_hash = os.urandom(32).hex()
block.txs = [add_0x(tx_unknown_hash)]
block.txs.append(add_0x(tx_data['hash']))
tx = Tx(tx_data, block=block)
block.txs = [add_0x(tx_unknown_hash)]
block.txs.append(tx_data)
tx = Tx(tx_data, block=block)
def test_apply_receipt(self):
nonce_oracle = RPCNonceOracle(self.accounts[0], self.rpc)
gas_oracle = RPCGasOracle(self.rpc)
c = Gas(signer=self.signer, nonce_oracle=nonce_oracle, gas_oracle=gas_oracle, chain_spec=self.chain_spec)
(tx_hash_hex, o) = c.create(self.accounts[0], self.accounts[1], 1024, tx_format=TxFormat.RLP_SIGNED)
tx_data = unpack(bytes.fromhex(strip_0x(o)), self.chain_spec)
rcpt = {
'transaction_hash': os.urandom(32).hex(),
'block_hash': os.urandom(32).hex(),
'status': 1,
'block_number': 42,
'transaction_index': 1,
'logs': [],
'gas_used': 21000,
}
with self.assertRaises(ValueError):
tx = Tx(tx_data, rcpt=rcpt)
rcpt['transaction_hash'] = tx_data['hash']
tx = Tx(tx_data, rcpt=rcpt)
block_hash = os.urandom(32).hex()
block = Block({
'hash': block_hash,
'number': 42,
'timestamp': 13241324,
'transactions': [],
})
block.txs = [add_0x(tx_data['hash'])]
with self.assertRaises(ValueError):
tx = Tx(tx_data, rcpt=rcpt, block=block)
rcpt['block_hash'] = block.hash
tx = Tx(tx_data, rcpt=rcpt, block=block)
if __name__ == '__main__':
unittest.main()