Merge branch 'lash/chain-stat' into 'master'

Add chain stat, info cli

See merge request nolash/chainlib!2
This commit is contained in:
Louis Holbrook 2021-04-08 15:39:33 +00:00
commit 72f49841f8
11 changed files with 316 additions and 20 deletions

View File

@ -42,10 +42,16 @@ class Block:
def __init__(self, src): def __init__(self, src):
self.hash = src['hash'] self.hash = src['hash']
try:
self.number = int(strip_0x(src['number']), 16) self.number = int(strip_0x(src['number']), 16)
except TypeError:
self.number = int(src['number'])
self.txs = src['transactions'] self.txs = src['transactions']
self.block_src = src self.block_src = src
try:
self.timestamp = int(strip_0x(src['timestamp']), 16) self.timestamp = int(strip_0x(src['timestamp']), 16)
except TypeError:
self.timestamp = int(src['timestamp'])
def src(self): def src(self):

View File

@ -36,6 +36,7 @@ from chainlib.eth.gas import (
OverrideGasOracle, OverrideGasOracle,
balance, balance,
) )
from chainlib.chain import ChainSpec
logging.basicConfig(level=logging.WARNING) logging.basicConfig(level=logging.WARNING)
logg = logging.getLogger() 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('-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('-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('-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('-v', action='store_true', help='Be verbose')
argparser.add_argument('-vv', action='store_true', help='Be more verbose') argparser.add_argument('-vv', action='store_true', help='Be more verbose')
argparser.add_argument('address', type=str, help='Account address') argparser.add_argument('address', type=str, help='Account address')

View File

@ -2,7 +2,7 @@
import sys import sys
# local imports # 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]))

View File

@ -11,6 +11,7 @@ import logging
from chainlib.eth.address import to_checksum from chainlib.eth.address import to_checksum
from chainlib.eth.connection import EthHTTPConnection from chainlib.eth.connection import EthHTTPConnection
from chainlib.eth.tx import count from chainlib.eth.tx import count
from chainlib.chain import ChainSpec
from crypto_dev_signer.keystore.dict import DictKeystore from crypto_dev_signer.keystore.dict import DictKeystore
from crypto_dev_signer.eth.signer import ReferenceSigner as EIP155Signer from crypto_dev_signer.eth.signer import ReferenceSigner as EIP155Signer

View File

@ -34,6 +34,7 @@ from chainlib.jsonrpc import (
from chainlib.eth.connection import EthHTTPConnection from chainlib.eth.connection import EthHTTPConnection
from chainlib.eth.tx import Tx from chainlib.eth.tx import Tx
from chainlib.eth.block import Block from chainlib.eth.block import Block
from chainlib.chain import ChainSpec
logging.basicConfig(level=logging.WARNING) logging.basicConfig(level=logging.WARNING)
logg = logging.getLogger() 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('-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('-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('-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('-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('--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('-v', action='store_true', help='Be verbose')

View File

@ -0,0 +1,136 @@
#!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
# 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()

View File

@ -1,6 +1,7 @@
# standard imports # standard imports
import logging import logging
import enum import enum
import re
# external imports # external imports
import coincurve import coincurve
@ -270,20 +271,56 @@ class TxFactory:
class Tx: 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): def __init__(self, src, block=None, rcpt=None):
logg.debug('src {}'.format(src))
self.src = self.src_normalize(src)
self.index = -1 self.index = -1
tx_hash = add_0x(src['hash'])
if block != None: if block != None:
self.index = int(strip_0x(src['transactionIndex']), 16) 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) self.value = int(strip_0x(src['value']), 16)
except TypeError:
self.value = int(src['value'])
try:
self.nonce = int(strip_0x(src['nonce']), 16) self.nonce = int(strip_0x(src['nonce']), 16)
self.hash = strip_0x(src['hash']) except TypeError:
self.nonce = int(src['nonce'])
address_from = strip_0x(src['from']) address_from = strip_0x(src['from'])
self.gasPrice = int(strip_0x(src['gasPrice']), 16) try:
self.gasLimit = int(strip_0x(src['gas']), 16) 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.outputs = [to_checksum(address_from)]
self.contract = None self.contract = None
try:
inpt = src['input'] inpt = src['input']
except KeyError:
inpt = src['data']
if inpt != '0x': if inpt != '0x':
inpt = strip_0x(inpt) inpt = strip_0x(inpt)
else: else:
@ -310,8 +347,31 @@ class Tx:
self.apply_receipt(rcpt) 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): 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: if status_number == 1:
self.status = Status.SUCCESS self.status = Status.SUCCESS
elif status_number == 0: elif status_number == 0:
@ -323,6 +383,7 @@ class Tx:
if contract_address != None: if contract_address != None:
self.contract = contract_address self.contract = contract_address
self.logs = rcpt['logs'] self.logs = rcpt['logs']
self.gas_used = int(rcpt['gasUsed'], 16)
def __repr__(self): def __repr__(self):
@ -338,19 +399,25 @@ nonce {}
gasPrice {} gasPrice {}
gasLimit {} gasLimit {}
input {} input {}
status {}
""".format( """.format(
self.hash, self.hash,
self.outputs[0], self.outputs[0],
self.inputs[0], self.inputs[0],
self.value, self.value,
self.nonce, self.nonce,
self.gasPrice, self.gas_price,
self.gasLimit, self.gas_limit,
self.payload, 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: if self.contract != None:
s += """contract {} s += """contract {}
""".format( """.format(

View File

@ -80,6 +80,11 @@ class TestRPCConnection(RPCConnection):
return jsonrpc_result(r, error_parser) 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): def eth_getBlockByNumber(self, p):
b = bytes.fromhex(strip_0x(p[0])) b = bytes.fromhex(strip_0x(p[0]))
n = int.from_bytes(b, 'big') n = int.from_bytes(b, 'big')

30
chainlib/stat.py Normal file
View File

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

View File

@ -1,6 +1,6 @@
[metadata] [metadata]
name = chainlib name = chainlib
version = 0.0.2a1 version = 0.0.2a6
description = Generic blockchain access library and tooling description = Generic blockchain access library and tooling
author = Louis Holbrook author = Louis Holbrook
author_email = dev@holbrook.no author_email = dev@holbrook.no
@ -41,3 +41,5 @@ console_scripts =
eth-transfer = chainlib.eth.runnable.transfer:main eth-transfer = chainlib.eth.runnable.transfer:main
eth-get = chainlib.eth.runnable.get:main eth-get = chainlib.eth.runnable.get:main
eth-decode = chainlib.eth.runnable.decode:main eth-decode = chainlib.eth.runnable.decode:main
eth-info = chainlib.eth.runnable.info:main
eth = chainlib.eth.runnable.info:main

49
tests/test_stat.py Normal file
View File

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