diff --git a/ethjsonrpc.py b/ethjsonrpc.py index 248528f..b0e3831 100644 --- a/ethjsonrpc.py +++ b/ethjsonrpc.py @@ -1,12 +1,25 @@ -from ethereum.abi import ContractTranslator import serpent import requests import json +from ethereum import utils +from ethereum.abi import ContractTranslator, encode_abi, decode_abi -class EthJsonRpc: + +class EthJsonRpc(object): + + DEFAULT_GAS_FOR_TRANSACTIONS = 500000 + DEFAULT_GAS_PRICE = 10000 def __init__(self, host, port, contract_code=None, contract_address=None): + + # If we don't raise the exceptions below, it's kind of hard to identify + # the problem when any of these variables are null. + if host is None: + raise RuntimeError('RPC hostname cannot be null') + if port is None: + raise RuntimeError('RPC port cannot be null') + self.host = host self.port = port self.contract_code = None @@ -14,6 +27,22 @@ def __init__(self, host, port, contract_code=None, contract_address=None): self.translation = None self.contract_address = contract_address self.update_code(contract_code) + self.compilers = {} + try: + import serpent + self.compilers['serpent'] = serpent.compile + self.compilers['lll'] = serpent.compile_lll + except ImportError: + print "[WARNING] Could not import module 'serpent'. Compiler will not be available." + try: + import solidity + self.compilers['solidity'] = solidity.compile + except ImportError: + try: + from ethereum._solidity import solc_wrapper + self.compilers['solidity'] = solc_wrapper.compile + except ImportError: + print "[WARNING] Could not import module 'solidity or solc_wrapper'. Compiler will not be available." def update_code(self, contract_code): if contract_code: @@ -22,8 +51,8 @@ def update_code(self, contract_code): self.translation = ContractTranslator(self.signature) def _call(self, method, params=None, _id=0): - if params is None: - params = [] + + params = params or [] data = json.dumps({ 'jsonrpc': '2.0', 'method': method, @@ -36,6 +65,93 @@ def _call(self, method, params=None, _id=0): else: raise RuntimeError('Error from RPC call. Returned payload: {0}'.format(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 + + def _install_contract(self, language, contract_code, value=0, from_address=None, gas=None, gas_price=None): + byte_code = self.compilers[language](contract_code) + return self.eth_sendTransaction(data=byte_code, value=value, from_address=from_address, gas=gas, gas_price=gas_price) + + def install_solidity_contract(self, contract_code, value=0, from_address=None, gas=None, gas_price=None): + """ + Installs a solidity contract into ethereum node + """ + return self._install_contract('solidity', contract_code, value, from_address, gas, gas_price) + + def install_serpent_contract(self, contract_code, value=0, from_address=None, gas=None, gas_price=None): + """ + Installs a serpent contract into ethereum node + """ + return self._install_contract('serpent', contract_code, value, from_address, gas, gas_price) + + def install_lll_contract(self, contract_code, value=0, from_address=None, gas=None, gas_price=None): + """ + Installs a lll contract into ethereum node + """ + return self._install_contract('lll', contract_code, value, from_address, gas, gas_price) + + def contract_instant_call(self, to_address, function_signature, function_parameters=None, result_types=None, default_block="latest"): + """ + This method makes a instant call on a contract function without the need to have the contract source code. + Examples of function_signature in solidity: + mult(uint x, uint y) => sig: mult(uint256,uint256) (all uint should be transformed to uint256) + setAddress(address entity_address) => sig:setAddress(address) + doSomething() => sig: doSomething() (functions with no parameters must end with the '()') + In serpent, all functions parameter signatures are int256. Example: + setXYZ(x, y, z) => sig: setXYZ(int256,int256,int256) + """ + data = self._encode_function(function_signature, function_parameters) + params = [ + { + 'to': to_address, + 'data': '0x{0}'.format(data.encode('hex')) + }, + default_block + ] + response = self._call('eth_call', params) + return decode_abi(result_types, response[2:].decode('hex')) + + def contract_transaction_call(self, to_address, function_signature, function_parameters=None, from_address=None, gas=None, gas_price=None, default_block="latest"): + """ + This method makes a call on a contract function through a transaction. Returns the transaction_id. + Examples of function_signature in solidity: + mult(uint x, uint y) => sig: mult(uint256,uint256) (all uint should be transformed to uint256) + setAddress(address entity_address) => sig:setAddress(address) + doSomething() => sig: doSomething() (functions with no parameters must end with the '()') + In serpent, all functions parameter signatures are int256. Example: + setXYZ(x, y, z) => sig: setXYZ(int256,int256,int256) + """ + # Default values for gas and gas_price + gas = gas or self.DEFAULT_GAS_FOR_TRANSACTIONS + gas_price = gas_price or self.DEFAULT_GAS_PRICE + + # Default value for from_address + from_address = from_address or self.eth_accounts()[0] + + data = self._encode_function(function_signature, function_parameters) + + params = { + 'from': from_address, + 'to': to_address, + 'gas': '0x{0:x}'.format(gas), + 'gasPrice': '0x{0:x}'.format(gas_price), + 'value': None, + 'data': '0x{0}'.format(data.encode('hex')) if data else None + } + response = self._call('eth_sendTransaction', [params]) + return response + def create_contract(self, contract_code, value=0, from_address=None, gas=None, gas_price=None): self.update_code(contract_code) byte_code = serpent.compile(contract_code) @@ -48,24 +164,22 @@ def eth_sendTransaction(self, to_address=None, function_name=None, data=None, va Creates new message call transaction or a contract creation, if the data field contains code. """ # Default values for gas and gas_price - if gas is None: - gas = 500000 - if gas_price is None: - gas_price = 10000 + gas = gas or self.DEFAULT_GAS_FOR_TRANSACTIONS + gas_price = gas_price or self.DEFAULT_GAS_PRICE # Default value for from_address - if from_address is None: - from_address = self.eth_accounts()[0] + from_address = from_address or self.eth_accounts()[0] if function_name: if data is None: data = [] data = self.translation.encode(function_name, data) + params = { 'from': from_address, 'to': to_address, - 'gas': '0x{0:x}'.format(gas) if gas else None, - 'gasPrice': '0x{0:x}'.format(gas_price) if gas_price else None, + 'gas': '0x{0:x}'.format(gas), + 'gasPrice': '0x{0:x}'.format(gas_price), 'value': '0x{0:x}'.format(value) if value else None, 'data': '0x{0}'.format(data.encode('hex')) if data else None } @@ -75,8 +189,7 @@ def eth_call(self, to_address, function_name, data=None, code=None, default_bloc """ Executes a new message call immediately without creating a transaction on the block chain. """ - if data is None: - data = [] + data = data or [] data = self.translation.encode(function_name, data) params = [ { @@ -117,7 +230,7 @@ def net_listening(self): def net_peerCount(self): """ - Returns number of peers currenly connected to the client. + Returns number of peers currently connected to the client. """ return self._call('net_peerCount') @@ -297,7 +410,7 @@ def eth_newBlockFilter(self, default_block="latest"): def eth_uninstallFilter(self, filter_id): """ - Uninstalls a filter with given id. Should always be called when watch is no longer needed. Additonally Filters timeout when they aren't requested with eth_getFilterChanges for a period of time. + Uninstalls a filter with given id. Should always be called when watch is no longer needed. Additionally Filters timeout when they aren't requested with eth_getFilterChanges for a period of time. """ return self._call('eth_uninstallFilter', [filter_id]) @@ -412,7 +525,7 @@ def shh_newFilter(self, to, topics): def shh_uninstallFilter(self, filter_id): """ Uninstalls a filter with given id. Should always be called when watch is no longer needed. - Additonally Filters timeout when they aren't requested with shh_getFilterChanges for a period of time. + Additionally Filters timeout when they aren't requested with shh_getFilterChanges for a period of time. """ return self._call('shh_uninstallFilter', [filter_id])