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
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)
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()
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 print "\ndengan param" print request4.serialize() """ Hasil balik dari SERVER kita asumsikan server telah mengirim data yang diminta maka tahap selanjutnya 1. buat data dari server menjadi object rpc menggunakan parse_reply 2. hasilnya bs d dapat dgn method .result """ data_dari_server = '{"jsonrpc": "2.0", "id": 1, "result": "ini_jawaban"}' response = ini_instance.parse_reply(data_dari_server) print "\n=====================\nresult reply=", response.result #ini_handler = methods[request.method]
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: """ async def transport(request: bytes, timeout: Optional[float] = None) -> bytes: # send request via http, amqp, ... return response clt = JsonRpcClient(transport, app) res = await clt.exec('test', (1, 2, 3)) res_1, res_2, res_3, res_4 = await clt.batch( clt.exec('test', (1, 2, 3)), clt.exec('test', (4, 5, 6)), clt.exec('test2', (4, 5, 6)), clt.exec('err'), ) """ def __init__( self, transport: Callable[[bytes, Optional[float]], Awaitable[bytes]], app: BaseApplication, exception_mapping_callback: Optional[ Callable[[Optional[int], Optional[str], Optional[Any]], None] ] = None, ): self._app = app self._proto = JSONRPCProtocol() self._transport = transport self._exception_mapping_callback = exception_mapping_callback def exec( self, method: str, params: Union[Iterable[Any], Mapping[str, Any], None] = None, one_way: bool = False, timeout: Optional[float] = None, model: Optional[Type[BaseModel]] = None, ) -> JsonRpcCall: return JsonRpcCall(self, method, params, one_way, timeout, model) async def exec_batch( self, *calls: JsonRpcCall, timeout: Optional[float] = None ) -> Tuple[Union[JsonRpcError, Any], ...]: if len(calls) == 0: return () b = [] results_order: List[Any] = [] one_way = True for c in calls: b.append(c._encode()) results_order.append(c.unique_id) if not c.one_way: one_way = False request = b'[%s]' % (b','.join(b),) results: List[Any] = [None for _ in results_order] rep = await self._send_batch_request(request, timeout, not one_way) if rep is None: # one_way=true return tuple(results) for r in rep: rd = json.dumps(r).encode() try: result = self._proto.parse_reply(rd) # FIXME in tinyrpc(batch) except InvalidReplyError as err: raise JsonRpcError(jsonrpc_error_code=-32000, message=str(err)) if result.unique_id is not None: try: idx = results_order.index(result.unique_id) except ValueError: raise JsonRpcError( jsonrpc_error_code=-32000, message="Invalid reply: Unexpected request id %s" "" % result.unique_id, ) if isinstance(result, JSONRPCErrorResponse): data: Any = None if hasattr(result, 'data'): data = result.data code = getattr(result, '_jsonrpc_error_code') results[idx] = JsonRpcError( jsonrpc_error_code=code, message=str(result.error), data=data, ) elif isinstance(result, JSONRPCSuccessResponse): results[idx] = calls[idx]._convert_result(result.result) else: raise NotImplementedError return tuple(results) def _raise_jsonrpc_error( self, code: Optional[int] = None, message: Optional[str] = None, data: Optional[Any] = None, ) -> None: if self._exception_mapping_callback is not None: return self._exception_mapping_callback(code, message, data) else: raise JsonRpcError( jsonrpc_error_code=code, message=message, data=data ) async def _send_single_request( self, request: bytes, timeout: Optional[float], method: str, one_way: bool, ) -> Any: with self._app.logger.capture_span(Span) as trap: response = await self._transport(request, timeout) if trap.is_captured: trap.span.name = 'rpc::out (%s)' % method trap.span.set_name4adapter( self._app.logger.ADAPTER_PROMETHEUS, 'rpc_out' ) trap.span.tag(SPAN_TAG_RPC_METHOD, method) if one_way: return None try: data = self._proto.parse_reply(response) except InvalidReplyError as err: raise JsonRpcError(jsonrpc_error_code=-32000, message=str(err)) if isinstance(data, JSONRPCErrorResponse): code: int = int(data._jsonrpc_error_code) if trap.is_captured: trap.span.tag(SPAN_TAG_RPC_CODE, str(code)) self._raise_jsonrpc_error( code, str(data.error), data.data if hasattr(data, 'data') else None, ) if isinstance(data, JSONRPCSuccessResponse): return data.result raise RuntimeError async def _send_batch_request( self, request: bytes, timeout: Optional[float], parse_resp: bool ) -> Optional[List[Any]]: with self._app.logger.capture_span(Span) as trap: result = await self._transport(request, timeout) if trap.is_captured: trap.span.name = 'rpc::out::batch' trap.span.tag(SPAN_TAG_JSONRPC_IS_BATCH, 'true') if not parse_resp: return None try: rep = json.loads(result) except Exception as err: self._raise_jsonrpc_error(message='Invalid reply: %s' % err) if not isinstance(rep, list): rep_err = self._proto.parse_reply(result) if isinstance(rep_err, JSONRPCErrorResponse): code: int = int(rep_err._jsonrpc_error_code) if trap.is_captured: trap.span.tag(SPAN_TAG_RPC_CODE, str(code)) self._raise_jsonrpc_error( code=code, message=str(rep_err.error), data=rep_err.data if hasattr(rep_err, 'data') else None, ) else: self._raise_jsonrpc_error(message='Invalid reply') return rep
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