Merge branch 'lash/chainlib-cli' into 'master'

Add docstrings

See merge request chaintool/chainlib-eth!1
This commit is contained in:
Louis Holbrook 2021-08-21 07:27:40 +00:00
commit ffcde95e5f
33 changed files with 1333 additions and 585 deletions

View File

@ -1,2 +1,3 @@
- 0.0.5-pending - 0.0.5-pending
* Receive all ethereum components from chainlib package * 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 import sha3
from hexathon import ( from hexathon import (
strip_0x, strip_0x,
@ -11,3 +11,34 @@ from crypto_dev_signer.encoding import (
) )
to_checksum = to_checksum_address 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.jsonrpc import JSONRPCRequest
from chainlib.eth.tx import Tx from chainlib.block import Block as BaseBlock
from hexathon import ( from hexathon import (
add_0x, add_0x,
strip_0x, strip_0x,
even, even,
) )
# local imports
from chainlib.eth.tx import Tx
def block_latest(id_generator=None): def block_latest(id_generator=None):
"""Implements chainlib.interface.ChainInterface method
"""
j = JSONRPCRequest(id_generator) j = JSONRPCRequest(id_generator)
o = j.template() o = j.template()
o['method'] = 'eth_blockNumber' 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): def block_by_hash(hsh, include_tx=True, id_generator=None):
"""Implements chainlib.interface.ChainInterface method
"""
j = JSONRPCRequest(id_generator) j = JSONRPCRequest(id_generator)
o = j.template() o = j.template()
o['method'] = 'eth_getBlockByHash' 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): def block_by_number(n, include_tx=True, id_generator=None):
"""Implements chainlib.interface.ChainInterface method
"""
nhx = add_0x(even(hex(n)[2:])) nhx = add_0x(even(hex(n)[2:]))
j = JSONRPCRequest(id_generator) j = JSONRPCRequest(id_generator)
o = j.template() 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): 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) j = JSONRPCRequest(id_generator)
o = j.template() o = j.template()
o['method'] = 'eth_getBlockTransactionCountByHash' o['method'] = 'eth_getBlockTransactionCountByHash'
@ -42,7 +60,13 @@ def transaction_count(block_hash, id_generator=None):
return j.finalize(o) 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): def __init__(self, src):
self.hash = src['hash'] self.hash = src['hash']
@ -58,22 +82,21 @@ class Block:
self.timestamp = int(src['timestamp']) self.timestamp = int(src['timestamp'])
def src(self): def get_tx(self, tx_hash):
return self.block_src 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): 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) j = JSONRPCRequest(id_generator=id_generator)
o = j.template() o = j.template()
o['method'] = 'net_version' 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): 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): 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() t = datetime.datetime.utcnow()
i = 0 i = 0
while True: while True:
@ -59,13 +84,13 @@ class EthHTTPConnection(JSONRPCHTTPConnection):
) )
req.add_header('Content-Type', 'application/json') req.add_header('Content-Type', 'application/json')
data = json.dumps(o) 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')) res = urlopen(req, data=data.encode('utf-8'))
r = json.load(res) r = json.load(res)
e = jsonrpc_result(r, error_parser) e = jsonrpc_result(r, error_parser)
if e != None: 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']))) logg.debug('e {}'.format(strip_0x(e['status'])))
if strip_0x(e['status']) == '00': if strip_0x(e['status']) == '00':
raise RevertEthException(tx_hash_hex) raise RevertEthException(tx_hash_hex)
@ -80,7 +105,17 @@ class EthHTTPConnection(JSONRPCHTTPConnection):
i += 1 i += 1
def __str__(self):
return 'ETH HTTP JSONRPC'
def check_rpc(self, id_generator=None): 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) j = JSONRPCRequest(id_generator)
req = j.template() req = j.template()
req['method'] = 'net_version' req['method'] = 'net_version'
@ -89,12 +124,29 @@ class EthHTTPConnection(JSONRPCHTTPConnection):
class EthUnixConnection(JSONRPCUnixConnection): 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): 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') raise NotImplementedError('Not yet implemented for unix socket')
def sign_transaction_to_rlp(chain_spec, doer, tx): 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() txs = tx.serialize()
logg.debug('serializing {}'.format(txs)) 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 # 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): 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) o = sign_message(msg)
return doer(o) return doer(o)
class EthUnixSignerConnection(EthUnixConnection): class EthUnixSignerConnection(EthUnixConnection):
"""Connects rpc signer methods to Unix socket connection interface
"""
def sign_transaction_to_rlp(self, tx): 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) return sign_transaction_to_rlp(self.chain_spec, self.do, tx)
def sign_message(self, tx): def sign_message(self, msg):
return sign_message(self.do, tx) """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): class EthHTTPSignerConnection(EthHTTPConnection):
def sign_transaction_to_rlp(self, tx): 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) return sign_transaction_to_rlp(self.chain_spec, self.do, tx)
def sign_message(self, 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) return sign_message(self.do, tx)

View File

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

View File

@ -15,14 +15,14 @@ from chainlib.block import BlockSpec
from chainlib.jsonrpc import JSONRPCRequest from chainlib.jsonrpc import JSONRPCRequest
from .address import to_checksum_address from .address import to_checksum_address
#logg = logging.getLogger(__name__) logg = logging.getLogger(__name__)
logg = logging.getLogger()
re_method = r'^[a-zA-Z0-9_]+$' re_method = r'^[a-zA-Z0-9_]+$'
class ABIContractType(enum.Enum): class ABIContractType(enum.Enum):
"""Data types used by ABI encoders
"""
BYTES32 = 'bytes32' BYTES32 = 'bytes32'
BYTES4 = 'bytes4' BYTES4 = 'bytes4'
UINT256 = 'uint256' UINT256 = 'uint256'
@ -36,14 +36,16 @@ dynamic_contract_types = [
class ABIContract: class ABIContract:
"""Base class for Ethereum smart contract encoder
"""
def __init__(self): def __init__(self):
self.types = [] self.types = []
self.contents = [] self.contents = []
class ABIMethodEncoder(ABIContract): class ABIMethodEncoder(ABIContract):
"""Generate ABI method signatures from method signature string.
"""
def __init__(self): def __init__(self):
super(ABIMethodEncoder, self).__init__() super(ABIMethodEncoder, self).__init__()
self.method_name = None self.method_name = None
@ -51,6 +53,12 @@ class ABIMethodEncoder(ABIContract):
def method(self, m): 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: if re.match(re_method, m) == None:
raise ValueError('Invalid method {}, must match regular expression {}'.format(re_method)) raise ValueError('Invalid method {}, must match regular expression {}'.format(re_method))
self.method_name = m self.method_name = m
@ -58,12 +66,26 @@ class ABIMethodEncoder(ABIContract):
def get_method(self): def get_method(self):
"""Return currently set method signature string.
:rtype: str
:returns: Method signature
"""
if self.method_name == None: if self.method_name == None:
return '' return ''
return '{}({})'.format(self.method_name, ','.join(self.method_contents)) return '{}({})'.format(self.method_name, ','.join(self.method_contents))
def typ(self, v): 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: if self.method_name == None:
raise AttributeError('method name must be set before adding types') raise AttributeError('method name must be set before adding types')
if not isinstance(v, ABIContractType): if not isinstance(v, ABIContractType):
@ -78,9 +100,16 @@ class ABIMethodEncoder(ABIContract):
class ABIContractDecoder(ABIContract): class ABIContractDecoder(ABIContract):
"""Decode serialized ABI contract input data to corresponding python primitives.
"""
def typ(self, v): 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): if not isinstance(v, ABIContractType):
raise TypeError('method type not valid; expected {}, got {}'.format(type(ABIContractType).__name__, type(v).__name__)) raise TypeError('method type not valid; expected {}, got {}'.format(type(ABIContractType).__name__, type(v).__name__))
self.types.append(v.value) self.types.append(v.value)
@ -88,32 +117,74 @@ class ABIContractDecoder(ABIContract):
def val(self, v): def val(self, v):
"""Add value to value array.
:param v: Value, in hex
:type v: str
"""
self.contents.append(v) self.contents.append(v)
logg.debug('content is now {}'.format(self.contents)) logg.debug('content is now {}'.format(self.contents))
def uint256(self, v): 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) return int(v, 16)
def bytes32(self, v): def bytes32(self, v):
"""Parse value as bytes32.
:param v: Value, in hex
:type v: str
:rtype: str
:returns: Value, in hex
"""
return v return v
def bool(self, 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)) return bool(self.uint256(v))
def boolean(self, v): def boolean(self, v):
"""Alias of chainlib.eth.contract.ABIContractDecoder.bool
"""
return bool(self.uint256(v)) return bool(self.uint256(v))
def address(self, 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:] a = strip_0x(v)[64-40:]
return to_checksum_address(a) return to_checksum_address(a)
def string(self, v): def string(self, v):
"""Parse value as string.
:param v: Value, in hex
:type v: str
:rtype: str
:returns: Value
"""
s = strip_0x(v) s = strip_0x(v)
b = bytes.fromhex(s) b = bytes.fromhex(s)
cursor = 0 cursor = 0
@ -131,18 +202,23 @@ class ABIContractDecoder(ABIContract):
def decode(self): def decode(self):
"""Apply decoder on value array using argument type array.
:rtype: list
:returns: List of decoded values
"""
r = [] r = []
logg.debug('contents {}'.format(self.contents)) logg.debug('contents {}'.format(self.contents))
for i in range(len(self.types)): for i in range(len(self.types)):
m = getattr(self, self.types[i]) m = getattr(self, self.types[i])
s = self.contents[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)) r.append(m(s))
return r return r
def get(self): def get(self):
"""Alias of chainlib.eth.contract.ABIContractDecoder.decode
"""
return self.decode() return self.decode()
@ -151,7 +227,10 @@ class ABIContractDecoder(ABIContract):
class ABIContractLogDecoder(ABIMethodEncoder, ABIContractDecoder): 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): def __init__(self):
super(ABIContractLogDecoder, self).__init__() super(ABIContractLogDecoder, self).__init__()
self.method_name = None self.method_name = None
@ -159,20 +238,45 @@ class ABIContractLogDecoder(ABIMethodEncoder, ABIContractDecoder):
def topic(self, event): def topic(self, event):
"""Set topic to match.
:param event: Topic name
:type event: str
"""
self.method(event) self.method(event)
def get_method_signature(self): def get_method_signature(self):
"""Generate topic signature from set topic.
:rtype: str
:returns: Topic signature, in hex
"""
s = self.get_method() s = self.get_method()
return keccak256_string_to_hex(s) return keccak256_string_to_hex(s)
def typ(self, v): def typ(self, v):
"""Add type to event argument array.
:param v: Type
:type v: chainlib.eth.contract.ABIContractType
"""
super(ABIContractLogDecoder, self).typ(v) super(ABIContractLogDecoder, self).typ(v)
self.types.append(v.value) self.types.append(v.value)
def apply(self, topics, data): 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() t = self.get_method_signature()
if topics[0] != t: if topics[0] != t:
raise ValueError('topic mismatch') raise ValueError('topic mismatch')
@ -189,6 +293,11 @@ class ABIContractEncoder(ABIMethodEncoder):
def uint256(self, v): def uint256(self, v):
"""Encode value to uint256 and add to input value vector.
:param v: Integer value
:type v: int
"""
v = int(v) v = int(v)
b = v.to_bytes(32, 'big') b = v.to_bytes(32, 'big')
self.contents.append(b.hex()) self.contents.append(b.hex())
@ -197,28 +306,52 @@ class ABIContractEncoder(ABIMethodEncoder):
def bool(self, v): def bool(self, v):
"""Alias of chainlib.eth.contract.ABIContractEncoder.boolean.
"""
return self.boolean(v) return self.boolean(v)
def boolean(self, 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): if bool(v):
return self.uint256(1) return self.uint256(1)
return self.uint256(0) return self.uint256(0)
def address(self, v): 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.bytes_fixed(32, v, 20)
self.types.append(ABIContractType.ADDRESS) self.types.append(ABIContractType.ADDRESS)
self.__log_latest(v) self.__log_latest(v)
def bytes32(self, 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.bytes_fixed(32, v)
self.types.append(ABIContractType.BYTES32) self.types.append(ABIContractType.BYTES32)
self.__log_latest(v) self.__log_latest(v)
def bytes4(self, 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.bytes_fixed(4, v)
self.types.append(ABIContractType.BYTES4) self.types.append(ABIContractType.BYTES4)
self.__log_latest(v) self.__log_latest(v)
@ -226,6 +359,11 @@ class ABIContractEncoder(ABIMethodEncoder):
def string(self, v): 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') b = v.encode('utf-8')
l = len(b) l = len(b)
contents = l.to_bytes(32, 'big') contents = l.to_bytes(32, 'big')
@ -239,6 +377,16 @@ class ABIContractEncoder(ABIMethodEncoder):
def bytes_fixed(self, mx, v, exact=0): 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__ typ = type(v).__name__
if typ == 'str': if typ == 'str':
v = strip_0x(v) v = strip_0x(v)
@ -260,8 +408,9 @@ class ABIContractEncoder(ABIMethodEncoder):
self.contents.append(v.ljust(64, '0')) self.contents.append(v.ljust(64, '0'))
def get_method_signature(self): def get_method_signature(self):
"""Return abi encoded signature of currently set method.
"""
s = self.get_method() s = self.get_method()
if s == '': if s == '':
return s return s
@ -269,6 +418,11 @@ class ABIContractEncoder(ABIMethodEncoder):
def get_contents(self): def get_contents(self):
"""Encode value array.
:rtype: str
:returns: ABI encoded values, in hex
"""
direct_contents = '' direct_contents = ''
pointer_contents = '' pointer_contents = ''
l = len(self.types) l = len(self.types)
@ -291,10 +445,19 @@ class ABIContractEncoder(ABIMethodEncoder):
def get(self): def get(self):
"""Alias of chainlib.eth.contract.ABIContractEncoder.encode
"""
return self.encode() return self.encode()
def encode(self): 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() m = self.get_method_signature()
c = self.get_contents() c = self.get_contents()
return m + c return m + c
@ -306,6 +469,13 @@ class ABIContractEncoder(ABIMethodEncoder):
def abi_decode_single(typ, v): 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 = ABIContractDecoder()
d.typ(typ) d.typ(typ)
d.val(v) d.val(v)
@ -314,6 +484,17 @@ def abi_decode_single(typ, v):
def code(address, block_spec=BlockSpec.LATEST, id_generator=None): 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 block_height = None
if block_spec == BlockSpec.LATEST: if block_spec == BlockSpec.LATEST:
block_height = '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 # local imports
from chainlib.error import ExecutionError from chainlib.error import ExecutionError
class EthException(Exception): class EthException(Exception):
"""Base class for all Ethereum related errors.
"""
pass pass
class RevertEthException(EthException, ExecutionError): class RevertEthException(EthException, ExecutionError):
"""Raised when an rpc call or transaction reverts.
"""
pass pass
class NotFoundEthException(EthException): class NotFoundEthException(EthException):
"""Raised when rpc query is made against an identifier that is not known by the node.
"""
pass pass
class RequestMismatchException(EthException): class RequestMismatchException(EthException):
"""Raised when a request data parser is given unexpected input data.
"""
pass pass
class DefaultErrorParser: class DefaultErrorParser:
"""Generate eth specific exception for the default json-rpc query error parser.
"""
def translate(self, error): def translate(self, error):
return EthException('default parser code {}'.format(error)) return EthException('default parser code {}'.format(error))

View File

@ -1,7 +1,7 @@
# standard imports # standard imports
import logging import logging
# third-party imports # external imports
from hexathon import ( from hexathon import (
add_0x, add_0x,
strip_0x, strip_0x,
@ -16,6 +16,8 @@ from chainlib.eth.tx import (
TxFormat, TxFormat,
raw, raw,
) )
from chainlib.eth.jsonrpc import to_blockheight_param
from chainlib.block import BlockSpec
from chainlib.eth.constant import ( from chainlib.eth.constant import (
MINIMUM_FEE_UNITS, MINIMUM_FEE_UNITS,
) )
@ -24,22 +26,48 @@ logg = logging.getLogger(__name__)
def price(id_generator=None): 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) j = JSONRPCRequest(id_generator)
o = j.template() o = j.template()
o['method'] = 'eth_gasPrice' o['method'] = 'eth_gasPrice'
return j.finalize(o) 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) j = JSONRPCRequest(id_generator)
o = j.template() o = j.template()
o['method'] = 'eth_getBalance' o['method'] = 'eth_getBalance'
o['params'].append(address) o['params'].append(address)
o['params'].append('latest') height = to_blockheight_param(height)
o['params'].append(height)
return j.finalize(o) return j.finalize(o)
def parse_balance(balance): 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: try:
r = int(balance, 10) r = int(balance, 10)
except ValueError: except ValueError:
@ -48,10 +76,29 @@ def parse_balance(balance):
class Gas(TxFactory): 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 = self.template(sender_address, recipient_address, use_nonce=True)
tx['value'] = value tx['value'] = value
if data != None:
tx['data'] = data
txe = EIP155Transaction(tx, tx['nonce'], tx['chainId']) txe = EIP155Transaction(tx, tx['nonce'], tx['chainId'])
tx_raw = self.signer.sign_transaction_to_rlp(txe) tx_raw = self.signer.sign_transaction_to_rlp(txe)
tx_raw_hex = add_0x(tx_raw.hex()) tx_raw_hex = add_0x(tx_raw.hex())
@ -68,6 +115,17 @@ class Gas(TxFactory):
class RPCGasOracle: 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): def __init__(self, conn, code_callback=None, min_price=1, id_generator=None):
self.conn = conn self.conn = conn
@ -76,7 +134,20 @@ class RPCGasOracle:
self.id_generator = id_generator 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 gas_price = 0
if self.conn != None: if self.conn != None:
o = price(id_generator=self.id_generator) o = price(id_generator=self.id_generator)
@ -93,13 +164,39 @@ class RPCGasOracle:
class RPCPureGasOracle(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): 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) super(RPCPureGasOracle, self).__init__(conn, code_callback=code_callback, min_price=0, id_generator=id_generator)
class OverrideGasOracle(RPCGasOracle): 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): def __init__(self, price=None, limit=None, conn=None, code_callback=None, id_generator=None):
self.conn = None self.conn = None
self.code_callback = None self.code_callback = None
@ -117,6 +214,8 @@ class OverrideGasOracle(RPCGasOracle):
def get_gas(self, code=None): def get_gas(self, code=None):
"""See chainlib.eth.gas.RPCGasOracle.
"""
r = None r = None
fee_units = None fee_units = None
fee_price = None fee_price = None

View File

@ -14,3 +14,31 @@
#106 Timeout Should be used when an action timedout. #106 Timeout Should be used when an action timedout.
#107 Conflict Should be used when an action conflicts with another (ongoing?) action. #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: class LogBloom:
"""Helper for Ethereum receipt log bloom filters.
"""
def __init__(self): def __init__(self):
self.content = bytearray(256) self.content = bytearray(256)
def add(self, element): def add(self, element):
"""Add topic element to filter.
:param element: Topic element
:type element: bytes
"""
if not isinstance(element, bytes): if not isinstance(element, bytes):
raise ValueError('element must be bytes') raise ValueError('element must be bytes')
h = sha3.keccak_256() h = sha3.keccak_256()

View File

@ -1,4 +1,4 @@
# third-party imports # external imports
from hexathon import ( from hexathon import (
add_0x, add_0x,
strip_0x, strip_0x,
@ -8,17 +8,40 @@ from hexathon import (
from chainlib.jsonrpc import JSONRPCRequest 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) j = JSONRPCRequest(id_generator)
o = j.template() o = j.template()
o['method'] = 'eth_getTransactionCount' o['method'] = 'eth_getTransactionCount'
o['params'].append(address) o['params'].append(address)
o['params'].append('pending') o['params'].append('pending')
if confirmed:
o['params'].append('latest')
else:
o['params'].append('pending')
return j.finalize(o) 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): def __init__(self, address, id_generator=None):
self.address = address self.address = address
self.id_generator = id_generator self.id_generator = id_generator
@ -26,23 +49,45 @@ class NonceOracle:
def get_nonce(self): def get_nonce(self):
"""Load initial nonce value.
"""
raise NotImplementedError('Class must be extended') raise NotImplementedError('Class must be extended')
def next_nonce(self): def next_nonce(self):
"""Return next nonce value and advance.
:rtype: int
:returns: Next nonce for address.
"""
n = self.nonce n = self.nonce
self.nonce += 1 self.nonce += 1
return n return n
class RPCNonceOracle(NonceOracle): 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): def __init__(self, address, conn, id_generator=None):
self.conn = conn self.conn = conn
super(RPCNonceOracle, self).__init__(address, id_generator=id_generator) super(RPCNonceOracle, self).__init__(address, id_generator=id_generator)
def get_nonce(self): 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) o = nonce(self.address, id_generator=self.id_generator)
r = self.conn.do(o) r = self.conn.do(o)
n = strip_0x(r) n = strip_0x(r)
@ -50,14 +95,28 @@ class RPCNonceOracle(NonceOracle):
class OverrideNonceOracle(NonceOracle): class OverrideNonceOracle(NonceOracle):
"""Manually set initial nonce value.
def __init__(self, address, nonce): :param address: Address to retireve nonce for, in hex
self.nonce = nonce :type address: str
super(OverrideNonceOracle, self).__init__(address) :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): 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 DefaultNonceOracle = RPCNonceOracle

