Add docstrings

This commit is contained in:
Louis Holbrook 2021-08-21 07:27:40 +00:00
parent 8374d83045
commit d13708d3ce
33 changed files with 1333 additions and 585 deletions

View File

@ -1,2 +1,3 @@
- 0.0.5-pending
* Receive all ethereum components from chainlib package
* Make settings configurable

View File

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

View File

@ -1,4 +1,4 @@
# third-party imports
# external imports
import sha3
from hexathon import (
strip_0x,
@ -11,3 +11,34 @@ from crypto_dev_signer.encoding import (
)
to_checksum = to_checksum_address
class AddressChecksum:
"""Address checksummer implementation.
Primarily for use with chainlib.cli.wallet.Wallet
"""
@classmethod
def valid(cls, v):
"""Check if address is a valid checksum address
:param v: Address value, in hex
:type v: str
:rtype: bool
:returns: True if valid checksum
"""
return is_checksum_address(v)
@classmethod
def sum(cls, v):
"""Create checksum from address
:param v: Address value, in hex
:type v: str
:raises ValueError: Invalid address
:rtype: str
:returns: Checksum address
"""
return to_checksum_address(v)

View File

@ -1,14 +1,19 @@
# third-party imports
# external imports
from chainlib.jsonrpc import JSONRPCRequest
from chainlib.eth.tx import Tx
from chainlib.block import Block as BaseBlock
from hexathon import (
add_0x,
strip_0x,
even,
)
# local imports
from chainlib.eth.tx import Tx
def block_latest(id_generator=None):
"""Implements chainlib.interface.ChainInterface method
"""
j = JSONRPCRequest(id_generator)
o = j.template()
o['method'] = 'eth_blockNumber'
@ -16,6 +21,8 @@ def block_latest(id_generator=None):
def block_by_hash(hsh, include_tx=True, id_generator=None):
"""Implements chainlib.interface.ChainInterface method
"""
j = JSONRPCRequest(id_generator)
o = j.template()
o['method'] = 'eth_getBlockByHash'
@ -25,6 +32,8 @@ def block_by_hash(hsh, include_tx=True, id_generator=None):
def block_by_number(n, include_tx=True, id_generator=None):
"""Implements chainlib.interface.ChainInterface method
"""
nhx = add_0x(even(hex(n)[2:]))
j = JSONRPCRequest(id_generator)
o = j.template()
@ -35,6 +44,15 @@ def block_by_number(n, include_tx=True, id_generator=None):
def transaction_count(block_hash, id_generator=None):
"""Generate json-rpc query to get transaction count of block
:param block_hash: Block hash, in hex
:type block_hash: str
:param id_generator: JSONRPC id generator
:type id_generator: JSONRPCIdGenerator
:rtype: dict
:returns: rpc query object
"""
j = JSONRPCRequest(id_generator)
o = j.template()
o['method'] = 'eth_getBlockTransactionCountByHash'
@ -42,7 +60,13 @@ def transaction_count(block_hash, id_generator=None):
return j.finalize(o)
class Block:
class Block(BaseBlock):
"""Encapsulates an Ethereum block
:param src: Block representation data
:type src: dict
:todo: Add hex to number parse to normalize
"""
def __init__(self, src):
self.hash = src['hash']
@ -58,22 +82,21 @@ class Block:
self.timestamp = int(src['timestamp'])
def src(self):
return self.block_src
def get_tx(self, tx_hash):
i = 0
idx = -1
tx_hash = add_0x(tx_hash)
for tx in self.txs:
tx_hash_block = None
try:
tx_hash_block = add_0x(tx['hash'])
except TypeError:
tx_hash_block = add_0x(tx)
if tx_hash_block == tx_hash:
idx = i
break
i += 1
if idx == -1:
raise AttributeError('tx {} not found in block {}'.format(tx_hash, self.hash))
return idx
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))
@staticmethod
def from_src(src):
return Block(src)

View File

@ -2,6 +2,13 @@ from chainlib.jsonrpc import JSONRPCRequest
def network_id(id_generator=None):
"""Generate json-rpc query to retrieve network id from node
:param id_generator: JSON-RPC id generator
:type id_generator: JSONRPCIdGenerator
:rtype: dict
:returns: rpc query object
"""
j = JSONRPCRequest(id_generator=id_generator)
o = j.template()
o['method'] = 'net_version'

44
chainlib/eth/cli.py Normal file
View File

