import json import warnings import requests from requests.adapters import HTTPAdapter from requests.exceptions import ConnectionError as RequestsConnectionError from ethereum import utils from ethereum.abi import encode_abi, decode_abi from .constants import BLOCK_TAGS, BLOCK_TAG_LATEST from .utils import hex_to_dec, clean_hex, validate_block from .exceptions import (ConnectionError, BadStatusCodeError, BadJsonError, BadResponseError) GETH_DEFAULT_RPC_PORT = 8545 ETH_DEFAULT_RPC_PORT = 8545 PARITY_DEFAULT_RPC_PORT = 8545 PYETHAPP_DEFAULT_RPC_PORT = 4000 MAX_RETRIES = 3 JSON_MEDIA_TYPE = 'application/json' ''' This code is adapted from: https://github.com/ConsenSys/ethjsonrpc ''' class EthJsonRpc(object): ''' Ethereum JSON-RPC client class ''' DEFAULT_GAS_PER_TX = 90000 DEFAULT_GAS_PRICE = 50 * 10**9 # 50 gwei def __init__(self, host='localhost', port=GETH_DEFAULT_RPC_PORT, tls=False): self.host = host self.port = port self.tls = tls self.session = requests.Session() self.session.mount(self.host, HTTPAdapter(max_retries=MAX_RETRIES)) def _call(self, method, params=None, _id=1): params = params or [] data = { 'jsonrpc': '2.0', 'method': method, 'params': params, 'id': _id, } scheme = 'http' if self.tls: scheme += 's' url = '{}://{}:{}'.format(scheme, self.host, self.port) headers = {'Content-Type': JSON_MEDIA_TYPE} try: r = self.session.post(url, headers=headers, data=json.dumps(data)) except RequestsConnectionError: raise ConnectionError if r.status_code / 100 != 2: raise BadStatusCodeError(r.status_code) try: response = r.json() except ValueError: raise BadJsonError(r.text) try: return response['result'] except KeyError: raise BadResponseError(response) def _encode_function(self, signature, param_values): prefix = utils.big_endian_to_int(utils.sha3(signature)[:4]) if signature.find('(') == -1: raise RuntimeError('Invalid function signature. Missing "(" and/or ")"...') if signature.find(')') - signature.find('(') == 1: return utils.encode_int(prefix) types = signature[signature.find('(') + 1: signature.find(')')].split(',') encoded_params = encode_abi(types, param_values) return utils.zpad(utils.encode_int(prefix), 4) + encoded_params ################################################################################ # high-level methods ################################################################################ def transfer(self, from_, to, amount): ''' Send wei from one address to another ''' return self.eth_sendTransaction(from_address=from_, to_address=to, value=amount) def create_contract(self, from_, code, gas, sig=None, args=None): ''' Create a contract on the blockchain from compiled EVM code. Returns the transaction hash. ''' from_ = from_ or self.eth_coinbase() if sig is not None and args is not None: types = sig[sig.find('(') + 1: sig.find(')')].split(',') encoded_params = encode_abi(types, args) code += encoded_params.encode('hex') return self.eth_sendTransaction(from_address=from_, gas=gas, data=code) def get_contract_address(self, tx): ''' Get the address for a contract from the transaction that created it ''' receipt = self.eth_getTransactionReceipt(tx) return receipt['contractAddress'] def call(self, address, sig, args, result_types): ''' Call a contract function on the RPC server, without sending a transaction (useful for reading data) ''' data = self._encode_function(sig, args) data_hex = data.encode('hex') response = self.eth_call(to_address=address, data=data_hex) return decode_abi(result_types, response[2:].decode('hex')) def call_with_transaction(self, from_, address, sig, args, gas=None, gas_price=None, value=None): ''' Call a contract function by sending a transaction (useful for storing data) ''' gas = gas or self.DEFAULT_GAS_PER_TX gas_price = gas_price or self.DEFAULT_GAS_PRICE data = self._encode_function(sig, args) data_hex = data.encode('hex') return self.eth_sendTransaction(from_address=from_, to_address=address, data=data_hex, gas=gas, gas_price=gas_price, value=value) ################################################################################ # JSON-RPC methods ################################################################################ def web3_clientVersion(self): ''' https://github.com/ethereum/wiki/wiki/JSON-RPC#web3_clientversion TESTED ''' return self._call('web3_clientVersion') def web3_sha3(self, data): ''' https://github.com/ethereum/wiki/wiki/JSON-RPC#web3_sha3 TESTED ''' data = str(data).encode('hex') return self._call('web3_sha3', [data]) def net_version(self): ''' https://github.com/ethereum/wiki/wiki/JSON-RPC#net_version TESTED ''' return self._call('net_version') def net_listening(self): ''' https://github.com/ethereum/wiki/wiki/JSON-RPC#net_listening TESTED ''' return self._call('net_listening') def net_peerCount(self): ''' https://github.com/ethereum/wiki/wiki/JSON-RPC#net_peercount TESTED ''' return hex_to_dec(self._call('net_peerCount')) def eth_protocolVersion(self): ''' https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_protocolversion TESTED ''' return self._call('eth_protocolVersion') def eth_syncing(self): ''' https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_syncing TESTED ''' return self._call('eth_syncing') def eth_coinbase(self): ''' https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_coinbase TESTED ''' return self._call('eth_coinbase') def eth_mining(self): ''' https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_mining TESTED ''' return self._call('eth_mining') def eth_hashrate(self): ''' https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_hashrate TESTED ''' return hex_to_dec(self._call('eth_hashrate')) def eth_gasPrice(self): ''' https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_gasprice TESTED ''' return hex_to_dec(self._call('eth_gasPrice')) def eth_accounts(self): ''' https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_accounts TESTED ''' return self._call('eth_accounts') def eth_blockNumber(self): ''' https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_blocknumber TESTED ''' return hex_to_dec(self._call('eth_blockNumber')) def eth_getBalance(self, address=None, block=BLOCK_TAG_LATEST): ''' https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_getbalance TESTED ''' address = address or self.eth_coinbase() block = validate_block(block) return hex_to_dec(self._call('eth_getBalance', [address, block])) def eth_getStorageAt(self, address=None, position=0, block=BLOCK_TAG_LATEST): ''' https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_getstorageat TESTED ''' block = validate_block(block) return self._call('eth_getStorageAt', [address, hex(position), block]) def eth_getTransactionCount(self, address, block=BLOCK_TAG_LATEST): ''' https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_gettransactioncount TESTED ''' block = validate_block(block) return hex_to_dec(self._call('eth_getTransactionCount', [address, block])) def eth_getBlockTransactionCountByHash(self, block_hash): ''' https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_getblocktransactioncountbyhash TESTED ''' return hex_to_dec(self._call('eth_getBlockTransactionCountByHash', [block_hash])) def eth_getBlockTransactionCountByNumber(self, block=BLOCK_TAG_LATEST): ''' https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_getblocktransactioncountbynumber TESTED ''' block = validate_block(block) return hex_to_dec(self._call('eth_getBlockTransactionCountByNumber', [block])) def eth_getUncleCountByBlockHash(self, block_hash): ''' https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_getunclecountbyblockhash TESTED ''' return hex_to_dec(self._call('eth_getUncleCountByBlockHash', [block_hash])) def eth_getUncleCountByBlockNumber(self, block=BLOCK_TAG_LATEST): ''' https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_getunclecountbyblocknumber TESTED ''' block = validate_block(block) return hex_to_dec(self._call('eth_getUncleCountByBlockNumber', [block])) def eth_getCode(self, address, default_block=BLOCK_TAG_LATEST): ''' https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_getcode NEEDS TESTING ''' if isinstance(default_block, basestring): if default_block not in BLOCK_TAGS: raise ValueError return self._call('eth_getCode', [address, default_block]) def eth_sign(self, address, data): ''' https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_sign NEEDS TESTING ''' return self._call('eth_sign', [address, data]) def eth_sendTransaction(self, to_address=None, from_address=None, gas=None, gas_price=None, value=None, data=None, nonce=None): ''' https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_sendtransaction NEEDS TESTING ''' params = {} params['from'] = from_address or self.eth_coinbase() if to_address is not None: params['to'] = to_address if gas is not None: params['gas'] = hex(gas) if gas_price is not None: params['gasPrice'] = clean_hex(gas_price) if value is not None: params['value'] = clean_hex(value) if data is not None: params['data'] = data if nonce is not None: params['nonce'] = hex(nonce) return self._call('eth_sendTransaction', [params]) def eth_sendRawTransaction(self, data): ''' https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_sendrawtransaction NEEDS TESTING ''' return self._call('eth_sendRawTransaction', [data]) def eth_call(self, to_address, from_address=None, gas=None, gas_price=None, value=None, data=None, default_block=BLOCK_TAG_LATEST): ''' https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_call NEEDS TESTING ''' if isinstance(default_block, basestring): if default_block not in BLOCK_TAGS: raise ValueError obj = {} obj['to'] = to_address if from_address is not None: obj['from'] = from_address if gas is not None: obj['gas'] = hex(gas) if gas_price is not None: obj['gasPrice'] = clean_hex(gas_price) if value is not None: obj['value'] = value if data is not None: obj['data'] = data return self._call('eth_call', [obj, default_block]) def eth_estimateGas(self, to_address=None, from_address=None, gas=None, gas_price=None, value=None, data=None, default_block=BLOCK_TAG_LATEST): ''' https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_estimategas NEEDS TESTING ''' if isinstance(default_block, basestring): if default_block not in BLOCK_TAGS: raise ValueError obj = {} if to_address is not None: obj['to'] = to_address if from_address is not None: obj['from'] = from_address if gas is not None: obj['gas'] = hex(gas) if gas_price is not None: obj['gasPrice'] = clean_hex(gas_price) if value is not None: obj['value'] = value if data is not None: obj['data'] = data return hex_to_dec(self._call('eth_estimateGas', [obj, default_block])) def eth_getBlockByHash(self, block_hash, tx_objects=True): ''' https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_getblockbyhash TESTED ''' return self._call('eth_getBlockByHash', [block_hash, tx_objects]) def eth_getBlockByNumber(self, block=BLOCK_TAG_LATEST, tx_objects=True): ''' https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_getblockbynumber TESTED ''' block = validate_block(block) return self._call('eth_getBlockByNumber', [block, tx_objects]) def eth_getTransactionByHash(self, tx_hash): ''' https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_gettransactionbyhash TESTED ''' return self._call('eth_getTransactionByHash', [tx_hash]) def eth_getTransactionByBlockHashAndIndex(self, block_hash, index=0): ''' https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_gettransactionbyblockhashandindex TESTED ''' return self._call('eth_getTransactionByBlockHashAndIndex', [block_hash, hex(index)]) def eth_getTransactionByBlockNumberAndIndex(self, block=BLOCK_TAG_LATEST, index=0): ''' https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_gettransactionbyblocknumberandindex TESTED ''' block = validate_block(block) return self._call('eth_getTransactionByBlockNumberAndIndex', [block, hex(index)]) def eth_getTransactionReceipt(self, tx_hash): ''' https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_gettransactionreceipt TESTED ''' return self._call('eth_getTransactionReceipt', [tx_hash]) def eth_getUncleByBlockHashAndIndex(self, block_hash, index=0): ''' https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_getunclebyblockhashandindex TESTED ''' return self._call('eth_getUncleByBlockHashAndIndex', [block_hash, hex(index)]) def eth_getUncleByBlockNumberAndIndex(self, block=BLOCK_TAG_LATEST, index=0): ''' https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_getunclebyblocknumberandindex TESTED ''' block = validate_block(block) return self._call('eth_getUncleByBlockNumberAndIndex', [block, hex(index)]) def eth_getCompilers(self): ''' https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_getcompilers TESTED ''' return self._call('eth_getCompilers') def eth_compileSolidity(self, code): ''' https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_compilesolidity TESTED ''' return self._call('eth_compileSolidity', [code]) def eth_compileLLL(self, code): ''' https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_compilelll N/A ''' return self._call('eth_compileLLL', [code]) def eth_compileSerpent(self, code): ''' https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_compileserpent N/A ''' return self._call('eth_compileSerpent', [code]) def eth_newFilter(self, from_block=BLOCK_TAG_LATEST, to_block=BLOCK_TAG_LATEST, address=None, topics=None): ''' https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_newfilter NEEDS TESTING ''' _filter = { 'fromBlock': from_block, 'toBlock': to_block, 'address': address, 'topics': topics, } return self._call('eth_newFilter', [_filter]) def eth_newBlockFilter(self): ''' https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_newblockfilter TESTED ''' return self._call('eth_newBlockFilter') def eth_newPendingTransactionFilter(self): ''' https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_newpendingtransactionfilter TESTED ''' return hex_to_dec(self._call('eth_newPendingTransactionFilter')) def eth_uninstallFilter(self, filter_id): ''' https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_uninstallfilter NEEDS TESTING ''' return self._call('eth_uninstallFilter', [filter_id]) def eth_getFilterChanges(self, filter_id): ''' https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_getfilterchanges NEEDS TESTING ''' return self._call('eth_getFilterChanges', [filter_id]) def eth_getFilterLogs(self, filter_id): ''' https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_getfilterlogs NEEDS TESTING ''' return self._call('eth_getFilterLogs', [filter_id]) def eth_getLogs(self, filter_object): ''' https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_getlogs NEEDS TESTING ''' return self._call('eth_getLogs', [filter_object]) def eth_getWork(self): ''' https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_getwork TESTED ''' return self._call('eth_getWork') def eth_submitWork(self, nonce, header, mix_digest): ''' https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_submitwork NEEDS TESTING ''' return self._call('eth_submitWork', [nonce, header, mix_digest]) def eth_submitHashrate(self, hash_rate, client_id): ''' https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_submithashrate TESTED ''' return self._call('eth_submitHashrate', [hex(hash_rate), client_id]) def db_putString(self, db_name, key, value): ''' https://github.com/ethereum/wiki/wiki/JSON-RPC#db_putstring TESTED ''' warnings.warn('deprecated', DeprecationWarning) return self._call('db_putString', [db_name, key, value]) def db_getString(self, db_name, key): ''' https://github.com/ethereum/wiki/wiki/JSON-RPC#db_getstring TESTED ''' warnings.warn('deprecated', DeprecationWarning) return self._call('db_getString', [db_name, key]) def db_putHex(self, db_name, key, value): ''' https://github.com/ethereum/wiki/wiki/JSON-RPC#db_puthex TESTED ''' if not value.startswith('0x'): value = '0x{}'.format(value) warnings.warn('deprecated', DeprecationWarning) return self._call('db_putHex', [db_name, key, value]) def db_getHex(self, db_name, key): ''' https://github.com/ethereum/wiki/wiki/JSON-RPC#db_gethex TESTED ''' warnings.warn('deprecated', DeprecationWarning) return self._call('db_getHex', [db_name, key]) def shh_version(self): ''' https://github.com/ethereum/wiki/wiki/JSON-RPC#shh_version N/A ''' return self._call('shh_version') def shh_post(self, topics, payload, priority, ttl, from_=None, to=None): ''' https://github.com/ethereum/wiki/wiki/JSON-RPC#shh_post NEEDS TESTING ''' whisper_object = { 'from': from_, 'to': to, 'topics': topics, 'payload': payload, 'priority': hex(priority), 'ttl': hex(ttl), } return self._call('shh_post', [whisper_object]) def shh_newIdentity(self): ''' https://github.com/ethereum/wiki/wiki/JSON-RPC#shh_newidentity N/A ''' return self._call('shh_newIdentity') def shh_hasIdentity(self, address): ''' https://github.com/ethereum/wiki/wiki/JSON-RPC#shh_hasidentity NEEDS TESTING ''' return self._call('shh_hasIdentity', [address]) def shh_newGroup(self): ''' https://github.com/ethereum/wiki/wiki/JSON-RPC#shh_newgroup N/A ''' return self._call('shh_newGroup') def shh_addToGroup(self): ''' https://github.com/ethereum/wiki/wiki/JSON-RPC#shh_addtogroup NEEDS TESTING ''' return self._call('shh_addToGroup') def shh_newFilter(self, to, topics): ''' https://github.com/ethereum/wiki/wiki/JSON-RPC#shh_newfilter NEEDS TESTING ''' _filter = { 'to': to, 'topics': topics, } return self._call('shh_newFilter', [_filter]) def shh_uninstallFilter(self, filter_id): ''' https://github.com/ethereum/wiki/wiki/JSON-RPC#shh_uninstallfilter NEEDS TESTING ''' return self._call('shh_uninstallFilter', [filter_id]) def shh_getFilterChanges(self, filter_id): ''' https://github.com/ethereum/wiki/wiki/JSON-RPC#shh_getfilterchanges NEEDS TESTING ''' return self._call('shh_getFilterChanges', [filter_id]) def shh_getMessages(self, filter_id): ''' https://github.com/ethereum/wiki/wiki/JSON-RPC#shh_getmessages NEEDS TESTING ''' return self._call('shh_getMessages', [filter_id]) def getBlockRlp(self, number=0): return self._call('debug_getBlockRlp', [number]) def traceTransaction(self, txHash): return self._call('debug_traceTransaction', [txHash])