def test_add_edit_token_with_wrong_swapped_for(globaldb): """Test that giving a non-existing swapped_for token in the DB raises InputError This can only be unit tested since via the API, marshmallow checks for Asset existence already """ # To unit test it we need to even hack it a bit. Make a new token, add it in the DB # then delete it and then try to add a new one referencing the old one. Since we # need to obtain a valid CustomEthereumToken object address_to_delete = make_ethereum_address() token_to_delete = CustomEthereumToken( address=address_to_delete, decimals=18, name='willdell', symbol='DELME', ) token_to_delete_id = 'DELMEID1' globaldb.add_asset( asset_id=token_to_delete_id, asset_type=AssetType.ETHEREUM_TOKEN, data=token_to_delete, ) asset_to_delete = Asset(token_to_delete_id) assert globaldb.delete_ethereum_token( address_to_delete) == token_to_delete_id # now try to add a new token with swapped_for pointing to a non existing token in the DB with pytest.raises(InputError): globaldb.add_asset( asset_id='NEWID', asset_type=AssetType.ETHEREUM_TOKEN, data=CustomEthereumToken( address=make_ethereum_address(), swapped_for=asset_to_delete, ), ) # now edit a new token with swapped_for pointing to a non existing token in the DB bat_custom = A_BAT.to_custom_ethereum_token() bat_custom = CustomEthereumToken( address=A_BAT.ethereum_address, decimals=A_BAT.decimals, name=A_BAT.name, symbol=A_BAT.symbol, started=A_BAT.started, swapped_for=asset_to_delete, coingecko=A_BAT.coingecko, cryptocompare=A_BAT.cryptocompare, protocol=None, underlying_tokens=None, ) with pytest.raises(InputError): globaldb.edit_ethereum_token(bat_custom)
def add_ethereum_token(entry: CustomEthereumToken) -> None: """Adds a new ethereum token into the global DB May raise InputError if the token already exists Returns the token's rotki identifier """ connection = GlobalDBHandler()._conn cursor = connection.cursor() try: cursor.execute( 'INSERT INTO ' 'ethereum_tokens(address, decimals, name, symbol, started, ' 'swapped_for, coingecko, cryptocompare, protocol) ' 'VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?)', entry.to_db_tuple(), ) except sqlite3.IntegrityError as e: exception_msg = str(e) if 'FOREIGN KEY' in exception_msg: # should not really happen since API should check for this msg = ( f'Ethereum token with address {entry.address} can not be put ' f'in the DB due to swapped for entry not existing') else: msg = f'Ethereum token with address {entry.address} already exists in the DB' raise InputError(msg) from e if entry.underlying_tokens is not None: GlobalDBHandler()._add_underlying_tokens( connection=connection, parent_token_address=entry.address, underlying_tokens=entry.underlying_tokens, )
def test_price_underlying_tokens(inquirer, globaldb): aave_weight, link_weight, crv_weight = FVal('0.6'), FVal('0.2'), FVal( '0.2') address = make_ethereum_address() token = CustomEthereumToken( address=address, decimals=18, name='Test', symbol='YAB', underlying_tokens=[ UnderlyingToken(address=A_AAVE.ethereum_address, weight=aave_weight), UnderlyingToken(address=A_LINK.ethereum_address, weight=link_weight), UnderlyingToken(address=A_CRV.ethereum_address, weight=crv_weight), ], ) globaldb.add_asset( asset_id=ethaddress_to_identifier(address), asset_type=AssetType.ETHEREUM_TOKEN, data=token, ) price = inquirer.find_price(EthereumToken(address), A_USD) assert price == FVal(67)
def deserialize_ethereum_token(token_data: dict) -> CustomEthereumToken: token = CustomEthereumToken( address=deserialize_ethereum_address(token_data.get('address')), name=token_data.get('name'), symbol=token_data.get('symbol'), decimals=token_data.get('decimals'), coingecko=token_data.get('coingecko') ) return token
def edit_ethereum_token(entry: CustomEthereumToken, ) -> str: """Adds a new ethereum token into the global DB May raise InputError if the token already exists or other error Returns the token's rotki identifier """ connection = GlobalDBHandler()._conn cursor = connection.cursor() db_tuple = entry.to_db_tuple() swapped_tuple = (*db_tuple[1:], db_tuple[0]) try: cursor.execute( 'UPDATE ethereum_tokens SET decimals=?, name=?, symbol=?, started=?, swapped_for=?, ' # noqa: E501 'coingecko=?, cryptocompare=?, protocol=? WHERE address = ?', swapped_tuple, ) except sqlite3.IntegrityError as e: raise InputError( f'Failed to update DB entry for ethereum token with address {entry.address} ' f'due to a consraint being hit. Make sure the new values are valid ', ) from e if cursor.rowcount != 1: raise InputError( f'Tried to edit non existing ethereum token with address {entry.address}', ) # Since this is editing, make sure no underlying tokens exist cursor.execute( 'DELETE from underlying_tokens_list WHERE parent_token_entry=?', (entry.address, ), ) if entry.underlying_tokens is not None: # and now add any if needed GlobalDBHandler()._add_underlying_tokens( connection=connection, parent_token_address=entry.address, underlying_tokens=entry.underlying_tokens, ) rotki_id = GlobalDBHandler().get_ethereum_token_identifier( entry.address) if rotki_id is None: connection.rollback() raise InputError( f'Unexpected DB state. Ethereum token {entry.address} exists in the DB ' f'but its corresponding asset entry was not found.', ) connection.commit() return rotki_id
def test_find_uniswap_v2_lp_token_price(inquirer, globaldb, ethereum_manager): addess = '0xa2107FA5B38d9bbd2C461D6EDf11B11A50F6b974' inquirer.inject_ethereum(ethereum_manager) token = CustomEthereumToken( address=addess, decimals=18, name='Uniswap LINK/ETH', symbol='UNI-V2', protocol='UNI-V2', ) globaldb.add_asset( asset_id=ethaddress_to_identifier(addess), asset_type=AssetType.ETHEREUM_TOKEN, data=token, ) price = inquirer.find_uniswap_v2_lp_price(EthereumToken(addess)) assert price is not None
def test_update_conflicts(rotkehlchen_api_server, globaldb): """Test that conflicts in an asset update are handled properly""" async_query = random.choice([False, True]) rotki = rotkehlchen_api_server.rest_api.rotkehlchen update_1 = """INSERT INTO assets(identifier,type,name,symbol,started, swapped_for, coingecko, cryptocompare, details_reference) VALUES("121-ada-FADS-as", "F","A name","SYMBOL",NULL, NULL,"", "", "121-ada-FADS-as");INSERT INTO common_asset_details(asset_id, forked) VALUES("121-ada-FADS-as", "BTC"); * INSERT INTO ethereum_tokens(address, decimals, protocol) VALUES("0x6B175474E89094C44Da98b954EedeAC495271d0F", 8, "maker");INSERT INTO assets(identifier,type, name, symbol,started, swapped_for, coingecko, cryptocompare, details_reference) VALUES("_ceth_0x6B175474E89094C44Da98b954EedeAC495271d0F", "C", "New Multi Collateral DAI", "NDAI", 1573672677, NULL, "dai", NULL, "0x6B175474E89094C44Da98b954EedeAC495271d0F"); * INSERT INTO assets(identifier,type,name,symbol,started, swapped_for, coingecko, cryptocompare, details_reference) VALUES("DASH", "B","Dash","DASH",1337, NULL, "dash-coingecko", NULL, "DASH");INSERT INTO common_asset_details(asset_id, forked) VALUES("DASH", "BTC"); * INSERT INTO ethereum_tokens(address, decimals, protocol) VALUES("0x1B175474E89094C44Da98b954EedeAC495271d0F", 18, NULL); INSERT INTO assets(identifier,type, name, symbol,started, swapped_for, coingecko, cryptocompare, details_reference) VALUES("_ceth_0x1B175474E89094C44Da98b954EedeAC495271d0F", "C", "Conflicting token", "CTK", 1573672677, NULL, "ctk", NULL, "0x1B175474E89094C44Da98b954EedeAC495271d0F"); * """ # noqa: E501 globaldb.add_asset( # add a conflicting token asset_id='_ceth_0x1B175474E89094C44Da98b954EedeAC495271d0F', asset_type=AssetType.ETHEREUM_TOKEN, data=CustomEthereumToken( address=ChecksumEthAddress( '0x1B175474E89094C44Da98b954EedeAC495271d0F'), decimals=12, name='Conflicting token', symbol='CTK', started=None, swapped_for=None, coingecko='ctk', cryptocompare=None, protocol=None, underlying_tokens=None, ), ) globaldb.add_user_owned_assets( [Asset('_ceth_0x1B175474E89094C44Da98b954EedeAC495271d0F')]) update_patch = mock_asset_updates( original_requests_get=requests.get, latest=999999991, updates={ "999999991": { "changes": 3, "min_schema_version": GLOBAL_DB_VERSION, "max_schema_version": GLOBAL_DB_VERSION, } }, sql_actions={"999999991": update_1}, ) globaldb.add_setting_value(ASSETS_VERSION_KEY, 999999990) start_assets_num = len(globaldb.get_all_asset_data(mapping=False)) with update_patch: response = requests.get( api_url_for( rotkehlchen_api_server, 'assetupdatesresource', ), json={'async_query': async_query}, ) if async_query: task_id = assert_ok_async_response(response) outcome = wait_for_async_task( rotkehlchen_api_server, task_id, ) result = outcome['result'] assert outcome['message'] == '' else: result = assert_proper_response_with_result(response) assert result['local'] == 999999990 assert result['remote'] == 999999991 assert result['new_changes'] == 3 response = requests.post( api_url_for( rotkehlchen_api_server, 'assetupdatesresource', ), json={'async_query': async_query}, ) if async_query: task_id = assert_ok_async_response(response) outcome = wait_for_async_task( rotkehlchen_api_server, task_id, ) assert outcome[ 'message'] == 'Found conflicts during assets upgrade' result = outcome['result'] else: result = assert_proper_response_with_result( response, message='Found conflicts during assets upgrade', status_code=HTTPStatus.CONFLICT, ) # Make sure that nothing was committed assert globaldb.get_setting_value(ASSETS_VERSION_KEY, None) == 999999990 assert len( globaldb.get_all_asset_data(mapping=False)) == start_assets_num with pytest.raises(UnknownAsset): Asset('121-ada-FADS-as') errors = rotki.msg_aggregator.consume_errors() warnings = rotki.msg_aggregator.consume_warnings() assert len(errors) == 0, f'Found errors: {errors}' assert len(warnings) == 0, f'Found warnings: {warnings}' # See that we get 3 conflicts expected_result = [{ 'identifier': '_ceth_0x6B175474E89094C44Da98b954EedeAC495271d0F', 'local': { 'name': 'Multi Collateral Dai', 'symbol': 'DAI', 'asset_type': 'ethereum token', 'started': 1573672677, 'forked': None, 'swapped_for': None, 'ethereum_address': '0x6B175474E89094C44Da98b954EedeAC495271d0F', 'decimals': 18, 'cryptocompare': None, 'coingecko': 'dai', 'protocol': None, }, 'remote': { 'name': 'New Multi Collateral DAI', 'symbol': 'NDAI', 'asset_type': 'ethereum token', 'started': 1573672677, 'forked': None, 'swapped_for': None, 'ethereum_address': '0x6B175474E89094C44Da98b954EedeAC495271d0F', 'decimals': 8, 'cryptocompare': None, 'coingecko': 'dai', 'protocol': 'maker', }, }, { 'identifier': 'DASH', 'local': { 'name': 'Dash', 'symbol': 'DASH', 'asset_type': 'own chain', 'started': 1390095618, 'forked': None, 'swapped_for': None, 'ethereum_address': None, 'decimals': None, 'cryptocompare': None, 'coingecko': 'dash', 'protocol': None, }, 'remote': { 'name': 'Dash', 'symbol': 'DASH', 'asset_type': 'own chain', 'started': 1337, 'forked': 'BTC', 'swapped_for': None, 'ethereum_address': None, 'decimals': None, 'cryptocompare': None, 'coingecko': 'dash-coingecko', 'protocol': None, }, }, { 'identifier': '_ceth_0x1B175474E89094C44Da98b954EedeAC495271d0F', 'local': { 'asset_type': 'ethereum token', 'coingecko': 'ctk', 'cryptocompare': None, 'decimals': 12, 'ethereum_address': '0x1B175474E89094C44Da98b954EedeAC495271d0F', 'forked': None, 'name': 'Conflicting token', 'protocol': None, 'started': None, 'swapped_for': None, 'symbol': 'CTK', }, 'remote': { 'asset_type': 'ethereum token', 'coingecko': 'ctk', 'cryptocompare': None, 'decimals': 18, 'ethereum_address': '0x1b175474E89094C44DA98B954EeDEAC495271d0f', 'forked': None, 'name': 'Conflicting token', 'protocol': None, 'started': 1573672677, 'swapped_for': None, 'symbol': 'CTK', }, }] assert result == expected_result # now try the update again but specify the conflicts resolution conflicts = { '_ceth_0x6B175474E89094C44Da98b954EedeAC495271d0F': 'remote', 'DASH': 'local', '_ceth_0x1B175474E89094C44Da98b954EedeAC495271d0F': 'remote' } # noqa: E501 response = requests.post( api_url_for( rotkehlchen_api_server, 'assetupdatesresource', ), json={ 'async_query': async_query, 'conflicts': conflicts }, ) if async_query: task_id = assert_ok_async_response(response) outcome = wait_for_async_task( rotkehlchen_api_server, task_id, ) assert outcome['message'] == '' result = outcome['result'] else: result = assert_proper_response_with_result( response, message='', status_code=HTTPStatus.OK, ) cursor = globaldb._conn.cursor() # check conflicts were solved as per the given choices and new asset also added assert result is True assert globaldb.get_setting_value(ASSETS_VERSION_KEY, None) == 999999991 errors = rotki.msg_aggregator.consume_errors() warnings = rotki.msg_aggregator.consume_warnings() assert len(errors) == 0, f'Found errors: {errors}' assert len(warnings) == 0, f'Found warnings: {warnings}' dai = EthereumToken('0x6B175474E89094C44Da98b954EedeAC495271d0F') assert dai.identifier == strethaddress_to_identifier( '0x6B175474E89094C44Da98b954EedeAC495271d0F') # noqa: E501 assert dai.name == 'New Multi Collateral DAI' assert dai.symbol == 'NDAI' assert dai.asset_type == AssetType.ETHEREUM_TOKEN assert dai.started == 1573672677 assert dai.forked is None assert dai.swapped_for is None assert dai.coingecko == 'dai' assert dai.cryptocompare is None assert dai.ethereum_address == '0x6B175474E89094C44Da98b954EedeAC495271d0F' assert dai.decimals == 8 assert dai.protocol == 'maker' # make sure data is in both tables assert cursor.execute( 'SELECT COUNT(*) from ethereum_tokens WHERE address="0x6B175474E89094C44Da98b954EedeAC495271d0F";' ).fetchone()[0] == 1 # noqa: E501 assert cursor.execute( 'SELECT COUNT(*) from assets WHERE identifier="_ceth_0x6B175474E89094C44Da98b954EedeAC495271d0F";' ).fetchone()[0] == 1 # noqa: E501 dash = Asset('DASH') assert dash.identifier == 'DASH' assert dash.name == 'Dash' assert dash.symbol == 'DASH' assert dash.asset_type == AssetType.OWN_CHAIN assert dash.started == 1390095618 assert dash.forked is None assert dash.swapped_for is None assert dash.coingecko == 'dash' assert dash.cryptocompare is None assert cursor.execute( 'SELECT COUNT(*) from common_asset_details WHERE asset_id="DASH";' ).fetchone()[0] == 1 # noqa: E501 assert cursor.execute( 'SELECT COUNT(*) from assets WHERE identifier="DASH";').fetchone( )[0] == 1 # noqa: E501 new_asset = Asset('121-ada-FADS-as') assert new_asset.identifier == '121-ada-FADS-as' assert new_asset.name == 'A name' assert new_asset.symbol == 'SYMBOL' assert new_asset.asset_type == AssetType.COUNTERPARTY_TOKEN assert new_asset.started is None assert new_asset.forked == 'BTC' assert new_asset.swapped_for is None assert new_asset.coingecko == '' assert new_asset.cryptocompare == '' assert cursor.execute( 'SELECT COUNT(*) from common_asset_details WHERE asset_id="121-ada-FADS-as";' ).fetchone()[0] == 1 # noqa: E501 assert cursor.execute( 'SELECT COUNT(*) from assets WHERE identifier="121-ada-FADS-as";' ).fetchone()[0] == 1 # noqa: E501 ctk = EthereumToken('0x1B175474E89094C44Da98b954EedeAC495271d0F') assert ctk.name == 'Conflicting token' assert ctk.symbol == 'CTK' assert ctk.asset_type == AssetType.ETHEREUM_TOKEN assert ctk.started == 1573672677 assert ctk.forked is None assert ctk.swapped_for is None assert ctk.coingecko == 'ctk' assert ctk.cryptocompare is None assert ctk.ethereum_address == '0x1B175474E89094C44Da98b954EedeAC495271d0F' assert ctk.decimals == 18 assert ctk.protocol is None assert cursor.execute( 'SELECT COUNT(*) from ethereum_tokens WHERE address="0x1B175474E89094C44Da98b954EedeAC495271d0F";' ).fetchone()[0] == 1 # noqa: E501 assert cursor.execute( 'SELECT COUNT(*) from assets WHERE identifier="_ceth_0x1B175474E89094C44Da98b954EedeAC495271d0F";' ).fetchone()[0] == 1 # noqa: E501
def test_adding_custom_tokens(rotkehlchen_api_server): """Test that the endpoint for adding a custom ethereum token works""" response = requests.put( api_url_for( rotkehlchen_api_server, 'ethereumassetsresource', ), json={'token': CUSTOM_TOKEN3.serialize()}, ) result = assert_proper_response_with_result(response) assert result == {'identifier': ETHEREUM_DIRECTIVE + CUSTOM_TOKEN3.address} response = requests.get( api_url_for( rotkehlchen_api_server, 'ethereumassetsresource', ), ) result = assert_proper_response_with_result(response) expected_tokens = INITIAL_EXPECTED_TOKENS.copy() + [ CUSTOM_TOKEN3, CustomEthereumToken(address=underlying_address4), ] expected_result = [x.serialize() for x in expected_tokens] assert_token_entry_exists_in_result(result, expected_result) # test that adding an already existing address is handled properly response = requests.put( api_url_for( rotkehlchen_api_server, 'ethereumassetsresource', ), json={'token': INITIAL_TOKENS[1].serialize()}, ) expected_msg = ( f'Ethereum token with address {INITIAL_TOKENS[1].address} already ' f'exists in the DB', ) assert_error_response( response=response, contained_in_msg=expected_msg, status_code=HTTPStatus.CONFLICT, ) # also test that the addition of underlying tokens has created proper asset entires for them cursor = GlobalDBHandler()._conn.cursor() result = cursor.execute( 'SELECT COUNT(*) from assets WHERE identifier IN (?, ?, ?, ?)', [ ETHEREUM_DIRECTIVE + x for x in [ underlying_address1, underlying_address2, underlying_address3, underlying_address4 ] ], # noqa: E501 ).fetchone()[0] assert result == 4 result = cursor.execute( 'SELECT COUNT(*) from ethereum_tokens WHERE address IN (?, ?, ?, ?)', (underlying_address1, underlying_address2, underlying_address3, underlying_address4), # noqa: E501 ).fetchone()[0] assert result == 4 # now test that adding a token with underlying tokens adding up to more than 100% is caught bad_token = CustomEthereumToken( address=make_ethereum_address(), decimals=18, name='foo', symbol='BBB', underlying_tokens=[ UnderlyingToken(address=make_ethereum_address(), weight=FVal('0.5055')), UnderlyingToken(address=make_ethereum_address(), weight=FVal('0.7055')), ], ) response = requests.put( api_url_for( rotkehlchen_api_server, 'ethereumassetsresource', ), json={'token': bad_token.serialize()}, ) expected_msg = ( f'The sum of underlying token weights for {bad_token.address} is ' f'121.1000 and exceeds 100%') assert_error_response( response=response, contained_in_msg=expected_msg, status_code=HTTPStatus.BAD_REQUEST, ) # and test that adding a token with underlying tokens adding up to less than 100% is caught bad_token = CustomEthereumToken( address=make_ethereum_address(), decimals=18, name='foo', symbol='BBB', underlying_tokens=[ UnderlyingToken(address=make_ethereum_address(), weight=FVal('0.1055')), UnderlyingToken(address=make_ethereum_address(), weight=FVal('0.2055')), ], ) response = requests.put( api_url_for( rotkehlchen_api_server, 'ethereumassetsresource', ), json={'token': bad_token.serialize()}, ) expected_msg = ( f'The sum of underlying token weights for {bad_token.address} is ' f'31.1000 and does not add up to 100%') assert_error_response( response=response, contained_in_msg=expected_msg, status_code=HTTPStatus.BAD_REQUEST, ) # and test that adding a token with empty list of underlying tokens and not null is an error bad_token = CustomEthereumToken( address=make_ethereum_address(), decimals=18, name='foo', symbol='BBB', underlying_tokens=[], ) serialized_bad_token = bad_token.serialize() serialized_bad_token['underlying_tokens'] = [] response = requests.put( api_url_for( rotkehlchen_api_server, 'ethereumassetsresource', ), json={'token': serialized_bad_token}, ) expected_msg = ( f'Gave an empty list for underlying tokens of {bad_token.address}') assert_error_response( response=response, contained_in_msg=expected_msg, status_code=HTTPStatus.BAD_REQUEST, )
underlying_address1 = make_ethereum_address() underlying_address2 = make_ethereum_address() underlying_address3 = make_ethereum_address() custom_address1 = make_ethereum_address() custom_address2 = make_ethereum_address() INITIAL_TOKENS = [ CustomEthereumToken( address=custom_address1, decimals=4, name='Custom 1', symbol='CST1', started=Timestamp(0), swapped_for=A_MKR, coingecko='foo', cryptocompare='boo', protocol='uniswap', underlying_tokens=[ UnderlyingToken(address=underlying_address1, weight=FVal('0.5055')), UnderlyingToken(address=underlying_address2, weight=FVal('0.1545')), UnderlyingToken(address=underlying_address3, weight=FVal('0.34')), ], ), CustomEthereumToken( address=custom_address2, decimals=18, name='Custom 2', symbol='CST2', ), ]