import hashlib
import hmac
import logging
import time
from json.decoder import JSONDecodeError
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple
from urllib.parse import urlencode

import requests

from rotkehlchen.assets.asset import Asset
from rotkehlchen.assets.converters import asset_from_coinbase
from rotkehlchen.constants.misc import ZERO
from rotkehlchen.errors import DeserializationError, RemoteError, UnknownAsset, UnsupportedAsset
from rotkehlchen.exchanges.data_structures import AssetMovement, Trade
from rotkehlchen.exchanges.exchange import ExchangeInterface
from rotkehlchen.exchanges.utils import deserialize_asset_movement_address, get_key_if_has_val
from rotkehlchen.inquirer import Inquirer
from rotkehlchen.logging import RotkehlchenLogsAdapter
from rotkehlchen.serialization.deserialize import (
    deserialize_asset_amount,
    deserialize_asset_amount_force_positive,
    deserialize_asset_movement_category,
    deserialize_fee,
    deserialize_timestamp_from_date,
    deserialize_trade_type,
)
from rotkehlchen.typing import (
    ApiKey,
    ApiSecret,
    AssetMovementCategory,
    Fee,
    Location,
    Price,
    Timestamp,
    TradePair,
)
from rotkehlchen.user_messages import MessagesAggregator
from rotkehlchen.utils.interfaces import cache_response_timewise, protect_with_lock
from rotkehlchen.utils.serialization import rlk_jsonloads_dict

if TYPE_CHECKING:
    from rotkehlchen.db.dbhandler import DBHandler

logger = logging.getLogger(__name__)
log = RotkehlchenLogsAdapter(logger)


def trade_from_coinbase(raw_trade: Dict[str, Any]) -> Optional[Trade]:
    """Turns a coinbase transaction into a rotkehlchen Trade.

    https://developers.coinbase.com/api/v2?python#buys
    If the coinbase transaction is not a trade related transaction returns None

    Throws:
        - UnknownAsset due to Asset instantiation
        - DeserializationError due to unexpected format of dict entries
        - KeyError due to dict entires missing an expected entry
    """

    if raw_trade['status'] != 'completed':
        # We only want to deal with completed trades
        return None

    if raw_trade['instant']:
        raw_time = raw_trade['created_at']
    else:
        raw_time = raw_trade['payout_at']
    timestamp = deserialize_timestamp_from_date(raw_time, 'iso8601', 'coinbase')
    trade_type = deserialize_trade_type(raw_trade['resource'])
    tx_amount = deserialize_asset_amount(raw_trade['amount']['amount'])
    tx_asset = asset_from_coinbase(raw_trade['amount']['currency'], time=timestamp)
    native_amount = deserialize_asset_amount(raw_trade['subtotal']['amount'])
    native_asset = asset_from_coinbase(raw_trade['subtotal']['currency'], time=timestamp)
    # in coinbase you are buying/selling tx_asset for native_asset
    pair = TradePair(f'{tx_asset.identifier}_{native_asset.identifier}')
    amount = tx_amount
    # The rate is how much you get/give in quotecurrency if you buy/sell 1 unit of base currency
    rate = Price(native_amount / tx_amount)
    fee_amount = deserialize_fee(raw_trade['fee']['amount'])
    fee_asset = asset_from_coinbase(raw_trade['fee']['currency'], time=timestamp)

    return Trade(
        timestamp=timestamp,
        location=Location.COINBASE,
        pair=pair,
        trade_type=trade_type,
        amount=amount,
        rate=rate,
        fee=fee_amount,
        fee_currency=fee_asset,
        link=str(raw_trade['id']),
    )


class CoinbasePermissionError(Exception):
    pass


