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