@ -0,0 +1,44 @@
# standard imports
import os
# external imports
from chainlib.cli import (
ArgumentParser,
argflag_std_read,
argflag_std_write,
argflag_std_base,
Config as BaseConfig,
Wallet as BaseWallet,
Rpc as BaseRpc,
Flag,
)
from crypto_dev_signer.eth.signer import ReferenceSigner as EIP155Signer
# local imports
from chainlib.eth.address import AddressChecksum
from chainlib.eth.connection import EthHTTPConnection
script_dir = os.path.dirname(os.path.realpath(__file__))
class Wallet(BaseWallet):
"""Convenience constructor to set Ethereum defaults for chainlib cli Wallet object
:param checksummer: Address checksummer object
:type checksummer: Implementation of chainlib.eth.address.AddressChecksum
"""
def __init__(self, checksummer=AddressChecksum):
super(Wallet, self).__init__(EIP155Signer, checksummer=checksummer)
class Rpc(BaseRpc):
"""Convenience constructor to set Ethereum defaults for chainlib cli Rpc object
"""
def __init__(self, wallet=None):
super(Rpc, self).__init__(EthHTTPConnection, wallet=wallet)
class Config(BaseConfig):
"""Convenience constructor to set Ethereum defaults for the chainlib cli config object
"""
default_base_config_dir = os.path.join(script_dir, 'data', 'config')
default_fee_limit = 21000

View File