class Coinbase(ExchangeInterface):

    def __init__(
            self,
            api_key: ApiKey,
            secret: ApiSecret,
            database: 'DBHandler',
            msg_aggregator: MessagesAggregator,
    ):
        super(Coinbase, self).__init__('coinbase', api_key, secret, database)
        self.apiversion = 'v2'
        self.base_uri = 'https://api.coinbase.com'
        self.msg_aggregator = msg_aggregator

    def first_connection(self) -> None:
        self.first_connection_made = True

    def _validate_single_api_key_action(
            self,
            method_str: str,
            ignore_pagination: bool = False,
    ) -> Tuple[Optional[List[Any]], str]:
        try:
            result = self._api_query(method_str, ignore_pagination=ignore_pagination)

        except CoinbasePermissionError as e:
            error = str(e)
            if 'transactions' in method_str:
                permission = 'wallet:transactions:read'
            elif 'buys' in method_str:
                permission = 'wallet:buys:read'
            elif 'sells' in method_str:
                permission = 'wallet:sells:read'
            elif 'deposits' in method_str:
                permission = 'wallet:deposits:read'
            elif 'withdrawals' in method_str:
                permission = 'wallet:withdrawals:read'
            elif 'trades' in method_str:
                permission = 'wallet:trades:read'
            # the accounts elif should be at the end since the word appears
            # in other endpoints
            elif 'accounts' in method_str:
                permission = 'wallet:accounts:read'
            else:
                raise AssertionError(
                    f'Unexpected coinbase method {method_str} at API key validation',
                )
            msg = (
                f'Provided Coinbase API key needs to have {permission} permission activated. '
                f'Please log into your coinbase account and set all required permissions: '
                f'wallet:accounts:read, wallet:transactions:read, '
                f'wallet:buys:read, wallet:sells:read, wallet:withdrawals:read, '
                f'wallet:deposits:read, wallet:trades:read'
            )

            return None, msg
        except RemoteError as e:
            error = str(e)
            if 'invalid signature' in error:
                return None, 'Failed to authenticate with the Provided API key/secret'
            elif 'invalid api key' in error:
                return None, 'Provided API Key is invalid'
            else:
                # any other remote error
                return None, error

        return result, ''

    def validate_api_key(self) -> Tuple[bool, str]:
        """Validates that the Coinbase API key is good for usage in Rotki

        Makes sure that the following permissions are given to the key:
        wallet:accounts:read, wallet:transactions:read,
        wallet:buys:read, wallet:sells:read, wallet:withdrawals:read,
        wallet:deposits:read
        """
        result, msg = self._validate_single_api_key_action('accounts')
        if result is None:
            return False, msg

        # now get the account ids
        account_ids = self._get_account_ids(result)
        if len(account_ids) != 0:
            # and now try to get all transactions of an account to see if that's possible
            method = f'accounts/{account_ids[0]}/transactions'
            result, msg = self._validate_single_api_key_action(method)
            if result is None:
                return False, msg

            # and now try to get all buys of an account to see if that's possible
            method = f'accounts/{account_ids[0]}/buys'
            result, msg = self._validate_single_api_key_action(method)
            if result is None:
                return False, msg

            # and now try to get all sells of an account to see if that's possible
            method = f'accounts/{account_ids[0]}/sells'
            result, msg = self._validate_single_api_key_action(method)
            if result is None:
                return False, msg

            # and now try to get all deposits of an account to see if that's possible
            method = f'accounts/{account_ids[0]}/deposits'
            result, msg = self._validate_single_api_key_action(method)
            if result is None:
                return False, msg

            # and now try to get all withdrawals of an account to see if that's possible
            method = f'accounts/{account_ids[0]}/withdrawals'
            result, msg = self._validate_single_api_key_action(method)
            if result is None:
                return False, msg

        return True, ''

    def _get_account_ids(self, accounts: List[Dict[str, Any]]) -> List[str]:
        """Gets the account ids out of the accounts response"""
        account_ids = []
        for account_data in accounts:
            if 'id' not in account_data:
                self.msg_aggregator.add_error(
                    'Found coinbase account entry without an id key. Skipping it. ',
                )
                continue

            if not isinstance(account_data['id'], str):
                self.msg_aggregator.add_error(
                    f'Found coinbase account entry with a non string id: '
                    f'{account_data["id"]}. Skipping it. ',
                )
                continue

            account_ids.append(account_data['id'])

        return account_ids

    def _api_query(
            self,
            endpoint: str,
            options: Optional[Dict[str, Any]] = None,
            pagination_next_uri: str = None,
            ignore_pagination: bool = False,
    ) -> List[Any]:
        """Performs a coinbase API Query for endpoint

        You can optionally provide extra arguments to the endpoint via the options argument.
        If this is an ongoing paginating call then provide pagination_next_uri.
        If you want just the first results then set ignore_pagination to True.
        """
        request_verb = "GET"
        if pagination_next_uri:
            request_url = pagination_next_uri
        else:
            request_url = f'/{self.apiversion}/{endpoint}'
            if options:
                request_url += urlencode(options)

        timestamp = str(int(time.time()))
        message = timestamp + request_verb + request_url

        signature = hmac.new(
            self.secret,
            message.encode(),
            hashlib.sha256,
        ).hexdigest()
        log.debug('Coinbase API query', request_url=request_url)

        self.session.headers.update({
            'CB-ACCESS-SIGN': signature,
            'CB-ACCESS-TIMESTAMP': timestamp,
            'CB-ACCESS-KEY': self.api_key,
            # This is needed to guarantee the up to the given date
            # API version response.
            'CB-VERSION': '2019-08-25',
        })
        full_url = self.base_uri + request_url
        try:
            response = self.session.get(full_url)
        except requests.exceptions.RequestException as e:
            raise RemoteError(f'Coinbase API request failed due to {str(e)}')

        if response.status_code == 403:
            raise CoinbasePermissionError(f'API key does not have permission for {endpoint}')

        if response.status_code != 200:
            raise RemoteError(
                f'Coinbase query {full_url} responded with error status code: '
                f'{response.status_code} and text: {response.text}',
            )

        try:
            json_ret = rlk_jsonloads_dict(response.text)
        except JSONDecodeError:
            raise RemoteError(f'Coinbase returned invalid JSON response: {response.text}')

        if 'data' not in json_ret:
            raise RemoteError(f'Coinbase json response does not contain data: {response.text}')

        final_data = json_ret['data']

        # If we got pagination and this is the first query, gather all the subsequent queries
        if 'pagination' in json_ret and not pagination_next_uri and not ignore_pagination:
            if 'next_uri' not in json_ret['pagination']:
                raise RemoteError('Coinbase json response contained no "next_uri" key')

            next_uri = json_ret['pagination']['next_uri']
            if not next_uri:
                # As per the docs: https://developers.coinbase.com/api/v2?python#pagination
                # once we get an empty next_uri we are done
                return final_data

            additional_data = self._api_query(
                endpoint=endpoint,
                options=options,
                pagination_next_uri=next_uri,
            )
            final_data.extend(additional_data)

        return final_data

    @protect_with_lock()
    @cache_response_timewise()
    def query_balances(self) -> Tuple[Optional[Dict[Asset, Dict[str, Any]]], str]:
        try:
            resp = self._api_query('accounts')
        except RemoteError as e:
            msg = (
                'Coinbase API request failed. Could not reach coinbase due '
                'to {}'.format(e)
            )
            log.error(msg)
            return None, msg

        returned_balances: Dict[Asset, Dict[str, Any]] = {}
        for account in resp:
            try:
                if not account['balance']:
                    continue

                amount = deserialize_asset_amount(account['balance']['amount'])

                # ignore empty balances. Coinbase returns zero balances for everything
                # a user does not own
                if amount == ZERO:
                    continue

                asset = asset_from_coinbase(account['balance']['currency'])

                try:
                    usd_price = Inquirer().find_usd_price(asset=asset)
                except RemoteError as e:
                    self.msg_aggregator.add_error(
                        f'Error processing coinbase balance entry due to inability to '
                        f'query USD price: {str(e)}. Skipping balance entry',
                    )
                    continue

                if asset in returned_balances:
                    amount = returned_balances[asset]['amount'] + amount
                else:
                    returned_balances[asset] = {}

                returned_balances[asset]['amount'] = amount
                usd_value = returned_balances[asset]['amount'] * usd_price
                returned_balances[asset]['usd_value'] = usd_value

            except UnknownAsset as e:
                self.msg_aggregator.add_warning(
                    f'Found coinbase balance result with unknown asset '
                    f'{e.asset_name}. Ignoring it.',
                )
                continue
            except UnsupportedAsset as e:
                self.msg_aggregator.add_warning(
                    f'Found coinbase balance result with unsupported 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 coinbase account balance. Check logs '
                    'for details. Ignoring it.',
                )
                log.error(
                    'Error processing a coinbase account balance',
                    account_balance=account,
                    error=msg,
                )
                continue

        return returned_balances, ''

    def query_online_trade_history(
            self,
            start_ts: Timestamp,
            end_ts: Timestamp,
    ) -> List[Trade]:
        account_data = self._api_query('accounts')
        # now get the account ids and for each one query buys/sells
        # Looking at coinbase's API no other type of transaction
        # https://developers.coinbase.com/api/v2?python#list-transactions
        # consitutes something that Rotkehlchen would need to return in query_trade_history
        account_ids = self._get_account_ids(account_data)

        raw_data = []
        for account_id in account_ids:
            raw_data.extend(self._api_query(f'accounts/{account_id}/buys'))
            raw_data.extend(self._api_query(f'accounts/{account_id}/sells'))
        log.debug('coinbase buys/sells history result', results_num=len(raw_data))

        trades = []
        for raw_trade in raw_data:
            try:
                trade = trade_from_coinbase(raw_trade)
            except UnknownAsset as e:
                self.msg_aggregator.add_warning(
                    f'Found coinbase transaction with unknown asset '
                    f'{e.asset_name}. Ignoring it.',
                )
                continue
            except UnsupportedAsset as e:
                self.msg_aggregator.add_warning(
                    f'Found coinbase trade with unsupported 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 coinbase trade. Check logs '
                    'for details. Ignoring it.',
                )
                log.error(
                    'Error processing a coinbase trade',
                    trade=raw_trade,
                    error=msg,
                )
                continue

            # limit coinbase trades in the requested time range here since there
            # is no argument in the API call
            if trade and trade.timestamp >= start_ts and trade.timestamp <= end_ts:
                trades.append(trade)

        return trades

    def _deserialize_asset_movement(self, raw_data: Dict[str, Any]) -> Optional[AssetMovement]:
        """Processes a single deposit/withdrawal from coinbase and deserializes it

        Can log error/warning and return None if something went wrong at deserialization
        """
        try:
            if raw_data['status'] != 'completed':
                return None

            payout_date = raw_data.get('payout_at', None)
            if payout_date:
                timestamp = deserialize_timestamp_from_date(payout_date, 'iso8601', 'coinbase')
            else:
                timestamp = deserialize_timestamp_from_date(
                    raw_data['created_at'],
                    'iso8601',
                    'coinbase',
                )

            # Only get address/transaction id for "send" type of transactions
            address = None
            transaction_id = None
            # movement_category: Union[Literal['deposit'], Literal['withdrawal']]
            if 'type' in raw_data:
                # Then this should be a "send" which is the way Coinbase uses to send
                # crypto outside of the exchange
                # https://developers.coinbase.com/api/v2?python#transaction-resource
                msg = 'Non "send" type found in coinbase deposit/withdrawal processing'
                assert raw_data['type'] == 'send', msg
                movement_category = AssetMovementCategory.WITHDRAWAL
                # Can't see the fee being charged from the "send" resource

                amount = deserialize_asset_amount_force_positive(raw_data['amount']['amount'])
                asset = asset_from_coinbase(raw_data['amount']['currency'], time=timestamp)
                # Fees dont appear in the docs but from an experiment of sending ETH
                # to an address from coinbase there is the network fee in the response
                fee = Fee(ZERO)
                raw_network = raw_data.get('network', None)
                if raw_network:
                    raw_fee = raw_network.get('transaction_fee', None)

                if raw_fee:
                    # Since this is a withdrawal the fee should be the same as the moved asset
                    if asset != asset_from_coinbase(raw_fee['currency'], time=timestamp):
                        # If not we set ZERO fee and ignore
                        log.error(
                            f'In a coinbase withdrawal of {asset.identifier} the fee'
                            f'is denoted in {raw_fee["currency"]}',
                        )
                    else:
                        fee = deserialize_fee(raw_fee['amount'])

                if 'network' in raw_data:
                    transaction_id = get_key_if_has_val(raw_data['network'], 'hash')
                if 'to' in raw_data:
                    address = deserialize_asset_movement_address(raw_data['to'], 'address', asset)
            else:
                movement_category = deserialize_asset_movement_category(raw_data['resource'])
                amount = deserialize_asset_amount_force_positive(raw_data['amount']['amount'])
                fee = deserialize_fee(raw_data['fee']['amount'])
                asset = asset_from_coinbase(raw_data['amount']['currency'], time=timestamp)

            return AssetMovement(
                location=Location.COINBASE,
                category=movement_category,
                address=address,
                transaction_id=transaction_id,
                timestamp=timestamp,
                asset=asset,
                amount=amount,
                fee_asset=asset,
                fee=fee,
                link=str(raw_data['id']),
            )
        except UnknownAsset as e:
            self.msg_aggregator.add_warning(
                f'Found coinbase deposit/withdrawal with unknown asset '
                f'{e.asset_name}. Ignoring it.',
            )
        except UnsupportedAsset as e:
            self.msg_aggregator.add_warning(
                f'Found coinbase deposit/withdrawal with unsupported asset '
                f'{e.asset_name}. Ignoring it.',
            )
        except (DeserializationError, KeyError) as e:
            msg = str(e)
            if isinstance(e, KeyError):
                msg = f'Missing key entry for {msg}.'
            self.msg_aggregator.add_error(
                'Unexpected data encountered during deserialization of a coinbase '
                'asset movement. Check logs for details and open a bug report.',
            )
            log.error(
                f'Unexpected data encountered during deserialization of coinbase '
                f'asset_movement {raw_data}. Error was: {str(e)}',
            )

        return None

    def query_online_deposits_withdrawals(
            self,
            start_ts: Timestamp,
            end_ts: Timestamp,
    ) -> List[AssetMovement]:
        account_data = self._api_query('accounts')
        account_ids = self._get_account_ids(account_data)
        raw_data = []
        for account_id in account_ids:
            raw_data.extend(self._api_query(f'accounts/{account_id}/deposits'))
            raw_data.extend(self._api_query(f'accounts/{account_id}/withdrawals'))
            # also get transactions to get the "sends", which in Coinbase is the
            # way to send Crypto out of the exchange
            txs = self._api_query(f'accounts/{account_id}/transactions')
            for tx in txs:
                if 'type' not in tx:
                    continue
                if tx['type'] == 'send':
                    raw_data.append(tx)

        log.debug('coinbase deposits/withdrawals history result', results_num=len(raw_data))

        movements = []
        for raw_movement in raw_data:
            movement = self._deserialize_asset_movement(raw_movement)
            # limit coinbase deposit/withdrawals in the requested time range
            #  here since there is no argument in the API call
            if movement and movement.timestamp >= start_ts and movement.timestamp <= end_ts:
                movements.append(movement)

        return movements

