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