View File

@ -17,7 +17,7 @@ from chainlib.connection import (
from chainlib.eth.unittest.ethtester import create_tester_signer from chainlib.eth.unittest.ethtester import create_tester_signer
from chainlib.eth.address import to_checksum_address from chainlib.eth.address import to_checksum_address
logg = logging.getLogger() #__name__) logg = logging.getLogger(__name__)
@pytest.fixture(scope='function') @pytest.fixture(scope='function')
@ -37,13 +37,6 @@ def call_sender(
eth_accounts, eth_accounts,
): ):
return eth_accounts[0] return eth_accounts[0]
#
#
#@pytest.fixture(scope='function')
#def eth_signer(
# init_eth_tester,
# ):
# return init_eth_tester
@pytest.fixture(scope='function') @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 # SPDX-License-Identifier: GPL-3.0-or-later
# standard imports # standard imports
import os import os
import json
import argparse
import logging import logging
# third-party imports # external imports
from hexathon import ( from hexathon import (
add_0x, add_0x,
strip_0x, strip_0x,
even, even,
) )
import sha3
# local imports # local imports
from chainlib.eth.address import to_checksum import chainlib.eth.cli
from chainlib.eth.address import AddressChecksum
from chainlib.jsonrpc import ( from chainlib.jsonrpc import (
jsonrpc_result, jsonrpc_result,
IntSequenceGenerator, IntSequenceGenerator,
@ -35,53 +24,37 @@ from chainlib.eth.gas import (
balance, balance,
) )
from chainlib.chain import ChainSpec from chainlib.chain import ChainSpec
from crypto_dev_signer.eth.signer import ReferenceSigner as EIP155Signer
logging.basicConfig(level=logging.WARNING) logging.basicConfig(level=logging.WARNING)
logg = logging.getLogger() logg = logging.getLogger()
default_eth_provider = os.environ.get('RPC_PROVIDER') script_dir = os.path.dirname(os.path.realpath(__file__))
if default_eth_provider == None: #config_dir = os.path.join(script_dir, '..', 'data', 'config')
default_eth_provider = os.environ.get('ETH_PROVIDER', 'http://localhost:8545')
argparser = argparse.ArgumentParser() arg_flags = chainlib.eth.cli.argflag_std_read
argparser.add_argument('-p', '--provider', dest='p', default=default_eth_provider, type=str, help='Web3 provider url (http only)') argparser = chainlib.eth.cli.ArgumentParser(arg_flags)
argparser.add_argument('-i', '--chain-spec', dest='i', type=str, default='evm:ethereum:1', help='Chain specification string') argparser.add_positional('address', type=str, help='Ethereum address of recipient')
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')
args = argparser.parse_args() 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: rpc = chainlib.eth.cli.Rpc()
logg.setLevel(logging.DEBUG) conn = rpc.connect_by_config(config)
elif args.v:
logg.setLevel(logging.INFO)
rpc_id_generator = None chain_spec = ChainSpec.from_chain_str(config.get('CHAIN_SPEC'))
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)
def main(): def main():
r = None r = None
decimals = 18 decimals = 18
o = balance(address, id_generator=rpc_id_generator) o = balance(holder_address, id_generator=rpc.id_generator)
r = conn.do(o) r = conn.do(o)
hx = strip_0x(r) hx = strip_0x(r)

