async def _run(self): while len(self._keys) > 0: try: # I'm worried here about attacks on the login endpoint that # would fill the keys list and block further login attempts # and/or use up all the memory on the server. This block # is in place to warn and debug issues should this end up # happening if len(self._keys) > 500: grouping = len(self._keys) // 500 if grouping > self._posted_warning: log.warning( "Login keys list has reached {} keys".format( len(self._keys))) self._posted_warning = grouping elif grouping < self._posted_warning: log.warning( "Login keys list has returned to {} keys".format( len(self._keys))) self._posted_warning = grouping elif self._posted_warning > 0: log.warning( "Login keys list length has returned to {} keys". format(len(self._keys))) self._posted_warning = 0 allkeys = self._keys[:] for offset in range(0, len(allkeys), 500): keys = allkeys[offset:offset + 500] if len(keys) == 0: continue # the timeout here will cause <1 second latency on new login # requests. result = await get_redis_connection().blpop( *keys, timeout=1, encoding='utf-8') if result: key, result = result self._keys.remove(key) keys.remove(key) if key not in self._futures: log.warning("got result for missing login key") else: future = self._futures.pop(key) future.set_result(result) # cleanup stale keys for key in keys: if key not in self._futures: self._keys.remove(key) future = self._futures[key] if asyncio.get_event_loop().time( ) - future._time > LOGIN_TOKEN_EXPIRY: future.set_excetion(TimeoutError()) if future.done(): del self._futures[key] self._keys.remove(key) except: log.exception("error while checking logins") if len(self._keys) > 0: await asyncio.sleep(1) self._running = False
async def _on_message(self, message): try: response = await WebsocketJsonRPCHandler( self.user_toshi_id, self.application, self)(message) if response: self.write_message(response) except: log.exception("unexpected error handling message: {}".format(message)) raise
async def _get_token_details(self, contract_address): """Get token details from the contract's metadata endpoints""" bulk = self.eth.bulk() balanceof_future = bulk.eth_call(to_address=contract_address, data="{}000000000000000000000000{}".format( ERC20_BALANCEOF_CALL_DATA, self.user_toshi_id[2:] if self.user_toshi_id is not None else "0000000000000000000000000000000000000000")) name_future = bulk.eth_call(to_address=contract_address, data=ERC20_NAME_CALL_DATA) sym_future = bulk.eth_call(to_address=contract_address, data=ERC20_SYMBOL_CALL_DATA) decimals_future = bulk.eth_call(to_address=contract_address, data=ERC20_DECIMALS_CALL_DATA) try: await bulk.execute() except: log.exception("failed getting token details") return None balance = data_decoder(unwrap_or(balanceof_future, "0x")) if balance and balance != "0x": try: balance = decode_abi(['uint256'], balance)[0] except: log.exception("Invalid erc20.balanceOf() result: {}".format(balance)) balance = None else: balance = None name = data_decoder(unwrap_or(name_future, "0x")) if name and name != "0x": try: name = decode_abi(['string'], name)[0].decode('utf-8') except: log.exception("Invalid erc20.name() data: {}".format(name)) name = None else: name = None symbol = data_decoder(unwrap_or(sym_future, "0x")) if symbol and symbol != "0x": try: symbol = decode_abi(['string'], symbol)[0].decode('utf-8') except: log.exception("Invalid erc20.symbol() data: {}".format(symbol)) symbol = None else: symbol = None decimals = data_decoder(unwrap_or(decimals_future, "0x")) if decimals and decimals != "0x": try: decimals = decode_abi(['uint256'], decimals)[0] except: log.exception("Invalid erc20.decimals() data: {}".format(decimals)) decimals = None else: decimals = None return balance, name, symbol, decimals
async def post(self): if self.is_request_signed(): sender_toshi_id = self.verify_request() else: # this is an anonymous transaction sender_toshi_id = None try: result = await ToshiEthJsonRPC( sender_toshi_id, self.application, self.request).send_transaction(**self.json) except JsonRPCInternalError as e: log.exception("Error in POST /tx from: {}: args: {}".format( sender_toshi_id, json_encode(self.json))) raise JSONHTTPError(500, body={'errors': [e.data]}) except JsonRPCError as e: log.exception("Error in POST /tx from: {}: args: {}".format( sender_toshi_id, json_encode(self.json))) raise JSONHTTPError(400, body={'errors': [e.data]}) except TypeError: log.exception("Error in POST /tx from {}: args: {}".format( sender_toshi_id, json_encode(self.json))) raise JSONHTTPError(400, body={ 'errors': [{ 'id': 'bad_arguments', 'message': 'Bad Arguments' }] }) self.write({"tx_hash": result})
async def post(self): eth_address = self.verify_request() try: result = await ToshiEthJsonRPC(eth_address, self.application, self.request).add_token(**self.json) except JsonRPCInternalError as e: raise JSONHTTPError(500, body={'errors': [e.data]}) except JsonRPCError as e: raise JSONHTTPError(400, body={'errors': [e.data]}) except TypeError: log.exception("bad_arguments") raise JSONHTTPError(400, body={ 'errors': [{ 'id': 'bad_arguments', 'message': 'Bad Arguments' }] }) self.write(result)
async def flush(self, endpoint, flush_delay_limit=10, max_size=50): last_flush = 0 # 0 so that the first event is always sent batch = [] while True: batch.append(await self._queues[endpoint].get()) while len(batch) < max_size and time.time() - flush_delay_limit < last_flush: try: batch.append((await asyncio.wait_for(self._queues[endpoint].get(), 2))) except asyncio.TimeoutError: break batch_json = '[{0}]'.format(','.join(batch)) batch = [] last_flush = time.time() data = { 'data': base64.b64encode(batch_json.encode('utf8')), 'verbose': 1, 'ip': 0, } if self._api_key: data.update({'api_key': self._api_key}) encoded_data = urllib.parse.urlencode(data).encode('utf8') resp = await to_asyncio_future(self._httpclient.fetch( self._endpoints[endpoint], method="POST", headers={'Content-Type': "application/x-www-form-urlencoded"}, body=encoded_data)) try: response = json_decode(resp.body) if response['status'] != 1: log.error('Mixpanel error: {0}'.format(response['error'])) except ValueError: log.exception('Cannot interpret Mixpanel server response: {0}'.format(resp.body))
async def __aenter__(self): if self.connection is not None: raise DatabaseError("Connection already in progress") try: self.connection = await self.pool.acquire(timeout=self.timeout) except asyncpg.exceptions.ConnectionDoesNotExistError: log.exception("Error acquiring connection") # attempt to recover the database connection if self.pool is get_database_pool(): set_database_pool(None) try: await self.pool.close() self.pool = await prepare_database(handle_migration=False) self.connection = await self.pool.acquire( timeout=self.timeout) except: log.exception("Unable to recover global database pool") # fail hard in the hope that restarting the system will fix things sys.exit(1) else: raise self.transaction = self.connection.transaction() await self.transaction.start() return self
async def send_transaction(self, *, tx, signature=None): try: tx = decode_transaction(tx) except: raise JsonRPCInvalidParamsError(data={ 'id': 'invalid_transaction', 'message': 'Invalid Transaction' }) if is_transaction_signed(tx): tx_sig = data_encoder(signature_from_transaction(tx)) if signature: if tx_sig != signature: raise JsonRPCInvalidParamsError( data={ 'id': 'invalid_signature', 'message': 'Invalid Signature: Signature in payload and signature of transaction do not match' }) else: signature = tx_sig else: if signature is None: raise JsonRPCInvalidParamsError(data={ 'id': 'missing_signature', 'message': 'Missing Signature' }) if not validate_signature(signature): raise JsonRPCInvalidParamsError( data={ 'id': 'invalid_signature', 'message': 'Invalid Signature: {}'.format('Invalid length' if len( signature) != 132 else 'Invalid hex value') }) try: sig = data_decoder(signature) except Exception: log.exception( "Unexpected error decoding valid signature: {}".format( signature)) raise JsonRPCInvalidParamsError(data={ 'id': 'invalid_signature', 'message': 'Invalid Signature' }) add_signature_to_transaction(tx, sig) # validate network id, if it's not for "all networks" if tx.network_id is not None and self.network_id != tx.network_id: raise JsonRPCInvalidParamsError(data={ 'id': 'invalid_network_id', 'message': 'Invalid Network ID' }) from_address = data_encoder(tx.sender) to_address = data_encoder(tx.to) # prevent spamming of transactions with the same nonce from the same sender with RedisLock(self.redis, "{}:{}".format(from_address, tx.nonce), raise_when_locked=partial(JsonRPCInvalidParamsError, data={ 'id': 'invalid_nonce', 'message': 'Nonce already used' }), ex=5): # disallow transaction overwriting for known transactions async with self.db: existing = await self.db.fetchrow( "SELECT * FROM transactions WHERE " "from_address = $1 AND nonce = $2 AND status != $3", from_address, tx.nonce, 'error') if existing: # debugging checks existing_tx = await self.eth.eth_getTransactionByHash( existing['hash']) raise JsonRPCInvalidParamsError(data={ 'id': 'invalid_nonce', 'message': 'Nonce already used' }) # make sure the account has enough funds for the transaction network_balance, balance, _, _ = await self.get_balances( from_address) #log.info("Attempting to send transaction\nHash: {}\n{} -> {}\nValue: {} + {} (gas) * {} (startgas) = {}\nSender's Balance {} ({} unconfirmed)".format( # calculate_transaction_hash(tx), from_address, to_address, tx.value, tx.startgas, tx.gasprice, tx.value + (tx.startgas * tx.gasprice), network_balance, balance)) if balance < (tx.value + (tx.startgas * tx.gasprice)): raise JsonRPCInsufficientFundsError( data={ 'id': 'insufficient_funds', 'message': 'Insufficient Funds' }) # validate the nonce c_nonce = await self.get_transaction_count(from_address) if tx.nonce < c_nonce: raise JsonRPCInvalidParamsError( data={ 'id': 'invalid_nonce', 'message': 'Provided nonce is too low' }) if tx.nonce > c_nonce: raise JsonRPCInvalidParamsError( data={ 'id': 'invalid_nonce', 'message': 'Provided nonce is too high' }) if tx.intrinsic_gas_used > tx.startgas: 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, tx.startgas) }) # now this tx fits enough of the criteria to allow it # onto the transaction queue tx_hash = calculate_transaction_hash(tx) # add tx to database async with self.db: await self.db.execute( "INSERT INTO transactions " "(hash, from_address, to_address, nonce, " "value, gas, gas_price, " "data, v, r, s, " "sender_toshi_id) " "VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)", tx_hash, from_address, to_address, tx.nonce, hex(tx.value), hex(tx.startgas), hex(tx.gasprice), data_encoder(tx.data), hex(tx.v), hex(tx.r), hex(tx.s), self.user_toshi_id) await self.db.commit() # trigger processing the transaction queue self.tasks.process_transaction_queue(from_address) # analytics # use notification registrations to try find toshi ids for users if self.user_toshi_id: sender_toshi_id = self.user_toshi_id else: async with self.db: sender_toshi_id = await self.db.fetchval( "SELECT toshi_id FROM notification_registrations WHERE " "eth_address = $1", from_address) async with self.db: receiver_toshi_id = await self.db.fetchval( "SELECT toshi_id FROM notification_registrations WHERE " "eth_address = $1", to_address) self.track(sender_toshi_id, "Sent transaction") # it doesn't make sense to add user agent here as we # don't know the receiver's user agent self.track(receiver_toshi_id, "Received transaction", add_user_agent=False) return tx_hash
async def get_token_balances(self, eth_address, token_address=None): if not validate_address(eth_address): raise JsonRPCInvalidParamsError(data={ 'id': 'invalid_address', 'message': 'Invalid Address' }) if token_address is not None and not validate_address(token_address): raise JsonRPCInvalidParamsError(data={ 'id': 'invalid_token_address', 'message': 'Invalid Token Address' }) # get token balances while True: async with self.db: result = await self.db.execute( "UPDATE token_registrations SET last_queried = (now() AT TIME ZONE 'utc') WHERE eth_address = $1", eth_address) await self.db.commit() registered = result == "UPDATE 1" if not registered: try: async with RedisLock( "token_balance_update:{}".format(eth_address)): try: await erc20_dispatcher.update_token_cache( "*", eth_address) except: log.exception("Error updating token cache") raise async with self.db: await self.db.execute( "INSERT INTO token_registrations (eth_address) VALUES ($1)", eth_address) await self.db.commit() break except RedisLockException: # wait until the previous task is done and try again await asyncio.sleep(0.1) continue else: break if token_address: async with self.db: token = await self.db.fetchrow( "SELECT symbol, name, decimals, format " "FROM tokens WHERE contract_address = $1", token_address) if token is None: return None balance = await self.db.fetchval( "SELECT value " "FROM token_balances " "WHERE eth_address = $1 AND contract_address = $2", eth_address, token_address) if balance is None: balance = "0x0" details = { "symbol": token['symbol'], "name": token['name'], "decimals": token['decimals'], "value": balance, "contract_address": token_address } if token['format'] is not None: details["icon"] = "{}://{}/token/{}.{}".format( self.request.protocol, self.request.host, token_address, token['format']) else: details['icon'] = None return details else: async with self.db: balances = await self.db.fetch( "SELECT t.symbol, t.name, t.decimals, b.value, b.contract_address, t.format " "FROM token_balances b " "JOIN tokens t " "ON t.contract_address = b.contract_address " "WHERE eth_address = $1 ORDER BY t.symbol", eth_address) tokens = [] for b in balances: details = { "symbol": b['symbol'], "name": b['name'], "decimals": b['decimals'], "value": b['value'], "contract_address": b['contract_address'] } if b['format'] is not None: details["icon"] = "{}://{}/token/{}.{}".format( self.request.protocol, self.request.host, b['contract_address'], b['format']) else: details['icon'] = None tokens.append(details) return tokens
async def send_transaction(self, *, tx, signature=None): try: tx = decode_transaction(tx) except: raise JsonRPCInvalidParamsError(data={ 'id': 'invalid_transaction', 'message': 'Invalid Transaction' }) if is_transaction_signed(tx): tx_sig = data_encoder(signature_from_transaction(tx)) if signature: if tx_sig != signature: raise JsonRPCInvalidParamsError( data={ 'id': 'invalid_signature', 'message': 'Invalid Signature: Signature in payload and signature of transaction do not match' }) else: signature = tx_sig else: if signature is None: raise JsonRPCInvalidParamsError(data={ 'id': 'missing_signature', 'message': 'Missing Signature' }) if not validate_signature(signature): raise JsonRPCInvalidParamsError( data={ 'id': 'invalid_signature', 'message': 'Invalid Signature: {}'.format('Invalid length' if len( signature) != 132 else 'Invalid hex value') }) try: sig = data_decoder(signature) except Exception: log.exception( "Unexpected error decoding valid signature: {}".format( signature)) raise JsonRPCInvalidParamsError(data={ 'id': 'invalid_signature', 'message': 'Invalid Signature' }) add_signature_to_transaction(tx, sig) # validate network id, if it's not for "all networks" if tx.network_id is not None and self.network_id != tx.network_id: raise JsonRPCInvalidParamsError(data={ 'id': 'invalid_network_id', 'message': 'Invalid Network ID' }) from_address = data_encoder(tx.sender) to_address = data_encoder(tx.to) # prevent spamming of transactions with the same nonce from the same sender async with RedisLock("{}:{}".format(from_address, tx.nonce), raise_when_locked=partial( JsonRPCInvalidParamsError, data={ 'id': 'invalid_nonce', 'message': 'Nonce already used' }), ex=5): # check for transaction overwriting async with self.db: existing = await self.db.fetchrow( "SELECT * FROM transactions WHERE " "from_address = $1 AND nonce = $2 AND " "(status != 'error' or status is NULL)", from_address, tx.nonce) # disallow transaction overwriting when the gas is lower or the transaction is confirmed if existing and (parse_int(existing['gas_price']) >= tx.gasprice or existing['status'] == 'confirmed'): raise JsonRPCInvalidParamsError(data={ 'id': 'invalid_nonce', 'message': 'Nonce already used' }) # make sure the account has enough funds for the transaction network_balance, balance, _, _ = await self.get_balances( from_address) if existing: balance += parse_int(existing['value']) + parse_int( existing['gas']) * parse_int(existing['gas_price']) if balance < (tx.value + (tx.startgas * tx.gasprice)): raise JsonRPCInsufficientFundsError( data={ 'id': 'insufficient_funds', 'message': 'Insufficient Funds' }) # validate the nonce (only necessary if tx doesn't already exist) if not existing: c_nonce = await self.get_transaction_count(from_address) if tx.nonce < c_nonce: raise JsonRPCInvalidParamsError( data={ 'id': 'invalid_nonce', 'message': 'Provided nonce is too low' }) if tx.nonce > c_nonce: raise JsonRPCInvalidParamsError( data={ 'id': 'invalid_nonce', 'message': 'Provided nonce is too high' }) if tx.intrinsic_gas_used > tx.startgas: 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, tx.startgas) }) # now this tx fits enough of the criteria to allow it # onto the transaction queue tx_hash = calculate_transaction_hash(tx) if existing: log.info( "Setting tx '{}' to error due to forced overwrite".format( existing['hash'])) manager_dispatcher.update_transaction( existing['transaction_id'], 'error') data = data_encoder(tx.data) if data and \ ((data.startswith("0xa9059cbb") and len(data) == 138) or \ (data.startswith("0x23b872dd") and len(data) == 202)): # check if the token is a known erc20 token async with self.db: erc20_token = await self.db.fetchrow( "SELECT * FROM tokens WHERE contract_address = $1", to_address) else: erc20_token = False # add tx to database async with self.db: db_tx = await self.db.fetchrow( "INSERT INTO transactions " "(hash, from_address, to_address, nonce, " "value, gas, gas_price, " "data, v, r, s, " "sender_toshi_id) " "VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) " "RETURNING transaction_id", tx_hash, from_address, to_address, tx.nonce, hex(tx.value), hex(tx.startgas), hex(tx.gasprice), data_encoder(tx.data), hex(tx.v), hex(tx.r), hex(tx.s), self.user_toshi_id) if erc20_token: token_value = int(data[-64:], 16) if data.startswith("0x23b872dd"): erc20_from_address = "0x" + data[34:74] erc20_to_address = "0x" + data[98:138] else: erc20_from_address = from_address erc20_to_address = "0x" + data[34:74] await self.db.execute( "INSERT INTO token_transactions " "(transaction_id, transaction_log_index, contract_address, from_address, to_address, value) " "VALUES ($1, $2, $3, $4, $5, $6)", db_tx['transaction_id'], 0, erc20_token['contract_address'], erc20_from_address, erc20_to_address, hex(token_value)) await self.db.commit() # trigger processing the transaction queue manager_dispatcher.process_transaction_queue(from_address) # analytics # use notification registrations to try find toshi ids for users if self.user_toshi_id: sender_toshi_id = self.user_toshi_id else: async with self.db: sender_toshi_id = await self.db.fetchval( "SELECT toshi_id FROM notification_registrations WHERE " "eth_address = $1", from_address) async with self.db: receiver_toshi_id = await self.db.fetchval( "SELECT toshi_id FROM notification_registrations WHERE " "eth_address = $1", to_address) self.track(sender_toshi_id, "Sent transaction") # it doesn't make sense to add user agent here as we # don't know the receiver's user agent self.track(receiver_toshi_id, "Received transaction", add_user_agent=False) return tx_hash
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 get_token(self, contract_address): if not validate_address(contract_address): raise JsonRPCInvalidParamsError(data={'id': 'invalid_address', 'message': 'Invalid Contract Address'}) if contract_address != contract_address.lower(): if not checksum_validate_address(contract_address): raise JsonRPCInvalidParamsError(data={'id': 'invalid_address', 'message': 'Invalid Contract Address Checksum'}) contract_address = contract_address.lower() async with self.db: row = await self.db.fetchrow( "SELECT symbol, name, contract_address, decimals, icon_url, format FROM tokens WHERE contract_address = $1", contract_address) if row: token = { 'symbol': row['symbol'], 'name': row['name'], 'contract_address': row['contract_address'], 'decimals': row['decimals'] } if self.user_toshi_id: async with self.db: balance = await self.db.fetchval( "SELECT balance FROM token_balances WHERE contract_address = $1 and eth_address = $2", contract_address, self.user_toshi_id) if balance is None: try: balance = await self.eth.eth_call(to_address=contract_address, data="{}000000000000000000000000{}".format( ERC20_BALANCEOF_CALL_DATA, self.user_toshi_id[2:])) except: log.exception("Unable to get balance of erc20 token {} for address {}".format(contract_address, self.user_toshi_id)) return None if balance == "0x": balance = "0x0" else: # strip 0 padding balance = hex(int(balance, 16)) token['balance'] = balance if row['icon_url'] is not None: token['icon'] = row['icon_url'] elif row['format'] is not None: token['icon'] = "{}://{}/token/{}.{}".format(self.request.protocol, self.request.host, token['contract_address'], row['format']) else: token['icon'] = None return token balance, name, symbol, decimals = await self._get_token_details(contract_address) if balance is None: return None async with self.db: await self.db.execute("INSERT INTO tokens (contract_address, name, symbol, decimals, custom, ready) " "VALUES ($1, $2, $3, $4, $5, $6) " "ON CONFLICT (contract_address) DO NOTHING", contract_address, name, symbol, decimals, True, True) await self.db.commit() rval = { 'symbol': symbol, 'name': name, 'contract_address': contract_address, 'decimals': decimals } if self.user_toshi_id: rval['balance'] = hex(balance) return rval