commit 6cbb86ca16ba075a2b041549288d78bb830163e7
parent 1f19aecd0e827da1a045b123b779d83745b4ad34
Author: Louis Holbrook <accounts-gitlab@holbrook.no>
Date: Sun, 4 Apr 2021 12:55:27 +0000
Merge branch 'lash/abi-encoder' into 'master'
Lash/abi encoder
See merge request nolash/chainlib!1
Diffstat:
43 files changed, 2138 insertions(+), 330 deletions(-)
diff --git a/chainlib/block.py 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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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:
-
- 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)
+class EthHTTPConnection(JSONRPCHTTPConnection):
-
- 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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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, conn):
+ def __init__(self, address):
self.address = address
- self.conn = conn
- self.nonce = self.get()
+ self.nonce = self.get_nonce()
- def get(self):
- o = nonce(self.address)
- r = self.conn.do(o)
- n = strip_0x(r)
- return int(n, 16)
+ def get_nonce(self):
+ raise NotImplementedError('Class must be extended')
- def next(self):
+ def next_nonce(self):
n = self.nonce
self.nonce += 1
return n
-class OverrideNonceOracle(DefaultNonceOracle):
+class RPCNonceOracle(NonceOracle):
+
+ def __init__(self, address, conn):
+ self.conn = conn
+ super(RPCNonceOracle, self).__init__(address)
+
+
+ def get_nonce(self):
+ o = nonce(self.address)
+ r = self.conn.do(o)
+ n = strip_0x(r)
+ return int(n, 16)
+
+
+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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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')
-def main():
- account = to_checksum(args.account)
- if not args.u and account != add_0x(args.account):
- raise ValueError('invalid checksum address')
+token_symbol = 'eth'
+
+chain_spec = ChainSpec.from_chain_str(args.i)
+def main():
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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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)))
-
- (tx_hash_hex, o) = g.erc20_transfer(args.t, signer_address, recipient, value)
- conn.do(o)
-
- 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)))
-
- print(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)))
+
+ (tx_hash_hex, o) = g.transfer(args.a, signer_address, recipient, value)
+
+ 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)
+
+ 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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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()