@ -43,8 +43,33 @@ logg = logging.getLogger(__name__)
class EthHTTPConnection(JSONRPCHTTPConnection):
"""HTTP Interface for Ethereum node JSON-RPC
:todo: support https
"""
def wait(self, tx_hash_hex, delay=0.5, timeout=0.0, error_parser=error_parser, id_generator=None):
"""Poll for confirmation of a transaction on network.
Returns the result of the transaction if it was successfully executed on the network, and raises RevertEthException if execution fails.
This is a blocking call.
:param tx_hash_hex: Transaction hash to wait for, hex
:type tx_hash_hex: str
:param delay: Polling interval
:type delay: float
:param timeout: Max time to wait for confirmation (0 = no timeout)
:type timeout: float
:param error_parser: json-rpc response error parser
:type error_parser: chainlib.jsonrpc.ErrorParser
:param id_generator: json-rpc id generator
:type id_generator: chainlib.jsonrpc.JSONRPCIdGenerator
:raises TimeoutError: Timeout reached
:raises chainlib.eth.error.RevertEthException: Transaction confirmed but failed
:rtype: dict
:returns: Transaction receipt
"""
t = datetime.datetime.utcnow()
i = 0
while True:
@ -59,13 +84,13 @@ class EthHTTPConnection(JSONRPCHTTPConnection):
)
req.add_header('Content-Type', 'application/json')
data = json.dumps(o)
logg.debug('(HTTP) poll receipt attempt {} {}'.format(i, data))
logg.debug('({}) poll receipt attempt {} {}'.format(str(self), 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('({}) poll receipt completed {}'.format(str(self), r))
logg.debug('e {}'.format(strip_0x(e['status'])))
if strip_0x(e['status']) == '00':
raise RevertEthException(tx_hash_hex)
@ -80,7 +105,17 @@ class EthHTTPConnection(JSONRPCHTTPConnection):
i += 1
def __str__(self):
return 'ETH HTTP JSONRPC'
def check_rpc(self, id_generator=None):
"""Execute Ethereum specific json-rpc query to (superficially) check whether node is sane.
:param id_generator: json-rpc id generator
:type id_generator: chainlib.jsonrpc.JSONRPCIdGenerator
:raises Exception: Any exception indicates an invalid node
"""
j = JSONRPCRequest(id_generator)
req = j.template()
req['method'] = 'net_version'
@ -89,12 +124,29 @@ class EthHTTPConnection(JSONRPCHTTPConnection):
class EthUnixConnection(JSONRPCUnixConnection):
"""Unix socket implementation of Ethereum JSON-RPC
"""
def wait(self, tx_hash_hex, delay=0.5, timeout=0.0, error_parser=error_parser):
"""See EthHTTPConnection. Not yet implemented for unix socket.
"""
raise NotImplementedError('Not yet implemented for unix socket')
def sign_transaction_to_rlp(chain_spec, doer, tx):
"""Generate a signature query and execute it against a json-rpc signer backend.
Uses the `eth_signTransaction` json-rpc method, generated by chainlib.eth.sign.sign_transaction.
:param chain_spec: Chain spec to use for EIP155 signature.
:type chain_spec: chainlib.chain.ChainSpec
:param doer: Signer rpc backend
:type doer: chainlib.connection.RPCConnection implementing json-rpc
:param tx: Transaction object
:type tx: dict
:rtype: bytes
:returns: Ethereum signature
"""
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
@ -110,27 +162,66 @@ def sign_transaction_to_rlp(chain_spec, doer, tx):
def sign_message(doer, msg):
"""Sign arbitrary data using the Ethereum message signer protocol.
:param doer: Signer rpc backend
:type doer: chainlib.connection.RPCConnection with json-rpc
:param msg: Message to sign, in hex
:type msg: str
:rtype: str
:returns: Signature, hex
"""
o = sign_message(msg)
return doer(o)
class EthUnixSignerConnection(EthUnixConnection):
"""Connects rpc signer methods to Unix socket connection interface
"""
def sign_transaction_to_rlp(self, tx):
"""Sign transaction using unix socket rpc.
:param tx: Transaction object
:type tx: dict
:rtype: See chainlin.eth.connection.sign_transaction_to_rlp
:returns: See chainlin.eth.connection.sign_transaction_to_rlp
"""
return sign_transaction_to_rlp(self.chain_spec, self.do, tx)
def sign_message(self, tx):
return sign_message(self.do, tx)
def sign_message(self, msg):
"""Sign message using unix socket json-rpc.
:param msg: Message to sign, in hex
:type msg: str
:rtype: See chainlin.eth.connection.sign_message
:returns: See chainlin.eth.connection.sign_message
"""
return sign_message(self.do, msg)
class EthHTTPSignerConnection(EthHTTPConnection):
def sign_transaction_to_rlp(self, tx):
"""Sign transaction using http json-rpc.
:param tx: Transaction object
:type tx: dict
:rtype: See chainlin.eth.connection.sign_transaction_to_rlp
:returns: See chainlin.eth.connection.sign_transaction_to_rlp
"""
return sign_transaction_to_rlp(self.chain_spec, self.do, tx)
def sign_message(self, tx):
"""Sign message using http json-rpc.
:param msg: Message to sign, in hex
:type msg: str
:rtype: See chainlin.eth.connection.sign_message
:returns: See chainlin.eth.connection.sign_message
"""
return sign_message(self.do, tx)

View File

@ -2,4 +2,5 @@ ZERO_ADDRESS = '0x{:040x}'.format(0)
ZERO_CONTENT = '0x{:064x}'.format(0)
MINIMUM_FEE_UNITS = 21000
MINIMUM_FEE_PRICE = 1000000000
DEFAULT_FEE_LIMIT = 8000000
MAX_UINT = int('0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff', 16)

View File

@ -15,14 +15,14 @@ from chainlib.block import BlockSpec
from chainlib.jsonrpc import JSONRPCRequest
from .address import to_checksum_address
#logg = logging.getLogger(__name__)
logg = logging.getLogger()
logg = logging.getLogger(__name__)
re_method = r'^[a-zA-Z0-9_]+$'
class ABIContractType(enum.Enum):
"""Data types used by ABI encoders
"""
BYTES32 = 'bytes32'
BYTES4 = 'bytes4'
UINT256 = 'uint256'
@ -36,14 +36,16 @@ dynamic_contract_types = [
class ABIContract:
"""Base class for Ethereum smart contract encoder
"""
def __init__(self):
self.types = []
self.contents = []
class ABIMethodEncoder(ABIContract):
"""Generate ABI method signatures from method signature string.
"""
def __init__(self):
super(ABIMethodEncoder, self).__init__()
self.method_name = None
@ -51,6 +53,12 @@ class ABIMethodEncoder(ABIContract):
def method(self, m):
"""Set method name.
:param m: Method name
:type m: str
:raises ValueError: Invalid method name
"""
if re.match(re_method, m) == None:
raise ValueError('Invalid method {}, must match regular expression {}'.format(re_method))
self.method_name = m
@ -58,12 +66,26 @@ class ABIMethodEncoder(ABIContract):
def get_method(self):
"""Return currently set method signature string.
:rtype: str
:returns: Method signature
"""
if self.method_name == None:
return ''
return '{}({})'.format(self.method_name, ','.join(self.method_contents))
def typ(self, v):
"""Add argument type to argument vector.
Method name must be set before this is called.
:param v: Type to add
:type v: chainlib.eth.contract.ABIContractType
:raises AttributeError: Type set before method name
:raises TypeError: Invalid type
"""
if self.method_name == None:
raise AttributeError('method name must be set before adding types')
if not isinstance(v, ABIContractType):
@ -78,9 +100,16 @@ class ABIMethodEncoder(ABIContract):
class ABIContractDecoder(ABIContract):
"""Decode serialized ABI contract input data to corresponding python primitives.
"""
def typ(self, v):
"""Add type to argument array to parse input against.
:param v: Type
:type v: chainlib.eth.contract.ABIContractType
:raises TypeError: Invalid type
"""
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)
@ -88,32 +117,74 @@ class ABIContractDecoder(ABIContract):
def val(self, v):
"""Add value to value array.
:param v: Value, in hex
:type v: str
"""
self.contents.append(v)
logg.debug('content is now {}'.format(self.contents))
def uint256(self, v):
"""Parse value as uint256.
:param v: Value, in hex
:type v: str
:rtype: int
:returns: Int value
"""
return int(v, 16)
def bytes32(self, v):
"""Parse value as bytes32.
:param v: Value, in hex
:type v: str
:rtype: str
:returns: Value, in hex
"""
return v
def bool(self, v):
"""Parse value as bool.
:param v: Value, in hex
:type v: str
:rtype: bool
:returns: Value
"""
return bool(self.uint256(v))
def boolean(self, v):
"""Alias of chainlib.eth.contract.ABIContractDecoder.bool
"""
return bool(self.uint256(v))
def address(self, v):
"""Parse value as address.
:param v: Value, in hex
:type v: str
:rtype: str
:returns: Value. in hex
"""
a = strip_0x(v)[64-40:]
return to_checksum_address(a)
def string(self, v):
"""Parse value as string.
:param v: Value, in hex
:type v: str
:rtype: str
:returns: Value
"""
s = strip_0x(v)
b = bytes.fromhex(s)
cursor = 0
@ -131,18 +202,23 @@ class ABIContractDecoder(ABIContract):
def decode(self):
"""Apply decoder on value array using argument type array.
:rtype: list
:returns: List of decoded values
"""
r = []
logg.debug('contents {}'.format(self.contents))
for i in range(len(self.types)):
m = getattr(self, self.types[i])
s = self.contents[i]
logg.debug('{} {} {} {} {}'.format(i, m, self.types[i], self.contents[i], s))
#r.append(m(s.hex()))
r.append(m(s))
return r
def get(self):
"""Alias of chainlib.eth.contract.ABIContractDecoder.decode
"""
return self.decode()
@ -151,7 +227,10 @@ class ABIContractDecoder(ABIContract):
class ABIContractLogDecoder(ABIMethodEncoder, ABIContractDecoder):
"""Decoder utils for log entries of an Ethereum network transaction receipt.
Uses chainlib.eth.contract.ABIContractDecoder.decode to render output from template.
"""
def __init__(self):
super(ABIContractLogDecoder, self).__init__()
self.method_name = None
@ -159,20 +238,45 @@ class ABIContractLogDecoder(ABIMethodEncoder, ABIContractDecoder):
def topic(self, event):
"""Set topic to match.
:param event: Topic name
:type event: str
"""
self.method(event)
def get_method_signature(self):
"""Generate topic signature from set topic.
:rtype: str
:returns: Topic signature, in hex
"""
s = self.get_method()
return keccak256_string_to_hex(s)
def typ(self, v):
"""Add type to event argument array.
:param v: Type
:type v: chainlib.eth.contract.ABIContractType
"""
super(ABIContractLogDecoder, self).typ(v)
self.types.append(v.value)
def apply(self, topics, data):
"""Set log entry data to parse.
After set, self.decode can be used to render the output.
:param topics: The topics array of the receipt, list of hex
:type topics: list
:param data: Non-indexed data, in hex
:type data: str
:raises ValueError: Topic of input does not match topic set in parser
"""
t = self.get_method_signature()
if topics[0] != t:
raise ValueError('topic mismatch')
@ -189,6 +293,11 @@ class ABIContractEncoder(ABIMethodEncoder):
def uint256(self, v):
"""Encode value to uint256 and add to input value vector.
:param v: Integer value
:type v: int
"""
v = int(v)
b = v.to_bytes(32, 'big')
self.contents.append(b.hex())
@ -197,28 +306,52 @@ class ABIContractEncoder(ABIMethodEncoder):
def bool(self, v):
"""Alias of chainlib.eth.contract.ABIContractEncoder.boolean.
"""
return self.boolean(v)
def boolean(self, v):
"""Encode value to boolean and add to input value vector.
:param v: Trueish or falsish value
:type v: any
:rtype: See chainlib.eth.contract.ABIContractEncoder.uint256
:returns: See chainlib.eth.contract.ABIContractEncoder.uint256
"""
if bool(v):
return self.uint256(1)
return self.uint256(0)
def address(self, v):
"""Encode value to address and add to input value vector.
:param v: Ethereum address, in hex
:type v: str
"""
self.bytes_fixed(32, v, 20)
self.types.append(ABIContractType.ADDRESS)
self.__log_latest(v)
def bytes32(self, v):
"""Encode value to bytes32 and add to input value vector.
:param v: Bytes, in hex
:type v: str
"""
self.bytes_fixed(32, v)
self.types.append(ABIContractType.BYTES32)
self.__log_latest(v)
def bytes4(self, v):
"""Encode value to bytes4 and add to input value vector.
:param v: Bytes, in hex
:type v: str
"""
self.bytes_fixed(4, v)
self.types.append(ABIContractType.BYTES4)
self.__log_latest(v)
@ -226,6 +359,11 @@ class ABIContractEncoder(ABIMethodEncoder):
def string(self, v):
"""Encode value to string and add to input value vector.
:param v: String input
:type v: str
"""
b = v.encode('utf-8')
l = len(b)
contents = l.to_bytes(32, 'big')
@ -239,6 +377,16 @@ class ABIContractEncoder(ABIMethodEncoder):
def bytes_fixed(self, mx, v, exact=0):
"""Add arbirary length byte data to value vector.
:param mx: Max length of input data.
:type mx: int
:param v: Byte input, hex or bytes
:type v: str | bytes
:param exact: Fail parsing if input does not translate to given byte length.
:type exact: int
:raises ValueError: Input length or input format mismatch.
"""
typ = type(v).__name__
if typ == 'str':
v = strip_0x(v)
@ -259,9 +407,10 @@ class ABIContractEncoder(ABIMethodEncoder):
raise ValueError('invalid input {}'.format(typ))
self.contents.append(v.ljust(64, '0'))
def get_method_signature(self):
"""Return abi encoded signature of currently set method.
"""
s = self.get_method()
if s == '':
return s
@ -269,6 +418,11 @@ class ABIContractEncoder(ABIMethodEncoder):
def get_contents(self):
"""Encode value array.
:rtype: str
:returns: ABI encoded values, in hex
"""
direct_contents = ''
pointer_contents = ''
l = len(self.types)
@ -291,10 +445,19 @@ class ABIContractEncoder(ABIMethodEncoder):
def get(self):
"""Alias of chainlib.eth.contract.ABIContractEncoder.encode
"""
return self.encode()
def encode(self):
"""Encode method and value array.
The data generated by this method is the literal data used as input to contract calls or transactions.
:rtype: str
:returns: ABI encoded contract input data, in hex
"""
m = self.get_method_signature()
c = self.get_contents()
return m + c
@ -306,6 +469,13 @@ class ABIContractEncoder(ABIMethodEncoder):
def abi_decode_single(typ, v):
"""Convenience function to decode a single ABI encoded value against a given type.
:param typ: Type to parse value as
:type typ: chainlib.eth.contract.ABIContractEncoder
:param v: Value to parse, in hex
:type v: str
"""
d = ABIContractDecoder()
d.typ(typ)
d.val(v)
@ -314,6 +484,17 @@ def abi_decode_single(typ, v):
def code(address, block_spec=BlockSpec.LATEST, id_generator=None):
"""Generate json-rpc query to retrieve code stored at an Ethereum address.
:param address: Address to use for query, in hex
:type address: str
:param block_spec: Block height spec
:type block_spec: chainlib.block.BlockSpec
:param id_generator: json-rpc id generator
:type id_generator: chainlib.jsonrpc.JSONRPCIdGenerator
:rtype: dict
:returns: rpc query object
"""
block_height = None
if block_spec == BlockSpec.LATEST:
block_height = 'latest'

View File

@ -0,0 +1,12 @@
[rpc]
http_provider = http://localhost:8545
http_authentication =
http_username =
http_password =
[chain]
spec = evm:ethereum:1
[wallet]
key_file =
passphrase =

View File

@ -1,23 +1,33 @@
# local imports
from chainlib.error import ExecutionError
class EthException(Exception):
"""Base class for all Ethereum related errors.
"""
pass
class RevertEthException(EthException, ExecutionError):
"""Raised when an rpc call or transaction reverts.
"""
pass
class NotFoundEthException(EthException):
"""Raised when rpc query is made against an identifier that is not known by the node.
"""
pass
class RequestMismatchException(EthException):
"""Raised when a request data parser is given unexpected input data.
"""
pass
class DefaultErrorParser:
"""Generate eth specific exception for the default json-rpc query error parser.
"""
def translate(self, error):
return EthException('default parser code {}'.format(error))

View File

@ -1,7 +1,7 @@
# standard imports
import logging
# third-party imports
# external imports
from hexathon import (
add_0x,
strip_0x,
@ -16,6 +16,8 @@ from chainlib.eth.tx import (
TxFormat,
raw,
)
from chainlib.eth.jsonrpc import to_blockheight_param
from chainlib.block import BlockSpec
from chainlib.eth.constant import (
MINIMUM_FEE_UNITS,
)
@ -24,22 +26,48 @@ logg = logging.getLogger(__name__)
def price(id_generator=None):
"""Generate json-rpc query to retrieve current network gas price guess from node.
:param id_generator: json-rpc id generator
:type id_generator: chainlib.connection.JSONRPCIdGenerator
:rtype: dict
:returns: rpc query object
"""
j = JSONRPCRequest(id_generator)
o = j.template()
o['method'] = 'eth_gasPrice'
return j.finalize(o)
def balance(address, id_generator=None):
def balance(address, id_generator=None, height=BlockSpec.LATEST):
"""Generate json-rpc query to retrieve gas balance of address.
:param address: Address to query balance for, in hex
:type address: str
:param id_generator: json-rpc id generator
:type id_generator: chainlib.connection.JSONRPCIdGenerator
:param height: Block height specifier
:type height: chainlib.block.BlockSpec
:rtype: dict
:returns: rpc query object
"""
j = JSONRPCRequest(id_generator)
o = j.template()
o['method'] = 'eth_getBalance'
o['params'].append(address)
o['params'].append('latest')
height = to_blockheight_param(height)
o['params'].append(height)
return j.finalize(o)
def parse_balance(balance):
"""Parse result of chainlib.eth.gas.balance rpc query
:param balance: rpc result value, in hex or int
:type balance: any
:rtype: int
:returns: Balance integer value
"""
try:
r = int(balance, 10)
except ValueError:
@ -48,10 +76,29 @@ def parse_balance(balance):
class Gas(TxFactory):
"""Gas transaction helper.
"""
def create(self, sender_address, recipient_address, value, tx_format=TxFormat.JSONRPC, id_generator=None):
def create(self, sender_address, recipient_address, value, data=None, tx_format=TxFormat.JSONRPC, id_generator=None):
"""Generate json-rpc query to execute gas transaction.
See parent class TxFactory for details on output format and general usage.
:param sender_address: Sender address, in hex
:type sender_address: str
:param recipient_address: Recipient address, in hex
:type recipient_address: str
:param value: Value of transaction, integer decimal value (wei)
:type value: int
:param data: Arbitrary input data, in hex. None means no data (vanilla gas transaction).
:type data: str
:param tx_format: Output format
:type tx_format: chainlib.eth.tx.TxFormat
"""
tx = self.template(sender_address, recipient_address, use_nonce=True)
tx['value'] = value
if data != None:
tx['data'] = data
txe = EIP155Transaction(tx, tx['nonce'], tx['chainId'])
tx_raw = self.signer.sign_transaction_to_rlp(txe)
tx_raw_hex = add_0x(tx_raw.hex())
@ -68,6 +115,17 @@ class Gas(TxFactory):
class RPCGasOracle:
"""JSON-RPC only gas parameter helper.
:param conn: RPC connection
:type conn: chainlib.connection.RPCConnection
:param code_callback: Callback method to evaluate gas usage for method and inputs.
:type code_callback: method taking abi encoded input data as single argument
:param min_price: Override gas price if less than given value
:type min_price: int
:param id_generator: json-rpc id generator
:type id_generator: chainlib.connection.JSONRPCIdGenerator
"""
def __init__(self, conn, code_callback=None, min_price=1, id_generator=None):
self.conn = conn
@ -76,7 +134,20 @@ class RPCGasOracle:
self.id_generator = id_generator
def get_gas(self, code=None):
def get_gas(self, code=None, input_data=None):
"""Retrieve gas parameters from node.
If code is given, the set code callback will be used to estimate gas usage.
If code is not given or code callback is not set, the chainlib.eth.constant.MINIMUM_FEE_UNITS constant will be used. This gas limit will only be enough gas for a gas transaction without input data.
:param code: EVM execution code to evaluate against, in hex
:type code: str
:param input_data: Contract input data, in hex
:type input_data: str
:rtype: tuple
:returns: Gas price in wei, and gas limit in gas units
"""
gas_price = 0
if self.conn != None:
o = price(id_generator=self.id_generator)
@ -93,13 +164,39 @@ class RPCGasOracle:
class RPCPureGasOracle(RPCGasOracle):
"""Convenience constructor for rpc gas oracle without minimum price.
:param conn: RPC connection
:type conn: chainlib.connection.RPCConnection
:param code_callback: Callback method to evaluate gas usage for method and inputs.
:type code_callback: method taking abi encoded input data as single argument
:param id_generator: json-rpc id generator
:type id_generator: chainlib.connection.JSONRPCIdGenerator
"""
def __init__(self, conn, code_callback=None, id_generator=None):
super(RPCPureGasOracle, self).__init__(conn, code_callback=code_callback, min_price=0, id_generator=id_generator)
class OverrideGasOracle(RPCGasOracle):
"""Gas parameter helper that can be selectively overridden.
If both price and limit are set, the conn parameter will not be used.
If either price or limit is set to None, the rpc in the conn value will be used to query the missing value.
If both are None, behaves the same as chainlib.eth.gas.RPCGasOracle.
:param price: Set exact gas price
:type price: int
:param limit: Set exact gas limit
:type limit: int
:param conn: RPC connection for fallback query
:type conn: chainlib.connection.RPCConnection
:param code_callback: Callback method to evaluate gas usage for method and inputs.
:type code_callback: method taking abi encoded input data as single argument
:param id_generator: json-rpc id generator
:type id_generator: chainlib.connection.JSONRPCIdGenerator
"""
def __init__(self, price=None, limit=None, conn=None, code_callback=None, id_generator=None):
self.conn = None
self.code_callback = None
@ -117,6 +214,8 @@ class OverrideGasOracle(RPCGasOracle):
def get_gas(self, code=None):
"""See chainlib.eth.gas.RPCGasOracle.
"""
r = None
fee_units = None
fee_price = None

View File

@ -14,3 +14,31 @@
#106 Timeout Should be used when an action timedout.
#107 Conflict Should be used when an action conflicts with another (ongoing?) action.
# external imports
from hexathon import add_0x
def to_blockheight_param(height):
"""Translate blockheight specifier to Ethereum json-rpc blockheight argument.
:param height: Height argument
:type height: any
:rtype: str
:returns: Argument value
"""
if height == None:
height = 'latest'
elif isinstance(height, str):
try:
height = int(height)
except ValueError:
pass
if isinstance(height, int):
if height == 0:
height = 'latest'
elif height < 0:
height = 'pending'
else:
height = add_0x(int(height).to_bytes(8, 'big').hex())
return height

View File

@ -3,12 +3,18 @@ import sha3
class LogBloom:
"""Helper for Ethereum receipt log bloom filters.
"""
def __init__(self):
self.content = bytearray(256)
def add(self, element):
"""Add topic element to filter.
:param element: Topic element
:type element: bytes
"""
if not isinstance(element, bytes):
raise ValueError('element must be bytes')
h = sha3.keccak_256()

View File

@ -1,4 +1,4 @@
# third-party imports
# external imports
from hexathon import (
add_0x,
strip_0x,
@ -8,17 +8,40 @@ from hexathon import (
from chainlib.jsonrpc import JSONRPCRequest
def nonce(address, id_generator=None):
def nonce(address, confirmed=False, id_generator=None):
"""Generate json-rpc query to retrieve next nonce of address from node.
:param address: Address to retrieve nonce for, in hex
:type address: str
:param id_generator: json-rpc id generator
:type id_generator: chainlib.connection.JSONRPCIdGenerator
:rtype: dict
:returns: rpc query object
"""
j = JSONRPCRequest(id_generator)
o = j.template()
o['method'] = 'eth_getTransactionCount'
o['params'].append(address)
o['params'].append('pending')
if confirmed:
o['params'].append('latest')
else:
o['params'].append('pending')
return j.finalize(o)
class NonceOracle:
def nonce_confirmed(address, id_generator=None):
return nonce(address, confirmed=True, id_generator=id_generator)
class NonceOracle:
"""Base class for the nonce parameter helpers.
:param address: Address to retireve nonce for, in hex
:type address: str
:param id_generator: json-rpc id generator
:type id_generator: chainlib.connection.JSONRPCIdGenerator
"""
def __init__(self, address, id_generator=None):
self.address = address
self.id_generator = id_generator
@ -26,23 +49,45 @@ class NonceOracle:
def get_nonce(self):
"""Load initial nonce value.
"""
raise NotImplementedError('Class must be extended')
def next_nonce(self):
"""Return next nonce value and advance.
:rtype: int
:returns: Next nonce for address.
"""
n = self.nonce
self.nonce += 1
return n
class RPCNonceOracle(NonceOracle):
"""JSON-RPC only nonce parameter helper.
:param address: Address to retireve nonce for, in hex
:type address: str
:param conn: RPC connection
:type conn: chainlib.connection.RPCConnection
:param id_generator: json-rpc id generator
:type id_generator: chainlib.connection.JSONRPCIdGenerator
"""
def __init__(self, address, conn, id_generator=None):
self.conn = conn
super(RPCNonceOracle, self).__init__(address, id_generator=id_generator)
def get_nonce(self):
"""Load and return nonce value from network.
Note! First call to next_nonce after calling get_nonce will return the same value!
:rtype: int
:returns: Initial nonce
"""
o = nonce(self.address, id_generator=self.id_generator)
r = self.conn.do(o)
n = strip_0x(r)
@ -50,14 +95,28 @@ class RPCNonceOracle(NonceOracle):
class OverrideNonceOracle(NonceOracle):
"""Manually set initial nonce value.
def __init__(self, address, nonce):
self.nonce = nonce
super(OverrideNonceOracle, self).__init__(address)
:param address: Address to retireve nonce for, in hex
:type address: str
:param nonce: Nonce value
:type nonce: int
:param id_generator: json-rpc id generator (not used)
:type id_generator: chainlib.connection.JSONRPCIdGenerator
"""
def __init__(self, address, nonce, id_generator=None):
self.initial_nonce = nonce
self.nonce = self.initial_nonce
super(OverrideNonceOracle, self).__init__(address, id_generator=id_generator)
def get_nonce(self):
return self.nonce
"""Returns initial nonce value set at object construction.
:rtype: int
:returns: Initial nonce value.
"""
return self.initial_nonce
DefaultNonceOracle = RPCNonceOracle

View File

@ -17,7 +17,7 @@ from chainlib.connection import (
from chainlib.eth.unittest.ethtester import create_tester_signer
from chainlib.eth.address import to_checksum_address
logg = logging.getLogger() #__name__)
logg = logging.getLogger(__name__)
@pytest.fixture(scope='function')
@ -37,13 +37,6 @@ 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')

View File

@ -1,30 +1,19 @@
#!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
# external imports
from hexathon import (
add_0x,
strip_0x,
even,
)
import sha3
# local imports
from chainlib.eth.address import to_checksum
import chainlib.eth.cli
from chainlib.eth.address import AddressChecksum
from chainlib.jsonrpc import (
jsonrpc_result,
IntSequenceGenerator,
@ -35,53 +24,37 @@ from chainlib.eth.gas import (
balance,
)
from chainlib.chain import ChainSpec
from crypto_dev_signer.eth.signer import ReferenceSigner as EIP155Signer
logging.basicConfig(level=logging.WARNING)
logg = logging.getLogger()
default_eth_provider = os.environ.get('RPC_PROVIDER')
if default_eth_provider == None:
default_eth_provider = os.environ.get('ETH_PROVIDER', 'http://localhost:8545')
script_dir = os.path.dirname(os.path.realpath(__file__))
#config_dir = os.path.join(script_dir, '..', 'data', 'config')
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('--seq', action='store_true', help='Use sequential rpc ids')
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')
arg_flags = chainlib.eth.cli.argflag_std_read
argparser = chainlib.eth.cli.ArgumentParser(arg_flags)
argparser.add_positional('address', type=str, help='Ethereum address of recipient')
args = argparser.parse_args()
#config = chainlib.eth.cli.Config.from_args(args, arg_flags, default_config_dir=config_dir)
config = chainlib.eth.cli.Config.from_args(args, arg_flags)
wallet = chainlib.eth.cli.Wallet()
wallet.from_config(config)
holder_address = args.address
if wallet.get_signer_address() == None and holder_address != None:
holder_address = wallet.from_address(holder_address)
if args.vv:
logg.setLevel(logging.DEBUG)
elif args.v:
logg.setLevel(logging.INFO)
rpc = chainlib.eth.cli.Rpc()
conn = rpc.connect_by_config(config)
rpc_id_generator = None
if args.seq:
rpc_id_generator = IntSequenceGenerator()
auth = None
if os.environ.get('RPC_AUTHENTICATION') == 'basic':
from chainlib.auth import BasicAuth
auth = BasicAuth(os.environ['RPC_USERNAME'], os.environ['RPC_PASSWORD'])
conn = EthHTTPConnection(args.p, auth=auth)
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)
chain_spec = ChainSpec.from_chain_str(config.get('CHAIN_SPEC'))
def main():
r = None
decimals = 18
o = balance(address, id_generator=rpc_id_generator)