async def get_collectibles(self, address, contract_address=None): if not validate_address(address): raise JsonRPCInvalidParamsError(data={'id': 'invalid_address', 'message': 'Invalid Address'}) if contract_address is not None and not validate_address(contract_address): raise JsonRPCInvalidParamsError(data={'id': 'invalid_contract_address', 'message': 'Invalid Contract Address'}) if contract_address is None: async with self.db: collectibles = await self.db.fetch(COLLECTIBLE_UNION_QUERY, address) return {"collectibles": [{ "contract_address": c['contract_address'], "value": hex(c['value']), "balance": hex(c['value']), "name": c['name'], "url": c["url"], "icon": c['icon'] } for c in collectibles]} else: async with self.db: collectible = await self.db.fetchrow( "SELECT * FROM collectibles WHERE contract_address = $1 AND ready = true", contract_address) if collectible is None: return None if collectible['type'] == 2: token_results = await self.db.fetch( "SELECT c.contract_address AS token_id, c.name, c.image, c.creator_address, c.description, c.token_uri, b.balance " "FROM fungible_collectible_balances b " "JOIN fungible_collectibles c ON b.contract_address = c.contract_address " "WHERE c.collectible_address = $1 AND b.balance != '0x0' AND owner_address = $2", contract_address, address) tokens = [] for t in token_results: token = dict(t) if token['description'] is None: token['description'] = "Created by: {}, balance: {}".format(token.pop('creator_address'), parse_int(token.pop('balance'))) tokens.append(token) else: tokens = await self.db.fetch( "SELECT token_id, name, image, description, token_uri FROM collectible_tokens " "WHERE contract_address = $1 AND owner_address = $2", contract_address, address) tokens = [dict(t) for t in tokens] if collectible is None: return None return { "contract_address": collectible["contract_address"], "name": collectible["name"], "icon": collectible["icon"], "url": collectible["url"], "value": hex(len(tokens)), "balance": hex(len(tokens)), "tokens": tokens }
async def get_collectibles(self, address, contract_address=None): if not validate_address(address): raise JsonRPCInvalidParamsError(data={ 'id': 'invalid_address', 'message': 'Invalid Address' }) if contract_address is not None and not validate_address( contract_address): raise JsonRPCInvalidParamsError( data={ 'id': 'invalid_contract_address', 'message': 'Invalid Contract Address' }) if contract_address is None: async with self.db: collectibles = await self.db.fetch( "SELECT t.contract_address, COUNT(t.token_id) AS value, c.name, c.icon, c.url " "FROM collectible_tokens t " "JOIN collectibles c ON c.contract_address = t.contract_address " "WHERE t.owner_address = $1 AND c.ready = true " "GROUP BY t.contract_address, c.name, c.icon, c.url", address) return { "collectibles": [{ "contract_address": c['contract_address'], "value": hex(c['value']), "name": c['name'], "url": c["url"], "icon": c['icon'] } for c in collectibles] } else: async with self.db: collectible = await self.db.fetchrow( "SELECT * FROM collectibles WHERE contract_address = $1 AND ready = true", contract_address) tokens = await self.db.fetch( "SELECT * FROM collectible_tokens " "WHERE contract_address = $1 AND owner_address = $2", contract_address, address) if collectible is None: return None return { "contract_address": collectible["contract_address"], "name": collectible["name"], "icon": collectible["icon"], "url": collectible["url"], "value": hex(len(tokens)), "tokens": [dict(t) for t in tokens] }
async def get_transaction_count(self, address): if not validate_address(address): raise JsonRPCInvalidParamsError(data={ 'id': 'invalid_address', 'message': 'Invalid Address' }) # get the network nonce nw_nonce = await self.eth.eth_getTransactionCount(address) # check the database for queued txs async with self.db: nonce = await self.db.fetchval( "SELECT nonce FROM transactions " "WHERE from_address = $1 " "AND (status is NULL OR status = 'queued' OR status = 'unconfirmed') " "ORDER BY nonce DESC", address) #nonce = nonce[0]['nonce'] if nonce else None if nonce is not None: # return the next usable nonce nonce = nonce + 1 if nonce < nw_nonce: return nw_nonce return nonce else: return nw_nonce
async def unsubscribe(self, *addresses): for address in addresses: if not validate_address(address): raise JsonRPCInvalidParamsError(data={'id': 'bad_arguments', 'message': 'Bad Arguments'}) await self.request_handler.unsubscribe(addresses) return True
async def get_balance(self, address): if not validate_address(address): raise JsonRPCInvalidParamsError(data={'id': 'invalid_address', 'message': 'Invalid Address'}) confirmed, unconfirmed, _, _ = await self.get_balances(address) return { "confirmed_balance": hex(confirmed), "unconfirmed_balance": hex(unconfirmed) }
async def post(self): toshi_id = self.verify_request() payload = self.json if 'addresses' not in payload or len(payload['addresses']) == 0: raise JSONHTTPError(400, body={ 'errors': [{ 'id': 'bad_arguments', 'message': 'Bad Arguments' }] }) addresses = payload['addresses'] for address in addresses: if not validate_address(address): raise JSONHTTPError(400, body={ 'errors': [{ 'id': 'bad_arguments', 'message': 'Bad Arguments' }] }) async with self.db: # see if this toshi_id is already registered, listening to it's own toshi_id rows = await self.db.fetch( "SELECT * FROM notification_registrations " "WHERE toshi_id = $1 AND eth_address = $1 AND service != 'ws'", toshi_id) if rows: if len(rows) > 1: log.warning( "LEGACY REGISTRATION FOR '{}' HAS MORE THAN ONE DEVICE OR SERVICE" .format(toshi_id)) registration_id = rows[0]['registration_id'] service = rows[0]['service'] else: service = 'LEGACY' registration_id = 'LEGACY' # simply store all the entered addresses with no service/registrations id for address in addresses: await self.db.execute( "INSERT INTO notification_registrations (toshi_id, service, registration_id, eth_address) " "VALUES ($1, $2, $3, $4) ON CONFLICT (toshi_id, service, registration_id, eth_address) DO NOTHING", toshi_id, service, registration_id, address) await self.db.commit() self.set_status(204)
async def filter(self, *, address=None, topic=None): if address is not None: if not validate_address(address): raise JsonRPCInvalidParamsError(data={'id': 'bad_arguments', 'message': 'Invalid Adddress'}) if topic is not None: try: topic_id, topic = encode_topic(topic) except ValueError: raise JsonRPCInvalidParamsError(data={'id': 'bad_arguments', 'message': 'Invalid Topic'}) filter_id = await self.request_handler.filter(address, topic_id, topic) return filter_id
async def post(self, service): toshi_id = self.verify_request() payload = self.json if 'registration_id' not in payload: raise JSONHTTPError(400, body={ 'errors': [{ 'id': 'bad_arguments', 'message': 'Bad Arguments' }] }) # eth address verification (if none is supplied, delete all the matching addresses) if 'address' in payload: eth_addresses = [payload['address']] elif 'addresses' in payload: eth_addresses = payload['addresses'] if not type(eth_addresses) == list: raise JSONHTTPError(400, data={ 'id': 'bad_arguments', 'message': '`addresses` must be a list' }) else: eth_addresses = [] if not all( validate_address(eth_address) for eth_address in eth_addresses): raise JSONHTTPError(400, data={ 'id': 'bad_arguments', 'message': 'Bad Arguments' }) async with self.db: if eth_addresses: await self.db.executemany( "DELETE FROM notification_registrations WHERE toshi_id = $1 AND service = $2 AND registration_id = $3 and eth_address = $4", [(toshi_id, service, payload['registration_id'], eth_address) for eth_address in eth_addresses]) else: await self.db.execute( "DELETE FROM notification_registrations WHERE toshi_id = $1 AND service = $2 AND registration_id = $3", toshi_id, service, payload['registration_id']) await self.db.commit() request_to_migrate(toshi_id) self.set_status(204) self.track(toshi_id, "Deregistered ETH notifications")
async def post(self, service): toshi_id = self.verify_request() payload = self.json if not all(arg in payload for arg in ['registration_id']): raise JSONHTTPError(400, body={ 'errors': [{ 'id': 'bad_arguments', 'message': 'Bad Arguments' }] }) # eth address verification (default to toshi_id if eth_address is not supplied) if 'address' in payload: eth_addresses = [payload['address']] elif 'addresses' in payload: eth_addresses = payload['addresses'] if not type(eth_addresses) == list: raise JSONHTTPError(400, data={ 'id': 'bad_arguments', 'message': '`addresses` must be a list' }) else: raise JSONHTTPError(400, data={ 'id': 'bad_arguments', 'message': 'Bad Arguments' }) if not all( validate_address(eth_address) for eth_address in eth_addresses): raise JSONHTTPError(400, data={ 'id': 'bad_arguments', 'message': 'Bad Arguments' }) async with self.db: await self.db.executemany( "INSERT INTO notification_registrations (toshi_id, service, registration_id, eth_address) " "VALUES ($1, $2, $3, $4) ON CONFLICT (toshi_id, service, registration_id, eth_address) DO NOTHING", [(toshi_id, service, payload['registration_id'], eth_address) for eth_address in eth_addresses]) await self.db.commit() request_to_migrate(toshi_id) self.set_status(204)
async def remove_token(self, *, contract_address): if not self.user_toshi_id: raise JsonRPCInvalidParamsError(data={'id': 'bad_arguments', 'message': "Missing authorisation"}) if not validate_address(contract_address): raise JsonRPCInvalidParamsError(data={'id': 'invalid_address', 'message': 'Invalid Contract Address'}) contract_address = contract_address.lower() async with self.db: await self.db.execute("UPDATE token_balances SET visibility = 0 " "WHERE eth_address = $1 AND contract_address = $2", self.user_toshi_id, contract_address) await self.db.commit()
def test_validate_address(self): self.assertTrue( utils.validate_address( "0x056db290f8ba3250ca64a45d16284d04bc6f5fbf")) self.assertTrue( utils.validate_address( u"0x056db290f8ba3250ca64a45d16284d04bc6f5fbf")) self.assertFalse(utils.validate_address("hello")) self.assertFalse(utils.validate_address("0x12345")) self.assertFalse(utils.validate_address(None)) self.assertFalse(utils.validate_address({})) self.assertFalse( utils.validate_address(0x056db290f8ba3250ca64a45d16284d04bc6f5fbf)) self.assertFalse( utils.validate_address( "0x114655db4898a6580f0abfc53fc0c0a88110724abf8d41f2abf206c69d7d4c821ed2cdf6939484ef6aebc39ce5662363b82140106bbc374a0f1381b6948214b001" ))
async def list_users(self, *, toshi_ids=None, payment_addresses=None): sql = "SELECT u.* FROM users u JOIN (VALUES " values = [] if toshi_ids is not None: column = 'toshi_id' addresses = toshi_ids elif payment_addresses is not None: column = 'payment_address' addresses = payment_addresses else: raise Exception( "list_users called without toshi_ids or payment_addresses") for i, address in enumerate(addresses): if not validate_address(address): raise JSONHTTPError(400, body={ 'errors': [{ 'id': 'bad_arguments', 'message': 'Bad Arguments' }] }) values.append("('{}', {}) ".format(address, i)) # in case this request is made with a lot of ids # make sure we yield to other processes rather # than taking up all resources on this if i > 0 and i % 100 == 0: await asyncio.sleep(0.001) sql += ", ".join(values) sql += ") AS v ({column}, ordering) ON u.{column} = v.{column} ".format( column=column) sql += "ORDER BY v.ordering" async with self.db: rows = await self.db.fetch(sql) self.write({ 'limit': len(rows), 'total': len(rows), 'offset': 0, 'query': '', 'results': [user_row_for_json(row) for row in rows] })
async def post(self): toshi_id = self.verify_request() payload = self.json if 'addresses' not in payload or len(payload['addresses']) == 0: raise JSONHTTPError(400, body={ 'errors': [{ 'id': 'bad_arguments', 'message': 'Bad Arguments' }] }) addresses = payload['addresses'] for address in addresses: if not validate_address(address): raise JSONHTTPError(400, body={ 'errors': [{ 'id': 'bad_arguments', 'message': 'Bad Arguments' }] }) async with self.db: await self.db.execute( "DELETE FROM notification_registrations WHERE service != 'ws' AND toshi_id = $1 AND ({})" .format(' OR '.join('eth_address = ${}'.format(i + 2) for i, _ in enumerate(addresses))), toshi_id, *addresses) await self.db.commit() self.set_status(204) self.track(toshi_id, "Deregistered ETH notifications")
async def post(self, service): toshi_id = self.verify_request() payload = self.json if 'registration_id' not in payload: raise JSONHTTPError(400, body={ 'errors': [{ 'id': 'bad_arguments', 'message': 'Bad Arguments' }] }) # TODO: registration id verification # eth address verification (if none is supplied, delete all the matching addresses) eth_address = payload.get('address', None) if eth_address and not validate_address(eth_address): raise JSONHTTPError(data={ 'id': 'bad_arguments', 'message': 'Bad Arguments' }) async with self.db: args = [toshi_id, service, payload['registration_id']] if eth_address: args.append(eth_address) await self.db.execute( "DELETE FROM notification_registrations WHERE toshi_id = $1 AND service = $2 AND registration_id = $3{}" .format("AND eth_address = $4" if eth_address else ""), *args) await self.db.commit() self.set_status(204) self.track(toshi_id, "Deregistered ETH notifications")
async def get_token_balances(self, eth_address, token_address=None, force_update=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 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 or force_update: erc20_dispatcher.update_token_cache("*", eth_address) async with self.db: await self.db.execute( "INSERT INTO token_registrations (eth_address) VALUES ($1) ON CONFLICT (eth_address) DO NOTHING", eth_address) await self.db.commit() if token_address: async with self.db: token = await self.db.fetchrow( "SELECT symbol, name, decimals, format, custom " "FROM tokens WHERE contract_address = $1", token_address) if token is None: return None if token['custom']: custom_token = await self.db.fetchrow( "SELECT name, symbol, decimals FROM token_balances " "WHERE contract_address = $1 AND eth_address = $2", token_address, eth_address) if custom_token: token = { 'name': custom_token['name'], 'symbol': custom_token['symbol'], 'decimals': custom_token['decimals'], 'format': token['format'] } balance = await self.db.fetchval( "SELECT balance " "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, # NOTE: 'value' left in for backwards compatibility "balance": 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 COALESCE(b.symbol, t.symbol) AS symbol, COALESCE(b.name, t.name) AS name, COALESCE(b.decimals, t.decimals) AS decimals, b.balance, b.contract_address, t.format " "FROM token_balances b " "JOIN tokens t " "ON t.contract_address = b.contract_address " "WHERE b.eth_address = $1 AND " "(b.visibility = 2 OR (b.visibility = 1 AND b.balance != '0x0')) " "ORDER BY t.symbol", eth_address) tokens = [] for b in balances: details = { "symbol": b['symbol'], "name": b['name'], "decimals": b['decimals'], "value": b['balance'], # NOTE: 'value' left in for backwards compatibility "balance": b['balance'], "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
def do_POST(self): # TODO: figure out why read is blocking here data = self.rfile.read(len(self.rfile.peek())) data = data.decode('utf-8') data = json.loads(data) if self.path == "/v1/tx/skel": gas_price = parse_int( data['gas_price']) if 'gas_price' in data else DEFAULT_GASPRICE gas = parse_int(data['gas']) if 'gas' in data else DEFAULT_STARTGAS nonce = parse_int(data['nonce']) if 'nonce' in data else 0 if 'value' not in data or 'from' not in data or 'to' not in data: self.write_data( 400, { 'errors': [{ 'id': 'bad_arguments', 'message': 'Bad Arguments' }] }) return value = parse_int(data['value']) to_address = data['to'] from_address = data['from'] if not validate_address(to_address): self.write_data( 400, { 'errors': [{ 'id': 'invalid_to_address', 'message': 'Invalid To Address' }] }) return if not validate_address(from_address): self.write_data( 400, { 'errors': [{ 'id': 'invalid_from_address', 'message': 'Invalid From Address' }] }) return tx = create_transaction(nonce=nonce, gasprice=gas_price, startgas=gas, to=to_address, value=value) transaction = encode_transaction(tx) self.write_data( 200, { "tx_data": { "nonce": hex(nonce), "from": from_address, "to": to_address, "value": hex(value), "startGas": hex(gas), "gasPrice": hex(gas_price) }, "tx": transaction }) elif self.path == "/v1/tx": tx = decode_transaction(data['tx']) if 'signature' in data: sig = data_decoder(data['signature']) add_signature_to_transaction(tx, sig) self.write_data(200, {"tx_hash": data_encoder(tx.hash)}) else: self.write_data(404)
async def post(self, service): toshi_id = self.verify_request() payload = self.json if not all(arg in payload for arg in ['registration_id']): raise JSONHTTPError(400, body={ 'errors': [{ 'id': 'bad_arguments', 'message': 'Bad Arguments' }] }) # TODO: registration id verification # XXX: BACKWARDS COMPAT FOR OLD PN REGISTARTION # remove when no longer needed if 'address' not in payload: async with self.db: legacy = await self.db.fetch( "SELECT eth_address FROM notification_registrations " "WHERE toshi_id = $1 AND service = 'LEGACY' AND registration_id = 'LEGACY'", toshi_id) else: legacy = False if legacy: async with self.db: for row in legacy: eth_address = row['eth_address'] await self.db.execute( "INSERT INTO notification_registrations (toshi_id, service, registration_id, eth_address) " "VALUES ($1, $2, $3, $4) ON CONFLICT (toshi_id, service, registration_id, eth_address) DO NOTHING", toshi_id, service, payload['registration_id'], eth_address) await self.db.execute( "DELETE FROM notification_registrations " "WHERE toshi_id = $1 AND service = 'LEGACY' AND registration_id = 'LEGACY'", toshi_id) await self.db.commit() else: # eth address verification (default to toshi_id if eth_address is not supplied) eth_address = payload[ 'address'] if 'address' in payload else toshi_id if not validate_address(eth_address): raise JSONHTTPError(data={ 'id': 'bad_arguments', 'message': 'Bad Arguments' }) async with self.db: await self.db.execute( "INSERT INTO notification_registrations (toshi_id, service, registration_id, eth_address) " "VALUES ($1, $2, $3, $4) ON CONFLICT (toshi_id, service, registration_id, eth_address) DO NOTHING", toshi_id, service, payload['registration_id'], eth_address) # XXX: temporary fix for old ios versions sending their payment address as toshi_id # should be removed after enough time has passed that most people should be using the fixed version if eth_address != toshi_id: # remove any apn registrations where toshi_id == eth_address for this eth_address await self.db.execute( "DELETE FROM notification_registrations " "WHERE toshi_id = $1 AND eth_address = $1 AND service = 'apn'", eth_address) await self.db.commit() self.set_status(204)
async def get(self, address): self.set_header("Access-Control-Allow-Origin", "*") self.set_header("Access-Control-Allow-Headers", "x-requested-with") self.set_header('Access-Control-Allow-Methods', 'GET') offset = parse_int(self.get_argument('offset', '0')) limit = parse_int(self.get_argument('limit', '10')) status = set([s.lower() for s in self.get_arguments('status')]) direction = set([d.lower() for d in self.get_arguments('direction')]) order = self.get_argument('order', 'desc').upper() if not validate_address(address) or \ offset is None or \ limit is None or \ (status and not status.issubset(['confirmed', 'unconfirmed', 'queued', 'error'])) or \ (direction and not direction.issubset(['in', 'out'])) or \ (order not in ['DESC', 'ASC']): raise JSONHTTPError(400, body={ 'id': 'bad_arguments', 'message': 'Bad Arguments' }) query = "SELECT * FROM transactions WHERE " args = [address, offset, limit] if len(direction) == 0 or len(direction) == 2: query += "(from_address = $1 OR to_address = $1) " elif 'in' in direction: query += "to_address = $1 " elif 'out' in direction: query += "from_address = $1 " if len(status) == 0: query += "AND (status != $4 OR status IS NULL) " args.append('error') else: status_query = [] for s in status: if s == 'queued': status_query.extend([ "status = ${}".format(len(args) + 1), "status IS NULL" ]) else: status_query.append("status = ${}".format(len(args) + 1)) args.append(s) query += "AND (" + " OR ".join(status_query) + ") " query += "ORDER BY created {} OFFSET $2 LIMIT $3".format(order) async with self.db: rows = await self.db.fetch(query, *args) transactions = [] for row in rows: transactions.append({ "hash": row['hash'], "to": row['to_address'], "from": row['from_address'], "nonce": hex(row['nonce']), "value": row['value'], "gas": row['gas'], "gas_price": row['gas_price'], "created_data": row['created'].isoformat(), "confirmed_data": row['updated'].isoformat() if row['blocknumber'] else None, "status": row['status'] if row['status'] is not None else 'queued', "data": row['data'] }) resp = { "transactions": transactions, "offset": offset, "limit": limit, "order": order } if len(direction) == 1: resp['direction'] = direction.pop() if status: resp['status'] = "&".join(status) self.write(resp)
async def create_transaction_skeleton(self, *, to_address, from_address, value=0, nonce=None, gas=None, gas_price=None, data=None): 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'}) if value: value = parse_int(value) if value is None or value < 0: raise JsonRPCInvalidParamsError(data={'id': 'invalid_value', 'message': 'Invalid Value'}) # check optional arguments # 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 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'' 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 raise JsonRPCInvalidParamsError(data={'id': 'invalid_data', 'message': 'Unable to estimate gas for contract call'}) else: gas = parse_int(gas) if gas is None: raise JsonRPCInvalidParamsError(data={'id': 'invalid_gas', 'message': 'Invalid Gas'}) if gas_price is None: # try and use cached gas station gas price gas_station_gas_price = 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 = self.application.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'}) 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)}) 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(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
async def create_transaction_skeleton(self, *, to_address, from_address, value=0, nonce=None, gas=None, gas_price=None, data=None, token_address=None): 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 value 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) 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 value 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) }) 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) }
def verify_request(self): """Verifies that the signature and the payload match the expected address raising a JSONHTTPError (400) if something is wrong with the request""" if TOSHI_ID_ADDRESS_HEADER in self.request.headers: expected_address = self.request.headers[ TOSHI_ID_ADDRESS_HEADER] elif self.get_argument(TOSHI_ID_ADDRESS_QUERY_ARG, None): expected_address = self.get_argument( TOSHI_ID_ADDRESS_QUERY_ARG) elif TOKEN_ID_ADDRESS_HEADER in self.request.headers: expected_address = self.request.headers[ TOKEN_ID_ADDRESS_HEADER] elif self.get_argument(TOKEN_ID_ADDRESS_QUERY_ARG, None): expected_address = self.get_argument( TOKEN_ID_ADDRESS_QUERY_ARG) else: raise JSONHTTPError(400, body={ 'errors': [{ 'id': 'bad_arguments', 'message': 'Missing Toshi-ID-Address' }] }) if TOSHI_SIGNATURE_HEADER in self.request.headers: signature = self.request.headers[TOSHI_SIGNATURE_HEADER] elif self.get_argument(TOSHI_SIGNATURE_QUERY_ARG, None): signature = self.get_argument(TOSHI_SIGNATURE_QUERY_ARG) elif TOKEN_SIGNATURE_HEADER in self.request.headers: signature = self.request.headers[TOKEN_SIGNATURE_HEADER] elif self.get_argument(TOKEN_SIGNATURE_QUERY_ARG, None): signature = self.get_argument(TOKEN_SIGNATURE_QUERY_ARG) else: raise JSONHTTPError(400, body={ 'errors': [{ 'id': 'bad_arguments', 'message': 'Missing Toshi-Signature' }] }) if TOSHI_TIMESTAMP_HEADER in self.request.headers: timestamp = self.request.headers[TOSHI_TIMESTAMP_HEADER] elif self.get_argument(TOSHI_TIMESTAMP_QUERY_ARG, None): timestamp = self.get_argument(TOSHI_TIMESTAMP_QUERY_ARG) elif TOKEN_TIMESTAMP_HEADER in self.request.headers: timestamp = self.request.headers[TOKEN_TIMESTAMP_HEADER] elif self.get_argument(TOKEN_TIMESTAMP_QUERY_ARG, None): timestamp = self.get_argument(TOKEN_TIMESTAMP_QUERY_ARG) else: raise JSONHTTPError(400, body={ 'errors': [{ 'id': 'bad_arguments', 'message': 'Missing Toshi-Timestamp' }] }) timestamp = parse_int(timestamp) if timestamp is None: raise JSONHTTPError(400, body={ 'errors': [{ 'id': 'invalid_timestamp', 'message': 'Given Toshi-Timestamp is invalid' }] }) if not validate_address(expected_address): raise JSONHTTPError(400, body={ 'errors': [{ 'id': 'invalid_id_address', 'message': 'Invalid Toshi-ID-Address' }] }) if not validate_signature(signature): raise JSONHTTPError(400, body={ 'errors': [{ 'id': 'invalid_signature', 'message': 'Invalid Toshi-Signature' }] }) try: signature = data_decoder(signature) except Exception: raise JSONHTTPError(400, body={ 'errors': [{ 'id': 'invalid_signature', 'message': 'Invalid Toshi-Signature' }] }) verb = self.request.method uri = self.request.path if self.request.body: datahash = self.request.body else: datahash = "" data_string = generate_request_signature_data_string( verb, uri, timestamp, datahash) if not ecrecover(data_string, signature, expected_address): raise JSONHTTPError(400, body={ 'errors': [{ 'id': 'invalid_signature', 'message': 'Invalid Toshi-Signature' }] }) if abs(int(time.time()) - timestamp) > TIMESTAMP_EXPIRY: raise JSONHTTPError( 400, body={ 'errors': [{ 'id': 'invalid_timestamp', 'message': 'The difference between the timestamp and the current time is too large' }] }) return expected_address
async def create_transaction_skeleton(self, *, to_address, from_address, value=0, nonce=None, gas=None, gas_price=None, data=None): 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 value: value = parse_int(value) if value is None or value < 0: raise JsonRPCInvalidParamsError(data={ 'id': 'invalid_value', 'message': 'Invalid Value' }) # check optional arguments 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'' 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 raise JsonRPCInvalidParamsError(data={ 'id': 'invalid_data', 'message': 'Invalid Data field' }) else: gas = parse_int(gas) if gas is None: raise JsonRPCInvalidParamsError(data={ 'id': 'invalid_gas', 'message': 'Invalid Gas' }) if gas_price is None: gas_price = self.application.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' }) 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) }) 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 transaction
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