chainlib-eth/chainlib/eth/connection.py

236 lines
7.6 KiB
Python

# standard imports
import copy
import logging
import json
import datetime
import time
import socket
from urllib.request import (
Request,
urlopen,
)
# third-party imports
from hexathon import (
add_0x,
strip_0x,
)
# local imports
from .error import RevertEthException
from chainlib.eth.dialect import DefaultErrorParser
from .sign import (
sign_transaction,
)
from chainlib.connection import (
ConnType,
RPCConnection,
JSONRPCHTTPConnection,
JSONRPCUnixConnection,
error_parser,
)
from chainlib.jsonrpc import (
JSONRPCRequest,
jsonrpc_result,
)
from chainlib.eth.tx import (
unpack,
)
from potaahto.symbols import snake_and_camel
logg = logging.getLogger(__name__)
class EthHTTPConnection(JSONRPCHTTPConnection):
"""HTTP Interface for Ethereum node JSON-RPC
:todo: support https
"""
def wait(self, tx_hash_hex, delay=0.5, timeout=0.0, error_parser=error_parser, id_generator=None):
"""Poll for confirmation of a transaction on network.
Returns the result of the transaction if it was successfully executed on the network, and raises RevertEthException if execution fails.
This is a blocking call.
:param tx_hash_hex: Transaction hash to wait for, hex
:type tx_hash_hex: str
:param delay: Polling interval
:type delay: float
:param timeout: Max time to wait for confirmation (0 = no timeout)
:type timeout: float
:param error_parser: json-rpc response error parser
:type error_parser: chainlib.jsonrpc.ErrorParser
:param id_generator: json-rpc id generator
:type id_generator: chainlib.jsonrpc.JSONRPCIdGenerator
:raises TimeoutError: Timeout reached
:raises chainlib.eth.error.RevertEthException: Transaction confirmed but failed
:rtype: dict
:returns: Transaction receipt
"""
t = datetime.datetime.utcnow()
i = 0
while True:
j = JSONRPCRequest(id_generator)
o = j.template()
o['method'] ='eth_getTransactionReceipt'
o['params'].append(add_0x(tx_hash_hex))
o = j.finalize(o)
req = Request(
self.location,
method='POST',
)
req.add_header('Content-Type', 'application/json')
data = json.dumps(o)
logg.debug('({}) poll receipt attempt {} {}'.format(str(self), i, data))
res = urlopen(req, data=data.encode('utf-8'))
r = json.load(res)
e = jsonrpc_result(r, error_parser)
if e != None:
e = snake_and_camel(e)
# In openethereum we encounter receipts that have NONE block hashes and numbers. WTF...
logg.debug('({}) poll receipt received {}'.format(str(self), r))
if e['block_hash'] == None:
logg.warning('poll receipt attempt {} returned receipt but with a null block hash value!'.format(i))
else:
logg.debug('e {}'.format(strip_0x(e['status'])))
if strip_0x(e['status']) == '00':
raise RevertEthException(tx_hash_hex)
return e
if timeout > 0.0:
delta = (datetime.datetime.utcnow() - t) + datetime.timedelta(seconds=delay)
if delta.total_seconds() >= timeout:
raise TimeoutError(tx_hash)
time.sleep(delay)
i += 1
def __str__(self):
return 'ETH HTTP JSONRPC'
def check_rpc(self, id_generator=None):
"""Execute Ethereum specific json-rpc query to (superficially) check whether node is sane.
:param id_generator: json-rpc id generator
:type id_generator: chainlib.jsonrpc.JSONRPCIdGenerator
:raises Exception: Any exception indicates an invalid node
"""
j = JSONRPCRequest(id_generator)
req = j.template()
req['method'] = 'net_version'
req = j.finalize(req)
r = self.do(req)
class EthUnixConnection(JSONRPCUnixConnection):
"""Unix socket implementation of Ethereum JSON-RPC
"""
def wait(self, tx_hash_hex, delay=0.5, timeout=0.0, error_parser=error_parser):
"""See EthHTTPConnection. Not yet implemented for unix socket.
"""
raise NotImplementedError('Not yet implemented for unix socket')
def sign_transaction_to_rlp(chain_spec, doer, tx):
"""Generate a signature query and execute it against a json-rpc signer backend.
Uses the `eth_signTransaction` json-rpc method, generated by chainlib.eth.sign.sign_transaction.
:param chain_spec: Chain spec to use for EIP155 signature.
:type chain_spec: chainlib.chain.ChainSpec
:param doer: Signer rpc backend
:type doer: chainlib.connection.RPCConnection implementing json-rpc
:param tx: Transaction object
:type tx: dict
:rtype: bytes
:returns: Ethereum signature
"""
txs = tx.serialize()
logg.debug('serializing {}'.format(txs))
# TODO: because some rpc servers may fail when chainId is included, we are forced to spend cpu here on this
chain_id = txs.get('chainId') or 1
if chain_spec != None:
chain_id = 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 = doer(o)
logg.debug('sig got {}'.format(r))
return bytes.fromhex(strip_0x(r))
def sign_message(doer, msg):
"""Sign arbitrary data using the Ethereum message signer protocol.
:param doer: Signer rpc backend
:type doer: chainlib.connection.RPCConnection with json-rpc
:param msg: Message to sign, in hex
:type msg: str
:rtype: str
:returns: Signature, hex
"""
o = sign_message(msg)
return doer(o)
class EthUnixSignerConnection(EthUnixConnection):
"""Connects rpc signer methods to Unix socket connection interface
"""
def sign_transaction_to_wire(self, tx):
"""Sign transaction using unix socket rpc.
:param tx: Transaction object
:type tx: dict
:rtype: See chainlib.eth.connection.sign_transaction_to_rlp
:returns: Serialized signature
"""
return sign_transaction_to_rlp(self.chain_spec, self.do, tx)
def sign_message(self, msg):
"""Sign message using unix socket json-rpc.
:param msg: Message to sign, in hex
:type msg: str
:rtype: See chainlin.eth.connection.sign_message
:returns: See chainlin.eth.connection.sign_message
"""
return sign_message(self.do, msg)
class EthHTTPSignerConnection(EthHTTPConnection):
def sign_transaction_to_wire(self, tx):
"""Sign transaction using http json-rpc.
:param tx: Transaction object
:type tx: dict
:rtype: See chainlin.eth.connection.sign_transaction_to_rlp
:returns: Serialized signature
"""
return sign_transaction_to_rlp(self.chain_spec, self.do, tx)
def sign_message(self, tx):
"""Sign message using http json-rpc.
:param msg: Message to sign, in hex
:type msg: str
:rtype: See chainlin.eth.connection.sign_message
:returns: See chainlin.eth.connection.sign_message
"""
return sign_message(self.do, tx)
RPCConnection.register_constructor(ConnType.HTTP, EthHTTPConnection, tag='eth_default')
RPCConnection.register_constructor(ConnType.HTTP_SSL, EthHTTPConnection, tag='eth_default')
RPCConnection.register_constructor(ConnType.UNIX, EthUnixConnection, tag='eth_default')