Add docstrings
This commit is contained in:
		
							parent
							
								
									64ba891b21
								
							
						
					
					
						commit
						1b946447ae
					
				| @ -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 | ||||
| 	*  | ||||
|  | ||||
| @ -1 +1 @@ | ||||
| include requirements.txt | ||||
| include *requirements.txt LICENSE chainlib/data/config/* | ||||
|  | ||||
| @ -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. | ||||
| 
 | ||||
|  | ||||
							
								
								
									
										32
									
								
								chainlib/auth.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								chainlib/auth.py
									
									
									
									
									
										Normal file
									
								
							| @ -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,) | ||||
| @ -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) | ||||
|  | ||||
| @ -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: | ||||
| 
 | ||||
|         - <engine>:<common_name>:<network_id> | ||||
|         - <engine>:<common_name>:<network_id>:<tag> | ||||
| 
 | ||||
|         :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) | ||||
| 
 | ||||
| 
 | ||||
|  | ||||
							
								
								
									
										10
									
								
								chainlib/cli/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								chainlib/cli/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @ -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 | ||||
							
								
								
									
										101
									
								
								chainlib/cli/arg.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										101
									
								
								chainlib/cli/arg.py
									
									
									
									
									
										Normal file
									
								
							| @ -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') | ||||
| 
 | ||||
							
								
								
									
										38
									
								
								chainlib/cli/base.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								chainlib/cli/base.py
									
									
									
									
									
										Normal file
									
								
							| @ -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 | ||||
							
								
								
									
										159
									
								
								chainlib/cli/config.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										159
									
								
								chainlib/cli/config.py
									
									
									
									
									
										Normal file
									
								
							| @ -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 | ||||
							
								
								
									
										82
									
								
								chainlib/cli/rpc.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								chainlib/cli/rpc.py
									
									
									
									
									
										Normal file
									
								
							| @ -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 | ||||
							
								
								
									
										57
									
								
								chainlib/cli/wallet.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								chainlib/cli/wallet.py
									
									
									
									
									
										Normal file
									
								
							| @ -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 | ||||
| @ -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') | ||||
|  | ||||
							
								
								
									
										12
									
								
								chainlib/data/config/config.ini
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								chainlib/data/config/config.ini
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,12 @@ | ||||
| [rpc] | ||||
| http_provider =  | ||||
| http_authentication =  | ||||
| http_username = | ||||
| http_password = | ||||
| 
 | ||||
| [chain] | ||||
| spec =  | ||||
| 
 | ||||
| [wallet] | ||||
| key_file =  | ||||
| passphrase =  | ||||
| @ -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 | ||||
|     """ | ||||
|  | ||||
| @ -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 | ||||
| @ -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)) | ||||
| @ -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') | ||||
| @ -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) | ||||
| @ -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 | ||||
| @ -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)) | ||||
| @ -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 | ||||
| @ -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. | ||||
| @ -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 | ||||
| @ -1,3 +0,0 @@ | ||||
| from .fixtures_ethtester import * | ||||
| from .fixtures_chain import * | ||||
| from .fixtures_signer import * | ||||
| @ -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, | ||||
|             } | ||||
| @ -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 | ||||
| @ -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], | ||||
|         } | ||||
| @ -1,91 +0,0 @@ | ||||
| #!python3 | ||||
| 
 | ||||
| """Token balance query script | ||||
| 
 | ||||
| .. moduleauthor:: Louis Holbrook <dev@holbrook.no> | ||||
| .. pgp:: 0826EDA1702D1E87C6E2875121D2E7BB88C2A746  | ||||
| 
 | ||||
| """ | ||||
| 
 | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
| 
 | ||||
| # standard imports | ||||
| import os | ||||
| import json | ||||
| import argparse | ||||
| import logging | ||||
| 
 | ||||
| # third-party imports | ||||
| 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() | ||||
| @ -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() | ||||
| @ -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() | ||||
| @ -1,50 +0,0 @@ | ||||
| #!python3 | ||||
| 
 | ||||
| """Decode raw transaction | ||||
| 
 | ||||
| .. moduleauthor:: Louis Holbrook <dev@holbrook.no> | ||||
| .. 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() | ||||
| @ -1,163 +0,0 @@ | ||||
| #!python3 | ||||
| 
 | ||||
| """Gas transfer script | ||||
| 
 | ||||
| .. moduleauthor:: Louis Holbrook <dev@holbrook.no> | ||||
| .. 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() | ||||
| @ -1,118 +0,0 @@ | ||||
| #!python3 | ||||
| 
 | ||||
| """Token balance query script | ||||
| 
 | ||||
| .. moduleauthor:: Louis Holbrook <dev@holbrook.no> | ||||
| .. 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() | ||||
| @ -1,154 +0,0 @@ | ||||
| #!python3 | ||||
| 
 | ||||
| """Token balance query script | ||||
| 
 | ||||
| .. moduleauthor:: Louis Holbrook <dev@holbrook.no> | ||||
| .. 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() | ||||
| @ -1,175 +0,0 @@ | ||||
| #!python3 | ||||
| 
 | ||||
| """Gas transfer script | ||||
| 
 | ||||
| .. moduleauthor:: Louis Holbrook <dev@holbrook.no> | ||||
| .. 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() | ||||
| @ -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() | ||||
| @ -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))) | ||||
| @ -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 | ||||
| @ -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 | ||||
| 
 | ||||
| @ -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 | ||||
| @ -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 | ||||
| @ -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) | ||||
|  | ||||
| @ -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) | ||||
|  | ||||
							
								
								
									
										255
									
								
								chainlib/interface.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										255
									
								
								chainlib/interface.py
									
									
									
									
									
										Normal file
									
								
							| @ -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)) | ||||
| @ -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'] | ||||
|  | ||||
| @ -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 | ||||
|  | ||||
| @ -2,6 +2,8 @@ | ||||
| import enum | ||||
| 
 | ||||
| class Status(enum.Enum): | ||||
|     """Representation of transaction status in network. | ||||
|     """ | ||||
|     PENDING = 0 | ||||
|     SUCCESS = 1 | ||||
|     ERROR = 2 | ||||
|  | ||||
							
								
								
									
										14
									
								
								chainlib/tx.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								chainlib/tx.py
									
									
									
									
									
										Normal file
									
								
							| @ -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 | ||||
| @ -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 | ||||
|  | ||||
							
								
								
									
										23
									
								
								setup.cfg
									
									
									
									
									
								
							
							
						
						
									
										23
									
								
								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 | ||||
|  | ||||
							
								
								
									
										14
									
								
								setup.py
									
									
									
									
									
								
							
							
						
						
									
										14
									
								
								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", | ||||
|              } | ||||
|     ) | ||||
|  | ||||
| @ -1,4 +0,0 @@ | ||||
| eth_tester==0.5.0b3 | ||||
| py-evm==0.3.0a20 | ||||
| rlp==2.0.1 | ||||
| pytest==6.0.1 | ||||
| @ -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() | ||||
| @ -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() | ||||
							
								
								
									
										28
									
								
								tests/test_interface.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								tests/test_interface.py
									
									
									
									
									
										Normal file
									
								
							| @ -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() | ||||
| @ -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() | ||||
| @ -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() | ||||
| @ -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() | ||||
| @ -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() | ||||
| @ -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} | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user