View File

@ -4,12 +4,13 @@
import sys import sys
import os import os
import json import json
import argparse #import argparse
import logging import logging
import select import select
# local imports # 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.connection import EthHTTPConnection
from chainlib.eth.tx import count from chainlib.eth.tx import count
from chainlib.chain import ChainSpec from chainlib.chain import ChainSpec
@ -21,63 +22,28 @@ from hexathon import add_0x
logging.basicConfig(level=logging.WARNING) logging.basicConfig(level=logging.WARNING)
logg = logging.getLogger() logg = logging.getLogger()
default_eth_provider = os.environ.get('RPC_PROVIDER') script_dir = os.path.dirname(os.path.realpath(__file__))
if default_eth_provider == None: config_dir = os.path.join(script_dir, '..', 'data', 'config')
default_eth_provider = os.environ.get('ETH_PROVIDER', 'http://localhost:8545')
def stdin_arg(): arg_flags = chainlib.eth.cli.argflag_std_read
h = select.select([sys.stdin], [], [], 0) argparser = chainlib.eth.cli.ArgumentParser(arg_flags)
if len(h[0]) > 0: argparser.add_positional('address', type=str, help='Ethereum address of recipient')
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')
args = argparser.parse_args() args = argparser.parse_args()
config = chainlib.eth.cli.Config.from_args(args, arg_flags, default_config_dir=config_dir)
if args.address == None: holder_address = args.address
argparser.error('need first positional argument or value from stdin') 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: rpc = chainlib.eth.cli.Rpc(wallet=wallet)
logg.setLevel(logging.DEBUG) conn = rpc.connect_by_config(config)
elif args.v:
logg.setLevel(logging.INFO)
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(): def main():
recipient = to_checksum(args.address) o = count(holder_address, id_generator=rpc.id_generator)
if not args.u and recipient != add_0x(args.address): r = conn.do(o)
raise ValueError('invalid checksum address')
o = count(recipient, id_generator=rpc_id_generator)
r = rpc.do(o)
count_result = None count_result = None
try: try:
count_result = int(r, 16) 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 # SPDX-License-Identifier: GPL-3.0-or-later
# standard imports # standard imports
@ -18,48 +9,29 @@ import logging
import select import select
# external imports # external imports
import chainlib.eth.cli
from chainlib.eth.tx import unpack from chainlib.eth.tx import unpack
from chainlib.chain import ChainSpec from chainlib.chain import ChainSpec
# local imports # local imports
from chainlib.eth.runnable.util import decode_for_puny_humans from chainlib.eth.runnable.util import decode_for_puny_humans
logging.basicConfig(level=logging.WARNING) logging.basicConfig(level=logging.WARNING)
logg = logging.getLogger() logg = logging.getLogger()
def stdin_arg(t=0): script_dir = os.path.dirname(os.path.realpath(__file__))
h = select.select([sys.stdin], [], [], t) config_dir = os.path.join(script_dir, '..', 'data', 'config')
if len(h[0]) > 0:
v = h[0][0].read()
return v.rstrip()
return None
argparser = argparse.ArgumentParser() 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.add_argument('-i', '--chain-id', dest='i', default='evm:ethereum:1', type=str, help='Numeric network id') argparser = chainlib.eth.cli.ArgumentParser(arg_flags)
argparser.add_argument('-v', action='store_true', help='Be verbose') argparser.add_positional('tx_data', type=str, help='Transaction data to decode')
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')
args = argparser.parse_args() args = argparser.parse_args()
config = chainlib.eth.cli.Config.from_args(args, arg_flags, default_config_dir=config_dir)
if args.vv: chain_spec = ChainSpec.from_chain_str(config.get('CHAIN_SPEC'))
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)
def main(): def main():
tx_raw = argp decode_for_puny_humans(args.tx_data, chain_spec, sys.stdout)
decode_for_puny_humans(tx_raw, chain_spec, sys.stdout)
if __name__ == '__main__': if __name__ == '__main__':
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 # SPDX-License-Identifier: GPL-3.0-or-later
# standard imports # standard imports
@ -19,114 +10,53 @@ import logging
import urllib import urllib
# external imports # external imports
from crypto_dev_signer.eth.signer import ReferenceSigner as EIP155Signer
from crypto_dev_signer.keystore.dict import DictKeystore
from hexathon import ( from hexathon import (
add_0x, add_0x,
strip_0x, strip_0x,
) )
# local imports # 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.eth.connection import EthHTTPConnection
from chainlib.jsonrpc import ( from chainlib.jsonrpc import (
JSONRPCRequest, JSONRPCRequest,
IntSequenceGenerator, IntSequenceGenerator,
) )
from chainlib.eth.nonce import ( from chainlib.eth.gas import Gas
RPCNonceOracle,
OverrideNonceOracle,
)
from chainlib.eth.gas import (
RPCGasOracle,
OverrideGasOracle,
Gas,
)
from chainlib.eth.gas import balance as gas_balance from chainlib.eth.gas import balance as gas_balance
from chainlib.chain import ChainSpec from chainlib.chain import ChainSpec
from chainlib.eth.runnable.util import decode_for_puny_humans from chainlib.eth.runnable.util import decode_for_puny_humans
import chainlib.eth.cli
logging.basicConfig(level=logging.WARNING) logging.basicConfig(level=logging.WARNING)
logg = logging.getLogger() logg = logging.getLogger()
default_eth_provider = os.environ.get('RPC_PROVIDER') arg_flags = chainlib.eth.cli.argflag_std_write | chainlib.eth.cli.Flag.WALLET
if default_eth_provider == None: argparser = chainlib.eth.cli.ArgumentParser(arg_flags)
default_eth_provider = os.environ.get('ETH_PROVIDER', 'http://localhost:8545') argparser.add_argument('--data', type=str, help='Transaction data')
argparser.add_positional('amount', type=int, help='Token amount to send')
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')
args = argparser.parse_args() args = argparser.parse_args()
extra_args = {
'data': None,
if args.vv: 'amount': None,
logg.setLevel(logging.DEBUG) }
elif args.v: #config = chainlib.eth.cli.Config.from_args(args, arg_flags, extra_args=extra_args, default_config_dir=config_dir)
logg.setLevel(logging.INFO) config = chainlib.eth.cli.Config.from_args(args, arg_flags, extra_args=extra_args)
block_all = args.ww block_all = args.ww
block_last = args.w or block_all block_last = args.w or block_all
passphrase_env = 'ETH_PASSPHRASE' wallet = chainlib.eth.cli.Wallet()
if args.env_prefix != None: wallet.from_config(config)
passphrase_env = args.env_prefix + '_' + passphrase_env
passphrase = os.environ.get(passphrase_env)
if passphrase == None:
logg.warning('no passphrase given')
passphrase=''
signer_address = None rpc = chainlib.eth.cli.Rpc(wallet=wallet)
keystore = DictKeystore() conn = rpc.connect_by_config(config)
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_id_generator = None chain_spec = ChainSpec.from_chain_str(config.get('CHAIN_SPEC'))
if args.seq:
rpc_id_generator = IntSequenceGenerator()
auth = None value = config.get('_AMOUNT')
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)
nonce_oracle = None send = config.true('_RPC_SEND')
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)
def balance(address, id_generator): def balance(address, id_generator):
@ -137,29 +67,34 @@ def balance(address, id_generator):
def main(): def main():
recipient = to_checksum(args.recipient) signer = rpc.get_signer()
if not args.u and recipient != add_0x(args.recipient): 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') raise ValueError('invalid checksum address')
logg.info('gas transfer from {} to {} value {}'.format(signer_address, recipient, value)) logg.info('gas transfer from {} to {} value {}'.format(signer_address, recipient, value))
if logg.isEnabledFor(logging.DEBUG): if logg.isEnabledFor(logging.DEBUG):
try: try:
sender_balance = balance(signer_address, rpc_id_generator) sender_balance = balance(signer_address, rpc.id_generator)
recipient_balance = balance(recipient, rpc_id_generator) recipient_balance = balance(recipient, rpc.id_generator)
logg.debug('sender {} balance before: {}'.format(signer_address, sender_balance)) logg.debug('sender {} balance before: {}'.format(signer_address, sender_balance))
logg.debug('recipient {} balance before: {}'.format(recipient, recipient_balance)) logg.debug('recipient {} balance before: {}'.format(recipient, recipient_balance))
except urllib.error.URLError: except urllib.error.URLError:
pass 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: if send:
conn.do(o) conn.do(o)
if block_last: if block_last:
r = conn.wait(tx_hash_hex) r = conn.wait(tx_hash_hex)
if logg.isEnabledFor(logging.DEBUG): if logg.isEnabledFor(logging.DEBUG):
sender_balance = balance(signer_address, rpc_id_generator) sender_balance = balance(signer_address, rpc.id_generator)
recipient_balance = balance(recipient, rpc_id_generator) recipient_balance = balance(recipient, rpc.id_generator)
logg.debug('sender {} balance after: {}'.format(signer_address, sender_balance)) logg.debug('sender {} balance after: {}'.format(signer_address, sender_balance))
logg.debug('recipient {} balance after: {}'.format(recipient, recipient_balance)) logg.debug('recipient {} balance after: {}'.format(recipient, recipient_balance))
if r['status'] == 0: if r['status'] == 0:
@ -167,12 +102,13 @@ def main():
sys.exit(1) sys.exit(1)
print(tx_hash_hex) print(tx_hash_hex)
else: else:
if logg.isEnabledFor(logging.INFO): #if logg.isEnabledFor(logging.INFO):
if config.true('_RAW'):
print(o['params'][0])
else:
io_str = io.StringIO() io_str = io.StringIO()
decode_for_puny_humans(o['params'][0], chain_spec, io_str) decode_for_puny_humans(o['params'][0], chain_spec, io_str)
print(io_str.getvalue()) 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 # SPDX-License-Identifier: GPL-3.0-or-later
# standard imports # standard imports
@ -19,80 +10,56 @@ import enum
import select import select
# external imports # external imports
from potaahto.symbols import snake_and_camel
from hexathon import ( from hexathon import (
add_0x, add_0x,
strip_0x, strip_0x,
) )
import sha3 import sha3
# local imports
from chainlib.eth.address import to_checksum
from chainlib.jsonrpc import ( from chainlib.jsonrpc import (
JSONRPCRequest, JSONRPCRequest,
jsonrpc_result, jsonrpc_result,
IntSequenceGenerator, IntSequenceGenerator,
) )
from chainlib.chain import ChainSpec
from chainlib.status import Status
# local imports
from chainlib.eth.connection import EthHTTPConnection from chainlib.eth.connection import EthHTTPConnection
from chainlib.eth.tx import ( from chainlib.eth.tx import (
Tx, Tx,
pack, pack,
) )
from chainlib.eth.address import to_checksum_address from chainlib.eth.address import (
from chainlib.eth.block import Block to_checksum_address,
from chainlib.chain import ChainSpec is_checksum_address,
from chainlib.status import Status )
from chainlib.eth.block import (
Block,
block_by_hash,
)
from chainlib.eth.runnable.util import decode_for_puny_humans 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') logging.basicConfig(level=logging.WARNING, format='%(asctime)s %(levelname)s %(filename)s:%(lineno)d %(message)s')
logg = logging.getLogger() logg = logging.getLogger()
default_eth_provider = os.environ.get('RPC_PROVIDER') script_dir = os.path.dirname(os.path.realpath(__file__))
if default_eth_provider == None: config_dir = os.path.join(script_dir, '..', 'data', 'config')
default_eth_provider = os.environ.get('ETH_PROVIDER', 'http://localhost:8545')
def stdin_arg(t=0): arg_flags = chainlib.eth.cli.argflag_std_read
h = select.select([sys.stdin], [], [], t) argparser = chainlib.eth.cli.ArgumentParser(arg_flags)
if len(h[0]) > 0: argparser.add_positional('item', type=str, help='Address or transaction to retrieve data for')
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)')
args = argparser.parse_args() args = argparser.parse_args()
config = chainlib.eth.cli.Config.from_args(args, arg_flags, default_config_dir=config_dir)
if args.vv: rpc = chainlib.eth.cli.Rpc()
logg.setLevel(logging.DEBUG) conn = rpc.connect_by_config(config)
elif args.v:
logg.setLevel(logging.INFO)
argp = args.item chain_spec = ChainSpec.from_chain_str(config.get('CHAIN_SPEC'))
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)
item = add_0x(args.item) item = add_0x(args.item)
as_rlp = bool(args.rlp)
def get_transaction(conn, tx_hash, id_generator): 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)) logg.error('Transaction {} not found'.format(tx_hash))
sys.exit(1) sys.exit(1)
if as_rlp: if config.true('_RAW'):
tx_src = Tx.src_normalize(tx_src) tx_src = Tx.src_normalize(tx_src)
return pack(tx_src, chain_spec).hex() return pack(tx_src, chain_spec).hex()
@ -125,16 +92,24 @@ def get_transaction(conn, tx_hash, id_generator):
tx = Tx(tx_src) tx = Tx(tx_src)
if rcpt != None: if rcpt != None:
tx.apply_receipt(rcpt) 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) tx.generate_wire(chain_spec)
return tx return tx
def get_address(conn, address, id_generator):
def get_address(conn, address, id_generator, height):
j = JSONRPCRequest(id_generator=id_generator) j = JSONRPCRequest(id_generator=id_generator)
o = j.template() o = j.template()
o['method'] = 'eth_getCode' o['method'] = 'eth_getCode'
o['params'].append(address) o['params'].append(address)
o['params'].append('latest') height = to_blockheight_param(height)
o['params'].append(height)
o = j.finalize(o) o = j.finalize(o)
code = conn.do(o) code = conn.do(o)
@ -146,11 +121,18 @@ def get_address(conn, address, id_generator):
def main(): def main():
address = item
r = None r = None
if len(item) > 42: if len(address) > 42:
r = get_transaction(conn, item, rpc_id_generator).to_human() r = get_transaction(conn, address, rpc.id_generator)
elif args.u or to_checksum_address(item): if not config.true('_RAW'):
r = get_address(conn, item, rpc_id_generator) 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) 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 # SPDX-License-Identifier: GPL-3.0-or-later
# standard imports # standard imports
@ -17,19 +8,18 @@ import json
import argparse import argparse
import logging import logging
# third-party imports # external imports
from chainlib.chain import ChainSpec
from hexathon import ( from hexathon import (
add_0x, add_0x,
strip_0x, strip_0x,
even, even,
) )
import sha3 import sha3
from crypto_dev_signer.eth.signer import ReferenceSigner as EIP155Signer
# local imports # local imports
from chainlib.eth.address import ( from chainlib.eth.address import AddressChecksum
to_checksum_address,
is_checksum_address,
)
from chainlib.eth.chain import network_id from chainlib.eth.chain import network_id
from chainlib.eth.block import ( from chainlib.eth.block import (
block_latest, block_latest,
@ -43,79 +33,49 @@ from chainlib.eth.gas import (
balance, balance,
price, price,
) )
from chainlib.jsonrpc import ( import chainlib.eth.cli
IntSequenceGenerator,
)
from chainlib.chain import ChainSpec
BLOCK_SAMPLES = 10 BLOCK_SAMPLES = 10
logging.basicConfig(level=logging.WARNING) logging.basicConfig(level=logging.WARNING)
logg = logging.getLogger() logg = logging.getLogger()
default_eth_provider = os.environ.get('RPC_PROVIDER') script_dir = os.path.dirname(os.path.realpath(__file__))
if default_eth_provider == None: config_dir = os.path.join(script_dir, '..', 'data', 'config')
default_eth_provider = os.environ.get('ETH_PROVIDER', 'http://localhost:8545')
argparser = argparse.ArgumentParser() arg_flags = chainlib.eth.cli.argflag_std_read
argparser.add_argument('-p', '--provider', dest='p', default=default_eth_provider, type=str, help='Web3 provider url (http only)') argparser = chainlib.eth.cli.ArgumentParser(arg_flags)
argparser.add_argument('-i', '--chain-spec', dest='i', type=str, default='evm:ethereum:1', help='Chain specification string') argparser.add_positional('address', type=str, help='Address to retrieve info for', required=False)
argparser.add_argument('-H', '--human', dest='human', action='store_true', help='Use human-friendly formatting') argparser.add_argument('--long', action='store_true', help='Calculate averages through sampling of blocks and txs')
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)')
args = argparser.parse_args() 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: holder_address = args.address
logg.setLevel(logging.DEBUG) wallet = chainlib.eth.cli.Wallet()
elif args.v: wallet.from_config(config)
logg.setLevel(logging.INFO) if wallet.get_signer_address() == None and holder_address != None:
wallet.from_address(holder_address)
signer = None rpc = chainlib.eth.cli.Rpc(wallet=wallet)
holder_address = None conn = rpc.connect_by_config(config)
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)
token_symbol = 'eth' 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(): def main():
o = network_id(id_generator=rpc_id_generator) o = network_id(id_generator=rpc.id_generator)
r = conn.do(o) r = conn.do(o)
#if human: #if human:
# n = format(n, ',') # n = format(n, ',')
sys.stdout.write('Network id: {}\n'.format(r)) 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) r = conn.do(o)
n = int(r, 16) n = int(r, 16)
first_block_number = n first_block_number = n
@ -123,7 +83,7 @@ def main():
n = format(n, ',') n = format(n, ',')
sys.stdout.write('Block: {}\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) r = conn.do(o)
last_block = Block(r) last_block = Block(r)
last_timestamp = last_block.timestamp last_timestamp = last_block.timestamp
@ -132,7 +92,7 @@ def main():
aggr_time = 0.0 aggr_time = 0.0
aggr_gas = 0 aggr_gas = 0
for i in range(BLOCK_SAMPLES): 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) r = conn.do(o)
block = Block(r) block = Block(r)
aggr_time += last_block.timestamp - block.timestamp aggr_time += last_block.timestamp - block.timestamp
@ -150,7 +110,7 @@ def main():
sys.stdout.write('Gaslimit: {}\n'.format(n)) sys.stdout.write('Gaslimit: {}\n'.format(n))
sys.stdout.write('Blocktime: {}\n'.format(aggr_time / BLOCK_SAMPLES)) 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) r = conn.do(o)
n = int(r, 16) n = int(r, 16)
if human: 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 # SPDX-License-Identifier: GPL-3.0-or-later
# standard imports # standard imports
@ -19,6 +10,7 @@ import logging
import urllib import urllib
# external imports # external imports
import chainlib.eth.cli
from crypto_dev_signer.eth.signer import ReferenceSigner as EIP155Signer from crypto_dev_signer.eth.signer import ReferenceSigner as EIP155Signer
from crypto_dev_signer.keystore.dict import DictKeystore from crypto_dev_signer.keystore.dict import DictKeystore
from hexathon import ( from hexathon import (
@ -45,129 +37,88 @@ from chainlib.eth.tx import (
TxFactory, TxFactory,
raw, raw,
) )
from chainlib.error import SignerMissingException
from chainlib.chain import ChainSpec from chainlib.chain import ChainSpec
from chainlib.eth.runnable.util import decode_for_puny_humans from chainlib.eth.runnable.util import decode_for_puny_humans
from chainlib.eth.jsonrpc import to_blockheight_param
logging.basicConfig(level=logging.WARNING) logging.basicConfig(level=logging.WARNING)
logg = logging.getLogger() logg = logging.getLogger()
default_eth_provider = os.environ.get('RPC_PROVIDER') script_dir = os.path.dirname(os.path.realpath(__file__))
if default_eth_provider == None: config_dir = os.path.join(script_dir, '..', 'data', 'config')
default_eth_provider = os.environ.get('ETH_PROVIDER', 'http://localhost:8545')
argparser = argparse.ArgumentParser() arg_flags = chainlib.eth.cli.argflag_std_write | chainlib.eth.cli.Flag.EXEC
argparser.add_argument('-p', '--provider', dest='p', default=default_eth_provider, type=str, help='Web3 provider url (http only)') argparser = chainlib.eth.cli.ArgumentParser(arg_flags)
argparser.add_argument('-w', action='store_true', help='Wait for the last transaction to be confirmed') argparser.add_positional('data', type=str, help='Transaction data')
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')
args = argparser.parse_args() 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)
block_all = args.ww block_all = args.ww
block_last = args.w or block_all block_last = args.w or block_all
passphrase_env = 'ETH_PASSPHRASE' wallet = chainlib.eth.cli.Wallet(EIP155Signer)
if args.env_prefix != None: wallet.from_config(config)
passphrase_env = args.env_prefix + '_' + passphrase_env
passphrase = os.environ.get(passphrase_env)
if passphrase == None:
logg.warning('no passphrase given')
passphrase=''
signer_address = None rpc = chainlib.eth.cli.Rpc(wallet=wallet)
keystore = DictKeystore() conn = rpc.connect_by_config(config)
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_id_generator = None send = config.true('_RPC_SEND')
if args.seq:
rpc_id_generator = IntSequenceGenerator()
auth = None if config.get('_EXEC_ADDRESS') != 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:
send = False send = False
nonce_oracle = None chain_spec = None
gas_oracle = None try:
if signer_address != None and not local: chain_spec = ChainSpec.from_chain_str(config.get('CHAIN_SPEC'))
if args.nonce != None: except AttributeError:
nonce_oracle = OverrideNonceOracle(signer_address, args.nonce) pass
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)
def main(): def main():
recipient = None
if args.a != None: signer_address = None
recipient = add_0x(to_checksum(args.a)) try:
if not args.u and recipient != add_0x(recipient): 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') 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 = j.template()
o['method'] = 'eth_call' o['method'] = 'eth_call'
o['params'].append({ o['params'].append({
'to': recipient, 'to': exec_address,
'from': signer_address, 'from': signer_address,
'value': '0x00', 'value': '0x00',
'gas': add_0x(int.to_bytes(8000000, 8, byteorder='big').hex()), # TODO: better get of network gas limit 'gas': add_0x(int.to_bytes(8000000, 8, byteorder='big').hex()), # TODO: better get of network gas limit
'gasPrice': '0x01', 'gasPrice': '0x01',
'data': add_0x(args.data), 'data': add_0x(args.data),
}) })
o['params'].append('latest') height = to_blockheight_param(config.get('_HEIGHT'))
o['params'].append(height)
o = j.finalize(o) o = j.finalize(o)
r = conn.do(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 return
elif signer_address != None: 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) tx = g.template(signer_address, recipient, use_nonce=True)
if args.data != None: if args.data != None:
tx = g.set_code(tx, add_0x(args.data)) 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: if send:
r = conn.do(o) r = conn.do(o)
@ -177,7 +128,7 @@ def main():
print(tx_hash_hex) print(tx_hash_hex)
else: else:
o = raw(args.data, id_generator=rpc_id_generator) o = raw(args.data, id_generator=rpc.id_generator)
if send: if send:
r = conn.do(o) r = conn.do(o)
print(r) print(r)

