From d13708d3ce9639a6af9962f7fbd68859605d5996 Mon Sep 17 00:00:00 2001 From: Louis Holbrook Date: Sat, 21 Aug 2021 07:27:40 +0000 Subject: [PATCH] Add docstrings --- CHANGELOG | 1 + MANIFEST.in | 2 +- chainlib/eth/address.py | 33 +- chainlib/eth/block.py | 65 ++-- chainlib/eth/chain.py | 7 + chainlib/eth/cli.py | 44 +++ chainlib/eth/connection.py | 99 +++++- chainlib/eth/constant.py | 1 + chainlib/eth/contract.py | 201 +++++++++++- chainlib/eth/data/config/config.ini | 12 + chainlib/eth/error.py | 12 +- chainlib/eth/gas.py | 109 ++++++- chainlib/eth/jsonrpc.py | 28 ++ chainlib/eth/log.py | 8 +- chainlib/eth/nonce.py | 73 ++++- chainlib/eth/pytest/fixtures_ethtester.py | 9 +- chainlib/eth/runnable/balance.py | 67 ++-- chainlib/eth/runnable/count.py | 70 ++--- chainlib/eth/runnable/decode.py | 46 +-- chainlib/eth/runnable/gas.py | 136 +++------ chainlib/eth/runnable/get.py | 110 +++---- chainlib/eth/runnable/info.py | 94 ++---- chainlib/eth/runnable/raw.py | 139 +++------ chainlib/eth/sign.py | 35 +++ chainlib/eth/tx.py | 357 ++++++++++++++++++---- example/call_balance.py | 1 + example/jsonrpc.py | 11 +- example/jsonrpc_factory.py | 41 +++ example/online_transaction.py | 10 +- requirements.txt | 7 +- setup.cfg | 4 +- tests/test_block.py | 19 ++ tests/test_tx.py | 67 ++++ 33 files changed, 1333 insertions(+), 585 deletions(-) create mode 100644 chainlib/eth/cli.py create mode 100644 chainlib/eth/data/config/config.ini create mode 100644 example/jsonrpc_factory.py create mode 100644 tests/test_block.py diff --git a/CHANGELOG b/CHANGELOG index eda22ae..0aaeda4 100644 --- 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 index 829a7e6..5e9afde 100644 --- 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 index ea8818b..48acf17 100644 --- 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 index e02e5fa..2a0c328 100644 --- 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 get_tx(self, tx_hash): + i = 0 + idx = -1 + tx_hash = add_0x(tx_hash) + for tx in self.txs: + tx_hash_block = None + try: + tx_hash_block = add_0x(tx['hash']) + except TypeError: + tx_hash_block = add_0x(tx) + if tx_hash_block == tx_hash: + idx = i + break + i += 1 + if idx == -1: + raise AttributeError('tx {} not found in block {}'.format(tx_hash, self.hash)) + return idx - - def tx(self, i): - return Tx(self.txs[i], self) - - - def tx_src(self, i): - return self.txs[i] - - - def __str__(self): - return 'block {} {} ({} txs)'.format(self.number, self.hash, len(self.txs)) - - - @staticmethod - def from_src(src): - return Block(src) diff --git a/chainlib/eth/chain.py b/chainlib/eth/chain.py index c68f1c8..6bcf639 100644 --- 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 new file mode 100644 index 0000000..971b779 --- /dev/null +++ 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 index 4bc284b..6c30297 100644 --- 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 index 1e10a2a..8391a95 100644 --- 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 index 87466c4..494d2a2 100644 --- 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 new file mode 100644 index 0000000..70278d1 --- /dev/null +++ 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 index 50e8e51..f33e44b 100644 --- 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 index 0b86858..e56079b 100644 --- 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): + """Gas transaction helper. + """ - def create(self, sender_address, recipient_address, value, tx_format=TxFormat.JSONRPC, id_generator=None): + def create(self, sender_address, recipient_address, value, data=None, tx_format=TxFormat.JSONRPC, id_generator=None): + """Generate json-rpc query to execute gas transaction. + + See parent class TxFactory for details on output format and general usage. + + :param sender_address: Sender address, in hex + :type sender_address: str + :param recipient_address: Recipient address, in hex + :type recipient_address: str + :param value: Value of transaction, integer decimal value (wei) + :type value: int + :param data: Arbitrary input data, in hex. None means no data (vanilla gas transaction). + :type data: str + :param tx_format: Output format + :type tx_format: chainlib.eth.tx.TxFormat + """ tx = self.template(sender_address, recipient_address, use_nonce=True) tx['value'] = value + if data != None: + tx['data'] = data txe = EIP155Transaction(tx, tx['nonce'], tx['chainId']) tx_raw = self.signer.sign_transaction_to_rlp(txe) tx_raw_hex = add_0x(tx_raw.hex()) @@ -68,6 +115,17 @@ class Gas(TxFactory): class RPCGasOracle: + """JSON-RPC only gas parameter helper. + + :param conn: RPC connection + :type conn: chainlib.connection.RPCConnection + :param code_callback: Callback method to evaluate gas usage for method and inputs. + :type code_callback: method taking abi encoded input data as single argument + :param min_price: Override gas price if less than given value + :type min_price: int + :param id_generator: json-rpc id generator + :type id_generator: chainlib.connection.JSONRPCIdGenerator + """ def __init__(self, conn, code_callback=None, min_price=1, id_generator=None): self.conn = conn @@ -76,7 +134,20 @@ class RPCGasOracle: self.id_generator = id_generator - def get_gas(self, code=None): + def get_gas(self, code=None, input_data=None): + """Retrieve gas parameters from node. + + If code is given, the set code callback will be used to estimate gas usage. + + If code is not given or code callback is not set, the chainlib.eth.constant.MINIMUM_FEE_UNITS constant will be used. This gas limit will only be enough gas for a gas transaction without input data. + + :param code: EVM execution code to evaluate against, in hex + :type code: str + :param input_data: Contract input data, in hex + :type input_data: str + :rtype: tuple + :returns: Gas price in wei, and gas limit in gas units + """ gas_price = 0 if self.conn != None: o = price(id_generator=self.id_generator) @@ -93,13 +164,39 @@ class RPCGasOracle: class RPCPureGasOracle(RPCGasOracle): + """Convenience constructor for rpc gas oracle without minimum price. + :param conn: RPC connection + :type conn: chainlib.connection.RPCConnection + :param code_callback: Callback method to evaluate gas usage for method and inputs. + :type code_callback: method taking abi encoded input data as single argument + :param id_generator: json-rpc id generator + :type id_generator: chainlib.connection.JSONRPCIdGenerator + """ def __init__(self, conn, code_callback=None, id_generator=None): super(RPCPureGasOracle, self).__init__(conn, code_callback=code_callback, min_price=0, id_generator=id_generator) class OverrideGasOracle(RPCGasOracle): + """Gas parameter helper that can be selectively overridden. + If both price and limit are set, the conn parameter will not be used. + + If either price or limit is set to None, the rpc in the conn value will be used to query the missing value. + + If both are None, behaves the same as chainlib.eth.gas.RPCGasOracle. + + :param price: Set exact gas price + :type price: int + :param limit: Set exact gas limit + :type limit: int + :param conn: RPC connection for fallback query + :type conn: chainlib.connection.RPCConnection + :param code_callback: Callback method to evaluate gas usage for method and inputs. + :type code_callback: method taking abi encoded input data as single argument + :param id_generator: json-rpc id generator + :type id_generator: chainlib.connection.JSONRPCIdGenerator + """ def __init__(self, price=None, limit=None, conn=None, code_callback=None, id_generator=None): self.conn = None self.code_callback = None @@ -117,6 +214,8 @@ class OverrideGasOracle(RPCGasOracle): def get_gas(self, code=None): + """See chainlib.eth.gas.RPCGasOracle. + """ r = None fee_units = None fee_price = None diff --git a/chainlib/eth/jsonrpc.py b/chainlib/eth/jsonrpc.py index 03db2c4..1761be7 100644 --- 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 index 919d8e0..69d5326 100644 --- 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 index 34f17dc..46f2e79 100644 --- 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) -class NonceOracle: +def nonce_confirmed(address, id_generator=None): + return nonce(address, confirmed=True, id_generator=id_generator) + +class NonceOracle: + """Base class for the nonce parameter helpers. + + :param address: Address to retireve nonce for, in hex + :type address: str + :param id_generator: json-rpc id generator + :type id_generator: chainlib.connection.JSONRPCIdGenerator + """ def __init__(self, address, id_generator=None): self.address = address self.id_generator = id_generator @@ -26,23 +49,45 @@ class NonceOracle: def get_nonce(self): + """Load initial nonce value. + """ raise NotImplementedError('Class must be extended') def next_nonce(self): + """Return next nonce value and advance. + + :rtype: int + :returns: Next nonce for address. + """ n = self.nonce self.nonce += 1 return n class RPCNonceOracle(NonceOracle): + """JSON-RPC only nonce parameter helper. + :param address: Address to retireve nonce for, in hex + :type address: str + :param conn: RPC connection + :type conn: chainlib.connection.RPCConnection + :param id_generator: json-rpc id generator + :type id_generator: chainlib.connection.JSONRPCIdGenerator + """ def __init__(self, address, conn, id_generator=None): self.conn = conn super(RPCNonceOracle, self).__init__(address, id_generator=id_generator) def get_nonce(self): + """Load and return nonce value from network. + + Note! First call to next_nonce after calling get_nonce will return the same value! + + :rtype: int + :returns: Initial nonce + """ o = nonce(self.address, id_generator=self.id_generator) r = self.conn.do(o) n = strip_0x(r) @@ -50,14 +95,28 @@ class RPCNonceOracle(NonceOracle): class OverrideNonceOracle(NonceOracle): + """Manually set initial nonce value. - def __init__(self, address, nonce): - self.nonce = nonce - super(OverrideNonceOracle, self).__init__(address) + :param address: Address to retireve nonce for, in hex + :type address: str + :param nonce: Nonce value + :type nonce: int + :param id_generator: json-rpc id generator (not used) + :type id_generator: chainlib.connection.JSONRPCIdGenerator + """ + def __init__(self, address, nonce, id_generator=None): + self.initial_nonce = nonce + self.nonce = self.initial_nonce + super(OverrideNonceOracle, self).__init__(address, id_generator=id_generator) def get_nonce(self): - return self.nonce + """Returns initial nonce value set at object construction. + + :rtype: int + :returns: Initial nonce value. + """ + return self.initial_nonce DefaultNonceOracle = RPCNonceOracle diff --git a/chainlib/eth/pytest/fixtures_ethtester.py b/chainlib/eth/pytest/fixtures_ethtester.py index 61b0b44..da593c9 100644 --- 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 index 52f42de..e44b128 100644 --- a/chainlib/eth/runnable/balance.py +++ b/chainlib/eth/runnable/balance.py @@ -1,30 +1,19 @@ -#!python3 - -"""Token balance query script - -.. moduleauthor:: Louis Holbrook -.. pgp:: 0826EDA1702D1E87C6E2875121D2E7BB88C2A746 - -""" - # SPDX-License-Identifier: GPL-3.0-or-later # standard imports import os -import json -import argparse import logging -# third-party imports +# external imports from hexathon import ( add_0x, strip_0x, even, ) -import sha3 # local imports -from chainlib.eth.address import to_checksum +import chainlib.eth.cli +from chainlib.eth.address import AddressChecksum from chainlib.jsonrpc import ( jsonrpc_result, IntSequenceGenerator, @@ -35,53 +24,37 @@ from chainlib.eth.gas import ( balance, ) from chainlib.chain import ChainSpec +from crypto_dev_signer.eth.signer import ReferenceSigner as EIP155Signer logging.basicConfig(level=logging.WARNING) logg = logging.getLogger() -default_eth_provider = os.environ.get('RPC_PROVIDER') -if default_eth_provider == None: - default_eth_provider = os.environ.get('ETH_PROVIDER', 'http://localhost:8545') +script_dir = os.path.dirname(os.path.realpath(__file__)) +#config_dir = os.path.join(script_dir, '..', 'data', 'config') -argparser = argparse.ArgumentParser() -argparser.add_argument('-p', '--provider', dest='p', default=default_eth_provider, type=str, help='Web3 provider url (http only)') -argparser.add_argument('-i', '--chain-spec', dest='i', type=str, default='evm:ethereum:1', help='Chain specification string') -argparser.add_argument('-u', '--unsafe', dest='u', action='store_true', help='Auto-convert address to checksum adddress') -argparser.add_argument('--seq', action='store_true', help='Use sequential rpc ids') -argparser.add_argument('-v', action='store_true', help='Be verbose') -argparser.add_argument('-vv', action='store_true', help='Be more verbose') -argparser.add_argument('address', type=str, help='Account address') +arg_flags = chainlib.eth.cli.argflag_std_read +argparser = chainlib.eth.cli.ArgumentParser(arg_flags) +argparser.add_positional('address', type=str, help='Ethereum address of recipient') args = argparser.parse_args() +#config = chainlib.eth.cli.Config.from_args(args, arg_flags, default_config_dir=config_dir) +config = chainlib.eth.cli.Config.from_args(args, arg_flags) +wallet = chainlib.eth.cli.Wallet() +wallet.from_config(config) +holder_address = args.address +if wallet.get_signer_address() == None and holder_address != None: + holder_address = wallet.from_address(holder_address) -if args.vv: - logg.setLevel(logging.DEBUG) -elif args.v: - logg.setLevel(logging.INFO) +rpc = chainlib.eth.cli.Rpc() +conn = rpc.connect_by_config(config) -rpc_id_generator = None -if args.seq: - rpc_id_generator = IntSequenceGenerator() - -auth = None -if os.environ.get('RPC_AUTHENTICATION') == 'basic': - from chainlib.auth import BasicAuth - auth = BasicAuth(os.environ['RPC_USERNAME'], os.environ['RPC_PASSWORD']) -conn = EthHTTPConnection(args.p, auth=auth) - -gas_oracle = OverrideGasOracle(conn) - -address = to_checksum(args.address) -if not args.u and address != add_0x(args.address): - raise ValueError('invalid checksum address') - -chain_spec = ChainSpec.from_chain_str(args.i) +chain_spec = ChainSpec.from_chain_str(config.get('CHAIN_SPEC')) def main(): r = None decimals = 18 - o = balance(address, id_generator=rpc_id_generator) + o = balance(holder_address, id_generator=rpc.id_generator) r = conn.do(o) hx = strip_0x(r) diff --git a/chainlib/eth/runnable/count.py b/chainlib/eth/runnable/count.py index 039fc2f..bb59a4d 100644 --- 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') +holder_address = args.address +wallet = chainlib.eth.cli.Wallet() +wallet.from_config(config) +if wallet.get_signer_address() == None and holder_address != None: + wallet.from_address(holder_address) -if args.vv: - logg.setLevel(logging.DEBUG) -elif args.v: - logg.setLevel(logging.INFO) +rpc = chainlib.eth.cli.Rpc(wallet=wallet) +conn = rpc.connect_by_config(config) -signer_address = None -keystore = DictKeystore() -if args.y != None: - logg.debug('loading keystore file {}'.format(args.y)) - signer_address = keystore.import_keystore_file(args.y, passphrase) - logg.debug('now have key for signer address {}'.format(signer_address)) -signer = EIP155Signer(keystore) - -rpc_id_generator = None -if args.seq: - rpc_id_generator = IntSequenceGenerator() - -auth = None -if os.environ.get('RPC_AUTHENTICATION') == 'basic': - from chainlib.auth import BasicAuth - auth = BasicAuth(os.environ['RPC_USERNAME'], os.environ['RPC_PASSWORD']) -rpc = EthHTTPConnection(args.p, auth=auth) - def main(): - recipient = to_checksum(args.address) - if not args.u and recipient != add_0x(args.address): - raise ValueError('invalid checksum address') - - o = count(recipient, id_generator=rpc_id_generator) - r = rpc.do(o) + o = count(holder_address, id_generator=rpc.id_generator) + r = conn.do(o) count_result = None try: count_result = int(r, 16) diff --git a/chainlib/eth/runnable/decode.py b/chainlib/eth/runnable/decode.py index b598fcf..aaf7abd 100644 --- a/chainlib/eth/runnable/decode.py +++ b/chainlib/eth/runnable/decode.py @@ -1,12 +1,3 @@ -#!python3 - -"""Decode raw transaction - -.. moduleauthor:: Louis Holbrook -.. 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 index 09ad3da..9e1638d 100644 --- a/chainlib/eth/runnable/gas.py +++ b/chainlib/eth/runnable/gas.py @@ -1,12 +1,3 @@ -#!python3 - -"""Gas transfer script - -.. moduleauthor:: Louis Holbrook -.. pgp:: 0826EDA1702D1E87C6E2875121D2E7BB88C2A746 - -""" - # SPDX-License-Identifier: GPL-3.0-or-later # standard imports @@ -19,114 +10,53 @@ import logging import urllib # external imports -from crypto_dev_signer.eth.signer import ReferenceSigner as EIP155Signer -from crypto_dev_signer.keystore.dict import DictKeystore from hexathon import ( add_0x, strip_0x, ) # local imports -from chainlib.eth.address import to_checksum +from chainlib.eth.address import to_checksum_address from chainlib.eth.connection import EthHTTPConnection from chainlib.jsonrpc import ( JSONRPCRequest, IntSequenceGenerator, ) -from chainlib.eth.nonce import ( - RPCNonceOracle, - OverrideNonceOracle, - ) -from chainlib.eth.gas import ( - RPCGasOracle, - OverrideGasOracle, - Gas, - ) +from chainlib.eth.gas import Gas from chainlib.eth.gas import balance as gas_balance from chainlib.chain import ChainSpec from chainlib.eth.runnable.util import decode_for_puny_humans +import chainlib.eth.cli logging.basicConfig(level=logging.WARNING) logg = logging.getLogger() -default_eth_provider = os.environ.get('RPC_PROVIDER') -if default_eth_provider == None: - default_eth_provider = os.environ.get('ETH_PROVIDER', 'http://localhost:8545') - -argparser = argparse.ArgumentParser() -argparser.add_argument('-p', '--provider', dest='p', default=default_eth_provider, type=str, help='Web3 provider url (http only)') -argparser.add_argument('-w', action='store_true', help='Wait for the last transaction to be confirmed') -argparser.add_argument('-ww', action='store_true', help='Wait for every transaction to be confirmed') -argparser.add_argument('-i', '--chain-spec', dest='i', type=str, default='evm:ethereum:1', help='Chain specification string') -argparser.add_argument('-y', '--key-file', dest='y', type=str, help='Ethereum keystore file to use for signing') -argparser.add_argument('--env-prefix', default=os.environ.get('CONFINI_ENV_PREFIX'), dest='env_prefix', type=str, help='environment prefix for variables to overwrite configuration') -argparser.add_argument('--nonce', type=int, help='override nonce') -argparser.add_argument('--gas-price', dest='gas_price', type=int, help='override gas price') -argparser.add_argument('--gas-limit', dest='gas_limit', type=int, help='override gas limit') -argparser.add_argument('-u', '--unsafe', dest='u', action='store_true', help='Auto-convert address to checksum adddress') -argparser.add_argument('--seq', action='store_true', help='Use sequential rpc ids') -argparser.add_argument('-v', action='store_true', help='Be verbose') -argparser.add_argument('-vv', action='store_true', help='Be more verbose') -argparser.add_argument('-s', '--send', dest='s', action='store_true', help='Send to network') -argparser.add_argument('recipient', type=str, help='ethereum address of recipient') -argparser.add_argument('amount', type=int, help='gas value in wei') +arg_flags = chainlib.eth.cli.argflag_std_write | chainlib.eth.cli.Flag.WALLET +argparser = chainlib.eth.cli.ArgumentParser(arg_flags) +argparser.add_argument('--data', type=str, help='Transaction data') +argparser.add_positional('amount', type=int, help='Token amount to send') args = argparser.parse_args() - - -if args.vv: - logg.setLevel(logging.DEBUG) -elif args.v: - logg.setLevel(logging.INFO) +extra_args = { + 'data': None, + 'amount': None, + } +#config = chainlib.eth.cli.Config.from_args(args, arg_flags, extra_args=extra_args, default_config_dir=config_dir) +config = chainlib.eth.cli.Config.from_args(args, arg_flags, extra_args=extra_args) block_all = args.ww block_last = args.w or block_all -passphrase_env = 'ETH_PASSPHRASE' -if args.env_prefix != None: - passphrase_env = args.env_prefix + '_' + passphrase_env -passphrase = os.environ.get(passphrase_env) -if passphrase == None: - logg.warning('no passphrase given') - passphrase='' +wallet = chainlib.eth.cli.Wallet() +wallet.from_config(config) -signer_address = None -keystore = DictKeystore() -if args.y != None: - logg.debug('loading keystore file {}'.format(args.y)) - signer_address = keystore.import_keystore_file(args.y, password=passphrase) - logg.debug('now have key for signer address {}'.format(signer_address)) -signer = EIP155Signer(keystore) +rpc = chainlib.eth.cli.Rpc(wallet=wallet) +conn = rpc.connect_by_config(config) -rpc_id_generator = None -if args.seq: - rpc_id_generator = IntSequenceGenerator() +chain_spec = ChainSpec.from_chain_str(config.get('CHAIN_SPEC')) -auth = None -if os.environ.get('RPC_AUTHENTICATION') == 'basic': - from chainlib.auth import BasicAuth - auth = BasicAuth(os.environ['RPC_USERNAME'], os.environ['RPC_PASSWORD']) -conn = EthHTTPConnection(args.p, auth=auth) +value = config.get('_AMOUNT') -nonce_oracle = None -if args.nonce != None: - nonce_oracle = OverrideNonceOracle(signer_address, args.nonce, id_generator=rpc_id_generator) -else: - nonce_oracle = RPCNonceOracle(signer_address, conn, id_generator=rpc_id_generator) - -gas_oracle = None -if args.gas_price or args.gas_limit != None: - gas_oracle = OverrideGasOracle(price=args.gas_price, limit=args.gas_limit, conn=conn, id_generator=rpc_id_generator) -else: - gas_oracle = RPCGasOracle(conn, id_generator=rpc_id_generator) - - -chain_spec = ChainSpec.from_chain_str(args.i) - -value = args.amount - -send = args.s - -g = Gas(chain_spec, signer=signer, gas_oracle=gas_oracle, nonce_oracle=nonce_oracle) +send = config.true('_RPC_SEND') def balance(address, id_generator): @@ -137,29 +67,34 @@ def balance(address, id_generator): def main(): - recipient = to_checksum(args.recipient) - if not args.u and recipient != add_0x(args.recipient): + signer = rpc.get_signer() + signer_address = rpc.get_sender_address() + + g = Gas(chain_spec, signer=signer, gas_oracle=rpc.get_gas_oracle(), nonce_oracle=rpc.get_nonce_oracle()) + + recipient = to_checksum_address(config.get('_RECIPIENT')) + if not config.true('_UNSAFE') and recipient != add_0x(config.get('_RECIPIENT')): raise ValueError('invalid checksum address') logg.info('gas transfer from {} to {} value {}'.format(signer_address, recipient, value)) if logg.isEnabledFor(logging.DEBUG): try: - sender_balance = balance(signer_address, rpc_id_generator) - recipient_balance = balance(recipient, rpc_id_generator) + sender_balance = balance(signer_address, rpc.id_generator) + recipient_balance = balance(recipient, rpc.id_generator) logg.debug('sender {} balance before: {}'.format(signer_address, sender_balance)) logg.debug('recipient {} balance before: {}'.format(recipient, recipient_balance)) except urllib.error.URLError: pass - (tx_hash_hex, o) = g.create(signer_address, recipient, value, id_generator=rpc_id_generator) + (tx_hash_hex, o) = g.create(signer_address, recipient, value, data=config.get('_DATA'), id_generator=rpc.id_generator) if send: conn.do(o) if block_last: r = conn.wait(tx_hash_hex) if logg.isEnabledFor(logging.DEBUG): - sender_balance = balance(signer_address, rpc_id_generator) - recipient_balance = balance(recipient, rpc_id_generator) + sender_balance = balance(signer_address, rpc.id_generator) + recipient_balance = balance(recipient, rpc.id_generator) logg.debug('sender {} balance after: {}'.format(signer_address, sender_balance)) logg.debug('recipient {} balance after: {}'.format(recipient, recipient_balance)) if r['status'] == 0: @@ -167,12 +102,13 @@ def main(): sys.exit(1) print(tx_hash_hex) else: - if logg.isEnabledFor(logging.INFO): + #if logg.isEnabledFor(logging.INFO): + if config.true('_RAW'): + print(o['params'][0]) + else: io_str = io.StringIO() decode_for_puny_humans(o['params'][0], chain_spec, io_str) print(io_str.getvalue()) - else: - print(o['params'][0]) diff --git a/chainlib/eth/runnable/get.py b/chainlib/eth/runnable/get.py index 8516b75..b358e39 100644 --- a/chainlib/eth/runnable/get.py +++ b/chainlib/eth/runnable/get.py @@ -1,12 +1,3 @@ -#!python3 - -"""Data retrieval script - -.. moduleauthor:: Louis Holbrook -.. pgp:: 0826EDA1702D1E87C6E2875121D2E7BB88C2A746 - -""" - # SPDX-License-Identifier: GPL-3.0-or-later # standard imports @@ -19,80 +10,56 @@ import enum import select # external imports +from potaahto.symbols import snake_and_camel from hexathon import ( add_0x, strip_0x, ) import sha3 - -# local imports -from chainlib.eth.address import to_checksum from chainlib.jsonrpc import ( JSONRPCRequest, jsonrpc_result, IntSequenceGenerator, ) +from chainlib.chain import ChainSpec +from chainlib.status import Status + +# local imports from chainlib.eth.connection import EthHTTPConnection from chainlib.eth.tx import ( Tx, pack, ) -from chainlib.eth.address import to_checksum_address -from chainlib.eth.block import Block -from chainlib.chain import ChainSpec -from chainlib.status import Status +from chainlib.eth.address import ( + to_checksum_address, + is_checksum_address, + ) +from chainlib.eth.block import ( + Block, + block_by_hash, + ) from chainlib.eth.runnable.util import decode_for_puny_humans +from chainlib.eth.jsonrpc import to_blockheight_param +import chainlib.eth.cli logging.basicConfig(level=logging.WARNING, format='%(asctime)s %(levelname)s %(filename)s:%(lineno)d %(message)s') logg = logging.getLogger() -default_eth_provider = os.environ.get('RPC_PROVIDER') -if default_eth_provider == None: - default_eth_provider = os.environ.get('ETH_PROVIDER', 'http://localhost:8545') +script_dir = os.path.dirname(os.path.realpath(__file__)) +config_dir = os.path.join(script_dir, '..', 'data', 'config') -def stdin_arg(t=0): - h = select.select([sys.stdin], [], [], t) - if len(h[0]) > 0: - v = h[0][0].read() - return v.rstrip() - return None - -argparser = argparse.ArgumentParser('eth-get', description='display information about an Ethereum address or transaction', epilog='address/transaction can be provided as an argument or from standard input') -argparser.add_argument('-p', '--provider', dest='p', default=default_eth_provider, type=str, help='Web3 provider url (http only)') -argparser.add_argument('-i', '--chain-spec', dest='i', type=str, default='evm:ethereum:1', help='Chain specification string') -argparser.add_argument('--rlp', action='store_true', help='Display transaction as raw rlp') -argparser.add_argument('--seq', action='store_true', help='Use sequential rpc ids') -argparser.add_argument('-u', '--unsafe', dest='u', action='store_true', help='Auto-convert address to checksum adddress') -argparser.add_argument('-v', action='store_true', help='Be verbose') -argparser.add_argument('-vv', action='store_true', help='Be more verbose') -argparser.add_argument('item', nargs='?', default=stdin_arg(), type=str, help='Item to get information for (address og transaction)') +arg_flags = chainlib.eth.cli.argflag_std_read +argparser = chainlib.eth.cli.ArgumentParser(arg_flags) +argparser.add_positional('item', type=str, help='Address or transaction to retrieve data for') args = argparser.parse_args() +config = chainlib.eth.cli.Config.from_args(args, arg_flags, default_config_dir=config_dir) -if args.vv: - logg.setLevel(logging.DEBUG) -elif args.v: - logg.setLevel(logging.INFO) +rpc = chainlib.eth.cli.Rpc() +conn = rpc.connect_by_config(config) -argp = args.item -if argp == None: - argp = stdin_arg(None) - if argsp == None: - argparser.error('need first positional argument or value from stdin') - -rpc_id_generator = None -if args.seq: - rpc_id_generator = IntSequenceGenerator() - -auth = None -if os.environ.get('RPC_AUTHENTICATION') == 'basic': - from chainlib.auth import BasicAuth - auth = BasicAuth(os.environ['RPC_USERNAME'], os.environ['RPC_PASSWORD']) -conn = EthHTTPConnection(args.p, auth=auth) - -chain_spec = ChainSpec.from_chain_str(args.i) +chain_spec = ChainSpec.from_chain_str(config.get('CHAIN_SPEC')) item = add_0x(args.item) -as_rlp = bool(args.rlp) def get_transaction(conn, tx_hash, id_generator): @@ -106,7 +73,7 @@ def get_transaction(conn, tx_hash, id_generator): logg.error('Transaction {} not found'.format(tx_hash)) sys.exit(1) - if as_rlp: + if config.true('_RAW'): tx_src = Tx.src_normalize(tx_src) return pack(tx_src, chain_spec).hex() @@ -125,16 +92,24 @@ def get_transaction(conn, tx_hash, id_generator): tx = Tx(tx_src) if rcpt != None: tx.apply_receipt(rcpt) + rcpt = snake_and_camel(rcpt) + o = block_by_hash(rcpt['block_hash']) + r = conn.do(o) + block = Block(r) + tx.apply_block(block) + logg.debug('foo {}'.format(tx_src)) tx.generate_wire(chain_spec) return tx + -def get_address(conn, address, id_generator): +def get_address(conn, address, id_generator, height): j = JSONRPCRequest(id_generator=id_generator) o = j.template() o['method'] = 'eth_getCode' o['params'].append(address) - o['params'].append('latest') + height = to_blockheight_param(height) + o['params'].append(height) o = j.finalize(o) code = conn.do(o) @@ -146,11 +121,18 @@ def get_address(conn, address, id_generator): def main(): + address = item r = None - if len(item) > 42: - r = get_transaction(conn, item, rpc_id_generator).to_human() - elif args.u or to_checksum_address(item): - r = get_address(conn, item, rpc_id_generator) + if len(address) > 42: + r = get_transaction(conn, address, rpc.id_generator) + if not config.true('_RAW'): + r = r.to_human() + else: + if config.get('_UNSAFE'): + address = to_checksum_address(address) + elif not is_checksum_address(address): + raise ValueError('invalid checksum address: {}'.format(address)) + r = get_address(conn, address, rpc.id_generator, config.get('_HEIGHT')) print(r) diff --git a/chainlib/eth/runnable/info.py b/chainlib/eth/runnable/info.py index 4b73972..9ee0458 100644 --- a/chainlib/eth/runnable/info.py +++ b/chainlib/eth/runnable/info.py @@ -1,12 +1,3 @@ -#!python3 - -"""Token balance query script - -.. moduleauthor:: Louis Holbrook -.. pgp:: 0826EDA1702D1E87C6E2875121D2E7BB88C2A746 - -""" - # SPDX-License-Identifier: GPL-3.0-or-later # standard imports @@ -17,19 +8,18 @@ import json import argparse import logging -# third-party imports +# external imports +from chainlib.chain import ChainSpec from hexathon import ( add_0x, strip_0x, even, ) import sha3 +from crypto_dev_signer.eth.signer import ReferenceSigner as EIP155Signer # local imports -from chainlib.eth.address import ( - to_checksum_address, - is_checksum_address, - ) +from chainlib.eth.address import AddressChecksum from chainlib.eth.chain import network_id from chainlib.eth.block import ( block_latest, @@ -43,79 +33,49 @@ from chainlib.eth.gas import ( balance, price, ) -from chainlib.jsonrpc import ( - IntSequenceGenerator, - ) -from chainlib.chain import ChainSpec +import chainlib.eth.cli BLOCK_SAMPLES = 10 logging.basicConfig(level=logging.WARNING) logg = logging.getLogger() -default_eth_provider = os.environ.get('RPC_PROVIDER') -if default_eth_provider == None: - default_eth_provider = os.environ.get('ETH_PROVIDER', 'http://localhost:8545') +script_dir = os.path.dirname(os.path.realpath(__file__)) +config_dir = os.path.join(script_dir, '..', 'data', 'config') -argparser = argparse.ArgumentParser() -argparser.add_argument('-p', '--provider', dest='p', default=default_eth_provider, type=str, help='Web3 provider url (http only)') -argparser.add_argument('-i', '--chain-spec', dest='i', type=str, default='evm:ethereum:1', help='Chain specification string') -argparser.add_argument('-H', '--human', dest='human', action='store_true', help='Use human-friendly formatting') -argparser.add_argument('-u', '--unsafe', dest='u', action='store_true', help='Auto-convert address to checksum adddress') -argparser.add_argument('-l', '--long', dest='l', action='store_true', help='Calculate averages through sampling of blocks and txs') -argparser.add_argument('-v', action='store_true', help='Be verbose') -argparser.add_argument('--seq', action='store_true', help='Use sequential rpc ids') -argparser.add_argument('-vv', action='store_true', help='Be more verbose') -argparser.add_argument('-y', '--key-file', dest='y', type=str, help='Include summary for keyfile') -argparser.add_argument('address', nargs='?', type=str, help='Include summary for address (conflicts with -y)') +arg_flags = chainlib.eth.cli.argflag_std_read +argparser = chainlib.eth.cli.ArgumentParser(arg_flags) +argparser.add_positional('address', type=str, help='Address to retrieve info for', required=False) +argparser.add_argument('--long', action='store_true', help='Calculate averages through sampling of blocks and txs') args = argparser.parse_args() +config = chainlib.eth.cli.Config.from_args(args, arg_flags, extra_args={'long': None}, default_config_dir=config_dir) -if args.vv: - logg.setLevel(logging.DEBUG) -elif args.v: - logg.setLevel(logging.INFO) +holder_address = args.address +wallet = chainlib.eth.cli.Wallet() +wallet.from_config(config) +if wallet.get_signer_address() == None and holder_address != None: + wallet.from_address(holder_address) -signer = None -holder_address = None -if args.address != None: - if not args.u and not is_checksum_address(args.address): - raise ValueError('invalid checksum address {}'.format(args.address)) - holder_address = add_0x(args.address) -elif args.y != None: - f = open(args.y, 'r') - o = json.load(f) - f.close() - holder_address = add_0x(to_checksum_address(o['address'])) - -rpc_id_generator = None -if args.seq: - rpc_id_generator = IntSequenceGenerator() - -auth = None -if os.environ.get('RPC_AUTHENTICATION') == 'basic': - from chainlib.auth import BasicAuth - auth = BasicAuth(os.environ['RPC_USERNAME'], os.environ['RPC_PASSWORD']) -conn = EthHTTPConnection(args.p, auth=auth) - -gas_oracle = OverrideGasOracle(conn) +rpc = chainlib.eth.cli.Rpc(wallet=wallet) +conn = rpc.connect_by_config(config) token_symbol = 'eth' -chain_spec = ChainSpec.from_chain_str(args.i) +chain_spec = ChainSpec.from_chain_str(config.get('CHAIN_SPEC')) -human = args.human +human = not config.true('_RAW') -longmode = args.l +longmode = config.true('_LONG') def main(): - o = network_id(id_generator=rpc_id_generator) + o = network_id(id_generator=rpc.id_generator) r = conn.do(o) #if human: # n = format(n, ',') sys.stdout.write('Network id: {}\n'.format(r)) - o = block_latest(id_generator=rpc_id_generator) + o = block_latest(id_generator=rpc.id_generator) r = conn.do(o) n = int(r, 16) first_block_number = n @@ -123,7 +83,7 @@ def main(): n = format(n, ',') sys.stdout.write('Block: {}\n'.format(n)) - o = block_by_number(first_block_number, False, id_generator=rpc_id_generator) + o = block_by_number(first_block_number, False, id_generator=rpc.id_generator) r = conn.do(o) last_block = Block(r) last_timestamp = last_block.timestamp @@ -132,7 +92,7 @@ def main(): aggr_time = 0.0 aggr_gas = 0 for i in range(BLOCK_SAMPLES): - o = block_by_number(first_block_number-i, False, id_generator=rpc_id_generator) + o = block_by_number(first_block_number-i, False, id_generator=rpc.id_generator) r = conn.do(o) block = Block(r) aggr_time += last_block.timestamp - block.timestamp @@ -150,7 +110,7 @@ def main(): sys.stdout.write('Gaslimit: {}\n'.format(n)) sys.stdout.write('Blocktime: {}\n'.format(aggr_time / BLOCK_SAMPLES)) - o = price(id_generator=rpc_id_generator) + o = price(id_generator=rpc.id_generator) r = conn.do(o) n = int(r, 16) if human: diff --git a/chainlib/eth/runnable/raw.py b/chainlib/eth/runnable/raw.py index 887fa0f..7f74d80 100644 --- a/chainlib/eth/runnable/raw.py +++ b/chainlib/eth/runnable/raw.py @@ -1,12 +1,3 @@ -#!python3 - -"""Gas transfer script - -.. moduleauthor:: Louis Holbrook -.. pgp:: 0826EDA1702D1E87C6E2875121D2E7BB88C2A746 - -""" - # SPDX-License-Identifier: GPL-3.0-or-later # standard imports @@ -19,6 +10,7 @@ import logging import urllib # external imports +import chainlib.eth.cli from crypto_dev_signer.eth.signer import ReferenceSigner as EIP155Signer from crypto_dev_signer.keystore.dict import DictKeystore from hexathon import ( @@ -45,129 +37,88 @@ from chainlib.eth.tx import ( TxFactory, raw, ) +from chainlib.error import SignerMissingException from chainlib.chain import ChainSpec from chainlib.eth.runnable.util import decode_for_puny_humans +from chainlib.eth.jsonrpc import to_blockheight_param logging.basicConfig(level=logging.WARNING) logg = logging.getLogger() -default_eth_provider = os.environ.get('RPC_PROVIDER') -if default_eth_provider == None: - default_eth_provider = os.environ.get('ETH_PROVIDER', 'http://localhost:8545') +script_dir = os.path.dirname(os.path.realpath(__file__)) +config_dir = os.path.join(script_dir, '..', 'data', 'config') -argparser = argparse.ArgumentParser() -argparser.add_argument('-p', '--provider', dest='p', default=default_eth_provider, type=str, help='Web3 provider url (http only)') -argparser.add_argument('-w', action='store_true', help='Wait for the last transaction to be confirmed') -argparser.add_argument('-ww', action='store_true', help='Wait for every transaction to be confirmed') -argparser.add_argument('-i', '--chain-spec', dest='i', type=str, default='evm:ethereum:1', help='Chain specification string') -argparser.add_argument('-y', '--key-file', dest='y', type=str, help='Ethereum keystore file to use for signing') -argparser.add_argument('-u', '--unsafe', dest='u', action='store_true', help='Auto-convert address to checksum adddress') -argparser.add_argument('--env-prefix', default=os.environ.get('CONFINI_ENV_PREFIX'), dest='env_prefix', type=str, help='environment prefix for variables to overwrite configuration') -argparser.add_argument('--nonce', type=int, help='override nonce') -argparser.add_argument('--gas-price', dest='gas_price', type=int, help='override gas price') -argparser.add_argument('--gas-limit', dest='gas_limit', type=int, help='override gas limit') -argparser.add_argument('-a', '--recipient', dest='a', type=str, help='recipient address (None for contract creation)') -argparser.add_argument('-value', type=int, help='gas value of transaction in wei') -argparser.add_argument('--seq', action='store_true', help='Use sequential rpc ids') -argparser.add_argument('-v', action='store_true', help='Be verbose') -argparser.add_argument('-vv', action='store_true', help='Be more verbose') -argparser.add_argument('-s', '--send', dest='s', action='store_true', help='Send to network') -argparser.add_argument('-l', '--local', dest='l', action='store_true', help='Local contract call') -argparser.add_argument('data', nargs='?', type=str, help='Transaction data') +arg_flags = chainlib.eth.cli.argflag_std_write | chainlib.eth.cli.Flag.EXEC +argparser = chainlib.eth.cli.ArgumentParser(arg_flags) +argparser.add_positional('data', type=str, help='Transaction data') args = argparser.parse_args() - - -if args.vv: - logg.setLevel(logging.DEBUG) -elif args.v: - logg.setLevel(logging.INFO) +config = chainlib.eth.cli.Config.from_args(args, arg_flags, default_config_dir=config_dir) block_all = args.ww block_last = args.w or block_all -passphrase_env = 'ETH_PASSPHRASE' -if args.env_prefix != None: - passphrase_env = args.env_prefix + '_' + passphrase_env -passphrase = os.environ.get(passphrase_env) -if passphrase == None: - logg.warning('no passphrase given') - passphrase='' +wallet = chainlib.eth.cli.Wallet(EIP155Signer) +wallet.from_config(config) -signer_address = None -keystore = DictKeystore() -if args.y != None: - logg.debug('loading keystore file {}'.format(args.y)) - signer_address = keystore.import_keystore_file(args.y, password=passphrase) - logg.debug('now have key for signer address {}'.format(signer_address)) -signer = EIP155Signer(keystore) +rpc = chainlib.eth.cli.Rpc(wallet=wallet) +conn = rpc.connect_by_config(config) -rpc_id_generator = None -if args.seq: - rpc_id_generator = IntSequenceGenerator() +send = config.true('_RPC_SEND') -auth = None -if os.environ.get('RPC_AUTHENTICATION') == 'basic': - from chainlib.auth import BasicAuth - auth = BasicAuth(os.environ['RPC_USERNAME'], os.environ['RPC_PASSWORD']) -conn = EthHTTPConnection(args.p, auth=auth) - -send = args.s - -local = args.l -if local: +if config.get('_EXEC_ADDRESS') != None: send = False -nonce_oracle = None -gas_oracle = None -if signer_address != None and not local: - if args.nonce != None: - nonce_oracle = OverrideNonceOracle(signer_address, args.nonce) - else: - nonce_oracle = RPCNonceOracle(signer_address, conn) - - if args.gas_price or args.gas_limit != None: - gas_oracle = OverrideGasOracle(price=args.gas_price, limit=args.gas_limit, conn=conn, id_generator=rpc_id_generator) - else: - gas_oracle = RPCGasOracle(conn, id_generator=rpc_id_generator) - -chain_spec = ChainSpec.from_chain_str(args.i) - -value = args.value - - -g = TxFactory(chain_spec, signer=signer, gas_oracle=gas_oracle, nonce_oracle=nonce_oracle) +chain_spec = None +try: + chain_spec = ChainSpec.from_chain_str(config.get('CHAIN_SPEC')) +except AttributeError: + pass def main(): - recipient = None - if args.a != None: - recipient = add_0x(to_checksum(args.a)) - if not args.u and recipient != add_0x(recipient): + + signer_address = None + try: + signer = rpc.get_signer() + signer_address = rpc.get_signer_address() + except SignerMissingException: + pass + + if config.get('_EXEC_ADDRESS') != None: + exec_address = add_0x(to_checksum(config.get('_EXEC_ADDRESS'))) + if not args.u and exec_address != add_0x(exec_address): raise ValueError('invalid checksum address') - if local: - j = JSONRPCRequest(id_generator=rpc_id_generator) + j = JSONRPCRequest(id_generator=rpc.id_generator) o = j.template() o['method'] = 'eth_call' o['params'].append({ - 'to': recipient, + 'to': exec_address, 'from': signer_address, 'value': '0x00', 'gas': add_0x(int.to_bytes(8000000, 8, byteorder='big').hex()), # TODO: better get of network gas limit 'gasPrice': '0x01', 'data': add_0x(args.data), }) - o['params'].append('latest') + height = to_blockheight_param(config.get('_HEIGHT')) + o['params'].append(height) o = j.finalize(o) r = conn.do(o) - print(strip_0x(r)) + try: + print(strip_0x(r)) + except ValueError: + sys.stderr.write('query returned an empty value\n') + sys.exit(1) return elif signer_address != None: + if chain_spec == None: + raise ValueError('chain spec must be specified') + g = TxFactory(chain_spec, signer=rpc.get_signer(), gas_oracle=rpc.get_gas_oracle(), nonce_oracle=rpc.get_nonce_oracle()) tx = g.template(signer_address, recipient, use_nonce=True) if args.data != None: tx = g.set_code(tx, add_0x(args.data)) - (tx_hash_hex, o) = g.finalize(tx, id_generator=rpc_id_generator) + (tx_hash_hex, o) = g.finalize(tx, id_generator=rpc.id_generator) if send: r = conn.do(o) @@ -177,7 +128,7 @@ def main(): print(tx_hash_hex) else: - o = raw(args.data, id_generator=rpc_id_generator) + o = raw(args.data, id_generator=rpc.id_generator) if send: r = conn.do(o) print(r) diff --git a/chainlib/eth/sign.py b/chainlib/eth/sign.py index 2822ed2..8809feb 100644 --- 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 index f89d431..9a7842a 100644 --- 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 index 10a0438..81710f3 100644 --- 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 index 0e6d326..7f22530 100644 --- 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 new file mode 100644 index 0000000..ad63b7c --- /dev/null +++ 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 index 7fad016..ab8b0de 100644 --- 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 index 0f302b6..0ab89d1 100644 --- 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 index 0dc3e90..8301435 100644 --- 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 new file mode 100644 index 0000000..19079e1 --- /dev/null +++ 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 index ca976b9..f64ec80 100644 --- 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()