commit d13708d3ce9639a6af9962f7fbd68859605d5996
parent 8374d830450a560430ce2c615172700c20440390
Author: Louis Holbrook <accounts-gitlab@holbrook.no>
Date: Sat, 21 Aug 2021 07:27:40 +0000
Add docstrings
Diffstat:
33 files changed, 1338 insertions(+), 590 deletions(-)
diff --git a/CHANGELOG b/CHANGELOG
@@ -1,2 +1,3 @@
- 0.0.5-pending
* Receive all ethereum components from chainlib package
+ * Make settings configurable
diff --git a/MANIFEST.in b/MANIFEST.in
@@ -1 +1 @@
-include *requirements.txt LICENSE
+include *requirements.txt LICENSE chainlib/eth/data/config/*
diff --git a/chainlib/eth/address.py b/chainlib/eth/address.py
@@ -1,4 +1,4 @@
-# third-party imports
+# external imports
import sha3
from hexathon import (
strip_0x,
@@ -11,3 +11,34 @@ from crypto_dev_signer.encoding import (
)
to_checksum = to_checksum_address
+
+
+class AddressChecksum:
+ """Address checksummer implementation.
+
+ Primarily for use with chainlib.cli.wallet.Wallet
+ """
+
+ @classmethod
+ def valid(cls, v):
+ """Check if address is a valid checksum address
+
+ :param v: Address value, in hex
+ :type v: str
+ :rtype: bool
+ :returns: True if valid checksum
+ """
+ return is_checksum_address(v)
+
+
+ @classmethod
+ def sum(cls, v):
+ """Create checksum from address
+
+ :param v: Address value, in hex
+ :type v: str
+ :raises ValueError: Invalid address
+ :rtype: str
+ :returns: Checksum address
+ """
+ return to_checksum_address(v)
diff --git a/chainlib/eth/block.py b/chainlib/eth/block.py
@@ -1,14 +1,19 @@
-# third-party imports
+# external imports
from chainlib.jsonrpc import JSONRPCRequest
-from chainlib.eth.tx import Tx
+from chainlib.block import Block as BaseBlock
from hexathon import (
add_0x,
strip_0x,
even,
)
+# local imports
+from chainlib.eth.tx import Tx
+
def block_latest(id_generator=None):
+ """Implements chainlib.interface.ChainInterface method
+ """
j = JSONRPCRequest(id_generator)
o = j.template()
o['method'] = 'eth_blockNumber'
@@ -16,6 +21,8 @@ def block_latest(id_generator=None):
def block_by_hash(hsh, include_tx=True, id_generator=None):
+ """Implements chainlib.interface.ChainInterface method
+ """
j = JSONRPCRequest(id_generator)
o = j.template()
o['method'] = 'eth_getBlockByHash'
@@ -25,6 +32,8 @@ def block_by_hash(hsh, include_tx=True, id_generator=None):
def block_by_number(n, include_tx=True, id_generator=None):
+ """Implements chainlib.interface.ChainInterface method
+ """
nhx = add_0x(even(hex(n)[2:]))
j = JSONRPCRequest(id_generator)
o = j.template()
@@ -35,6 +44,15 @@ def block_by_number(n, include_tx=True, id_generator=None):
def transaction_count(block_hash, id_generator=None):
+ """Generate json-rpc query to get transaction count of block
+
+ :param block_hash: Block hash, in hex
+ :type block_hash: str
+ :param id_generator: JSONRPC id generator
+ :type id_generator: JSONRPCIdGenerator
+ :rtype: dict
+ :returns: rpc query object
+ """
j = JSONRPCRequest(id_generator)
o = j.template()
o['method'] = 'eth_getBlockTransactionCountByHash'
@@ -42,7 +60,13 @@ def transaction_count(block_hash, id_generator=None):
return j.finalize(o)
-class Block:
+class Block(BaseBlock):
+ """Encapsulates an Ethereum block
+
+ :param src: Block representation data
+ :type src: dict
+ :todo: Add hex to number parse to normalize
+ """
def __init__(self, src):
self.hash = src['hash']
@@ -58,22 +82,21 @@ class Block:
self.timestamp = int(src['timestamp'])
- def src(self):
- return self.block_src
-
-
- def 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))
-
+ def get_tx(self, tx_hash):
+ i = 0
+ idx = -1
+ tx_hash = add_0x(tx_hash)
+ for tx in self.txs:
+ tx_hash_block = None
+ try:
+ tx_hash_block = add_0x(tx['hash'])
+ except TypeError:
+ tx_hash_block = add_0x(tx)
+ if tx_hash_block == tx_hash:
+ idx = i
+ break
+ i += 1
+ if idx == -1:
+ raise AttributeError('tx {} not found in block {}'.format(tx_hash, self.hash))
+ return idx
- @staticmethod
- def from_src(src):
- return Block(src)
diff --git a/chainlib/eth/chain.py b/chainlib/eth/chain.py
@@ -2,6 +2,13 @@ from chainlib.jsonrpc import JSONRPCRequest
def network_id(id_generator=None):
+ """Generate json-rpc query to retrieve network id from node
+
+ :param id_generator: JSON-RPC id generator
+ :type id_generator: JSONRPCIdGenerator
+ :rtype: dict
+ :returns: rpc query object
+ """
j = JSONRPCRequest(id_generator=id_generator)
o = j.template()
o['method'] = 'net_version'
diff --git a/chainlib/eth/cli.py b/chainlib/eth/cli.py
@@ -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
diff --git a/chainlib/eth/connection.py b/chainlib/eth/connection.py
@@ -43,8 +43,33 @@ logg = logging.getLogger(__name__)
class EthHTTPConnection(JSONRPCHTTPConnection):
+ """HTTP Interface for Ethereum node JSON-RPC
+
+ :todo: support https
+ """
def wait(self, tx_hash_hex, delay=0.5, timeout=0.0, error_parser=error_parser, id_generator=None):
+ """Poll for confirmation of a transaction on network.
+
+ Returns the result of the transaction if it was successfully executed on the network, and raises RevertEthException if execution fails.
+
+ This is a blocking call.
+
+ :param tx_hash_hex: Transaction hash to wait for, hex
+ :type tx_hash_hex: str
+ :param delay: Polling interval
+ :type delay: float
+ :param timeout: Max time to wait for confirmation (0 = no timeout)
+ :type timeout: float
+ :param error_parser: json-rpc response error parser
+ :type error_parser: chainlib.jsonrpc.ErrorParser
+ :param id_generator: json-rpc id generator
+ :type id_generator: chainlib.jsonrpc.JSONRPCIdGenerator
+ :raises TimeoutError: Timeout reached
+ :raises chainlib.eth.error.RevertEthException: Transaction confirmed but failed
+ :rtype: dict
+ :returns: Transaction receipt
+ """
t = datetime.datetime.utcnow()
i = 0
while True:
@@ -59,13 +84,13 @@ class EthHTTPConnection(JSONRPCHTTPConnection):
)
req.add_header('Content-Type', 'application/json')
data = json.dumps(o)
- logg.debug('(HTTP) poll receipt attempt {} {}'.format(i, data))
+ logg.debug('({}) poll receipt attempt {} {}'.format(str(self), i, data))
res = urlopen(req, data=data.encode('utf-8'))
r = json.load(res)
e = jsonrpc_result(r, error_parser)
if e != None:
- logg.debug('(HTTP) poll receipt completed {}'.format(r))
+ logg.debug('({}) poll receipt completed {}'.format(str(self), r))
logg.debug('e {}'.format(strip_0x(e['status'])))
if strip_0x(e['status']) == '00':
raise RevertEthException(tx_hash_hex)
@@ -80,7 +105,17 @@ class EthHTTPConnection(JSONRPCHTTPConnection):
i += 1
+ def __str__(self):
+ return 'ETH HTTP JSONRPC'
+
+
def check_rpc(self, id_generator=None):
+ """Execute Ethereum specific json-rpc query to (superficially) check whether node is sane.
+
+ :param id_generator: json-rpc id generator
+ :type id_generator: chainlib.jsonrpc.JSONRPCIdGenerator
+ :raises Exception: Any exception indicates an invalid node
+ """
j = JSONRPCRequest(id_generator)
req = j.template()
req['method'] = 'net_version'
@@ -89,12 +124,29 @@ class EthHTTPConnection(JSONRPCHTTPConnection):
class EthUnixConnection(JSONRPCUnixConnection):
+ """Unix socket implementation of Ethereum JSON-RPC
+ """
def wait(self, tx_hash_hex, delay=0.5, timeout=0.0, error_parser=error_parser):
+ """See EthHTTPConnection. Not yet implemented for unix socket.
+ """
raise NotImplementedError('Not yet implemented for unix socket')
def sign_transaction_to_rlp(chain_spec, doer, tx):
+ """Generate a signature query and execute it against a json-rpc signer backend.
+
+ Uses the `eth_signTransaction` json-rpc method, generated by chainlib.eth.sign.sign_transaction.
+
+ :param chain_spec: Chain spec to use for EIP155 signature.
+ :type chain_spec: chainlib.chain.ChainSpec
+ :param doer: Signer rpc backend
+ :type doer: chainlib.connection.RPCConnection implementing json-rpc
+ :param tx: Transaction object
+ :type tx: dict
+ :rtype: bytes
+ :returns: Ethereum signature
+ """
txs = tx.serialize()
logg.debug('serializing {}'.format(txs))
# TODO: because some rpc servers may fail when chainId is included, we are forced to spend cpu here on this
@@ -110,27 +162,66 @@ def sign_transaction_to_rlp(chain_spec, doer, tx):
def sign_message(doer, msg):
+ """Sign arbitrary data using the Ethereum message signer protocol.
+
+ :param doer: Signer rpc backend
+ :type doer: chainlib.connection.RPCConnection with json-rpc
+ :param msg: Message to sign, in hex
+ :type msg: str
+ :rtype: str
+ :returns: Signature, hex
+ """
o = sign_message(msg)
return doer(o)
class EthUnixSignerConnection(EthUnixConnection):
+ """Connects rpc signer methods to Unix socket connection interface
+ """
def sign_transaction_to_rlp(self, tx):
+ """Sign transaction using unix socket rpc.
+
+ :param tx: Transaction object
+ :type tx: dict
+ :rtype: See chainlin.eth.connection.sign_transaction_to_rlp
+ :returns: See chainlin.eth.connection.sign_transaction_to_rlp
+ """
return sign_transaction_to_rlp(self.chain_spec, self.do, tx)
- def sign_message(self, tx):
- return sign_message(self.do, tx)
+ def sign_message(self, msg):
+ """Sign message using unix socket json-rpc.
+
+ :param msg: Message to sign, in hex
+ :type msg: str
+ :rtype: See chainlin.eth.connection.sign_message
+ :returns: See chainlin.eth.connection.sign_message
+ """
+ return sign_message(self.do, msg)
class EthHTTPSignerConnection(EthHTTPConnection):
def sign_transaction_to_rlp(self, tx):
+ """Sign transaction using http json-rpc.
+
+ :param tx: Transaction object
+ :type tx: dict
+ :rtype: See chainlin.eth.connection.sign_transaction_to_rlp
+ :returns: See chainlin.eth.connection.sign_transaction_to_rlp
+ """
return sign_transaction_to_rlp(self.chain_spec, self.do, tx)
def sign_message(self, tx):
+ """Sign message using http json-rpc.
+
+ :param msg: Message to sign, in hex
+ :type msg: str
+ :rtype: See chainlin.eth.connection.sign_message
+ :returns: See chainlin.eth.connection.sign_message
+ """
return sign_message(self.do, tx)
diff --git a/chainlib/eth/constant.py b/chainlib/eth/constant.py
@@ -2,4 +2,5 @@ ZERO_ADDRESS = '0x{:040x}'.format(0)
ZERO_CONTENT = '0x{:064x}'.format(0)
MINIMUM_FEE_UNITS = 21000
MINIMUM_FEE_PRICE = 1000000000
+DEFAULT_FEE_LIMIT = 8000000
MAX_UINT = int('0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff', 16)
diff --git a/chainlib/eth/contract.py b/chainlib/eth/contract.py
@@ -15,14 +15,14 @@ from chainlib.block import BlockSpec
from chainlib.jsonrpc import JSONRPCRequest
from .address import to_checksum_address
-#logg = logging.getLogger(__name__)
-logg = logging.getLogger()
+logg = logging.getLogger(__name__)
re_method = r'^[a-zA-Z0-9_]+$'
class ABIContractType(enum.Enum):
-
+ """Data types used by ABI encoders
+ """
BYTES32 = 'bytes32'
BYTES4 = 'bytes4'
UINT256 = 'uint256'
@@ -36,14 +36,16 @@ dynamic_contract_types = [
class ABIContract:
-
+ """Base class for Ethereum smart contract encoder
+ """
def __init__(self):
self.types = []
self.contents = []
class ABIMethodEncoder(ABIContract):
-
+ """Generate ABI method signatures from method signature string.
+ """
def __init__(self):
super(ABIMethodEncoder, self).__init__()
self.method_name = None
@@ -51,6 +53,12 @@ class ABIMethodEncoder(ABIContract):
def method(self, m):
+ """Set method name.
+
+ :param m: Method name
+ :type m: str
+ :raises ValueError: Invalid method name
+ """
if re.match(re_method, m) == None:
raise ValueError('Invalid method {}, must match regular expression {}'.format(re_method))
self.method_name = m
@@ -58,12 +66,26 @@ class ABIMethodEncoder(ABIContract):
def get_method(self):
+ """Return currently set method signature string.
+
+ :rtype: str
+ :returns: Method signature
+ """
if self.method_name == None:
return ''
return '{}({})'.format(self.method_name, ','.join(self.method_contents))
def typ(self, v):
+ """Add argument type to argument vector.
+
+ Method name must be set before this is called.
+
+ :param v: Type to add
+ :type v: chainlib.eth.contract.ABIContractType
+ :raises AttributeError: Type set before method name
+ :raises TypeError: Invalid type
+ """
if self.method_name == None:
raise AttributeError('method name must be set before adding types')
if not isinstance(v, ABIContractType):
@@ -78,9 +100,16 @@ class ABIMethodEncoder(ABIContract):
class ABIContractDecoder(ABIContract):
-
+ """Decode serialized ABI contract input data to corresponding python primitives.
+ """
def typ(self, v):
+ """Add type to argument array to parse input against.
+
+ :param v: Type
+ :type v: chainlib.eth.contract.ABIContractType
+ :raises TypeError: Invalid type
+ """
if not isinstance(v, ABIContractType):
raise TypeError('method type not valid; expected {}, got {}'.format(type(ABIContractType).__name__, type(v).__name__))
self.types.append(v.value)
@@ -88,32 +117,74 @@ class ABIContractDecoder(ABIContract):
def val(self, v):
+ """Add value to value array.
+
+ :param v: Value, in hex
+ :type v: str
+ """
self.contents.append(v)
logg.debug('content is now {}'.format(self.contents))
def uint256(self, v):
+ """Parse value as uint256.
+
+ :param v: Value, in hex
+ :type v: str
+ :rtype: int
+ :returns: Int value
+ """
return int(v, 16)
def bytes32(self, v):
+ """Parse value as bytes32.
+
+ :param v: Value, in hex
+ :type v: str
+ :rtype: str
+ :returns: Value, in hex
+ """
return v
def bool(self, v):
+ """Parse value as bool.
+
+ :param v: Value, in hex
+ :type v: str
+ :rtype: bool
+ :returns: Value
+ """
return bool(self.uint256(v))
def boolean(self, v):
+ """Alias of chainlib.eth.contract.ABIContractDecoder.bool
+ """
return bool(self.uint256(v))
def address(self, v):
+ """Parse value as address.
+
+ :param v: Value, in hex
+ :type v: str
+ :rtype: str
+ :returns: Value. in hex
+ """
a = strip_0x(v)[64-40:]
return to_checksum_address(a)
def string(self, v):
+ """Parse value as string.
+
+ :param v: Value, in hex
+ :type v: str
+ :rtype: str
+ :returns: Value
+ """
s = strip_0x(v)
b = bytes.fromhex(s)
cursor = 0
@@ -131,18 +202,23 @@ class ABIContractDecoder(ABIContract):
def decode(self):
+ """Apply decoder on value array using argument type array.
+
+ :rtype: list
+ :returns: List of decoded values
+ """
r = []
logg.debug('contents {}'.format(self.contents))
for i in range(len(self.types)):
m = getattr(self, self.types[i])
s = self.contents[i]
- logg.debug('{} {} {} {} {}'.format(i, m, self.types[i], self.contents[i], s))
- #r.append(m(s.hex()))
r.append(m(s))
return r
def get(self):
+ """Alias of chainlib.eth.contract.ABIContractDecoder.decode
+ """
return self.decode()
@@ -151,7 +227,10 @@ class ABIContractDecoder(ABIContract):
class ABIContractLogDecoder(ABIMethodEncoder, ABIContractDecoder):
-
+ """Decoder utils for log entries of an Ethereum network transaction receipt.
+
+ Uses chainlib.eth.contract.ABIContractDecoder.decode to render output from template.
+ """
def __init__(self):
super(ABIContractLogDecoder, self).__init__()
self.method_name = None
@@ -159,20 +238,45 @@ class ABIContractLogDecoder(ABIMethodEncoder, ABIContractDecoder):
def topic(self, event):
+ """Set topic to match.
+
+ :param event: Topic name
+ :type event: str
+ """
self.method(event)
def get_method_signature(self):
+ """Generate topic signature from set topic.
+
+ :rtype: str
+ :returns: Topic signature, in hex
+ """
s = self.get_method()
return keccak256_string_to_hex(s)
def typ(self, v):
+ """Add type to event argument array.
+
+ :param v: Type
+ :type v: chainlib.eth.contract.ABIContractType
+ """
super(ABIContractLogDecoder, self).typ(v)
self.types.append(v.value)
def apply(self, topics, data):
+ """Set log entry data to parse.
+
+ After set, self.decode can be used to render the output.
+
+ :param topics: The topics array of the receipt, list of hex
+ :type topics: list
+ :param data: Non-indexed data, in hex
+ :type data: str
+ :raises ValueError: Topic of input does not match topic set in parser
+ """
t = self.get_method_signature()
if topics[0] != t:
raise ValueError('topic mismatch')
@@ -189,6 +293,11 @@ class ABIContractEncoder(ABIMethodEncoder):
def uint256(self, v):
+ """Encode value to uint256 and add to input value vector.
+
+ :param v: Integer value
+ :type v: int
+ """
v = int(v)
b = v.to_bytes(32, 'big')
self.contents.append(b.hex())
@@ -197,28 +306,52 @@ class ABIContractEncoder(ABIMethodEncoder):
def bool(self, v):
+ """Alias of chainlib.eth.contract.ABIContractEncoder.boolean.
+ """
return self.boolean(v)
def boolean(self, v):
+ """Encode value to boolean and add to input value vector.
+
+ :param v: Trueish or falsish value
+ :type v: any
+ :rtype: See chainlib.eth.contract.ABIContractEncoder.uint256
+ :returns: See chainlib.eth.contract.ABIContractEncoder.uint256
+ """
if bool(v):
return self.uint256(1)
return self.uint256(0)
def address(self, v):
+ """Encode value to address and add to input value vector.
+
+ :param v: Ethereum address, in hex
+ :type v: str
+ """
self.bytes_fixed(32, v, 20)
self.types.append(ABIContractType.ADDRESS)
self.__log_latest(v)
def bytes32(self, v):
+ """Encode value to bytes32 and add to input value vector.
+
+ :param v: Bytes, in hex
+ :type v: str
+ """
self.bytes_fixed(32, v)
self.types.append(ABIContractType.BYTES32)
self.__log_latest(v)
def bytes4(self, v):
+ """Encode value to bytes4 and add to input value vector.
+
+ :param v: Bytes, in hex
+ :type v: str
+ """
self.bytes_fixed(4, v)
self.types.append(ABIContractType.BYTES4)
self.__log_latest(v)
@@ -226,6 +359,11 @@ class ABIContractEncoder(ABIMethodEncoder):
def string(self, v):
+ """Encode value to string and add to input value vector.
+
+ :param v: String input
+ :type v: str
+ """
b = v.encode('utf-8')
l = len(b)
contents = l.to_bytes(32, 'big')
@@ -239,6 +377,16 @@ class ABIContractEncoder(ABIMethodEncoder):
def bytes_fixed(self, mx, v, exact=0):
+ """Add arbirary length byte data to value vector.
+
+ :param mx: Max length of input data.
+ :type mx: int
+ :param v: Byte input, hex or bytes
+ :type v: str | bytes
+ :param exact: Fail parsing if input does not translate to given byte length.
+ :type exact: int
+ :raises ValueError: Input length or input format mismatch.
+ """
typ = type(v).__name__
if typ == 'str':
v = strip_0x(v)
@@ -259,9 +407,10 @@ class ABIContractEncoder(ABIMethodEncoder):
raise ValueError('invalid input {}'.format(typ))
self.contents.append(v.ljust(64, '0'))
-
def get_method_signature(self):
+ """Return abi encoded signature of currently set method.
+ """
s = self.get_method()
if s == '':
return s
@@ -269,6 +418,11 @@ class ABIContractEncoder(ABIMethodEncoder):
def get_contents(self):
+ """Encode value array.
+
+ :rtype: str
+ :returns: ABI encoded values, in hex
+ """
direct_contents = ''
pointer_contents = ''
l = len(self.types)
@@ -291,10 +445,19 @@ class ABIContractEncoder(ABIMethodEncoder):
def get(self):
+ """Alias of chainlib.eth.contract.ABIContractEncoder.encode
+ """
return self.encode()
def encode(self):
+ """Encode method and value array.
+
+ The data generated by this method is the literal data used as input to contract calls or transactions.
+
+ :rtype: str
+ :returns: ABI encoded contract input data, in hex
+ """
m = self.get_method_signature()
c = self.get_contents()
return m + c
@@ -306,6 +469,13 @@ class ABIContractEncoder(ABIMethodEncoder):
def abi_decode_single(typ, v):
+ """Convenience function to decode a single ABI encoded value against a given type.
+
+ :param typ: Type to parse value as
+ :type typ: chainlib.eth.contract.ABIContractEncoder
+ :param v: Value to parse, in hex
+ :type v: str
+ """
d = ABIContractDecoder()
d.typ(typ)
d.val(v)
@@ -314,6 +484,17 @@ def abi_decode_single(typ, v):
def code(address, block_spec=BlockSpec.LATEST, id_generator=None):
+ """Generate json-rpc query to retrieve code stored at an Ethereum address.
+
+ :param address: Address to use for query, in hex
+ :type address: str
+ :param block_spec: Block height spec
+ :type block_spec: chainlib.block.BlockSpec
+ :param id_generator: json-rpc id generator
+ :type id_generator: chainlib.jsonrpc.JSONRPCIdGenerator
+ :rtype: dict
+ :returns: rpc query object
+ """
block_height = None
if block_spec == BlockSpec.LATEST:
block_height = 'latest'
diff --git a/chainlib/eth/data/config/config.ini b/chainlib/eth/data/config/config.ini
@@ -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 =
diff --git a/chainlib/eth/error.py b/chainlib/eth/error.py
@@ -1,23 +1,33 @@
# local imports
from chainlib.error import ExecutionError
+
class EthException(Exception):
+ """Base class for all Ethereum related errors.
+ """
pass
class RevertEthException(EthException, ExecutionError):
+ """Raised when an rpc call or transaction reverts.
+ """
pass
class NotFoundEthException(EthException):
+ """Raised when rpc query is made against an identifier that is not known by the node.
+ """
pass
class RequestMismatchException(EthException):
+ """Raised when a request data parser is given unexpected input data.
+ """
pass
class DefaultErrorParser:
-
+ """Generate eth specific exception for the default json-rpc query error parser.
+ """
def translate(self, error):
return EthException('default parser code {}'.format(error))
diff --git a/chainlib/eth/gas.py b/chainlib/eth/gas.py
@@ -1,7 +1,7 @@
# standard imports
import logging
-# third-party imports
+# external imports
from hexathon import (
add_0x,
strip_0x,
@@ -16,6 +16,8 @@ from chainlib.eth.tx import (
TxFormat,
raw,
)
+from chainlib.eth.jsonrpc import to_blockheight_param
+from chainlib.block import BlockSpec
from chainlib.eth.constant import (
MINIMUM_FEE_UNITS,
)
@@ -24,22 +26,48 @@ logg = logging.getLogger(__name__)
def price(id_generator=None):
+ """Generate json-rpc query to retrieve current network gas price guess from node.
+
+ :param id_generator: json-rpc id generator
+ :type id_generator: chainlib.connection.JSONRPCIdGenerator
+ :rtype: dict
+ :returns: rpc query object
+ """
j = JSONRPCRequest(id_generator)
o = j.template()
o['method'] = 'eth_gasPrice'
return j.finalize(o)
-def balance(address, id_generator=None):
+def balance(address, id_generator=None, height=BlockSpec.LATEST):
+ """Generate json-rpc query to retrieve gas balance of address.
+
+ :param address: Address to query balance for, in hex
+ :type address: str
+ :param id_generator: json-rpc id generator
+ :type id_generator: chainlib.connection.JSONRPCIdGenerator
+ :param height: Block height specifier
+ :type height: chainlib.block.BlockSpec
+ :rtype: dict
+ :returns: rpc query object
+ """
j = JSONRPCRequest(id_generator)
o = j.template()
o['method'] = 'eth_getBalance'
o['params'].append(address)
- o['params'].append('latest')
+ height = to_blockheight_param(height)
+ o['params'].append(height)
return j.finalize(o)
def parse_balance(balance):
+ """Parse result of chainlib.eth.gas.balance rpc query
+
+ :param balance: rpc result value, in hex or int
+ :type balance: any
+ :rtype: int
+ :returns: Balance integer value
+ """
try:
r = int(balance, 10)
except ValueError:
@@ -48,10 +76,29 @@ def parse_balance(balance):
class Gas(TxFactory):
-
- def create(self, sender_address, recipient_address, value, tx_format=TxFormat.JSONRPC, id_generator=None):
+ """Gas transaction helper.
+ """
+
+ def create(self, sender_address, recipient_address, value, data=None, tx_format=TxFormat.JSONRPC, id_generator=None):
+ """Generate json-rpc query to execute gas transaction.
+
+ See parent class TxFactory for details on output format and general usage.
+
+ :param sender_address: Sender address, in hex
+ :type sender_address: str
+ :param recipient_address: Recipient address, in hex
+ :type recipient_address: str
+ :param value: Value of transaction, integer decimal value (wei)
+ :type value: int
+ :param data: Arbitrary input data, in hex. None means no data (vanilla gas transaction).
+ :type data: str
+ :param tx_format: Output format
+ :type tx_format: chainlib.eth.tx.TxFormat
+ """
tx = self.template(sender_address, recipient_address, use_nonce=True)
tx['value'] = value
+ if data != None:
+ tx['data'] = data
txe = EIP155Transaction(tx, tx['nonce'], tx['chainId'])
tx_raw = self.signer.sign_transaction_to_rlp(txe)
tx_raw_hex = add_0x(tx_raw.hex())
@@ -68,6 +115,17 @@ class Gas(TxFactory):
class RPCGasOracle:
+ """JSON-RPC only gas parameter helper.
+
+ :param conn: RPC connection
+ :type conn: chainlib.connection.RPCConnection
+ :param code_callback: Callback method to evaluate gas usage for method and inputs.
+ :type code_callback: method taking abi encoded input data as single argument
+ :param min_price: Override gas price if less than given value
+ :type min_price: int
+ :param id_generator: json-rpc id generator
+ :type id_generator: chainlib.connection.JSONRPCIdGenerator
+ """
def __init__(self, conn, code_callback=None, min_price=1, id_generator=None):
self.conn = conn
@@ -76,7 +134,20 @@ class RPCGasOracle:
self.id_generator = id_generator
- def get_gas(self, code=None):
+ def get_gas(self, code=None, input_data=None):
+ """Retrieve gas parameters from node.
+
+ If code is given, the set code callback will be used to estimate gas usage.
+
+ If code is not given or code callback is not set, the chainlib.eth.constant.MINIMUM_FEE_UNITS constant will be used. This gas limit will only be enough gas for a gas transaction without input data.
+
+ :param code: EVM execution code to evaluate against, in hex
+ :type code: str
+ :param input_data: Contract input data, in hex
+ :type input_data: str
+ :rtype: tuple
+ :returns: Gas price in wei, and gas limit in gas units
+ """
gas_price = 0
if self.conn != None:
o = price(id_generator=self.id_generator)
@@ -93,13 +164,39 @@ class RPCGasOracle:
class RPCPureGasOracle(RPCGasOracle):
-
+ """Convenience constructor for rpc gas oracle without minimum price.
+
+ :param conn: RPC connection
+ :type conn: chainlib.connection.RPCConnection
+ :param code_callback: Callback method to evaluate gas usage for method and inputs.
+ :type code_callback: method taking abi encoded input data as single argument
+ :param id_generator: json-rpc id generator
+ :type id_generator: chainlib.connection.JSONRPCIdGenerator
+ """
def __init__(self, conn, code_callback=None, id_generator=None):
super(RPCPureGasOracle, self).__init__(conn, code_callback=code_callback, min_price=0, id_generator=id_generator)
class OverrideGasOracle(RPCGasOracle):
+ """Gas parameter helper that can be selectively overridden.
+
+ If both price and limit are set, the conn parameter will not be used.
+
+ If either price or limit is set to None, the rpc in the conn value will be used to query the missing value.
+
+ If both are None, behaves the same as chainlib.eth.gas.RPCGasOracle.
+ :param price: Set exact gas price
+ :type price: int
+ :param limit: Set exact gas limit
+ :type limit: int
+ :param conn: RPC connection for fallback query
+ :type conn: chainlib.connection.RPCConnection
+ :param code_callback: Callback method to evaluate gas usage for method and inputs.
+ :type code_callback: method taking abi encoded input data as single argument
+ :param id_generator: json-rpc id generator
+ :type id_generator: chainlib.connection.JSONRPCIdGenerator
+ """
def __init__(self, price=None, limit=None, conn=None, code_callback=None, id_generator=None):
self.conn = None
self.code_callback = None
@@ -117,6 +214,8 @@ class OverrideGasOracle(RPCGasOracle):
def get_gas(self, code=None):
+ """See chainlib.eth.gas.RPCGasOracle.
+ """
r = None
fee_units = None
fee_price = None
diff --git a/chainlib/eth/jsonrpc.py b/chainlib/eth/jsonrpc.py
@@ -14,3 +14,31 @@
#106 Timeout Should be used when an action timedout.
#107 Conflict Should be used when an action conflicts with another (ongoing?) action.
+
+# external imports
+from hexathon import add_0x
+
+
+def to_blockheight_param(height):
+ """Translate blockheight specifier to Ethereum json-rpc blockheight argument.
+
+ :param height: Height argument
+ :type height: any
+ :rtype: str
+ :returns: Argument value
+ """
+ if height == None:
+ height = 'latest'
+ elif isinstance(height, str):
+ try:
+ height = int(height)
+ except ValueError:
+ pass
+ if isinstance(height, int):
+ if height == 0:
+ height = 'latest'
+ elif height < 0:
+ height = 'pending'
+ else:
+ height = add_0x(int(height).to_bytes(8, 'big').hex())
+ return height
diff --git a/chainlib/eth/log.py b/chainlib/eth/log.py
@@ -3,12 +3,18 @@ import sha3
class LogBloom:
-
+ """Helper for Ethereum receipt log bloom filters.
+ """
def __init__(self):
self.content = bytearray(256)
def add(self, element):
+ """Add topic element to filter.
+
+ :param element: Topic element
+ :type element: bytes
+ """
if not isinstance(element, bytes):
raise ValueError('element must be bytes')
h = sha3.keccak_256()
diff --git a/chainlib/eth/nonce.py b/chainlib/eth/nonce.py
@@ -1,4 +1,4 @@
-# third-party imports
+# external imports
from hexathon import (
add_0x,
strip_0x,
@@ -8,17 +8,40 @@ from hexathon import (
from chainlib.jsonrpc import JSONRPCRequest
-def nonce(address, id_generator=None):
+def nonce(address, confirmed=False, id_generator=None):
+ """Generate json-rpc query to retrieve next nonce of address from node.
+
+ :param address: Address to retrieve nonce for, in hex
+ :type address: str
+ :param id_generator: json-rpc id generator
+ :type id_generator: chainlib.connection.JSONRPCIdGenerator
+ :rtype: dict
+ :returns: rpc query object
+ """
j = JSONRPCRequest(id_generator)
o = j.template()
o['method'] = 'eth_getTransactionCount'
o['params'].append(address)
o['params'].append('pending')
+ if confirmed:
+ o['params'].append('latest')
+ else:
+ o['params'].append('pending')
return j.finalize(o)
+def nonce_confirmed(address, id_generator=None):
+ return nonce(address, confirmed=True, id_generator=id_generator)
+
+
class NonceOracle:
+ """Base class for the nonce parameter helpers.
+ :param address: Address to retireve nonce for, in hex
+ :type address: str
+ :param id_generator: json-rpc id generator
+ :type id_generator: chainlib.connection.JSONRPCIdGenerator
+ """
def __init__(self, address, id_generator=None):
self.address = address
self.id_generator = id_generator
@@ -26,23 +49,45 @@ class NonceOracle:
def get_nonce(self):
+ """Load initial nonce value.
+ """
raise NotImplementedError('Class must be extended')
def next_nonce(self):
+ """Return next nonce value and advance.
+
+ :rtype: int
+ :returns: Next nonce for address.
+ """
n = self.nonce
self.nonce += 1
return n
class RPCNonceOracle(NonceOracle):
-
+ """JSON-RPC only nonce parameter helper.
+
+ :param address: Address to retireve nonce for, in hex
+ :type address: str
+ :param conn: RPC connection
+ :type conn: chainlib.connection.RPCConnection
+ :param id_generator: json-rpc id generator
+ :type id_generator: chainlib.connection.JSONRPCIdGenerator
+ """
def __init__(self, address, conn, id_generator=None):
self.conn = conn
super(RPCNonceOracle, self).__init__(address, id_generator=id_generator)
def get_nonce(self):
+ """Load and return nonce value from network.
+
+ Note! First call to next_nonce after calling get_nonce will return the same value!
+
+ :rtype: int
+ :returns: Initial nonce
+ """
o = nonce(self.address, id_generator=self.id_generator)
r = self.conn.do(o)
n = strip_0x(r)
@@ -50,14 +95,28 @@ class RPCNonceOracle(NonceOracle):
class OverrideNonceOracle(NonceOracle):
+ """Manually set initial nonce value.
- def __init__(self, address, nonce):
- self.nonce = nonce
- super(OverrideNonceOracle, self).__init__(address)
+ :param address: Address to retireve nonce for, in hex
+ :type address: str
+ :param nonce: Nonce value
+ :type nonce: int
+ :param id_generator: json-rpc id generator (not used)
+ :type id_generator: chainlib.connection.JSONRPCIdGenerator
+ """
+ def __init__(self, address, nonce, id_generator=None):
+ self.initial_nonce = nonce
+ self.nonce = self.initial_nonce
+ super(OverrideNonceOracle, self).__init__(address, id_generator=id_generator)
def get_nonce(self):
- return self.nonce
+ """Returns initial nonce value set at object construction.
+
+ :rtype: int
+ :returns: Initial nonce value.
+ """
+ return self.initial_nonce
DefaultNonceOracle = RPCNonceOracle
diff --git a/chainlib/eth/pytest/fixtures_ethtester.py b/chainlib/eth/pytest/fixtures_ethtester.py
@@ -17,7 +17,7 @@ from chainlib.connection import (
from chainlib.eth.unittest.ethtester import create_tester_signer
from chainlib.eth.address import to_checksum_address
-logg = logging.getLogger() #__name__)
+logg = logging.getLogger(__name__)
@pytest.fixture(scope='function')
@@ -37,13 +37,6 @@ def call_sender(
eth_accounts,
):
return eth_accounts[0]
-#
-#
-#@pytest.fixture(scope='function')
-#def eth_signer(
-# init_eth_tester,
-# ):
-# return init_eth_tester
@pytest.fixture(scope='function')
diff --git a/chainlib/eth/runnable/balance.py b/chainlib/eth/runnable/balance.py
@@ -1,30 +1,19 @@
-#!python3
-
-"""Token balance query script
-
-.. moduleauthor:: Louis Holbrook <dev@holbrook.no>
-.. pgp:: 0826EDA1702D1E87C6E2875121D2E7BB88C2A746
-
-"""
-
# SPDX-License-Identifier: GPL-3.0-or-later
# standard imports
import os
-import json
-import argparse
import logging
-# third-party imports
+# external imports
from hexathon import (
add_0x,
strip_0x,
even,
)
-import sha3
# local imports
-from chainlib.eth.address import to_checksum
+import chainlib.eth.cli
+from chainlib.eth.address import AddressChecksum
from chainlib.jsonrpc import (
jsonrpc_result,
IntSequenceGenerator,
@@ -35,53 +24,37 @@ from chainlib.eth.gas import (
balance,
)
from chainlib.chain import ChainSpec
+from crypto_dev_signer.eth.signer import ReferenceSigner as EIP155Signer
logging.basicConfig(level=logging.WARNING)
logg = logging.getLogger()
-default_eth_provider = os.environ.get('RPC_PROVIDER')
-if default_eth_provider == None:
- default_eth_provider = os.environ.get('ETH_PROVIDER', 'http://localhost:8545')
+script_dir = os.path.dirname(os.path.realpath(__file__))
+#config_dir = os.path.join(script_dir, '..', 'data', 'config')
-argparser = argparse.ArgumentParser()
-argparser.add_argument('-p', '--provider', dest='p', default=default_eth_provider, type=str, help='Web3 provider url (http only)')
-argparser.add_argument('-i', '--chain-spec', dest='i', type=str, default='evm:ethereum:1', help='Chain specification string')
-argparser.add_argument('-u', '--unsafe', dest='u', action='store_true', help='Auto-convert address to checksum adddress')
-argparser.add_argument('--seq', action='store_true', help='Use sequential rpc ids')
-argparser.add_argument('-v', action='store_true', help='Be verbose')
-argparser.add_argument('-vv', action='store_true', help='Be more verbose')
-argparser.add_argument('address', type=str, help='Account address')
+arg_flags = chainlib.eth.cli.argflag_std_read
+argparser = chainlib.eth.cli.ArgumentParser(arg_flags)
+argparser.add_positional('address', type=str, help='Ethereum address of recipient')
args = argparser.parse_args()
+#config = chainlib.eth.cli.Config.from_args(args, arg_flags, default_config_dir=config_dir)
+config = chainlib.eth.cli.Config.from_args(args, arg_flags)
+wallet = chainlib.eth.cli.Wallet()
+wallet.from_config(config)
+holder_address = args.address
+if wallet.get_signer_address() == None and holder_address != None:
+ holder_address = wallet.from_address(holder_address)
-if args.vv:
- logg.setLevel(logging.DEBUG)
-elif args.v:
- logg.setLevel(logging.INFO)
-
-rpc_id_generator = None
-if args.seq:
- rpc_id_generator = IntSequenceGenerator()
-
-auth = None
-if os.environ.get('RPC_AUTHENTICATION') == 'basic':
- from chainlib.auth import BasicAuth
- auth = BasicAuth(os.environ['RPC_USERNAME'], os.environ['RPC_PASSWORD'])
-conn = EthHTTPConnection(args.p, auth=auth)
-
-gas_oracle = OverrideGasOracle(conn)
-
-address = to_checksum(args.address)
-if not args.u and address != add_0x(args.address):
- raise ValueError('invalid checksum address')
+rpc = chainlib.eth.cli.Rpc()
+conn = rpc.connect_by_config(config)
-chain_spec = ChainSpec.from_chain_str(args.i)
+chain_spec = ChainSpec.from_chain_str(config.get('CHAIN_SPEC'))
def main():
r = None
decimals = 18
- o = balance(address, id_generator=rpc_id_generator)
+ o = balance(holder_address, id_generator=rpc.id_generator)
r = conn.do(o)
hx = strip_0x(r)
diff --git a/chainlib/eth/runnable/count.py b/chainlib/eth/runnable/count.py
@@ -4,12 +4,13 @@
import sys
import os
import json
-import argparse
+#import argparse
import logging
import select
# local imports
-from chainlib.eth.address import to_checksum
+import chainlib.eth.cli
+from chainlib.eth.address import AddressChecksum
from chainlib.eth.connection import EthHTTPConnection
from chainlib.eth.tx import count
from chainlib.chain import ChainSpec
@@ -21,63 +22,28 @@ from hexathon import add_0x
logging.basicConfig(level=logging.WARNING)
logg = logging.getLogger()
-default_eth_provider = os.environ.get('RPC_PROVIDER')
-if default_eth_provider == None:
- default_eth_provider = os.environ.get('ETH_PROVIDER', 'http://localhost:8545')
+script_dir = os.path.dirname(os.path.realpath(__file__))
+config_dir = os.path.join(script_dir, '..', 'data', 'config')
-def stdin_arg():
- h = select.select([sys.stdin], [], [], 0)
- if len(h[0]) > 0:
- v = h[0][0].read()
- return v.rstrip()
- return None
-
-argparser = argparse.ArgumentParser()
-argparser.add_argument('-p', '--provider', dest='p', default=default_eth_provider, type=str, help='Web3 provider url (http only)')
-argparser.add_argument('-i', '--chain-spec', dest='i', type=str, default='evm:ethereum:1', help='Chain specification string')
-argparser.add_argument('-y', '--key-file', dest='y', type=str, help='Ethereum keystore file to use for signing')
-argparser.add_argument('--env-prefix', default=os.environ.get('CONFINI_ENV_PREFIX'), dest='env_prefix', type=str, help='environment prefix for variables to overwrite configuration')
-argparser.add_argument('-u', '--unsafe', dest='u', action='store_true', help='Auto-convert address to checksum adddress')
-argparser.add_argument('--seq', action='store_true', help='Use sequential rpc ids')
-argparser.add_argument('-v', action='store_true', help='Be verbose')
-argparser.add_argument('-vv', action='store_true', help='Be more verbose')
-argparser.add_argument('address', nargs='?', type=str, default=stdin_arg(), help='Ethereum address of recipient')
+arg_flags = chainlib.eth.cli.argflag_std_read
+argparser = chainlib.eth.cli.ArgumentParser(arg_flags)
+argparser.add_positional('address', type=str, help='Ethereum address of recipient')
args = argparser.parse_args()
+config = chainlib.eth.cli.Config.from_args(args, arg_flags, default_config_dir=config_dir)
-if args.address == None:
- argparser.error('need first positional argument or value from stdin')
-
-if args.vv:
- logg.setLevel(logging.DEBUG)
-elif args.v:
- logg.setLevel(logging.INFO)
-
+holder_address = args.address
+wallet = chainlib.eth.cli.Wallet()
+wallet.from_config(config)
+if wallet.get_signer_address() == None and holder_address != None:
+ wallet.from_address(holder_address)
-signer_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 = chainlib.eth.cli.Rpc(wallet=wallet)
+conn = rpc.connect_by_config(config)
-rpc_id_generator = None
-if args.seq:
- rpc_id_generator = IntSequenceGenerator()
-
-auth = None
-if os.environ.get('RPC_AUTHENTICATION') == 'basic':
- from chainlib.auth import BasicAuth
- auth = BasicAuth(os.environ['RPC_USERNAME'], os.environ['RPC_PASSWORD'])
-rpc = EthHTTPConnection(args.p, auth=auth)
def main():
- recipient = to_checksum(args.address)
- if not args.u and recipient != add_0x(args.address):
- raise ValueError('invalid checksum address')
-
- o = count(recipient, id_generator=rpc_id_generator)
- r = rpc.do(o)
+ o = count(holder_address, id_generator=rpc.id_generator)
+ r = conn.do(o)
count_result = None
try:
count_result = int(r, 16)
diff --git a/chainlib/eth/runnable/decode.py b/chainlib/eth/runnable/decode.py
@@ -1,12 +1,3 @@
-#!python3
-
-"""Decode raw transaction
-
-.. moduleauthor:: Louis Holbrook <dev@holbrook.no>
-.. pgp:: 0826EDA1702D1E87C6E2875121D2E7BB88C2A746
-
-"""
-
# SPDX-License-Identifier: GPL-3.0-or-later
# standard imports
@@ -18,48 +9,29 @@ import logging
import select
# external imports
+import chainlib.eth.cli
from chainlib.eth.tx import unpack
from chainlib.chain import ChainSpec
# local imports
from chainlib.eth.runnable.util import decode_for_puny_humans
-
logging.basicConfig(level=logging.WARNING)
logg = logging.getLogger()
-def stdin_arg(t=0):
- h = select.select([sys.stdin], [], [], t)
- if len(h[0]) > 0:
- v = h[0][0].read()
- return v.rstrip()
- return None
+script_dir = os.path.dirname(os.path.realpath(__file__))
+config_dir = os.path.join(script_dir, '..', 'data', 'config')
-argparser = argparse.ArgumentParser()
-argparser.add_argument('-i', '--chain-id', dest='i', default='evm:ethereum:1', type=str, help='Numeric network id')
-argparser.add_argument('-v', action='store_true', help='Be verbose')
-argparser.add_argument('-vv', action='store_true', help='Be more verbose')
-argparser.add_argument('tx', type=str, nargs='?', default=stdin_arg(), help='hex-encoded signed raw transaction')
+arg_flags = chainlib.eth.cli.Flag.VERBOSE | chainlib.eth.cli.Flag.CHAIN_SPEC | chainlib.eth.cli.Flag.ENV_PREFIX | chainlib.eth.cli.Flag.RAW
+argparser = chainlib.eth.cli.ArgumentParser(arg_flags)
+argparser.add_positional('tx_data', type=str, help='Transaction data to decode')
args = argparser.parse_args()
+config = chainlib.eth.cli.Config.from_args(args, arg_flags, default_config_dir=config_dir)
-if args.vv:
- logg.setLevel(logging.DEBUG)
-elif args.v:
- logg.setLevel(logging.INFO)
-
-argp = args.tx
-logg.debug('txxxx {}'.format(args.tx))
-if argp == None:
- argp = stdin_arg(t=3)
- if argp == None:
- argparser.error('need first positional argument or value from stdin')
-
-chain_spec = ChainSpec.from_chain_str(args.i)
-
+chain_spec = ChainSpec.from_chain_str(config.get('CHAIN_SPEC'))
def main():
- tx_raw = argp
- decode_for_puny_humans(tx_raw, chain_spec, sys.stdout)
+ decode_for_puny_humans(args.tx_data, chain_spec, sys.stdout)
if __name__ == '__main__':
main()
diff --git a/chainlib/eth/runnable/gas.py b/chainlib/eth/runnable/gas.py
@@ -1,12 +1,3 @@
-#!python3
-
-"""Gas transfer script
-
-.. moduleauthor:: Louis Holbrook <dev@holbrook.no>
-.. pgp:: 0826EDA1702D1E87C6E2875121D2E7BB88C2A746
-
-"""
-
# SPDX-License-Identifier: GPL-3.0-or-later
# standard imports
@@ -19,114 +10,53 @@ import logging
import urllib
# external imports
-from crypto_dev_signer.eth.signer import ReferenceSigner as EIP155Signer
-from crypto_dev_signer.keystore.dict import DictKeystore
from hexathon import (
add_0x,
strip_0x,
)
# local imports
-from chainlib.eth.address import to_checksum
+from chainlib.eth.address import to_checksum_address
from chainlib.eth.connection import EthHTTPConnection
from chainlib.jsonrpc import (
JSONRPCRequest,
IntSequenceGenerator,
)
-from chainlib.eth.nonce import (
- RPCNonceOracle,
- OverrideNonceOracle,
- )
-from chainlib.eth.gas import (
- RPCGasOracle,
- OverrideGasOracle,
- Gas,
- )
+from chainlib.eth.gas import Gas
from chainlib.eth.gas import balance as gas_balance
from chainlib.chain import ChainSpec
from chainlib.eth.runnable.util import decode_for_puny_humans
+import chainlib.eth.cli
logging.basicConfig(level=logging.WARNING)
logg = logging.getLogger()
-default_eth_provider = os.environ.get('RPC_PROVIDER')
-if default_eth_provider == None:
- default_eth_provider = os.environ.get('ETH_PROVIDER', 'http://localhost:8545')
-
-argparser = argparse.ArgumentParser()
-argparser.add_argument('-p', '--provider', dest='p', default=default_eth_provider, type=str, help='Web3 provider url (http only)')
-argparser.add_argument('-w', action='store_true', help='Wait for the last transaction to be confirmed')
-argparser.add_argument('-ww', action='store_true', help='Wait for every transaction to be confirmed')
-argparser.add_argument('-i', '--chain-spec', dest='i', type=str, default='evm:ethereum:1', help='Chain specification string')
-argparser.add_argument('-y', '--key-file', dest='y', type=str, help='Ethereum keystore file to use for signing')
-argparser.add_argument('--env-prefix', default=os.environ.get('CONFINI_ENV_PREFIX'), dest='env_prefix', type=str, help='environment prefix for variables to overwrite configuration')
-argparser.add_argument('--nonce', type=int, help='override nonce')
-argparser.add_argument('--gas-price', dest='gas_price', type=int, help='override gas price')
-argparser.add_argument('--gas-limit', dest='gas_limit', type=int, help='override gas limit')
-argparser.add_argument('-u', '--unsafe', dest='u', action='store_true', help='Auto-convert address to checksum adddress')
-argparser.add_argument('--seq', action='store_true', help='Use sequential rpc ids')
-argparser.add_argument('-v', action='store_true', help='Be verbose')
-argparser.add_argument('-vv', action='store_true', help='Be more verbose')
-argparser.add_argument('-s', '--send', dest='s', action='store_true', help='Send to network')
-argparser.add_argument('recipient', type=str, help='ethereum address of recipient')
-argparser.add_argument('amount', type=int, help='gas value in wei')
+arg_flags = chainlib.eth.cli.argflag_std_write | chainlib.eth.cli.Flag.WALLET
+argparser = chainlib.eth.cli.ArgumentParser(arg_flags)
+argparser.add_argument('--data', type=str, help='Transaction data')
+argparser.add_positional('amount', type=int, help='Token amount to send')
args = argparser.parse_args()
-
-
-if args.vv:
- logg.setLevel(logging.DEBUG)
-elif args.v:
- logg.setLevel(logging.INFO)
+extra_args = {
+ 'data': None,
+ 'amount': None,
+ }
+#config = chainlib.eth.cli.Config.from_args(args, arg_flags, extra_args=extra_args, default_config_dir=config_dir)
+config = chainlib.eth.cli.Config.from_args(args, arg_flags, extra_args=extra_args)
block_all = args.ww
block_last = args.w or block_all
-passphrase_env = 'ETH_PASSPHRASE'
-if args.env_prefix != None:
- passphrase_env = args.env_prefix + '_' + passphrase_env
-passphrase = os.environ.get(passphrase_env)
-if passphrase == None:
- logg.warning('no passphrase given')
- passphrase=''
-
-signer_address = None
-keystore = DictKeystore()
-if args.y != None:
- logg.debug('loading keystore file {}'.format(args.y))
- signer_address = keystore.import_keystore_file(args.y, password=passphrase)
- logg.debug('now have key for signer address {}'.format(signer_address))
-signer = EIP155Signer(keystore)
-
-rpc_id_generator = None
-if args.seq:
- rpc_id_generator = IntSequenceGenerator()
+wallet = chainlib.eth.cli.Wallet()
+wallet.from_config(config)
-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)
+rpc = chainlib.eth.cli.Rpc(wallet=wallet)
+conn = rpc.connect_by_config(config)
-nonce_oracle = None
-if args.nonce != None:
- nonce_oracle = OverrideNonceOracle(signer_address, args.nonce, id_generator=rpc_id_generator)
-else:
- nonce_oracle = RPCNonceOracle(signer_address, conn, id_generator=rpc_id_generator)
+chain_spec = ChainSpec.from_chain_str(config.get('CHAIN_SPEC'))
-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)
+value = config.get('_AMOUNT')
-
-chain_spec = ChainSpec.from_chain_str(args.i)
-
-value = args.amount
-
-send = args.s
-
-g = Gas(chain_spec, signer=signer, gas_oracle=gas_oracle, nonce_oracle=nonce_oracle)
+send = config.true('_RPC_SEND')
def balance(address, id_generator):
@@ -137,29 +67,34 @@ def balance(address, id_generator):
def main():
- recipient = to_checksum(args.recipient)
- if not args.u and recipient != add_0x(args.recipient):
+ signer = rpc.get_signer()
+ signer_address = rpc.get_sender_address()
+
+ g = Gas(chain_spec, signer=signer, gas_oracle=rpc.get_gas_oracle(), nonce_oracle=rpc.get_nonce_oracle())
+
+ recipient = to_checksum_address(config.get('_RECIPIENT'))
+ if not config.true('_UNSAFE') and recipient != add_0x(config.get('_RECIPIENT')):
raise ValueError('invalid checksum address')
logg.info('gas transfer from {} to {} value {}'.format(signer_address, recipient, value))
if logg.isEnabledFor(logging.DEBUG):
try:
- sender_balance = balance(signer_address, rpc_id_generator)
- recipient_balance = balance(recipient, rpc_id_generator)
+ sender_balance = balance(signer_address, rpc.id_generator)
+ recipient_balance = balance(recipient, rpc.id_generator)
logg.debug('sender {} balance before: {}'.format(signer_address, sender_balance))
logg.debug('recipient {} balance before: {}'.format(recipient, recipient_balance))
except urllib.error.URLError:
pass
- (tx_hash_hex, o) = g.create(signer_address, recipient, value, id_generator=rpc_id_generator)
+ (tx_hash_hex, o) = g.create(signer_address, recipient, value, data=config.get('_DATA'), id_generator=rpc.id_generator)
if send:
conn.do(o)
if block_last:
r = conn.wait(tx_hash_hex)
if logg.isEnabledFor(logging.DEBUG):
- sender_balance = balance(signer_address, rpc_id_generator)
- recipient_balance = balance(recipient, rpc_id_generator)
+ sender_balance = balance(signer_address, rpc.id_generator)
+ recipient_balance = balance(recipient, rpc.id_generator)
logg.debug('sender {} balance after: {}'.format(signer_address, sender_balance))
logg.debug('recipient {} balance after: {}'.format(recipient, recipient_balance))
if r['status'] == 0:
@@ -167,12 +102,13 @@ def main():
sys.exit(1)
print(tx_hash_hex)
else:
- if logg.isEnabledFor(logging.INFO):
+ #if logg.isEnabledFor(logging.INFO):
+ if config.true('_RAW'):
+ print(o['params'][0])
+ else:
io_str = io.StringIO()
decode_for_puny_humans(o['params'][0], chain_spec, io_str)
print(io_str.getvalue())
- else:
- print(o['params'][0])
diff --git a/chainlib/eth/runnable/get.py b/chainlib/eth/runnable/get.py
@@ -1,12 +1,3 @@
-#!python3
-
-"""Data retrieval script
-
-.. moduleauthor:: Louis Holbrook <dev@holbrook.no>
-.. pgp:: 0826EDA1702D1E87C6E2875121D2E7BB88C2A746
-
-"""
-
# SPDX-License-Identifier: GPL-3.0-or-later
# standard imports
@@ -19,80 +10,56 @@ import enum
import select
# external imports
+from potaahto.symbols import snake_and_camel
from hexathon import (
add_0x,
strip_0x,
)
import sha3
-
-# local imports
-from chainlib.eth.address import to_checksum
from chainlib.jsonrpc import (
JSONRPCRequest,
jsonrpc_result,
IntSequenceGenerator,
)
+from chainlib.chain import ChainSpec
+from chainlib.status import Status
+
+# local imports
from chainlib.eth.connection import EthHTTPConnection
from chainlib.eth.tx import (
Tx,
pack,
)
-from chainlib.eth.address import to_checksum_address
-from chainlib.eth.block import Block
-from chainlib.chain import ChainSpec
-from chainlib.status import Status
+from chainlib.eth.address import (
+ to_checksum_address,
+ is_checksum_address,
+ )
+from chainlib.eth.block import (
+ Block,
+ block_by_hash,
+ )
from chainlib.eth.runnable.util import decode_for_puny_humans
+from chainlib.eth.jsonrpc import to_blockheight_param
+import chainlib.eth.cli
logging.basicConfig(level=logging.WARNING, format='%(asctime)s %(levelname)s %(filename)s:%(lineno)d %(message)s')
logg = logging.getLogger()
-default_eth_provider = os.environ.get('RPC_PROVIDER')
-if default_eth_provider == None:
- default_eth_provider = os.environ.get('ETH_PROVIDER', 'http://localhost:8545')
-
-def stdin_arg(t=0):
- h = select.select([sys.stdin], [], [], t)
- if len(h[0]) > 0:
- v = h[0][0].read()
- return v.rstrip()
- return None
-
-argparser = argparse.ArgumentParser('eth-get', description='display information about an Ethereum address or transaction', epilog='address/transaction can be provided as an argument or from standard input')
-argparser.add_argument('-p', '--provider', dest='p', default=default_eth_provider, type=str, help='Web3 provider url (http only)')
-argparser.add_argument('-i', '--chain-spec', dest='i', type=str, default='evm:ethereum:1', help='Chain specification string')
-argparser.add_argument('--rlp', action='store_true', help='Display transaction as raw rlp')
-argparser.add_argument('--seq', action='store_true', help='Use sequential rpc ids')
-argparser.add_argument('-u', '--unsafe', dest='u', action='store_true', help='Auto-convert address to checksum adddress')
-argparser.add_argument('-v', action='store_true', help='Be verbose')
-argparser.add_argument('-vv', action='store_true', help='Be more verbose')
-argparser.add_argument('item', nargs='?', default=stdin_arg(), type=str, help='Item to get information for (address og transaction)')
-args = argparser.parse_args()
+script_dir = os.path.dirname(os.path.realpath(__file__))
+config_dir = os.path.join(script_dir, '..', 'data', 'config')
-if args.vv:
- logg.setLevel(logging.DEBUG)
-elif args.v:
- logg.setLevel(logging.INFO)
-
-argp = args.item
-if argp == None:
- argp = stdin_arg(None)
- if argsp == None:
- argparser.error('need first positional argument or value from stdin')
-
-rpc_id_generator = None
-if args.seq:
- rpc_id_generator = IntSequenceGenerator()
+arg_flags = chainlib.eth.cli.argflag_std_read
+argparser = chainlib.eth.cli.ArgumentParser(arg_flags)
+argparser.add_positional('item', type=str, help='Address or transaction to retrieve data for')
+args = argparser.parse_args()
+config = chainlib.eth.cli.Config.from_args(args, arg_flags, default_config_dir=config_dir)
-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)
+rpc = chainlib.eth.cli.Rpc()
+conn = rpc.connect_by_config(config)
-chain_spec = ChainSpec.from_chain_str(args.i)
+chain_spec = ChainSpec.from_chain_str(config.get('CHAIN_SPEC'))
item = add_0x(args.item)
-as_rlp = bool(args.rlp)
def get_transaction(conn, tx_hash, id_generator):
@@ -106,7 +73,7 @@ def get_transaction(conn, tx_hash, id_generator):
logg.error('Transaction {} not found'.format(tx_hash))
sys.exit(1)
- if as_rlp:
+ if config.true('_RAW'):
tx_src = Tx.src_normalize(tx_src)
return pack(tx_src, chain_spec).hex()
@@ -125,16 +92,24 @@ def get_transaction(conn, tx_hash, id_generator):
tx = Tx(tx_src)
if rcpt != None:
tx.apply_receipt(rcpt)
+ rcpt = snake_and_camel(rcpt)
+ o = block_by_hash(rcpt['block_hash'])
+ r = conn.do(o)
+ block = Block(r)
+ tx.apply_block(block)
+ logg.debug('foo {}'.format(tx_src))
tx.generate_wire(chain_spec)
return tx
+
-def get_address(conn, address, id_generator):
+def get_address(conn, address, id_generator, height):
j = JSONRPCRequest(id_generator=id_generator)
o = j.template()
o['method'] = 'eth_getCode'
o['params'].append(address)
- o['params'].append('latest')
+ height = to_blockheight_param(height)
+ o['params'].append(height)
o = j.finalize(o)
code = conn.do(o)
@@ -146,11 +121,18 @@ def get_address(conn, address, id_generator):
def main():
+ address = item
r = None
- if len(item) > 42:
- r = get_transaction(conn, item, rpc_id_generator).to_human()
- elif args.u or to_checksum_address(item):
- r = get_address(conn, item, rpc_id_generator)
+ if len(address) > 42:
+ r = get_transaction(conn, address, rpc.id_generator)
+ if not config.true('_RAW'):
+ r = r.to_human()
+ else:
+ if config.get('_UNSAFE'):
+ address = to_checksum_address(address)
+ elif not is_checksum_address(address):
+ raise ValueError('invalid checksum address: {}'.format(address))
+ r = get_address(conn, address, rpc.id_generator, config.get('_HEIGHT'))
print(r)
diff --git a/chainlib/eth/runnable/info.py b/chainlib/eth/runnable/info.py
@@ -1,12 +1,3 @@
-#!python3
-
-"""Token balance query script
-
-.. moduleauthor:: Louis Holbrook <dev@holbrook.no>
-.. pgp:: 0826EDA1702D1E87C6E2875121D2E7BB88C2A746
-
-"""
-
# SPDX-License-Identifier: GPL-3.0-or-later
# standard imports
@@ -17,19 +8,18 @@ import json
import argparse
import logging
-# third-party imports
+# external imports
+from chainlib.chain import ChainSpec
from hexathon import (
add_0x,
strip_0x,
even,
)
import sha3
+from crypto_dev_signer.eth.signer import ReferenceSigner as EIP155Signer
# local imports
-from chainlib.eth.address import (
- to_checksum_address,
- is_checksum_address,
- )
+from chainlib.eth.address import AddressChecksum
from chainlib.eth.chain import network_id
from chainlib.eth.block import (
block_latest,
@@ -43,79 +33,49 @@ from chainlib.eth.gas import (
balance,
price,
)
-from chainlib.jsonrpc import (
- IntSequenceGenerator,
- )
-from chainlib.chain import ChainSpec
+import chainlib.eth.cli
BLOCK_SAMPLES = 10
logging.basicConfig(level=logging.WARNING)
logg = logging.getLogger()
-default_eth_provider = os.environ.get('RPC_PROVIDER')
-if default_eth_provider == None:
- default_eth_provider = os.environ.get('ETH_PROVIDER', 'http://localhost:8545')
-
-argparser = argparse.ArgumentParser()
-argparser.add_argument('-p', '--provider', dest='p', default=default_eth_provider, type=str, help='Web3 provider url (http only)')
-argparser.add_argument('-i', '--chain-spec', dest='i', type=str, default='evm:ethereum:1', help='Chain specification string')
-argparser.add_argument('-H', '--human', dest='human', action='store_true', help='Use human-friendly formatting')
-argparser.add_argument('-u', '--unsafe', dest='u', action='store_true', help='Auto-convert address to checksum adddress')
-argparser.add_argument('-l', '--long', dest='l', action='store_true', help='Calculate averages through sampling of blocks and txs')
-argparser.add_argument('-v', action='store_true', help='Be verbose')
-argparser.add_argument('--seq', action='store_true', help='Use sequential rpc ids')
-argparser.add_argument('-vv', action='store_true', help='Be more verbose')
-argparser.add_argument('-y', '--key-file', dest='y', type=str, help='Include summary for keyfile')
-argparser.add_argument('address', nargs='?', type=str, help='Include summary for address (conflicts with -y)')
-args = argparser.parse_args()
-
-
-if args.vv:
- logg.setLevel(logging.DEBUG)
-elif args.v:
- logg.setLevel(logging.INFO)
+script_dir = os.path.dirname(os.path.realpath(__file__))
+config_dir = os.path.join(script_dir, '..', 'data', 'config')
-signer = None
-holder_address = None
-if args.address != None:
- if not args.u and not is_checksum_address(args.address):
- raise ValueError('invalid checksum address {}'.format(args.address))
- holder_address = add_0x(args.address)
-elif args.y != None:
- f = open(args.y, 'r')
- o = json.load(f)
- f.close()
- holder_address = add_0x(to_checksum_address(o['address']))
+arg_flags = chainlib.eth.cli.argflag_std_read
+argparser = chainlib.eth.cli.ArgumentParser(arg_flags)
+argparser.add_positional('address', type=str, help='Address to retrieve info for', required=False)
+argparser.add_argument('--long', action='store_true', help='Calculate averages through sampling of blocks and txs')
+args = argparser.parse_args()
-rpc_id_generator = None
-if args.seq:
- rpc_id_generator = IntSequenceGenerator()
+config = chainlib.eth.cli.Config.from_args(args, arg_flags, extra_args={'long': None}, default_config_dir=config_dir)
-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)
+holder_address = args.address
+wallet = chainlib.eth.cli.Wallet()
+wallet.from_config(config)
+if wallet.get_signer_address() == None and holder_address != None:
+ wallet.from_address(holder_address)
-gas_oracle = OverrideGasOracle(conn)
+rpc = chainlib.eth.cli.Rpc(wallet=wallet)
+conn = rpc.connect_by_config(config)
token_symbol = 'eth'
-chain_spec = ChainSpec.from_chain_str(args.i)
+chain_spec = ChainSpec.from_chain_str(config.get('CHAIN_SPEC'))
-human = args.human
+human = not config.true('_RAW')
-longmode = args.l
+longmode = config.true('_LONG')
def main():
- o = network_id(id_generator=rpc_id_generator)
+ o = network_id(id_generator=rpc.id_generator)
r = conn.do(o)
#if human:
# n = format(n, ',')
sys.stdout.write('Network id: {}\n'.format(r))
- o = block_latest(id_generator=rpc_id_generator)
+ o = block_latest(id_generator=rpc.id_generator)
r = conn.do(o)
n = int(r, 16)
first_block_number = n
@@ -123,7 +83,7 @@ def main():
n = format(n, ',')
sys.stdout.write('Block: {}\n'.format(n))
- o = block_by_number(first_block_number, False, id_generator=rpc_id_generator)
+ o = block_by_number(first_block_number, False, id_generator=rpc.id_generator)
r = conn.do(o)
last_block = Block(r)
last_timestamp = last_block.timestamp
@@ -132,7 +92,7 @@ def main():
aggr_time = 0.0
aggr_gas = 0
for i in range(BLOCK_SAMPLES):
- o = block_by_number(first_block_number-i, False, id_generator=rpc_id_generator)
+ o = block_by_number(first_block_number-i, False, id_generator=rpc.id_generator)
r = conn.do(o)
block = Block(r)
aggr_time += last_block.timestamp - block.timestamp
@@ -150,7 +110,7 @@ def main():
sys.stdout.write('Gaslimit: {}\n'.format(n))
sys.stdout.write('Blocktime: {}\n'.format(aggr_time / BLOCK_SAMPLES))
- o = price(id_generator=rpc_id_generator)
+ o = price(id_generator=rpc.id_generator)
r = conn.do(o)
n = int(r, 16)
if human:
diff --git a/chainlib/eth/runnable/raw.py b/chainlib/eth/runnable/raw.py
@@ -1,12 +1,3 @@
-#!python3
-
-"""Gas transfer script
-
-.. moduleauthor:: Louis Holbrook <dev@holbrook.no>
-.. pgp:: 0826EDA1702D1E87C6E2875121D2E7BB88C2A746
-
-"""
-
# SPDX-License-Identifier: GPL-3.0-or-later
# standard imports
@@ -19,6 +10,7 @@ import logging
import urllib
# external imports
+import chainlib.eth.cli
from crypto_dev_signer.eth.signer import ReferenceSigner as EIP155Signer
from crypto_dev_signer.keystore.dict import DictKeystore
from hexathon import (
@@ -45,129 +37,88 @@ from chainlib.eth.tx import (
TxFactory,
raw,
)
+from chainlib.error import SignerMissingException
from chainlib.chain import ChainSpec
from chainlib.eth.runnable.util import decode_for_puny_humans
+from chainlib.eth.jsonrpc import to_blockheight_param
logging.basicConfig(level=logging.WARNING)
logg = logging.getLogger()
-default_eth_provider = os.environ.get('RPC_PROVIDER')
-if default_eth_provider == None:
- default_eth_provider = os.environ.get('ETH_PROVIDER', 'http://localhost:8545')
-
-argparser = argparse.ArgumentParser()
-argparser.add_argument('-p', '--provider', dest='p', default=default_eth_provider, type=str, help='Web3 provider url (http only)')
-argparser.add_argument('-w', action='store_true', help='Wait for the last transaction to be confirmed')
-argparser.add_argument('-ww', action='store_true', help='Wait for every transaction to be confirmed')
-argparser.add_argument('-i', '--chain-spec', dest='i', type=str, default='evm:ethereum:1', help='Chain specification string')
-argparser.add_argument('-y', '--key-file', dest='y', type=str, help='Ethereum keystore file to use for signing')
-argparser.add_argument('-u', '--unsafe', dest='u', action='store_true', help='Auto-convert address to checksum adddress')
-argparser.add_argument('--env-prefix', default=os.environ.get('CONFINI_ENV_PREFIX'), dest='env_prefix', type=str, help='environment prefix for variables to overwrite configuration')
-argparser.add_argument('--nonce', type=int, help='override nonce')
-argparser.add_argument('--gas-price', dest='gas_price', type=int, help='override gas price')
-argparser.add_argument('--gas-limit', dest='gas_limit', type=int, help='override gas limit')
-argparser.add_argument('-a', '--recipient', dest='a', type=str, help='recipient address (None for contract creation)')
-argparser.add_argument('-value', type=int, help='gas value of transaction in wei')
-argparser.add_argument('--seq', action='store_true', help='Use sequential rpc ids')
-argparser.add_argument('-v', action='store_true', help='Be verbose')
-argparser.add_argument('-vv', action='store_true', help='Be more verbose')
-argparser.add_argument('-s', '--send', dest='s', action='store_true', help='Send to network')
-argparser.add_argument('-l', '--local', dest='l', action='store_true', help='Local contract call')
-argparser.add_argument('data', nargs='?', type=str, help='Transaction data')
-args = argparser.parse_args()
-
+script_dir = os.path.dirname(os.path.realpath(__file__))
+config_dir = os.path.join(script_dir, '..', 'data', 'config')
-if args.vv:
- logg.setLevel(logging.DEBUG)
-elif args.v:
- logg.setLevel(logging.INFO)
+arg_flags = chainlib.eth.cli.argflag_std_write | chainlib.eth.cli.Flag.EXEC
+argparser = chainlib.eth.cli.ArgumentParser(arg_flags)
+argparser.add_positional('data', type=str, help='Transaction data')
+args = argparser.parse_args()
+config = chainlib.eth.cli.Config.from_args(args, arg_flags, default_config_dir=config_dir)
block_all = args.ww
block_last = args.w or block_all
-passphrase_env = 'ETH_PASSPHRASE'
-if args.env_prefix != None:
- passphrase_env = args.env_prefix + '_' + passphrase_env
-passphrase = os.environ.get(passphrase_env)
-if passphrase == None:
- logg.warning('no passphrase given')
- passphrase=''
-
-signer_address = None
-keystore = DictKeystore()
-if args.y != None:
- logg.debug('loading keystore file {}'.format(args.y))
- signer_address = keystore.import_keystore_file(args.y, password=passphrase)
- logg.debug('now have key for signer address {}'.format(signer_address))
-signer = EIP155Signer(keystore)
-
-rpc_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)
-
-send = args.s
-
-local = args.l
-if local:
- send = False
+wallet = chainlib.eth.cli.Wallet(EIP155Signer)
+wallet.from_config(config)
-nonce_oracle = None
-gas_oracle = None
-if signer_address != None and not local:
- if args.nonce != None:
- nonce_oracle = OverrideNonceOracle(signer_address, args.nonce)
- else:
- nonce_oracle = RPCNonceOracle(signer_address, conn)
+rpc = chainlib.eth.cli.Rpc(wallet=wallet)
+conn = rpc.connect_by_config(config)
- 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)
+send = config.true('_RPC_SEND')
-chain_spec = ChainSpec.from_chain_str(args.i)
+if config.get('_EXEC_ADDRESS') != None:
+ send = False
-value = args.value
+chain_spec = None
+try:
+ chain_spec = ChainSpec.from_chain_str(config.get('CHAIN_SPEC'))
+except AttributeError:
+ pass
+def main():
-g = TxFactory(chain_spec, signer=signer, gas_oracle=gas_oracle, nonce_oracle=nonce_oracle)
+ signer_address = None
+ try:
+ signer = rpc.get_signer()
+ signer_address = rpc.get_signer_address()
+ except SignerMissingException:
+ pass
-def main():
- recipient = None
- if args.a != None:
- recipient = add_0x(to_checksum(args.a))
- if not args.u and recipient != add_0x(recipient):
+ if config.get('_EXEC_ADDRESS') != None:
+ exec_address = add_0x(to_checksum(config.get('_EXEC_ADDRESS')))
+ if not args.u and exec_address != add_0x(exec_address):
raise ValueError('invalid checksum address')
- if local:
- j = JSONRPCRequest(id_generator=rpc_id_generator)
+ j = JSONRPCRequest(id_generator=rpc.id_generator)
o = j.template()
o['method'] = 'eth_call'
o['params'].append({
- 'to': recipient,
+ 'to': exec_address,
'from': signer_address,
'value': '0x00',
'gas': add_0x(int.to_bytes(8000000, 8, byteorder='big').hex()), # TODO: better get of network gas limit
'gasPrice': '0x01',
'data': add_0x(args.data),
})
- o['params'].append('latest')
+ height = to_blockheight_param(config.get('_HEIGHT'))
+ o['params'].append(height)
o = j.finalize(o)
r = conn.do(o)
- print(strip_0x(r))
+ try:
+ print(strip_0x(r))
+ except ValueError:
+ sys.stderr.write('query returned an empty value\n')
+ sys.exit(1)
return
elif signer_address != None:
+ if chain_spec == None:
+ raise ValueError('chain spec must be specified')
+ g = TxFactory(chain_spec, signer=rpc.get_signer(), gas_oracle=rpc.get_gas_oracle(), nonce_oracle=rpc.get_nonce_oracle())
tx = g.template(signer_address, recipient, use_nonce=True)
if args.data != None:
tx = g.set_code(tx, add_0x(args.data))
- (tx_hash_hex, o) = g.finalize(tx, id_generator=rpc_id_generator)
+ (tx_hash_hex, o) = g.finalize(tx, id_generator=rpc.id_generator)
if send:
r = conn.do(o)
@@ -177,7 +128,7 @@ def main():
print(tx_hash_hex)
else:
- o = raw(args.data, id_generator=rpc_id_generator)
+ o = raw(args.data, id_generator=rpc.id_generator)
if send:
r = conn.do(o)
print(r)
diff --git a/chainlib/eth/sign.py b/chainlib/eth/sign.py
@@ -3,6 +3,17 @@ from chainlib.jsonrpc import JSONRPCRequest
def new_account(passphrase='', id_generator=None):
+ """Generate json-rpc query to create new account in keystore.
+
+ Uses the personal_newAccount rpc call.
+
+ :param passphrase: Passphrase string
+ :type passphrase: str
+ :param id_generator: JSONRPC id generator
+ :type id_generator: JSONRPCIdGenerator
+ :rtype: dict
+ :returns: rpc query object
+ """
j = JSONRPCRequest(id_generator)
o = j.template()
o['method'] = 'personal_newAccount'
@@ -11,6 +22,17 @@ def new_account(passphrase='', id_generator=None):
def sign_transaction(payload, id_generator=None):
+ """Generate json-rpc query to sign transaction using the node keystore.
+
+ The node must have the private key corresponding to the from-field in the transaction object.
+
+ :param payload: Transaction
+ :type payload: dict
+ :param id_generator: JSONRPC id generator
+ :type id_generator: JSONRPCIdGenerator
+ :rtype: dict
+ :returns: rpc query object
+ """
j = JSONRPCRequest(id_generator)
o = j.template()
o['method'] = 'eth_signTransaction'
@@ -19,6 +41,19 @@ def sign_transaction(payload, id_generator=None):
def sign_message(address, payload, id_generator=None):
+ """Generate json-rpc query to sign an arbirary message using the node keystore.
+
+ The node must have the private key corresponding to the address parameter.
+
+ :param address: Address of key to sign with, in hex
+ :type address: str
+ :param payload: Arbirary message, in hex
+ :type payload: str
+ :param id_generator: JSONRPC id generator
+ :type id_generator: JSONRPCIdGenerator
+ :rtype: dict
+ :returns: rpc query object
+ """
j = JSONRPCRequest(id_generator)
o = j.template()
o['method'] = 'eth_sign'
diff --git a/chainlib/eth/tx.py b/chainlib/eth/tx.py
@@ -9,6 +9,7 @@ import sha3
from hexathon import (
strip_0x,
add_0x,
+ compact,
)
from rlp import decode as rlp_decode
from rlp import encode as rlp_encode
@@ -16,25 +17,34 @@ from crypto_dev_signer.eth.transaction import EIP155Transaction
from crypto_dev_signer.encoding import public_key_to_address
from crypto_dev_signer.eth.encoding import chain_id_to_v
from potaahto.symbols import snake_and_camel
-
-
-# local imports
from chainlib.hash import keccak256_hex_to_hex
from chainlib.status import Status
+from chainlib.jsonrpc import JSONRPCRequest
+from chainlib.tx import Tx as BaseTx
+from chainlib.eth.nonce import (
+ nonce as nonce_query,
+ nonce_confirmed as nonce_query_confirmed,
+ )
+from chainlib.block import BlockSpec
+
+# local imports
from .address import to_checksum
from .constant import (
MINIMUM_FEE_UNITS,
MINIMUM_FEE_PRICE,
ZERO_ADDRESS,
+ DEFAULT_FEE_LIMIT,
)
from .contract import ABIContractEncoder
-from chainlib.jsonrpc import JSONRPCRequest
+from .jsonrpc import to_blockheight_param
-logg = logging.getLogger().getChild(__name__)
+logg = logging.getLogger(__name__)
class TxFormat(enum.IntEnum):
+ """Tx generator output formats
+ """
DICT = 0x00
RAW = 0x01
RAW_SIGNED = 0x02
@@ -56,24 +66,22 @@ field_debugs = [
's',
]
-def count(address, confirmed=False, id_generator=None):
- j = JSONRPCRequest(id_generator=id_generator)
- o = j.template()
- o['method'] = 'eth_getTransactionCount'
- o['params'].append(address)
- if confirmed:
- o['params'].append('latest')
- else:
- o['params'].append('pending')
- return j.finalize(o)
-
-count_pending = count
-def count_confirmed(address):
- return count(address, True)
+count = nonce_query
+count_pending = nonce_query
+count_confirmed = nonce_query_confirmed
def pack(tx_src, chain_spec):
+ """Serialize wire format transaction from transaction representation.
+
+ :param tx_src: Transaction source.
+ :type tx_src: dict
+ :param chain_spec: Chain spec to calculate EIP155 v value
+ :type chain_spec: chainlib.chain.ChainSpec
+ :rtype: bytes
+ :returns: Serialized transaction
+ """
if isinstance(tx_src, Tx):
tx_src = tx_src.as_dict()
tx_src = Tx.src_normalize(tx_src)
@@ -96,6 +104,15 @@ def pack(tx_src, chain_spec):
def unpack(tx_raw_bytes, chain_spec):
+ """Deserialize wire format transaction to transaction representation.
+
+ :param tx_raw_bytes: Serialized transaction
+ :type tx_raw_bytes: bytes
+ :param chain_spec: Chain spec to calculate EIP155 v value
+ :type chain_spec: chainlib.chain.ChainSpec
+ :rtype: dict
+ :returns: Transaction representation
+ """
chain_id = chain_spec.chain_id()
tx = __unpack_raw(tx_raw_bytes, chain_id)
tx['nonce'] = int.from_bytes(tx['nonce'], 'big')
@@ -106,6 +123,15 @@ def unpack(tx_raw_bytes, chain_spec):
def unpack_hex(tx_raw_bytes, chain_spec):
+ """Deserialize wire format transaction to transaction representation, using hex values for all numeric value fields.
+
+ :param tx_raw_bytes: Serialized transaction
+ :type tx_raw_bytes: bytes
+ :param chain_spec: Chain spec to calculate EIP155 v value
+ :type chain_spec: chainlib.chain.ChainSpec
+ :rtype: dict
+ :returns: Transaction representation
+ """
chain_id = chain_spec.chain_id()
tx = __unpack_raw(tx_raw_bytes, chain_id)
tx['nonce'] = add_0x(hex(tx['nonce']))
@@ -193,6 +219,15 @@ def __unpack_raw(tx_raw_bytes, chain_id=1):
def transaction(hsh, id_generator=None):
+ """Generate json-rpc query to retrieve transaction by hash from node.
+
+ :param hsh: Transaction hash, in hex
+ :type hsh: str
+ :param id_generator: json-rpc id generator
+ :type id_generator: JSONRPCIdGenerator
+ :rtype: dict
+ :returns: rpc query object
+ """
j = JSONRPCRequest(id_generator=id_generator)
o = j.template()
o['method'] = 'eth_getTransactionByHash'
@@ -201,6 +236,17 @@ def transaction(hsh, id_generator=None):
def transaction_by_block(hsh, idx, id_generator=None):
+ """Generate json-rpc query to retrieve transaction by block hash and index.
+
+ :param hsh: Block hash, in hex
+ :type hsh: str
+ :param idx: Transaction index
+ :type idx: int
+ :param id_generator: json-rpc id generator
+ :type id_generator: JSONRPCIdGenerator
+ :rtype: dict
+ :returns: rpc query object
+ """
j = JSONRPCRequest(id_generator=id_generator)
o = j.template()
o['method'] = 'eth_getTransactionByBlockHashAndIndex'
@@ -210,6 +256,15 @@ def transaction_by_block(hsh, idx, id_generator=None):
def receipt(hsh, id_generator=None):
+ """Generate json-rpc query to retrieve transaction receipt by transaction hash from node.
+
+ :param hsh: Transaction hash, in hex
+ :type hsh: str
+ :param id_generator: json-rpc id generator
+ :type id_generator: JSONRPCIdGenerator
+ :rtype: dict
+ :returns: rpc query object
+ """
j = JSONRPCRequest(id_generator=id_generator)
o = j.template()
o['method'] = 'eth_getTransactionReceipt'
@@ -218,6 +273,15 @@ def receipt(hsh, id_generator=None):
def raw(tx_raw_hex, id_generator=None):
+ """Generator json-rpc query to send raw transaction to node.
+
+ :param hsh: Serialized transaction, in hex
+ :type hsh: str
+ :param id_generator: json-rpc id generator
+ :type id_generator: JSONRPCIdGenerator
+ :rtype: dict
+ :returns: rpc query object
+ """
j = JSONRPCRequest(id_generator=id_generator)
o = j.template()
o['method'] = 'eth_sendRawTransaction'
@@ -226,8 +290,23 @@ def raw(tx_raw_hex, id_generator=None):
class TxFactory:
+ """Base class for generating and signing transactions or contract calls.
- fee = 8000000
+ For transactions (state changes), a signer, gas oracle and nonce oracle needs to be supplied.
+
+ Gas oracle and nonce oracle may in some cases be needed for contract calls, if the node insists on counting gas for read-only operations.
+
+ :param chain_spec: Chain spec to use for signer.
+ :type chain_spec: chainlib.chain.ChainSpec
+ :param signer: Signer middleware.
+ :type param: Object implementing interface ofchainlib.eth.connection.sign_transaction_to_rlp.
+ :param gas_oracle: Backend to generate gas parameters
+ :type gas_oracle: Object implementing chainlib.eth.gas.GasOracle interface
+ :param nonce_oracle: Backend to generate gas parameters
+ :type nonce_oracle: Object implementing chainlib.eth.nonce.NonceOracle interface
+ """
+
+ fee = DEFAULT_FEE_LIMIT
def __init__(self, chain_spec, signer=None, gas_oracle=None, nonce_oracle=None):
self.gas_oracle = gas_oracle
@@ -237,6 +316,15 @@ class TxFactory:
def build_raw(self, tx):
+ """Sign transaction data, returning the transaction hash and serialized transaction.
+
+ In most cases, chainlib.eth.tx.TxFactory.finalize should be used instead.
+
+ :param tx: Transaction representation
+ :type tx: dict
+ :rtype: tuple
+ :returns: Transaction hash (in hex), serialized transaction (in hex)
+ """
if tx['to'] == None or tx['to'] == '':
tx['to'] = '0x'
txe = EIP155Transaction(tx, tx['nonce'], tx['chainId'])
@@ -247,12 +335,34 @@ class TxFactory:
def build(self, tx, id_generator=None):
+ """Sign transaction and wrap in raw transaction json-rpc query.
+
+ In most cases, chainlib.eth.tx.TxFactory.finalize should be used instead.
+
+ :param tx: Transaction representation
+ type tx: dict
+ :param id_generator: JSONRPC id generator
+ :type id_generator: JSONRPCIdGenerator
+ :rtype: tuple
+ :returns: Transaction hash (in hex), raw transaction rpc query object
+ """
(tx_hash_hex, tx_raw_hex) = self.build_raw(tx)
o = raw(tx_raw_hex, id_generator=id_generator)
return (tx_hash_hex, o)
def template(self, sender, recipient, use_nonce=False):
+ """Generate a base transaction template.
+
+ :param sender: Sender address, in hex
+ :type sender: str
+ :param receipient: Recipient address, in hex
+ :type recipient: str
+ :param use_nonce: Use and advance nonce in nonce generator.
+ :type use_nonce: bool
+ :rtype: dict
+ :returns: Transaction representation.
+ """
gas_price = MINIMUM_FEE_PRICE
gas_limit = MINIMUM_FEE_UNITS
if self.gas_oracle != None:
@@ -276,18 +386,35 @@ class TxFactory:
def normalize(self, tx):
+ """Generate field name redundancies (camel-case, snake-case).
+
+ :param tx: Transaction representation
+ :type tx: dict
+ :rtype: dict:
+ :returns: Transaction representation with redudant field names
+ """
txe = EIP155Transaction(tx, tx['nonce'], tx['chainId'])
txes = txe.serialize()
return {
'from': tx['from'],
'to': txes['to'],
- 'gasPrice': txes['gasPrice'],
- 'gas': txes['gas'],
+ 'gasPrice': '0x' + compact(txes['gasPrice']),
+ 'gas': '0x' + compact(txes['gas']),
'data': txes['data'],
}
def finalize(self, tx, tx_format=TxFormat.JSONRPC, id_generator=None):
+ """Sign transaction and for specified output format.
+
+ :param tx: Transaction representation
+ :type tx: dict
+ :param tx_format: Transaction output format
+ :type tx_format: chainlib.eth.tx.TxFormat
+ :raises NotImplementedError: Unknown tx_format value
+ :rtype: varies
+ :returns: Transaction output in specified format.
+ """
if tx_format == TxFormat.JSONRPC:
return self.build(tx, id_generator=id_generator)
elif tx_format == TxFormat.RLP_SIGNED:
@@ -296,6 +423,17 @@ class TxFactory:
def set_code(self, tx, data, update_fee=True):
+ """Apply input data to transaction.
+
+ :param tx: Transaction representation
+ :type tx: dict
+ :param data: Input data to apply, in hex
+ :type data: str
+ :param update_fee: Recalculate gas limit based on added input
+ :type update_fee: bool
+ :rtype: dict
+ :returns: Transaction representation
+ """
tx['data'] = data
if update_fee:
tx['gas'] = TxFactory.fee
@@ -307,6 +445,19 @@ class TxFactory:
def transact_noarg(self, method, contract_address, sender_address, tx_format=TxFormat.JSONRPC):
+ """Convenience generator for contract transaction with no arguments.
+
+ :param method: Method name
+ :type method: str
+ :param contract_address: Contract address to transaction against, in hex
+ :type contract_address: str
+ :param sender_address: Transaction sender, in hex
+ :type sender_address: str
+ :param tx_format: Transaction output format
+ :type tx_format: chainlib.eth.tx.TxFormat
+ :rtype: varies
+ :returns: Transaction output in selected format
+ """
enc = ABIContractEncoder()
enc.method(method)
data = enc.get()
@@ -316,7 +467,22 @@ class TxFactory:
return tx
- def call_noarg(self, method, contract_address, sender_address=ZERO_ADDRESS, id_generator=None):
+ def call_noarg(self, method, contract_address, sender_address=ZERO_ADDRESS, height=BlockSpec.LATEST, id_generator=None):
+ """Convenience generator for contract (read-only) call with no arguments.
+
+ :param method: Method name
+ :type method: str
+ :param contract_address: Contract address to transaction against, in hex
+ :type contract_address: str
+ :param sender_address: Transaction sender, in hex
+ :type sender_address: str
+ :param height: Transaction height specifier
+ :type height: chainlib.block.BlockSpec
+ :param id_generator: json-rpc id generator
+ :type id_generator: JSONRPCIdGenerator
+ :rtype: varies
+ :returns: Transaction output in selected format
+ """
j = JSONRPCRequest(id_generator)
o = j.template()
o['method'] = 'eth_call'
@@ -326,34 +492,38 @@ class TxFactory:
tx = self.template(sender_address, contract_address)
tx = self.set_code(tx, data)
o['params'].append(self.normalize(tx))
- o['params'].append('latest')
+ height = to_blockheight_param(height)
+ o['params'].append(height)
o = j.finalize(o)
return o
-class Tx:
+class Tx(BaseTx):
+ """Wraps transaction data, transaction receipt data and block data, enforces local standardization of fields, and provides useful output formats for viewing transaction contents.
+
+ If block is applied, the transaction data or transaction hash must exist in its transactions array.
+
+ If receipt is applied, the transaction hash in the receipt must match the hash in the transaction data.
+
+ :param src: Transaction representation
+ :type src: dict
+ :param block: Apply block object in which transaction in mined.
+ :type block: chainlib.block.Block
+ :param rcpt: Apply receipt data
+ :type rcpt: dict
+ #:todo: force tx type schema parser (whether expect hex or int etc)
+ #:todo: divide up constructor method
+ """
- # TODO: force tx type schema parser (whether expect hex or int etc)
def __init__(self, src, block=None, rcpt=None):
- self.tx_src = self.src_normalize(src)
+ self.__rcpt_block_hash = None
+
+ src = self.src_normalize(src)
self.index = -1
tx_hash = add_0x(src['hash'])
- if block != None:
- i = 0
- for tx in block.txs:
- tx_hash_block = None
- try:
- tx_hash_block = tx['hash']
- except TypeError:
- tx_hash_block = add_0x(tx)
- if tx_hash_block == tx_hash:
- self.index = i
- break
- i += 1
- if self.index == -1:
- raise AttributeError('tx {} not found in block {}'.format(tx_hash, block.hash))
- self.block = block
self.hash = strip_0x(tx_hash)
+ if block != None:
+ self.apply_block(block)
try:
self.value = int(strip_0x(src['value']), 16)
except TypeError:
@@ -378,6 +548,7 @@ class Tx:
inpt = src['input']
except KeyError:
inpt = src['data']
+ src['input'] = src['data']
if inpt != '0x':
inpt = strip_0x(inpt)
@@ -408,13 +579,27 @@ class Tx:
self.wire = None
+ self.tx_src = src
+
def src(self):
+ """Retrieve normalized representation source used to construct transaction object.
+
+ :rtype: dict
+ :returns: Transaction representation
+ """
return self.tx_src
@classmethod
def src_normalize(self, src):
+ """Normalizes transaction representation source data.
+
+ :param src: Transaction representation
+ :type src: dict
+ :rtype: dict
+ :returns: Transaction representation, normalized
+ """
src = snake_and_camel(src)
if isinstance(src.get('v'), str):
@@ -430,16 +615,40 @@ class Tx:
def apply_receipt(self, rcpt):
+ """Apply receipt data to transaction object.
+
+ Effect is the same as passing a receipt at construction.
+
+ :param rcpt: Receipt data
+ :type rcpt: dict
+ """
rcpt = self.src_normalize(rcpt)
logg.debug('rcpt {}'.format(rcpt))
+
+ tx_hash = add_0x(rcpt['transaction_hash'])
+ if rcpt['transaction_hash'] != add_0x(self.hash):
+ raise ValueError('rcpt hash {} does not match transaction hash {}'.format(rcpt['transaction_hash'], self.hash))
+
+ block_hash = add_0x(rcpt['block_hash'])
+ if self.block != None:
+ if block_hash != add_0x(self.block.hash):
+ raise ValueError('rcpt block hash {} does not match transaction block hash {}'.format(rcpt['block_hash'], self.block.hash))
+
try:
status_number = int(rcpt['status'], 16)
except TypeError:
status_number = int(rcpt['status'])
- if status_number == 1:
- self.status = Status.SUCCESS
- elif status_number == 0:
- self.status = Status.ERROR
+ if rcpt['block_number'] == None:
+ self.status = Status.PENDING
+ else:
+ if status_number == 1:
+ self.status = Status.SUCCESS
+ elif status_number == 0:
+ self.status = Status.ERROR
+ try:
+ self.tx_index = int(rcpt['transaction_index'], 16)
+ except TypeError:
+ self.tx_index = int(rcpt['transaction_index'])
# TODO: replace with rpc receipt/transaction translator when available
contract_address = rcpt.get('contractAddress')
if contract_address == None:
@@ -452,20 +661,43 @@ class Tx:
except TypeError:
self.gas_used = int(rcpt['gasUsed'])
+ self.__rcpt_block_hash = rcpt['block_hash']
+
def apply_block(self, block):
- #block_src = self.src_normalize(block_src)
+ """Apply block to transaction object.
+
+ :param block: Block object
+ :type block: chainlib.block.Block
+ """
+ if self.__rcpt_block_hash != None:
+ if block.hash != self.__rcpt_block_hash:
+ raise ValueError('block hash {} does not match already applied receipt block hash {}'.format(block.hash, self.__rcpt_block_hash))
+ self.index = block.get_tx(self.hash)
self.block = block
def generate_wire(self, chain_spec):
- b = pack(self.src(), chain_spec)
- self.wire = add_0x(b.hex())
+ """Generate transaction wire format.
+
+ :param chain_spec: Chain spec to interpret EIP155 v value.
+ :type chain_spec: chainlib.chain.ChainSpec
+ :rtype: str
+ :returns: Wire format, in hex
+ """
+ if self.wire == None:
+ b = pack(self.src(), chain_spec)
+ self.wire = add_0x(b.hex())
+ return self.wire
@staticmethod
- def from_src(src, block=None):
- return Tx(src, block=block)
+ def from_src(src, block=None, rcpt=None):
+ """Creates a new Tx object.
+
+ Alias of constructor.
+ """
+ return Tx(src, block=block, rcpt=rcpt)
def __str__(self):
@@ -480,13 +712,18 @@ class Tx:
def to_human(self):
+ """Human-readable string dump of transaction contents.
+
+ :rtype: str
+ :returns: Contents
+ """
s = """hash {}
from {}
to {}
value {}
nonce {}
-gasPrice {}
-gasLimit {}
+gas_price {}
+gas_limit {}
input {}
""".format(
self.hash,
@@ -500,13 +737,24 @@ input {}
)
if self.status != Status.PENDING:
- s += """gasUsed {}
+ s += """gas_used {}
""".format(
self.gas_used,
)
s += 'status ' + self.status.name + '\n'
+ if self.block != None:
+ s += """block_number {}
+block_hash {}
+tx_index {}
+""".format(
+ self.block.number,
+ self.block.hash,
+ self.tx_index,
+ )
+
+
if self.contract != None:
s += """contract {}
""".format(
@@ -520,4 +768,3 @@ input {}
)
return s
-
diff --git a/example/call_balance.py b/example/call_balance.py
@@ -17,6 +17,7 @@ address = add_0x(address_bytes.hex())
rpc_provider = os.environ.get('RPC_PROVIDER', 'http://localhost:8545')
rpc = EthHTTPConnection(rpc_provider)
o = balance(address)
+print(o)
r = rpc.do(o)
clean_address = strip_0x(address)
diff --git a/example/jsonrpc.py b/example/jsonrpc.py
@@ -3,22 +3,23 @@ import os
import sys
# local imports
-from chainlib.jsonrpc import jsonrpc_template
+from chainlib.jsonrpc import JSONRPCRequest
from chainlib.eth.connection import EthHTTPConnection
# set up node connection and execute rpc call
rpc_provider = os.environ.get('RPC_PROVIDER', 'http://localhost:8545')
-rpc = EthHTTPConnection(rpc_provider)
+conn = EthHTTPConnection(rpc_provider)
# check the connection
-if not rpc.check():
+if not conn.check():
sys.stderr.write('node {} not usable\n'.format(rpc_provider))
sys.exit(1)
# build and send rpc call
-o = jsonrpc_template()
+g = JSONRPCRequest()
+o = g.template()
o['method'] = 'eth_blockNumber'
-r = rpc.do(o)
+r = conn.do(o)
# interpret result for humans
try:
diff --git a/example/jsonrpc_factory.py b/example/jsonrpc_factory.py
@@ -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))
diff --git a/example/online_transaction.py b/example/online_transaction.py
@@ -21,13 +21,21 @@ from chainlib.eth.tx import (
)
from chainlib.error import JSONRPCException
+script_dir = os.path.dirname(os.path.realpath(__file__))
+
+
# eth transactions need an explicit chain parameter as part of their signature
chain_spec = ChainSpec.from_chain_str('evm:ethereum:1')
# create keystore and signer
keystore = DictKeystore()
signer = EIP155Signer(keystore)
-sender_address = keystore.new()
+
+# import private key for sender
+sender_keystore_file = os.path.join(script_dir, '..', 'tests', 'testdata', 'keystore', 'UTC--2021-01-08T17-18-44.521011372Z--eb3907ecad74a0013c259d5874ae7f22dcbcc95c')
+sender_address = keystore.import_keystore_file(sender_keystore_file)
+
+# create a new address to use as recipient
recipient_address = keystore.new()
# set up node connection
diff --git a/requirements.txt b/requirements.txt
@@ -1,6 +1,7 @@
-crypto-dev-signer~=0.4.14b6
+crypto-dev-signer>=0.4.14b7,<=0.4.14
pysha3==1.0.2
-hexathon~=0.0.1a7
+hexathon~=0.0.1a8
websocket-client==0.57.0
potaahto~=0.0.1a1
-chainlib==0.0.5a1
+chainlib==0.0.8a2
+confini>=0.4.1a1,<0.5.0
diff --git a/setup.cfg b/setup.cfg
@@ -1,6 +1,6 @@
[metadata]
name = chainlib-eth
-version = 0.0.5a1
+version = 0.0.8a2
description = Ethereum implementation of the chainlib interface
author = Louis Holbrook
author_email = dev@holbrook.no
@@ -24,6 +24,7 @@ licence_files =
LICENSE.txt
[options]
+include_package_data = True
python_requires = >= 3.6
packages =
chainlib.eth
@@ -40,4 +41,5 @@ console_scripts =
eth-get = chainlib.eth.runnable.get:main
eth-decode = chainlib.eth.runnable.decode:main
eth-info = chainlib.eth.runnable.info:main
+ eth-nonce = chainlib.eth.runnable.count:main
eth = chainlib.eth.runnable.info:main
diff --git a/tests/test_block.py b/tests/test_block.py
@@ -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()
diff --git a/tests/test_tx.py b/tests/test_tx.py
@@ -28,6 +28,7 @@ from hexathon import (
strip_0x,
add_0x,
)
+from chainlib.eth.block import Block
logging.basicConfig(level=logging.DEBUG)
logg = logging.getLogger()
@@ -35,6 +36,7 @@ logg = logging.getLogger()
class TxTestCase(EthTesterCase):
+
def test_tx_reciprocal(self):
nonce_oracle = RPCNonceOracle(self.accounts[0], self.rpc)
gas_oracle = RPCGasOracle(self.rpc)
@@ -75,5 +77,70 @@ class TxTestCase(EthTesterCase):
logg.debug('r {}'.format(tx_signed_raw_bytes_recovered.hex()))
self.assertEqual(tx_signed_raw_bytes, tx_signed_raw_bytes_recovered)
+
+ def test_apply_block(self):
+ nonce_oracle = RPCNonceOracle(self.accounts[0], self.rpc)
+ gas_oracle = RPCGasOracle(self.rpc)
+ c = Gas(signer=self.signer, nonce_oracle=nonce_oracle, gas_oracle=gas_oracle, chain_spec=self.chain_spec)
+ (tx_hash_hex, o) = c.create(self.accounts[0], self.accounts[1], 1024, tx_format=TxFormat.RLP_SIGNED)
+ tx_data = unpack(bytes.fromhex(strip_0x(o)), self.chain_spec)
+
+ block_hash = os.urandom(32).hex()
+ block = Block({
+ 'hash': block_hash,
+ 'number': 42,
+ 'timestamp': 13241324,
+ 'transactions': [],
+ })
+ with self.assertRaises(AttributeError):
+ tx = Tx(tx_data, block=block)
+
+ tx_unknown_hash = os.urandom(32).hex()
+ block.txs = [add_0x(tx_unknown_hash)]
+ block.txs.append(add_0x(tx_data['hash']))
+ tx = Tx(tx_data, block=block)
+
+ block.txs = [add_0x(tx_unknown_hash)]
+ block.txs.append(tx_data)
+ tx = Tx(tx_data, block=block)
+
+
+ def test_apply_receipt(self):
+ nonce_oracle = RPCNonceOracle(self.accounts[0], self.rpc)
+ gas_oracle = RPCGasOracle(self.rpc)
+ c = Gas(signer=self.signer, nonce_oracle=nonce_oracle, gas_oracle=gas_oracle, chain_spec=self.chain_spec)
+ (tx_hash_hex, o) = c.create(self.accounts[0], self.accounts[1], 1024, tx_format=TxFormat.RLP_SIGNED)
+ tx_data = unpack(bytes.fromhex(strip_0x(o)), self.chain_spec)
+
+ rcpt = {
+ 'transaction_hash': os.urandom(32).hex(),
+ 'block_hash': os.urandom(32).hex(),
+ 'status': 1,
+ 'block_number': 42,
+ 'transaction_index': 1,
+ 'logs': [],
+ 'gas_used': 21000,
+ }
+ with self.assertRaises(ValueError):
+ tx = Tx(tx_data, rcpt=rcpt)
+
+ rcpt['transaction_hash'] = tx_data['hash']
+ tx = Tx(tx_data, rcpt=rcpt)
+
+ block_hash = os.urandom(32).hex()
+ block = Block({
+ 'hash': block_hash,
+ 'number': 42,
+ 'timestamp': 13241324,
+ 'transactions': [],
+ })
+ block.txs = [add_0x(tx_data['hash'])]
+ with self.assertRaises(ValueError):
+ tx = Tx(tx_data, rcpt=rcpt, block=block)
+
+ rcpt['block_hash'] = block.hash
+ tx = Tx(tx_data, rcpt=rcpt, block=block)
+
+
if __name__ == '__main__':
unittest.main()