[((45, 9, 45, 36), 'logging.getLogger', 'logging.getLogger', ({(45, 27, 45, 35): '__name__'}, {}), '(__name__)', False, 'import logging\n'), ((46, 6, 46, 36), 'rotkehlchen.logging.RotkehlchenLogsAdapter', 'RotkehlchenLogsAdapter', ({(46, 29, 46, 35): 'logger'}, {}), '(logger)', False, 'from rotkehlchen.logging import RotkehlchenLogsAdapter\n'), ((69, 16, 69, 80), 'rotkehlchen.serialization.deserialize.deserialize_timestamp_from_date', 'deserialize_timestamp_from_date', ({(69, 48, 69, 56): 'raw_time', (69, 58, 69, 67): '"""iso8601"""', (69, 69, 69, 79): '"""coinbase"""'}, {}), "(raw_time, 'iso8601', 'coinbase')", False, 'from rotkehlchen.serialization.deserialize import deserialize_asset_amount, deserialize_asset_amount_force_positive, deserialize_asset_movement_category, deserialize_fee, deserialize_timestamp_from_date, deserialize_trade_type\n'), ((70, 17, 70, 62), 'rotkehlchen.serialization.deserialize.deserialize_trade_type', 'deserialize_trade_type', ({(70, 40, 70, 61): "raw_trade['resource']"}, {}), "(raw_trade['resource'])", False, 'from rotkehlchen.serialization.deserialize import deserialize_asset_amount, deserialize_asset_amount_force_positive, deserialize_asset_movement_category, deserialize_fee, deserialize_timestamp_from_date, deserialize_trade_type\n'), ((71, 16, 71, 71), 'rotkehlchen.serialization.deserialize.deserialize_asset_amount', 'deserialize_asset_amount', ({(71, 41, 71, 70): "raw_trade['amount']['amount']"}, {}), "(raw_trade['amount']['amount'])", False, 'from rotkehlchen.serialization.deserialize import deserialize_asset_amount, deserialize_asset_amount_force_positive, deserialize_asset_movement_category, deserialize_fee, deserialize_timestamp_from_date, deserialize_trade_type\n'), ((72, 15, 72, 83), 'rotkehlchen.assets.converters.asset_from_coinbase', 'asset_from_coinbase', (), '', False, 'from rotkehlchen.assets.converters import asset_from_coinbase\n'), ((73, 20, 73, 77), 'rotkehlchen.serialization.deserialize.deserialize_asset_amount', 'deserialize_asset_amount', ({(73, 45, 73, 76): "raw_trade['subtotal']['amount']"}, {}), "(raw_trade['subtotal']['amount'])", False, 'from rotkehlchen.serialization.deserialize import deserialize_asset_amount, deserialize_asset_amount_force_positive, deserialize_asset_movement_category, deserialize_fee, deserialize_timestamp_from_date, deserialize_trade_type\n'), ((74, 19, 74, 89), 'rotkehlchen.assets.converters.asset_from_coinbase', 'asset_from_coinbase', (), '', False, 'from rotkehlchen.assets.converters import asset_from_coinbase\n'), ((76, 11, 76, 72), 'rotkehlchen.typing.TradePair', 'TradePair', ({(76, 21, 76, 71): 'f"""{tx_asset.identifier}_{native_asset.identifier}"""'}, {}), "(f'{tx_asset.identifier}_{native_asset.identifier}')", False, 'from rotkehlchen.typing import ApiKey, ApiSecret, AssetMovementCategory, Fee, Location, Price, Timestamp, TradePair\n'), ((79, 11, 79, 43), 'rotkehlchen.typing.Price', 'Price', ({(79, 17, 79, 42): 'native_amount / tx_amount'}, {}), '(native_amount / tx_amount)', False, 'from rotkehlchen.typing import ApiKey, ApiSecret, AssetMovementCategory, Fee, Location, Price, Timestamp, TradePair\n'), ((80, 17, 80, 60), 'rotkehlchen.serialization.deserialize.deserialize_fee', 'deserialize_fee', ({(80, 33, 80, 59): "raw_trade['fee']['amount']"}, {}), "(raw_trade['fee']['amount'])", False, 'from rotkehlchen.serialization.deserialize import deserialize_asset_amount, deserialize_asset_amount_force_positive, deserialize_asset_movement_category, deserialize_fee, deserialize_timestamp_from_date, deserialize_trade_type\n'), ((81, 16, 81, 81), 'rotkehlchen.assets.converters.asset_from_coinbase', 'asset_from_coinbase', (), '', False, 'from rotkehlchen.assets.converters import asset_from_coinbase\n'), ((320, 5, 320, 24), 'rotkehlchen.utils.interfaces.protect_with_lock', 'protect_with_lock', ({}, {}), '()', False, 'from rotkehlchen.utils.interfaces import cache_response_timewise, protect_with_lock\n'), ((321, 5, 321, 30), 'rotkehlchen.utils.interfaces.cache_response_timewise', 'cache_response_timewise', ({}, {}), '()', False, 'from rotkehlchen.utils.interfaces import cache_response_timewise, protect_with_lock\n'), ((285, 18, 288, 13), 'rotkehlchen.errors.RemoteError', 'RemoteError', ({(286, 16, 287, 67): 'f"""Coinbase query {full_url} responded with error status code: {response.status_code} and text: {response.text}"""'}, {}), "(\n    f'Coinbase query {full_url} responded with error status code: {response.status_code} and text: {response.text}'\n    )", False, 'from rotkehlchen.errors import DeserializationError, RemoteError, UnknownAsset, UnsupportedAsset\n'), ((291, 23, 291, 56), 'rotkehlchen.utils.serialization.rlk_jsonloads_dict', 'rlk_jsonloads_dict', ({(291, 42, 291, 55): 'response.text'}, {}), '(response.text)', False, 'from rotkehlchen.utils.serialization import rlk_jsonloads_dict\n'), ((296, 18, 296, 95), 'rotkehlchen.errors.RemoteError', 'RemoteError', ({(296, 30, 296, 94): 'f"""Coinbase json response does not contain data: {response.text}"""'}, {}), "(f'Coinbase json response does not contain data: {response.text}')", False, 'from rotkehlchen.errors import DeserializationError, RemoteError, UnknownAsset, UnsupportedAsset\n'), ((255, 31, 255, 49), 'urllib.parse.urlencode', 'urlencode', ({(255, 41, 255, 48): 'options'}, {}), '(options)', False, 'from urllib.parse import urlencode\n'), ((257, 28, 257, 39), 'time.time', 'time.time', ({}, {}), '()', False, 'import time\n'), ((293, 18, 293, 90), 'rotkehlchen.errors.RemoteError', 'RemoteError', ({(293, 30, 293, 89): 'f"""Coinbase returned invalid JSON response: {response.text}"""'}, {}), "(f'Coinbase returned invalid JSON response: {response.text}')", False, 'from rotkehlchen.errors import DeserializationError, RemoteError, UnknownAsset, UnsupportedAsset\n'), ((303, 22, 303, 87), 'rotkehlchen.errors.RemoteError', 'RemoteError', ({(303, 34, 303, 86): '"""Coinbase json response contained no "next_uri" key"""'}, {}), '(\'Coinbase json response contained no "next_uri" key\')', False, 'from rotkehlchen.errors import DeserializationError, RemoteError, UnknownAsset, UnsupportedAsset\n'), ((339, 25, 339, 79), 'rotkehlchen.serialization.deserialize.deserialize_asset_amount', 'deserialize_asset_amount', ({(339, 50, 339, 78): "account['balance']['amount']"}, {}), "(account['balance']['amount'])", False, 'from rotkehlchen.serialization.deserialize import deserialize_asset_amount, deserialize_asset_amount_force_positive, deserialize_asset_movement_category, deserialize_fee, deserialize_timestamp_from_date, deserialize_trade_type\n'), ((346, 24, 346, 75), 'rotkehlchen.assets.converters.asset_from_coinbase', 'asset_from_coinbase', ({(346, 44, 346, 74): "account['balance']['currency']"}, {}), "(account['balance']['currency'])", False, 'from rotkehlchen.assets.converters import asset_from_coinbase\n'), ((462, 28, 462, 95), 'rotkehlchen.serialization.deserialize.deserialize_timestamp_from_date', 'deserialize_timestamp_from_date', ({(462, 60, 462, 71): 'payout_date', (462, 73, 462, 82): '"""iso8601"""', (462, 84, 462, 94): '"""coinbase"""'}, {}), "(payout_date, 'iso8601', 'coinbase')", False, 'from rotkehlchen.serialization.deserialize import deserialize_asset_amount, deserialize_asset_amount_force_positive, deserialize_asset_movement_category, deserialize_fee, deserialize_timestamp_from_date, deserialize_trade_type\n'), ((464, 28, 468, 17), 'rotkehlchen.serialization.deserialize.deserialize_timestamp_from_date', 'deserialize_timestamp_from_date', ({(465, 20, 465, 42): "raw_data['created_at']", (466, 20, 466, 29): '"""iso8601"""', (467, 20, 467, 30): '"""coinbase"""'}, {}), "(raw_data['created_at'], 'iso8601', 'coinbase')", False, 'from rotkehlchen.serialization.deserialize import deserialize_asset_amount, deserialize_asset_amount_force_positive, deserialize_asset_movement_category, deserialize_fee, deserialize_timestamp_from_date, deserialize_trade_type\n'), ((483, 25, 483, 94), 'rotkehlchen.serialization.deserialize.deserialize_asset_amount_force_positive', 'deserialize_asset_amount_force_positive', ({(483, 65, 483, 93): "raw_data['amount']['amount']"}, {}), "(raw_data['amount']['amount'])", False, 'from rotkehlchen.serialization.deserialize import deserialize_asset_amount, deserialize_asset_amount_force_positive, deserialize_asset_movement_category, deserialize_fee, deserialize_timestamp_from_date, deserialize_trade_type\n'), ((484, 24, 484, 91), 'rotkehlchen.assets.converters.asset_from_coinbase', 'asset_from_coinbase', (), '', False, 'from rotkehlchen.assets.converters import asset_from_coinbase\n'), ((487, 22, 487, 31), 'rotkehlchen.typing.Fee', 'Fee', ({(487, 26, 487, 30): 'ZERO'}, {}), '(ZERO)', False, 'from rotkehlchen.typing import ApiKey, ApiSecret, AssetMovementCategory, Fee, Location, Price, Timestamp, TradePair\n'), ((508, 36, 508, 93), 'rotkehlchen.serialization.deserialize.deserialize_asset_movement_category', 'deserialize_asset_movement_category', ({(508, 72, 508, 92): "raw_data['resource']"}, {}), "(raw_data['resource'])", False, 'from rotkehlchen.serialization.deserialize import deserialize_asset_amount, deserialize_asset_amount_force_positive, deserialize_asset_movement_category, deserialize_fee, deserialize_timestamp_from_date, deserialize_trade_type\n'), ((509, 25, 509, 94), 'rotkehlchen.serialization.deserialize.deserialize_asset_amount_force_positive', 'deserialize_asset_amount_force_positive', ({(509, 65, 509, 93): "raw_data['amount']['amount']"}, {}), "(raw_data['amount']['amount'])", False, 'from rotkehlchen.serialization.deserialize import deserialize_asset_amount, deserialize_asset_amount_force_positive, deserialize_asset_movement_category, deserialize_fee, deserialize_timestamp_from_date, deserialize_trade_type\n'), ((510, 22, 510, 64), 'rotkehlchen.serialization.deserialize.deserialize_fee', 'deserialize_fee', ({(510, 38, 510, 63): "raw_data['fee']['amount']"}, {}), "(raw_data['fee']['amount'])", False, 'from rotkehlchen.serialization.deserialize import deserialize_asset_amount, deserialize_asset_amount_force_positive, deserialize_asset_movement_category, deserialize_fee, deserialize_timestamp_from_date, deserialize_trade_type\n'), ((511, 24, 511, 91), 'rotkehlchen.assets.converters.asset_from_coinbase', 'asset_from_coinbase', (), '', False, 'from rotkehlchen.assets.converters import asset_from_coinbase\n'), ((504, 37, 504, 84), 'rotkehlchen.exchanges.utils.get_key_if_has_val', 'get_key_if_has_val', ({(504, 56, 504, 75): "raw_data['network']", (504, 77, 504, 83): '"""hash"""'}, {}), "(raw_data['network'], 'hash')", False, 'from rotkehlchen.exchanges.utils import deserialize_asset_movement_address, get_key_if_has_val\n'), ((506, 30, 506, 98), 'rotkehlchen.exchanges.utils.deserialize_asset_movement_address', 'deserialize_asset_movement_address', ({(506, 65, 506, 79): "raw_data['to']", (506, 81, 506, 90): '"""address"""', (506, 92, 506, 97): 'asset'}, {}), "(raw_data['to'], 'address', asset)", False, 'from rotkehlchen.exchanges.utils import deserialize_asset_movement_address, get_key_if_has_val\n'), ((494, 32, 494, 88), 'rotkehlchen.assets.converters.asset_from_coinbase', 'asset_from_coinbase', (), '', False, 'from rotkehlchen.assets.converters import asset_from_coinbase\n'), ((501, 30, 501, 64), 'rotkehlchen.serialization.deserialize.deserialize_fee', 'deserialize_fee', ({(501, 46, 501, 63): "raw_fee['amount']"}, {}), "(raw_fee['amount'])", False, 'from rotkehlchen.serialization.deserialize import deserialize_asset_amount, deserialize_asset_amount_force_positive, deserialize_asset_movement_category, deserialize_fee, deserialize_timestamp_from_date, deserialize_trade_type\n'), ((349, 32, 349, 42), 'rotkehlchen.inquirer.Inquirer', 'Inquirer', ({}, {}), '()', False, 'from rotkehlchen.inquirer import Inquirer\n')]