class MQTTRPC(MQTTClient): client_uid = CLIENT_UID cleansession = True mqtt_reply_timeout = MQTT_REPLY_TIMEOUT mqtt_url = MQTT_URL request_count = 0 rpc_replies = {} replied = Event( ) # This event is triggered every time a new reply has come. subscriptions = [ ] # We hold a list of our subscriptions not to subscribe to # every request to the same client. def __init__(self, mqtt_url=None, client_uid=None, loop=None, config=None): if not loop: loop = asyncio.get_event_loop() self.loop = loop self.protocol = JSONRPCProtocol() self.dispatcher = dispatcher self.config = config if mqtt_url: self.mqtt_url = mqtt_url if client_uid: self.client_uid = client_uid super(MQTTRPC, self).__init__(client_id=self.client_uid, loop=loop, config=self.config) for signame in ('SIGINT', 'SIGTERM'): self.loop.add_signal_handler( getattr(signal, signame), lambda: asyncio.ensure_future(self.stop())) logger.info('Client {} initialized'.format(self.client_uid)) async def stop(self): logger.info('Stopping mqttrpc...') # Check subscriptions if self._connected_state.is_set(): await self.unsubscribe(self.subscriptions) await self.disconnect() tasks = [ task for task in asyncio.Task.all_tasks() if task is not asyncio.tasks.Task.current_task() ] list(map(lambda task: task.cancel(), tasks)) results = await asyncio.gather(*tasks, return_exceptions=True) logger.debug('Finished cancelling tasks, result: {}'.format(results)) self.loop.stop() async def process_messages(self): self.mqtt_url = self.config.get('broker', {}).get('uri', self.mqtt_url) logger.info('Connecting to {}'.format(self.mqtt_url)) await self.connect(self.mqtt_url, cleansession=self.cleansession) logger.info('Connected.') await self.subscribe([ ('rpc/{}/+'.format(self.client_uid), QOS_2), ]) logger.debug('Starting process messages.') while True: try: self.loop.create_task( self.process_message(await self.deliver_message())) except asyncio.CancelledError: return async def process_message(self, message): logger.debug('Message at topic {}'.format(message.topic)) if re.search('^rpc/(\w+)/(\w+)$', message.topic): # RPC request logger.debug('RPC request at {}'.format(message.topic)) _, _, context = message.topic.split('/') data_str = message.data.decode() await self.receive_rpc_request(context, data_str) elif re.search('^rpc/(\w+)/(\w+)/reply$', message.topic): # RPC reply logger.debug('RPC reply at {}'.format(message.topic)) _, _, context, _ = message.topic.split('/') data_str = message.data.decode() waiting_replies = self.rpc_replies.get(message.topic) if not waiting_replies: logger.warning( 'Got an unexpected RPC reply from {}: {}'.format( message.topic, data_str)) else: try: data_js = json.loads(data_str) except json.decoder.JSONDecodeError: logger.error( 'RPC reply bad json data: {}'.format(data_str)) else: request_id = data_js.get('id') if request_id not in waiting_replies.keys(): logger.warning( 'Got a reply from {} to bad request id: {}'.format( message.topic, data_str)) else: # Finally matched the request by id logger.debug( 'Waiting reply found for request {}'.format( request_id)) await waiting_replies[request_id].put(data_str) else: logger.debug('Passing to on_message handler') await self.on_message(message) async def on_message(self, message): # Override it to implement other handlres. logger.debug('Not implemented') async def receive_rpc_request(self, context, data): logger.debug('Request: {}'.format(data)) self.request_count += 1 if type(data) != str: data = json.dumps(data) message = data async def handle_message(context, message): try: request = self.protocol.parse_request(message) except RPCError as e: response = e.error_respond() else: # Hack to add first params as self if not self in request.args: request.args.insert(0, self) response = await self.dispatcher.dispatch( request, getattr(self.protocol, '_caller', None)) # send reply if response is not None: result = response.serialize() logger.debug('RPC reply to {}: {}'.format(context, result)) self.loop.create_task( self.publish( 'rpc/{}/{}/reply'.format(self.client_uid, context), result.encode())) await handle_message(context, message) def get_proxy_for(self, destination, one_way=False): return RPCProxy(self, destination, one_way) async def _send_and_handle_reply(self, destination, req, one_way, no_exception=False): # Convert to bytes and send to destination if one_way: # We do not need a reply it's a notification call await self.publish( 'rpc/{}/{}'.format(destination, self.client_uid), req.serialize().encode()) return # We need a reply reply_topic = ('rpc/{}/{}/reply'.format(destination, self.client_uid)) self.rpc_replies.setdefault(reply_topic, {})[req.unique_id] = Queue() if reply_topic not in self.subscriptions: logger.debug( 'Adding subscrption to reply topic {}'.format(reply_topic)) self.subscriptions.append(reply_topic) await self.subscribe([(reply_topic, QOS_2)]) logger.debug('Subscribed to reply topic {}'.format(reply_topic)) else: logger.debug('Already subscribed for topic {}'.format(reply_topic)) await self.publish('rpc/{}/{}'.format(destination, self.client_uid), req.serialize().encode()) logger.debug('Published request id {} to {}'.format( req.unique_id, destination)) try: reply_data = await asyncio.wait_for( self.rpc_replies[reply_topic][req.unique_id].get(), self.mqtt_reply_timeout) self.rpc_replies[reply_topic][req.unique_id].task_done() except asyncio.TimeoutError: del self.rpc_replies[reply_topic][req.unique_id] raise RPCError( 'Reply Timeout, topic {}, id {}, method {}, args {}, kwargs {}' .format(reply_topic, req.unique_id, req.method, req.args, req.kwargs)) else: # We got a reply, handle it. logger.debug('Got a reply for request id: {}'.format( req.unique_id)) rpc_response = self.protocol.parse_reply(reply_data) del self.rpc_replies[reply_topic][req.unique_id] # Check that there is no RPC errors. if not no_exception and hasattr(rpc_response, 'error'): raise RPCError('Error calling remote procedure: %s' %\ rpc_response.error) return rpc_response async def _call(self, destination, method, args, kwargs, one_way=False): req = self.protocol.create_request(method, args, kwargs, one_way) rep = await self._send_and_handle_reply(destination, req, one_way) if one_way: return return rep.result
class RPCClient(object): """Client for making RPC calls to connected servers. :param protocol: An :py:class:`~tinyrpc.RPCProtocol` instance. :param transport: A :py:class:`~tinyrpc.transports.ClientTransport` instance. """ JSON_RPC_VERSION = "2.0" _ALLOWED_REPLY_KEYS = sorted(['id', 'jsonrpc', 'error', 'result']) _ALLOWED_REQUEST_KEYS = sorted(['id', 'jsonrpc', 'method', 'params']) def parse_reply(self, data): try: rep = json.loads(data) except Exception as e: raise InvalidReplyError(e) for k in rep.keys(): if not k in self._ALLOWED_REPLY_KEYS: raise InvalidReplyError('Key not allowed: %s' % k) if not 'jsonrpc' in rep: raise InvalidReplyError('Missing jsonrpc (version) in response.') if rep['jsonrpc'] != self.JSON_RPC_VERSION: raise InvalidReplyError('Wrong JSONRPC version') if not 'id' in rep: raise InvalidReplyError('Missing id in response') if ('error' in rep) == ('result' in rep): raise InvalidReplyError( 'Reply must contain exactly one of result and error.') if 'error' in rep: response = JSONRPCErrorResponse() error = rep['error'] response.error = error['message'] response._jsonrpc_error_code = error['code'] else: response = JSONRPCSuccessResponse() response.result = rep.get('result', None) response.unique_id = rep['id'] return response def __init__(self, url): self.protocol = JSONRPCProtocol() self.url = url def _send_and_handle_reply(self, req): # print (req.serialize()) # show JSON string headers = {'content-type': 'application/json'} reply = requests.post(self.url, req.serialize(), headers=headers) # print (reply.json()); response = self.parse_reply(str(reply.json()).replace("'", '"')) if hasattr(response, 'error'): raise RPCError('Error calling remote procedure: %s' % \ response.error) return response def call(self, method, args, kwargs, one_way=False): """Calls the requested method and returns the result. If an error occured, an :py:class:`~tinyrpc.exc.RPCError` instance is raised. :param method: Name of the method to call. :param args: Arguments to pass to the method. :param kwargs: Keyword arguments to pass to the method. :param one_way: Whether or not a reply is desired. """ req = self.protocol.create_request(method, args, kwargs, one_way) return self._send_and_handle_reply(req).result def get_proxy(self, prefix='', one_way=False): """Convenience method for creating a proxy. :param prefix: Passed on to :py:class:`~tinyrpc.client.RPCProxy`. :param one_way: Passed on to :py:class:`~tinyrpc.client.RPCProxy`. :return: :py:class:`~tinyrpc.client.RPCProxy` instance.""" return RPCProxy(self, prefix, one_way) def batch_call(self, calls): """Experimental, use at your own peril.""" req = self.protocol.create_batch_request() for call_args in calls: req.append(self.protocol.create_request(*call_args)) return self._send_and_handle_reply(req)
class JSONRPCClient(object): """ Ethereum JSON RPC client. Args: host (str): Ethereum node host address. port (int): Ethereum node port number. privkey (bin): Local user private key, used to sign transactions. nonce_update_interval (float): Update the account nonce every `nonce_update_interval` seconds. nonce_offset (int): Network's default base nonce number. """ def __init__(self, host, port, privkey, nonce_update_interval=5.0, nonce_offset=0): endpoint = 'http://{}:{}'.format(host, port) session = requests.Session() adapter = requests.adapters.HTTPAdapter(pool_maxsize=50) session.mount(endpoint, adapter) self.transport = HttpPostClientTransport( endpoint, post_method=session.post, headers={'content-type': 'application/json'}, ) self.port = port self.privkey = privkey self.protocol = JSONRPCProtocol() self.sender = privatekey_to_address(privkey) self.nonce_last_update = 0 self.nonce_current_value = None self.nonce_lock = Semaphore() self.nonce_update_interval = nonce_update_interval self.nonce_offset = nonce_offset def __repr__(self): return '<JSONRPCClient @%d>' % self.port def blocknumber(self): """ Return the most recent block. """ return quantity_decoder(self.call('eth_blockNumber')) def nonce(self, address): if len(address) == 40: address = unhexlify(address) with self.nonce_lock: initialized = self.nonce_current_value is not None query_time = now() if self.nonce_last_update > query_time: # Python's 2.7 time is not monotonic and it's affected by clock # resets, force an update. self.nonce_update_interval = query_time - self.nonce_update_interval needs_update = True else: last_update_interval = query_time - self.nonce_last_update needs_update = last_update_interval > self.nonce_update_interval if initialized and not needs_update: self.nonce_current_value += 1 return self.nonce_current_value pending_transactions_hex = self.call( 'eth_getTransactionCount', address_encoder(address), 'pending', ) pending_transactions = quantity_decoder(pending_transactions_hex) nonce = pending_transactions + self.nonce_offset # we may have hammered the server and not all tx are # registered as `pending` yet while nonce < self.nonce_current_value: log.debug( 'nonce on server too low; retrying', server=nonce, local=self.nonce_current_value, ) query_time = now() pending_transactions_hex = self.call( 'eth_getTransactionCount', address_encoder(address), 'pending', ) pending_transactions = quantity_decoder( pending_transactions_hex) nonce = pending_transactions + self.nonce_offset self.nonce_current_value = nonce self.nonce_last_update = query_time return self.nonce_current_value def balance(self, account): """ Return the balance of the account of given address. """ res = self.call('eth_getBalance', address_encoder(account), 'pending') return quantity_decoder(res) def gaslimit(self): last_block = self.call('eth_getBlockByNumber', 'latest', True) gas_limit = quantity_decoder(last_block['gasLimit']) return gas_limit def new_contract_proxy(self, contract_interface, address): """ Return a proxy for interacting with a smart contract. Args: contract_interface: The contract interface as defined by the json. address: The contract's address. """ return ContractProxy( self.sender, contract_interface, address, self.eth_call, self.send_transaction, self.eth_estimateGas, ) def deploy_solidity_contract( self, # pylint: disable=too-many-locals sender, contract_name, all_contracts, libraries, constructor_parameters, contract_path=None, timeout=None, gasprice=GAS_PRICE): """ Deploy a solidity contract. Args: sender (address): the sender address contract_name (str): the name of the contract to compile all_contracts (dict): the json dictionary containing the result of compiling a file libraries (list): A list of libraries to use in deployment constructor_parameters (tuple): A tuple of arguments to pass to the constructor contract_path (str): If we are dealing with solc >= v0.4.9 then the path to the contract is a required argument to extract the contract data from the `all_contracts` dict. timeout (int): Amount of time to poll the chain to confirm deployment gasprice: The gasprice to provide for the transaction """ if contract_name in all_contracts: contract_key = contract_name elif contract_path is not None: _, filename = os.path.split(contract_path) contract_key = filename + ':' + contract_name if contract_key not in all_contracts: raise ValueError('Unknown contract {}'.format(contract_name)) else: raise ValueError( 'Unknown contract {} and no contract_path given'.format( contract_name)) libraries = dict(libraries) contract = all_contracts[contract_key] contract_interface = contract['abi'] symbols = solidity_unresolved_symbols(contract['bin_hex']) if symbols: available_symbols = map(solidity_library_symbol, all_contracts.keys()) unknown_symbols = set(symbols) - set(available_symbols) if unknown_symbols: msg = 'Cannot deploy contract, known symbols {}, unresolved symbols {}.'.format( available_symbols, unknown_symbols, ) raise Exception(msg) dependencies = deploy_dependencies_symbols(all_contracts) deployment_order = dependencies_order_of_build( contract_key, dependencies) deployment_order.pop() # remove `contract_name` from the list log.debug('Deploying dependencies: {}'.format( str(deployment_order))) for deploy_contract in deployment_order: dependency_contract = all_contracts[deploy_contract] hex_bytecode = solidity_resolve_symbols( dependency_contract['bin_hex'], libraries) bytecode = unhexlify(hex_bytecode) dependency_contract['bin_hex'] = hex_bytecode dependency_contract['bin'] = bytecode transaction_hash_hex = self.send_transaction( sender, to='', data=bytecode, gasprice=gasprice, ) transaction_hash = unhexlify(transaction_hash_hex) self.poll(transaction_hash, timeout=timeout) receipt = self.eth_getTransactionReceipt(transaction_hash) contract_address = receipt['contractAddress'] # remove the hexadecimal prefix 0x from the address contract_address = contract_address[2:] libraries[deploy_contract] = contract_address deployed_code = self.eth_getCode(unhexlify(contract_address)) if deployed_code == '0x': raise RuntimeError( 'Contract address has no code, check gas usage.') hex_bytecode = solidity_resolve_symbols(contract['bin_hex'], libraries) bytecode = unhexlify(hex_bytecode) contract['bin_hex'] = hex_bytecode contract['bin'] = bytecode if constructor_parameters: translator = ContractTranslator(contract_interface) parameters = translator.encode_constructor_arguments( constructor_parameters) bytecode = contract['bin'] + parameters else: bytecode = contract['bin'] transaction_hash_hex = self.send_transaction( sender, to='', data=bytecode, gasprice=gasprice, ) transaction_hash = unhexlify(transaction_hash_hex) self.poll(transaction_hash, timeout=timeout) receipt = self.eth_getTransactionReceipt(transaction_hash) contract_address = receipt['contractAddress'] deployed_code = self.eth_getCode(unhexlify(contract_address[2:])) if deployed_code == '0x': raise RuntimeError( 'Deployment of {} failed. Contract address has no code, check gas usage.' .format(contract_name, )) return self.new_contract_proxy( contract_interface, contract_address, ) def new_filter(self, fromBlock=None, toBlock=None, address=None, topics=None): """ Creates a filter object, based on filter options, to notify when the state changes (logs). To check if the state has changed, call eth_getFilterChanges. """ json_data = { 'fromBlock': block_tag_encoder(fromBlock or ''), 'toBlock': block_tag_encoder(toBlock or ''), } if address is not None: json_data['address'] = address_encoder(address) if topics is not None: if not isinstance(topics, list): raise ValueError('topics must be a list') json_data['topics'] = [topic_encoder(topic) for topic in topics] filter_id = self.call('eth_newFilter', json_data) return quantity_decoder(filter_id) def filter_changes(self, fid): changes = self.call('eth_getFilterChanges', quantity_encoder(fid)) if not changes: return None if isinstance(changes, bytes): return data_decoder(changes) decoders = { 'blockHash': data_decoder, 'transactionHash': data_decoder, 'data': data_decoder, 'address': address_decoder, 'topics': lambda x: [topic_decoder(t) for t in x], 'blockNumber': quantity_decoder, 'logIndex': quantity_decoder, 'transactionIndex': quantity_decoder } return [{k: decoders[k](v) for k, v in c.items() if v is not None} for c in changes] @check_node_connection def call(self, method, *args): """ Do the request and return the result. Args: method (str): The RPC method. args: The encoded arguments expected by the method. - Object arguments must be supplied as a dictionary. - Quantity arguments must be hex encoded starting with '0x' and without left zeros. - Data arguments must be hex encoded starting with '0x' """ request = self.protocol.create_request(method, args) reply = self.transport.send_message(request.serialize()) jsonrpc_reply = self.protocol.parse_reply(reply) if isinstance(jsonrpc_reply, JSONRPCSuccessResponse): return jsonrpc_reply.result elif isinstance(jsonrpc_reply, JSONRPCErrorResponse): raise EthNodeCommunicationError(jsonrpc_reply.error) else: raise EthNodeCommunicationError('Unknown type of JSONRPC reply') def send_transaction(self, sender, to, value=0, data='', startgas=0, gasprice=GAS_PRICE, nonce=None): """ Helper to send signed messages. This method will use the `privkey` provided in the constructor to locally sign the transaction. This requires an extended server implementation that accepts the variables v, r, and s. """ if not self.privkey and not sender: raise ValueError('Either privkey or sender needs to be supplied.') if self.privkey: privkey_address = privatekey_to_address(self.privkey) sender = sender or privkey_address if sender != privkey_address: raise ValueError('sender for a different privkey.') if nonce is None: nonce = self.nonce(sender) else: if nonce is None: nonce = 0 if not startgas: startgas = self.gaslimit() - 1 tx = Transaction(nonce, gasprice, startgas, to=to, value=value, data=data) if self.privkey: tx.sign(self.privkey) result = self.call( 'eth_sendRawTransaction', data_encoder(rlp.encode(tx)), ) return result[2 if result.startswith('0x') else 0:] else: # rename the fields to match the eth_sendTransaction signature tx_dict = tx.to_dict() tx_dict.pop('hash') tx_dict['sender'] = sender tx_dict['gasPrice'] = tx_dict.pop('gasprice') tx_dict['gas'] = tx_dict.pop('startgas') res = self.eth_sendTransaction(**tx_dict) assert len(res) in (20, 32) return hexlify(res) def eth_sendTransaction(self, nonce=None, sender='', to='', value=0, data='', gasPrice=GAS_PRICE, gas=GAS_PRICE): """ Creates new message call transaction or a contract creation, if the data field contains code. Args: sender (address): The 20 bytes address the transaction is sent from. to (address): DATA, 20 Bytes - (optional when creating new contract) The address the transaction is directed to. gas (int): Gas provided for the transaction execution. It will return unused gas. gasPrice (int): gasPrice used for each unit of gas paid. value (int): Value sent with this transaction. data (bin): The compiled code of a contract OR the hash of the invoked method signature and encoded parameters. nonce (int): This allows to overwrite your own pending transactions that use the same nonce. """ if to == '' and data.isalnum(): warnings.warn( 'Verify that the data parameter is _not_ hex encoded, if this is the case ' 'the data will be double encoded and result in unexpected ' 'behavior.') if to == '0' * 40: warnings.warn( 'For contract creation the empty string must be used.') if sender is None: raise ValueError('sender needs to be provided.') json_data = { 'to': data_encoder(normalize_address(to, allow_blank=True)), 'value': quantity_encoder(value), 'gasPrice': quantity_encoder(gasPrice), 'gas': quantity_encoder(gas), 'data': data_encoder(data), 'from': address_encoder(sender), } if nonce is not None: json_data['nonce'] = quantity_encoder(nonce) res = self.call('eth_sendTransaction', json_data) return data_decoder(res) def eth_call(self, sender='', to='', value=0, data='', startgas=GAS_PRICE, gasprice=GAS_PRICE, block_number='latest'): """ Executes a new message call immediately without creating a transaction on the blockchain. Args: sender: The address the transaction is sent from. to: The address the transaction is directed to. gas (int): Gas provided for the transaction execution. eth_call consumes zero gas, but this parameter may be needed by some executions. gasPrice (int): gasPrice used for unit of gas paid. value (int): Integer of the value sent with this transaction. data (bin): Hash of the method signature and encoded parameters. For details see Ethereum Contract ABI. block_number: Determines the state of ethereum used in the call. """ json_data = format_data_for_call( sender, to, value, data, startgas, gasprice, ) res = self.call('eth_call', json_data, block_number) return data_decoder(res) def eth_estimateGas(self, sender='', to='', value=0, data='', startgas=GAS_PRICE, gasprice=GAS_PRICE): """ Makes a call or transaction, which won't be added to the blockchain and returns the used gas, which can be used for estimating the used gas. Args: sender: The address the transaction is sent from. to: The address the transaction is directed to. gas (int): Gas provided for the transaction execution. eth_call consumes zero gas, but this parameter may be needed by some executions. gasPrice (int): gasPrice used for unit of gas paid. value (int): Integer of the value sent with this transaction. data (bin): Hash of the method signature and encoded parameters. For details see Ethereum Contract ABI. block_number: Determines the state of ethereum used in the call. """ json_data = format_data_for_call( sender, to, value, data, startgas, gasprice, ) res = self.call('eth_estimateGas', json_data) return quantity_decoder(res) def eth_getTransactionReceipt(self, transaction_hash): """ Returns the receipt of a transaction by transaction hash. Args: transaction_hash: Hash of a transaction. Returns: A dict representing the transaction receipt object, or null when no receipt was found. """ if transaction_hash.startswith('0x'): warnings.warn( 'transaction_hash seems to be already encoded, this will' ' result in unexpected behavior') if len(transaction_hash) != 32: raise ValueError( 'transaction_hash length must be 32 (it might be hex encoded)') transaction_hash = data_encoder(transaction_hash) return self.call('eth_getTransactionReceipt', transaction_hash) def eth_getCode(self, address, block='latest'): """ Returns code at a given address. Args: address: An address. block: Integer block number, or the string 'latest', 'earliest' or 'pending'. """ if address.startswith('0x'): warnings.warn( 'address seems to be already encoded, this will result ' 'in unexpected behavior') if len(address) != 20: raise ValueError( 'address length must be 20 (it might be hex encoded)') return self.call( 'eth_getCode', address_encoder(address), block, ) def eth_getTransactionByHash(self, transaction_hash): """ Returns the information about a transaction requested by transaction hash. """ if transaction_hash.startswith('0x'): warnings.warn( 'transaction_hash seems to be already encoded, this will' ' result in unexpected behavior') if len(transaction_hash) != 32: raise ValueError( 'transaction_hash length must be 32 (it might be hex encoded)') transaction_hash = data_encoder(transaction_hash) return self.call('eth_getTransactionByHash', transaction_hash) def poll(self, transaction_hash, confirmations=None, timeout=None): """ Wait until the `transaction_hash` is applied or rejected. If timeout is None, this could wait indefinitely! Args: transaction_hash (hash): Transaction hash that we are waiting for. confirmations (int): Number of block confirmations that we will wait for. timeout (float): Timeout in seconds, raise an Excpetion on timeout. """ if transaction_hash.startswith('0x'): warnings.warn( 'transaction_hash seems to be already encoded, this will' ' result in unexpected behavior') if len(transaction_hash) != 32: raise ValueError( 'transaction_hash length must be 32 (it might be hex encoded)') transaction_hash = data_encoder(transaction_hash) deadline = None if timeout: deadline = gevent.Timeout(timeout) deadline.start() try: # used to check if the transaction was removed, this could happen # if gas price is too low: # # > Transaction (acbca3d6) below gas price (tx=1 Wei ask=18 # > Shannon). All sequential txs from this address(7d0eae79) # > will be ignored # last_result = None while True: # Could return None for a short period of time, until the # transaction is added to the pool transaction = self.call('eth_getTransactionByHash', transaction_hash) # if the transaction was added to the pool and then removed if transaction is None and last_result is not None: raise Exception('invalid transaction, check gas price') # the transaction was added to the pool and mined if transaction and transaction['blockNumber'] is not None: break last_result = transaction gevent.sleep(.5) if confirmations: # this will wait for both APPLIED and REVERTED transactions transaction_block = quantity_decoder( transaction['blockNumber']) confirmation_block = transaction_block + confirmations block_number = self.blocknumber() while block_number < confirmation_block: gevent.sleep(.5) block_number = self.blocknumber() except gevent.Timeout: raise Exception('timeout when polling for transaction') finally: if deadline: deadline.cancel()
from tinyrpc.protocols.jsonrpc import JSONRPCProtocol rpc = JSONRPCProtocol() # again, code below is protocol-independent # assuming you want to call method(*args, **kwargs) request = rpc.create_request(method, args, kwargs) reply = send_to_server_and_get_reply(request) response = rpc.parse_reply(reply) if hasattr(response, 'error'): pass else: # the return value is found in response.result do_something_with(response.result)
from tinyrpc.protocols.jsonrpc import JSONRPCProtocol """ membuat rpc 1. buat instance dari JSONRPCProtocol 2. tambahkan method create_request """ ini_instance = JSONRPCProtocol() request = ini_instance.create_request( 'flight-event-subscribe' ) #bisa juga menambahkan parameter misal request = self.rpc.create_request('authenticate', {'id' : self.client_id}) #contoh request dengan param request4 = ini_instance.create_request('flight-event-subscribe', {'id': '1'}) """ melihat uniq id yg dibuat secara otomatis oleh rpc """ request2 = ini_instance.create_request('flight-event-subscribe2', ) request3 = ini_instance.create_request('flight-event-subscribe3') print request.unique_id print request2.unique_id print request3.unique_id print request4.unique_id """ Hasil Akhir yang siap dikirim """ print "type request", type(request) print "type request.serialize =", type(request.serialize()) print request.serialize() #lihat bedanya dengan yang ada paramnya
class JSONRPCClient: """ Ethereum JSON RPC client. Args: host: Ethereum node host address. port: Ethereum node port number. privkey: Local user private key, used to sign transactions. nonce_update_interval: Update the account nonce every `nonce_update_interval` seconds. nonce_offset: Network's default base nonce number. """ def __init__( self, host: str, port: int, privkey: bytes, nonce_update_interval: float = 5.0, nonce_offset: int = 0): endpoint = 'http://{}:{}'.format(host, port) session = requests.Session() adapter = requests.adapters.HTTPAdapter(pool_maxsize=50) session.mount(endpoint, adapter) self.transport = HttpPostClientTransport( endpoint, post_method=session.post, headers={'content-type': 'application/json'}, ) self.port = port self.privkey = privkey self.protocol = JSONRPCProtocol() self.sender = privatekey_to_address(privkey) # Needs to be initialized to None in the beginning since JSONRPCClient # gets constructed before the RaidenService Object. self.stop_event = None self.nonce_last_update = 0 self.nonce_current_value = None self.nonce_lock = Semaphore() self.nonce_update_interval = nonce_update_interval self.nonce_offset = nonce_offset def __repr__(self): return '<JSONRPCClient @%d>' % self.port def block_number(self): """ Return the most recent block. """ return quantity_decoder(self.call('eth_blockNumber')) def nonce(self, address): if len(address) == 40: address = unhexlify(address) with self.nonce_lock: initialized = self.nonce_current_value is not None query_time = now() if self.nonce_last_update > query_time: # Python's 2.7 time is not monotonic and it's affected by clock # resets, force an update. self.nonce_update_interval = query_time - self.nonce_update_interval needs_update = True else: last_update_interval = query_time - self.nonce_last_update needs_update = last_update_interval > self.nonce_update_interval if initialized and not needs_update: self.nonce_current_value += 1 return self.nonce_current_value pending_transactions_hex = self.call( 'eth_getTransactionCount', address_encoder(address), 'pending', ) pending_transactions = quantity_decoder(pending_transactions_hex) nonce = pending_transactions + self.nonce_offset # we may have hammered the server and not all tx are # registered as `pending` yet if initialized: while nonce < self.nonce_current_value: log.debug( 'nonce on server too low; retrying', server=nonce, local=self.nonce_current_value, ) query_time = now() pending_transactions_hex = self.call( 'eth_getTransactionCount', address_encoder(address), 'pending', ) pending_transactions = quantity_decoder(pending_transactions_hex) nonce = pending_transactions + self.nonce_offset self.nonce_current_value = nonce self.nonce_last_update = query_time return self.nonce_current_value def inject_stop_event(self, event): self.stop_event = event def balance(self, account: address): """ Return the balance of the account of given address. """ res = self.call('eth_getBalance', address_encoder(account), 'pending') return quantity_decoder(res) def gaslimit(self) -> int: last_block = self.call('eth_getBlockByNumber', 'latest', True) gas_limit = quantity_decoder(last_block['gasLimit']) return gas_limit def new_contract_proxy(self, contract_interface, contract_address: address): """ Return a proxy for interacting with a smart contract. Args: contract_interface: The contract interface as defined by the json. address: The contract's address. """ return ContractProxy( self.sender, contract_interface, contract_address, self.eth_call, self.send_transaction, self.eth_estimateGas, ) def deploy_solidity_contract( self, # pylint: disable=too-many-locals sender, contract_name, all_contracts, libraries, constructor_parameters, contract_path=None, timeout=None, gasprice=GAS_PRICE): """ Deploy a solidity contract. Args: sender (address): the sender address contract_name (str): the name of the contract to compile all_contracts (dict): the json dictionary containing the result of compiling a file libraries (list): A list of libraries to use in deployment constructor_parameters (tuple): A tuple of arguments to pass to the constructor contract_path (str): If we are dealing with solc >= v0.4.9 then the path to the contract is a required argument to extract the contract data from the `all_contracts` dict. timeout (int): Amount of time to poll the chain to confirm deployment gasprice: The gasprice to provide for the transaction """ if contract_name in all_contracts: contract_key = contract_name elif contract_path is not None: _, filename = os.path.split(contract_path) contract_key = filename + ':' + contract_name if contract_key not in all_contracts: raise ValueError('Unknown contract {}'.format(contract_name)) else: raise ValueError( 'Unknown contract {} and no contract_path given'.format(contract_name) ) libraries = dict(libraries) contract = all_contracts[contract_key] contract_interface = contract['abi'] symbols = solidity_unresolved_symbols(contract['bin_hex']) if symbols: available_symbols = list(map(solidity_library_symbol, all_contracts.keys())) unknown_symbols = set(symbols) - set(available_symbols) if unknown_symbols: msg = 'Cannot deploy contract, known symbols {}, unresolved symbols {}.'.format( available_symbols, unknown_symbols, ) raise Exception(msg) dependencies = deploy_dependencies_symbols(all_contracts) deployment_order = dependencies_order_of_build(contract_key, dependencies) deployment_order.pop() # remove `contract_name` from the list log.debug('Deploying dependencies: {}'.format(str(deployment_order))) for deploy_contract in deployment_order: dependency_contract = all_contracts[deploy_contract] hex_bytecode = solidity_resolve_symbols(dependency_contract['bin_hex'], libraries) bytecode = unhexlify(hex_bytecode) dependency_contract['bin_hex'] = hex_bytecode dependency_contract['bin'] = bytecode transaction_hash_hex = self.send_transaction( sender, to=b'', data=bytecode, gasprice=gasprice, ) transaction_hash = unhexlify(transaction_hash_hex) self.poll(transaction_hash, timeout=timeout) receipt = self.eth_getTransactionReceipt(transaction_hash) contract_address = receipt['contractAddress'] # remove the hexadecimal prefix 0x from the address contract_address = contract_address[2:] libraries[deploy_contract] = contract_address deployed_code = self.eth_getCode(address_decoder(contract_address)) if len(deployed_code) == 0: raise RuntimeError('Contract address has no code, check gas usage.') hex_bytecode = solidity_resolve_symbols(contract['bin_hex'], libraries) bytecode = unhexlify(hex_bytecode) contract['bin_hex'] = hex_bytecode contract['bin'] = bytecode if constructor_parameters: translator = ContractTranslator(contract_interface) parameters = translator.encode_constructor_arguments(constructor_parameters) bytecode = contract['bin'] + parameters else: bytecode = contract['bin'] transaction_hash_hex = self.send_transaction( sender, to=b'', data=bytecode, gasprice=gasprice, ) transaction_hash = unhexlify(transaction_hash_hex) self.poll(transaction_hash, timeout=timeout) receipt = self.eth_getTransactionReceipt(transaction_hash) contract_address = receipt['contractAddress'] deployed_code = self.eth_getCode(address_decoder(contract_address)) if len(deployed_code) == 0: raise RuntimeError( 'Deployment of {} failed. Contract address has no code, check gas usage.'.format( contract_name, ) ) return self.new_contract_proxy( contract_interface, contract_address, ) def new_filter(self, fromBlock=None, toBlock=None, address=None, topics=None): """ Creates a filter object, based on filter options, to notify when the state changes (logs). To check if the state has changed, call eth_getFilterChanges. """ json_data = { 'fromBlock': block_tag_encoder(fromBlock or ''), 'toBlock': block_tag_encoder(toBlock or ''), } if address is not None: json_data['address'] = address_encoder(address) if topics is not None: if not isinstance(topics, list): raise ValueError('topics must be a list') json_data['topics'] = [topic_encoder(topic) for topic in topics] filter_id = self.call('eth_newFilter', json_data) return quantity_decoder(filter_id) def filter_changes(self, fid: int) -> List: changes = self.call('eth_getFilterChanges', quantity_encoder(fid)) if not changes: return list() assert isinstance(changes, list) decoders = { 'blockHash': data_decoder, 'transactionHash': data_decoder, 'data': data_decoder, 'address': address_decoder, 'topics': lambda x: [topic_decoder(t) for t in x], 'blockNumber': quantity_decoder, 'logIndex': quantity_decoder, 'transactionIndex': quantity_decoder } return [ {k: decoders[k](v) for k, v in c.items() if v is not None} for c in changes ] @check_node_connection def call(self, method: str, *args): """ Do the request and return the result. Args: method: The RPC method. args: The encoded arguments expected by the method. - Object arguments must be supplied as a dictionary. - Quantity arguments must be hex encoded starting with '0x' and without left zeros. - Data arguments must be hex encoded starting with '0x' """ request = self.protocol.create_request(method, args) reply = self.transport.send_message(request.serialize().encode()) jsonrpc_reply = self.protocol.parse_reply(reply) if isinstance(jsonrpc_reply, JSONRPCSuccessResponse): return jsonrpc_reply.result elif isinstance(jsonrpc_reply, JSONRPCErrorResponse): raise EthNodeCommunicationError(jsonrpc_reply.error, jsonrpc_reply._jsonrpc_error_code) else: raise EthNodeCommunicationError('Unknown type of JSONRPC reply') def send_transaction( self, sender: address, to: address, value: int = 0, data: bytes = b'', startgas: int = 0, gasprice: int = GAS_PRICE, nonce: Optional[int] = None): """ Helper to send signed messages. This method will use the `privkey` provided in the constructor to locally sign the transaction. This requires an extended server implementation that accepts the variables v, r, and s. """ if not self.privkey and not sender: raise ValueError('Either privkey or sender needs to be supplied.') if self.privkey: privkey_address = privatekey_to_address(self.privkey) sender = sender or privkey_address if sender != privkey_address: raise ValueError('sender for a different privkey.') if nonce is None: nonce = self.nonce(sender) else: if nonce is None: nonce = 0 if not startgas: startgas = self.gaslimit() - 1 tx = Transaction(nonce, gasprice, startgas, to=to, value=value, data=data) if self.privkey: tx.sign(self.privkey) result = self.call( 'eth_sendRawTransaction', data_encoder(rlp.encode(tx)), ) return result[2 if result.startswith('0x') else 0:] else: # rename the fields to match the eth_sendTransaction signature tx_dict = tx.to_dict() tx_dict.pop('hash') tx_dict['sender'] = sender tx_dict['gasPrice'] = tx_dict.pop('gasprice') tx_dict['gas'] = tx_dict.pop('startgas') res = self.eth_sendTransaction(**tx_dict) assert len(res) in (20, 32) return hexlify(res) def eth_sendTransaction( self, sender: address = b'', to: address = b'', value: int = 0, data: bytes = b'', gasPrice: int = GAS_PRICE, gas: int = GAS_PRICE, nonce: Optional[int] = None): """ Creates new message call transaction or a contract creation, if the data field contains code. Args: sender: The address the transaction is sent from. to: The address the transaction is directed to. (optional when creating new contract) gas: Gas provided for the transaction execution. It will return unused gas. gasPrice: gasPrice used for each unit of gas paid. value: Value sent with this transaction. data: The compiled code of a contract OR the hash of the invoked method signature and encoded parameters. nonce: This allows to overwrite your own pending transactions that use the same nonce. """ if to == b'' and data.isalnum(): warnings.warn( 'Verify that the data parameter is _not_ hex encoded, if this is the case ' 'the data will be double encoded and result in unexpected ' 'behavior.' ) if to == b'0' * 40: warnings.warn('For contract creation the empty string must be used.') if sender is None: raise ValueError('sender needs to be provided.') json_data = format_data_for_call( sender, to, value, data, gas, gasPrice ) if nonce is not None: json_data['nonce'] = quantity_encoder(nonce) res = self.call('eth_sendTransaction', json_data) return data_decoder(res) def eth_call( self, sender: address = b'', to: address = b'', value: int = 0, data: bytes = b'', startgas: int = GAS_PRICE, gasprice: int = GAS_PRICE, block_number: Union[str, int] = 'latest'): """ Executes a new message call immediately without creating a transaction on the blockchain. Args: sender: The address the transaction is sent from. to: The address the transaction is directed to. gas: Gas provided for the transaction execution. eth_call consumes zero gas, but this parameter may be needed by some executions. gasPrice: gasPrice used for unit of gas paid. value: Integer of the value sent with this transaction. data: Hash of the method signature and encoded parameters. For details see Ethereum Contract ABI. block_number: Determines the state of ethereum used in the call. """ json_data = format_data_for_call( sender, to, value, data, startgas, gasprice, ) res = self.call('eth_call', json_data, block_number) return data_decoder(res) def eth_estimateGas( self, sender: address = b'', to: address = b'', value: int = 0, data: bytes = b'', startgas: int = GAS_PRICE, gasprice: int = GAS_PRICE) -> int: """ Makes a call or transaction, which won't be added to the blockchain and returns the used gas, which can be used for estimating the used gas. Args: sender: The address the transaction is sent from. to: The address the transaction is directed to. gas: Gas provided for the transaction execution. eth_call consumes zero gas, but this parameter may be needed by some executions. gasPrice: gasPrice used for unit of gas paid. value: Integer of the value sent with this transaction. data: Hash of the method signature and encoded parameters. For details see Ethereum Contract ABI. block_number: Determines the state of ethereum used in the call. """ json_data = format_data_for_call( sender, to, value, data, startgas, gasprice, ) try: res = self.call('eth_estimateGas', json_data) except EthNodeCommunicationError as e: tx_would_fail = e.error_code and e.error_code in (-32015, -32000) if tx_would_fail: # -32015 is parity and -32000 is geth return None else: raise e return quantity_decoder(res) def eth_getTransactionReceipt(self, transaction_hash: bytes) -> Dict: """ Returns the receipt of a transaction by transaction hash. Args: transaction_hash: Hash of a transaction. Returns: A dict representing the transaction receipt object, or null when no receipt was found. """ if transaction_hash.startswith(b'0x'): warnings.warn( 'transaction_hash seems to be already encoded, this will' ' result in unexpected behavior' ) if len(transaction_hash) != 32: raise ValueError( 'transaction_hash length must be 32 (it might be hex encoded)' ) transaction_hash = data_encoder(transaction_hash) return self.call('eth_getTransactionReceipt', transaction_hash) def eth_getCode(self, code_address: address, block: Union[int, str] = 'latest') -> bytes: """ Returns code at a given address. Args: code_address: An address. block: Integer block number, or the string 'latest', 'earliest' or 'pending'. Default is 'latest'. """ if code_address.startswith(b'0x'): warnings.warn( 'address seems to be already encoded, this will result ' 'in unexpected behavior' ) if len(code_address) != 20: raise ValueError( 'address length must be 20 (it might be hex encoded)' ) result = self.call('eth_getCode', address_encoder(code_address), block) return data_decoder(result) def eth_getTransactionByHash(self, transaction_hash: bytes): """ Returns the information about a transaction requested by transaction hash. """ if transaction_hash.startswith(b'0x'): warnings.warn( 'transaction_hash seems to be already encoded, this will' ' result in unexpected behavior' ) if len(transaction_hash) != 32: raise ValueError( 'transaction_hash length must be 32 (it might be hex encoded)' ) transaction_hash = data_encoder(transaction_hash) return self.call('eth_getTransactionByHash', transaction_hash) def poll( self, transaction_hash: bytes, confirmations: Optional[int] = None, timeout: Optional[float] = None): """ Wait until the `transaction_hash` is applied or rejected. If timeout is None, this could wait indefinitely! Args: transaction_hash: Transaction hash that we are waiting for. confirmations: Number of block confirmations that we will wait for. timeout: Timeout in seconds, raise an Excpetion on timeout. """ if transaction_hash.startswith(b'0x'): warnings.warn( 'transaction_hash seems to be already encoded, this will' ' result in unexpected behavior' ) if len(transaction_hash) != 32: raise ValueError( 'transaction_hash length must be 32 (it might be hex encoded)' ) transaction_hash = data_encoder(transaction_hash) deadline = None if timeout: deadline = gevent.Timeout(timeout) deadline.start() try: # used to check if the transaction was removed, this could happen # if gas price is too low: # # > Transaction (acbca3d6) below gas price (tx=1 Wei ask=18 # > Shannon). All sequential txs from this address(7d0eae79) # > will be ignored # last_result = None while True: # Could return None for a short period of time, until the # transaction is added to the pool transaction = self.call('eth_getTransactionByHash', transaction_hash) # if the transaction was added to the pool and then removed if transaction is None and last_result is not None: raise Exception('invalid transaction, check gas price') # the transaction was added to the pool and mined if transaction and transaction['blockNumber'] is not None: break last_result = transaction gevent.sleep(.5) if confirmations: # this will wait for both APPLIED and REVERTED transactions transaction_block = quantity_decoder(transaction['blockNumber']) confirmation_block = transaction_block + confirmations block_number = self.block_number() while block_number < confirmation_block: gevent.sleep(.5) block_number = self.block_number() except gevent.Timeout: raise Exception('timeout when polling for transaction') finally: if deadline: deadline.cancel()
class JSONRPCClient: """ Ethereum JSON RPC client. Args: host: Ethereum node host address. port: Ethereum node port number. privkey: Local user private key, used to sign transactions. nonce_update_interval: Update the account nonce every `nonce_update_interval` seconds. nonce_offset: Network's default base nonce number. """ def __init__(self, host: str, port: int, privkey: bytes, gasprice: int = None, nonce_update_interval: float = 5.0, nonce_offset: int = 0): if privkey is None or len(privkey) != 32: raise ValueError('Invalid private key') endpoint = 'http://{}:{}'.format(host, port) self.session = requests.Session() adapter = requests.adapters.HTTPAdapter(pool_maxsize=50) self.session.mount(endpoint, adapter) self.transport = HttpPostClientTransport( endpoint, post_method=self.session.post, headers={'content-type': 'application/json'}, ) self.port = port self.privkey = privkey self.protocol = JSONRPCProtocol() self.sender = privatekey_to_address(privkey) # Needs to be initialized to None in the beginning since JSONRPCClient # gets constructed before the RaidenService Object. self.stop_event = None self.nonce_last_update = 0 self.nonce_available_value = None self.nonce_lock = Semaphore() self.nonce_update_interval = nonce_update_interval self.nonce_offset = nonce_offset self.given_gas_price = gasprice cache = cachetools.TTLCache( maxsize=1, ttl=RPC_CACHE_TTL, ) cache_wrapper = cachetools.cached(cache=cache) self.gaslimit = cache_wrapper(self._gaslimit) cache = cachetools.TTLCache( maxsize=1, ttl=RPC_CACHE_TTL, ) cache_wrapper = cachetools.cached(cache=cache) self.gasprice = cache_wrapper(self._gasprice) def __del__(self): self.session.close() def __repr__(self): return '<JSONRPCClient @%d>' % self.port def block_number(self): """ Return the most recent block. """ return quantity_decoder(self.rpccall_with_retry('eth_blockNumber')) def nonce_needs_update(self): if self.nonce_available_value is None: return True now = time.time() # Python's 2.7 time is not monotonic and it's affected by clock resets, # force an update. if self.nonce_last_update > now: return True return now - self.nonce_last_update > self.nonce_update_interval def nonce_update_from_node(self): nonce = -2 nonce_available_value = self.nonce_available_value or -1 # Wait until all tx are registered as pending while nonce < nonce_available_value: pending_transactions_hex = self.rpccall_with_retry( 'eth_getTransactionCount', address_encoder(self.sender), 'pending', ) pending_transactions = quantity_decoder(pending_transactions_hex) nonce = pending_transactions + self.nonce_offset log.debug( 'updated nonce from server', server=nonce, local=nonce_available_value, ) self.nonce_last_update = time.time() self.nonce_available_value = nonce def nonce(self): with self.nonce_lock: if self.nonce_needs_update(): self.nonce_update_from_node() self.nonce_available_value += 1 return self.nonce_available_value - 1 def inject_stop_event(self, event): self.stop_event = event def balance(self, account: Address): """ Return the balance of the account of given address. """ res = self.rpccall_with_retry('eth_getBalance', address_encoder(account), 'pending') return quantity_decoder(res) def _gaslimit(self, location='pending') -> int: last_block = self.rpccall_with_retry('eth_getBlockByNumber', location, True) gas_limit = quantity_decoder(last_block['gasLimit']) return gas_limit * 8 // 10 def _gasprice(self) -> int: if self.given_gas_price: return self.given_gas_price gas_price = self.rpccall_with_retry('eth_gasPrice') return quantity_decoder(gas_price) def check_startgas(self, startgas): if not startgas: return self.gaslimit() return startgas def new_contract_proxy(self, contract_interface, contract_address: Address): """ Return a proxy for interacting with a smart contract. Args: contract_interface: The contract interface as defined by the json. address: The contract's address. """ return ContractProxy( self.sender, contract_interface, contract_address, self.eth_call, self.send_transaction, self.eth_estimateGas, ) def deploy_solidity_contract( self, # pylint: disable=too-many-locals contract_name, all_contracts, libraries, constructor_parameters, contract_path=None, timeout=None): """ Deploy a solidity contract. Args: sender (address): the sender address contract_name (str): the name of the contract to compile all_contracts (dict): the json dictionary containing the result of compiling a file libraries (list): A list of libraries to use in deployment constructor_parameters (tuple): A tuple of arguments to pass to the constructor contract_path (str): If we are dealing with solc >= v0.4.9 then the path to the contract is a required argument to extract the contract data from the `all_contracts` dict. timeout (int): Amount of time to poll the chain to confirm deployment """ if contract_name in all_contracts: contract_key = contract_name elif contract_path is not None: _, filename = os.path.split(contract_path) contract_key = filename + ':' + contract_name if contract_key not in all_contracts: raise ValueError('Unknown contract {}'.format(contract_name)) else: raise ValueError( 'Unknown contract {} and no contract_path given'.format( contract_name)) libraries = dict(libraries) contract = all_contracts[contract_key] contract_interface = contract['abi'] symbols = solidity_unresolved_symbols(contract['bin_hex']) if symbols: available_symbols = list( map(solidity_library_symbol, all_contracts.keys())) unknown_symbols = set(symbols) - set(available_symbols) if unknown_symbols: msg = 'Cannot deploy contract, known symbols {}, unresolved symbols {}.'.format( available_symbols, unknown_symbols, ) raise Exception(msg) dependencies = deploy_dependencies_symbols(all_contracts) deployment_order = dependencies_order_of_build( contract_key, dependencies) deployment_order.pop() # remove `contract_name` from the list log.debug('Deploying dependencies: {}'.format( str(deployment_order))) for deploy_contract in deployment_order: dependency_contract = all_contracts[deploy_contract] hex_bytecode = solidity_resolve_symbols( dependency_contract['bin_hex'], libraries) bytecode = unhexlify(hex_bytecode) dependency_contract['bin_hex'] = hex_bytecode dependency_contract['bin'] = bytecode transaction_hash_hex = self.send_transaction( to=b'', data=bytecode, ) transaction_hash = unhexlify(transaction_hash_hex) self.poll(transaction_hash, timeout=timeout) receipt = self.eth_getTransactionReceipt(transaction_hash) contract_address = receipt['contractAddress'] # remove the hexadecimal prefix 0x from the address contract_address = contract_address[2:] libraries[deploy_contract] = contract_address deployed_code = self.eth_getCode( address_decoder(contract_address)) if not deployed_code: raise RuntimeError( 'Contract address has no code, check gas usage.') hex_bytecode = solidity_resolve_symbols(contract['bin_hex'], libraries) bytecode = unhexlify(hex_bytecode) contract['bin_hex'] = hex_bytecode contract['bin'] = bytecode if constructor_parameters: translator = ContractTranslator(contract_interface) parameters = translator.encode_constructor_arguments( constructor_parameters) bytecode = contract['bin'] + parameters else: bytecode = contract['bin'] transaction_hash_hex = self.send_transaction( to=b'', data=bytecode, ) transaction_hash = unhexlify(transaction_hash_hex) self.poll(transaction_hash, timeout=timeout) receipt = self.eth_getTransactionReceipt(transaction_hash) contract_address = receipt['contractAddress'] deployed_code = self.eth_getCode(address_decoder(contract_address)) if not deployed_code: raise RuntimeError( 'Deployment of {} failed. Contract address has no code, check gas usage.' .format(contract_name, )) return self.new_contract_proxy( contract_interface, contract_address, ) def new_filter(self, fromBlock=None, toBlock=None, address=None, topics=None): """ Creates a filter object, based on filter options, to notify when the state changes (logs). To check if the state has changed, call eth_getFilterChanges. """ json_data = { 'fromBlock': block_tag_encoder(fromBlock or ''), 'toBlock': block_tag_encoder(toBlock or ''), } if address is not None: json_data['address'] = address_encoder(address) if topics is not None: if not isinstance(topics, list): raise ValueError('topics must be a list') json_data['topics'] = [topic_encoder(topic) for topic in topics] filter_id = self.rpccall_with_retry('eth_newFilter', json_data) return quantity_decoder(filter_id) def filter_changes(self, fid: int) -> List: changes = self.rpccall_with_retry('eth_getFilterChanges', quantity_encoder(fid)) if not changes: return list() assert isinstance(changes, list) decoders = { 'blockHash': data_decoder, 'transactionHash': data_decoder, 'data': data_decoder, 'address': address_decoder, 'topics': lambda x: [topic_decoder(t) for t in x], 'blockNumber': quantity_decoder, 'logIndex': quantity_decoder, 'transactionIndex': quantity_decoder } return [{k: decoders[k](v) for k, v in c.items() if v is not None} for c in changes] def rpccall_with_retry(self, method: str, *args): """ Do the request and return the result. Args: method: The JSON-RPC method. args: The encoded arguments expected by the method. - Object arguments must be supplied as a dictionary. - Quantity arguments must be hex encoded starting with '0x' and without left zeros. - Data arguments must be hex encoded starting with '0x' """ request = self.protocol.create_request(method, args) request_serialized = request.serialize().encode() for i, timeout in enumerate(timeout_two_stage(10, 3, 10)): if self.stop_event and self.stop_event.is_set(): raise RaidenShuttingDown() try: reply = self.transport.send_message(request_serialized) except (requests.exceptions.ConnectionError, InvalidReplyError): log.info( 'Timeout in eth client connection to {}. Is the client offline? Trying ' 'again in {}s.'.format(self.transport.endpoint, timeout)) else: if self.stop_event and self.stop_event.is_set(): raise RaidenShuttingDown() if i > 0: log.info('Client reconnected') jsonrpc_reply = self.protocol.parse_reply(reply) if isinstance(jsonrpc_reply, JSONRPCSuccessResponse): return jsonrpc_reply.result elif isinstance(jsonrpc_reply, JSONRPCErrorResponse): raise EthNodeCommunicationError( jsonrpc_reply.error, jsonrpc_reply._jsonrpc_error_code, # pylint: disable=protected-access ) else: raise EthNodeCommunicationError( 'Unknown type of JSONRPC reply') gevent.sleep(timeout) def send_transaction( self, to: Address, value: int = 0, data: bytes = b'', startgas: int = None, ): """ Helper to send signed messages. This method will use the `privkey` provided in the constructor to locally sign the transaction. This requires an extended server implementation that accepts the variables v, r, and s. """ if to == b'' and data.isalnum(): warnings.warn( 'Verify that the data parameter is _not_ hex encoded, if this is the case ' 'the data will be double encoded and result in unexpected ' 'behavior.') if to == b'0' * 40: warnings.warn( 'For contract creation the empty string must be used.') nonce = self.nonce() startgas = self.check_startgas(startgas) tx = Transaction( nonce, self.gasprice(), startgas, to=to, value=value, data=data, ) tx.sign(self.privkey) result = self.rpccall_with_retry( 'eth_sendRawTransaction', data_encoder(rlp.encode(tx)), ) return result[2 if result.startswith('0x') else 0:] def eth_call(self, sender: Address = b'', to: Address = b'', value: int = 0, data: bytes = b'', startgas: int = None, block_number: Union[str, int] = 'latest') -> bytes: """ Executes a new message call immediately without creating a transaction on the blockchain. Args: sender: The address the transaction is sent from. to: The address the transaction is directed to. gas: Gas provided for the transaction execution. eth_call consumes zero gas, but this parameter may be needed by some executions. gasPrice: gasPrice used for unit of gas paid. value: Integer of the value sent with this transaction. data: Hash of the method signature and encoded parameters. For details see Ethereum Contract ABI. block_number: Determines the state of ethereum used in the call. """ startgas = self.check_startgas(startgas) json_data = format_data_for_rpccall( sender, to, value, data, startgas, self.gasprice(), ) res = self.rpccall_with_retry('eth_call', json_data, block_number) return data_decoder(res) def eth_estimateGas(self, sender: Address = b'', to: Address = b'', value: int = 0, data: bytes = b'', startgas: int = None) -> Optional[int]: """ Makes a call or transaction, which won't be added to the blockchain and returns the used gas, which can be used for estimating the used gas. Args: sender: The address the transaction is sent from. to: The address the transaction is directed to. gas: Gas provided for the transaction execution. eth_call consumes zero gas, but this parameter may be needed by some executions. value: Integer of the value sent with this transaction. data: Hash of the method signature and encoded parameters. For details see Ethereum Contract ABI. block_number: Determines the state of ethereum used in the call. """ startgas = self.check_startgas(startgas) json_data = format_data_for_rpccall(sender, to, value, data, startgas) try: res = self.rpccall_with_retry('eth_estimateGas', json_data) except EthNodeCommunicationError as e: tx_would_fail = e.error_code and e.error_code in (-32015, -32000) if tx_would_fail: # -32015 is parity and -32000 is geth return None else: raise e return quantity_decoder(res) def eth_getTransactionReceipt(self, transaction_hash: bytes) -> Dict: """ Returns the receipt of a transaction by transaction hash. Args: transaction_hash: Hash of a transaction. Returns: A dict representing the transaction receipt object, or null when no receipt was found. """ if transaction_hash.startswith(b'0x'): warnings.warn( 'transaction_hash seems to be already encoded, this will' ' result in unexpected behavior') if len(transaction_hash) != 32: raise ValueError( 'transaction_hash length must be 32 (it might be hex encoded)') transaction_hash = data_encoder(transaction_hash) return self.rpccall_with_retry('eth_getTransactionReceipt', transaction_hash) def eth_getCode(self, code_address: Address, block: Union[int, str] = 'latest') -> bytes: """ Returns code at a given address. Args: code_address: An address. block: Integer block number, or the string 'latest', 'earliest' or 'pending'. Default is 'latest'. """ if code_address.startswith(b'0x'): warnings.warn( 'address seems to be already encoded, this will result ' 'in unexpected behavior') if len(code_address) != 20: raise ValueError( 'address length must be 20 (it might be hex encoded)') result = self.rpccall_with_retry('eth_getCode', address_encoder(code_address), block) return data_decoder(result) def eth_getTransactionByHash(self, transaction_hash: bytes): """ Returns the information about a transaction requested by transaction hash. """ if transaction_hash.startswith(b'0x'): warnings.warn( 'transaction_hash seems to be already encoded, this will' ' result in unexpected behavior') if len(transaction_hash) != 32: raise ValueError( 'transaction_hash length must be 32 (it might be hex encoded)') transaction_hash = data_encoder(transaction_hash) return self.rpccall_with_retry('eth_getTransactionByHash', transaction_hash) def poll(self, transaction_hash: bytes, confirmations: int = None, timeout: float = None): """ Wait until the `transaction_hash` is applied or rejected. If timeout is None, this could wait indefinitely! Args: transaction_hash: Transaction hash that we are waiting for. confirmations: Number of block confirmations that we will wait for. timeout: Timeout in seconds, raise an Excpetion on timeout. """ if transaction_hash.startswith(b'0x'): warnings.warn( 'transaction_hash seems to be already encoded, this will' ' result in unexpected behavior') if len(transaction_hash) != 32: raise ValueError( 'transaction_hash length must be 32 (it might be hex encoded)') transaction_hash = data_encoder(transaction_hash) deadline = None if timeout: deadline = gevent.Timeout(timeout) deadline.start() try: # used to check if the transaction was removed, this could happen # if gas price is too low: # # > Transaction (acbca3d6) below gas price (tx=1 Wei ask=18 # > Shannon). All sequential txs from this address(7d0eae79) # > will be ignored # last_result = None while True: # Could return None for a short period of time, until the # transaction is added to the pool transaction = self.rpccall_with_retry( 'eth_getTransactionByHash', transaction_hash, ) # if the transaction was added to the pool and then removed if transaction is None and last_result is not None: raise Exception('invalid transaction, check gas price') # the transaction was added to the pool and mined if transaction and transaction['blockNumber'] is not None: break last_result = transaction gevent.sleep(.5) if confirmations: # this will wait for both APPLIED and REVERTED transactions transaction_block = quantity_decoder( transaction['blockNumber']) confirmation_block = transaction_block + confirmations block_number = self.block_number() while block_number < confirmation_block: gevent.sleep(.5) block_number = self.block_number() except gevent.Timeout: raise Exception('timeout when polling for transaction') finally: if deadline: deadline.cancel()
class GJsonRpcClient(GObject.GObject): """ Wrap a raw socket and uses JSON-RPC over it. Supports calling methods, but not receiving server-initiated messages (ie: signals) """ __gsignals__ = { "connection-closed": ( GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE, (), ), "response": ( GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE, (GObject.TYPE_INT, GObject.TYPE_STRING), ), "response-error": ( GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE, (GObject.TYPE_INT, GObject.TYPE_STRING), ), } MAX_LINESIZE = 1024 def __init__(self, sock: socket.socket): GObject.GObject.__init__(self) self.protocol = JSONRPCProtocol() self.sock = sock self.buffer = b"" def run(self): GLib.io_add_watch(self.sock.fileno(), GLib.IO_IN, self._on_data) GLib.io_add_watch(self.sock.fileno(), GLib.IO_HUP | GLib.IO_ERR, self._on_close) def call_async(self, method: str, args=[], kwargs={}): req = self.protocol.create_request(method, args, kwargs) print('call async', req.unique_id) output = req.serialize() + "\n" self.sock.send(output.encode("utf8")) def _on_close(self, *args): self.emit('connection-closed') def _on_data(self, *args): self.buffer += self.sock.recv(self.MAX_LINESIZE) while b"\n" in self.buffer: newline_pos = self.buffer.find(b"\n") msg = self.buffer[:newline_pos] self.buffer = self.buffer[newline_pos + 1:] try: response = self.protocol.parse_reply(msg) except BadReplyError: return if hasattr(response, "error"): self.emit("response-error", response.unique_id, response.error) else: self.emit("response", response.unique_id, response.result) return True