commit ec07317dffa1ffafc1887c7a139b04adc69ebc9f
parent 6cbb86ca16ba075a2b041549288d78bb830163e7
Author: Louis Holbrook <accounts-gitlab@holbrook.no>
Date: Thu, 8 Apr 2021 15:39:32 +0000
Add chain stat, info cli
Diffstat:
11 files changed, 316 insertions(+), 20 deletions(-)
diff --git 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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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()
diff --git 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
@@ -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
@@ -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
@@ -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
@@ -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()