View File

@ -3,6 +3,17 @@ from chainlib.jsonrpc import JSONRPCRequest
def new_account(passphrase='', id_generator=None): 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) j = JSONRPCRequest(id_generator)
o = j.template() o = j.template()
o['method'] = 'personal_newAccount' o['method'] = 'personal_newAccount'
@ -11,6 +22,17 @@ def new_account(passphrase='', id_generator=None):
def sign_transaction(payload, 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) j = JSONRPCRequest(id_generator)
o = j.template() o = j.template()
o['method'] = 'eth_signTransaction' o['method'] = 'eth_signTransaction'
@ -19,6 +41,19 @@ def sign_transaction(payload, id_generator=None):
def sign_message(address, 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) j = JSONRPCRequest(id_generator)
o = j.template() o = j.template()
o['method'] = 'eth_sign' o['method'] = 'eth_sign'

View File

@ -9,6 +9,7 @@ import sha3
from hexathon import ( from hexathon import (
strip_0x, strip_0x,
add_0x, add_0x,
compact,
) )
from rlp import decode as rlp_decode from rlp import decode as rlp_decode
from rlp import encode as rlp_encode from rlp import encode as rlp_encode
@ -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.encoding import public_key_to_address
from crypto_dev_signer.eth.encoding import chain_id_to_v from crypto_dev_signer.eth.encoding import chain_id_to_v
from potaahto.symbols import snake_and_camel from potaahto.symbols import snake_and_camel
# local imports
from chainlib.hash import keccak256_hex_to_hex from chainlib.hash import keccak256_hex_to_hex
from chainlib.status import Status from chainlib.status import Status
from chainlib.jsonrpc import JSONRPCRequest
from chainlib.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 .address import to_checksum
from .constant import ( from .constant import (
MINIMUM_FEE_UNITS, MINIMUM_FEE_UNITS,
MINIMUM_FEE_PRICE, MINIMUM_FEE_PRICE,
ZERO_ADDRESS, ZERO_ADDRESS,
DEFAULT_FEE_LIMIT,
) )
from .contract import ABIContractEncoder 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): class TxFormat(enum.IntEnum):
"""Tx generator output formats
"""
DICT = 0x00 DICT = 0x00
RAW = 0x01 RAW = 0x01
RAW_SIGNED = 0x02 RAW_SIGNED = 0x02
@ -56,24 +66,22 @@ field_debugs = [
's', '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 count = nonce_query
count_pending = nonce_query
def count_confirmed(address): count_confirmed = nonce_query_confirmed
return count(address, True)
def pack(tx_src, chain_spec): 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): if isinstance(tx_src, Tx):
tx_src = tx_src.as_dict() tx_src = tx_src.as_dict()
tx_src = Tx.src_normalize(tx_src) tx_src = Tx.src_normalize(tx_src)
@ -96,6 +104,15 @@ def pack(tx_src, chain_spec):
def unpack(tx_raw_bytes, 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() chain_id = chain_spec.chain_id()
tx = __unpack_raw(tx_raw_bytes, chain_id) tx = __unpack_raw(tx_raw_bytes, chain_id)
tx['nonce'] = int.from_bytes(tx['nonce'], 'big') 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): 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() chain_id = chain_spec.chain_id()
tx = __unpack_raw(tx_raw_bytes, chain_id) tx = __unpack_raw(tx_raw_bytes, chain_id)
tx['nonce'] = add_0x(hex(tx['nonce'])) 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): 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) j = JSONRPCRequest(id_generator=id_generator)
o = j.template() o = j.template()
o['method'] = 'eth_getTransactionByHash' o['method'] = 'eth_getTransactionByHash'
@ -201,6 +236,17 @@ def transaction(hsh, id_generator=None):
def transaction_by_block(hsh, idx, 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) j = JSONRPCRequest(id_generator=id_generator)
o = j.template() o = j.template()
o['method'] = 'eth_getTransactionByBlockHashAndIndex' o['method'] = 'eth_getTransactionByBlockHashAndIndex'
@ -210,6 +256,15 @@ def transaction_by_block(hsh, idx, id_generator=None):
def receipt(hsh, 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) j = JSONRPCRequest(id_generator=id_generator)
o = j.template() o = j.template()
o['method'] = 'eth_getTransactionReceipt' o['method'] = 'eth_getTransactionReceipt'
@ -218,6 +273,15 @@ def receipt(hsh, id_generator=None):
def raw(tx_raw_hex, 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) j = JSONRPCRequest(id_generator=id_generator)
o = j.template() o = j.template()
o['method'] = 'eth_sendRawTransaction' o['method'] = 'eth_sendRawTransaction'
@ -226,8 +290,23 @@ def raw(tx_raw_hex, id_generator=None):
class TxFactory: 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): def __init__(self, chain_spec, signer=None, gas_oracle=None, nonce_oracle=None):
self.gas_oracle = gas_oracle self.gas_oracle = gas_oracle
@ -237,6 +316,15 @@ class TxFactory:
def build_raw(self, tx): 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'] == '': if tx['to'] == None or tx['to'] == '':
tx['to'] = '0x' tx['to'] = '0x'
txe = EIP155Transaction(tx, tx['nonce'], tx['chainId']) txe = EIP155Transaction(tx, tx['nonce'], tx['chainId'])
@ -247,12 +335,34 @@ class TxFactory:
def build(self, tx, id_generator=None): 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) (tx_hash_hex, tx_raw_hex) = self.build_raw(tx)
o = raw(tx_raw_hex, id_generator=id_generator) o = raw(tx_raw_hex, id_generator=id_generator)
return (tx_hash_hex, o) return (tx_hash_hex, o)
def template(self, sender, recipient, use_nonce=False): 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_price = MINIMUM_FEE_PRICE
gas_limit = MINIMUM_FEE_UNITS gas_limit = MINIMUM_FEE_UNITS
if self.gas_oracle != None: if self.gas_oracle != None:
@ -276,18 +386,35 @@ class TxFactory:
def normalize(self, tx): 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']) txe = EIP155Transaction(tx, tx['nonce'], tx['chainId'])
txes = txe.serialize() txes = txe.serialize()
return { return {
'from': tx['from'], 'from': tx['from'],
'to': txes['to'], 'to': txes['to'],
'gasPrice': txes['gasPrice'], 'gasPrice': '0x' + compact(txes['gasPrice']),
'gas': txes['gas'], 'gas': '0x' + compact(txes['gas']),
'data': txes['data'], 'data': txes['data'],
} }
def finalize(self, tx, tx_format=TxFormat.JSONRPC, id_generator=None): 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: if tx_format == TxFormat.JSONRPC:
return self.build(tx, id_generator=id_generator) return self.build(tx, id_generator=id_generator)
elif tx_format == TxFormat.RLP_SIGNED: elif tx_format == TxFormat.RLP_SIGNED:
@ -296,6 +423,17 @@ class TxFactory:
def set_code(self, tx, data, update_fee=True): 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 tx['data'] = data
if update_fee: if update_fee:
tx['gas'] = TxFactory.fee tx['gas'] = TxFactory.fee
@ -307,6 +445,19 @@ class TxFactory:
def transact_noarg(self, method, contract_address, sender_address, tx_format=TxFormat.JSONRPC): 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 = ABIContractEncoder()
enc.method(method) enc.method(method)
data = enc.get() data = enc.get()
@ -316,7 +467,22 @@ class TxFactory:
return tx 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) j = JSONRPCRequest(id_generator)
o = j.template() o = j.template()
o['method'] = 'eth_call' o['method'] = 'eth_call'
@ -326,34 +492,38 @@ class TxFactory:
tx = self.template(sender_address, contract_address) tx = self.template(sender_address, contract_address)
tx = self.set_code(tx, data) tx = self.set_code(tx, data)
o['params'].append(self.normalize(tx)) o['params'].append(self.normalize(tx))
o['params'].append('latest') height = to_blockheight_param(height)
o['params'].append(height)
o = j.finalize(o) o = j.finalize(o)
return 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): 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 self.index = -1
tx_hash = add_0x(src['hash']) 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) self.hash = strip_0x(tx_hash)
if block != None:
self.apply_block(block)
try: try:
self.value = int(strip_0x(src['value']), 16) self.value = int(strip_0x(src['value']), 16)
except TypeError: except TypeError:
@ -378,6 +548,7 @@ class Tx:
inpt = src['input'] inpt = src['input']
except KeyError: except KeyError:
inpt = src['data'] inpt = src['data']
src['input'] = src['data']
if inpt != '0x': if inpt != '0x':
inpt = strip_0x(inpt) inpt = strip_0x(inpt)
@ -408,13 +579,27 @@ class Tx:
self.wire = None self.wire = None
self.tx_src = src
def src(self): def src(self):
"""Retrieve normalized representation source used to construct transaction object.
:rtype: dict
:returns: Transaction representation
"""
return self.tx_src return self.tx_src
@classmethod @classmethod
def src_normalize(self, src): 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) src = snake_and_camel(src)
if isinstance(src.get('v'), str): if isinstance(src.get('v'), str):
@ -430,16 +615,40 @@ class Tx:
def apply_receipt(self, rcpt): 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) rcpt = self.src_normalize(rcpt)
logg.debug('rcpt {}'.format(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: try:
status_number = int(rcpt['status'], 16) status_number = int(rcpt['status'], 16)
except TypeError: except TypeError:
status_number = int(rcpt['status']) status_number = int(rcpt['status'])
if status_number == 1: if rcpt['block_number'] == None:
self.status = Status.SUCCESS self.status = Status.PENDING
elif status_number == 0: else:
self.status = Status.ERROR 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 # TODO: replace with rpc receipt/transaction translator when available
contract_address = rcpt.get('contractAddress') contract_address = rcpt.get('contractAddress')
if contract_address == None: if contract_address == None:
@ -452,20 +661,43 @@ class Tx:
except TypeError: except TypeError:
self.gas_used = int(rcpt['gasUsed']) self.gas_used = int(rcpt['gasUsed'])
self.__rcpt_block_hash = rcpt['block_hash']
def apply_block(self, block): 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 self.block = block
def generate_wire(self, chain_spec): def generate_wire(self, chain_spec):
b = pack(self.src(), chain_spec) """Generate transaction wire format.
self.wire = add_0x(b.hex())
: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 @staticmethod
def from_src(src, block=None): def from_src(src, block=None, rcpt=None):
return Tx(src, block=block) """Creates a new Tx object.
Alias of constructor.
"""
return Tx(src, block=block, rcpt=rcpt)
def __str__(self): def __str__(self):
@ -480,13 +712,18 @@ class Tx:
def to_human(self): def to_human(self):
"""Human-readable string dump of transaction contents.
:rtype: str
:returns: Contents
"""
s = """hash {} s = """hash {}
from {} from {}
to {} to {}
value {} value {}
nonce {} nonce {}
gasPrice {} gas_price {}
gasLimit {} gas_limit {}
input {} input {}
""".format( """.format(
self.hash, self.hash,
@ -500,13 +737,24 @@ input {}
) )
if self.status != Status.PENDING: if self.status != Status.PENDING:
s += """gasUsed {} s += """gas_used {}
""".format( """.format(
self.gas_used, self.gas_used,
) )
s += 'status ' + self.status.name + '\n' 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: if self.contract != None:
s += """contract {} s += """contract {}
""".format( """.format(
@ -520,4 +768,3 @@ input {}
) )
return s 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_provider = os.environ.get('RPC_PROVIDER', 'http://localhost:8545')
rpc = EthHTTPConnection(rpc_provider) rpc = EthHTTPConnection(rpc_provider)
o = balance(address) o = balance(address)
print(o)
r = rpc.do(o) r = rpc.do(o)
clean_address = strip_0x(address) clean_address = strip_0x(address)

View File

@ -3,22 +3,23 @@ import os
import sys import sys
# local imports # local imports
from chainlib.jsonrpc import jsonrpc_template from chainlib.jsonrpc import JSONRPCRequest
from chainlib.eth.connection import EthHTTPConnection from chainlib.eth.connection import EthHTTPConnection
# set up node connection and execute rpc call # set up node connection and execute rpc call
rpc_provider = os.environ.get('RPC_PROVIDER', 'http://localhost:8545') rpc_provider = os.environ.get('RPC_PROVIDER', 'http://localhost:8545')
rpc = EthHTTPConnection(rpc_provider) conn = EthHTTPConnection(rpc_provider)
# check the connection # check the connection
if not rpc.check(): if not conn.check():
sys.stderr.write('node {} not usable\n'.format(rpc_provider)) sys.stderr.write('node {} not usable\n'.format(rpc_provider))
sys.exit(1) sys.exit(1)
# build and send rpc call # build and send rpc call
o = jsonrpc_template() g = JSONRPCRequest()
o = g.template()
o['method'] = 'eth_blockNumber' o['method'] = 'eth_blockNumber'
r = rpc.do(o) r = conn.do(o)
# interpret result for humans # interpret result for humans
try: 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 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 # eth transactions need an explicit chain parameter as part of their signature
chain_spec = ChainSpec.from_chain_str('evm:ethereum:1') chain_spec = ChainSpec.from_chain_str('evm:ethereum:1')
# create keystore and signer # create keystore and signer
keystore = DictKeystore() keystore = DictKeystore()
signer = EIP155Signer(keystore) 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() recipient_address = keystore.new()
# set up node connection # 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 pysha3==1.0.2
hexathon~=0.0.1a7 hexathon~=0.0.1a8
websocket-client==0.57.0 websocket-client==0.57.0
potaahto~=0.0.1a1 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] [metadata]
name = chainlib-eth name = chainlib-eth
version = 0.0.5a1 version = 0.0.8a2
description = Ethereum implementation of the chainlib interface description = Ethereum implementation of the chainlib interface
author = Louis Holbrook author = Louis Holbrook
author_email = dev@holbrook.no author_email = dev@holbrook.no
@ -24,6 +24,7 @@ licence_files =
LICENSE.txt LICENSE.txt
[options] [options]
include_package_data = True
python_requires = >= 3.6 python_requires = >= 3.6
packages = packages =
chainlib.eth chainlib.eth
@ -40,4 +41,5 @@ console_scripts =
eth-get = chainlib.eth.runnable.get:main eth-get = chainlib.eth.runnable.get:main
eth-decode = chainlib.eth.runnable.decode:main eth-decode = chainlib.eth.runnable.decode:main
eth-info = chainlib.eth.runnable.info:main eth-info = chainlib.eth.runnable.info:main
eth-nonce = chainlib.eth.runnable.count:main
eth = chainlib.eth.runnable.info: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, strip_0x,
add_0x, add_0x,
) )
from chainlib.eth.block import Block
logging.basicConfig(level=logging.DEBUG) logging.basicConfig(level=logging.DEBUG)
logg = logging.getLogger() logg = logging.getLogger()
@ -35,6 +36,7 @@ logg = logging.getLogger()
class TxTestCase(EthTesterCase): class TxTestCase(EthTesterCase):
def test_tx_reciprocal(self): def test_tx_reciprocal(self):
nonce_oracle = RPCNonceOracle(self.accounts[0], self.rpc) nonce_oracle = RPCNonceOracle(self.accounts[0], self.rpc)
gas_oracle = RPCGasOracle(self.rpc) gas_oracle = RPCGasOracle(self.rpc)
@ -75,5 +77,70 @@ class TxTestCase(EthTesterCase):
logg.debug('r {}'.format(tx_signed_raw_bytes_recovered.hex())) logg.debug('r {}'.format(tx_signed_raw_bytes_recovered.hex()))
self.assertEqual(tx_signed_raw_bytes, tx_signed_raw_bytes_recovered) 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__': if __name__ == '__main__':
unittest.main() unittest.main()