class JSONRPCClient(object): protocol = JSONRPCProtocol() def __init__(self, port=4000, print_communication=True): self.transport = HttpPostClientTransport('http://127.0.0.1:{}'.format(port)) self.print_communication = print_communication def call(self, method, *args, **kwargs): request = self.protocol.create_request(method, args, kwargs) reply = self.transport.send_message(request.serialize()) if self.print_communication: print "Request:" print json.dumps(json.loads(request.serialize()), indent=2) print "Reply:" print reply return self.protocol.parse_reply(reply).result __call__ = call def find_block(self, condition): """Query all blocks one by one and return the first one for which `condition(block)` evaluates to `True`. """ i = 0 while True: block = self.call('eth_getBlockByNumber', quantity_encoder(i), True, print_comm=False) if condition(block): return block i += 1 def eth_sendTransaction(self, nonce=None, sender='', to='', value=0, data='', gasprice=default_gasprice, startgas=default_startgas, v=None, r=None, s=None): encoders = dict(nonce=quantity_encoder, sender=address_encoder, to=data_encoder, value=quantity_encoder, gasprice=quantity_encoder, startgas=quantity_encoder, data=data_encoder, v=quantity_encoder, r=quantity_encoder, s=quantity_encoder) data = {k: encoders[k](v) for k, v in locals().items() if k not in ('self', 'encoders') and v is not None} data['from'] = data.pop('sender') assert data.get('from') or (v and r and s) res = self.call('eth_sendTransaction', data) return data_decoder(res)
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()
class JSONRPCClient(object): protocol = JSONRPCProtocol() def __init__(self, port=4000, print_communication=True, privkey=None, sender=None): "specify privkey for local signing" self.transport = HttpPostClientTransport( 'http://127.0.0.1:{}'.format(port)) self.print_communication = print_communication self.privkey = privkey self._sender = sender @property def sender(self): if self.privkey: return privtoaddr(self.privkey) if self._sender is None: self._sender = self.coinbase return self._sender def call(self, method, *args): request = self.protocol.create_request(method, args) reply = self.transport.send_message(request.serialize()) if self.print_communication: print json.dumps(json.loads(request.serialize()), indent=2) print reply return self.protocol.parse_reply(reply).result __call__ = call def find_block(self, condition): """Query all blocks one by one and return the first one for which `condition(block)` evaluates to `True`. """ i = 0 while True: block = self.call('eth_getBlockByNumber', quantity_encoder(i), True) if condition(block): return block i += 1 return None def new_filter(self, fromBlock="", toBlock="", address=None, topics=[]): encoders = dict(fromBlock=block_tag_encoder, toBlock=block_tag_encoder, address=address_encoder, topics=lambda x: [topic_encoder(t) for t in x]) data = { k: encoders[k](v) for k, v in locals().items() if k not in ('self', 'encoders') and v is not None } fid = self.call('eth_newFilter', data) return quantity_decoder(fid) def filter_changes(self, fid): changes = self.call('eth_getFilterChanges', quantity_encoder(fid)) if not changes: return None elif isinstance(changes, bytes): return data_decoder(changes) else: decoders = dict(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 eth_sendTransaction(self, nonce=None, sender='', to='', value=0, data='', gasPrice=default_gasprice, gas=default_startgas, v=None, r=None, s=None, secret=None): to = address20(to) encoders = dict(nonce=quantity_encoder, sender=address_encoder, to=data_encoder, value=quantity_encoder, gasPrice=quantity_encoder, gas=quantity_encoder, data=data_encoder, v=quantity_encoder, r=quantity_encoder, s=quantity_encoder, secret=secret_encoder) data = { k: encoders[k](v) for k, v in locals().items() if k not in ('self', 'encoders') and v is not None } data['from'] = data.pop('sender') assert data.get('from') or (v and r and s) res = self.call('eth_sendTransaction', data) return data_decoder(res) def eth_call(self, sender='', to='', value=0, data='', startgas=default_startgas, gasprice=default_gasprice): "call on pending block" encoders = dict(sender=address_encoder, to=data_encoder, value=quantity_encoder, gasprice=quantity_encoder, startgas=quantity_encoder, data=data_encoder) data = { k: encoders[k](v) for k, v in locals().items() if k not in ('self', 'encoders') and v is not None } for k, v in dict(gasprice='gasPrice', startgas='gas', sender='from').items(): data[v] = data.pop(k) res = self.call('eth_call', data) return data_decoder(res) def blocknumber(self): return quantity_decoder(self.call('eth_blockNumber')) def nonce(self, address): if len(address) == 40: address = address.decode('hex') return quantity_decoder( self.call('eth_getTransactionCount', address_encoder(address), 'pending')) @property def coinbase(self): return address_decoder(self.call('eth_coinbase')) def balance(self, account): b = quantity_decoder( self.call('eth_getBalance', address_encoder(account), 'pending')) return b def gaslimit(self): return quantity_decoder(self.call('eth_gasLimit')) def lastgasprice(self): return quantity_decoder(self.call('eth_lastGasPrice')) def send_transaction(self, sender, to, value=0, data='', startgas=0, gasprice=10 * denoms.szabo): "can send a locally signed transaction if privkey is given" assert self.privkey or sender if self.privkey: _sender = sender sender = privtoaddr(self.privkey) assert sender == _sender assert sender # fetch nonce nonce = self.nonce(sender) if not startgas: startgas = quantity_decoder(self.call('eth_gasLimit')) - 1 # create transaction tx = Transaction(nonce, gasprice, startgas, to=to, value=value, data=data) if self.privkey: tx.sign(self.privkey) tx_dict = tx.to_dict() tx_dict.pop('hash') for k, v in dict(gasprice='gasPrice', startgas='gas').items(): tx_dict[v] = tx_dict.pop(k) tx_dict['sender'] = sender res = self.eth_sendTransaction(**tx_dict) assert len(res) in (20, 32) return res.encode('hex') def new_abi_contract(self, _abi, address): sender = self.sender or privtoaddr(self.privkey) return ABIContract(sender, _abi, address, self.eth_call, self.send_transaction)
class JSONRPCClient(object): protocol = JSONRPCProtocol() def __init__(self, host='127.0.0.1', port=4000, print_communication=True, privkey=None, sender=None, use_ssl=False, transport=None): "specify privkey for local signing" if transport is None: self.transport = HttpPostClientTransport( '{}://{}:{}'.format('https' if use_ssl else 'http', host, port), headers={'content-type': 'application/json'}) else: self.transport = transport self.print_communication = print_communication self.privkey = privkey self._sender = sender self.port = port def __repr__(self): return '<JSONRPCClient @%d>' % self.port @property def sender(self): if self.privkey: return privtoaddr(self.privkey) if self._sender is None: self._sender = self.coinbase return self._sender @property def coinbase(self): """ Return the client coinbase address. """ return address_decoder(self.call('eth_coinbase')) def blocknumber(self): """ Return the most recent block. """ return quantity_decoder(self.call('eth_blockNumber')) def nonce(self, address): if len(address) == 40: address = address.decode('hex') try: res = self.call('eth_nonce', address_encoder(address), 'pending') return quantity_decoder(res) except JSONRPCClientReplyError as e: if e.message == 'Method not found': raise JSONRPCClientReplyError( "'eth_nonce' is not supported by your endpoint (pyethapp only). " "For transactions use server-side nonces: " "('eth_sendTransaction' with 'nonce=None')") raise e 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): return quantity_decoder(self.call('eth_gasLimit')) def lastgasprice(self): return quantity_decoder(self.call('eth_lastGasPrice')) def new_abi_contract(self, contract_interface, address): warnings.warn('deprecated, use new_contract_proxy', DeprecationWarning) return self.new_contract_proxy(contract_interface, address) 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. """ sender = self.sender or privtoaddr(self.privkey) return ContractProxy( sender, contract_interface, address, self.eth_call, self.send_transaction, ) def deploy_solidity_contract( self, sender, contract_name, all_contracts, # pylint: disable=too-many-locals libraries, constructor_parameters, timeout=None, gasprice=denoms.wei): if contract_name not in all_contracts: raise ValueError('Unkonwn contract {}'.format(contract_name)) libraries = dict(libraries) contract = all_contracts[contract_name] contract_interface = contract['abi'] symbols = solidity_unresolved_symbols(contract['bin_hex']) if symbols: available_symbols = map(solidity_library_symbol, all_contracts.keys()) # pylint: disable=bad-builtin 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_name, dependencies) deployment_order.pop() # remove `contract_name` from the list log.debug('Deploing 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 = hex_bytecode.decode('hex') dependency_contract['bin_hex'] = hex_bytecode dependency_contract['bin'] = bytecode transaction_hash = self.send_transaction( sender, to='', data=bytecode, gasprice=gasprice, ) self.poll(transaction_hash.decode('hex'), timeout=timeout) receipt = self.call('eth_getTransactionReceipt', '0x' + transaction_hash) contract_address = receipt['contractAddress'] contract_address = contract_address[ 2:] # remove the hexadecimal prefix 0x from the address libraries[deploy_contract] = contract_address deployed_code = self.call('eth_getCode', contract_address, 'latest') if deployed_code == '0x': raise RuntimeError( "Contract address has no code, check gas usage.") hex_bytecode = solidity_resolve_symbols(contract['bin_hex'], libraries) bytecode = hex_bytecode.decode('hex') 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 = self.send_transaction( sender, to='', data=bytecode, gasprice=gasprice, ) self.poll(transaction_hash.decode('hex'), timeout=timeout) receipt = self.call('eth_getTransactionReceipt', '0x' + transaction_hash) contract_address = receipt['contractAddress'] deployed_code = self.call('eth_getCode', contract_address, 'latest') if deployed_code == '0x': raise RuntimeError( "Deployment of {} failed. Contract address has no code, check gas usage." .format(contract_name)) return ContractProxy( sender, contract_interface, contract_address, self.eth_call, self.send_transaction, ) def find_block(self, condition): """Query all blocks one by one and return the first one for which `condition(block)` evaluates to `True`. """ i = 0 while True: block = self.call('eth_getBlockByNumber', quantity_encoder(i), True) if condition(block) or not block: return block i += 1 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 elif isinstance(changes, bytes): return data_decoder(changes) else: decoders = dict(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 call(self, method, *args): """ Do the request and returns the result. Args: method (str): The RPC method. args: The encoded arguments expected by the method. - Object arguments must be supplied as an 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()) if self.print_communication: print json.dumps(json.loads(request.serialize()), indent=2) print reply jsonrpc_reply = self.protocol.parse_reply(reply) if isinstance(jsonrpc_reply, JSONRPCSuccessResponse): return jsonrpc_reply.result elif isinstance(jsonrpc_reply, JSONRPCErrorResponse): raise JSONRPCClientReplyError(jsonrpc_reply.error) else: raise JSONRPCClientReplyError('Unknown type of JSONRPC reply') __call__ = call def send_transaction(self, sender, to, value=0, data='', startgas=0, gasprice=10 * denoms.szabo, 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 and not sender: sender = privtoaddr(self.privkey) if nonce is None: nonce = self.nonce(sender) elif self.privkey: if sender != privtoaddr(self.privkey): 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: # add the fields v, r and s tx.sign(self.privkey) tx_dict = tx.to_dict() # rename the fields to match the eth_sendTransaction signature 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 res.encode('hex') def eth_sendTransaction(self, nonce=None, sender='', to='', value=0, data='', gasPrice=default_gasprice, gas=default_startgas, v=None, r=None, s=None): """ Creates new message call transaction or a contract creation, if the data field contains code. Note: The support for local signing through the variables v,r,s is not part of the standard spec, a extended server is required. Args: from (address): The 20 bytes address the transaction is send 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 paid gas. value (int): Value send 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 creating the empty string must be used.') 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), } if not sender and not (v and r and s): raise ValueError('Either sender or v, r, s needs to be informed.') if sender is not None: json_data['from'] = address_encoder(sender) if v and r and s: json_data['v'] = quantity_encoder(v) json_data['r'] = quantity_encoder(r) json_data['s'] = quantity_encoder(s) 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=default_startgas, gasprice=default_gasprice, block_number='latest'): """ Executes a new message call immediately without creating a transaction on the block chain. Args: from: The address the transaction is send 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 each paid gas. value (int): Integer of the value send 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 = dict() if sender is not None: json_data['from'] = address_encoder(sender) if to is not None: json_data['to'] = data_encoder(to) if value is not None: json_data['value'] = quantity_encoder(value) if gasprice is not None: json_data['gasPrice'] = quantity_encoder(gasprice) if startgas is not None: json_data['gas'] = quantity_encoder(startgas) if data is not None: json_data['data'] = data_encoder(data) res = self.call('eth_call', json_data, block_number) return data_decoder(res) 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 encode)') deadline = None if timeout: deadline = time.time() + timeout transaction_hash = data_encoder(transaction_hash) transaction = self.call('eth_getTransactionByHash', transaction_hash) while transaction is None or transaction["blockNumber"] is None: if deadline and time.time() > deadline: raise Exception('timeout when polling for transaction') gevent.sleep(.5) transaction = self.call('eth_getTransactionByHash', transaction_hash) if confirmations is None: return # 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 confirmation_block > block_number: if deadline and time.time() > deadline: raise Exception('timeout when waiting for confirmation') gevent.sleep(.5) block_number = self.blocknumber()
class JSONRPCClient(object): protocol = JSONRPCProtocol() def __init__(self, port=4000, print_communication=True, privkey=None, sender=None): "specify privkey for local signing" self.transport = HttpPostClientTransport('http://127.0.0.1:{}'.format(port)) self.print_communication = print_communication self.privkey = privkey self._sender = sender @property def sender(self): if self.privkey: return privtoaddr(self.privkey) if self._sender is None: self._sender = self.coinbase return self._sender def call(self, method, *args, **kwargs): request = self.protocol.create_request(method, args, kwargs) reply = self.transport.send_message(request.serialize()) if self.print_communication: print json.dumps(json.loads(request.serialize()), indent=2) print reply return self.protocol.parse_reply(reply).result __call__ = call def find_block(self, condition): """Query all blocks one by one and return the first one for which `condition(block)` evaluates to `True`. """ i = 0 while True: block = self.call('eth_getBlockByNumber', quantity_encoder(i), True, print_comm=False) if condition(block): return block i += 1 def eth_sendTransaction(self, nonce=None, sender='', to='', value=0, data='', gasPrice=default_gasprice, gas=default_startgas, v=None, r=None, s=None): encoders = dict(nonce=quantity_encoder, sender=address_encoder, to=data_encoder, value=quantity_encoder, gasPrice=quantity_encoder, gas=quantity_encoder, data=data_encoder, v=quantity_encoder, r=quantity_encoder, s=quantity_encoder) data = {k: encoders[k](v) for k, v in locals().items() if k not in ('self', 'encoders') and v is not None} data['from'] = data.pop('sender') assert data.get('from') or (v and r and s) res = self.call('eth_sendTransaction', data) return data_decoder(res) def eth_call(self, sender='', to='', value=0, data='', startgas=default_startgas, gasprice=default_gasprice): "call on pending block" encoders = dict(sender=address_encoder, to=data_encoder, value=quantity_encoder, gasprice=quantity_encoder, startgas=quantity_encoder, data=data_encoder) data = {k: encoders[k](v) for k, v in locals().items() if k not in ('self', 'encoders') and v is not None} for k, v in dict(gasprice='gasPrice', startgas='gas', sender='from').items(): data[v] = data.pop(k) res = self.call('eth_call', data) return data_decoder(res) def blocknumber(self): return quantity_decoder(self.call('eth_blockNumber')) def nonce(self, address): if len(address) == 40: address = address.decode('hex') return quantity_decoder( self.call('eth_getTransactionCount', address_encoder(address), 'pending')) @property def coinbase(self): return address_decoder(self.call('eth_coinbase')) def balance(self, account): b = quantity_decoder( self.call('eth_getBalance', address_encoder(account), 'pending')) return b def gaslimit(self): return quantity_decoder(self.call('eth_gasLimit')) def lastgasprice(self): return quantity_decoder(self.call('eth_lastGasPrice')) def send_transaction(self, sender, to, value=0, data='', startgas=0, gasprice=10*denoms.szabo): "can send a locally signed transaction if privkey is given" assert self.privkey or sender if self.privkey: _sender = sender sender = privtoaddr(self.privkey) assert sender == _sender # fetch nonce nonce = self.nonce(sender) if not startgas: startgas = quantity_decoder(self.call('eth_gasLimit')) - 1 # create transaction tx = Transaction(nonce, gasprice, startgas, to=to, value=value, data=data) if self.privkey: tx.sign(self.privkey) tx_dict = tx.to_dict() tx_dict.pop('hash') for k, v in dict(gasprice='gasPrice', startgas='gas', sender='from').items(): tx_dict[v] = tx_dict.pop(k) res = self.eth_sendTransaction(**tx_dict) assert len(res) in (20, 32) return res.encode('hex') def new_abi_contract(self, _abi, address): sender = self.sender or privtoaddr(self.privkey) return ABIContract(sender, _abi, address, self.eth_call, self.send_transaction)
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(object): protocol = JSONRPCProtocol() def __init__(self, port=4000, print_communication=True): self.transport = HttpPostClientTransport( 'http://127.0.0.1:{}'.format(port)) self.print_communication = print_communication def call(self, method, *args, **kwargs): request = self.protocol.create_request(method, args, kwargs) reply = self.transport.send_message(request.serialize()) if self.print_communication: print "Request:" print json.dumps(json.loads(request.serialize()), indent=2) print "Reply:" print reply return self.protocol.parse_reply(reply).result __call__ = call def find_block(self, condition): """Query all blocks one by one and return the first one for which `condition(block)` evaluates to `True`. """ i = 0 while True: block = self.call('eth_getBlockByNumber', quantity_encoder(i), True, print_comm=False) if condition(block): return block i += 1 def eth_sendTransaction(self, nonce=None, sender='', to='', value=0, data='', gasprice=default_gasprice, startgas=default_startgas, v=None, r=None, s=None): encoders = dict(nonce=quantity_encoder, sender=address_encoder, to=data_encoder, value=quantity_encoder, gasprice=quantity_encoder, startgas=quantity_encoder, data=data_encoder, v=quantity_encoder, r=quantity_encoder, s=quantity_encoder) data = { k: encoders[k](v) for k, v in locals().items() if k not in ('self', 'encoders') and v is not None } data['from'] = data.pop('sender') assert data.get('from') or (v and r and s) res = self.call('eth_sendTransaction', data) return data_decoder(res)
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()