We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Importing IOTA trades from bitcoin.de throw errors
## Problem Definition

On importing exchange data from bitcoin.de an error is thrown:
"Query trades of bitcoinde exchange died with exception: Unknown asset IOT provided.. Check the logs for more details"
This is probably the same issue as in https://github.com/rotki/rotki/issues/299 -> IOT needs to be mapped to IOTA

## Logs

The log is empty.


### System Description


Operating system: 
Rotki version: 1.12.2


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `rotkehlchen/exchanges/bitcoinde.py`
Content:
```
1 import hashlib
2 import hmac
3 import logging
4 import time
5 from json.decoder import JSONDecodeError
6 from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple
7 from urllib.parse import urlencode
8 
9 import requests
10 from typing_extensions import Literal
11 
12 from rotkehlchen.assets.asset import Asset
13 from rotkehlchen.errors import RemoteError
14 from rotkehlchen.exchanges.data_structures import (
15     AssetMovement,
16     Location,
17     MarginPosition,
18     Price,
19     Trade,
20     TradePair,
21 )
22 from rotkehlchen.exchanges.exchange import ExchangeInterface
23 from rotkehlchen.inquirer import Inquirer
24 from rotkehlchen.logging import RotkehlchenLogsAdapter
25 from rotkehlchen.serialization.deserialize import (
26     deserialize_fee,
27     deserialize_timestamp_from_date,
28     deserialize_trade_type,
29 )
30 from rotkehlchen.typing import ApiKey, ApiSecret, AssetAmount, FVal, Timestamp
31 from rotkehlchen.user_messages import MessagesAggregator
32 from rotkehlchen.utils.misc import iso8601ts_to_timestamp
33 from rotkehlchen.utils.serialization import rlk_jsonloads
34 
35 if TYPE_CHECKING:
36     from rotkehlchen.db.dbhandler import DBHandler
37 
38 logger = logging.getLogger(__name__)
39 log = RotkehlchenLogsAdapter(logger)
40 
41 # This corresponds to md5('') and is used in signature generation
42 MD5_EMPTY_STR = 'd41d8cd98f00b204e9800998ecf8427e'
43 
44 # Pairs can be found in Basic API doc: https://www.bitcoin.de/de/api/marketplace
45 BITCOINDE_TRADING_PAIRS = (
46     'btceur',
47     'bcheur',
48     'btgeur',
49     'etheur',
50     'bsveur',
51     'ltceur',
52 )
53 
54 
55 def bitcoinde_asset(asset: str) -> Asset:
56     return Asset(asset.upper())
57 
58 
59 def bitcoinde_pair_to_world(pair: str) -> Tuple[Asset, Asset]:
60     tx_asset = bitcoinde_asset(pair[:3])
61     native_asset = bitcoinde_asset(pair[3:])
62     return tx_asset, native_asset
63 
64 
65 def trade_from_bitcoinde(raw_trade: Dict) -> Trade:
66 
67     try:
68         timestamp = deserialize_timestamp_from_date(
69             raw_trade['successfully_finished_at'],
70             'iso8601',
71             'bitcoinde',
72         )
73     except KeyError:
74         # For very old trades (2013) bitcoin.de does not return 'successfully_finished_at'
75         timestamp = deserialize_timestamp_from_date(
76             raw_trade['trade_marked_as_paid_at'],
77             'iso8601',
78             'bitcoinde',
79         )
80 
81     trade_type = deserialize_trade_type(raw_trade['type'])
82     tx_amount = AssetAmount(FVal(raw_trade['amount_currency_to_trade']))
83     native_amount = FVal(raw_trade['volume_currency_to_pay'])
84     tx_asset, native_asset = bitcoinde_pair_to_world(raw_trade['trading_pair'])
85     pair = TradePair(f'{tx_asset.identifier}_{native_asset.identifier}')
86     amount = tx_amount
87     rate = Price(native_amount / tx_amount)
88     fee_amount = deserialize_fee(raw_trade['fee_currency_to_pay'])
89     fee_asset = Asset('EUR')
90 
91     return Trade(
92         timestamp=timestamp,
93         location=Location.BITCOINDE,
94         pair=pair,
95         trade_type=trade_type,
96         amount=amount,
97         rate=rate,
98         fee=fee_amount,
99         fee_currency=fee_asset,
100         link=str(raw_trade['trade_id']),
101     )
102 
103 
104 class Bitcoinde(ExchangeInterface):  # lgtm[py/missing-call-to-init]
105     def __init__(
106             self,
107             api_key: ApiKey,
108             secret: ApiSecret,
109             database: 'DBHandler',
110             msg_aggregator: MessagesAggregator,
111     ):
112         super().__init__('bitcoinde', api_key, secret, database)
113         self.uri = 'https://api.bitcoin.de'
114         self.session.headers.update({'x-api-key': api_key})
115         self.msg_aggregator = msg_aggregator
116 
117     def _generate_signature(self, request_type: str, url: str, nonce: str) -> str:
118         signed_data = '#'.join([request_type, url, self.api_key, nonce, MD5_EMPTY_STR]).encode()
119         signature = hmac.new(
120             self.secret,
121             signed_data,
122             hashlib.sha256,
123         ).hexdigest()
124         self.session.headers.update({
125             'x-api-signature': signature,
126         })
127         return signature
128 
129     def _api_query(
130             self,
131             verb: Literal['get', 'post'],
132             path: str,
133             options: Optional[Dict] = None,
134     ) -> Dict:
135         """
136         Queries Bitcoin.de with the given verb for the given path and options
137         """
138         assert verb in ('get', 'post'), (
139             'Given verb {} is not a valid HTTP verb'.format(verb)
140         )
141 
142         request_path_no_args = '/v4/' + path
143 
144         data = ''
145         if not options:
146             request_path = request_path_no_args
147         else:
148             request_path = request_path_no_args + '?' + urlencode(options)
149 
150         nonce = str(int(time.time() * 1000))
151         request_url = self.uri + request_path
152 
153         self._generate_signature(
154             request_type=verb.upper(),
155             url=request_url,
156             nonce=nonce,
157         )
158 
159         headers = {
160             'x-api-nonce': nonce,
161         }
162         if data != '':
163             headers.update({
164                 'Content-Type': 'application/json',
165                 'Content-Length': str(len(data)),
166             })
167 
168         log.debug('Bitcoin.de API Query', verb=verb, request_url=request_url)
169 
170         try:
171             response = getattr(self.session, verb)(request_url, data=data, headers=headers)
172         except requests.exceptions.RequestException as e:
173             raise RemoteError(f'Bitcoin.de API request failed due to {str(e)}') from e
174 
175         try:
176             json_ret = rlk_jsonloads(response.text)
177         except JSONDecodeError as exc:
178             raise RemoteError('Bitcoin.de returned invalid JSON response') from exc
179 
180         if response.status_code not in (200, 401):
181             if isinstance(json_ret, dict) and 'errors' in json_ret:
182                 for error in json_ret['errors']:
183                     if error.get('field') == 'X-API-KEY' and error.get('code') == 1:
184                         raise RemoteError('Provided API Key is in invalid Format')
185 
186                     if error.get('code') == 3:
187                         raise RemoteError('Provided API Key is invalid')
188 
189                 raise RemoteError(json_ret['errors'])
190 
191             raise RemoteError(
192                 'Bitcoin.de api request for {} failed with HTTP status code {}'.format(
193                     response.url,
194                     response.status_code,
195                 ),
196             )
197 
198         if not isinstance(json_ret, dict):
199             raise RemoteError('Bitcoin.de returned invalid non-dict response')
200 
201         return json_ret
202 
203     def validate_api_key(self) -> Tuple[bool, str]:
204         """
205         Validates that the Bitcoin.de API key is good for usage in Rotki
206         """
207 
208         try:
209             self._api_query('get', 'account')
210             return True, ""
211 
212         except RemoteError as e:
213             return False, str(e)
214 
215     def query_balances(self, **kwargs: Any) -> Tuple[Optional[Dict[Asset, Dict[str, Any]]], str]:
216         balances = {}
217         try:
218             resp_info = self._api_query('get', 'account')
219         except RemoteError as e:
220             msg = (
221                 'Bitcoin.de request failed. Could not reach bitcoin.de due '
222                 'to {}'.format(e)
223             )
224             log.error(msg)
225             return None, msg
226 
227         for currency, balance in resp_info['data']['balances'].items():
228             asset = bitcoinde_asset(currency)
229             try:
230                 usd_price = Inquirer().find_usd_price(asset=asset)
231             except RemoteError as e:
232                 self.msg_aggregator.add_error(
233                     f'Error processing Bitcoin.de balance entry due to inability to '
234                     f'query USD price: {str(e)}. Skipping balance entry',
235                 )
236                 continue
237 
238             balances[asset] = {
239                 'amount': balance['total_amount'],
240                 'usd_value': balance['total_amount'] * usd_price,
241             }
242 
243         return balances, ''
244 
245     def query_online_trade_history(
246             self,
247             start_ts: Timestamp,
248             end_ts: Timestamp,
249     ) -> List[Trade]:
250 
251         page = 1
252         resp_trades = []
253 
254         while True:
255             resp = self._api_query('get', 'trades', {'state': 1, 'page': page})
256             resp_trades.extend(resp['trades'])
257 
258             if 'page' not in resp:
259                 break
260 
261             if resp['page']['current'] >= resp['page']['last']:
262                 break
263 
264             page = resp['page']['current'] + 1
265 
266         log.debug('Bitcoin.de trade history query', results_num=len(resp_trades))
267 
268         trades = []
269         for tx in resp_trades:
270             try:
271                 timestamp = iso8601ts_to_timestamp(tx['successfully_finished_at'])
272             except KeyError:
273                 # For very old trades (2013) bitcoin.de does not return 'successfully_finished_at'
274                 timestamp = iso8601ts_to_timestamp(tx['trade_marked_as_paid_at'])
275 
276             if tx['state'] != 1:
277                 continue
278             if timestamp < start_ts or timestamp > end_ts:
279                 continue
280             trades.append(trade_from_bitcoinde(tx))
281 
282         return trades
283 
284     def query_online_deposits_withdrawals(
285             self,  # pylint: disable=no-self-use
286             start_ts: Timestamp,  # pylint: disable=unused-argument
287             end_ts: Timestamp,  # pylint: disable=unused-argument
288     ) -> List[AssetMovement]:
289         return []  # noop for bitcoinde
290 
291     def query_online_margin_history(
292             self,  # pylint: disable=no-self-use
293             start_ts: Timestamp,  # pylint: disable=unused-argument
294             end_ts: Timestamp,  # pylint: disable=unused-argument
295     ) -> List[MarginPosition]:
296         return []  # noop for bitcoinde
297
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/rotkehlchen/exchanges/bitcoinde.py b/rotkehlchen/exchanges/bitcoinde.py
--- a/rotkehlchen/exchanges/bitcoinde.py
+++ b/rotkehlchen/exchanges/bitcoinde.py
@@ -10,7 +10,7 @@
 from typing_extensions import Literal
 
 from rotkehlchen.assets.asset import Asset
-from rotkehlchen.errors import RemoteError
+from rotkehlchen.errors import DeserializationError, RemoteError, UnknownAsset
 from rotkehlchen.exchanges.data_structures import (
     AssetMovement,
     Location,
@@ -41,7 +41,8 @@
 # This corresponds to md5('') and is used in signature generation
 MD5_EMPTY_STR = 'd41d8cd98f00b204e9800998ecf8427e'
 
-# Pairs can be found in Basic API doc: https://www.bitcoin.de/de/api/marketplace
+# Pairs can be found in Basic API doc:
+# https://www.bitcoin.de/en/api/tapi/v4/docu#handelspaarliste_c2f
 BITCOINDE_TRADING_PAIRS = (
     'btceur',
     'bcheur',
@@ -49,6 +50,10 @@
     'etheur',
     'bsveur',
     'ltceur',
+    'iotabtc',
+    'dashbtc',
+    'gntbtc',
+    'ltcbtc',
 )
 
 
@@ -57,8 +62,14 @@
 
 
 def bitcoinde_pair_to_world(pair: str) -> Tuple[Asset, Asset]:
-    tx_asset = bitcoinde_asset(pair[:3])
-    native_asset = bitcoinde_asset(pair[3:])
+    if len(pair) == 6:
+        tx_asset = bitcoinde_asset(pair[:3])
+        native_asset = bitcoinde_asset(pair[3:])
+    elif len(pair) in (7, 8):
+        tx_asset = bitcoinde_asset(pair[:4])
+        native_asset = bitcoinde_asset(pair[4:])
+    else:
+        raise DeserializationError(f'Could not parse pair: {pair}')
     return tx_asset, native_asset
 
 
@@ -277,7 +288,28 @@
                 continue
             if timestamp < start_ts or timestamp > end_ts:
                 continue
-            trades.append(trade_from_bitcoinde(tx))
+            try:
+                trades.append(trade_from_bitcoinde(tx))
+            except UnknownAsset as e:
+                self.msg_aggregator.add_warning(
+                    f'Found bitcoin.de trade with unknown asset '
+                    f'{e.asset_name}. Ignoring it.',
+                )
+                continue
+            except (DeserializationError, KeyError) as e:
+                msg = str(e)
+                if isinstance(e, KeyError):
+                    msg = f'Missing key entry for {msg}.'
+                self.msg_aggregator.add_error(
+                    'Error processing a Bitcoin.de trade. Check logs '
+                    'for details. Ignoring it.',
+                )
+                log.error(
+                    'Error processing a Bitcoin.de trade',
+                    trade=tx,
+                    error=msg,
+                )
+                continue
 
         return trades

{"golden_diff": "diff --git a/rotkehlchen/exchanges/bitcoinde.py b/rotkehlchen/exchanges/bitcoinde.py\n--- a/rotkehlchen/exchanges/bitcoinde.py\n+++ b/rotkehlchen/exchanges/bitcoinde.py\n@@ -10,7 +10,7 @@\n from typing_extensions import Literal\n \n from rotkehlchen.assets.asset import Asset\n-from rotkehlchen.errors import RemoteError\n+from rotkehlchen.errors import DeserializationError, RemoteError, UnknownAsset\n from rotkehlchen.exchanges.data_structures import (\n     AssetMovement,\n     Location,\n@@ -41,7 +41,8 @@\n # This corresponds to md5('') and is used in signature generation\n MD5_EMPTY_STR = 'd41d8cd98f00b204e9800998ecf8427e'\n \n-# Pairs can be found in Basic API doc: https://www.bitcoin.de/de/api/marketplace\n+# Pairs can be found in Basic API doc:\n+# https://www.bitcoin.de/en/api/tapi/v4/docu#handelspaarliste_c2f\n BITCOINDE_TRADING_PAIRS = (\n     'btceur',\n     'bcheur',\n@@ -49,6 +50,10 @@\n     'etheur',\n     'bsveur',\n     'ltceur',\n+    'iotabtc',\n+    'dashbtc',\n+    'gntbtc',\n+    'ltcbtc',\n )\n \n \n@@ -57,8 +62,14 @@\n \n \n def bitcoinde_pair_to_world(pair: str) -> Tuple[Asset, Asset]:\n-    tx_asset = bitcoinde_asset(pair[:3])\n-    native_asset = bitcoinde_asset(pair[3:])\n+    if len(pair) == 6:\n+        tx_asset = bitcoinde_asset(pair[:3])\n+        native_asset = bitcoinde_asset(pair[3:])\n+    elif len(pair) in (7, 8):\n+        tx_asset = bitcoinde_asset(pair[:4])\n+        native_asset = bitcoinde_asset(pair[4:])\n+    else:\n+        raise DeserializationError(f'Could not parse pair: {pair}')\n     return tx_asset, native_asset\n \n \n@@ -277,7 +288,28 @@\n                 continue\n             if timestamp < start_ts or timestamp > end_ts:\n                 continue\n-            trades.append(trade_from_bitcoinde(tx))\n+            try:\n+                trades.append(trade_from_bitcoinde(tx))\n+            except UnknownAsset as e:\n+                self.msg_aggregator.add_warning(\n+                    f'Found bitcoin.de trade with unknown asset '\n+                    f'{e.asset_name}. Ignoring it.',\n+                )\n+                continue\n+            except (DeserializationError, KeyError) as e:\n+                msg = str(e)\n+                if isinstance(e, KeyError):\n+                    msg = f'Missing key entry for {msg}.'\n+                self.msg_aggregator.add_error(\n+                    'Error processing a Bitcoin.de trade. Check logs '\n+                    'for details. Ignoring it.',\n+                )\n+                log.error(\n+                    'Error processing a Bitcoin.de trade',\n+                    trade=tx,\n+                    error=msg,\n+                )\n+                continue\n \n         return trades\n", "issue": "Importing IOTA trades from bitcoin.de throw errors\n## Problem Definition\r\n\r\nOn importing exchange data from bitcoin.de an error is thrown:\r\n\"Query trades of bitcoinde exchange died with exception: Unknown asset IOT provided.. Check the logs for more details\"\r\nThis is probably the same issue as in https://github.com/rotki/rotki/issues/299 -> IOT needs to be mapped to IOTA\r\n\r\n## Logs\r\n\r\nThe log is empty.\r\n\r\n\r\n### System Description\r\n\r\n\r\nOperating system: \r\nRotki version: 1.12.2\r\n\n", "before_files": [{"content": "import hashlib\nimport hmac\nimport logging\nimport time\nfrom json.decoder import JSONDecodeError\nfrom typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple\nfrom urllib.parse import urlencode\n\nimport requests\nfrom typing_extensions import Literal\n\nfrom rotkehlchen.assets.asset import Asset\nfrom rotkehlchen.errors import RemoteError\nfrom rotkehlchen.exchanges.data_structures import (\n    AssetMovement,\n    Location,\n    MarginPosition,\n    Price,\n    Trade,\n    TradePair,\n)\nfrom rotkehlchen.exchanges.exchange import ExchangeInterface\nfrom rotkehlchen.inquirer import Inquirer\nfrom rotkehlchen.logging import RotkehlchenLogsAdapter\nfrom rotkehlchen.serialization.deserialize import (\n    deserialize_fee,\n    deserialize_timestamp_from_date,\n    deserialize_trade_type,\n)\nfrom rotkehlchen.typing import ApiKey, ApiSecret, AssetAmount, FVal, Timestamp\nfrom rotkehlchen.user_messages import MessagesAggregator\nfrom rotkehlchen.utils.misc import iso8601ts_to_timestamp\nfrom rotkehlchen.utils.serialization import rlk_jsonloads\n\nif TYPE_CHECKING:\n    from rotkehlchen.db.dbhandler import DBHandler\n\nlogger = logging.getLogger(__name__)\nlog = RotkehlchenLogsAdapter(logger)\n\n# This corresponds to md5('') and is used in signature generation\nMD5_EMPTY_STR = 'd41d8cd98f00b204e9800998ecf8427e'\n\n# Pairs can be found in Basic API doc: https://www.bitcoin.de/de/api/marketplace\nBITCOINDE_TRADING_PAIRS = (\n    'btceur',\n    'bcheur',\n    'btgeur',\n    'etheur',\n    'bsveur',\n    'ltceur',\n)\n\n\ndef bitcoinde_asset(asset: str) -> Asset:\n    return Asset(asset.upper())\n\n\ndef bitcoinde_pair_to_world(pair: str) -> Tuple[Asset, Asset]:\n    tx_asset = bitcoinde_asset(pair[:3])\n    native_asset = bitcoinde_asset(pair[3:])\n    return tx_asset, native_asset\n\n\ndef trade_from_bitcoinde(raw_trade: Dict) -> Trade:\n\n    try:\n        timestamp = deserialize_timestamp_from_date(\n            raw_trade['successfully_finished_at'],\n            'iso8601',\n            'bitcoinde',\n        )\n    except KeyError:\n        # For very old trades (2013) bitcoin.de does not return 'successfully_finished_at'\n        timestamp = deserialize_timestamp_from_date(\n            raw_trade['trade_marked_as_paid_at'],\n            'iso8601',\n            'bitcoinde',\n        )\n\n    trade_type = deserialize_trade_type(raw_trade['type'])\n    tx_amount = AssetAmount(FVal(raw_trade['amount_currency_to_trade']))\n    native_amount = FVal(raw_trade['volume_currency_to_pay'])\n    tx_asset, native_asset = bitcoinde_pair_to_world(raw_trade['trading_pair'])\n    pair = TradePair(f'{tx_asset.identifier}_{native_asset.identifier}')\n    amount = tx_amount\n    rate = Price(native_amount / tx_amount)\n    fee_amount = deserialize_fee(raw_trade['fee_currency_to_pay'])\n    fee_asset = Asset('EUR')\n\n    return Trade(\n        timestamp=timestamp,\n        location=Location.BITCOINDE,\n        pair=pair,\n        trade_type=trade_type,\n        amount=amount,\n        rate=rate,\n        fee=fee_amount,\n        fee_currency=fee_asset,\n        link=str(raw_trade['trade_id']),\n    )\n\n\nclass Bitcoinde(ExchangeInterface):  # lgtm[py/missing-call-to-init]\n    def __init__(\n            self,\n            api_key: ApiKey,\n            secret: ApiSecret,\n            database: 'DBHandler',\n            msg_aggregator: MessagesAggregator,\n    ):\n        super().__init__('bitcoinde', api_key, secret, database)\n        self.uri = 'https://api.bitcoin.de'\n        self.session.headers.update({'x-api-key': api_key})\n        self.msg_aggregator = msg_aggregator\n\n    def _generate_signature(self, request_type: str, url: str, nonce: str) -> str:\n        signed_data = '#'.join([request_type, url, self.api_key, nonce, MD5_EMPTY_STR]).encode()\n        signature = hmac.new(\n            self.secret,\n            signed_data,\n            hashlib.sha256,\n        ).hexdigest()\n        self.session.headers.update({\n            'x-api-signature': signature,\n        })\n        return signature\n\n    def _api_query(\n            self,\n            verb: Literal['get', 'post'],\n            path: str,\n            options: Optional[Dict] = None,\n    ) -> Dict:\n        \"\"\"\n        Queries Bitcoin.de with the given verb for the given path and options\n        \"\"\"\n        assert verb in ('get', 'post'), (\n            'Given verb {} is not a valid HTTP verb'.format(verb)\n        )\n\n        request_path_no_args = '/v4/' + path\n\n        data = ''\n        if not options:\n            request_path = request_path_no_args\n        else:\n            request_path = request_path_no_args + '?' + urlencode(options)\n\n        nonce = str(int(time.time() * 1000))\n        request_url = self.uri + request_path\n\n        self._generate_signature(\n            request_type=verb.upper(),\n            url=request_url,\n            nonce=nonce,\n        )\n\n        headers = {\n            'x-api-nonce': nonce,\n        }\n        if data != '':\n            headers.update({\n                'Content-Type': 'application/json',\n                'Content-Length': str(len(data)),\n            })\n\n        log.debug('Bitcoin.de API Query', verb=verb, request_url=request_url)\n\n        try:\n            response = getattr(self.session, verb)(request_url, data=data, headers=headers)\n        except requests.exceptions.RequestException as e:\n            raise RemoteError(f'Bitcoin.de API request failed due to {str(e)}') from e\n\n        try:\n            json_ret = rlk_jsonloads(response.text)\n        except JSONDecodeError as exc:\n            raise RemoteError('Bitcoin.de returned invalid JSON response') from exc\n\n        if response.status_code not in (200, 401):\n            if isinstance(json_ret, dict) and 'errors' in json_ret:\n                for error in json_ret['errors']:\n                    if error.get('field') == 'X-API-KEY' and error.get('code') == 1:\n                        raise RemoteError('Provided API Key is in invalid Format')\n\n                    if error.get('code') == 3:\n                        raise RemoteError('Provided API Key is invalid')\n\n                raise RemoteError(json_ret['errors'])\n\n            raise RemoteError(\n                'Bitcoin.de api request for {} failed with HTTP status code {}'.format(\n                    response.url,\n                    response.status_code,\n                ),\n            )\n\n        if not isinstance(json_ret, dict):\n            raise RemoteError('Bitcoin.de returned invalid non-dict response')\n\n        return json_ret\n\n    def validate_api_key(self) -> Tuple[bool, str]:\n        \"\"\"\n        Validates that the Bitcoin.de API key is good for usage in Rotki\n        \"\"\"\n\n        try:\n            self._api_query('get', 'account')\n            return True, \"\"\n\n        except RemoteError as e:\n            return False, str(e)\n\n    def query_balances(self, **kwargs: Any) -> Tuple[Optional[Dict[Asset, Dict[str, Any]]], str]:\n        balances = {}\n        try:\n            resp_info = self._api_query('get', 'account')\n        except RemoteError as e:\n            msg = (\n                'Bitcoin.de request failed. Could not reach bitcoin.de due '\n                'to {}'.format(e)\n            )\n            log.error(msg)\n            return None, msg\n\n        for currency, balance in resp_info['data']['balances'].items():\n            asset = bitcoinde_asset(currency)\n            try:\n                usd_price = Inquirer().find_usd_price(asset=asset)\n            except RemoteError as e:\n                self.msg_aggregator.add_error(\n                    f'Error processing Bitcoin.de balance entry due to inability to '\n                    f'query USD price: {str(e)}. Skipping balance entry',\n                )\n                continue\n\n            balances[asset] = {\n                'amount': balance['total_amount'],\n                'usd_value': balance['total_amount'] * usd_price,\n            }\n\n        return balances, ''\n\n    def query_online_trade_history(\n            self,\n            start_ts: Timestamp,\n            end_ts: Timestamp,\n    ) -> List[Trade]:\n\n        page = 1\n        resp_trades = []\n\n        while True:\n            resp = self._api_query('get', 'trades', {'state': 1, 'page': page})\n            resp_trades.extend(resp['trades'])\n\n            if 'page' not in resp:\n                break\n\n            if resp['page']['current'] >= resp['page']['last']:\n                break\n\n            page = resp['page']['current'] + 1\n\n        log.debug('Bitcoin.de trade history query', results_num=len(resp_trades))\n\n        trades = []\n        for tx in resp_trades:\n            try:\n                timestamp = iso8601ts_to_timestamp(tx['successfully_finished_at'])\n            except KeyError:\n                # For very old trades (2013) bitcoin.de does not return 'successfully_finished_at'\n                timestamp = iso8601ts_to_timestamp(tx['trade_marked_as_paid_at'])\n\n            if tx['state'] != 1:\n                continue\n            if timestamp < start_ts or timestamp > end_ts:\n                continue\n            trades.append(trade_from_bitcoinde(tx))\n\n        return trades\n\n    def query_online_deposits_withdrawals(\n            self,  # pylint: disable=no-self-use\n            start_ts: Timestamp,  # pylint: disable=unused-argument\n            end_ts: Timestamp,  # pylint: disable=unused-argument\n    ) -> List[AssetMovement]:\n        return []  # noop for bitcoinde\n\n    def query_online_margin_history(\n            self,  # pylint: disable=no-self-use\n            start_ts: Timestamp,  # pylint: disable=unused-argument\n            end_ts: Timestamp,  # pylint: disable=unused-argument\n    ) -> List[MarginPosition]:\n        return []  # noop for bitcoinde\n", "path": "rotkehlchen/exchanges/bitcoinde.py"}], "after_files": [{"content": "import hashlib\nimport hmac\nimport logging\nimport time\nfrom json.decoder import JSONDecodeError\nfrom typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple\nfrom urllib.parse import urlencode\n\nimport requests\nfrom typing_extensions import Literal\n\nfrom rotkehlchen.assets.asset import Asset\nfrom rotkehlchen.errors import DeserializationError, RemoteError, UnknownAsset\nfrom rotkehlchen.exchanges.data_structures import (\n    AssetMovement,\n    Location,\n    MarginPosition,\n    Price,\n    Trade,\n    TradePair,\n)\nfrom rotkehlchen.exchanges.exchange import ExchangeInterface\nfrom rotkehlchen.inquirer import Inquirer\nfrom rotkehlchen.logging import RotkehlchenLogsAdapter\nfrom rotkehlchen.serialization.deserialize import (\n    deserialize_fee,\n    deserialize_timestamp_from_date,\n    deserialize_trade_type,\n)\nfrom rotkehlchen.typing import ApiKey, ApiSecret, AssetAmount, FVal, Timestamp\nfrom rotkehlchen.user_messages import MessagesAggregator\nfrom rotkehlchen.utils.misc import iso8601ts_to_timestamp\nfrom rotkehlchen.utils.serialization import rlk_jsonloads\n\nif TYPE_CHECKING:\n    from rotkehlchen.db.dbhandler import DBHandler\n\nlogger = logging.getLogger(__name__)\nlog = RotkehlchenLogsAdapter(logger)\n\n# This corresponds to md5('') and is used in signature generation\nMD5_EMPTY_STR = 'd41d8cd98f00b204e9800998ecf8427e'\n\n# Pairs can be found in Basic API doc:\n# https://www.bitcoin.de/en/api/tapi/v4/docu#handelspaarliste_c2f\nBITCOINDE_TRADING_PAIRS = (\n    'btceur',\n    'bcheur',\n    'btgeur',\n    'etheur',\n    'bsveur',\n    'ltceur',\n    'iotabtc',\n    'dashbtc',\n    'gntbtc',\n    'ltcbtc',\n)\n\n\ndef bitcoinde_asset(asset: str) -> Asset:\n    return Asset(asset.upper())\n\n\ndef bitcoinde_pair_to_world(pair: str) -> Tuple[Asset, Asset]:\n    if len(pair) == 6:\n        tx_asset = bitcoinde_asset(pair[:3])\n        native_asset = bitcoinde_asset(pair[3:])\n    elif len(pair) in (7, 8):\n        tx_asset = bitcoinde_asset(pair[:4])\n        native_asset = bitcoinde_asset(pair[4:])\n    else:\n        raise DeserializationError(f'Could not parse pair: {pair}')\n    return tx_asset, native_asset\n\n\ndef trade_from_bitcoinde(raw_trade: Dict) -> Trade:\n\n    try:\n        timestamp = deserialize_timestamp_from_date(\n            raw_trade['successfully_finished_at'],\n            'iso8601',\n            'bitcoinde',\n        )\n    except KeyError:\n        # For very old trades (2013) bitcoin.de does not return 'successfully_finished_at'\n        timestamp = deserialize_timestamp_from_date(\n            raw_trade['trade_marked_as_paid_at'],\n            'iso8601',\n            'bitcoinde',\n        )\n\n    trade_type = deserialize_trade_type(raw_trade['type'])\n    tx_amount = AssetAmount(FVal(raw_trade['amount_currency_to_trade']))\n    native_amount = FVal(raw_trade['volume_currency_to_pay'])\n    tx_asset, native_asset = bitcoinde_pair_to_world(raw_trade['trading_pair'])\n    pair = TradePair(f'{tx_asset.identifier}_{native_asset.identifier}')\n    amount = tx_amount\n    rate = Price(native_amount / tx_amount)\n    fee_amount = deserialize_fee(raw_trade['fee_currency_to_pay'])\n    fee_asset = Asset('EUR')\n\n    return Trade(\n        timestamp=timestamp,\n        location=Location.BITCOINDE,\n        pair=pair,\n        trade_type=trade_type,\n        amount=amount,\n        rate=rate,\n        fee=fee_amount,\n        fee_currency=fee_asset,\n        link=str(raw_trade['trade_id']),\n    )\n\n\nclass Bitcoinde(ExchangeInterface):  # lgtm[py/missing-call-to-init]\n    def __init__(\n            self,\n            api_key: ApiKey,\n            secret: ApiSecret,\n            database: 'DBHandler',\n            msg_aggregator: MessagesAggregator,\n    ):\n        super().__init__('bitcoinde', api_key, secret, database)\n        self.uri = 'https://api.bitcoin.de'\n        self.session.headers.update({'x-api-key': api_key})\n        self.msg_aggregator = msg_aggregator\n\n    def _generate_signature(self, request_type: str, url: str, nonce: str) -> str:\n        signed_data = '#'.join([request_type, url, self.api_key, nonce, MD5_EMPTY_STR]).encode()\n        signature = hmac.new(\n            self.secret,\n            signed_data,\n            hashlib.sha256,\n        ).hexdigest()\n        self.session.headers.update({\n            'x-api-signature': signature,\n        })\n        return signature\n\n    def _api_query(\n            self,\n            verb: Literal['get', 'post'],\n            path: str,\n            options: Optional[Dict] = None,\n    ) -> Dict:\n        \"\"\"\n        Queries Bitcoin.de with the given verb for the given path and options\n        \"\"\"\n        assert verb in ('get', 'post'), (\n            'Given verb {} is not a valid HTTP verb'.format(verb)\n        )\n\n        request_path_no_args = '/v4/' + path\n\n        data = ''\n        if not options:\n            request_path = request_path_no_args\n        else:\n            request_path = request_path_no_args + '?' + urlencode(options)\n\n        nonce = str(int(time.time() * 1000))\n        request_url = self.uri + request_path\n\n        self._generate_signature(\n            request_type=verb.upper(),\n            url=request_url,\n            nonce=nonce,\n        )\n\n        headers = {\n            'x-api-nonce': nonce,\n        }\n        if data != '':\n            headers.update({\n                'Content-Type': 'application/json',\n                'Content-Length': str(len(data)),\n            })\n\n        log.debug('Bitcoin.de API Query', verb=verb, request_url=request_url)\n\n        try:\n            response = getattr(self.session, verb)(request_url, data=data, headers=headers)\n        except requests.exceptions.RequestException as e:\n            raise RemoteError(f'Bitcoin.de API request failed due to {str(e)}') from e\n\n        try:\n            json_ret = rlk_jsonloads(response.text)\n        except JSONDecodeError as exc:\n            raise RemoteError('Bitcoin.de returned invalid JSON response') from exc\n\n        if response.status_code not in (200, 401):\n            if isinstance(json_ret, dict) and 'errors' in json_ret:\n                for error in json_ret['errors']:\n                    if error.get('field') == 'X-API-KEY' and error.get('code') == 1:\n                        raise RemoteError('Provided API Key is in invalid Format')\n\n                    if error.get('code') == 3:\n                        raise RemoteError('Provided API Key is invalid')\n\n                raise RemoteError(json_ret['errors'])\n\n            raise RemoteError(\n                'Bitcoin.de api request for {} failed with HTTP status code {}'.format(\n                    response.url,\n                    response.status_code,\n                ),\n            )\n\n        if not isinstance(json_ret, dict):\n            raise RemoteError('Bitcoin.de returned invalid non-dict response')\n\n        return json_ret\n\n    def validate_api_key(self) -> Tuple[bool, str]:\n        \"\"\"\n        Validates that the Bitcoin.de API key is good for usage in Rotki\n        \"\"\"\n\n        try:\n            self._api_query('get', 'account')\n            return True, \"\"\n\n        except RemoteError as e:\n            return False, str(e)\n\n    def query_balances(self, **kwargs: Any) -> Tuple[Optional[Dict[Asset, Dict[str, Any]]], str]:\n        balances = {}\n        try:\n            resp_info = self._api_query('get', 'account')\n        except RemoteError as e:\n            msg = (\n                'Bitcoin.de request failed. Could not reach bitcoin.de due '\n                'to {}'.format(e)\n            )\n            log.error(msg)\n            return None, msg\n\n        for currency, balance in resp_info['data']['balances'].items():\n            asset = bitcoinde_asset(currency)\n            try:\n                usd_price = Inquirer().find_usd_price(asset=asset)\n            except RemoteError as e:\n                self.msg_aggregator.add_error(\n                    f'Error processing Bitcoin.de balance entry due to inability to '\n                    f'query USD price: {str(e)}. Skipping balance entry',\n                )\n                continue\n\n            balances[asset] = {\n                'amount': balance['total_amount'],\n                'usd_value': balance['total_amount'] * usd_price,\n            }\n\n        return balances, ''\n\n    def query_online_trade_history(\n            self,\n            start_ts: Timestamp,\n            end_ts: Timestamp,\n    ) -> List[Trade]:\n\n        page = 1\n        resp_trades = []\n\n        while True:\n            resp = self._api_query('get', 'trades', {'state': 1, 'page': page})\n            resp_trades.extend(resp['trades'])\n\n            if 'page' not in resp:\n                break\n\n            if resp['page']['current'] >= resp['page']['last']:\n                break\n\n            page = resp['page']['current'] + 1\n\n        log.debug('Bitcoin.de trade history query', results_num=len(resp_trades))\n\n        trades = []\n        for tx in resp_trades:\n            try:\n                timestamp = iso8601ts_to_timestamp(tx['successfully_finished_at'])\n            except KeyError:\n                # For very old trades (2013) bitcoin.de does not return 'successfully_finished_at'\n                timestamp = iso8601ts_to_timestamp(tx['trade_marked_as_paid_at'])\n\n            if tx['state'] != 1:\n                continue\n            if timestamp < start_ts or timestamp > end_ts:\n                continue\n            try:\n                trades.append(trade_from_bitcoinde(tx))\n            except UnknownAsset as e:\n                self.msg_aggregator.add_warning(\n                    f'Found bitcoin.de trade with unknown asset '\n                    f'{e.asset_name}. Ignoring it.',\n                )\n                continue\n            except (DeserializationError, KeyError) as e:\n                msg = str(e)\n                if isinstance(e, KeyError):\n                    msg = f'Missing key entry for {msg}.'\n                self.msg_aggregator.add_error(\n                    'Error processing a Bitcoin.de trade. Check logs '\n                    'for details. Ignoring it.',\n                )\n                log.error(\n                    'Error processing a Bitcoin.de trade',\n                    trade=tx,\n                    error=msg,\n                )\n                continue\n\n        return trades\n\n    def query_online_deposits_withdrawals(\n            self,  # pylint: disable=no-self-use\n            start_ts: Timestamp,  # pylint: disable=unused-argument\n            end_ts: Timestamp,  # pylint: disable=unused-argument\n    ) -> List[AssetMovement]:\n        return []  # noop for bitcoinde\n\n    def query_online_margin_history(\n            self,  # pylint: disable=no-self-use\n            start_ts: Timestamp,  # pylint: disable=unused-argument\n            end_ts: Timestamp,  # pylint: disable=unused-argument\n    ) -> List[MarginPosition]:\n        return []  # noop for bitcoinde\n", "path": "rotkehlchen/exchanges/bitcoinde.py"}]}