Merge branch 'lash/chainlib-cli' into 'master'

Add docstrings

See merge request chaintool/chainlib!3
This commit is contained in:
Louis Holbrook 2021-08-21 07:32:00 +00:00
commit f1ffff8b90
62 changed files with 1301 additions and 2898 deletions

View File

@ -1,4 +1,10 @@
- 0.0.3-pending
- 0.0.5-pending
* Move eth code to separate package
- 0.0.4-unreleased
* Add pack tx from already signed tx struct
* Add http auth handling for jsonrpc connections
* Add customizable jsonrpc id generator (to allow for buggy server id handling)
- 0.0.3-unreleased
* Remove erc20 module (to new external package)
- 0.0.2-unreleased
*

View File

@ -1 +1 @@
include requirements.txt
include *requirements.txt LICENSE chainlib/data/config/*

View File

@ -28,7 +28,7 @@ Chainlib is not compatible with python2, nor is there any reason to expect it wi
Any generalizable structures and code can be found in the base module directory `chainlib/`
Currently the only operational code for available targets is for the `evm` and the `Ethereum` network protocol. This code can be found in `chainlib/eth`.
Currently the only operational code for available targets is for the `evm` and the `Ethereum` network protocol. This code can be found in the separate package `chainlib-eth`.
Every module will have a subdirectory `runnable` which contains CLI convenience tooling for common operations. Any directory `example` will contain code snippets demonstrating usage.

32
chainlib/auth.py Normal file
View 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,)

View File

@ -1,7 +1,66 @@
# standard imports
import enum
# local imports
from chainlib.tx import Tx
class BlockSpec(enum.IntEnum):
"""General-purpose block-height value designators
"""
PENDING = -1
LATEST = 0
class Block:
"""Base class to extend for implementation specific block object.
"""
tx_generator = Tx
def src(self):
"""Return implementation specific block representation.
:rtype: dict
:returns: Block representation
"""
return self.block_src
def tx(self, idx):
"""Return transaction object for transaction data at given index.
:param idx: Transaction index
:type idx: int
:rtype: chainlib.tx.Tx
:returns: Transaction object
"""
return self.tx_generator(self.txs[idx], self)
def tx_src(self, idx):
"""Return implementation specific transaction representation for transaction data at given index
:param idx: Transaction index
:type idx: int
:rtype: chainlib.tx.Tx
:returns: Transaction representation
"""
return self.txs[idx]
def __str__(self):
return 'block {} {} ({} txs)'.format(self.number, self.hash, len(self.txs))
@classmethod
def from_src(cls, src):
"""Instantiate an implementation specific block object from the given block representation.
:param src: Block representation
:type src: dict
:rtype: chainlib.block.Block
:returns: Block object
"""
return cls(src)

View File

@ -3,7 +3,19 @@ import copy
class ChainSpec:
"""Encapsulates a 3- to 4-part chain identifier, describing the architecture used and common name of the chain, along with the network id of the connected network.
The optional fourth field can be used to add a description value, independent of the chain identifier value.
:param engine: Chain architecture
:type engine: str
:param common_name: Well-known name of chain
:type common_name: str
:param network_id: Chain network identifier
:type network_id: int
:param tag: Descriptive tag
:type tag: str
"""
def __init__(self, engine, common_name, network_id, tag=None):
self.o = {
'engine': engine,
@ -13,23 +25,56 @@ class ChainSpec:
}
def network_id(self):
"""Returns the network id part of the spec.
:rtype: int
:returns: network_id
"""
return self.o['network_id']
def chain_id(self):
"""Alias of network_id
:rtype: int
:returns: network_id
"""
return self.o['network_id']
def engine(self):
"""Returns the chain architecture part of the spec
:rtype: str
:returns: engine
"""
return self.o['engine']
def common_name(self):
"""Returns the common name part of the spec
:rtype: str
:returns: common_name
"""
return self.o['common_name']
@staticmethod
def from_chain_str(chain_str):
"""Create a new ChainSpec object from a colon-separated string, as output by the string representation of the ChainSpec object.
String must be in one of the following formats:
- <engine>:<common_name>:<network_id>
- <engine>:<common_name>:<network_id>:<tag>
:param chain_str: Chainspec string
:type chain_str: str
:raises ValueError: Malformed chain string
:rtype: chainlib.chain.ChainSpec
:returns: Resulting chain spec
"""
o = chain_str.split(':')
if len(o) < 3:
raise ValueError('Chain string must have three sections, got {}'.format(len(o)))
@ -41,10 +86,29 @@ class ChainSpec:
@staticmethod
def from_dict(o):
"""Create a new ChainSpec object from a dictionary, as output from the asdict method.
The chain spec is described by the following keys:
- engine
- common_name
- network_id
- tag (optional)
:param o: Chainspec dictionary
:type o: dict
:rtype: chainlib.chain.ChainSpec
:returns: Resulting chain spec
"""
return ChainSpec(o['engine'], o['common_name'], o['network_id'], tag=o['tag'])
def asdict(self):
"""Create a dictionary representation of the chain spec.
:rtype: dict
:returns: Chain spec dictionary
"""
return copy.copy(self.o)

10
chainlib/cli/__init__.py Normal file
View 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
View 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
View 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
View 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
View 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
View 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

View File

@ -5,6 +5,7 @@ import logging
import enum
import re
import json
import base64
from urllib.request import (
Request,
urlopen,
@ -13,22 +14,26 @@ from urllib.request import (
build_opener,
install_opener,
)
from urllib.error import URLError
# local imports
from .jsonrpc import (
jsonrpc_template,
JSONRPCRequest,
jsonrpc_result,
DefaultErrorParser,
ErrorParser,
)
from .http import PreemptiveBasicAuthHandler
from .error import JSONRPCException
from .auth import Auth
logg = logging.getLogger().getChild(__name__)
logg = logging.getLogger(__name__)
error_parser = DefaultErrorParser()
error_parser = ErrorParser()
class ConnType(enum.Enum):
"""Describe the underlying RPC connection type.
"""
CUSTOM = 0x00
HTTP = 0x100
HTTP_SSL = 0x101
@ -41,7 +46,15 @@ re_http = '^http(s)?://'
re_ws = '^ws(s)?://'
re_unix = '^ipc://'
def str_to_connspec(s):
"""Determine the connection type from a connection string.
:param s: Connection string
:type d: str
:rtype: chainlib.connection.ConnType
:returns: Connection type value
"""
if s == 'custom':
return ConnType.CUSTOM
@ -57,7 +70,6 @@ def str_to_connspec(s):
return ConnType.WEBSOCKET_SSL
return ConnType.WEBSOCKET
m = re.match(re_unix, s)
if m != None:
return ConnType.UNIX
@ -65,7 +77,20 @@ def str_to_connspec(s):
raise ValueError('unknown connection type {}'.format(s))
class RPCConnection():
class RPCConnection:
"""Base class for defining an RPC connection to a chain node.
This class may be instantiated directly, or used as an object factory to provide a thread-safe RPC connection mechanism to a single RPC node.
:param url: A valid URL connection string for the RPC connection
:type url: str
:param chain_spec: The chain spec of
:type chain_spec: chainlib.chain.ChainSpec
:param auth: Authentication settings to use when connecting
:type auth: chainlib.auth.Auth
:todo: basic auth is currently parsed from the connection string, should be auth object instead. auth object effectively not in use.
"""
__locations = {}
__constructors = {
@ -74,15 +99,20 @@ class RPCConnection():
}
__constructors_for_chains = {}
def __init__(self, url=None, chain_spec=None):
def __init__(self, url=None, chain_spec=None, auth=None):
self.chain_spec = chain_spec
self.location = None
self.basic = None
if url == None:
return
self.auth = auth
if self.auth != None and not isinstance(self.auth, Auth):
raise TypeError('auth parameter needs to be subclass of chainlib.auth.Auth')
url_parsed = urlparse(url)
logg.debug('creating connection {} -> {}'.format(url, url_parsed))
# TODO: temporary basic auth parse
basic = url_parsed.netloc.split('@')
location = None
if len(basic) == 1:
@ -93,6 +123,7 @@ class RPCConnection():
#if url_parsed.port != None:
# location += ':' + str(url_parsed.port)
#
self.location = os.path.join('{}://'.format(url_parsed.scheme), location)
self.location = urljoin(self.location, url_parsed.path)
@ -101,20 +132,50 @@ class RPCConnection():
@staticmethod
def from_conntype(t, tag='default'):
"""Retrieve a connection constructor from the given tag and connection type.
:param t: Connection type
:type t: chainlib.connection.ConnType
:param tag: The connection selector tag
:type tag:
"""
return RPCConnection.__constructors[tag][t]
@staticmethod
def register_constructor(t, c, tag='default'):
def register_constructor(conntype, c, tag='default'):
"""Associate a connection constructor for a given tag and connection type.
The constructor must be a chainlib.connection.RPCConnection object or an object of a subclass thereof.
:param conntype: Connection type of constructor
:type conntype: chainlib.connection.ConnType
:param c: Constructor
:type c: chainlib.connection.RPCConnection
:param tag: Tag to store the connection constructor under
:type tag: str
"""
if RPCConnection.__constructors.get(tag) == None:
RPCConnection.__constructors[tag] = {}
RPCConnection.__constructors[tag][t] = c
logg.info('registered RPC connection constructor {} for type {} tag {}'.format(c, t, tag))
RPCConnection.__constructors[tag][conntype] = c
logg.info('registered RPC connection constructor {} for type {} tag {}'.format(c, conntype, tag))
# TODO: constructor needs to be constructor-factory, that itself can select on url type
@staticmethod
def register_location(location, chain_spec, tag='default', exist_ok=False):
"""Associate a URL for a given tag and chain spec.
:param location: URL of RPC connection
:type location: str
:param chain_spec: Chain spec describing the chain behind the RPC connection
:type chain_spec: chainlib.chain.ChainSpec
:param tag: Tag to store the connection location under
:type tag: str
:param exist_ok: Overwrite existing record
:type exist_ok: bool
:raises ValueError: Record already exists, and exist_ok is not set
"""
chain_str = str(chain_spec)
if RPCConnection.__locations.get(chain_str) == None:
RPCConnection.__locations[chain_str] = {}
@ -129,6 +190,19 @@ class RPCConnection():
@staticmethod
def connect(chain_spec, tag='default'):
"""Connect to the location defined by the given tag and chain spec, using the associated constructor.
Location must first be registered using the RPCConnection.register_location method.
Constructor must first be registered using the RPCConnection.register_constructor method.
:param chain_spec: Chain spec part of the location record
:type chain_spec: chainlib.chain.ChainSpec
:param tag: Tag part of the location record
:type tag: str
:rtype: chainlib.connection.RPCConnection
:returns: Instantiation of the matching registered constructor
"""
chain_str = str(chain_spec)
c = RPCConnection.__locations[chain_str][tag]
constructor = RPCConnection.from_conntype(c[0], tag=tag)
@ -136,9 +210,9 @@ class RPCConnection():
return constructor(url=c[1], chain_spec=chain_spec)
class HTTPConnection(RPCConnection):
def disconnect(self):
"""Should be overridden to clean up any resources bound by the connect method.
"""
pass
@ -146,27 +220,84 @@ class HTTPConnection(RPCConnection):
self.disconnect()
class HTTPConnection(RPCConnection):
"""Generic HTTP connection subclass of RPCConnection
"""
pass
class UnixConnection(RPCConnection):
"""Generic Unix socket connection subclass of RPCConnection
"""
pass
def disconnect(self):
pass
def __del__(self):
self.disconnect()
class JSONRPCHTTPConnection(HTTPConnection):
"""Generic JSON-RPC specific HTTP connection wrapper.
"""
def check_rpc(self):
"""Check if RPC connection is a valid JSON-RPC endpoint.
:raises Exception: Invalid connection.
"""
j = JSONRPCRequest()
req = j.template()
req['method'] = 'ping'
try:
self.do(req)
except JSONRPCException:
pass
def check(self):
"""Check if endpoint is reachable.
:rtype: bool
:returns: True if reachable
"""
try:
self.check_rpc()
except URLError as e:
logg.error('cannot connect to node {}; {}'.format(self.location, e))
return False
return True
def do(self, o, error_parser=error_parser):
"""Execute a JSON-RPC query, from dict as generated by chainlib.jsonrpc.JSONRPCRequest:finalize.
If connection was created with an auth object, the auth object will be used to authenticate the query.
If connection was created with a basic url string, the corresponding basic auth credentials will be used to authenticate the query.
:param o: JSON-RPC query object
:type o: dict
:param error_parser: Error parser object to process JSON-RPC error response with.
:type error_parser: chainlib.jsonrpc.ErrorParser
:raises ValueError: Invalid response from JSON-RPC endpoint
:raises URLError: Endpoint could not be reached
:rtype: any
:returns: Result value part of JSON RPC response
:todo: Invalid response exception from invalid json response
"""
req = Request(
self.location,
method='POST',
)
req.add_header('Content-Type', 'application/json')
# use specific auth if present
if self.auth != None:
p = self.auth.urllib_header()
req.add_header(p[0], p[1])
data = json.dumps(o)
logg.debug('(HTTP) send {}'.format(data))
# use basic auth if present
if self.basic != None:
handler = PreemptiveBasicAuthHandler()
handler.add_password(
@ -179,6 +310,7 @@ class JSONRPCHTTPConnection(HTTPConnection):
install_opener(ho)
r = urlopen(req, data=data.encode('utf-8'))
result = json.load(r)
logg.debug('(HTTP) recv {}'.format(result))
if o['id'] != result['id']:
@ -187,6 +319,18 @@ class JSONRPCHTTPConnection(HTTPConnection):
class JSONRPCUnixConnection(UnixConnection):
"""Execute a JSON-RPC query, from dict as generated by chainlib.jsonrpc.JSONRPCRequest:finalize.
:param o: JSON-RPC query object
:type o: dict
:param error_parser: Error parser object to process JSON-RPC error response with.
:type error_parser: chainlib.jsonrpc.ErrorParser
:raises ValueError: Invalid response from JSON-RPC endpoint
:raises IOError: Endpoint could not be reached
:rtype: any
:returns: Result value part of JSON RPC response
:todo: Invalid response exception from invalid json response
"""
def do(self, o, error_parser=error_parser):
conn = socket.socket(family=socket.AF_UNIX, type=socket.SOCK_STREAM, proto=0)
@ -217,6 +361,7 @@ class JSONRPCUnixConnection(UnixConnection):
return jsonrpc_result(result, error_parser)
# TODO: Automatic creation should be hidden behind symbol, in the spirit of no unsolicited side-effects. (perhaps connection should be module dir, and jsonrpc a submodule)
RPCConnection.register_constructor(ConnType.HTTP, JSONRPCHTTPConnection, tag='default')
RPCConnection.register_constructor(ConnType.HTTP_SSL, JSONRPCHTTPConnection, tag='default')
RPCConnection.register_constructor(ConnType.UNIX, JSONRPCUnixConnection, tag='default')

View File

@ -0,0 +1,12 @@
[rpc]
http_provider =
http_authentication =
http_username =
http_password =
[chain]
spec =
[wallet]
key_file =
passphrase =

View File

@ -1,7 +1,22 @@
# TODO: use json-rpc module
class JSONRPCException(Exception):
class RPCException(Exception):
"""Base RPC connection error
"""
pass
class JSONRPCException(RPCException):
"""Base JSON-RPC error
"""
pass
class ExecutionError(Exception):
"""Base error for transaction execution failures
"""
pass
class SignerMissingException(Exception):
"""Raised when attempting to retrieve a signer when none has been added
"""

View File

@ -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

View File

@ -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))

View File

@ -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')

View File

@ -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)

View File

@ -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

View File

@ -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))

View File

@ -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

View File

@ -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 doesnt exist Should be used when something which should be there is not found. (Doesnt 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.

View File

@ -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

View File

@ -1,3 +0,0 @@
from .fixtures_ethtester import *
from .fixtures_chain import *
from .fixtures_signer import *

View File

@ -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,
}

View File

@ -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

View File

@ -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],
}

View File

@ -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()

View File

@ -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()

View File

@ -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()

View File

@ -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()

View File

@ -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()

View File

@ -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()

View File

@ -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()

View File

@ -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()

View File

@ -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()

View File

@ -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)))

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -1,28 +1,48 @@
# third-party imports
# external imports
import sha3
from hexathon import (
add_0x,
strip_0x,
)
from hexathon import strip_0x
def keccak256_hex(s):
"""Hex representation of Keccak256 hash of utf-8 string content.
:param s: utf-8 string to hash
:type s: str
:rtype: str
:returns: Hex-value of keccak256 hash
"""
h = sha3.keccak_256()
h.update(s.encode('utf-8'))
return h.digest().hex()
def keccak256_string_to_hex(s):
"""Alias of keccak256_hex
"""
return keccak256_hex(s)
def keecak256_bytes_to_hex(b):
"""Hex representation of Keccak256 hash of literal byte content.
:param b: bytes to hash
:type b: bytes
:rtype: str
:returns: Hex-value of keccak256 hash
"""
h = sha3.keccak_256()
h.update(b)
return h.digest().hex()
def keccak256_hex_to_hex(hx):
"""Hex representation of Keccak256 hash of byte value of hex content.
:param hx: Hex-value of bytes to hash
:type hx: str
:rtype: str
:returns: Hex-value of keccak256 hash
"""
h = sha3.keccak_256()
b = bytes.fromhex(strip_0x(hx))
h.update(b)

View File

@ -1,3 +1,4 @@
# standard imports
import urllib
import base64
import logging
@ -8,14 +9,16 @@ logg = logging.getLogger(__name__)
# THANKS to https://stackoverflow.com/questions/2407126/python-urllib2-basic-auth-problem
class PreemptiveBasicAuthHandler(urllib.request.HTTPBasicAuthHandler):
"""Handler for basic auth urllib callback.
:param req: Request payload
:type req: str
:return: Request payload
:rtype: str
"""
def http_request(self, req):
"""Handler for basic auth urllib callback.
:param req: Request payload
:type req: str
:return: Request payload
:rtype: str
"""
url = req.get_full_url()
realm = None
user, pw = self.passwd.find_user_password(realm, url)

255
chainlib/interface.py Normal file
View 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))

View File

@ -4,35 +4,140 @@ import uuid
# local imports
from .error import JSONRPCException
# TODO: Move all contents in this file to independent package
class DefaultErrorParser:
class JSONRPCIdGenerator:
def next(self):
raise NotImplementedError
class UUIDGenerator(JSONRPCIdGenerator):
"""Create uuid ids for JSON-RPC queries.
"""
def next(self):
"""Create a new id
:rtype: str
:returns: uuid string
"""
return str(uuid.uuid4())
class IntSequenceGenerator(JSONRPCIdGenerator):
"""Create sequential numeric ids for JSON-RPC queries.
:param start: Start at the specificed numeric id
:type start: int
"""
def __init__(self, start=0):
self.id = start
def next(self):
"""Get the next id in the sequence.
:rtype: int
:returns: numeric id
"""
next_id = self.id
self.id += 1
return next_id
default_id_generator = UUIDGenerator()
class ErrorParser:
"""Base class for parsing JSON-RPC error repsonses
"""
def translate(self, error):
"""Interface method called by jsonrpc_result when encountering an error
This class method may be overriden to provide more fine-grained context for both general and implementation specific errors.
:param error: JSON-RPC error response object
:type error: dict
:rtype: chainlib.error.JSONRPCException
:returns: Descriptiv JSONRPCException
"""
return JSONRPCException('default parser code {}'.format(error))
def jsonrpc_template():
return {
'jsonrpc': '2.0',
'id': str(uuid.uuid4()),
'method': None,
'params': [],
}
# deprecated symbol, provided for backward compatibility
DefaultErrorParser = ErrorParser
def jsonrpc_result(o, ep):
if o.get('error') != None:
raise ep.translate(o)
return o['result']
class JSONRPCRequest:
"""JSON-RPC request builder class.
:param id_generator: Generator to use to define the id of the request.
:type id_generator: chainlib.jsonrpc.JSONRPCIdGenerator
"""
def __init__(self, id_generator=default_id_generator):
if id_generator == None:
id_generator = default_id_generator
self.id_generator = id_generator
def template(self):
"""Return a empty json-rpc 2.0 dictionary query object
:rtype: dict
:returns: json-rpc query object
"""
return {
'jsonrpc': '2.0',
'id': None,
'method': None,
'params': [],
}
def finalize(self, request):
"""Apply next json-rpc id to json-rpc dictionary query object
:param request: json-rpc query
:type request: dict
:rtype: dict
:returns: json-rpc query with id added
"""
request['id'] = self.id_generator.next()
return request
def jsonrpc_response(request_id, result):
return {
'jsonrpc': '2.0',
'id': request_id,
'result': result,
}
"""Create a json-rpc dictionary response object from the given id an result value.
:param request_id: json-rpc query id
:type request_id: str or int
:param result: result value
:type result: any json-serializable value
:rtype: dict
:result: json-rpc response object
"""
return {
'jsonrpc': '2.0',
'id': request_id,
'result': result,
}
def jsonrpc_error(request_id, code=-32000, message='Server error'):
"""Create a json-rpc dictionary error object for the given id with error code and message.
:param request_id: json-rpc query id
:type request_id: str or int
:param code: json-rpc error code
:type code: int
:param message: Error message
:type message: str
:rtype: dict
:returns: json-rpc error object
"""
return {
'jsonrpc': '2.0',
'id': request_id,
@ -41,3 +146,21 @@ def jsonrpc_error(request_id, code=-32000, message='Server error'):
'message': message,
},
}
def jsonrpc_result(o, ep):
"""Retrieve the result from a json-rpc response object.
If the result object is an error, the provided error parser will be used to generate the corresponding exception.
:param o: json-rpc response object
:type o: dict
:param ep: Error parser
:type ep: chainlib.jsonrpc.ErrorParser
:raises JSONRPCException: exception encapsulating the error value of the response
:rtype: any json-deserializable value
:returns: The result value of the response
"""
if o.get('error') != None:
raise ep.translate(o)
return o['result']

View File

@ -1,6 +1,11 @@
# standard imports
import datetime
class ChainStat:
"""Block time aggregator.
"""
def __init__(self):
self.block_timestamp_last = None
@ -9,6 +14,11 @@ class ChainStat:
def block_apply(self, block):
"""Add data from block to aggregate.
:param block: Block to add
:type block: chainlib.block.Block
"""
if self.block_timestamp_last == None:
self.block_timestamp_last = block.timestamp
@ -25,5 +35,11 @@ class ChainStat:
self.block_timestamp_last = block.timestamp
def block_average(self):
"""Get current aggregated average.
:rtype: float
:returns: Aggregate average block time, in seconds
"""
return self.block_avg_aggregate

View File

@ -2,6 +2,8 @@
import enum
class Status(enum.Enum):
"""Representation of transaction status in network.
"""
PENDING = 0
SUCCESS = 1
ERROR = 2

14
chainlib/tx.py Normal file
View 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

View File

@ -1,5 +1,3 @@
crypto-dev-signer~=0.4.14b3
crypto-dev-signer>=0.4.14b7,<=0.4.14
pysha3==1.0.2
hexathon~=0.0.1a7
websocket-client==0.57.0
potaahto~=0.0.1a1
hexathon~=0.0.1a8

View File

@ -1,6 +1,5 @@
[metadata]
name = chainlib
version = 0.0.3rc3
version = 0.0.8a2
description = Generic blockchain access library and tooling
author = Louis Holbrook
author_email = dev@holbrook.no
@ -9,7 +8,6 @@ keywords =
dlt
blockchain
cryptocurrency
ethereum
classifiers =
Programming Language :: Python :: 3
Operating System :: OS Independent
@ -18,27 +16,14 @@ classifiers =
Intended Audience :: Developers
License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
Topic :: Internet
# Topic :: Blockchain :: EVM
license = GPL3
licence_files =
LICENSE.txt
[options]
python_requires = >= 3.6
include_package_data = True
packages =
chainlib
chainlib.eth
chainlib.eth.runnable
chainlib.eth.pytest
chainlib.eth.unittest
[options.entry_points]
console_scripts =
eth-balance = chainlib.eth.runnable.balance:main
eth-checksum = chainlib.eth.runnable.checksum:main
eth-gas = chainlib.eth.runnable.gas:main
eth-raw = chainlib.eth.runnable.raw:main
eth-get = chainlib.eth.runnable.get:main
eth-decode = chainlib.eth.runnable.decode:main
eth-info = chainlib.eth.runnable.info:main
eth = chainlib.eth.runnable.info:main
chainlib.cli

View File

@ -12,17 +12,9 @@ while True:
requirements.append(l.rstrip())
f.close()
test_requirements = []
f = open('test_requirements.txt', 'r')
while True:
l = f.readline()
if l == '':
break
test_requirements.append(l.rstrip())
f.close()
setup(
install_requires=requirements,
tests_require=test_requirements,
extras_require={
'xdg': "pyxdg~=0.27",
}
)

View File

@ -1,4 +0,0 @@
eth_tester==0.5.0b3
py-evm==0.3.0a20
rlp==2.0.1
pytest==6.0.1

View File

@ -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()

View File

@ -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
View 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()

View File

@ -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()

View File

@ -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()

View File

@ -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()

View File

@ -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()

View File

@ -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}