async def call(self, method, params=None, notification=False): msg = {"jsonrpc": "2.0", "method": method} if params: msg['params'] = params if not notification: msg['id'] = self.id self.id += 1 self.con.write_message(tornado.escape.json_encode(msg)) if notification: return future = asyncio.Future() tornado.ioloop.IOLoop.current().add_callback(self.handle_calls, msg['id'], future) result = await future if 'error' in result: raise JsonRPCError( msg['id'], result['error']['code'], result['error']['message'], result['error']['data'] if 'data' in result['error'] else None) if 'result' not in result: raise JsonRPCError( msg['id'], -1, "missing result field from jsonrpc response: {}".format( result), None) return result['result']
async def cancel_queued_transaction(self, tx_hash, signature): if not validate_transaction_hash(tx_hash): raise JsonRPCInvalidParamsError(data={'id': 'invalid_transaction_hash', 'message': 'Invalid Transaction Hash'}) if not validate_signature(signature): raise JsonRPCInvalidParamsError(data={'id': 'invalid_signature', 'message': 'Invalid Signature'}) async with self.db: tx = await self.db.fetchrow("SELECT * FROM transactions WHERE hash = $1 AND (status != 'error' OR status IS NULL)", tx_hash) if tx is None: raise JsonRPCError(None, -32000, "Transaction not found", {'id': 'not_found', 'message': 'Transaction not found'}) elif tx['status'] != 'queued' and tx['status'] is not None: raise JsonRPCError(None, -32000, "Transaction already sent to node", {'id': 'invalid_transaction_status', 'message': 'Transaction already sent to node'}) message = "Cancel transaction " + tx_hash if not personal_ecrecover(message, signature, tx['from_address']): raise JsonRPCError(None, -32000, "Permission Denied", {'id': 'permission_denied', 'message': 'Permission Denied'}) log.info("Setting tx '{}' to error due to user cancelation".format(tx['hash'])) self.tasks.update_transaction(tx['transaction_id'], 'error')
async def _execute_single(self, data, result_processor, request_timeout=None): if request_timeout is None: request_timeout = self._request_timeout # NOTE: letting errors fall through here for now as it means # there is something drastically wrong with the jsonrpc server # which means something probably needs to be fixed req_start = time.time() retries = 0 while True: try: resp = await self._httpclient.fetch( self._url, method="POST", body=data, request_timeout=request_timeout) except Exception as e: self.log.error( "Error in JsonRPCClient._fetch ({}, {}) \"{}\" attempt {}". format(data['method'], data['params'], str(e), retries)) retries += 1 if self.should_retry and isinstance( e, HTTPError) and (e.status == 599 or e.status == 502): # always retry after 599 pass elif not self.should_retry or time.time( ) - req_start >= request_timeout: raise await asyncio.sleep(random.random()) continue rval = await resp.json() # verify the id we got back is the same as what we passed if data['id'] != rval['id']: raise JsonRPCError( -1, "returned id was not the same as the inital request") if "error" in rval: # handle potential issues with the block number requested being too high because # the nodes haven't all synced to the current block yet # TODO: this is only supported by parity: geth returns "<nil>" when the block number if too high if 'message' in rval['error'] and rval['error'][ 'message'] == "Unknown block number": retries += 1 if self.should_retry and time.time( ) - req_start < request_timeout: await asyncio.sleep(random.random()) continue raise JsonRPCError( rval['id'], rval['error']['code'], rval['error']['message'], rval['error']['data'] if 'data' in rval['error'] else None) if result_processor: return result_processor(rval['result']) return rval['result']
async def add_token(self, *, contract_address, name=None, symbol=None, decimals=None): if not self.user_toshi_id: raise JsonRPCInvalidParamsError(data={'id': 'bad_arguments', 'message': "Missing authorisation"}) token = await self.get_token(contract_address) if token is None: raise JsonRPCError(None, -32000, "Invalid ERC20 Token", {'id': 'bad_arguments', 'message': "Invalid ERC20 Token"}) contract_address = token['contract_address'] if 'balance' not in token: log.warning("didn't find a balance when adding custom token: {}".format(contract_address)) balance = '0x0' else: balance = token['balance'] async with self.db: await self.db.execute("INSERT INTO token_balances (eth_address, contract_address, name, symbol, decimals, balance, visibility) " "VALUES ($1, $2, $3, $4, $5, $6, $7) " "ON CONFLICT (eth_address, contract_address) DO UPDATE " "SET name = EXCLUDED.name, symbol = EXCLUDED.symbol, decimals = EXCLUDED.decimals, balance = EXCLUDED.balance, visibility = EXCLUDED.visibility", self.user_toshi_id, contract_address, name, symbol, decimals, balance, 2) await self.db.commit() return token
async def _eth_getLogs_with_block_number_validation(self, kwargs): req_start = time.time() from_block = parse_int(kwargs.get('fromBlock', None)) to_block = parse_int(kwargs.get('toBlock', None)) while True: bulk = self.bulk() bn_future = bulk.eth_blockNumber() lg_future = bulk._fetch("eth_getLogs", [kwargs]) await bulk.execute() bn = bn_future.result() if (from_block and bn < from_block) or (to_block and bn < to_block): if self.should_retry and time.time( ) - req_start < self._request_timeout: await asyncio.sleep(random.random()) continue raise JsonRPCError(None, -32000, "Unknown block number", None) return lg_future.result()
async def create_transaction_skeleton(self, *, to_address, from_address, value=0, nonce=None, gas=None, gas_price=None, data=None, token_address=None): # strip begining and trailing whitespace from addresses if to_address is not None and isinstance(to_address, str): to_address = to_address.strip() if from_address is not None and isinstance(from_address, str): to_address = to_address.strip() if token_address is not None and isinstance(token_address, str): token_address = token_address.strip() if not validate_address(from_address): raise JsonRPCInvalidParamsError(data={ 'id': 'invalid_from_address', 'message': 'Invalid From Address' }) if to_address is not None and not validate_address(to_address): raise JsonRPCInvalidParamsError(data={ 'id': 'invalid_to_address', 'message': 'Invalid To Address' }) if from_address != from_address.lower( ) and not checksum_validate_address(from_address): raise JsonRPCInvalidParamsError( data={ 'id': 'invalid_from_address', 'message': 'Invalid From Address Checksum' }) if to_address is not None and to_address != to_address.lower( ) and not checksum_validate_address(to_address): raise JsonRPCInvalidParamsError( data={ 'id': 'invalid_to_address', 'message': 'Invalid To Address Checksum' }) # check if we should ignore the given gasprice # NOTE: only meant to be here while cryptokitty fever is pushing # up gas prices... this shouldn't be perminant # anytime the nonce is also set, use the provided gas (this is to # support easier overwriting of transactions) if gas_price is not None and nonce is None: async with self.db: whitelisted = await self.db.fetchrow( "SELECT 1 FROM from_address_gas_price_whitelist WHERE address = $1", from_address) if not whitelisted: whitelisted = await self.db.fetchrow( "SELECT 1 FROM to_address_gas_price_whitelist WHERE address = $1", to_address) if not whitelisted: gas_price = None if gas_price is None: # try and use cached gas station gas price gas_station_gas_price = await self.redis.get( 'gas_station_standard_gas_price') if gas_station_gas_price: gas_price = parse_int(gas_station_gas_price) if gas_price is None: gas_price = config['ethereum'].getint('default_gasprice', DEFAULT_GASPRICE) else: gas_price = parse_int(gas_price) if gas_price is None: raise JsonRPCInvalidParamsError(data={ 'id': 'invalid_gas_price', 'message': 'Invalid Gas Price' }) if gas is not None: gas = parse_int(gas) if gas is None: raise JsonRPCInvalidParamsError(data={ 'id': 'invalid_gas', 'message': 'Invalid Gas' }) if nonce is None: # check cache for nonce nonce = await self.get_transaction_count(from_address) else: nonce = parse_int(nonce) if nonce is None: raise JsonRPCInvalidParamsError(data={ 'id': 'invalid_nonce', 'message': 'Invalid Nonce' }) if data is not None: if isinstance(data, int): data = hex(data) if isinstance(data, str): try: data = data_decoder(data) except binascii.Error: pass if not isinstance(data, bytes): raise JsonRPCInvalidParamsError(data={ 'id': 'invalid_data', 'message': 'Invalid Data field' }) else: data = b'' # flag to force arguments into an erc20 token transfer if token_address is not None: if not validate_address(token_address): raise JsonRPCInvalidParamsError( data={ 'id': 'invalid_token_address', 'message': 'Invalid Token Address' }) if data != b'': raise JsonRPCInvalidParamsError( data={ 'id': 'bad_arguments', 'message': 'Cannot include both data and token_address' }) if isinstance(value, str) and value.lower() == "max": # get the balance in the database async with self.db: value = await self.db.fetchval( "SELECT balance FROM token_balances " "WHERE contract_address = $1 AND eth_address = $2", token_address, from_address) if value is None: # get the value from the ethereum node data = "0x70a08231000000000000000000000000" + from_address[ 2:].lower() value = await self.eth.eth_call(to_address=token_address, data=data) value = parse_int(value) if value is None or value < 0: raise JsonRPCInvalidParamsError(data={ 'id': 'invalid_value', 'message': 'Invalid Value' }) data = data_decoder( "0xa9059cbb000000000000000000000000{}{:064x}".format( to_address[2:].lower(), value)) token_value = value value = 0 to_address = token_address elif value: if value == "max": network_balance, balance, _, _ = await self.get_balances( from_address) if gas is None: code = await self.eth.eth_getCode(to_address) if code: # we might have to do some work try: gas = await self.eth.eth_estimateGas(from_address, to_address, data=data, value=0) except JsonRPCError: # no fallback function implemented in the contract means no ether can be sent to it raise JsonRPCInvalidParamsError( data={ 'id': 'invalid_to_address', 'message': 'Cannot send payments to that address' }) attempts = 0 # because the default function could do different things based on the eth sent, we make sure # the value is suitable. if we get different values 3 times abort while True: if attempts > 2: log.warning( "Hit max attempts trying to get max value to send to contract '{}'" .format(to_address)) raise JsonRPCInvalidParamsError( data={ 'id': 'invalid_to_address', 'message': 'Cannot send payments to that address' }) value = balance - (gas_price * gas) # make sure the balance isn't negative if value < 0: raise JsonRPCInsufficientFundsError( data={ 'id': 'insufficient_funds', 'message': 'Insufficient Funds' }) try: gas_with_value = await self.eth.eth_estimateGas( from_address, to_address, data=data, value=value) except JsonRPCError: # no fallback function implemented in the contract means no ether can be sent to it raise JsonRPCInvalidParamsError( data={ 'id': 'invalid_to_address', 'message': 'Cannot send payments to that address' }) if gas_with_value != gas: gas = gas_with_value attempts += 1 continue else: break else: # normal address, 21000 gas per transaction gas = 21000 value = balance - (gas_price * gas) else: # preset gas, run with it! value = balance - (gas_price * gas) else: value = parse_int(value) if value is None or value < 0: raise JsonRPCInvalidParamsError(data={ 'id': 'invalid_value', 'message': 'Invalid Value' }) if gas is None: try: gas = await self.eth.eth_estimateGas(from_address, to_address, data=data, value=value) except JsonRPCError: # this can occur if sending a transaction to a contract that doesn't match a valid method # and the contract has no default method implemented. # this can also happen if the current state of the blockchain means that submitting the # transaction would fail (abort). if token_address is not None: # when dealing with erc20, this usually means the user's balance for that token isn't # high enough, check that and throw an error if it's the case, and if not fall # back to the standard invalid_data error async with self.db: bal = await self.db.fetchval( "SELECT balance FROM token_balances " "WHERE contract_address = $1 AND eth_address = $2", token_address, from_address) if bal is not None: bal = parse_int(bal) if bal < token_value: raise JsonRPCInsufficientFundsError( data={ 'id': 'insufficient_funds', 'message': 'Insufficient Funds' }) raise JsonRPCInvalidParamsError( data={ 'id': 'invalid_data', 'message': 'Unable to estimate gas for contract call' }) # if data is present, buffer gas estimate by 20% if len(data) > 0: gas = int(gas * 1.2) try: tx = create_transaction(nonce=nonce, gasprice=gas_price, startgas=gas, to=to_address, value=value, data=data, network_id=self.network_id) except InvalidTransaction as e: raise JsonRPCInvalidParamsError(data={ 'id': 'invalid_transaction', 'message': str(e) }) except binascii.Error as e: log.exception( "Error creating transaction skeleton: nonce:{} gasprice:{} startgas:{} to:{} value:{} data:{} network_id:{}" .format(nonce, gas_price, gas, to_address, value, data, self.network_id)) raise JsonRPCError( None, -32000, "Error creating transaction skeleton", { 'id': 'unexpected_error', 'message': "Error creating transaction skeleton" }) if tx.intrinsic_gas_used > gas: raise JsonRPCInvalidParamsError( data={ 'id': 'invalid_transaction', 'message': 'Transaction gas is too low. There is not enough gas to cover minimal cost of the transaction (minimal: {}, got: {}). Try increasing supplied gas.' .format(tx.intrinsic_gas_used, gas) }) transaction = encode_transaction(tx) return { "tx": transaction, "gas": hex(gas), "gas_price": hex(gas_price), "nonce": hex(nonce), "value": hex(token_value) if token_address else hex(value) }
async def execute(self): if not self._bulk_mode: raise Exception("No Bulk request started") if len(self._bulk_data) == 0: return [] data = self._bulk_data[:] self._bulk_data = [] futures = self._bulk_futures.copy() self._bulk_futures = {} req_start = time.time() retries = 0 while True: try: resp = await self._httpclient.fetch( self._url, method="POST", body=data, request_timeout= 60.0 # higher request timeout than other operations ) except Exception as e: self.log.error( "Error in JsonRPCClient.execute: retry {}".format(retries)) retries += 1 if self.should_retry and isinstance( e, HTTPError) and (e.status == 599 or e.status == 502): # always retry after 599 pass elif not self.should_retry or time.time( ) - req_start >= self._request_timeout: # give up after the request timeout raise await asyncio.sleep(random.random()) continue break rvals = await resp.json() results = [] for rval in rvals: if 'id' not in rval: continue future, result_processor = futures.pop(rval['id'], (None, None)) if future is None: self.log.warning("Got unexpected id in jsonrpc bulk response") continue if "error" in rval: future.set_exception( JsonRPCError( rval['id'], rval['error']['code'], rval['error']['message'], rval['error']['data'] if 'data' in rval['error'] else None)) result = None else: if result_processor: result = result_processor(rval['result']) else: result = rval['result'] future.set_result(result) results.append(result) if len(futures): self.log.warning( "Found some unprocessed requests in bulk jsonrpc request") for future, result_processor in futures: future.set_exception(Exception("Unexpectedly missing result")) return results