From 1b946447ae5feac97373bf942a76b0c59df594ac Mon Sep 17 00:00:00 2001 From: Louis Holbrook Date: Sat, 21 Aug 2021 07:31:59 +0000 Subject: [PATCH] Add docstrings --- CHANGELOG | 8 +- MANIFEST.in | 2 +- README.md | 2 +- chainlib/auth.py | 32 ++ chainlib/block.py | 59 +++ chainlib/chain.py | 64 +++ chainlib/cli/__init__.py | 10 + chainlib/cli/arg.py | 101 ++++ chainlib/cli/base.py | 38 ++ chainlib/cli/config.py | 159 +++++++ chainlib/cli/rpc.py | 82 ++++ chainlib/cli/wallet.py | 57 +++ chainlib/connection.py | 185 +++++++- chainlib/data/config/config.ini | 12 + chainlib/error.py | 17 +- chainlib/eth/address.py | 13 - chainlib/eth/block.py | 70 --- chainlib/eth/connection.py | 130 ------ chainlib/eth/constant.py | 5 - chainlib/eth/contract.py | 288 ------------ chainlib/eth/error.py | 23 - chainlib/eth/gas.py | 138 ------ chainlib/eth/jsonrpc.py | 16 - chainlib/eth/nonce.py | 62 --- chainlib/eth/pytest/__init__.py | 3 - chainlib/eth/pytest/fixtures_chain.py | 17 - chainlib/eth/pytest/fixtures_ethtester.py | 105 ----- chainlib/eth/pytest/fixtures_signer.py | 18 - chainlib/eth/runnable/__init__.py | 0 chainlib/eth/runnable/balance.py | 91 ---- chainlib/eth/runnable/checksum.py | 15 - chainlib/eth/runnable/count.py | 60 --- chainlib/eth/runnable/decode.py | 50 -- chainlib/eth/runnable/gas.py | 163 ------- chainlib/eth/runnable/get.py | 118 ----- chainlib/eth/runnable/info.py | 154 ------ chainlib/eth/runnable/raw.py | 175 ------- chainlib/eth/runnable/subscribe.py | 21 - chainlib/eth/runnable/util.py | 22 - chainlib/eth/sign.py | 23 - chainlib/eth/tx.py | 442 ------------------ chainlib/eth/unittest/base.py | 218 --------- chainlib/eth/unittest/ethtester.py | 80 ---- chainlib/hash.py | 30 +- chainlib/http.py | 13 +- chainlib/interface.py | 255 ++++++++++ chainlib/jsonrpc.py | 157 ++++++- chainlib/stat.py | 16 + chainlib/status.py | 2 + chainlib/tx.py | 14 + requirements.txt | 6 +- setup.cfg | 23 +- setup.py | 14 +- test_requirements.txt | 4 - tests/test_abi.py | 29 -- tests/test_address.py | 35 -- tests/test_interface.py | 28 ++ tests/test_nonce.py | 26 -- tests/test_sign.py | 119 ----- tests/test_stat.py | 49 -- tests/test_tx.py | 30 -- ...--eb3907ecad74a0013c259d5874ae7f22dcbcc95c | 1 - 62 files changed, 1301 insertions(+), 2898 deletions(-) create mode 100644 chainlib/auth.py create mode 100644 chainlib/cli/__init__.py create mode 100644 chainlib/cli/arg.py create mode 100644 chainlib/cli/base.py create mode 100644 chainlib/cli/config.py create mode 100644 chainlib/cli/rpc.py create mode 100644 chainlib/cli/wallet.py create mode 100644 chainlib/data/config/config.ini delete mode 100644 chainlib/eth/address.py delete mode 100644 chainlib/eth/block.py delete mode 100644 chainlib/eth/connection.py delete mode 100644 chainlib/eth/constant.py delete mode 100644 chainlib/eth/contract.py delete mode 100644 chainlib/eth/error.py delete mode 100644 chainlib/eth/gas.py delete mode 100644 chainlib/eth/jsonrpc.py delete mode 100644 chainlib/eth/nonce.py delete mode 100644 chainlib/eth/pytest/__init__.py delete mode 100644 chainlib/eth/pytest/fixtures_chain.py delete mode 100644 chainlib/eth/pytest/fixtures_ethtester.py delete mode 100644 chainlib/eth/pytest/fixtures_signer.py delete mode 100644 chainlib/eth/runnable/__init__.py delete mode 100644 chainlib/eth/runnable/balance.py delete mode 100644 chainlib/eth/runnable/checksum.py delete mode 100644 chainlib/eth/runnable/count.py delete mode 100644 chainlib/eth/runnable/decode.py delete mode 100644 chainlib/eth/runnable/gas.py delete mode 100644 chainlib/eth/runnable/get.py delete mode 100644 chainlib/eth/runnable/info.py delete mode 100644 chainlib/eth/runnable/raw.py delete mode 100644 chainlib/eth/runnable/subscribe.py delete mode 100644 chainlib/eth/runnable/util.py delete mode 100644 chainlib/eth/sign.py delete mode 100644 chainlib/eth/tx.py delete mode 100644 chainlib/eth/unittest/base.py delete mode 100644 chainlib/eth/unittest/ethtester.py create mode 100644 chainlib/interface.py create mode 100644 chainlib/tx.py delete mode 100644 test_requirements.txt delete mode 100644 tests/test_abi.py delete mode 100644 tests/test_address.py create mode 100644 tests/test_interface.py delete mode 100644 tests/test_nonce.py delete mode 100644 tests/test_sign.py delete mode 100644 tests/test_stat.py delete mode 100644 tests/test_tx.py delete mode 100644 tests/testdata/keystore/UTC--2021-01-08T17-18-44.521011372Z--eb3907ecad74a0013c259d5874ae7f22dcbcc95c diff --git a/CHANGELOG b/CHANGELOG index 0011b1b..39b43be 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,4 +1,10 @@ -- 0.0.3-pending +- 0.0.5-pending + * Move eth code to separate package +- 0.0.4-unreleased + * Add pack tx from already signed tx struct + * Add http auth handling for jsonrpc connections + * Add customizable jsonrpc id generator (to allow for buggy server id handling) +- 0.0.3-unreleased * Remove erc20 module (to new external package) - 0.0.2-unreleased * diff --git a/MANIFEST.in b/MANIFEST.in index f9bd145..c460b69 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1 +1 @@ -include requirements.txt +include *requirements.txt LICENSE chainlib/data/config/* diff --git a/README.md b/README.md index 2d764e6..1e7b28e 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ Chainlib is not compatible with python2, nor is there any reason to expect it wi Any generalizable structures and code can be found in the base module directory `chainlib/` -Currently the only operational code for available targets is for the `evm` and the `Ethereum` network protocol. This code can be found in `chainlib/eth`. +Currently the only operational code for available targets is for the `evm` and the `Ethereum` network protocol. This code can be found in the separate package `chainlib-eth`. Every module will have a subdirectory `runnable` which contains CLI convenience tooling for common operations. Any directory `example` will contain code snippets demonstrating usage. diff --git a/chainlib/auth.py b/chainlib/auth.py new file mode 100644 index 0000000..5e1a4a6 --- /dev/null +++ b/chainlib/auth.py @@ -0,0 +1,32 @@ +# standard imports +import base64 + + +class Auth: + + def urllib_header(self): + raise NotImplementedError() + + +class BasicAuth(Auth): + + def __init__(self, username, password): + self.username = username + self.password = password + + + def urllib_header(self): + s = '{}:{}'.format(self.username, self.password) + b = base64.b64encode(s.encode('utf-8')) + return (('Authorization'), ('Basic ' + b.decode('utf-8')),) + + +class CustomHeaderTokenAuth(Auth): + + def __init__(self, header_name, auth_token): + self.header_name = header_name + self.auth_token = auth_token + + + def urllib_header(self): + return (self.header_name, self.auth_token,) diff --git a/chainlib/block.py b/chainlib/block.py index a45c870..8bf318c 100644 --- a/chainlib/block.py +++ b/chainlib/block.py @@ -1,7 +1,66 @@ # standard imports import enum +# local imports +from chainlib.tx import Tx + class BlockSpec(enum.IntEnum): + """General-purpose block-height value designators + """ PENDING = -1 LATEST = 0 + + +class Block: + """Base class to extend for implementation specific block object. + """ + + tx_generator = Tx + + + def src(self): + """Return implementation specific block representation. + + :rtype: dict + :returns: Block representation + """ + return self.block_src + + + def tx(self, idx): + """Return transaction object for transaction data at given index. + + :param idx: Transaction index + :type idx: int + :rtype: chainlib.tx.Tx + :returns: Transaction object + """ + return self.tx_generator(self.txs[idx], self) + + + def tx_src(self, idx): + """Return implementation specific transaction representation for transaction data at given index + + :param idx: Transaction index + :type idx: int + :rtype: chainlib.tx.Tx + :returns: Transaction representation + """ + return self.txs[idx] + + + def __str__(self): + return 'block {} {} ({} txs)'.format(self.number, self.hash, len(self.txs)) + + + @classmethod + def from_src(cls, src): + """Instantiate an implementation specific block object from the given block representation. + + :param src: Block representation + :type src: dict + :rtype: chainlib.block.Block + :returns: Block object + """ + return cls(src) diff --git a/chainlib/chain.py b/chainlib/chain.py index c767613..cb8fb52 100644 --- a/chainlib/chain.py +++ b/chainlib/chain.py @@ -3,7 +3,19 @@ import copy class ChainSpec: + """Encapsulates a 3- to 4-part chain identifier, describing the architecture used and common name of the chain, along with the network id of the connected network. + The optional fourth field can be used to add a description value, independent of the chain identifier value. + + :param engine: Chain architecture + :type engine: str + :param common_name: Well-known name of chain + :type common_name: str + :param network_id: Chain network identifier + :type network_id: int + :param tag: Descriptive tag + :type tag: str + """ def __init__(self, engine, common_name, network_id, tag=None): self.o = { 'engine': engine, @@ -13,23 +25,56 @@ class ChainSpec: } def network_id(self): + """Returns the network id part of the spec. + + :rtype: int + :returns: network_id + """ return self.o['network_id'] def chain_id(self): + """Alias of network_id + + :rtype: int + :returns: network_id + """ return self.o['network_id'] def engine(self): + """Returns the chain architecture part of the spec + + :rtype: str + :returns: engine + """ return self.o['engine'] def common_name(self): + """Returns the common name part of the spec + + :rtype: str + :returns: common_name + """ return self.o['common_name'] @staticmethod def from_chain_str(chain_str): + """Create a new ChainSpec object from a colon-separated string, as output by the string representation of the ChainSpec object. + + String must be in one of the following formats: + + - :: + - ::: + + :param chain_str: Chainspec string + :type chain_str: str + :raises ValueError: Malformed chain string + :rtype: chainlib.chain.ChainSpec + :returns: Resulting chain spec + """ o = chain_str.split(':') if len(o) < 3: raise ValueError('Chain string must have three sections, got {}'.format(len(o))) @@ -41,10 +86,29 @@ class ChainSpec: @staticmethod def from_dict(o): + """Create a new ChainSpec object from a dictionary, as output from the asdict method. + + The chain spec is described by the following keys: + + - engine + - common_name + - network_id + - tag (optional) + + :param o: Chainspec dictionary + :type o: dict + :rtype: chainlib.chain.ChainSpec + :returns: Resulting chain spec + """ return ChainSpec(o['engine'], o['common_name'], o['network_id'], tag=o['tag']) def asdict(self): + """Create a dictionary representation of the chain spec. + + :rtype: dict + :returns: Chain spec dictionary + """ return copy.copy(self.o) diff --git a/chainlib/cli/__init__.py b/chainlib/cli/__init__.py new file mode 100644 index 0000000..a182b3a --- /dev/null +++ b/chainlib/cli/__init__.py @@ -0,0 +1,10 @@ +from .base import ( + Flag, + argflag_std_read, + argflag_std_write, + argflag_std_base, + ) +from .arg import ArgumentParser +from .config import Config +from .rpc import Rpc +from .wallet import Wallet diff --git a/chainlib/cli/arg.py b/chainlib/cli/arg.py new file mode 100644 index 0000000..5ac161d --- /dev/null +++ b/chainlib/cli/arg.py @@ -0,0 +1,101 @@ +# standard imports +import logging +import argparse +import enum +import os +import select +import sys + +# local imports +from .base import ( + default_config_dir, + Flag, + argflag_std_target, + ) + +logg = logging.getLogger(__name__) + + +def stdin_arg(): + h = select.select([sys.stdin], [], [], 0) + if len(h[0]) > 0: + v = h[0][0].read() + return v.rstrip() + return None + + +class ArgumentParser(argparse.ArgumentParser): + + def __init__(self, arg_flags=0x0f, env=os.environ, usage=None, description=None, epilog=None, *args, **kwargs): + super(ArgumentParser, self).__init__(usage=usage, description=description, epilog=epilog) + self.process_flags(arg_flags, env) + self.pos_args = [] + + + def add_positional(self, name, type=str, help=None, required=True): + self.pos_args.append((name, type, help, required,)) + + + def parse_args(self, argv=sys.argv[1:]): + if len(self.pos_args) == 1: + arg = self.pos_args[0] + self.add_argument(arg[0], nargs='?', type=arg[1], default=stdin_arg(), help=arg[2]) + else: + for arg in self.pos_args: + if arg[3]: + self.add_argument(arg[0], type=arg[1], help=arg[2]) + else: + self.add_argument(arg[0], nargs='?', type=arg[1], help=arg[2]) + args = super(ArgumentParser, self).parse_args(args=argv) + + if len(self.pos_args) == 1: + arg = self.pos_args[0] + argname = arg[0] + required = arg[3] + if getattr(args, arg[0], None) == None: + argp = stdin_arg() + if argp == None and required: + self.error('need first positional argument or value from stdin') + setattr(args, arg[0], argp) + + return args + + + def process_flags(self, arg_flags, env): + if arg_flags & Flag.VERBOSE: + self.add_argument('-v', action='store_true', help='Be verbose') + self.add_argument('-vv', action='store_true', help='Be more verbose') + if arg_flags & Flag.CONFIG: + self.add_argument('-c', '--config', type=str, default=env.get('CONFINI_DIR'), help='Configuration directory') + self.add_argument('-n', '--namespace', type=str, help='Configuration namespace') + if arg_flags & Flag.WAIT: + self.add_argument('-w', action='store_true', help='Wait for the last transaction to be confirmed') + self.add_argument('-ww', action='store_true', help='Wait for every transaction to be confirmed') + if arg_flags & Flag.ENV_PREFIX: + self.add_argument('--env-prefix', default=env.get('CONFINI_ENV_PREFIX'), dest='env_prefix', type=str, help='environment prefix for variables to overwrite configuration') + if arg_flags & Flag.PROVIDER: + self.add_argument('-p', '--provider', dest='p', type=str, help='RPC HTTP(S) provider url') + self.add_argument('--height', default='latest', help='Block height to execute against') + if arg_flags & Flag.CHAIN_SPEC: + self.add_argument('-i', '--chain-spec', dest='i', type=str, help='Chain specification string') + if arg_flags & Flag.UNSAFE: + self.add_argument('-u', '--unsafe', dest='u', action='store_true', help='Do not verify address checksums') + if arg_flags & Flag.SEQ: + self.add_argument('--seq', action='store_true', help='Use sequential rpc ids') + if arg_flags & Flag.KEY_FILE: + self.add_argument('-y', '--key-file', dest='y', type=str, help='Keystore file to use for signing or address') + if arg_flags & Flag.SEND: + self.add_argument('-s', '--send', dest='s', action='store_true', help='Send to network') + if arg_flags & Flag.RAW: + self.add_argument('--raw', action='store_true', help='Do not decode output') + if arg_flags & Flag.SIGN: + self.add_argument('--nonce', type=int, help='override nonce') + self.add_argument('--fee-price', dest='fee_price', type=int, help='override fee price') + self.add_argument('--fee-limit', dest='fee_limit', type=int, help='override fee limit') + if arg_flags & argflag_std_target == 0: + arg_flags |= Flag.WALLET + if arg_flags & Flag.EXEC: + self.add_argument('-e', '--exectuable-address', dest='executable_address', type=str, help='contract address') + if arg_flags & Flag.WALLET: + self.add_argument('-a', '--recipient', dest='recipient', type=str, help='recipient address') + diff --git a/chainlib/cli/base.py b/chainlib/cli/base.py new file mode 100644 index 0000000..eec7b35 --- /dev/null +++ b/chainlib/cli/base.py @@ -0,0 +1,38 @@ +# standard imports +import enum +import os + + +script_dir = os.path.dirname(os.path.realpath(__file__)) + +default_config_dir = os.path.join(script_dir, '..', 'data', 'config') + + +# powers of two +class Flag(enum.IntEnum): + # read - nibble 1-2 + VERBOSE = 1 + CONFIG = 2 + RAW = 4 + ENV_PREFIX = 8 + PROVIDER = 16 + CHAIN_SPEC = 32 + UNSAFE = 64 + SEQ = 128 + # read/write - nibble 3 + KEY_FILE = 256 + # write - nibble 4 + SIGN = 4096 + NO_TARGET = 8192 + EXEC = 16384 + WALLET = 32768 + # network - nibble 5 + WAIT = 65536 + WAIT_ALL = 131072 + SEND = 262144 + + +argflag_std_read = 0x2fff +argflag_std_write = 0xff3fff +argflag_std_base = 0x200f +argflag_std_target = 0x00e000 diff --git a/chainlib/cli/config.py b/chainlib/cli/config.py new file mode 100644 index 0000000..f6a2d85 --- /dev/null +++ b/chainlib/cli/config.py @@ -0,0 +1,159 @@ +# standard imports +import logging +import os + +# external imports +import confini + +# local imports +from .base import ( + Flag, + default_config_dir as default_parent_config_dir, + ) + +#logg = logging.getLogger(__name__) +logg = logging.getLogger() + + +def logcallback(config): + logg.debug('config loaded:\n{}'.format(config)) + + +class Config(confini.Config): + + default_base_config_dir = default_parent_config_dir + default_fee_limit = 0 + + @classmethod + def from_args(cls, args, arg_flags, extra_args={}, base_config_dir=None, default_config_dir=None, user_config_dir=None, default_fee_limit=None, logger=None, load_callback=logcallback): + + if logger == None: + logger = logging.getLogger() + + if arg_flags & Flag.CONFIG: + if args.vv: + logger.setLevel(logging.DEBUG) + elif args.v: + logger.setLevel(logging.INFO) + + override_config_dirs = [] + config_dir = [cls.default_base_config_dir] + + if user_config_dir == None: + try: + import xdg.BaseDirectory + user_config_dir = xdg.BaseDirectory.load_first_config('chainlib/eth') + except ModuleNotFoundError: + pass + + # if one or more additional base dirs are defined, add these after the default base dir + # the consecutive dirs cannot include duplicate sections + if base_config_dir != None: + logg.debug('have explicit base config addition {}'.format(base_config_dir)) + if isinstance(base_config_dir, str): + base_config_dir = [base_config_dir] + for d in base_config_dir: + config_dir.append(d) + logg.debug('processing config dir {}'.format(config_dir)) + + # confini dir env var will be used for override configs only in this case + if default_config_dir == None: + default_config_dir = os.environ.get('CONFINI_DIR') + if default_config_dir != None: + if isinstance(default_config_dir, str): + default_config_dir = [default_config_dir] + for d in default_config_dir: + override_config_dirs.append(d) + + # process config command line arguments + if arg_flags & Flag.CONFIG: + + effective_user_config_dir = getattr(args, 'config', None) + if effective_user_config_dir == None: + effective_user_config_dir = user_config_dir + + if effective_user_config_dir != None: + if config_dir == None: + if getattr(args, 'namespace', None) != None: + arg_config_dir = os.path.join(effective_user_config_dir, args.namespace) + config_dir = [cls.default_base_config_dir, effective_user_config_dir] + logg.debug('using config arg as base config addition {}'.format(effective_user_config_dir)) + else: + if getattr(args, 'namespace', None) != None: + arg_config_dir = os.path.join(effective_user_config_dir, args.namespace) + override_config_dirs.append(effective_user_config_dir) + logg.debug('using config arg as config override {}'.format(effective_user_config_dir)) + + + if config_dir == None: + if default_config_dir == None: + default_config_dir = default_parent_config_dir + config_dir = default_config_dir + override_config_dirs = [] + env_prefix = getattr(args, 'env_prefix', None) + + config = confini.Config(config_dir, env_prefix=args.env_prefix, override_dirs=override_config_dirs) + config.process() + + args_override = {} + + if arg_flags & Flag.PROVIDER: + args_override['RPC_HTTP_PROVIDER'] = getattr(args, 'p') + if arg_flags & Flag.CHAIN_SPEC: + args_override['CHAIN_SPEC'] = getattr(args, 'i') + if arg_flags & Flag.KEY_FILE: + args_override['WALLET_KEY_FILE'] = getattr(args, 'y') + + config.dict_override(args_override, 'cli args') + + if arg_flags & Flag.PROVIDER: + config.add(getattr(args, 'height'), '_HEIGHT') + if arg_flags & Flag.UNSAFE: + config.add(getattr(args, 'u'), '_UNSAFE') + if arg_flags & Flag.SEND: + fee_limit = getattr(args, 'fee_limit') + if fee_limit == None: + fee_limit = default_fee_limit + if fee_limit == None: + fee_limit = cls.default_fee_limit + config.add(fee_limit, '_FEE_LIMIT') + config.add(getattr(args, 'fee_price'), '_FEE_PRICE') + config.add(getattr(args, 'nonce'), '_NONCE') + config.add(getattr(args, 's'), '_RPC_SEND') + + # handle wait + wait = 0 + if args.w: + wait |= Flag.WAIT + if args.ww: + wait |= Flag.WAIT_ALL + wait_last = wait & (Flag.WAIT | Flag.WAIT_ALL) + config.add(bool(wait_last), '_WAIT') + wait_all = wait & Flag.WAIT_ALL + config.add(bool(wait_all), '_WAIT_ALL') + if arg_flags & Flag.SEQ: + config.add(getattr(args, 'seq'), '_SEQ') + if arg_flags & Flag.WALLET: + config.add(getattr(args, 'recipient'), '_RECIPIENT') + if arg_flags & Flag.EXEC: + config.add(getattr(args, 'executable_address'), '_EXEC_ADDRESS') + + config.add(getattr(args, 'raw'), '_RAW') + + for k in extra_args.keys(): + v = extra_args[k] + if v == None: + v = '_' + k.upper() + r = getattr(args, k) + existing_r = None + try: + existing_r = config.get(v) + except KeyError: + pass + if existing_r == None or r != None: + config.add(r, v, exists_ok=True) + + if load_callback != None: + load_callback(config) + + return config diff --git a/chainlib/cli/rpc.py b/chainlib/cli/rpc.py new file mode 100644 index 0000000..cd3c5c7 --- /dev/null +++ b/chainlib/cli/rpc.py @@ -0,0 +1,82 @@ +# standard imports +import logging + +# external imports +from chainlib.chain import ChainSpec +from chainlib.connection import RPCConnection +from chainlib.jsonrpc import IntSequenceGenerator +from chainlib.eth.nonce import ( + RPCNonceOracle, + OverrideNonceOracle, + ) +from chainlib.eth.gas import ( + RPCGasOracle, + OverrideGasOracle, + ) +from chainlib.error import SignerMissingException + +logg = logging.getLogger(__name__) + + +class Rpc: + + def __init__(self, cls, wallet=None): + self.constructor = cls + self.id_generator = None + self.conn = None + self.chain_spec = None + self.wallet = wallet + self.nonce_oracle = None + self.gas_oracle = None + + + def connect_by_config(self, config): + auth = None + if config.get('RPC_HTTP_AUTHENTICATION') == 'basic': + from chainlib.auth import BasicAuth + auth = BasicAuth(config.get('RPC_HTTP_USERNAME'), config.get('RPC_HTTP_PASSWORD')) + logg.debug('using basic http auth') + + if config.get('_SEQ'): + self.id_generator = IntSequenceGenerator() + + self.chain_spec = config.get('CHAIN_SPEC') + self.conn = self.constructor(url=config.get('RPC_HTTP_PROVIDER'), chain_spec=self.chain_spec, auth=auth) + + if self.can_sign(): + nonce = config.get('_NONCE') + if nonce != None: + self.nonce_oracle = OverrideNonceOracle(self.get_sender_address(), nonce, id_generator=self.id_generator) + else: + self.nonce_oracle = RPCNonceOracle(self.get_sender_address(), self.conn, id_generator=self.id_generator) + + fee_price = config.get('_FEE_PRICE') + fee_limit = config.get('_FEE_LIMIT') + if fee_price != None or fee_limit != None: + self.gas_oracle = OverrideGasOracle(price=fee_price, limit=fee_limit, conn=self.conn, id_generator=self.id_generator) + else: + self.gas_oracle = RPCGasOracle(self.conn, id_generator=self.id_generator) + + return self.conn + + + def get_nonce_oracle(self): + return self.nonce_oracle + + + def get_gas_oracle(self): + return self.gas_oracle + + + def can_sign(self): + return self.wallet != None and self.wallet.signer != None + + + def get_signer(self): + if self.wallet.signer == None: + raise SignerMissingException() + return self.wallet.signer + + + def get_sender_address(self): + return self.wallet.signer_address diff --git a/chainlib/cli/wallet.py b/chainlib/cli/wallet.py new file mode 100644 index 0000000..6d5b18d --- /dev/null +++ b/chainlib/cli/wallet.py @@ -0,0 +1,57 @@ +# standard imports +import logging + +# external imports +from crypto_dev_signer.keystore.dict import DictKeystore + +logg = logging.getLogger(__name__) + + +class Wallet: + + def __init__(self, signer_cls, keystore=DictKeystore(), checksummer=None): + self.signer_constructor = signer_cls + self.keystore = keystore + self.signer = None + self.signer_address = None + self.nonce_oracle = None + self.gas_oracle = None + self.checksummer = checksummer + self.use_checksum = False + + + def from_config(self, config): + wallet_keyfile = config.get('WALLET_KEY_FILE') + if wallet_keyfile: + logg.debug('keyfile {}'.format(wallet_keyfile)) + self.from_keyfile(wallet_keyfile, passphrase=config.get('WALLET_PASSPHRASE', '')) + self.use_checksum = not config.true('_UNSAFE') + + + def from_keyfile(self, key_file, passphrase=''): + logg.debug('importing key from keystore file {}'.format(key_file)) + self.signer_address = self.keystore.import_keystore_file(key_file, password=passphrase) + self.signer = self.signer_constructor(self.keystore) + logg.info('key for {} imported from keyfile {}'.format(self.signer_address, key_file)) + return self.signer + + + def from_address(self, address): + self.signer_address = address + if self.use_checksum: + if self.checksummer == None: + raise AttributeError('checksum required but no checksummer assigned') + if not self.checksummer.valid(self.signer_address): + raise ValueError('invalid checksum address {}'.format(self.signer_address)) + elif self.checksummer != None: + self.signer_address = self.checksummer.sum(self.signer_address) + logg.info('sender_address set to {}'.format(self.signer_address)) + return self.signer_address + + + def get_signer(self): + return self.signer + + + def get_signer_address(self): + return self.signer_address diff --git a/chainlib/connection.py b/chainlib/connection.py index b1f0a9a..d4244fa 100644 --- a/chainlib/connection.py +++ b/chainlib/connection.py @@ -5,6 +5,7 @@ import logging import enum import re import json +import base64 from urllib.request import ( Request, urlopen, @@ -13,22 +14,26 @@ from urllib.request import ( build_opener, install_opener, ) +from urllib.error import URLError # local imports from .jsonrpc import ( - jsonrpc_template, + JSONRPCRequest, jsonrpc_result, - DefaultErrorParser, + ErrorParser, ) from .http import PreemptiveBasicAuthHandler +from .error import JSONRPCException +from .auth import Auth -logg = logging.getLogger().getChild(__name__) +logg = logging.getLogger(__name__) -error_parser = DefaultErrorParser() +error_parser = ErrorParser() class ConnType(enum.Enum): - + """Describe the underlying RPC connection type. + """ CUSTOM = 0x00 HTTP = 0x100 HTTP_SSL = 0x101 @@ -41,7 +46,15 @@ re_http = '^http(s)?://' re_ws = '^ws(s)?://' re_unix = '^ipc://' + def str_to_connspec(s): + """Determine the connection type from a connection string. + + :param s: Connection string + :type d: str + :rtype: chainlib.connection.ConnType + :returns: Connection type value + """ if s == 'custom': return ConnType.CUSTOM @@ -57,7 +70,6 @@ def str_to_connspec(s): return ConnType.WEBSOCKET_SSL return ConnType.WEBSOCKET - m = re.match(re_unix, s) if m != None: return ConnType.UNIX @@ -65,7 +77,20 @@ def str_to_connspec(s): raise ValueError('unknown connection type {}'.format(s)) -class RPCConnection(): + +class RPCConnection: + """Base class for defining an RPC connection to a chain node. + + This class may be instantiated directly, or used as an object factory to provide a thread-safe RPC connection mechanism to a single RPC node. + + :param url: A valid URL connection string for the RPC connection + :type url: str + :param chain_spec: The chain spec of + :type chain_spec: chainlib.chain.ChainSpec + :param auth: Authentication settings to use when connecting + :type auth: chainlib.auth.Auth + :todo: basic auth is currently parsed from the connection string, should be auth object instead. auth object effectively not in use. + """ __locations = {} __constructors = { @@ -74,15 +99,20 @@ class RPCConnection(): } __constructors_for_chains = {} - def __init__(self, url=None, chain_spec=None): + def __init__(self, url=None, chain_spec=None, auth=None): self.chain_spec = chain_spec self.location = None self.basic = None if url == None: return + self.auth = auth + if self.auth != None and not isinstance(self.auth, Auth): + raise TypeError('auth parameter needs to be subclass of chainlib.auth.Auth') url_parsed = urlparse(url) logg.debug('creating connection {} -> {}'.format(url, url_parsed)) + + # TODO: temporary basic auth parse basic = url_parsed.netloc.split('@') location = None if len(basic) == 1: @@ -93,6 +123,7 @@ class RPCConnection(): #if url_parsed.port != None: # location += ':' + str(url_parsed.port) + # self.location = os.path.join('{}://'.format(url_parsed.scheme), location) self.location = urljoin(self.location, url_parsed.path) @@ -101,20 +132,50 @@ class RPCConnection(): @staticmethod def from_conntype(t, tag='default'): + """Retrieve a connection constructor from the given tag and connection type. + + :param t: Connection type + :type t: chainlib.connection.ConnType + :param tag: The connection selector tag + :type tag: + """ return RPCConnection.__constructors[tag][t] @staticmethod - def register_constructor(t, c, tag='default'): + def register_constructor(conntype, c, tag='default'): + """Associate a connection constructor for a given tag and connection type. + + The constructor must be a chainlib.connection.RPCConnection object or an object of a subclass thereof. + + :param conntype: Connection type of constructor + :type conntype: chainlib.connection.ConnType + :param c: Constructor + :type c: chainlib.connection.RPCConnection + :param tag: Tag to store the connection constructor under + :type tag: str + """ if RPCConnection.__constructors.get(tag) == None: RPCConnection.__constructors[tag] = {} - RPCConnection.__constructors[tag][t] = c - logg.info('registered RPC connection constructor {} for type {} tag {}'.format(c, t, tag)) + RPCConnection.__constructors[tag][conntype] = c + logg.info('registered RPC connection constructor {} for type {} tag {}'.format(c, conntype, tag)) # TODO: constructor needs to be constructor-factory, that itself can select on url type @staticmethod def register_location(location, chain_spec, tag='default', exist_ok=False): + """Associate a URL for a given tag and chain spec. + + :param location: URL of RPC connection + :type location: str + :param chain_spec: Chain spec describing the chain behind the RPC connection + :type chain_spec: chainlib.chain.ChainSpec + :param tag: Tag to store the connection location under + :type tag: str + :param exist_ok: Overwrite existing record + :type exist_ok: bool + :raises ValueError: Record already exists, and exist_ok is not set + """ chain_str = str(chain_spec) if RPCConnection.__locations.get(chain_str) == None: RPCConnection.__locations[chain_str] = {} @@ -129,6 +190,19 @@ class RPCConnection(): @staticmethod def connect(chain_spec, tag='default'): + """Connect to the location defined by the given tag and chain spec, using the associated constructor. + + Location must first be registered using the RPCConnection.register_location method. + + Constructor must first be registered using the RPCConnection.register_constructor method. + + :param chain_spec: Chain spec part of the location record + :type chain_spec: chainlib.chain.ChainSpec + :param tag: Tag part of the location record + :type tag: str + :rtype: chainlib.connection.RPCConnection + :returns: Instantiation of the matching registered constructor + """ chain_str = str(chain_spec) c = RPCConnection.__locations[chain_str][tag] constructor = RPCConnection.from_conntype(c[0], tag=tag) @@ -136,9 +210,9 @@ class RPCConnection(): return constructor(url=c[1], chain_spec=chain_spec) -class HTTPConnection(RPCConnection): - def disconnect(self): + """Should be overridden to clean up any resources bound by the connect method. + """ pass @@ -146,27 +220,84 @@ class HTTPConnection(RPCConnection): self.disconnect() + +class HTTPConnection(RPCConnection): + """Generic HTTP connection subclass of RPCConnection + """ + pass + + + class UnixConnection(RPCConnection): + """Generic Unix socket connection subclass of RPCConnection + """ + pass - def disconnect(self): - pass - - - def __del__(self): - self.disconnect() class JSONRPCHTTPConnection(HTTPConnection): + """Generic JSON-RPC specific HTTP connection wrapper. + """ + + def check_rpc(self): + """Check if RPC connection is a valid JSON-RPC endpoint. + + :raises Exception: Invalid connection. + """ + j = JSONRPCRequest() + req = j.template() + req['method'] = 'ping' + try: + self.do(req) + except JSONRPCException: + pass + + + def check(self): + """Check if endpoint is reachable. + + :rtype: bool + :returns: True if reachable + """ + try: + self.check_rpc() + except URLError as e: + logg.error('cannot connect to node {}; {}'.format(self.location, e)) + return False + return True + def do(self, o, error_parser=error_parser): + """Execute a JSON-RPC query, from dict as generated by chainlib.jsonrpc.JSONRPCRequest:finalize. + + If connection was created with an auth object, the auth object will be used to authenticate the query. + + If connection was created with a basic url string, the corresponding basic auth credentials will be used to authenticate the query. + + :param o: JSON-RPC query object + :type o: dict + :param error_parser: Error parser object to process JSON-RPC error response with. + :type error_parser: chainlib.jsonrpc.ErrorParser + :raises ValueError: Invalid response from JSON-RPC endpoint + :raises URLError: Endpoint could not be reached + :rtype: any + :returns: Result value part of JSON RPC response + :todo: Invalid response exception from invalid json response + """ req = Request( self.location, method='POST', ) req.add_header('Content-Type', 'application/json') + + # use specific auth if present + if self.auth != None: + p = self.auth.urllib_header() + req.add_header(p[0], p[1]) data = json.dumps(o) logg.debug('(HTTP) send {}'.format(data)) + # use basic auth if present if self.basic != None: handler = PreemptiveBasicAuthHandler() handler.add_password( @@ -177,8 +308,9 @@ class JSONRPCHTTPConnection(HTTPConnection): ) ho = build_opener(handler) install_opener(ho) - + r = urlopen(req, data=data.encode('utf-8')) + result = json.load(r) logg.debug('(HTTP) recv {}'.format(result)) if o['id'] != result['id']: @@ -187,6 +319,18 @@ class JSONRPCHTTPConnection(HTTPConnection): class JSONRPCUnixConnection(UnixConnection): + """Execute a JSON-RPC query, from dict as generated by chainlib.jsonrpc.JSONRPCRequest:finalize. + + :param o: JSON-RPC query object + :type o: dict + :param error_parser: Error parser object to process JSON-RPC error response with. + :type error_parser: chainlib.jsonrpc.ErrorParser + :raises ValueError: Invalid response from JSON-RPC endpoint + :raises IOError: Endpoint could not be reached + :rtype: any + :returns: Result value part of JSON RPC response + :todo: Invalid response exception from invalid json response + """ def do(self, o, error_parser=error_parser): conn = socket.socket(family=socket.AF_UNIX, type=socket.SOCK_STREAM, proto=0) @@ -217,6 +361,7 @@ class JSONRPCUnixConnection(UnixConnection): return jsonrpc_result(result, error_parser) +# TODO: Automatic creation should be hidden behind symbol, in the spirit of no unsolicited side-effects. (perhaps connection should be module dir, and jsonrpc a submodule) RPCConnection.register_constructor(ConnType.HTTP, JSONRPCHTTPConnection, tag='default') RPCConnection.register_constructor(ConnType.HTTP_SSL, JSONRPCHTTPConnection, tag='default') RPCConnection.register_constructor(ConnType.UNIX, JSONRPCUnixConnection, tag='default') diff --git a/chainlib/data/config/config.ini b/chainlib/data/config/config.ini new file mode 100644 index 0000000..1772181 --- /dev/null +++ b/chainlib/data/config/config.ini @@ -0,0 +1,12 @@ +[rpc] +http_provider = +http_authentication = +http_username = +http_password = + +[chain] +spec = + +[wallet] +key_file = +passphrase = diff --git a/chainlib/error.py b/chainlib/error.py index e2ac51e..9592bbc 100644 --- a/chainlib/error.py +++ b/chainlib/error.py @@ -1,7 +1,22 @@ # TODO: use json-rpc module -class JSONRPCException(Exception): +class RPCException(Exception): + """Base RPC connection error + """ + pass + + +class JSONRPCException(RPCException): + """Base JSON-RPC error + """ pass class ExecutionError(Exception): + """Base error for transaction execution failures + """ pass + + +class SignerMissingException(Exception): + """Raised when attempting to retrieve a signer when none has been added + """ diff --git a/chainlib/eth/address.py b/chainlib/eth/address.py deleted file mode 100644 index ea8818b..0000000 --- a/chainlib/eth/address.py +++ /dev/null @@ -1,13 +0,0 @@ -# third-party imports -import sha3 -from hexathon import ( - strip_0x, - uniform, - ) -from crypto_dev_signer.encoding import ( - is_address, - is_checksum_address, - to_checksum_address, - ) - -to_checksum = to_checksum_address diff --git a/chainlib/eth/block.py b/chainlib/eth/block.py deleted file mode 100644 index f563850..0000000 --- a/chainlib/eth/block.py +++ /dev/null @@ -1,70 +0,0 @@ -# third-party imports -from chainlib.jsonrpc import jsonrpc_template -from chainlib.eth.tx import Tx -from hexathon import ( - add_0x, - strip_0x, - even, - ) - - -def block_latest(): - o = jsonrpc_template() - o['method'] = 'eth_blockNumber' - return o - - -def block_by_hash(hsh, include_tx=True): - o = jsonrpc_template() - o['method'] = 'eth_getBlockByHash' - o['params'].append(hsh) - o['params'].append(include_tx) - return o - - -def block_by_number(n, include_tx=True): - nhx = add_0x(even(hex(n)[2:])) - o = jsonrpc_template() - o['method'] = 'eth_getBlockByNumber' - o['params'].append(nhx) - o['params'].append(include_tx) - return o - - -def transaction_count(block_hash): - o = jsonrpc_template() - o['method'] = 'eth_getBlockTransactionCountByHash' - o['params'].append(block_hash) - return o - - -class Block: - - def __init__(self, src): - self.hash = src['hash'] - try: - self.number = int(strip_0x(src['number']), 16) - except TypeError: - self.number = int(src['number']) - self.txs = src['transactions'] - self.block_src = src - try: - self.timestamp = int(strip_0x(src['timestamp']), 16) - except TypeError: - self.timestamp = int(src['timestamp']) - - - def src(self): - return self.block_src - - - def tx(self, i): - return Tx(self.txs[i], self) - - - def tx_src(self, i): - return self.txs[i] - - - def __str__(self): - return 'block {} {} ({} txs)'.format(self.number, self.hash, len(self.txs)) diff --git a/chainlib/eth/connection.py b/chainlib/eth/connection.py deleted file mode 100644 index c6564ab..0000000 --- a/chainlib/eth/connection.py +++ /dev/null @@ -1,130 +0,0 @@ -# standard imports -import copy -import logging -import json -import datetime -import time -import socket -from urllib.request import ( - Request, - urlopen, - ) - -# third-party imports -from hexathon import ( - add_0x, - strip_0x, - ) - -# local imports -from .error import ( - DefaultErrorParser, - RevertEthException, - ) -from .sign import ( - sign_transaction, - ) -from chainlib.connection import ( - ConnType, - RPCConnection, - JSONRPCHTTPConnection, - JSONRPCUnixConnection, - error_parser, - ) -from chainlib.jsonrpc import ( - jsonrpc_template, - jsonrpc_result, - ) -from chainlib.eth.tx import ( - unpack, - ) - -logg = logging.getLogger(__name__) - - -class EthHTTPConnection(JSONRPCHTTPConnection): - - def wait(self, tx_hash_hex, delay=0.5, timeout=0.0, error_parser=error_parser): - t = datetime.datetime.utcnow() - i = 0 - while True: - o = jsonrpc_template() - o['method'] ='eth_getTransactionReceipt' - o['params'].append(add_0x(tx_hash_hex)) - req = Request( - self.location, - method='POST', - ) - req.add_header('Content-Type', 'application/json') - data = json.dumps(o) - logg.debug('(HTTP) poll receipt attempt {} {}'.format(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('e {}'.format(strip_0x(e['status']))) - if strip_0x(e['status']) == '00': - raise RevertEthException(tx_hash_hex) - return e - - if timeout > 0.0: - delta = (datetime.datetime.utcnow() - t) + datetime.timedelta(seconds=delay) - if delta.total_seconds() >= timeout: - raise TimeoutError(tx_hash) - - time.sleep(delay) - i += 1 - - -class EthUnixConnection(JSONRPCUnixConnection): - - def wait(self, tx_hash_hex, delay=0.5, timeout=0.0, error_parser=error_parser): - raise NotImplementedError('Not yet implemented for unix socket') - - -def sign_transaction_to_rlp(chain_spec, doer, tx): - 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 - chain_id = txs.get('chainId') or 1 - if chain_spec != None: - chain_id = chain_spec.chain_id() - txs['chainId'] = add_0x(chain_id.to_bytes(2, 'big').hex()) - txs['from'] = add_0x(tx.sender) - o = sign_transaction(txs) - r = doer(o) - logg.debug('sig got {}'.format(r)) - return bytes.fromhex(strip_0x(r)) - - -def sign_message(doer, msg): - o = sign_message(msg) - return doer(o) - - -class EthUnixSignerConnection(EthUnixConnection): - - def sign_transaction_to_rlp(self, tx): - return sign_transaction_to_rlp(self.chain_spec, self.do, tx) - - - def sign_message(self, tx): - return sign_message(self.do, tx) - - -class EthHTTPSignerConnection(EthHTTPConnection): - - def sign_transaction_to_rlp(self, tx): - return sign_transaction_to_rlp(self.chain_spec, self.do, tx) - - - def sign_message(self, tx): - return sign_message(self.do, tx) - - - -RPCConnection.register_constructor(ConnType.HTTP, EthHTTPConnection, tag='eth_default') -RPCConnection.register_constructor(ConnType.HTTP_SSL, EthHTTPConnection, tag='eth_default') -RPCConnection.register_constructor(ConnType.UNIX, EthUnixConnection, tag='eth_default') diff --git a/chainlib/eth/constant.py b/chainlib/eth/constant.py deleted file mode 100644 index 1e10a2a..0000000 --- a/chainlib/eth/constant.py +++ /dev/null @@ -1,5 +0,0 @@ -ZERO_ADDRESS = '0x{:040x}'.format(0) -ZERO_CONTENT = '0x{:064x}'.format(0) -MINIMUM_FEE_UNITS = 21000 -MINIMUM_FEE_PRICE = 1000000000 -MAX_UINT = int('0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff', 16) diff --git a/chainlib/eth/contract.py b/chainlib/eth/contract.py deleted file mode 100644 index 85f4db4..0000000 --- a/chainlib/eth/contract.py +++ /dev/null @@ -1,288 +0,0 @@ -# standard imports -import enum -import re -import logging - -# external imports -from hexathon import ( - strip_0x, - pad, - ) - -# local imports -from chainlib.hash import keccak256_string_to_hex -from chainlib.block import BlockSpec -from chainlib.jsonrpc import jsonrpc_template -from .address import to_checksum_address - -#logg = logging.getLogger(__name__) -logg = logging.getLogger() - - -re_method = r'^[a-zA-Z0-9_]+$' - -class ABIContractType(enum.Enum): - - BYTES32 = 'bytes32' - BYTES4 = 'bytes4' - UINT256 = 'uint256' - ADDRESS = 'address' - STRING = 'string' - BOOLEAN = 'bool' - -dynamic_contract_types = [ - ABIContractType.STRING, - ] - -class ABIContractDecoder: - - def __init__(self): - self.types = [] - self.contents = [] - - - def typ(self, v): - 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) - self.__log_typ() - - - def val(self, v): - self.contents.append(v) - logg.debug('content is now {}'.format(self.contents)) - - - def uint256(self, v): - return int(v, 16) - - - def bytes32(self, v): - return v - - - def bool(self, v): - return bool(self.uint256(v)) - - - def boolean(self, v): - return bool(self.uint256(v)) - - - def address(self, v): - a = strip_0x(v)[64-40:] - return to_checksum_address(a) - - - def string(self, v): - s = strip_0x(v) - b = bytes.fromhex(s) - cursor = 0 - offset = int.from_bytes(b[cursor:cursor+32], 'big') - cursor += 32 - length = int.from_bytes(b[cursor:cursor+32], 'big') - cursor += 32 - content = b[cursor:cursor+length] - logg.debug('parsing string offset {} length {} content {}'.format(offset, length, content)) - return content.decode('utf-8') - - - def __log_typ(self): - logg.debug('types set to ({})'.format(','.join(self.types))) - - - def decode(self): - r = [] - for i in range(len(self.types)): - m = getattr(self, self.types[i]) - r.append(m(self.contents[i])) - return r - - - def get(self): - return self.decode() - - - def __str__(self): - return self.decode() - - - -class ABIContractEncoder: - - - def __init__(self): - self.types = [] - self.contents = [] - self.method_name = None - self.method_contents = [] - - - def method(self, m): - if re.match(re_method, m) == None: - raise ValueError('Invalid method {}, must match regular expression {}'.format(re_method)) - self.method_name = m - self.__log_method() - - - def typ(self, v): - if self.method_name == None: - raise AttributeError('method name must be set before adding types') - if not isinstance(v, ABIContractType): - raise TypeError('method type not valid; expected {}, got {}'.format(type(ABIContractType).__name__, type(v).__name__)) - self.method_contents.append(v.value) - self.__log_method() - - - def __log_method(self): - logg.debug('method set to {}'.format(self.get_method())) - - - def __log_latest(self, v): - l = len(self.types) - 1 - logg.debug('Encoder added {} -> {} ({})'.format(v, self.contents[l], self.types[l].value)) - - - def uint256(self, v): - v = int(v) - b = v.to_bytes(32, 'big') - self.contents.append(b.hex()) - self.types.append(ABIContractType.UINT256) - self.__log_latest(v) - - - def bool(self, v): - return self.boolean(v) - - - def boolean(self, v): - if bool(v): - return self.uint256(1) - return self.uint256(0) - - - def address(self, v): - self.bytes_fixed(32, v, 20) - self.types.append(ABIContractType.ADDRESS) - self.__log_latest(v) - - - def bytes32(self, v): - self.bytes_fixed(32, v) - self.types.append(ABIContractType.BYTES32) - self.__log_latest(v) - - - def bytes4(self, v): - self.bytes_fixed(4, v) - self.types.append(ABIContractType.BYTES4) - self.__log_latest(v) - - - - def string(self, v): - b = v.encode('utf-8') - l = len(b) - contents = l.to_bytes(32, 'big') - contents += b - padlen = 32 - (l % 32) - contents += padlen * b'\x00' - self.bytes_fixed(len(contents), contents) - self.types.append(ABIContractType.STRING) - self.__log_latest(v) - return contents - - - def bytes_fixed(self, mx, v, exact=0): - typ = type(v).__name__ - if typ == 'str': - v = strip_0x(v) - l = len(v) - if exact > 0 and l != exact * 2: - raise ValueError('value wrong size; expected {}, got {})'.format(mx, l)) - if l > mx * 2: - raise ValueError('value too long ({})'.format(l)) - v = pad(v, mx) - elif typ == 'bytes': - l = len(v) - if exact > 0 and l != exact: - raise ValueError('value wrong size; expected {}, got {})'.format(mx, l)) - b = bytearray(mx) - b[mx-l:] = v - v = pad(b.hex(), mx) - else: - raise ValueError('invalid input {}'.format(typ)) - self.contents.append(v.ljust(64, '0')) - - - def get_method(self): - if self.method_name == None: - return '' - return '{}({})'.format(self.method_name, ','.join(self.method_contents)) - - - def get_method_signature(self): - s = self.get_method() - if s == '': - return s - return keccak256_string_to_hex(s)[:8] - - - def get_contents(self): - direct_contents = '' - pointer_contents = '' - l = len(self.types) - pointer_cursor = 32 * l - for i in range(l): - if self.types[i] in dynamic_contract_types: - content_length = len(self.contents[i]) - pointer_contents += self.contents[i] - direct_contents += pointer_cursor.to_bytes(32, 'big').hex() - pointer_cursor += int(content_length / 2) - else: - direct_contents += self.contents[i] - s = ''.join(direct_contents + pointer_contents) - for i in range(0, len(s), 64): - l = len(s) - i - if l > 64: - l = 64 - logg.debug('code word {} {}'.format(int(i / 64), s[i:i+64])) - return s - - - def get(self): - return self.encode() - - - def encode(self): - m = self.get_method_signature() - c = self.get_contents() - return m + c - - - def __str__(self): - return self.encode() - - - -def abi_decode_single(typ, v): - d = ABIContractDecoder() - d.typ(typ) - d.val(v) - r = d.decode() - return r[0] - - -def code(address, block_spec=BlockSpec.LATEST): - block_height = None - if block_spec == BlockSpec.LATEST: - block_height = 'latest' - elif block_spec == BlockSpec.PENDING: - block_height = 'pending' - else: - block_height = int(block_spec) - o = jsonrpc_template() - o['method'] = 'eth_getCode' - o['params'].append(address) - o['params'].append(block_height) - return o diff --git a/chainlib/eth/error.py b/chainlib/eth/error.py deleted file mode 100644 index 50e8e51..0000000 --- a/chainlib/eth/error.py +++ /dev/null @@ -1,23 +0,0 @@ -# local imports -from chainlib.error import ExecutionError - -class EthException(Exception): - pass - - -class RevertEthException(EthException, ExecutionError): - pass - - -class NotFoundEthException(EthException): - pass - - -class RequestMismatchException(EthException): - pass - - -class DefaultErrorParser: - - def translate(self, error): - return EthException('default parser code {}'.format(error)) diff --git a/chainlib/eth/gas.py b/chainlib/eth/gas.py deleted file mode 100644 index 108b1be..0000000 --- a/chainlib/eth/gas.py +++ /dev/null @@ -1,138 +0,0 @@ -# standard imports -import logging - -# third-party imports -from hexathon import ( - add_0x, - strip_0x, - ) -from crypto_dev_signer.eth.transaction import EIP155Transaction - -# local imports -from chainlib.hash import keccak256_hex_to_hex -from chainlib.jsonrpc import jsonrpc_template -from chainlib.eth.tx import ( - TxFactory, - TxFormat, - raw, - ) -from chainlib.eth.constant import ( - MINIMUM_FEE_UNITS, - ) - -logg = logging.getLogger(__name__) - - -def price(): - o = jsonrpc_template() - o['method'] = 'eth_gasPrice' - return o - - -def balance(address): - o = jsonrpc_template() - o['method'] = 'eth_getBalance' - o['params'].append(address) - o['params'].append('latest') - return o - - -class Gas(TxFactory): - - def create(self, sender_address, recipient_address, value, tx_format=TxFormat.JSONRPC): - tx = self.template(sender_address, recipient_address, use_nonce=True) - tx['value'] = value - txe = EIP155Transaction(tx, tx['nonce'], tx['chainId']) - tx_raw = self.signer.sign_transaction_to_rlp(txe) - tx_raw_hex = add_0x(tx_raw.hex()) - tx_hash_hex = add_0x(keccak256_hex_to_hex(tx_raw_hex)) - - o = None - if tx_format == TxFormat.JSONRPC: - o = raw(tx_raw_hex) - elif tx_format == TxFormat.RLP_SIGNED: - o = tx_raw_hex - - return (tx_hash_hex, o) - - - -class RPCGasOracle: - - def __init__(self, conn, code_callback=None, min_price=1): - self.conn = conn - self.code_callback = code_callback - self.min_price = min_price - - - def get_gas(self, code=None): - gas_price = 0 - if self.conn != None: - o = price() - r = self.conn.do(o) - n = strip_0x(r) - gas_price = int(n, 16) - fee_units = MINIMUM_FEE_UNITS - if self.code_callback != None: - fee_units = self.code_callback(code) - if gas_price < self.min_price: - logg.debug('adjusting price {} to set minimum {}'.format(gas_price, self.min_price)) - gas_price = self.min_price - return (gas_price, fee_units) - - -class RPCPureGasOracle(RPCGasOracle): - - def __init__(self, conn, code_callback=None): - super(RPCPureGasOracle, self).__init__(conn, code_callback=code_callback, min_price=0) - - -class OverrideGasOracle(RPCGasOracle): - - def __init__(self, price=None, limit=None, conn=None, code_callback=None): - self.conn = None - self.code_callback = None - self.limit = limit - self.price = price - - price_conn = None - - if self.limit == None or self.price == None: - if self.price == None: - price_conn = conn - logg.debug('override gas oracle with rpc fallback; price {} limit {}'.format(self.price, self.limit)) - - super(OverrideGasOracle, self).__init__(price_conn, code_callback) - - - def get_gas(self, code=None): - r = None - fee_units = None - fee_price = None - - rpc_results = super(OverrideGasOracle, self).get_gas(code) - - if self.limit != None: - fee_units = self.limit - if self.price != None: - fee_price = self.price - - if fee_price == None: - if rpc_results != None: - fee_price = rpc_results[0] - logg.debug('override gas oracle without explicit price, setting from rpc {}'.format(fee_price)) - else: - fee_price = MINIMUM_FEE_PRICE - logg.debug('override gas oracle without explicit price, setting default {}'.format(fee_price)) - if fee_units == None: - if rpc_results != None: - fee_units = rpc_results[1] - logg.debug('override gas oracle without explicit limit, setting from rpc {}'.format(fee_units)) - else: - fee_units = MINIMUM_FEE_UNITS - logg.debug('override gas oracle without explicit limit, setting default {}'.format(fee_units)) - - return (fee_price, fee_units) - - -DefaultGasOracle = RPCGasOracle diff --git a/chainlib/eth/jsonrpc.py b/chainlib/eth/jsonrpc.py deleted file mode 100644 index 03db2c4..0000000 --- a/chainlib/eth/jsonrpc.py +++ /dev/null @@ -1,16 +0,0 @@ -# proposed custom errors -# source: https://eth.wiki/json-rpc/json-rpc-error-codes-improvement-proposal - -#1 Unauthorized Should be used when some action is not authorized, e.g. sending from a locked account. -#2 Action not allowed Should be used when some action is not allowed, e.g. preventing an action, while another depending action is processing on, like sending again when a confirmation popup is shown to the user (?). -#3 Execution error Will contain a subset of custom errors in the data field. See below. - -#100 X doesn’t exist Should be used when something which should be there is not found. (Doesn’t apply to eth_getTransactionBy_ and eth_getBlock_. They return a success with value null) -#101 Requires ether Should be used for actions which require somethin else, e.g. gas or a value. -#102 Gas too low Should be used when a to low value of gas was given. -#103 Gas limit exceeded Should be used when a limit is exceeded, e.g. for the gas limit in a block. -#104 Rejected Should be used when an action was rejected, e.g. because of its content (too long contract code, containing wrong characters ?, should differ from -32602 - Invalid params). -#105 Ether too low Should be used when a to low value of Ether was given. - -#106 Timeout Should be used when an action timedout. -#107 Conflict Should be used when an action conflicts with another (ongoing?) action. diff --git a/chainlib/eth/nonce.py b/chainlib/eth/nonce.py deleted file mode 100644 index 9aada86..0000000 --- a/chainlib/eth/nonce.py +++ /dev/null @@ -1,62 +0,0 @@ -# third-party imports -from hexathon import ( - add_0x, - strip_0x, - ) - -# local imports -from chainlib.jsonrpc import jsonrpc_template - - -def nonce(address): - o = jsonrpc_template() - o['method'] = 'eth_getTransactionCount' - o['params'].append(address) - o['params'].append('pending') - return o - - -class NonceOracle: - - def __init__(self, address): - self.address = address - self.nonce = self.get_nonce() - - - def get_nonce(self): - raise NotImplementedError('Class must be extended') - - - def next_nonce(self): - n = self.nonce - self.nonce += 1 - return n - - -class RPCNonceOracle(NonceOracle): - - def __init__(self, address, conn): - self.conn = conn - super(RPCNonceOracle, self).__init__(address) - - - def get_nonce(self): - o = nonce(self.address) - r = self.conn.do(o) - n = strip_0x(r) - return int(n, 16) - - -class OverrideNonceOracle(NonceOracle): - - - def __init__(self, address, nonce): - self.nonce = nonce - super(OverrideNonceOracle, self).__init__(address) - - - def get_nonce(self): - return self.nonce - - -DefaultNonceOracle = RPCNonceOracle diff --git a/chainlib/eth/pytest/__init__.py b/chainlib/eth/pytest/__init__.py deleted file mode 100644 index 8388db6..0000000 --- a/chainlib/eth/pytest/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .fixtures_ethtester import * -from .fixtures_chain import * -from .fixtures_signer import * diff --git a/chainlib/eth/pytest/fixtures_chain.py b/chainlib/eth/pytest/fixtures_chain.py deleted file mode 100644 index e223192..0000000 --- a/chainlib/eth/pytest/fixtures_chain.py +++ /dev/null @@ -1,17 +0,0 @@ -# external imports -import pytest - -# local imports -from chainlib.chain import ChainSpec - - -@pytest.fixture(scope='session') -def default_chain_spec(): - return ChainSpec('evm', 'foo', 42) - - -@pytest.fixture(scope='session') -def default_chain_config(): - return { - 'foo': 42, - } diff --git a/chainlib/eth/pytest/fixtures_ethtester.py b/chainlib/eth/pytest/fixtures_ethtester.py deleted file mode 100644 index 61b0b44..0000000 --- a/chainlib/eth/pytest/fixtures_ethtester.py +++ /dev/null @@ -1,105 +0,0 @@ -# standard imports -import os -import logging - -# external imports -import eth_tester -import pytest -from crypto_dev_signer.eth.signer import ReferenceSigner as EIP155Signer -from crypto_dev_signer.keystore.dict import DictKeystore - -# local imports -from chainlib.eth.unittest.base import * -from chainlib.connection import ( - RPCConnection, - ConnType, - ) -from chainlib.eth.unittest.ethtester import create_tester_signer -from chainlib.eth.address import to_checksum_address - -logg = logging.getLogger() #__name__) - - -@pytest.fixture(scope='function') -def eth_keystore(): - return DictKeystore() - - -@pytest.fixture(scope='function') -def init_eth_tester( - eth_keystore, - ): - return create_tester_signer(eth_keystore) - - -@pytest.fixture(scope='function') -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') -def eth_rpc( - default_chain_spec, - init_eth_rpc, - ): - return RPCConnection.connect(default_chain_spec, 'default') - - -@pytest.fixture(scope='function') -def eth_accounts( - init_eth_tester, - ): - addresses = list(init_eth_tester.get_accounts()) - for address in addresses: - balance = init_eth_tester.get_balance(address) - logg.debug('prefilled account {} balance {}'.format(address, balance)) - return addresses - - -@pytest.fixture(scope='function') -def eth_empty_accounts( - eth_keystore, - init_eth_tester, - ): - a = [] - for i in range(10): - #address = init_eth_tester.new_account() - address = eth_keystore.new() - checksum_address = add_0x(to_checksum_address(address)) - a.append(checksum_address) - logg.info('added address {}'.format(checksum_address)) - return a - - -@pytest.fixture(scope='function') -def eth_signer( - eth_keystore, - ): - return EIP155Signer(eth_keystore) - - -@pytest.fixture(scope='function') -def init_eth_rpc( - default_chain_spec, - init_eth_tester, - eth_signer, - ): - - rpc_conn = TestRPCConnection(None, init_eth_tester, eth_signer) - def rpc_with_tester(url=None, chain_spec=default_chain_spec): - return rpc_conn - - RPCConnection.register_constructor(ConnType.CUSTOM, rpc_with_tester, tag='default') - RPCConnection.register_constructor(ConnType.CUSTOM, rpc_with_tester, tag='signer') - RPCConnection.register_location('custom', default_chain_spec, tag='default', exist_ok=True) - RPCConnection.register_location('custom', default_chain_spec, tag='signer', exist_ok=True) - return None diff --git a/chainlib/eth/pytest/fixtures_signer.py b/chainlib/eth/pytest/fixtures_signer.py deleted file mode 100644 index 484a52b..0000000 --- a/chainlib/eth/pytest/fixtures_signer.py +++ /dev/null @@ -1,18 +0,0 @@ -# standard imports -#import os - -# external imports -import pytest -#from crypto_dev_signer.eth.signer import ReferenceSigner as EIP155Signer - - -@pytest.fixture(scope='function') -def agent_roles( - eth_accounts, - ): - return { - 'ALICE': eth_accounts[20], - 'BOB': eth_accounts[21], - 'CAROL': eth_accounts[23], - 'DAVE': eth_accounts[24], - } diff --git a/chainlib/eth/runnable/__init__.py b/chainlib/eth/runnable/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/chainlib/eth/runnable/balance.py b/chainlib/eth/runnable/balance.py deleted file mode 100644 index 4361145..0000000 --- a/chainlib/eth/runnable/balance.py +++ /dev/null @@ -1,91 +0,0 @@ -#!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 -from hexathon import ( - add_0x, - strip_0x, - even, - ) -import sha3 -from eth_abi import encode_single - -# local imports -from chainlib.eth.address import to_checksum -from chainlib.jsonrpc import ( - jsonrpc_template, - jsonrpc_result, - ) -from chainlib.eth.connection import EthHTTPConnection -from chainlib.eth.gas import ( - OverrideGasOracle, - balance, - ) -from chainlib.chain import ChainSpec - -logging.basicConfig(level=logging.WARNING) -logg = logging.getLogger() - -default_abi_dir = os.environ.get('ETH_ABI_DIR', '/usr/share/local/cic/solidity/abi') -default_eth_provider = os.environ.get('ETH_PROVIDER', 'http://localhost:8545') - -argparser = argparse.ArgumentParser() -argparser.add_argument('-p', '--provider', dest='p', default=default_eth_provider, type=str, help='Web3 provider url (http only)') -argparser.add_argument('-i', '--chain-spec', dest='i', type=str, default='evm:ethereum:1', help='Chain specification string') -argparser.add_argument('-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('address', type=str, help='Account address') -args = argparser.parse_args() - - -if args.vv: - logg.setLevel(logging.DEBUG) -elif args.v: - logg.setLevel(logging.INFO) - -conn = EthHTTPConnection(args.p) -gas_oracle = OverrideGasOracle(conn) - -address = to_checksum(args.address) -if not args.u and address != add_0x(args.address): - raise ValueError('invalid checksum address') - -chain_spec = ChainSpec.from_chain_str(args.i) - -def main(): - r = None - decimals = 18 - - o = balance(address) - r = conn.do(o) - - hx = strip_0x(r) - balance_value = int(hx, 16) - logg.debug('balance {} = {} decimals {}'.format(even(hx), balance_value, decimals)) - - balance_str = str(balance_value) - balance_len = len(balance_str) - if balance_len < decimals + 1: - print('0.{}'.format(balance_str.zfill(decimals))) - else: - offset = balance_len-decimals - print('{}.{}'.format(balance_str[:offset],balance_str[offset:])) - - -if __name__ == '__main__': - main() diff --git a/chainlib/eth/runnable/checksum.py b/chainlib/eth/runnable/checksum.py deleted file mode 100644 index ad0d2c6..0000000 --- a/chainlib/eth/runnable/checksum.py +++ /dev/null @@ -1,15 +0,0 @@ -# standard imports -import sys - -# external imports -from hexathon import strip_0x - -# local imports -from chainlib.eth.address import to_checksum_address - -def main(): - print(to_checksum_address(strip_0x(sys.argv[1]))) - - -if __name__ == '__main__': - main() diff --git a/chainlib/eth/runnable/count.py b/chainlib/eth/runnable/count.py deleted file mode 100644 index 969d207..0000000 --- a/chainlib/eth/runnable/count.py +++ /dev/null @@ -1,60 +0,0 @@ -# SPDX-License-Identifier: GPL-3.0-or-later - -# standard imports -import sys -import os -import json -import argparse -import logging - -# local imports -from chainlib.eth.address import to_checksum -from chainlib.eth.connection import EthHTTPConnection -from chainlib.eth.tx import count -from chainlib.chain import ChainSpec -from crypto_dev_signer.keystore.dict import DictKeystore -from crypto_dev_signer.eth.signer import ReferenceSigner as EIP155Signer - -logging.basicConfig(level=logging.WARNING) -logg = logging.getLogger() - -default_eth_provider = os.environ.get('ETH_PROVIDER', 'http://localhost:8545') - -argparser = argparse.ArgumentParser() -argparser.add_argument('-p', '--provider', dest='p', default='http://localhost:8545', 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('-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='Ethereum address of recipient') -args = argparser.parse_args() - -if args.vv: - logg.setLevel(logging.DEBUG) -elif args.v: - logg.setLevel(logging.INFO) - - -signer_address = None -keystore = DictKeystore() -if args.y != None: - logg.debug('loading keystore file {}'.format(args.y)) - signer_address = keystore.import_keystore_file(args.y, passphrase) - logg.debug('now have key for signer address {}'.format(signer_address)) -signer = EIP155Signer(keystore) - -rpc = EthHTTPConnection(args.p) - -def main(): - recipient = to_checksum(args.address) - if not args.u and recipient != add_0x(args.address): - raise ValueError('invalid checksum address') - - o = count(args.address) - print(rpc.do(o)) - - -if __name__ == '__main__': - main() diff --git a/chainlib/eth/runnable/decode.py b/chainlib/eth/runnable/decode.py deleted file mode 100644 index 211451e..0000000 --- a/chainlib/eth/runnable/decode.py +++ /dev/null @@ -1,50 +0,0 @@ -#!python3 - -"""Decode raw transaction - -.. moduleauthor:: Louis Holbrook -.. pgp:: 0826EDA1702D1E87C6E2875121D2E7BB88C2A746 - -""" - -# SPDX-License-Identifier: GPL-3.0-or-later - -# standard imports -import sys -import os -import json -import argparse -import logging - -# third-party imports -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() - -default_abi_dir = os.environ.get('ETH_ABI_DIR', '/usr/share/local/cic/solidity/abi') -default_eth_provider = os.environ.get('ETH_PROVIDER', 'http://localhost:8545') - -argparser = argparse.ArgumentParser() -argparser.add_argument('-v', action='store_true', help='Be verbose') -argparser.add_argument('-i', '--chain-id', dest='i', default='evm:ethereum:1', type=str, help='Numeric network id') -argparser.add_argument('tx', type=str, help='hex-encoded signed raw transaction') -args = argparser.parse_args() - -if args.v: - logg.setLevel(logging.DEBUG) - -chain_spec = ChainSpec.from_chain_str(args.i) - - -def main(): - tx_raw = args.tx - decode_for_puny_humans(tx_raw, chain_spec, sys.stdout) - -if __name__ == '__main__': - main() diff --git a/chainlib/eth/runnable/gas.py b/chainlib/eth/runnable/gas.py deleted file mode 100644 index b15fadc..0000000 --- a/chainlib/eth/runnable/gas.py +++ /dev/null @@ -1,163 +0,0 @@ -#!python3 - -"""Gas transfer script - -.. moduleauthor:: Louis Holbrook -.. pgp:: 0826EDA1702D1E87C6E2875121D2E7BB88C2A746 - -""" - -# SPDX-License-Identifier: GPL-3.0-or-later - -# standard imports -import io -import sys -import os -import json -import argparse -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.connection import EthHTTPConnection -from chainlib.jsonrpc import jsonrpc_template -from chainlib.eth.nonce import ( - RPCNonceOracle, - OverrideNonceOracle, - ) -from chainlib.eth.gas import ( - RPCGasOracle, - OverrideGasOracle, - 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 - -logging.basicConfig(level=logging.WARNING) -logg = logging.getLogger() - - -default_eth_provider = os.environ.get('ETH_PROVIDER', 'http://localhost:8545') - -argparser = argparse.ArgumentParser() -argparser.add_argument('-p', '--provider', dest='p', default='http://localhost:8545', 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('-v', action='store_true', help='Be verbose') -argparser.add_argument('-vv', action='store_true', help='Be more verbose') -argparser.add_argument('-s', '--send', dest='s', action='store_true', help='Send to network') -argparser.add_argument('recipient', type=str, help='ethereum address of recipient') -argparser.add_argument('amount', type=int, help='gas value in wei') -args = argparser.parse_args() - - -if args.vv: - logg.setLevel(logging.DEBUG) -elif args.v: - logg.setLevel(logging.INFO) - -block_all = args.ww -block_last = args.w or block_all - -passphrase_env = 'ETH_PASSPHRASE' -if args.env_prefix != None: - passphrase_env = args.env_prefix + '_' + passphrase_env -passphrase = os.environ.get(passphrase_env) -if passphrase == None: - logg.warning('no passphrase given') - passphrase='' - -signer_address = None -keystore = DictKeystore() -if args.y != None: - logg.debug('loading keystore file {}'.format(args.y)) - signer_address = keystore.import_keystore_file(args.y, password=passphrase) - logg.debug('now have key for signer address {}'.format(signer_address)) -signer = EIP155Signer(keystore) - -conn = EthHTTPConnection(args.p) - -nonce_oracle = None -if args.nonce != None: - nonce_oracle = OverrideNonceOracle(signer_address, args.nonce) -else: - nonce_oracle = RPCNonceOracle(signer_address, conn) - -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) -else: - gas_oracle = RPCGasOracle(conn) - - -chain_spec = ChainSpec.from_chain_str(args.i) - -value = args.amount - -send = args.s - -g = Gas(chain_spec, signer=signer, gas_oracle=gas_oracle, nonce_oracle=nonce_oracle) - - -def balance(address): - o = gas_balance(address) - r = conn.do(o) - hx = strip_0x(r) - return int(hx, 16) - - -def main(): - recipient = to_checksum(args.recipient) - if not args.u and recipient != add_0x(args.recipient): - raise ValueError('invalid checksum address') - - logg.info('gas transfer from {} to {} value {}'.format(signer_address, recipient, value)) - if logg.isEnabledFor(logging.DEBUG): - try: - logg.debug('sender {} balance before: {}'.format(signer_address, balance(signer_address))) - logg.debug('recipient {} balance before: {}'.format(recipient, balance(recipient))) - except urllib.error.URLError: - pass - - (tx_hash_hex, o) = g.create(signer_address, recipient, value) - - if send: - conn.do(o) - if block_last: - r = conn.wait(tx_hash_hex) - if logg.isEnabledFor(logging.DEBUG): - logg.debug('sender {} balance after: {}'.format(signer_address, balance(signer_address))) - logg.debug('recipient {} balance after: {}'.format(recipient, balance(recipient))) - if r['status'] == 0: - logg.critical('VM revert. Wish I could tell you more') - sys.exit(1) - print(tx_hash_hex) - else: - if logg.isEnabledFor(logging.INFO): - io_str = io.StringIO() - decode_for_puny_humans(o['params'][0], chain_spec, io_str) - print(io_str.getvalue()) - else: - print(o['params'][0]) - - - -if __name__ == '__main__': - main() diff --git a/chainlib/eth/runnable/get.py b/chainlib/eth/runnable/get.py deleted file mode 100644 index 8579a48..0000000 --- a/chainlib/eth/runnable/get.py +++ /dev/null @@ -1,118 +0,0 @@ -#!python3 - -"""Token balance query script - -.. moduleauthor:: Louis Holbrook -.. pgp:: 0826EDA1702D1E87C6E2875121D2E7BB88C2A746 - -""" - -# SPDX-License-Identifier: GPL-3.0-or-later - -# standard imports -import sys -import os -import json -import argparse -import logging -import enum - -# external imports -from hexathon import ( - add_0x, - strip_0x, - ) -import sha3 - -# local imports -from chainlib.eth.address import to_checksum -from chainlib.jsonrpc import ( - jsonrpc_template, - jsonrpc_result, - ) -from chainlib.eth.connection import EthHTTPConnection -from chainlib.eth.tx import Tx -from chainlib.eth.address import to_checksum_address -from chainlib.eth.block import Block -from chainlib.chain import ChainSpec -from chainlib.status import Status - -logging.basicConfig(level=logging.WARNING) -logg = logging.getLogger() - -default_abi_dir = os.environ.get('ETH_ABI_DIR', '/usr/share/local/cic/solidity/abi') -default_eth_provider = os.environ.get('ETH_PROVIDER', 'http://localhost:8545') - -argparser = argparse.ArgumentParser() -argparser.add_argument('-p', '--provider', dest='p', default=default_eth_provider, type=str, help='Web3 provider url (http only)') -argparser.add_argument('-i', '--chain-spec', dest='i', type=str, default='evm:ethereum:1', help='Chain specification string') -argparser.add_argument('-t', '--token-address', dest='t', type=str, help='Token address. If not set, will return gas balance') -argparser.add_argument('-u', '--unsafe', dest='u', action='store_true', help='Auto-convert address to checksum adddress') -argparser.add_argument('--abi-dir', dest='abi_dir', type=str, default=default_abi_dir, help='Directory containing bytecode and abi (default {})'.format(default_abi_dir)) -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', type=str, help='Item to get information for (address og transaction)') -args = argparser.parse_args() - -if args.vv: - logg.setLevel(logging.DEBUG) -elif args.v: - logg.setLevel(logging.INFO) - -conn = EthHTTPConnection(args.p) - -#tx_hash = add_0x(args.tx_hash) -item = add_0x(args.item) - - -def get_transaction(conn, tx_hash): - o = jsonrpc_template() - o['method'] = 'eth_getTransactionByHash' - o['params'].append(tx_hash) - tx_src = conn.do(o) - if tx_src == None: - logg.error('Transaction {} not found'.format(tx_hash)) - sys.exit(1) - - tx = None - status = -1 - rcpt = None - - o = jsonrpc_template() - o['method'] = 'eth_getTransactionReceipt' - o['params'].append(tx_hash) - rcpt = conn.do(o) - #status = int(strip_0x(rcpt['status']), 16) - - if tx == None: - tx = Tx(tx_src) - if rcpt != None: - tx.apply_receipt(rcpt) - return tx - - -def get_address(conn, address): - o = jsonrpc_template() - o['method'] = 'eth_getCode' - o['params'].append(address) - o['params'].append('latest') - code = conn.do(o) - - content = strip_0x(code, allow_empty=True) - if len(content) == 0: - return None - - return content - - -def main(): - r = None - if len(item) > 42: - r = get_transaction(conn, item) - elif args.u or to_checksum_address(item): - r = get_address(conn, item) - print(r) - - -if __name__ == '__main__': - main() diff --git a/chainlib/eth/runnable/info.py b/chainlib/eth/runnable/info.py deleted file mode 100644 index 39d6d4d..0000000 --- a/chainlib/eth/runnable/info.py +++ /dev/null @@ -1,154 +0,0 @@ -#!python3 - -"""Token balance query script - -.. moduleauthor:: Louis Holbrook -.. pgp:: 0826EDA1702D1E87C6E2875121D2E7BB88C2A746 - -""" - -# SPDX-License-Identifier: GPL-3.0-or-later - -# standard imports -import datetime -import sys -import os -import json -import argparse -import logging - -# third-party imports -from hexathon import ( - add_0x, - strip_0x, - even, - ) -import sha3 -from eth_abi import encode_single - -# local imports -from chainlib.eth.address import ( - to_checksum_address, - is_checksum_address, - ) -from chainlib.jsonrpc import ( - jsonrpc_template, - jsonrpc_result, - ) -from chainlib.eth.block import ( - block_latest, - block_by_number, - Block, - ) -from chainlib.eth.tx import count -from chainlib.eth.connection import EthHTTPConnection -from chainlib.eth.gas import ( - OverrideGasOracle, - balance, - price, - ) -from chainlib.chain import ChainSpec - -BLOCK_SAMPLES = 10 - -logging.basicConfig(level=logging.WARNING) -logg = logging.getLogger() - -default_abi_dir = os.environ.get('ETH_ABI_DIR', '/usr/share/local/cic/solidity/abi') -default_eth_provider = os.environ.get('ETH_PROVIDER', 'http://localhost:8545') - -argparser = argparse.ArgumentParser() -argparser.add_argument('-p', '--provider', dest='p', default=default_eth_provider, type=str, help='Web3 provider url (http only)') -argparser.add_argument('-i', '--chain-spec', dest='i', type=str, default='evm:ethereum:1', help='Chain specification string') -argparser.add_argument('-H', '--human', dest='human', action='store_true', help='Use human-friendly formatting') -argparser.add_argument('-u', '--unsafe', dest='u', action='store_true', help='Auto-convert address to checksum adddress') -argparser.add_argument('-l', '--long', dest='l', action='store_true', help='Calculate averages through sampling of blocks and txs') -argparser.add_argument('-v', action='store_true', help='Be verbose') -argparser.add_argument('-vv', action='store_true', help='Be more verbose') -argparser.add_argument('-y', '--key-file', dest='y', type=str, help='Include summary for keyfile') -argparser.add_argument('address', nargs='?', type=str, help='Include summary for address (conflicts with -y)') -args = argparser.parse_args() - - -if args.vv: - logg.setLevel(logging.DEBUG) -elif args.v: - logg.setLevel(logging.INFO) - -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'])) - -conn = EthHTTPConnection(args.p) -gas_oracle = OverrideGasOracle(conn) - -token_symbol = 'eth' - -chain_spec = ChainSpec.from_chain_str(args.i) - -human = args.human - -longmode = args.l - -def main(): - - o = block_latest() - r = conn.do(o) - n = int(r, 16) - first_block_number = n - if human: - n = format(n, ',') - sys.stdout.write('Block: {}\n'.format(n)) - - o = block_by_number(first_block_number, False) - r = conn.do(o) - last_block = Block(r) - last_timestamp = last_block.timestamp - - if longmode: - aggr_time = 0.0 - aggr_gas = 0 - for i in range(BLOCK_SAMPLES): - o = block_by_number(first_block_number-i, False) - r = conn.do(o) - block = Block(r) - aggr_time += last_block.timestamp - block.timestamp - - gas_limit = int(r['gasLimit'], 16) - aggr_gas += gas_limit - - last_block = block - last_timestamp = block.timestamp - - n = int(aggr_gas / BLOCK_SAMPLES) - if human: - n = format(n, ',') - - sys.stdout.write('Gaslimit: {}\n'.format(n)) - sys.stdout.write('Blocktime: {}\n'.format(aggr_time / BLOCK_SAMPLES)) - - o = price() - r = conn.do(o) - n = int(r, 16) - if human: - n = format(n, ',') - sys.stdout.write('Gasprice: {}\n'.format(n)) - - if holder_address != None: - o = count(holder_address) - r = conn.do(o) - n = int(r, 16) - sys.stdout.write('Address: {}\n'.format(holder_address)) - sys.stdout.write('Nonce: {}\n'.format(n)) - - -if __name__ == '__main__': - main() diff --git a/chainlib/eth/runnable/raw.py b/chainlib/eth/runnable/raw.py deleted file mode 100644 index 65af057..0000000 --- a/chainlib/eth/runnable/raw.py +++ /dev/null @@ -1,175 +0,0 @@ -#!python3 - -"""Gas transfer script - -.. moduleauthor:: Louis Holbrook -.. pgp:: 0826EDA1702D1E87C6E2875121D2E7BB88C2A746 - -""" - -# SPDX-License-Identifier: GPL-3.0-or-later - -# standard imports -import io -import sys -import os -import json -import argparse -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.connection import EthHTTPConnection -from chainlib.jsonrpc import jsonrpc_template -from chainlib.eth.nonce import ( - RPCNonceOracle, - OverrideNonceOracle, - ) -from chainlib.eth.gas import ( - RPCGasOracle, - OverrideGasOracle, - ) -from chainlib.eth.tx import ( - TxFactory, - raw, - ) -from chainlib.chain import ChainSpec -from chainlib.eth.runnable.util import decode_for_puny_humans - -logging.basicConfig(level=logging.WARNING) -logg = logging.getLogger() - - -default_eth_provider = os.environ.get('ETH_PROVIDER', 'http://localhost:8545') - -argparser = argparse.ArgumentParser() -argparser.add_argument('-p', '--provider', dest='p', default='http://localhost:8545', 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('-v', action='store_true', help='Be verbose') -argparser.add_argument('-vv', action='store_true', help='Be more verbose') -argparser.add_argument('-s', '--send', dest='s', action='store_true', help='Send to network') -argparser.add_argument('-l', '--local', dest='l', action='store_true', help='Local contract call') -argparser.add_argument('data', nargs='?', type=str, help='Transaction data') -args = argparser.parse_args() - - -if args.vv: - logg.setLevel(logging.DEBUG) -elif args.v: - logg.setLevel(logging.INFO) - -block_all = args.ww -block_last = args.w or block_all - -passphrase_env = 'ETH_PASSPHRASE' -if args.env_prefix != None: - passphrase_env = args.env_prefix + '_' + passphrase_env -passphrase = os.environ.get(passphrase_env) -if passphrase == None: - logg.warning('no passphrase given') - passphrase='' - -signer_address = None -keystore = DictKeystore() -if args.y != None: - logg.debug('loading keystore file {}'.format(args.y)) - signer_address = keystore.import_keystore_file(args.y, password=passphrase) - logg.debug('now have key for signer address {}'.format(signer_address)) -signer = EIP155Signer(keystore) - -conn = EthHTTPConnection(args.p) - -send = args.s - -local = args.l -if local: - 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) - else: - gas_oracle = RPCGasOracle(conn) - - -chain_spec = ChainSpec.from_chain_str(args.i) - -value = args.value - - -g = TxFactory(chain_spec, signer=signer, gas_oracle=gas_oracle, nonce_oracle=nonce_oracle) - -def main(): - recipient = None - if args.a != None: - recipient = add_0x(to_checksum(args.a)) - if not args.u and recipient != add_0x(recipient): - raise ValueError('invalid checksum address') - - if local: - o = jsonrpc_template() - o['method'] = 'eth_call' - o['params'].append({ - 'to': recipient, - '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') - r = conn.do(o) - print(strip_0x(r)) - return - - elif signer_address != None: - 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) - - if send: - r = conn.do(o) - print(r) - else: - print(o) - print(tx_hash_hex) - - else: - o = raw(args.data) - if send: - r = conn.do(o) - print(r) - else: - print(o) - - -if __name__ == '__main__': - main() diff --git a/chainlib/eth/runnable/subscribe.py b/chainlib/eth/runnable/subscribe.py deleted file mode 100644 index c31d988..0000000 --- a/chainlib/eth/runnable/subscribe.py +++ /dev/null @@ -1,21 +0,0 @@ -import json - -import websocket - -ws = websocket.create_connection('ws://localhost:8545') - -o = { - "jsonrpc": "2.0", - "method": "eth_subscribe", - "params": [ - "newHeads", - ], - "id": 0, - } - -ws.send(json.dumps(o).encode('utf-8')) - -while True: - print(ws.recv()) - -ws.close() diff --git a/chainlib/eth/runnable/util.py b/chainlib/eth/runnable/util.py deleted file mode 100644 index 24b9494..0000000 --- a/chainlib/eth/runnable/util.py +++ /dev/null @@ -1,22 +0,0 @@ -# local imports -from chainlib.eth.tx import unpack -from hexathon import ( - strip_0x, - add_0x, - ) - -def decode_for_puny_humans(tx_raw, chain_spec, writer): - tx_raw = strip_0x(tx_raw) - tx_raw_bytes = bytes.fromhex(tx_raw) - tx = unpack(tx_raw_bytes, chain_spec) - for k in tx.keys(): - x = None - if k == 'value': - x = '{:.18f} eth'.format(tx[k] / (10**18)) - elif k == 'gasPrice': - x = '{} gwei'.format(int(tx[k] / (10**9))) - if x != None: - writer.write('{}: {} ({})\n'.format(k, tx[k], x)) - else: - writer.write('{}: {}\n'.format(k, tx[k])) - writer.write('src: {}\n'.format(add_0x(tx_raw))) diff --git a/chainlib/eth/sign.py b/chainlib/eth/sign.py deleted file mode 100644 index f91ca62..0000000 --- a/chainlib/eth/sign.py +++ /dev/null @@ -1,23 +0,0 @@ -# local imports -from chainlib.jsonrpc import jsonrpc_template - - -def new_account(passphrase=''): - o = jsonrpc_template() - o['method'] = 'personal_newAccount' - o['params'] = [passphrase] - return o - - -def sign_transaction(payload): - o = jsonrpc_template() - o['method'] = 'eth_signTransaction' - o['params'] = [payload] - return o - - -def sign_message(address, payload): - o = jsonrpc_template() - o['method'] = 'eth_sign' - o['params'] = [address, payload] - return o diff --git a/chainlib/eth/tx.py b/chainlib/eth/tx.py deleted file mode 100644 index f347028..0000000 --- a/chainlib/eth/tx.py +++ /dev/null @@ -1,442 +0,0 @@ -# standard imports -import logging -import enum -import re - -# external imports -import coincurve -import sha3 -from hexathon import ( - strip_0x, - add_0x, - ) -from rlp import decode as rlp_decode -from rlp import encode as rlp_encode -from crypto_dev_signer.eth.transaction import EIP155Transaction -from crypto_dev_signer.encoding import public_key_to_address -from potaahto.symbols import snake_and_camel - - -# local imports -from chainlib.hash import keccak256_hex_to_hex -from chainlib.status import Status -from .address import to_checksum -from .constant import ( - MINIMUM_FEE_UNITS, - MINIMUM_FEE_PRICE, - ZERO_ADDRESS, - ) -from .contract import ABIContractEncoder -from chainlib.jsonrpc import jsonrpc_template - -logg = logging.getLogger().getChild(__name__) - - - -class TxFormat(enum.IntEnum): - DICT = 0x00 - RAW = 0x01 - RAW_SIGNED = 0x02 - RAW_ARGS = 0x03 - RLP = 0x10 - RLP_SIGNED = 0x11 - JSONRPC = 0x10 - - -field_debugs = [ - 'nonce', - 'gasPrice', - 'gas', - 'to', - 'value', - 'data', - 'v', - 'r', - 's', - ] - -def count(address, confirmed=False): - o = jsonrpc_template() - o['method'] = 'eth_getTransactionCount' - o['params'].append(address) - if confirmed: - o['params'].append('latest') - else: - o['params'].append('pending') - return o - -count_pending = count - -def count_confirmed(address): - return count(address, True) - - -def unpack(tx_raw_bytes, chain_spec): - chain_id = chain_spec.chain_id() - tx = __unpack_raw(tx_raw_bytes, chain_id) - tx['nonce'] = int.from_bytes(tx['nonce'], 'big') - tx['gasPrice'] = int.from_bytes(tx['gasPrice'], 'big') - tx['gas'] = int.from_bytes(tx['gas'], 'big') - tx['value'] = int.from_bytes(tx['value'], 'big') - return tx - - -def unpack_hex(tx_raw_bytes, chain_spec): - chain_id = chain_spec.chain_id() - tx = __unpack_raw(tx_raw_bytes, chain_id) - tx['nonce'] = add_0x(hex(tx['nonce'])) - tx['gasPrice'] = add_0x(hex(tx['gasPrice'])) - tx['gas'] = add_0x(hex(tx['gas'])) - tx['value'] = add_0x(hex(tx['value'])) - tx['chainId'] = add_0x(hex(tx['chainId'])) - return tx - - -def __unpack_raw(tx_raw_bytes, chain_id=1): - d = rlp_decode(tx_raw_bytes) - - logg.debug('decoding using chain id {}'.format(str(chain_id))) - - j = 0 - for i in d: - v = i.hex() - if j != 3 and v == '': - v = '00' - logg.debug('decoded {}: {}'.format(field_debugs[j], v)) - j += 1 - vb = chain_id - if chain_id != 0: - v = int.from_bytes(d[6], 'big') - vb = v - (chain_id * 2) - 35 - r = bytearray(32) - r[32-len(d[7]):] = d[7] - s = bytearray(32) - s[32-len(d[8]):] = d[8] - sig = b''.join([r, s, bytes([vb])]) - #so = KeyAPI.Signature(signature_bytes=sig) - - h = sha3.keccak_256() - h.update(rlp_encode(d)) - signed_hash = h.digest() - - d[6] = chain_id - d[7] = b'' - d[8] = b'' - - h = sha3.keccak_256() - h.update(rlp_encode(d)) - unsigned_hash = h.digest() - - #p = so.recover_public_key_from_msg_hash(unsigned_hash) - #a = p.to_checksum_address() - pubk = coincurve.PublicKey.from_signature_and_message(sig, unsigned_hash, hasher=None) - a = public_key_to_address(pubk) - logg.debug('decoded recovery byte {}'.format(vb)) - logg.debug('decoded address {}'.format(a)) - logg.debug('decoded signed hash {}'.format(signed_hash.hex())) - logg.debug('decoded unsigned hash {}'.format(unsigned_hash.hex())) - - to = d[3].hex() or None - if to != None: - to = to_checksum(to) - - data = d[5].hex() - try: - data = add_0x(data) - except: - data = '0x' - - return { - 'from': a, - 'to': to, - 'nonce': d[0], - 'gasPrice': d[1], - 'gas': d[2], - 'value': d[4], - 'data': data, - 'v': chain_id, - 'r': add_0x(sig[:32].hex()), - 's': add_0x(sig[32:64].hex()), - 'chainId': chain_id, - 'hash': add_0x(signed_hash.hex()), - 'hash_unsigned': add_0x(unsigned_hash.hex()), - } - - -def transaction(hsh): - o = jsonrpc_template() - o['method'] = 'eth_getTransactionByHash' - o['params'].append(add_0x(hsh)) - return o - - -def transaction_by_block(hsh, idx): - o = jsonrpc_template() - o['method'] = 'eth_getTransactionByBlockHashAndIndex' - o['params'].append(add_0x(hsh)) - o['params'].append(hex(idx)) - return o - - -def receipt(hsh): - o = jsonrpc_template() - o['method'] = 'eth_getTransactionReceipt' - o['params'].append(add_0x(hsh)) - return o - - -def raw(tx_raw_hex): - o = jsonrpc_template() - o['method'] = 'eth_sendRawTransaction' - o['params'].append(add_0x(tx_raw_hex)) - return o - - -class TxFactory: - - fee = 8000000 - - def __init__(self, chain_spec, signer=None, gas_oracle=None, nonce_oracle=None): - self.gas_oracle = gas_oracle - self.nonce_oracle = nonce_oracle - self.chain_spec = chain_spec - self.signer = signer - - - def build_raw(self, tx): - if tx['to'] == None or tx['to'] == '': - tx['to'] = '0x' - txe = EIP155Transaction(tx, tx['nonce'], tx['chainId']) - tx_raw = self.signer.sign_transaction_to_rlp(txe) - tx_raw_hex = add_0x(tx_raw.hex()) - tx_hash_hex = add_0x(keccak256_hex_to_hex(tx_raw_hex)) - return (tx_hash_hex, tx_raw_hex) - - - def build(self, tx): - (tx_hash_hex, tx_raw_hex) = self.build_raw(tx) - o = raw(tx_raw_hex) - return (tx_hash_hex, o) - - - def template(self, sender, recipient, use_nonce=False): - gas_price = MINIMUM_FEE_PRICE - gas_limit = MINIMUM_FEE_UNITS - if self.gas_oracle != None: - (gas_price, gas_limit) = self.gas_oracle.get_gas() - logg.debug('using gas price {} limit {}'.format(gas_price, gas_limit)) - nonce = 0 - o = { - 'from': sender, - 'to': recipient, - 'value': 0, - 'data': '0x', - 'gasPrice': gas_price, - 'gas': gas_limit, - 'chainId': self.chain_spec.chain_id(), - } - if self.nonce_oracle != None and use_nonce: - nonce = self.nonce_oracle.next_nonce() - logg.debug('using nonce {} for address {}'.format(nonce, sender)) - o['nonce'] = nonce - return o - - - def normalize(self, tx): - txe = EIP155Transaction(tx, tx['nonce'], tx['chainId']) - txes = txe.serialize() - return { - 'from': tx['from'], - 'to': txes['to'], - 'gasPrice': txes['gasPrice'], - 'gas': txes['gas'], - 'data': txes['data'], - } - - - def finalize(self, tx, tx_format=TxFormat.JSONRPC): - if tx_format == TxFormat.JSONRPC: - return self.build(tx) - elif tx_format == TxFormat.RLP_SIGNED: - return self.build_raw(tx) - raise NotImplementedError('tx formatting {} not implemented'.format(tx_format)) - - - def set_code(self, tx, data, update_fee=True): - tx['data'] = data - if update_fee: - tx['gas'] = TxFactory.fee - if self.gas_oracle != None: - (price, tx['gas']) = self.gas_oracle.get_gas(code=data) - else: - logg.debug('using hardcoded gas limit of 8000000 until we have reliable vm executor') - return tx - - - def transact_noarg(self, method, contract_address, sender_address, tx_format=TxFormat.JSONRPC): - enc = ABIContractEncoder() - enc.method(method) - data = enc.get() - tx = self.template(sender_address, contract_address, use_nonce=True) - tx = self.set_code(tx, data) - tx = self.finalize(tx, tx_format) - return tx - - - def call_noarg(self, method, contract_address, sender_address=ZERO_ADDRESS): - o = jsonrpc_template() - o['method'] = 'eth_call' - enc = ABIContractEncoder() - enc.method(method) - data = add_0x(enc.get()) - tx = self.template(sender_address, contract_address) - tx = self.set_code(tx, data) - o['params'].append(self.normalize(tx)) - o['params'].append('latest') - return o - - -class Tx: - - # TODO: force tx type schema parser (whether expect hex or int etc) - def __init__(self, src, block=None, rcpt=None): - logg.debug('src {}'.format(src)) - self.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) - logg.debug('tx {} cmp {}'.format(tx, tx_hash)) - 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) - try: - self.value = int(strip_0x(src['value']), 16) - except TypeError: - self.value = int(src['value']) - try: - self.nonce = int(strip_0x(src['nonce']), 16) - except TypeError: - self.nonce = int(src['nonce']) - address_from = strip_0x(src['from']) - try: - self.gas_price = int(strip_0x(src['gasPrice']), 16) - except TypeError: - self.gas_price = int(src['gasPrice']) - try: - self.gas_limit = int(strip_0x(src['gas']), 16) - except TypeError: - self.gas_limit = int(src['gas']) - self.outputs = [to_checksum(address_from)] - self.contract = None - - try: - inpt = src['input'] - except KeyError: - inpt = src['data'] - - if inpt != '0x': - inpt = strip_0x(inpt) - else: - inpt = '' - self.payload = inpt - - to = src['to'] - if to == None: - to = ZERO_ADDRESS - self.inputs = [to_checksum(strip_0x(to))] - - self.block = block - try: - self.wire = src['raw'] - except KeyError: - logg.warning('no inline raw tx src, and no raw rendering implemented, field will be "None"') - - self.src = src - - self.status = Status.PENDING - self.logs = None - - if rcpt != None: - self.apply_receipt(rcpt) - - - @classmethod - def src_normalize(self, src): - return snake_and_camel(src) - - - def apply_receipt(self, rcpt): - rcpt = self.src_normalize(rcpt) - logg.debug('rcpt {}'.format(rcpt)) - 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 - # TODO: replace with rpc receipt/transaction translator when available - contract_address = rcpt.get('contractAddress') - if contract_address == None: - contract_address = rcpt.get('contract_address') - if contract_address != None: - self.contract = contract_address - self.logs = rcpt['logs'] - try: - self.gas_used = int(rcpt['gasUsed'], 16) - except TypeError: - self.gas_used = int(rcpt['gasUsed']) - - - def __repr__(self): - return 'block {} tx {} {}'.format(self.block.number, self.index, self.hash) - - - def __str__(self): - s = """hash {} -from {} -to {} -value {} -nonce {} -gasPrice {} -gasLimit {} -input {} -""".format( - self.hash, - self.outputs[0], - self.inputs[0], - self.value, - self.nonce, - self.gas_price, - self.gas_limit, - self.payload, - ) - - if self.status != Status.PENDING: - s += """gasUsed {} -""".format( - self.gas_used, - ) - - s += 'status ' + self.status.name + '\n' - - if self.contract != None: - s += """contract {} -""".format( - self.contract, - ) - return s - diff --git a/chainlib/eth/unittest/base.py b/chainlib/eth/unittest/base.py deleted file mode 100644 index 48eb811..0000000 --- a/chainlib/eth/unittest/base.py +++ /dev/null @@ -1,218 +0,0 @@ -# standard imports -import os -import logging - -# external imports -import eth_tester -import coincurve -from chainlib.connection import ( - RPCConnection, - error_parser, - ) -from chainlib.eth.address import ( - to_checksum_address, - ) -from chainlib.jsonrpc import ( - jsonrpc_response, - jsonrpc_error, - jsonrpc_result, - ) -from hexathon import ( - unpad, - add_0x, - strip_0x, - ) - -from crypto_dev_signer.eth.signer import ReferenceSigner as EIP155Signer -from crypto_dev_signer.encoding import private_key_to_address - - -logg = logging.getLogger().getChild(__name__) - -test_pk = bytes.fromhex('5087503f0a9cc35b38665955eb830c63f778453dd11b8fa5bd04bc41fd2cc6d6') - - -class EthTesterSigner(eth_tester.EthereumTester): - - def __init__(self, backend, keystore): - super(EthTesterSigner, self).__init__(backend) - logg.debug('accounts {}'.format(self.get_accounts())) - - self.keystore = keystore - self.backend = backend - self.backend.add_account(test_pk) - for pk in self.backend.account_keys: - pubk = pk.public_key - address = pubk.to_checksum_address() - logg.debug('test keystore have pk {} pubk {} addr {}'.format(pk, pk.public_key, address)) - self.keystore.import_raw_key(pk._raw_key) - - - def new_account(self): - pk = os.urandom(32) - address = self.keystore.import_raw_key(pk) - checksum_address = add_0x(to_checksum_address(address)) - self.backend.add_account(pk) - return checksum_address - - -class TestRPCConnection(RPCConnection): - - def __init__(self, location, backend, signer): - super(TestRPCConnection, self).__init__(location) - self.backend = backend - self.signer = signer - - - def do(self, o, error_parser=error_parser): - logg.debug('testrpc do {}'.format(o)) - m = getattr(self, o['method']) - if m == None: - raise ValueError('unhandled method {}'.format(o['method'])) - r = None - try: - result = m(o['params']) - logg.debug('result {}'.format(result)) - r = jsonrpc_response(o['id'], result) - except Exception as e: - logg.exception(e) - r = jsonrpc_error(o['id'], message=str(e)) - return jsonrpc_result(r, error_parser) - - - def eth_blockNumber(self, p): - block = self.backend.get_block_by_number('latest') - return block['number'] - - - def eth_getBlockByNumber(self, p): - b = bytes.fromhex(strip_0x(p[0])) - n = int.from_bytes(b, 'big') - block = self.backend.get_block_by_number(n) - return block - - - def eth_getBlockByHash(self, p): - block = self.backend.get_block_by_hash(p[0]) - return block - - - def eth_getTransactionByBlock(self, p): - block = self.eth_getBlockByHash(p) - try: - tx_index = int(p[1], 16) - except TypeError: - tx_index = int(p[1]) - tx_hash = block['transactions'][tx_index] - tx = self.eth_getTransactionByHash([tx_hash]) - return tx - - def eth_getBalance(self, p): - balance = self.backend.get_balance(p[0]) - hx = balance.to_bytes(32, 'big').hex() - return add_0x(unpad(hx)) - - - def eth_getTransactionCount(self, p): - nonce = self.backend.get_nonce(p[0]) - hx = nonce.to_bytes(4, 'big').hex() - return add_0x(unpad(hx)) - - - def eth_getTransactionByHash(self, p): - tx = self.backend.get_transaction_by_hash(p[0]) - return tx - - - def eth_getTransactionByBlockHashAndIndex(self, p): - #logg.debug('p {}'.format(p)) - #block = self.eth_getBlockByHash(p[0]) - #tx = block.transactions[p[1]] - #return eth_getTransactionByHash(tx[0]) - return self.eth_getTransactionByBlock(p) - - - def eth_getTransactionReceipt(self, p): - rcpt = self.backend.get_transaction_receipt(p[0]) - if rcpt.get('block_number') == None: - rcpt['block_number'] = rcpt['blockNumber'] - else: - rcpt['blockNumber'] = rcpt['block_number'] - return rcpt - - - def eth_getCode(self, p): - r = self.backend.get_code(p[0]) - return r - - - def eth_call(self, p): - tx_ethtester = to_ethtester_call(p[0]) - r = self.backend.call(tx_ethtester) - return r - - - def eth_gasPrice(self, p): - return hex(1000000000) - - - def personal_newAccount(self, passphrase): - a = self.backend.new_account() - return a - - - def eth_sign(self, p): - r = self.signer.sign_ethereum_message(strip_0x(p[0]), strip_0x(p[1])) - return r - - - def eth_sendRawTransaction(self, p): - r = self.backend.send_raw_transaction(p[0]) - return r - - - def eth_signTransaction(self, p): - raise NotImplementedError('needs transaction deserializer for EIP155Transaction') - tx_dict = p[0] - tx = EIP155Transaction(tx_dict, tx_dict['nonce'], tx_dict['chainId']) - passphrase = p[1] - r = self.signer.sign_transaction_to_rlp(tx, passphrase) - return r - - - def __verify_signer(self, tx, passphrase=''): - pk_bytes = self.backend.keystore.get(tx.sender) - pk = coincurve.PrivateKey(secret=pk_bytes) - result_address = private_key_to_address(pk) - assert strip_0x(result_address) == strip_0x(tx.sender) - - - def sign_transaction(self, tx, passphrase=''): - self.__verify_signer(tx, passphrase) - return self.signer.sign_transaction(tx, passphrase) - - - def sign_transaction_to_rlp(self, tx, passphrase=''): - self.__verify_signer(tx, passphrase) - return self.signer.sign_transaction_to_rlp(tx, passphrase) - - - def disconnect(self): - pass - - -def to_ethtester_call(tx): - if tx['gas'] == '': - tx['gas'] = '0x00' - - if tx['gasPrice'] == '': - tx['gasPrice'] = '0x00' - - tx = { - 'to': tx['to'], - 'from': tx['from'], - 'gas': int(tx['gas'], 16), - 'gas_price': int(tx['gasPrice'], 16), - 'data': tx['data'], - } - return tx diff --git a/chainlib/eth/unittest/ethtester.py b/chainlib/eth/unittest/ethtester.py deleted file mode 100644 index ed3eba2..0000000 --- a/chainlib/eth/unittest/ethtester.py +++ /dev/null @@ -1,80 +0,0 @@ -# standard imports -import os -import unittest -import logging - -# external imports -import eth_tester -from crypto_dev_signer.eth.signer import ReferenceSigner as EIP155Signer -from crypto_dev_signer.keystore.dict import DictKeystore -from hexathon import ( - strip_0x, - add_0x, - ) -from eth import constants -from eth.vm.forks.byzantium import ByzantiumVM - -# local imports -from .base import ( - EthTesterSigner, - TestRPCConnection, - ) -from chainlib.connection import ( - RPCConnection, - ConnType, - ) -from chainlib.eth.address import to_checksum_address -from chainlib.chain import ChainSpec - -logg = logging.getLogger(__name__) - -test_address = bytes.fromhex('Eb3907eCad74a0013c259D5874AE7f22DcBcC95C') - - -def create_tester_signer(keystore): - genesis_params = eth_tester.backends.pyevm.main.get_default_genesis_params({ - 'gas_limit': 8000000, - 'coinbase': test_address, # doesn't seem to work - }) - vm_configuration = ( - (constants.GENESIS_BLOCK_NUMBER, ByzantiumVM), - ) - genesis_state = eth_tester.PyEVMBackend._generate_genesis_state(num_accounts=30) - eth_backend = eth_tester.PyEVMBackend( - genesis_state=genesis_state, - genesis_parameters=genesis_params, - vm_configuration=vm_configuration, - ) - return EthTesterSigner(eth_backend, keystore) - - -class EthTesterCase(unittest.TestCase): - - def __init__(self, foo): - super(EthTesterCase, self).__init__(foo) - self.accounts = [] - - - def setUp(self): - self.chain_spec = ChainSpec('evm', 'foochain', 42) - self.keystore = DictKeystore() - eth_tester_instance = create_tester_signer(self.keystore) - self.signer = EIP155Signer(self.keystore) - self.helper = eth_tester_instance - self.backend = self.helper.backend - self.rpc = TestRPCConnection(None, eth_tester_instance, self.signer) - for a in self.keystore.list(): - self.accounts.append(add_0x(to_checksum_address(a))) - - def rpc_with_tester(chain_spec=self.chain_spec, url=None): - return self.rpc - - RPCConnection.register_constructor(ConnType.CUSTOM, rpc_with_tester, tag='default') - RPCConnection.register_constructor(ConnType.CUSTOM, rpc_with_tester, tag='signer') - RPCConnection.register_location('custom', self.chain_spec, tag='default', exist_ok=True) - RPCConnection.register_location('custom', self.chain_spec, tag='signer', exist_ok=True) - - - - def tearDown(self): - pass diff --git a/chainlib/hash.py b/chainlib/hash.py index dd0e131..fd7f027 100644 --- a/chainlib/hash.py +++ b/chainlib/hash.py @@ -1,28 +1,48 @@ -# third-party imports +# external imports import sha3 -from hexathon import ( - add_0x, - strip_0x, - ) +from hexathon import strip_0x def keccak256_hex(s): + """Hex representation of Keccak256 hash of utf-8 string content. + + :param s: utf-8 string to hash + :type s: str + :rtype: str + :returns: Hex-value of keccak256 hash + """ h = sha3.keccak_256() h.update(s.encode('utf-8')) return h.digest().hex() def keccak256_string_to_hex(s): + """Alias of keccak256_hex + """ return keccak256_hex(s) def keecak256_bytes_to_hex(b): + """Hex representation of Keccak256 hash of literal byte content. + + :param b: bytes to hash + :type b: bytes + :rtype: str + :returns: Hex-value of keccak256 hash + """ h = sha3.keccak_256() h.update(b) return h.digest().hex() def keccak256_hex_to_hex(hx): + """Hex representation of Keccak256 hash of byte value of hex content. + + :param hx: Hex-value of bytes to hash + :type hx: str + :rtype: str + :returns: Hex-value of keccak256 hash + """ h = sha3.keccak_256() b = bytes.fromhex(strip_0x(hx)) h.update(b) diff --git a/chainlib/http.py b/chainlib/http.py index c47aaba..9ed69cd 100644 --- a/chainlib/http.py +++ b/chainlib/http.py @@ -1,3 +1,4 @@ +# standard imports import urllib import base64 import logging @@ -8,14 +9,16 @@ logg = logging.getLogger(__name__) # THANKS to https://stackoverflow.com/questions/2407126/python-urllib2-basic-auth-problem class PreemptiveBasicAuthHandler(urllib.request.HTTPBasicAuthHandler): """Handler for basic auth urllib callback. - - :param req: Request payload - :type req: str - :return: Request payload - :rtype: str """ def http_request(self, req): + """Handler for basic auth urllib callback. + + :param req: Request payload + :type req: str + :return: Request payload + :rtype: str + """ url = req.get_full_url() realm = None user, pw = self.passwd.find_user_password(realm, url) diff --git a/chainlib/interface.py b/chainlib/interface.py new file mode 100644 index 0000000..bc1d533 --- /dev/null +++ b/chainlib/interface.py @@ -0,0 +1,255 @@ +# standard imports +import logging + +logg = logging.getLogger(__name__) + + +class ChainInterface: + """Common interface for all chain RPC query generators. + + This class should be overridden for every implementation of chain architecture RPC. + + It is up to the implementer which of the symbols to implement code for. Any implemented symbols should be associated using the ChainInterface.set method. + + All implemented methods must generate RPC queries ready to submit using an implementation of chainlib.connection.RPCConnection + """ + + interface_name = 'custom' + + def __unimplemented(*args, **kwargs): + raise NotImplementedError() + + + def __init__(self): + self._block_latest = self.__unimplemented + self._block_by_hash = self.__unimplemented + self._block_by_number = self.__unimplemented + self._block_from_src = self.__unimplemented + self._block_to_src = self.__unimplemented + self._tx_by_hash = self.__unimplemented + self._tx_by_block = self.__unimplemented + self._tx_receipt = self.__unimplemented + self._tx_raw = self.__unimplemented + self._tx_pack = self.__unimplemented + self._tx_unpack = self.__unimplemented + self._tx_from_src = self.__unimplemented + self._tx_to_src = self.__unimplemented + self._address_safe = self.__unimplemented + self._address_normal = self.__unimplemented + self._src_normalize = self.__unimplemented + + + def block_latest(self, *args, **kwargs): + """Retrieve the last block known to the node. + + :rtype: dict + :returns: rpc query object + """ + return self._block_latest(*args, **kwargs) + + + def block_by_hash(self, hsh, *args, **kwargs): + """Retrieve the block representation from the given block hash + + :param hsh: Block hash, as hex + :type hsh: str + :param id_generator: JSONRPC id generator + :type id_generator: JSONRPCIdGenerator + :rtype: dict + :returns: rpc query object + """ + return self._block_by_hash(hsh, *args, **kwargs) + + + def block_by_number(self, idx, *args, **kwargs): + """Retrieve the block representation from the given block height index + + :param idx: Block index number + :type idx: int + :param id_generator: JSONRPC id generator + :type id_generator: JSONRPCIdGenerator + :rtype: dict + :returns: rpc query object + """ + return self._block_by_number(idx, *args, **kwargs) + + + def block_from_src(self, src): + """Instantiate an implementation specific block object from the block representation returned from an RPC result + + :param src: Block source + :type src: dict + :param id_generator: JSONRPC id generator + :type id_generator: JSONRPCIdGenerator + :rtype: chainlib.block.Block + :returns: Block object + """ + return self._block_from_src(src) + + + def block_to_src(self, block): + """Implementation specific serialization of a block object + + :param block: Block object + :type block: chainlib.block.Block + :param id_generator: JSONRPC id generator + :type id_generator: JSONRPCIdGenerator + :rtype: dict + :returns: Serialized block object + """ + return self._block_to_src() + + + def tx_by_hash(self, hsh, *args, **kwargs): + """Retrieve the transaction representation by the given transaction hash + + :param hsh: Transaction hash, as hex + :type hsh: str + :param id_generator: JSONRPC id generator + :type id_generator: JSONRPCIdGenerator + :rtype: dict + :returns: rpc query object + """ + return self._tx_by_hash(hsh, *args, **kwargs) + + + def tx_by_block(self, hsh, idx, *args, **kwargs): + """Retrieve the transaction representation by the given block hash and transaction index + + :param hsh: Block hash, as hex + :type hsh: str + :param idx: Transaction index + :type idx: int + :param id_generator: JSONRPC id generator + :type id_generator: JSONRPCIdGenerator + :rtype: dict + :returns: rpc query object + + """ + return self._tx_by_block(hsh, idx, *args, **kwargs) + + + def tx_receipt(self, hsh, *args, **kwargs): + """Retrieve representation of confirmed transaction result for given transaction hash + + :param hsh: Transaction hash, as hex + :type hsh: str + :param id_generator: JSONRPC id generator + :type id_generator: JSONRPCIdGenerator + :rtype: dict + :returns: rpc query object + """ + return self._tx_receipt(hsh, *args, **kwargs) + + + def tx_raw(self, data, *args, **kwargs): + """Create a raw transaction query from the given wire format + + :param data: Transaction wire format, in hex + :type data: str + :param id_generator: JSONRPC id generator + :type id_generator: JSONRPCIdGenerator + :rtype: dict + :returns: rpc query object + + """ + return self._tx_raw(data, *args, **kwargs) + + + def tx_pack(self, tx, chain_spec): + """Generate wire format for transaction + + :param tx: Transaction object + :type tx: dict + :param chain_spec: Chain spec to generate wire format for + :type chain_spec: chainlib.chain.ChainSpec + :rtype: bytes + :returns: Wire format, in bytes + """ + return self._tx_pack(tx, chain_spec) + + + def tx_unpack(self, data, chain_spec): + """Generate transaction representation from wire format. + + :param data: Wire format, in bytes + :type data: bytes + :param chain_spec: Chain spec to parse wire format with + :type chain_spec: chainlib.chain.ChainSpec + :rtype: dict + :returns: Transaction representation + """ + return self._tx_unpack(data, chain_spec) + + + def tx_from_src(self, src, block=None): + """Instantiate transaction object from implementation specific transaction representation. + + :param src: Transaction representation + :type src: dict + :param block: Block object which transaction has been included in + :type block: chainlib.block.Block + :rtype: chainlib.tx.Tx + :returns: Transaction object + """ + return self._tx_from_src(src, block) + + + def tx_to_src(self, tx): + """Generate implementation specific transaction representation from transaction object. + + :param tx: Transaction object + :type tx: chainlib.tx.Tx + :rtype: dict + :returns: Transaction representation + """ + return self._tx_to_src(tx) + + + def address_safe(self, address): + """Generate implementation specific checksummed version of a crypto address. + + :param address: Potentially unsafe address + :type address: str + :rtype: str + :returns: Checksummed address + """ + return self._address_safe(address) + + + def address_normal(self, address): + """Generate normalized version of a crypto address. + + :param address: Crypto address + :type address: str + :rtype: str + :returns: Normalized address + """ + return self._address_normal(address) + + + def src_normalize(self, src): + """Generate a normalized source of an object representation. + + :param src: Object representation source + :type src: dict + :rtype: dict + :returns: Normalized representation + """ + return self._src_normalize(src) + + + def set(self, method, target): + """Associate object with method symbol. + + :param method: Method string + :type method: str + :param target: Target method + :type target: object + :raises AttributeError: Invalid method + """ + imethod = '_' + method + if not hasattr(self, imethod): + raise AttributeError('invalid method {}'.format(imethod)) + setattr(self, imethod, target) + logg.debug('set method {} on interface {}'.format(method, self.interface_name)) diff --git a/chainlib/jsonrpc.py b/chainlib/jsonrpc.py index e823abf..3948359 100644 --- a/chainlib/jsonrpc.py +++ b/chainlib/jsonrpc.py @@ -4,35 +4,140 @@ import uuid # local imports from .error import JSONRPCException +# TODO: Move all contents in this file to independent package -class DefaultErrorParser: + +class JSONRPCIdGenerator: + + def next(self): + raise NotImplementedError + + +class UUIDGenerator(JSONRPCIdGenerator): + """Create uuid ids for JSON-RPC queries. + """ + + def next(self): + """Create a new id + + :rtype: str + :returns: uuid string + """ + return str(uuid.uuid4()) + + +class IntSequenceGenerator(JSONRPCIdGenerator): + """Create sequential numeric ids for JSON-RPC queries. + + :param start: Start at the specificed numeric id + :type start: int + """ + def __init__(self, start=0): + self.id = start + + + def next(self): + """Get the next id in the sequence. + + :rtype: int + :returns: numeric id + """ + next_id = self.id + self.id += 1 + return next_id + + +default_id_generator = UUIDGenerator() + + +class ErrorParser: + """Base class for parsing JSON-RPC error repsonses + """ def translate(self, error): + """Interface method called by jsonrpc_result when encountering an error + + This class method may be overriden to provide more fine-grained context for both general and implementation specific errors. + + :param error: JSON-RPC error response object + :type error: dict + :rtype: chainlib.error.JSONRPCException + :returns: Descriptiv JSONRPCException + """ return JSONRPCException('default parser code {}'.format(error)) -def jsonrpc_template(): - return { - 'jsonrpc': '2.0', - 'id': str(uuid.uuid4()), - 'method': None, - 'params': [], - } +# deprecated symbol, provided for backward compatibility +DefaultErrorParser = ErrorParser -def jsonrpc_result(o, ep): - if o.get('error') != None: - raise ep.translate(o) - return o['result'] + +class JSONRPCRequest: + """JSON-RPC request builder class. + + :param id_generator: Generator to use to define the id of the request. + :type id_generator: chainlib.jsonrpc.JSONRPCIdGenerator + """ + def __init__(self, id_generator=default_id_generator): + if id_generator == None: + id_generator = default_id_generator + self.id_generator = id_generator + + + def template(self): + """Return a empty json-rpc 2.0 dictionary query object + + :rtype: dict + :returns: json-rpc query object + """ + return { + 'jsonrpc': '2.0', + 'id': None, + 'method': None, + 'params': [], + } + + + def finalize(self, request): + """Apply next json-rpc id to json-rpc dictionary query object + + :param request: json-rpc query + :type request: dict + :rtype: dict + :returns: json-rpc query with id added + """ + request['id'] = self.id_generator.next() + return request def jsonrpc_response(request_id, result): - return { - 'jsonrpc': '2.0', - 'id': request_id, - 'result': result, - } + """Create a json-rpc dictionary response object from the given id an result value. + + :param request_id: json-rpc query id + :type request_id: str or int + :param result: result value + :type result: any json-serializable value + :rtype: dict + :result: json-rpc response object + """ + return { + 'jsonrpc': '2.0', + 'id': request_id, + 'result': result, + } + def jsonrpc_error(request_id, code=-32000, message='Server error'): + """Create a json-rpc dictionary error object for the given id with error code and message. + + :param request_id: json-rpc query id + :type request_id: str or int + :param code: json-rpc error code + :type code: int + :param message: Error message + :type message: str + :rtype: dict + :returns: json-rpc error object + """ return { 'jsonrpc': '2.0', 'id': request_id, @@ -41,3 +146,21 @@ def jsonrpc_error(request_id, code=-32000, message='Server error'): 'message': message, }, } + + +def jsonrpc_result(o, ep): + """Retrieve the result from a json-rpc response object. + + If the result object is an error, the provided error parser will be used to generate the corresponding exception. + + :param o: json-rpc response object + :type o: dict + :param ep: Error parser + :type ep: chainlib.jsonrpc.ErrorParser + :raises JSONRPCException: exception encapsulating the error value of the response + :rtype: any json-deserializable value + :returns: The result value of the response + """ + if o.get('error') != None: + raise ep.translate(o) + return o['result'] diff --git a/chainlib/stat.py b/chainlib/stat.py index a6f63d1..d265fd4 100644 --- a/chainlib/stat.py +++ b/chainlib/stat.py @@ -1,6 +1,11 @@ +# standard imports import datetime + + class ChainStat: + """Block time aggregator. + """ def __init__(self): self.block_timestamp_last = None @@ -9,6 +14,11 @@ class ChainStat: def block_apply(self, block): + """Add data from block to aggregate. + + :param block: Block to add + :type block: chainlib.block.Block + """ if self.block_timestamp_last == None: self.block_timestamp_last = block.timestamp @@ -25,5 +35,11 @@ class ChainStat: self.block_timestamp_last = block.timestamp + def block_average(self): + """Get current aggregated average. + + :rtype: float + :returns: Aggregate average block time, in seconds + """ return self.block_avg_aggregate diff --git a/chainlib/status.py b/chainlib/status.py index 7db09e9..abda47b 100644 --- a/chainlib/status.py +++ b/chainlib/status.py @@ -2,6 +2,8 @@ import enum class Status(enum.Enum): + """Representation of transaction status in network. + """ PENDING = 0 SUCCESS = 1 ERROR = 2 diff --git a/chainlib/tx.py b/chainlib/tx.py new file mode 100644 index 0000000..0671d64 --- /dev/null +++ b/chainlib/tx.py @@ -0,0 +1,14 @@ +class Tx: + """Base class to extend for implementation specific transaction objects. + + :param src: Transaction representation source + :type src: dict + :param block: Block in which transaction has been included + :type block: chainlib.block.Block + """ + + def __init__(self, src, block=None): + self.txs = [] + self.src = src + self.block = block + self.block_src = None diff --git a/requirements.txt b/requirements.txt index ae265d8..8757b1c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,3 @@ -crypto-dev-signer~=0.4.14b3 +crypto-dev-signer>=0.4.14b7,<=0.4.14 pysha3==1.0.2 -hexathon~=0.0.1a7 -websocket-client==0.57.0 -potaahto~=0.0.1a1 +hexathon~=0.0.1a8 diff --git a/setup.cfg b/setup.cfg index 5e8f3bf..8188948 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,5 @@ [metadata] -name = chainlib -version = 0.0.3rc3 +version = 0.0.8a2 description = Generic blockchain access library and tooling author = Louis Holbrook author_email = dev@holbrook.no @@ -9,7 +8,6 @@ keywords = dlt blockchain cryptocurrency - ethereum classifiers = Programming Language :: Python :: 3 Operating System :: OS Independent @@ -18,27 +16,14 @@ classifiers = Intended Audience :: Developers License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+) Topic :: Internet -# Topic :: Blockchain :: EVM license = GPL3 licence_files = LICENSE.txt + [options] python_requires = >= 3.6 +include_package_data = True packages = chainlib - chainlib.eth - chainlib.eth.runnable - chainlib.eth.pytest - chainlib.eth.unittest - -[options.entry_points] -console_scripts = - eth-balance = chainlib.eth.runnable.balance:main - eth-checksum = chainlib.eth.runnable.checksum:main - eth-gas = chainlib.eth.runnable.gas:main - eth-raw = chainlib.eth.runnable.raw:main - eth-get = chainlib.eth.runnable.get:main - eth-decode = chainlib.eth.runnable.decode:main - eth-info = chainlib.eth.runnable.info:main - eth = chainlib.eth.runnable.info:main + chainlib.cli diff --git a/setup.py b/setup.py index c481db7..172e0b5 100644 --- a/setup.py +++ b/setup.py @@ -12,17 +12,9 @@ while True: requirements.append(l.rstrip()) f.close() -test_requirements = [] -f = open('test_requirements.txt', 'r') -while True: - l = f.readline() - if l == '': - break - test_requirements.append(l.rstrip()) -f.close() - - setup( install_requires=requirements, - tests_require=test_requirements, + extras_require={ + 'xdg': "pyxdg~=0.27", + } ) diff --git a/test_requirements.txt b/test_requirements.txt deleted file mode 100644 index 62ab455..0000000 --- a/test_requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ -eth_tester==0.5.0b3 -py-evm==0.3.0a20 -rlp==2.0.1 -pytest==6.0.1 diff --git a/tests/test_abi.py b/tests/test_abi.py deleted file mode 100644 index d1e00bc..0000000 --- a/tests/test_abi.py +++ /dev/null @@ -1,29 +0,0 @@ -from chainlib.eth.contract import ( - ABIContractEncoder, - ABIContractType, - ) - - -def test_abi_param(): - - e = ABIContractEncoder() - e.uint256(42) - e.bytes32('0x666f6f') - e.address('0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef') - e.method('foo') - e.typ(ABIContractType.UINT256) - e.typ(ABIContractType.BYTES32) - e.typ(ABIContractType.ADDRESS) - - assert e.types[0] == ABIContractType.UINT256 - assert e.types[1] == ABIContractType.BYTES32 - assert e.types[2] == ABIContractType.ADDRESS - assert e.contents[0] == '000000000000000000000000000000000000000000000000000000000000002a' - assert e.contents[1] == '0000000000000000000000000000000000000000000000000000000000666f6f' - assert e.contents[2] == '000000000000000000000000deadbeefdeadbeefdeadbeefdeadbeefdeadbeef' - - assert e.get() == 'a08f54bb000000000000000000000000000000000000000000000000000000000000002a0000000000000000000000000000000000000000000000000000000000666f6f000000000000000000000000deadbeefdeadbeefdeadbeefdeadbeefdeadbeef' - - -if __name__ == '__main__': - test_abi_param() diff --git a/tests/test_address.py b/tests/test_address.py deleted file mode 100644 index 693f603..0000000 --- a/tests/test_address.py +++ /dev/null @@ -1,35 +0,0 @@ -import unittest - -from chainlib.eth.address import ( - is_address, - is_checksum_address, - to_checksum, - ) - -from tests.base import TestBase - - -class TestChain(TestBase): - - def test_chain_spec(self): - checksum_address = '0xEb3907eCad74a0013c259D5874AE7f22DcBcC95C' - plain_address = checksum_address.lower() - - self.assertEqual(checksum_address, to_checksum(checksum_address)) - - self.assertTrue(is_address(plain_address)) - self.assertFalse(is_checksum_address(plain_address)) - self.assertTrue(is_checksum_address(checksum_address)) - - self.assertFalse(is_address(plain_address + "00")) - self.assertFalse(is_address(plain_address[:len(plain_address)-2])) - - with self.assertRaises(ValueError): - to_checksum(plain_address + "00") - - with self.assertRaises(ValueError): - to_checksum(plain_address[:len(plain_address)-2]) - - -if __name__ == '__main__': - unittest.main() diff --git a/tests/test_interface.py b/tests/test_interface.py new file mode 100644 index 0000000..8ff889d --- /dev/null +++ b/tests/test_interface.py @@ -0,0 +1,28 @@ +# standard imports +import unittest +from unittest.mock import Mock +import logging + +# local imports +from chainlib.interface import ChainInterface + +logg = logging.getLogger() + + +# replace with mocker +def block_from_src(src): + logg.debug('from src called with ' + src) + + +class TestInterface(unittest.TestCase): + + def test_interface_set(self): + ifc = ChainInterface() + block_from_src = Mock() + ifc.set('block_from_src', block_from_src) + ifc.block_from_src('foo') + block_from_src.assert_called() + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_nonce.py b/tests/test_nonce.py deleted file mode 100644 index f9332c8..0000000 --- a/tests/test_nonce.py +++ /dev/null @@ -1,26 +0,0 @@ -# standard imports -import os -import unittest - -# local imports -from chainlib.eth.address import to_checksum_address -from chainlib.eth.nonce import OverrideNonceOracle -from hexathon import add_0x - -# test imports -from tests.base import TestBase - - -class TestNonce(TestBase): - - def test_nonce(self): - addr_bytes = os.urandom(20) - addr = add_0x(to_checksum_address(addr_bytes.hex())) - n = OverrideNonceOracle(addr, 42) - self.assertEqual(n.get_nonce(), 42) - self.assertEqual(n.next_nonce(), 42) - self.assertEqual(n.next_nonce(), 43) - - -if __name__ == '__main__': - unittest.main() diff --git a/tests/test_sign.py b/tests/test_sign.py deleted file mode 100644 index 8175600..0000000 --- a/tests/test_sign.py +++ /dev/null @@ -1,119 +0,0 @@ -# standard imports -import os -import socket -import unittest -import unittest.mock -import logging -import json - -# external imports -from crypto_dev_signer.eth.transaction import EIP155Transaction -from crypto_dev_signer.eth.signer.defaultsigner import ReferenceSigner -from crypto_dev_signer.keystore.dict import DictKeystore - -# local imports -import chainlib -from chainlib.eth.connection import EthUnixSignerConnection -from chainlib.eth.sign import sign_transaction -from chainlib.eth.tx import TxFactory -from chainlib.eth.address import to_checksum_address -from chainlib.jsonrpc import ( - jsonrpc_response, - jsonrpc_error, - ) -from hexathon import ( - add_0x, - ) -from chainlib.chain import ChainSpec - -from tests.base import TestBase - -logging.basicConfig(level=logging.DEBUG) -logg = logging.getLogger() - -keystore = DictKeystore() -alice = keystore.new() -bob = keystore.new() - - -class Mocket(socket.socket): - - req_id = None - error = False - tx = None - signer = None - - def connect(self, v): - return self - - - def send(self, v): - o = json.loads(v) - logg.debug('mocket received {}'.format(v)) - Mocket.req_id = o['id'] - params = o['params'][0] - if to_checksum_address(params.get('from')) != alice: - logg.error('from does not match alice {}'.format(params)) - Mocket.error = True - if to_checksum_address(params.get('to')) != bob: - logg.error('to does not match bob {}'.format(params)) - Mocket.error = True - if not Mocket.error: - Mocket.tx = EIP155Transaction(params, params['nonce'], params['chainId']) - logg.debug('mocket {}'.format(Mocket.tx)) - return len(v) - - - def recv(self, c): - if Mocket.req_id != None: - - o = None - if Mocket.error: - o = jsonrpc_error(Mocket.req_id) - else: - tx = Mocket.tx - r = Mocket.signer.sign_transaction_to_rlp(tx) - Mocket.tx = None - o = jsonrpc_response(Mocket.req_id, add_0x(r.hex())) - Mocket.req_id = None - return json.dumps(o).encode('utf-8') - - return b'' - - -class TestSign(TestBase): - - - def setUp(self): - super(TestSign, self).__init__() - self.chain_spec = ChainSpec('evm', 'foo', 42) - - - logg.debug('alice {}'.format(alice)) - logg.debug('bob {}'.format(bob)) - - self.signer = ReferenceSigner(keystore) - - Mocket.signer = self.signer - - - def test_sign_build(self): - with unittest.mock.patch('chainlib.connection.socket.socket', Mocket) as m: - rpc = EthUnixSignerConnection('foo', chain_spec=self.chain_spec) - f = TxFactory(self.chain_spec, signer=rpc) - tx = f.template(alice, bob, use_nonce=True) - tx = f.build(tx) - logg.debug('tx result {}'.format(tx)) - - - def test_sign_rpc(self): - with unittest.mock.patch('chainlib.connection.socket.socket', Mocket) as m: - rpc = EthUnixSignerConnection('foo') - f = TxFactory(self.chain_spec, signer=rpc) - tx = f.template(alice, bob, use_nonce=True) - tx_o = sign_transaction(tx) - rpc.do(tx_o) - - -if __name__ == '__main__': - unittest.main() diff --git a/tests/test_stat.py b/tests/test_stat.py deleted file mode 100644 index a47317b..0000000 --- a/tests/test_stat.py +++ /dev/null @@ -1,49 +0,0 @@ -# standard imports -import unittest -import datetime - -# external imports -from chainlib.stat import ChainStat -from chainlib.eth.block import Block - - -class TestStat(unittest.TestCase): - - def test_block(self): - - s = ChainStat() - - d = datetime.datetime.utcnow() - datetime.timedelta(seconds=30) - block_a = Block({ - 'timestamp': d.timestamp(), - 'hash': None, - 'transactions': [], - 'number': 41, - }) - - d = datetime.datetime.utcnow() - block_b = Block({ - 'timestamp': d.timestamp(), - 'hash': None, - 'transactions': [], - 'number': 42, - }) - - s.block_apply(block_a) - s.block_apply(block_b) - self.assertEqual(s.block_average(), 30.0) - - d = datetime.datetime.utcnow() + datetime.timedelta(seconds=10) - block_c = Block({ - 'timestamp': d.timestamp(), - 'hash': None, - 'transactions': [], - 'number': 43, - }) - - s.block_apply(block_c) - self.assertEqual(s.block_average(), 20.0) - - -if __name__ == '__main__': - unittest.main() diff --git a/tests/test_tx.py b/tests/test_tx.py deleted file mode 100644 index 548472d..0000000 --- a/tests/test_tx.py +++ /dev/null @@ -1,30 +0,0 @@ -# standard imports -import unittest - -# local imports -from chainlib.eth.unittest.ethtester import EthTesterCase -from chainlib.eth.nonce import RPCNonceOracle -from chainlib.eth.gas import ( - RPCGasOracle, - Gas, - ) -from chainlib.eth.tx import ( - unpack, - TxFormat, - ) -from hexathon import strip_0x - -class TxTestCase(EthTesterCase): - - def test_tx_reciprocal(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 = unpack(bytes.fromhex(strip_0x(o)), self.chain_spec) - self.assertEqual(tx['from'], self.accounts[0]) - self.assertEqual(tx['to'], self.accounts[1]) - - -if __name__ == '__main__': - unittest.main() diff --git a/tests/testdata/keystore/UTC--2021-01-08T17-18-44.521011372Z--eb3907ecad74a0013c259d5874ae7f22dcbcc95c b/tests/testdata/keystore/UTC--2021-01-08T17-18-44.521011372Z--eb3907ecad74a0013c259d5874ae7f22dcbcc95c deleted file mode 100644 index 2b843ec..0000000 --- a/tests/testdata/keystore/UTC--2021-01-08T17-18-44.521011372Z--eb3907ecad74a0013c259d5874ae7f22dcbcc95c +++ /dev/null @@ -1 +0,0 @@ -{"address":"eb3907ecad74a0013c259d5874ae7f22dcbcc95c","crypto":{"cipher":"aes-128-ctr","ciphertext":"b0f70a8af4071faff2267374e2423cbc7a71012096fd2215866d8de7445cc215","cipherparams":{"iv":"9ac89383a7793226446dcb7e1b45cdf3"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":262144,"p":1,"r":8,"salt":"299f7b5df1d08a0a7b7f9c9eb44fe4798683b78da3513fcf9603fd913ab3336f"},"mac":"6f4ed36c11345a9a48353cd2f93f1f92958c96df15f3112a192bc994250e8d03"},"id":"61a9dd88-24a9-495c-9a51-152bd1bfaa5b","version":3} \ No newline at end of file