diff --git a/chainlib/block.py b/chainlib/block.py new file mode 100644 index 0000000..a45c870 --- /dev/null +++ b/chainlib/block.py @@ -0,0 +1,7 @@ +# standard imports +import enum + + +class BlockSpec(enum.IntEnum): + PENDING = -1 + LATEST = 0 diff --git a/chainlib/chain.py b/chainlib/chain.py index 2cdbc64..c767613 100644 --- a/chainlib/chain.py +++ b/chainlib/chain.py @@ -1,3 +1,7 @@ +# standard imports +import copy + + class ChainSpec: def __init__(self, engine, common_name, network_id, tag=None): @@ -35,6 +39,15 @@ class ChainSpec: return ChainSpec(o[0], o[1], int(o[2]), tag) + @staticmethod + def from_dict(o): + return ChainSpec(o['engine'], o['common_name'], o['network_id'], tag=o['tag']) + + + def asdict(self): + return copy.copy(self.o) + + def __str__(self): s = '{}:{}:{}'.format(self.o['engine'], self.o['common_name'], self.o['network_id']) if self.o['tag'] != None: diff --git a/chainlib/connection.py b/chainlib/connection.py new file mode 100644 index 0000000..52cdce7 --- /dev/null +++ b/chainlib/connection.py @@ -0,0 +1,217 @@ +# standard imports +import socket +import os +import logging +import enum +import re +import json +from urllib.request import ( + Request, + urlopen, + urlparse, + urljoin, + build_opener, + install_opener, + ) + +# local imports +from .jsonrpc import ( + jsonrpc_template, + jsonrpc_result, + DefaultErrorParser, + ) +from .http import PreemptiveBasicAuthHandler + +logg = logging.getLogger(__name__) + +error_parser = DefaultErrorParser() + + +class ConnType(enum.Enum): + + CUSTOM = 0x00 + HTTP = 0x100 + HTTP_SSL = 0x101 + WEBSOCKET = 0x200 + WEBSOCKET_SSL = 0x201 + UNIX = 0x1000 + + +re_http = '^http(s)?://' +re_ws = '^ws(s)?://' +re_unix = '^ipc://' + +def str_to_connspec(s): + if s == 'custom': + return ConnType.CUSTOM + + m = re.match(re_http, s) + if m != None: + if m.group(1) != None: + return ConnType.HTTP_SSL + return ConnType.HTTP + + m = re.match(re_ws, s) + if m != None: + if m.group(1) != None: + return ConnType.WEBSOCKET_SSL + return ConnType.WEBSOCKET + + + m = re.match(re_unix, s) + if m != None: + return ConnType.UNIX + + raise ValueError('unknown connection type {}'.format(s)) + + +def from_conntype(t): + if t in [ConnType.HTTP, ConnType.HTTP_SSL]: + return JSONRPCHTTPConnection + elif t in [ConnType.UNIX]: + return JSONRPCUnixConnection + raise NotImplementedError(t) + + + +class RPCConnection(): + + __locations = {} + __constructors = {} + + def __init__(self, url=None, chain_spec=None): + self.chain_spec = chain_spec + self.location = None + self.basic = None + if url == None: + return + + url_parsed = urlparse(url) + logg.debug('creating connection {} -> {}'.format(url, url_parsed)) + basic = url_parsed.netloc.split('@') + location = None + if len(basic) == 1: + location = url_parsed.netloc + else: + location = basic[1] + self.basic = basic[0].split(':') + #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) + + logg.debug('parsed url {} to location {}'.format(url, self.location)) + + + # TODO: constructor needs to be constructor-factory, that itself can select on url type + @staticmethod + def register_location(location, chain_spec, tag='default', constructor=None, exist_ok=False): + chain_str = str(chain_spec) + if RPCConnection.__locations.get(chain_str) == None: + RPCConnection.__locations[chain_str] = {} + RPCConnection.__constructors[chain_str] = {} + elif not exist_ok: + v = RPCConnection.__locations[chain_str].get(tag) + if v != None: + raise ValueError('duplicate registration of tag {}:{}, requested {} already had {}'.format(chain_str, tag, location, v)) + conntype = str_to_connspec(location) + RPCConnection.__locations[chain_str][tag] = (conntype, location) + if constructor != None: + RPCConnection.__constructors[chain_str][tag] = constructor + logg.info('registered rpc connection {} ({}:{}) as {} with custom constructor {}'.format(location, chain_str, tag, conntype, constructor)) + else: + logg.info('registered rpc connection {} ({}:{}) as {}'.format(location, chain_str, tag, conntype)) + + + @staticmethod + def connect(chain_spec, tag='default'): + chain_str = str(chain_spec) + c = RPCConnection.__locations[chain_str][tag] + constructor = RPCConnection.__constructors[chain_str].get(tag) + if constructor == None: + constructor = from_conntype(c[0]) + logg.debug('cons {} {}'.format(constructor, c)) + return constructor(url=c[1], chain_spec=chain_spec) + + +class HTTPConnection(RPCConnection): + + def disconnect(self): + pass + + + def __del__(self): + self.disconnect() + + +class UnixConnection(RPCConnection): + + def disconnect(self): + pass + + + def __del__(self): + self.disconnect() + + +class JSONRPCHTTPConnection(HTTPConnection): + + def do(self, o, error_parser=error_parser): + req = Request( + self.location, + method='POST', + ) + req.add_header('Content-Type', 'application/json') + data = json.dumps(o) + logg.debug('(HTTP) send {}'.format(data)) + + if self.basic != None: + handler = PreemptiveBasicAuthHandler() + handler.add_password( + realm=None, + uri=self.location, + user=self.basic[0], + passwd=self.basic[1], + ) + ho = build_opener(handler) + 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']: + raise ValueError('RPC id mismatch; sent {} received {}'.format(o['id'], result['id'])) + return jsonrpc_result(result, error_parser) + + +class JSONRPCUnixConnection(UnixConnection): + + def do(self, o, error_parser=error_parser): + conn = socket.socket(family=socket.AF_UNIX, type=socket.SOCK_STREAM, proto=0) + conn.connect(self.location) + data = json.dumps(o) + + logg.debug('unix socket send {}'.format(data)) + l = len(data) + n = 0 + while n < l: + c = conn.send(data.encode('utf-8')) + if c == 0: + s.close() + raise IOError('unix socket ({}/{}) {}'.format(n, l, data)) + n += c + r = b'' + while True: + b = conn.recv(4096) + if len(b) == 0: + break + r += b + conn.close() + logg.debug('unix socket recv {}'.format(r.decode('utf-8'))) + result = json.loads(r) + if result['id'] != o['id']: + raise ValueError('RPC id mismatch; sent {} received {}'.format(o['id'], result['id'])) + + return jsonrpc_result(result, error_parser) + diff --git a/chainlib/error.py b/chainlib/error.py new file mode 100644 index 0000000..e2ac51e --- /dev/null +++ b/chainlib/error.py @@ -0,0 +1,7 @@ +# TODO: use json-rpc module +class JSONRPCException(Exception): + pass + + +class ExecutionError(Exception): + pass diff --git a/chainlib/eth/address.py b/chainlib/eth/address.py index ed5a8cf..ea8818b 100644 --- a/chainlib/eth/address.py +++ b/chainlib/eth/address.py @@ -4,44 +4,10 @@ from hexathon import ( strip_0x, uniform, ) +from crypto_dev_signer.encoding import ( + is_address, + is_checksum_address, + to_checksum_address, + ) - -def is_address(address_hex): - try: - address_hex = strip_0x(address_hex) - except ValueError: - return False - return len(address_hex) == 40 - - -def is_checksum_address(address_hex): - hx = None - try: - hx = to_checksum(address_hex) - except ValueError: - return False - print('{} {}'.format(hx, address_hex)) - return hx == address_hex - - -def to_checksum(address_hex): - address_hex = strip_0x(address_hex) - address_hex = uniform(address_hex) - if len(address_hex) != 40: - raise ValueError('Invalid address length') - h = sha3.keccak_256() - h.update(address_hex.encode('utf-8')) - z = h.digest() - - checksum_address_hex = '0x' - - for (i, c) in enumerate(address_hex): - if c in '1234567890': - checksum_address_hex += c - elif c in 'abcdef': - if z[int(i / 2)] & (0x80 >> ((i % 2) * 4)) > 1: - checksum_address_hex += c.upper() - else: - checksum_address_hex += c - - return checksum_address_hex +to_checksum = to_checksum_address diff --git a/chainlib/eth/block.py b/chainlib/eth/block.py index df6e617..39cc6ac 100644 --- a/chainlib/eth/block.py +++ b/chainlib/eth/block.py @@ -1,5 +1,5 @@ # third-party imports -from chainlib.eth.rpc import jsonrpc_template +from chainlib.jsonrpc import jsonrpc_template from chainlib.eth.tx import Tx from hexathon import ( add_0x, @@ -7,25 +7,34 @@ from hexathon import ( even, ) + def block_latest(): o = jsonrpc_template() o['method'] = 'eth_blockNumber' return o -def block_by_hash(hsh): +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): +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(True) + 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 @@ -36,6 +45,7 @@ class Block: self.number = int(strip_0x(src['number']), 16) self.txs = src['transactions'] self.block_src = src + self.timestamp = int(strip_0x(src['timestamp']), 16) def src(self): diff --git a/chainlib/eth/connection.py b/chainlib/eth/connection.py index 0c21847..a38695b 100644 --- a/chainlib/eth/connection.py +++ b/chainlib/eth/connection.py @@ -1,8 +1,10 @@ # standard imports +import copy import logging import json import datetime import time +import socket from urllib.request import ( Request, urlopen, @@ -19,36 +21,28 @@ from .error import ( DefaultErrorParser, RevertEthException, ) -from .rpc import ( +from .sign import ( + sign_transaction, + ) +from chainlib.connection import ( + JSONRPCHTTPConnection, + JSONRPCUnixConnection, + error_parser, + ) +from chainlib.jsonrpc import ( jsonrpc_template, jsonrpc_result, ) +from chainlib.eth.tx import ( + unpack, + ) -error_parser = DefaultErrorParser() logg = logging.getLogger(__name__) -class HTTPConnection: +class EthHTTPConnection(JSONRPCHTTPConnection): - def __init__(self, url): - self.url = url - - - def do(self, o, error_parser=error_parser): - req = Request( - self.url, - method='POST', - ) - req.add_header('Content-Type', 'application/json') - data = json.dumps(o) - logg.debug('(HTTP) send {}'.format(data)) - res = urlopen(req, data=data.encode('utf-8')) - o = json.load(res) - logg.debug('(HTTP) recv {}'.format(o)) - return jsonrpc_result(o, error_parser) - - - def wait(self, tx_hash_hex, delay=0.5, timeout=0.0): + def wait(self, tx_hash_hex, delay=0.5, timeout=0.0, error_parser=error_parser): t = datetime.datetime.utcnow() i = 0 while True: @@ -56,17 +50,18 @@ class HTTPConnection: o['method'] ='eth_getTransactionReceipt' o['params'].append(add_0x(tx_hash_hex)) req = Request( - self.url, + self.location, method='POST', ) req.add_header('Content-Type', 'application/json') data = json.dumps(o) - logg.debug('(HTTP) receipt attempt {} {}'.format(i, data)) + 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) @@ -79,3 +74,31 @@ class HTTPConnection: 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') + + +class EthUnixSignerConnection(EthUnixConnection): + + def sign_transaction_to_rlp(self, 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 self.chain_spec != None: + chain_id = self.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 = self.do(o) + logg.debug('sig got {}'.format(r)) + return bytes.fromhex(strip_0x(r)) + + + def sign_message(self, msg): + o = sign_message(msg) + return self.do(o) diff --git a/chainlib/eth/constant.py b/chainlib/eth/constant.py index 6dd2910..1e10a2a 100644 --- a/chainlib/eth/constant.py +++ b/chainlib/eth/constant.py @@ -2,3 +2,4 @@ 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) diff --git a/chainlib/eth/contract.py b/chainlib/eth/contract.py new file mode 100644 index 0000000..9a515d6 --- /dev/null +++ b/chainlib/eth/contract.py @@ -0,0 +1,263 @@ +# 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' + 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 bool(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 {}'.format(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 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 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) + + + + 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 diff --git a/chainlib/eth/encoding.py b/chainlib/eth/encoding.py deleted file mode 100644 index f99fd4b..0000000 --- a/chainlib/eth/encoding.py +++ /dev/null @@ -1,20 +0,0 @@ -from eth_abi import ( - encode_single as __encode_single, - decode_single as __decode_single, - ) - -def abi_encode(signature, *args): - return __encode_single(signature, *args) - - -def abi_encode_hex(signature, *args): - return __encode_single(signature, *args).hex() - - -def abi_decode(signature, *args): - return __decode_single(signature, *args) - - -def abi_decode_hex(signature, *args): - return __decode_single(signature, *args).hex() - diff --git a/chainlib/eth/erc20.py b/chainlib/eth/erc20.py index 1304992..c880d6f 100644 --- a/chainlib/eth/erc20.py +++ b/chainlib/eth/erc20.py @@ -1,6 +1,12 @@ -# third-party imports +# standard imports +import logging + +# external imports import sha3 -from hexathon import add_0x +from hexathon import ( + add_0x, + strip_0x, + ) from crypto_dev_signer.eth.transaction import EIP155Transaction # local imports @@ -9,26 +15,33 @@ from chainlib.hash import ( keccak256_string_to_hex, ) from .constant import ZERO_ADDRESS -from .rpc import jsonrpc_template -from .tx import TxFactory -from .encoding import abi_encode - +from .tx import ( + TxFactory, + TxFormat, + ) +from .contract import ( + ABIContractEncoder, + ABIContractDecoder, + ABIContractType, + abi_decode_single, + ) +from chainlib.jsonrpc import jsonrpc_template +from .error import RequestMismatchException -# TODO: move to cic-contracts -erc20_balance_signature = keccak256_string_to_hex('balanceOf(address)')[:8] -erc20_decimals_signature = keccak256_string_to_hex('decimals()')[:8] -erc20_transfer_signature = keccak256_string_to_hex('transfer(address,uint256)')[:8] +logg = logging.getLogger() -class ERC20TxFactory(TxFactory): +class ERC20(TxFactory): - def erc20_balance(self, contract_address, address, sender_address=ZERO_ADDRESS): + def balance_of(self, contract_address, address, sender_address=ZERO_ADDRESS): o = jsonrpc_template() o['method'] = 'eth_call' - data = erc20_balance_signature - data += abi_encode('address', address).hex() - data = add_0x(data) + enc = ABIContractEncoder() + enc.method('balanceOf') + enc.typ(ABIContractType.ADDRESS) + enc.address(address) + data = add_0x(enc.get()) tx = self.template(sender_address, contract_address) tx = self.set_code(tx, data) o['params'].append(self.normalize(tx)) @@ -36,10 +49,16 @@ class ERC20TxFactory(TxFactory): return o - def erc20_decimals(self, contract_address, sender_address=ZERO_ADDRESS): + def balance(self, contract_address, address, sender_address=ZERO_ADDRESS): + return self.balance_of(contract_address, address, sender_address=ZERO_ADDRESS) + + + def symbol(self, contract_address, sender_address=ZERO_ADDRESS): o = jsonrpc_template() o['method'] = 'eth_call' - data = add_0x(erc20_decimals_signature) + enc = ABIContractEncoder() + enc.method('symbol') + data = add_0x(enc.get()) tx = self.template(sender_address, contract_address) tx = self.set_code(tx, data) o['params'].append(self.normalize(tx)) @@ -47,11 +66,173 @@ class ERC20TxFactory(TxFactory): return o - def erc20_transfer(self, contract_address, sender_address, recipient_address, value): - data = erc20_transfer_signature - data += abi_encode('address', recipient_address).hex() - data += abi_encode('uint256', value).hex() - data = add_0x(data) + def name(self, contract_address, sender_address=ZERO_ADDRESS): + o = jsonrpc_template() + o['method'] = 'eth_call' + enc = ABIContractEncoder() + enc.method('name') + 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 + + + def decimals(self, contract_address, sender_address=ZERO_ADDRESS): + o = jsonrpc_template() + o['method'] = 'eth_call' + enc = ABIContractEncoder() + enc.method('decimals') + 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 + + + def transfer(self, contract_address, sender_address, recipient_address, value, tx_format=TxFormat.JSONRPC): + enc = ABIContractEncoder() + enc.method('transfer') + enc.typ(ABIContractType.ADDRESS) + enc.typ(ABIContractType.UINT256) + enc.address(recipient_address) + enc.uint256(value) + data = add_0x(enc.get()) tx = self.template(sender_address, contract_address, use_nonce=True) tx = self.set_code(tx, data) - return self.build(tx) + tx = self.finalize(tx, tx_format) + return tx + + + def transfer_from(self, contract_address, sender_address, holder_address, recipient_address, value, tx_format=TxFormat.JSONRPC): + enc = ABIContractEncoder() + enc.method('transfer') + enc.typ(ABIContractType.ADDRESS) + enc.typ(ABIContractType.ADDRESS) + enc.typ(ABIContractType.UINT256) + enc.address(holder_address) + enc.address(recipient_address) + enc.uint256(value) + data = add_0x(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 approve(self, contract_address, sender_address, recipient_address, value, tx_format=TxFormat.JSONRPC): + enc = ABIContractEncoder() + enc.method('approve') + enc.typ(ABIContractType.ADDRESS) + enc.typ(ABIContractType.UINT256) + enc.address(recipient_address) + enc.uint256(value) + data = add_0x(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 + + + @classmethod + def parse_symbol(self, v): + return abi_decode_single(ABIContractType.STRING, v) + + + @classmethod + def parse_name(self, v): + return abi_decode_single(ABIContractType.STRING, v) + + + @classmethod + def parse_decimals(self, v): + return abi_decode_single(ABIContractType.UINT256, v) + + + @classmethod + def parse_balance(self, v): + return abi_decode_single(ABIContractType.UINT256, v) + + + @classmethod + def parse_transfer_request(self, v): + v = strip_0x(v) + cursor = 0 + enc = ABIContractEncoder() + enc.method('transfer') + enc.typ(ABIContractType.ADDRESS) + enc.typ(ABIContractType.UINT256) + r = enc.get() + l = len(r) + m = v[:l] + if m != r: + logg.error('method mismatch, expected {}, got {}'.format(r, m)) + raise RequestMismatchException(v) + cursor += l + + dec = ABIContractDecoder() + dec.typ(ABIContractType.ADDRESS) + dec.typ(ABIContractType.UINT256) + dec.val(v[cursor:cursor+64]) + cursor += 64 + dec.val(v[cursor:cursor+64]) + r = dec.decode() + return r + + + @classmethod + def parse_transfer_from_request(self, v): + v = strip_0x(v) + cursor = 0 + enc = ABIContractEncoder() + enc.method('transferFrom') + enc.typ(ABIContractType.ADDRESS) + enc.typ(ABIContractType.ADDRESS) + enc.typ(ABIContractType.UINT256) + r = enc.get() + l = len(r) + m = v[:l] + if m != r: + logg.error('method mismatch, expected {}, got {}'.format(r, m)) + raise RequestMismatchException(v) + cursor += l + + dec = ABIContractDecoder() + dec.typ(ABIContractType.ADDRESS) + dec.typ(ABIContractType.ADDRESS) + dec.typ(ABIContractType.UINT256) + dec.val(v[cursor:cursor+64]) + cursor += 64 + dec.val(v[cursor:cursor+64]) + cursor += 64 + dec.val(v[cursor:cursor+64]) + r = dec.decode() + return r + + + @classmethod + def parse_approve_request(self, v): + v = strip_0x(v) + cursor = 0 + enc = ABIContractEncoder() + enc.method('approve') + enc.typ(ABIContractType.ADDRESS) + enc.typ(ABIContractType.UINT256) + r = enc.get() + l = len(r) + m = v[:l] + if m != r: + logg.error('method mismatch, expected {}, got {}'.format(r, m)) + raise RequestMismatchException(v) + cursor += l + + dec = ABIContractDecoder() + dec.typ(ABIContractType.ADDRESS) + dec.typ(ABIContractType.UINT256) + dec.val(v[cursor:cursor+64]) + cursor += 64 + dec.val(v[cursor:cursor+64]) + r = dec.decode() + return r diff --git a/chainlib/eth/error.py b/chainlib/eth/error.py index c87129f..50e8e51 100644 --- a/chainlib/eth/error.py +++ b/chainlib/eth/error.py @@ -1,8 +1,19 @@ +# local imports +from chainlib.error import ExecutionError + class EthException(Exception): pass -class RevertEthException(EthException): +class RevertEthException(EthException, ExecutionError): + pass + + +class NotFoundEthException(EthException): + pass + + +class RequestMismatchException(EthException): pass diff --git a/chainlib/eth/gas.py b/chainlib/eth/gas.py index 342f5fc..ff6ef98 100644 --- a/chainlib/eth/gas.py +++ b/chainlib/eth/gas.py @@ -1,3 +1,6 @@ +# standard imports +import logging + # third-party imports from hexathon import ( add_0x, @@ -7,12 +10,18 @@ from crypto_dev_signer.eth.transaction import EIP155Transaction # local imports from chainlib.hash import keccak256_hex_to_hex -from chainlib.eth.rpc import jsonrpc_template -from chainlib.eth.tx import TxFactory +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() @@ -24,47 +33,87 @@ def balance(address): o = jsonrpc_template() o['method'] = 'eth_getBalance' o['params'].append(address) + o['params'].append('latest') return o -class GasTxFactory(TxFactory): +class Gas(TxFactory): - def create(self, sender, recipient, value): - tx = self.template(sender, recipient, use_nonce=True) + 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']) - self.signer.signTransaction(txe) - tx_raw = txe.rlp_serialize() + 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 = jsonrpc_template() - o['method'] = 'eth_sendRawTransaction' - o['params'].append(tx_raw_hex) + 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 DefaultGasOracle: +class RPCGasOracle: - def __init__(self, conn): + def __init__(self, conn, code_callback=None): self.conn = conn + self.code_callback = code_callback - def get(self, code=None): + def get_gas(self, code=None): o = price() r = self.conn.do(o) n = strip_0x(r) - return (int(n, 16), MINIMUM_FEE_UNITS) + fee_units = MINIMUM_FEE_UNITS + if self.code_callback != None: + fee_units = self.code_callback(code) + return (int(n, 16), fee_units) -class OverrideGasOracle: +class OverrideGasOracle(RPCGasOracle): - def __init__(self, price, limit=None): - if limit == None: - limit = MINIMUM_FEE_UNITS + def __init__(self, price=None, limit=None, conn=None, code_callback=None): + self.conn = None + self.code_callback = None + if conn != None: + logg.debug('override gas oracle with rpc fallback') + super(OverrideGasOracle, self).__init__(conn, code_callback) self.limit = limit self.price = price - def get(self): - return (self.price, self.limit) + + def get_gas(self, code=None): + r = None + fee_units = None + fee_price = None + + rpc_results = None + if self.conn != 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_limit)) + else: + fee_units = MINIMUM_FEE_UNITS + logg.debug('override gas oracle without explicit limit, setting default {}'.format(fee_limit)) + + return (fee_price, fee_units) + + +DefaultGasOracle = RPCGasOracle diff --git a/chainlib/eth/nonce.py b/chainlib/eth/nonce.py index c73ddcb..9aada86 100644 --- a/chainlib/eth/nonce.py +++ b/chainlib/eth/nonce.py @@ -5,7 +5,7 @@ from hexathon import ( ) # local imports -from chainlib.eth.rpc import jsonrpc_template +from chainlib.jsonrpc import jsonrpc_template def nonce(address): @@ -16,34 +16,47 @@ def nonce(address): return o -class DefaultNonceOracle: +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.address = address self.conn = conn - self.nonce = self.get() + super(RPCNonceOracle, self).__init__(address) - def get(self): + def get_nonce(self): o = nonce(self.address) r = self.conn.do(o) n = strip_0x(r) return int(n, 16) - def next(self): - n = self.nonce - self.nonce += 1 - return n - - -class OverrideNonceOracle(DefaultNonceOracle): +class OverrideNonceOracle(NonceOracle): def __init__(self, address, nonce): self.nonce = nonce - super(OverrideNonceOracle, self).__init__(address, None) + super(OverrideNonceOracle, self).__init__(address) - def get(self): + def get_nonce(self): return self.nonce + + +DefaultNonceOracle = RPCNonceOracle diff --git a/chainlib/eth/pytest/__init__.py b/chainlib/eth/pytest/__init__.py new file mode 100644 index 0000000..8388db6 --- /dev/null +++ b/chainlib/eth/pytest/__init__.py @@ -0,0 +1,3 @@ +from .fixtures_ethtester import * +from .fixtures_chain import * +from .fixtures_signer import * diff --git a/chainlib/eth/pytest/fixtures_chain.py b/chainlib/eth/pytest/fixtures_chain.py new file mode 100644 index 0000000..e223192 --- /dev/null +++ b/chainlib/eth/pytest/fixtures_chain.py @@ -0,0 +1,17 @@ +# 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, + } diff --git a/chainlib/eth/pytest/fixtures_ethtester.py b/chainlib/eth/pytest/fixtures_ethtester.py new file mode 100644 index 0000000..303794c --- /dev/null +++ b/chainlib/eth/pytest/fixtures_ethtester.py @@ -0,0 +1,102 @@ +# 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, + ) +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_location('custom', default_chain_spec, tag='default', constructor=rpc_with_tester, exist_ok=True) + RPCConnection.register_location('custom', default_chain_spec, tag='signer', constructor=rpc_with_tester, exist_ok=True) + return None diff --git a/chainlib/eth/pytest/fixtures_signer.py b/chainlib/eth/pytest/fixtures_signer.py new file mode 100644 index 0000000..484a52b --- /dev/null +++ b/chainlib/eth/pytest/fixtures_signer.py @@ -0,0 +1,18 @@ +# 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], + } diff --git a/chainlib/eth/rpc.py b/chainlib/eth/rpc.py deleted file mode 100644 index 8698494..0000000 --- a/chainlib/eth/rpc.py +++ /dev/null @@ -1,17 +0,0 @@ -# standard imports -import uuid - - -def jsonrpc_template(): - return { - 'jsonrpc': '2.0', - 'id': str(uuid.uuid4()), - 'method': None, - 'params': [], - } - - -def jsonrpc_result(o, ep): - if o.get('error') != None: - raise ep.translate(o) - return o['result'] diff --git a/chainlib/eth/runnable/balance.py b/chainlib/eth/runnable/balance.py index 0b6746e..890a5da 100644 --- a/chainlib/eth/runnable/balance.py +++ b/chainlib/eth/runnable/balance.py @@ -26,14 +26,16 @@ from eth_abi import encode_single # local imports from chainlib.eth.address import to_checksum -from chainlib.eth.rpc import ( +from chainlib.jsonrpc import ( jsonrpc_template, jsonrpc_result, ) -from chainlib.eth.erc20 import ERC20TxFactory -from chainlib.eth.connection import HTTPConnection -from chainlib.eth.nonce import DefaultNonceOracle -from chainlib.eth.gas import DefaultGasOracle +from chainlib.eth.erc20 import ERC20 +from chainlib.eth.connection import EthHTTPConnection +from chainlib.eth.gas import ( + OverrideGasOracle, + balance, + ) logging.basicConfig(level=logging.WARNING) logg = logging.getLogger() @@ -43,50 +45,59 @@ 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('-t', '--token-address', dest='t', 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('-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('account', type=str, help='Account address') +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.v: +if args.vv: logg.setLevel(logging.DEBUG) +elif args.v: + logg.setLevel(logging.INFO) -conn = HTTPConnection(args.p) -gas_oracle = DefaultGasOracle(conn) +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') + +token_symbol = 'eth' + +chain_spec = ChainSpec.from_chain_str(args.i) def main(): - account = to_checksum(args.account) - if not args.u and account != add_0x(args.account): - raise ValueError('invalid checksum address') - r = None decimals = 18 - if args.t != None: - g = ERC20TxFactory(gas_oracle=gas_oracle) + if args.a != None: + #g = ERC20(gas_oracle=gas_oracle) + g = ERC20(chain_spec=chain_spec) # determine decimals - decimals_o = g.erc20_decimals(args.t) + decimals_o = g.decimals(args.a) r = conn.do(decimals_o) decimals = int(strip_0x(r), 16) + symbol_o = g.symbol(args.a) + r = conn.do(decimals_o) + token_symbol = r # get balance - balance_o = g.erc20_balance(args.t, account) + balance_o = g.balance(args.a, address) r = conn.do(balance_o) else: - o = jsonrpc_template() - o['method'] = 'eth_getBalance' - o['params'].append(account) + o = balance(address) r = conn.do(o) hx = strip_0x(r) - balance = int(hx, 16) - logg.debug('balance {} = {} decimals {}'.format(even(hx), balance, decimals)) + balance_value = int(hx, 16) + logg.debug('balance {} = {} decimals {}'.format(even(hx), balance_value, decimals)) - balance_str = str(balance) + balance_str = str(balance_value) balance_len = len(balance_str) if balance_len < decimals + 1: print('0.{}'.format(balance_str.zfill(decimals))) diff --git a/chainlib/eth/runnable/count.py b/chainlib/eth/runnable/count.py new file mode 100644 index 0000000..fe6d760 --- /dev/null +++ b/chainlib/eth/runnable/count.py @@ -0,0 +1,59 @@ +# 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 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() diff --git a/chainlib/eth/runnable/decode.py b/chainlib/eth/runnable/decode.py index 632a62f..211451e 100644 --- a/chainlib/eth/runnable/decode.py +++ b/chainlib/eth/runnable/decode.py @@ -10,6 +10,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later # standard imports +import sys import os import json import argparse @@ -19,6 +20,9 @@ import logging 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() @@ -28,7 +32,7 @@ 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', type=str, help='Numeric network id') +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() @@ -36,26 +40,11 @@ if args.v: logg.setLevel(logging.DEBUG) chain_spec = ChainSpec.from_chain_str(args.i) -chain_id = chain_spec.network_id() def main(): tx_raw = args.tx - if tx_raw[:2] == '0x': - tx_raw = tx_raw[2:] - tx_raw_bytes = bytes.fromhex(tx_raw) - tx = unpack(tx_raw_bytes, int(chain_id)) - 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**12))) - if x != None: - print('{}: {} ({})'.format(k, tx[k], x)) - else: - print('{}: {}'.format(k, tx[k])) - + decode_for_puny_humans(tx_raw, chain_spec, sys.stdout) if __name__ == '__main__': main() diff --git a/chainlib/eth/runnable/gas.py b/chainlib/eth/runnable/gas.py index 2ea2371..2c75e85 100644 --- a/chainlib/eth/runnable/gas.py +++ b/chainlib/eth/runnable/gas.py @@ -10,6 +10,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later # standard imports +import io import sys import os import json @@ -18,7 +19,7 @@ import logging # third-party imports from crypto_dev_signer.eth.signer import ReferenceSigner as EIP155Signer -from crypto_dev_signer.keystore import DictKeystore +from crypto_dev_signer.keystore.dict import DictKeystore from hexathon import ( add_0x, strip_0x, @@ -26,33 +27,32 @@ from hexathon import ( # local imports from chainlib.eth.address import to_checksum -from chainlib.eth.connection import HTTPConnection -from chainlib.eth.rpc import jsonrpc_template +from chainlib.eth.connection import EthHTTPConnection +from chainlib.jsonrpc import jsonrpc_template from chainlib.eth.nonce import ( - DefaultNonceOracle, + RPCNonceOracle, OverrideNonceOracle, ) from chainlib.eth.gas import ( - DefaultGasOracle, + RPCGasOracle, OverrideGasOracle, - GasTxFactory, + 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_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='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='Ethereum:1', help='Chain specification string') -argparser.add_argument('-a', '--signer-address', dest='a', type=str, help='Signing address') +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') @@ -61,8 +61,7 @@ argparser.add_argument('--gas', 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('-o', action='store_true', help='Print raw to to terminal') -argparser.add_argument('-n', action='store_true', help='Do not send to network') +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='Amount of tokens to mint and gift') args = argparser.parse_args() @@ -88,35 +87,32 @@ 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) + 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 = HTTPConnection(args.p) +conn = EthHTTPConnection(args.p) nonce_oracle = None if args.nonce != None: nonce_oracle = OverrideNonceOracle(signer_address, args.nonce) else: - nonce_oracle = DefaultNonceOracle(signer_address, conn) + nonce_oracle = RPCNonceOracle(signer_address, conn) gas_oracle = None -if args.price != None: - gas_oracle = OverrideGasOracle(args.price, args.gas) +if args.price or args.gas != None: + gas_oracle = OverrideGasOracle(price=args.price, limit=args.gas, conn=conn) else: - gas_oracle = DefaultGasOracle(conn) + gas_oracle = RPCGasOracle(conn) chain_spec = ChainSpec.from_chain_str(args.i) -chain_id = chain_spec.network_id() value = args.amount -out = args.o +send = args.s -send = not args.n - -g = GasTxFactory(signer=signer, gas_oracle=gas_oracle, nonce_oracle=nonce_oracle, chain_id=chain_id) +g = Gas(chain_spec, signer=signer, gas_oracle=gas_oracle, nonce_oracle=nonce_oracle) def balance(address): @@ -132,21 +128,31 @@ def main(): raise ValueError('invalid checksum address') logg.info('gas transfer from {} to {} value {}'.format(signer_address, recipient, value)) - logg.debug('sender {} balance before: {}'.format(signer_address, balance(signer_address))) - logg.debug('recipient {} balance before: {}'.format(recipient, balance(recipient))) - + if logg.isEnabledFor(logging.DEBUG): + logg.debug('sender {} balance before: {}'.format(signer_address, balance(signer_address))) + logg.debug('recipient {} balance before: {}'.format(recipient, balance(recipient))) + (tx_hash_hex, o) = g.create(signer_address, recipient, value) - if out: - print(o['params'][0]) + if send: conn.do(o) - if block_last: - conn.wait(tx_hash_hex) - logg.debug('sender {} balance after: {}'.format(signer_address, balance(signer_address))) - logg.debug('recipient {} balance after: {}'.format(recipient, balance(recipient))) + 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]) - print(tx_hash_hex) if __name__ == '__main__': diff --git a/chainlib/eth/runnable/get.py b/chainlib/eth/runnable/get.py index d926cb4..2b9ece5 100644 --- a/chainlib/eth/runnable/get.py +++ b/chainlib/eth/runnable/get.py @@ -10,6 +10,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later # standard imports +import sys import os import json import argparse @@ -23,15 +24,14 @@ from hexathon import ( even, ) import sha3 -from eth_abi import encode_single # local imports from chainlib.eth.address import to_checksum -from chainlib.eth.rpc import ( +from chainlib.jsonrpc import ( jsonrpc_template, jsonrpc_result, ) -from chainlib.eth.connection import HTTPConnection +from chainlib.eth.connection import EthHTTPConnection from chainlib.eth.tx import Tx from chainlib.eth.block import Block @@ -43,18 +43,22 @@ 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('-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('tx_hash', type=str, help='Transaction hash') args = argparser.parse_args() - -if args.v: +if args.vv: logg.setLevel(logging.DEBUG) +elif args.v: + logg.setLevel(logging.INFO) -conn = HTTPConnection(args.p) +conn = EthHTTPConnection(args.p) tx_hash = args.tx_hash @@ -70,26 +74,19 @@ def main(): 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 - if tx_src['blockHash'] != None: - o = jsonrpc_template() - o['method'] = 'eth_getBlockByHash' - o['params'].append(tx_src['blockHash']) - o['params'].append(True) - block_src = conn.do(o) - block = Block(block_src) - for t in block.txs: - if t['hash'] == tx_hash: - tx = Tx(t, block) - break - o = jsonrpc_template() - o['method'] = 'eth_getTransactionReceipt' - o['params'].append(tx_hash) - rcpt = conn.do(o) - #status = int(strip_0x(rcpt['status']), 16) + + 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) diff --git a/chainlib/eth/runnable/transfer.py b/chainlib/eth/runnable/transfer.py index 70790d9..1c18713 100644 --- a/chainlib/eth/runnable/transfer.py +++ b/chainlib/eth/runnable/transfer.py @@ -11,26 +11,33 @@ # standard imports import os +import io import json import argparse import logging # third-party imports from crypto_dev_signer.eth.signer import ReferenceSigner as EIP155Signer -from crypto_dev_signer.keystore import DictKeystore +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 HTTPConnection -from chainlib.eth.rpc import jsonrpc_template -from chainlib.eth.nonce import DefaultNonceOracle -from chainlib.eth.gas import DefaultGasOracle -from chainlib.eth.erc20 import ERC20TxFactory +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.erc20 import ERC20 from chainlib.chain import ChainSpec +from chainlib.eth.runnable.util import decode_for_puny_humans logging.basicConfig(level=logging.WARNING) @@ -44,13 +51,15 @@ 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='Ethereum:1', help='Chain specification string') -argparser.add_argument('--token-address', required='True', dest='t', type=str, help='Token address') -argparser.add_argument('-a', '--sender-address', dest='s', type=str, help='Sender account address') +argparser.add_argument('-i', '--chain-spec', dest='i', type=str, default='evm:ethereum:1', help='Chain specification string') +argparser.add_argument('-a', '--token-address', required='True', dest='a', type=str, help='Token address') argparser.add_argument('-y', '--key-file', dest='y', type=str, help='Ethereum keystore file to use for signing') -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('--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('-s', '--send', dest='s', action='store_true', help='Send to network') +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('-v', action='store_true', help='Be verbose') argparser.add_argument('-vv', action='store_true', help='Be more verbose') argparser.add_argument('recipient', type=str, help='Recipient account address') @@ -73,29 +82,45 @@ passphrase = os.environ.get(passphrase_env) logg.error('pass {}'.format(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) + 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 = HTTPConnection(args.p) -nonce_oracle = DefaultNonceOracle(signer_address, conn) -gas_oracle = DefaultGasOracle(conn) +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) + +def _max_gas(code=None): + return 8000000 + +gas_oracle = None +if args.gas_price != None: + gas_oracle = OverrideGasOracle(args.gas_price, args.gas_limit) +else: + gas_oracle = RPCGasOracle(conn, code_callback=_max_gas) chain_spec = ChainSpec.from_chain_str(args.i) chain_id = chain_spec.network_id() value = args.amount -g = ERC20TxFactory(signer=signer, gas_oracle=gas_oracle, nonce_oracle=nonce_oracle, chain_id=chain_id) +send = args.s + +g = ERC20(chain_spec, signer=signer, gas_oracle=gas_oracle, nonce_oracle=nonce_oracle) def balance(token_address, address): - o = g.erc20_balance(token_address, address) + o = g.balance(token_address, address) r = conn.do(o) hx = strip_0x(r) return int(hx, 16) @@ -106,18 +131,31 @@ def main(): if not args.u and recipient != add_0x(args.recipient): raise ValueError('invalid checksum address') - logg.debug('sender {} balance before: {}'.format(signer_address, balance(args.t, signer_address))) - logg.debug('recipient {} balance before: {}'.format(recipient, balance(args.t, recipient))) + if logg.isEnabledFor(logging.DEBUG): + logg.debug('sender {} balance after: {}'.format(signer_address, balance(args.a, signer_address))) + logg.debug('recipient {} balance after: {}'.format(recipient, balance(args.a, recipient))) - (tx_hash_hex, o) = g.erc20_transfer(args.t, signer_address, recipient, value) - conn.do(o) + (tx_hash_hex, o) = g.transfer(args.a, signer_address, recipient, value) - if block_last: - conn.wait(tx_hash_hex) - logg.debug('sender {} balance after: {}'.format(signer_address, balance(args.t, signer_address))) - logg.debug('recipient {} balance after: {}'.format(recipient, balance(args.t, recipient))) + 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(args.a, signer_address))) + logg.debug('recipient {} balance after: {}'.format(recipient, balance(args.a, recipient))) + if r['status'] == 0: + logg.critical('VM revert. Wish I could tell you more') + sys.exit(1) + print(tx_hash_hex) - 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__': diff --git a/chainlib/eth/runnable/util.py b/chainlib/eth/runnable/util.py new file mode 100644 index 0000000..24b9494 --- /dev/null +++ b/chainlib/eth/runnable/util.py @@ -0,0 +1,22 @@ +# 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))) diff --git a/chainlib/eth/sign.py b/chainlib/eth/sign.py new file mode 100644 index 0000000..f91ca62 --- /dev/null +++ b/chainlib/eth/sign.py @@ -0,0 +1,23 @@ +# 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 diff --git a/chainlib/eth/tx.py b/chainlib/eth/tx.py index ca1b134..4035f1a 100644 --- a/chainlib/eth/tx.py +++ b/chainlib/eth/tx.py @@ -1,17 +1,18 @@ # standard imports import logging +import enum -# third-party imports +# external imports +import coincurve import sha3 from hexathon import ( strip_0x, add_0x, ) -from eth_keys import KeyAPI -from eth_keys.backends import NativeECCBackend 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 # local imports @@ -23,11 +24,21 @@ from .constant import ( MINIMUM_FEE_PRICE, ZERO_ADDRESS, ) -from .rpc import jsonrpc_template +from chainlib.jsonrpc import jsonrpc_template -logg = logging.getLogger(__name__) +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', @@ -40,20 +51,65 @@ field_debugs = [ 's', ] -def unpack(tx_raw_bytes, chain_id=1): +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(chain_id)) + logg.debug('decoding using chain id {}'.format(str(chain_id))) + j = 0 for i in d: - logg.debug('decoded {}: {}'.format(field_debugs[j], i.hex())) + 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 - s = b''.join([d[7], d[8], bytes([vb])]) - so = KeyAPI.Signature(signature_bytes=s) + 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)) @@ -67,8 +123,10 @@ def unpack(tx_raw_bytes, chain_id=1): h.update(rlp_encode(d)) unsigned_hash = h.digest() - p = so.recover_public_key_from_msg_hash(unsigned_hash) - a = p.to_checksum_address() + #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())) @@ -78,23 +136,43 @@ def unpack(tx_raw_bytes, chain_id=1): if to != None: to = to_checksum(to) + data = d[5].hex() + try: + data = add_0x(data) + except: + data = '0x' + return { 'from': a, - 'nonce': int.from_bytes(d[0], 'big'), - 'gasPrice': int.from_bytes(d[1], 'big'), - 'gas': int.from_bytes(d[2], 'big'), 'to': to, - 'value': int.from_bytes(d[4], 'big'), - 'data': '0x' + d[5].hex(), + 'nonce': d[0], + 'gasPrice': d[1], + 'gas': d[2], + 'value': d[4], + 'data': data, 'v': chain_id, - 'r': '0x' + s[:32].hex(), - 's': '0x' + s[32:64].hex(), + 'r': add_0x(sig[:32].hex()), + 's': add_0x(sig[32:64].hex()), 'chainId': chain_id, - 'hash': '0x' + signed_hash.hex(), - 'hash_unsigned': '0x' + unsigned_hash.hex(), + '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' @@ -102,12 +180,21 @@ def receipt(hsh): return o +def raw(tx_raw_hex): + o = jsonrpc_template() + o['method'] = 'eth_sendRawTransaction' + o['params'].append(tx_raw_hex) + return o + + class TxFactory: - def __init__(self, signer=None, gas_oracle=None, nonce_oracle=None, chain_id=1): + 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_id = chain_id + self.chain_spec = chain_spec self.signer = signer @@ -115,19 +202,15 @@ class TxFactory: if tx['to'] == None or tx['to'] == '': tx['to'] = '0x' txe = EIP155Transaction(tx, tx['nonce'], tx['chainId']) - self.signer.signTransaction(txe) - tx_raw = txe.rlp_serialize() + 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 = jsonrpc_template() - o['method'] = 'eth_sendRawTransaction' - o['params'].append(tx_raw_hex) - + o = raw(tx_raw_hex) return (tx_hash_hex, o) @@ -135,7 +218,7 @@ class TxFactory: gas_price = MINIMUM_FEE_PRICE gas_limit = MINIMUM_FEE_UNITS if self.gas_oracle != None: - (gas_price, gas_limit) = self.gas_oracle.get() + (gas_price, gas_limit) = self.gas_oracle.get_gas() logg.debug('using gas price {} limit {}'.format(gas_price, gas_limit)) nonce = 0 o = { @@ -145,10 +228,10 @@ class TxFactory: 'data': '0x', 'gasPrice': gas_price, 'gas': gas_limit, - 'chainId': self.chain_id, + 'chainId': self.chain_spec.chain_id(), } if self.nonce_oracle != None and use_nonce: - nonce = self.nonce_oracle.next() + nonce = self.nonce_oracle.next_nonce() logg.debug('using nonce {} for address {}'.format(nonce, sender)) o['nonce'] = nonce return o @@ -166,11 +249,22 @@ class TxFactory: } + 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: - logg.debug('using hardcoded gas limit of 8000000 until we have reliable vm executor') - tx['gas'] = 8000000 + 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 @@ -187,6 +281,7 @@ class Tx: self.gasPrice = int(strip_0x(src['gasPrice']), 16) self.gasLimit = int(strip_0x(src['gas']), 16) self.outputs = [to_checksum(address_from)] + self.contract = None inpt = src['input'] if inpt != '0x': @@ -201,7 +296,11 @@ class Tx: self.inputs = [to_checksum(strip_0x(to))] self.block = block - self.wire = src['raw'] + 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 @@ -217,6 +316,12 @@ class Tx: 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'] @@ -225,7 +330,7 @@ class Tx: def __str__(self): - return """hash {} + s = """hash {} from {} to {} value {} @@ -245,3 +350,11 @@ status {} self.payload, self.status.name, ) + + if self.contract != None: + s += """contract {} +""".format( + self.contract, + ) + return s + diff --git a/chainlib/eth/unittest/base.py b/chainlib/eth/unittest/base.py new file mode 100644 index 0000000..97314eb --- /dev/null +++ b/chainlib/eth/unittest/base.py @@ -0,0 +1,202 @@ +# 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() + +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_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) + tx_hash = block['transactions'][p[1]] + tx = self.eth_getTransaction([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_getTransactionReceipt(self, p): + rcpt = self.backend.get_transaction_receipt(p[0]) + # TODO: use camelcase to snake case converter + 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 diff --git a/chainlib/eth/unittest/ethtester.py b/chainlib/eth/unittest/ethtester.py new file mode 100644 index 0000000..1a918f4 --- /dev/null +++ b/chainlib/eth/unittest/ethtester.py @@ -0,0 +1,75 @@ +# 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 +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_location('custom', self.chain_spec, tag='default', constructor=rpc_with_tester, exist_ok=True) + RPCConnection.register_location('custom', self.chain_spec, tag='signer', constructor=rpc_with_tester, exist_ok=True) + + + + def tearDown(self): + pass diff --git a/chainlib/http.py b/chainlib/http.py new file mode 100644 index 0000000..c47aaba --- /dev/null +++ b/chainlib/http.py @@ -0,0 +1,35 @@ +import urllib +import base64 +import logging + +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): + url = req.get_full_url() + realm = None + user, pw = self.passwd.find_user_password(realm, url) + + if pw: + raw = "%s:%s" % (user, pw) + raw_bytes = raw.encode('utf-8') + auth_base_bytes = base64.encodebytes(raw_bytes) + auth_base = auth_base_bytes.decode('utf-8') + auth_base_clean = auth_base.replace('\n', '').strip() + auth = 'Basic %s' % auth_base_clean + req.add_unredirected_header(self.auth_header, auth) + logg.debug('head {}'.format(req.header_items())) + + return req + + https_request = http_request diff --git a/chainlib/jsonrpc.py b/chainlib/jsonrpc.py new file mode 100644 index 0000000..e823abf --- /dev/null +++ b/chainlib/jsonrpc.py @@ -0,0 +1,43 @@ +# standard imports +import uuid + +# local imports +from .error import JSONRPCException + + +class DefaultErrorParser: + + def translate(self, error): + return JSONRPCException('default parser code {}'.format(error)) + + +def jsonrpc_template(): + return { + 'jsonrpc': '2.0', + 'id': str(uuid.uuid4()), + 'method': None, + 'params': [], + } + +def jsonrpc_result(o, ep): + if o.get('error') != None: + raise ep.translate(o) + return o['result'] + + +def jsonrpc_response(request_id, result): + return { + 'jsonrpc': '2.0', + 'id': request_id, + 'result': result, + } + +def jsonrpc_error(request_id, code=-32000, message='Server error'): + return { + 'jsonrpc': '2.0', + 'id': request_id, + 'error': { + 'code': code, + 'message': message, + }, + } diff --git a/chainlib/status.py b/chainlib/status.py new file mode 100644 index 0000000..7db09e9 --- /dev/null +++ b/chainlib/status.py @@ -0,0 +1,7 @@ +# standard imports +import enum + +class Status(enum.Enum): + PENDING = 0 + SUCCESS = 1 + ERROR = 2 diff --git a/requirements.txt b/requirements.txt index 64d18c2..371fdf9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,5 @@ -crypto-dev-signer~=0.4.13rc2 +crypto-dev-signer~=0.4.14a17 pysha3==1.0.2 -hexathon~=0.0.1a3 -eth-abi==2.1.1 -eth-keys==0.3.3 +hexathon~=0.0.1a7 websocket-client==0.57.0 redis==3.5.3 diff --git a/setup.cfg b/setup.cfg index c8478e0..3404edc 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = chainlib -version = 0.0.1a19 +version = 0.0.2a1 description = Generic blockchain access library and tooling author = Louis Holbrook author_email = dev@holbrook.no @@ -30,6 +30,8 @@ packages = chainlib chainlib.eth chainlib.eth.runnable + chainlib.eth.pytest + chainlib.eth.unittest [options.entry_points] console_scripts = @@ -38,3 +40,4 @@ console_scripts = eth-gas = chainlib.eth.runnable.gas:main eth-transfer = chainlib.eth.runnable.transfer:main eth-get = chainlib.eth.runnable.get:main + eth-decode = chainlib.eth.runnable.decode:main diff --git a/setup.py b/setup.py index 54e70e7..c481db7 100644 --- a/setup.py +++ b/setup.py @@ -2,6 +2,7 @@ from setuptools import setup import configparser import os + requirements = [] f = open('requirements.txt', 'r') while True: @@ -11,6 +12,17 @@ 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, + install_requires=requirements, + tests_require=test_requirements, ) diff --git a/test_requirements.txt b/test_requirements.txt new file mode 100644 index 0000000..62ab455 --- /dev/null +++ b/test_requirements.txt @@ -0,0 +1,4 @@ +eth_tester==0.5.0b3 +py-evm==0.3.0a20 +rlp==2.0.1 +pytest==6.0.1 diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..3b9aa9e --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,25 @@ +# standard imports +import os + +# external imports +import pytest +from crypto_dev_signer.keystore.dict import DictKeystore +from crypto_dev_signer.eth.signer import ReferenceSigner as EIP155Signer + + +@pytest.fixture(scope='session') +def keystore(): + ks = DictKeystore() + + pk = os.urandom(32) + ks.import_raw_key(pk) + return ks + + +@pytest.fixture(scope='session') +def signer( + keystore, + ): + + s = EIP155Signer(keystore) + return s diff --git a/tests/test_abi.py b/tests/test_abi.py new file mode 100644 index 0000000..d1e00bc --- /dev/null +++ b/tests/test_abi.py @@ -0,0 +1,29 @@ +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() diff --git a/tests/test_erc20.py b/tests/test_erc20.py new file mode 100644 index 0000000..4af64d0 --- /dev/null +++ b/tests/test_erc20.py @@ -0,0 +1,74 @@ +# standard imports +import logging +import os + +# external imports +from hexathon import ( + strip_0x, + add_0x, + ) + +# local imports +from chainlib.eth.erc20 import ERC20 +from chainlib.eth.address import to_checksum_address +from chainlib.eth.tx import ( + unpack, + TxFormat, + ) +from chainlib.eth.pytest import * + +logg = logging.getLogger() + +contract_address = to_checksum_address('0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef') +benefactor_address = to_checksum_address('0xefdeadbeefdeadbeefdeadbeefdeadbeefdeadbe') + + +# TODO: use unittest instead +def test_erc20_balance( + default_chain_spec, + ): + e = ERC20(default_chain_spec,) + + holder_address = to_checksum_address('0xbeefdeadbeefdeadbeefdeadbeefdeadbeefdead') + o = e.balance_of(contract_address, holder_address) + assert len(o['params'][0]['data']) == 64 + 8 + 2 + assert o['params'][0]['data'][:10] == add_0x('70a08231') + + +def test_erc20_decimals( + default_chain_spec, + ): + e = ERC20(default_chain_spec) + + o = e.decimals(contract_address) + assert o['params'][0]['data'] == add_0x('313ce567') + + +def test_erc20_transfer( + keystore, + signer, + default_chain_spec, + ): + e = ERC20(default_chain_spec, signer=signer) + + addresses = keystore.list() + (tx_hash_hex, o) = e.transfer(contract_address, addresses[0], benefactor_address, 1024) + + +def test_erc20_parse_transfer_request( + keystore, + signer, + default_chain_spec, + ): + + e = ERC20(default_chain_spec, signer=signer) + + addresses = keystore.list() + (tx_hash_hex, o) = e.transfer(contract_address, addresses[0], benefactor_address, 1024, tx_format=TxFormat.RLP_SIGNED) + b = bytes.fromhex(strip_0x(o)) + + #chain_spec = ChainSpec('evm', 'foo', 1, 'bar') + tx = unpack(b, default_chain_spec) + r = ERC20.parse_transfer_request(tx['data']) + assert r[0] == benefactor_address + assert r[1] == 1024 diff --git a/tests/test_nonce.py b/tests/test_nonce.py new file mode 100644 index 0000000..f9332c8 --- /dev/null +++ b/tests/test_nonce.py @@ -0,0 +1,26 @@ +# 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() diff --git a/tests/test_sign.py b/tests/test_sign.py new file mode 100644 index 0000000..dc78168 --- /dev/null +++ b/tests/test_sign.py @@ -0,0 +1,123 @@ +# 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) + #mock_sig = os.urandom(64) + #tx.r = mock_sig[:32] + #tx.s = mock_sig[32:] + #r = add_0x(tx.rlp_serialize().hex()) + 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() diff --git a/tests/test_tx.py b/tests/test_tx.py new file mode 100644 index 0000000..548472d --- /dev/null +++ b/tests/test_tx.py @@ -0,0 +1,30 @@ +# 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()