From ec07317dffa1ffafc1887c7a139b04adc69ebc9f Mon Sep 17 00:00:00 2001 From: Louis Holbrook Date: Thu, 8 Apr 2021 15:39:32 +0000 Subject: [PATCH] Add chain stat, info cli --- chainlib/eth/block.py | 10 ++- chainlib/eth/runnable/balance.py | 2 +- chainlib/eth/runnable/checksum.py | 4 +- chainlib/eth/runnable/count.py | 1 + chainlib/eth/runnable/get.py | 2 +- chainlib/eth/runnable/info.py | 136 ++++++++++++++++++++++++++++++ chainlib/eth/tx.py | 93 +++++++++++++++++--- chainlib/eth/unittest/base.py | 5 ++ chainlib/stat.py | 30 +++++++ setup.cfg | 4 +- tests/test_stat.py | 49 +++++++++++ 11 files changed, 316 insertions(+), 20 deletions(-) create mode 100644 chainlib/eth/runnable/info.py create mode 100644 chainlib/stat.py create mode 100644 tests/test_stat.py diff --git a/chainlib/eth/block.py b/chainlib/eth/block.py index 39cc6ac..f563850 100644 --- a/chainlib/eth/block.py +++ b/chainlib/eth/block.py @@ -42,10 +42,16 @@ class Block: def __init__(self, src): self.hash = src['hash'] - self.number = int(strip_0x(src['number']), 16) + try: + self.number = int(strip_0x(src['number']), 16) + except TypeError: + self.number = int(src['number']) self.txs = src['transactions'] self.block_src = src - self.timestamp = int(strip_0x(src['timestamp']), 16) + try: + self.timestamp = int(strip_0x(src['timestamp']), 16) + except TypeError: + self.timestamp = int(src['timestamp']) def src(self): diff --git a/chainlib/eth/runnable/balance.py b/chainlib/eth/runnable/balance.py index 890a5da..4a406ae 100644 --- a/chainlib/eth/runnable/balance.py +++ b/chainlib/eth/runnable/balance.py @@ -36,6 +36,7 @@ from chainlib.eth.gas import ( OverrideGasOracle, balance, ) +from chainlib.chain import ChainSpec logging.basicConfig(level=logging.WARNING) logg = logging.getLogger() @@ -48,7 +49,6 @@ argparser.add_argument('-p', '--provider', dest='p', default=default_eth_provide argparser.add_argument('-a', '--token-address', dest='a', type=str, help='Token address. If not set, will return gas balance') 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('--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('address', type=str, help='Account address') diff --git a/chainlib/eth/runnable/checksum.py b/chainlib/eth/runnable/checksum.py index 76538dc..21c28c2 100644 --- a/chainlib/eth/runnable/checksum.py +++ b/chainlib/eth/runnable/checksum.py @@ -2,7 +2,7 @@ import sys # local imports -from chainlib.eth.address import to_checksum +from chainlib.eth.address import to_checksum_address -print(to_checksum(sys.argv[1])) +print(to_checksum_address(sys.argv[1])) diff --git a/chainlib/eth/runnable/count.py b/chainlib/eth/runnable/count.py index fe6d760..969d207 100644 --- a/chainlib/eth/runnable/count.py +++ b/chainlib/eth/runnable/count.py @@ -11,6 +11,7 @@ import logging 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 diff --git a/chainlib/eth/runnable/get.py b/chainlib/eth/runnable/get.py index 2b9ece5..a9c58f0 100644 --- a/chainlib/eth/runnable/get.py +++ b/chainlib/eth/runnable/get.py @@ -34,6 +34,7 @@ from chainlib.jsonrpc import ( from chainlib.eth.connection import EthHTTPConnection from chainlib.eth.tx import Tx from chainlib.eth.block import Block +from chainlib.chain import ChainSpec logging.basicConfig(level=logging.WARNING) logg = logging.getLogger() @@ -45,7 +46,6 @@ 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('-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') diff --git a/chainlib/eth/runnable/info.py b/chainlib/eth/runnable/info.py new file mode 100644 index 0000000..e9f5de9 --- /dev/null +++ b/chainlib/eth/runnable/info.py @@ -0,0 +1,136 @@ +#!python3 + +"""Token balance query script + +.. moduleauthor:: Louis Holbrook +.. pgp:: 0826EDA1702D1E87C6E2875121D2E7BB88C2A746 + +""" + +# SPDX-License-Identifier: GPL-3.0-or-later + +# standard imports +import sys +import os +import json +import argparse +import logging + +# 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 +from chainlib.eth.tx import count +from chainlib.eth.erc20 import ERC20 +from chainlib.eth.connection import EthHTTPConnection +from chainlib.eth.gas import ( + OverrideGasOracle, + balance, + price, + ) +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('-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('-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 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'])) + + +#if holder_address != None: +# passphrase_env = 'ETH_PASSPHRASE' +# if args.env_prefix != None: +# passphrase_env = args.env_prefix + '_' + passphrase_env +# passphrase = os.environ.get(passphrase_env) +# logg.error('pass {}'.format(passphrase_env)) +# if passphrase == None: +# logg.warning('no passphrase given') +# passphrase='' +# +# holder_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) +gas_oracle = OverrideGasOracle(conn) + +token_symbol = 'eth' + +chain_spec = ChainSpec.from_chain_str(args.i) + +human = args.human + + +def main(): + o = block_latest() + r = conn.do(o) + n = int(r, 16) + if human: + n = format(n, ',') + sys.stdout.write('Block: {}\n'.format(n)) + + o = price() + r = conn.do(o) + n = int(r, 16) + if human: + n = format(n, ',') + sys.stdout.write('Gasprice: {}\n'.format(n)) + + if holder_address != None: + o = count(holder_address) + r = conn.do(o) + n = int(r, 16) + sys.stdout.write('Address: {}\n'.format(holder_address)) + sys.stdout.write('Nonce: {}\n'.format(n)) + + +if __name__ == '__main__': + main() diff --git a/chainlib/eth/tx.py b/chainlib/eth/tx.py index 4035f1a..13b317a 100644 --- a/chainlib/eth/tx.py +++ b/chainlib/eth/tx.py @@ -1,6 +1,7 @@ # standard imports import logging import enum +import re # external imports import coincurve @@ -270,20 +271,56 @@ class TxFactory: class Tx: + re_camel_snake = re.compile(r'([a-z0-9]+)([A-Z])') + + # 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: - self.index = int(strip_0x(src['transactionIndex']), 16) - self.value = int(strip_0x(src['value']), 16) - self.nonce = int(strip_0x(src['nonce']), 16) - self.hash = strip_0x(src['hash']) + 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']) - self.gasPrice = int(strip_0x(src['gasPrice']), 16) - self.gasLimit = int(strip_0x(src['gas']), 16) + 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 - inpt = src['input'] + try: + inpt = src['input'] + except KeyError: + inpt = src['data'] + if inpt != '0x': inpt = strip_0x(inpt) else: @@ -308,10 +345,33 @@ class Tx: if rcpt != None: self.apply_receipt(rcpt) - + + + @classmethod + def src_normalize(self, src): + src_normal = {} + for k in src.keys(): + s = '' + right_pos = 0 + for m in self.re_camel_snake.finditer(k): + g = m.group(0) + s += g[:len(g)-1] + s += '_' + g[len(g)-1].lower() + right_pos = m.span()[1] + + + s += k[right_pos:] + src_normal[k] = src[k] + if s != k: + logg.debug('adding snake {} for camel {}'.format(s, k)) + src_normal[s] = src[k] + + return src_normal + def apply_receipt(self, rcpt): - status_number = int(strip_0x(rcpt['status'])) + logg.debug('rcpt {}'.format(rcpt)) + status_number = int(rcpt['status'], 16) if status_number == 1: self.status = Status.SUCCESS elif status_number == 0: @@ -323,6 +383,7 @@ class Tx: if contract_address != None: self.contract = contract_address self.logs = rcpt['logs'] + self.gas_used = int(rcpt['gasUsed'], 16) def __repr__(self): @@ -338,19 +399,25 @@ nonce {} gasPrice {} gasLimit {} input {} -status {} """.format( self.hash, self.outputs[0], self.inputs[0], self.value, self.nonce, - self.gasPrice, - self.gasLimit, + self.gas_price, + self.gas_limit, self.payload, - self.status.name, ) + if self.status != Status.PENDING: + s += """gasUsed {} +""".format( + self.gas_used, + ) + + s += 'status ' + self.status.name + if self.contract != None: s += """contract {} """.format( diff --git a/chainlib/eth/unittest/base.py b/chainlib/eth/unittest/base.py index 97314eb..6d4f9e4 100644 --- a/chainlib/eth/unittest/base.py +++ b/chainlib/eth/unittest/base.py @@ -80,6 +80,11 @@ class TestRPCConnection(RPCConnection): 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') diff --git a/chainlib/stat.py b/chainlib/stat.py new file mode 100644 index 0000000..3cd008a --- /dev/null +++ b/chainlib/stat.py @@ -0,0 +1,30 @@ +import datetime + +class ChainStat: + + def __init__(self): + self.block_timestamp_last = None + self.block_avg_aggregate = None + self.block_avg_count = -1 + + + def block_apply(self, block): + if self.block_timestamp_last == None: + self.block_timestamp_last = block.timestamp + + aggregate = block.timestamp - self.block_timestamp_last + + if self.block_avg_aggregate == None: + self.block_avg_aggregate = float(aggregate) + else: + self.block_avg_aggregate *= self.block_avg_count + self.block_avg_aggregate += block.timestamp - self.block_timestamp_last + self.block_avg_aggregate /= (self.block_avg_count + 1) + + print('aggr {}'.format(type(self.block_avg_aggregate))) + self.block_avg_count += 1 + + self.block_timestamp_last = block.timestamp + + def block_average(self): + return self.block_avg_aggregate diff --git a/setup.cfg b/setup.cfg index 3404edc..d94abf6 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = chainlib -version = 0.0.2a1 +version = 0.0.2a6 description = Generic blockchain access library and tooling author = Louis Holbrook author_email = dev@holbrook.no @@ -41,3 +41,5 @@ console_scripts = eth-transfer = chainlib.eth.runnable.transfer: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 diff --git a/tests/test_stat.py b/tests/test_stat.py new file mode 100644 index 0000000..17905a4 --- /dev/null +++ b/tests/test_stat.py @@ -0,0 +1,49 @@ +# standard imports +import unittest +import datetime + +# external imports +from chainlib.stat import Stat +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()