# -*- coding: utf-8 -*-

# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN:
# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code

from ccxt.async_support.base.exchange import Exchange
import hashlib
from ccxt.base.errors import ExchangeError
from ccxt.base.errors import AuthenticationError
from ccxt.base.errors import PermissionDenied
from ccxt.base.errors import AccountSuspended
from ccxt.base.errors import ArgumentsRequired
from ccxt.base.errors import BadRequest
from ccxt.base.errors import BadSymbol
from ccxt.base.errors import InsufficientFunds
from ccxt.base.errors import InvalidOrder
from ccxt.base.errors import OrderNotFound
from ccxt.base.errors import RateLimitExceeded
from ccxt.base.errors import ExchangeNotAvailable
from ccxt.base.errors import OnMaintenance
from ccxt.base.decimal_to_precision import TICK_SIZE
from ccxt.base.precise import Precise


class hitbtc3(Exchange):

    def describe(self):
        return self.deep_extend(super(hitbtc3, self).describe(), {
            'id': 'hitbtc3',
            'name': 'HitBTC',
            'countries': ['HK'],
            'rateLimit': 100,  # TODO: optimize https://api.hitbtc.com/#rate-limiting
            'version': '3',
            'pro': True,
            'has': {
                'cancelOrder': True,
                'CORS': False,
                'createOrder': True,
                'editOrder': True,
                'fetchBalance': True,
                'fetchClosedOrders': True,
                'fetchCurrencies': True,
                'fetchDepositAddress': True,
                'fetchDeposits': False,
                'fetchFundingRateHistory': True,
                'fetchMarkets': True,
                'fetchMyTrades': True,
                'fetchOHLCV': True,
                'fetchOpenOrder': True,
                'fetchOpenOrders': True,
                'fetchOrder': True,
                'fetchOrderBook': True,
                'fetchOrders': False,
                'fetchOrderTrades': True,
                'fetchTicker': True,
                'fetchTickers': True,
                'fetchTrades': True,
                'fetchTradingFee': True,
                'fetchTransactions': True,
                'fetchWithdrawals': False,
                'withdraw': True,
                'transfer': True,
            },
            'precisionMode': TICK_SIZE,
            'urls': {
                'logo': 'https://user-images.githubusercontent.com/1294454/27766555-8eaec20e-5edc-11e7-9c5b-6dc69fc42f5e.jpg',
                'test': {
                    'public': 'https://api.demo.hitbtc.com',
                    'private': 'https://api.demo.hitbtc.com',
                },
                'api': {
                    'public': 'https://api.hitbtc.com/api/3',
                    'private': 'https://api.hitbtc.com/api/3',
                },
                'www': 'https://hitbtc.com',
                'referral': 'https://hitbtc.com/?ref_id=5a5d39a65d466',
                'doc': [
                    'https://api.hitbtc.com',
                    'https://github.com/hitbtc-com/hitbtc-api/blob/master/APIv2.md',
                ],
                'fees': [
                    'https://hitbtc.com/fees-and-limits',
                    'https://support.hitbtc.com/hc/en-us/articles/115005148605-Fees-and-limits',
                ],
            },
            'api': {
                'public': {
                    'get': [
                        'public/currency',
                        'public/symbol',
                        'public/ticker',
                        'public/price/rate',
                        'public/trades',
                        'public/orderbook',
                        'public/candles',
                        'public/futures/info',
                        'public/futures/history/funding',
                        'public/futures/candles/index_price',
                        'public/futures/candles/mark_price',
                        'public/futures/candles/premium_index',
                        'public/futures/candles/open_interest',
                    ],
                },
                'private': {
                    'get': [
                        'spot/balance',
                        'spot/order',
                        'spot/order/{client_order_id}',
                        'spot/fee',
                        'spot/fee/{symbol}',
                        'spot/history/order',
                        'spot/history/trade',
                        'margin/account',
                        'margin/account/isolated/{symbol}',
                        'margin/order',
                        'margin/order/{client_order_id}',
                        'margin/history/order',
                        'margin/history/trade',
                        'futures/balance',
                        'futures/account',
                        'futures/account/isolated/{symbol}',
                        'futures/order',
                        'futures/order/{client_order_id}',
                        'futures/fee',
                        'futures/fee/{symbol}',
                        'futures/history/order',
                        'futures/history/trade',
                        'wallet/balance',
                        'wallet/crypto/address',
                        'wallet/crypto/address/recent-deposit',
                        'wallet/crypto/address/recent-withdraw',
                        'wallet/crypto/address/check-mine',
                        'wallet/transactions',
                        'wallet/crypto/check-offchain-available',
                        'wallet/crypto/fee/estimate',
                        'sub-account',
                        'sub-account/acl',
                        'sub-account/balance/{subAccID}',
                        'sub-account/crypto/address/{subAccID}/{currency}',
                    ],
                    'post': [
                        'spot/order',
                        'margin/order',
                        'futures/order',
                        'wallet/convert',
                        'wallet/crypto/withdraw',
                        'wallet/transfer',
                        'sub-account/freeze',
                        'sub-account/activate',
                        'sub-account/transfer',
                        'sub-account/acl',
                    ],
                    'patch': [
                        'spot/order/{client_order_id}',
                        'margin/order/{client_order_id}',
                        'futures/order/{client_order_id}',
                    ],
                    'delete': [
                        'spot/order',
                        'spot/order/{client_order_id}',
                        'margin/position',
                        'margin/position/isolated/{symbol}',
                        'margin/order',
                        'margin/order/{client_order_id}',
                        'futures/position',
                        'futures/position/isolated/{symbol}',
                        'futures/order',
                        'futures/order/{client_order_id}',
                        'wallet/crypto/withdraw/{id}',
                    ],
                    'put': [
                        'margin/account/isolated/{symbol}',
                        'futures/account/isolated/{symbol}',
                        'wallet/crypto/withdraw/{id}',
                    ],
                },
            },
            'fees': {
                'trading': {
                    'tierBased': True,
                    'percentage': True,
                    'taker': self.parse_number('0.0009'),
                    'maker': self.parse_number('0.0009'),
                    'tiers': {
                        'maker': [
                            [self.parse_number('0'), self.parse_number('0.0009')],
                            [self.parse_number('10'), self.parse_number('0.0007')],
                            [self.parse_number('100'), self.parse_number('0.0006')],
                            [self.parse_number('500'), self.parse_number('0.0005')],
                            [self.parse_number('1000'), self.parse_number('0.0003')],
                            [self.parse_number('5000'), self.parse_number('0.0002')],
                            [self.parse_number('10000'), self.parse_number('0.0001')],
                            [self.parse_number('20000'), self.parse_number('0')],
                            [self.parse_number('50000'), self.parse_number('-0.0001')],
                            [self.parse_number('100000'), self.parse_number('-0.0001')],
                        ],
                        'taker': [
                            [self.parse_number('0'), self.parse_number('0.0009')],
                            [self.parse_number('10'), self.parse_number('0.0008')],
                            [self.parse_number('100'), self.parse_number('0.0007')],
                            [self.parse_number('500'), self.parse_number('0.0007')],
                            [self.parse_number('1000'), self.parse_number('0.0006')],
                            [self.parse_number('5000'), self.parse_number('0.0006')],
                            [self.parse_number('10000'), self.parse_number('0.0005')],
                            [self.parse_number('20000'), self.parse_number('0.0004')],
                            [self.parse_number('50000'), self.parse_number('0.0003')],
                            [self.parse_number('100000'), self.parse_number('0.0002')],
                        ],
                    },
                },
            },
            'timeframes': {
                '1m': 'M1',
                '3m': 'M3',
                '5m': 'M5',
                '15m': 'M15',
                '30m': 'M30',  # default
                '1h': 'H1',
                '4h': 'H4',
                '1d': 'D1',
                '1w': 'D7',
                '1M': '1M',
            },
            'exceptions': {
                'exact': {
                    '429': RateLimitExceeded,
                    '500': ExchangeError,
                    '503': ExchangeNotAvailable,
                    '504': ExchangeNotAvailable,
                    '600': PermissionDenied,
                    '800': ExchangeError,
                    '1002': AuthenticationError,
                    '1003': PermissionDenied,
                    '1004': AuthenticationError,
                    '1005': AuthenticationError,
                    '2001': BadSymbol,
                    '2002': BadRequest,
                    '2003': BadRequest,
                    '2010': BadRequest,
                    '2011': BadRequest,
                    '2012': BadRequest,
                    '2020': BadRequest,
                    '2022': BadRequest,
                    '10001': BadRequest,
                    '10021': AccountSuspended,
                    '10022': BadRequest,
                    '20001': InsufficientFunds,
                    '20002': OrderNotFound,
                    '20003': ExchangeError,
                    '20004': ExchangeError,
                    '20005': ExchangeError,
                    '20006': ExchangeError,
                    '20007': ExchangeError,
                    '20008': InvalidOrder,
                    '20009': InvalidOrder,
                    '20010': OnMaintenance,
                    '20011': ExchangeError,
                    '20012': ExchangeError,
                    '20014': ExchangeError,
                    '20016': ExchangeError,
                    '20031': ExchangeError,
                    '20032': ExchangeError,
                    '20033': ExchangeError,
                    '20034': ExchangeError,
                    '20040': ExchangeError,
                    '20041': ExchangeError,
                    '20042': ExchangeError,
                    '20043': ExchangeError,
                    '20044': PermissionDenied,
                    '20045': ExchangeError,
                    '20080': ExchangeError,
                    '21001': ExchangeError,
                    '21003': AccountSuspended,
                    '21004': AccountSuspended,
                },
                'broad': {},
            },
            'options': {
                'networks': {
                    'ETH': 'USDT20',
                    'ERC20': 'USDT20',
                    'TRX': 'USDTRX',
                    'TRC20': 'USDTRX',
                    'OMNI': 'USDT',
                },
                'accountsByType': {
                    'spot': 'spot',
                    'wallet': 'wallet',
                    'derivatives': 'derivatives',
                },
            },
        })

    def nonce(self):
        return self.milliseconds()

    async def fetch_markets(self, params={}):
        response = await self.publicGetPublicSymbol(params)
        #
        #     {
        #         "AAVEUSDT_PERP":{
        #             "type":"futures",
        #             "expiry":null,
        #             "underlying":"AAVE",
        #             "base_currency":null,
        #             "quote_currency":"USDT",
        #             "quantity_increment":"0.01",
        #             "tick_size":"0.001",
        #             "take_rate":"0.0005",
        #             "make_rate":"0.0002",
        #             "fee_currency":"USDT",
        #             "margin_trading":true,
        #             "max_initial_leverage":"50.00"
        #         },
        #         "MANAUSDT":{
        #             "type":"spot",
        #             "base_currency":"MANA",
        #             "quote_currency":"USDT",
        #             "quantity_increment":"1",
        #             "tick_size":"0.0000001",
        #             "take_rate":"0.0025",
        #             "make_rate":"0.001",
        #             "fee_currency":"USDT",
        #             "margin_trading":true,
        #             "max_initial_leverage":"5.00"
        #         },
        #     }
        #
        result = []
        ids = list(response.keys())
        for i in range(0, len(ids)):
            id = ids[i]
            market = self.safe_value(response, id)
            marketType = self.safe_string(market, 'type')
            expiry = self.safe_integer(market, 'expiry')
            contract = (marketType == 'futures')
            spot = (marketType == 'spot')
            marginTrading = self.safe_value(market, 'margin_trading', False)
            margin = spot and marginTrading
            future = (expiry is not None)
            swap = (contract and not future)
            option = False
            baseId = self.safe_string_2(market, 'base_currency', 'underlying')
            quoteId = self.safe_string(market, 'quote_currency')
            feeCurrencyId = self.safe_string(market, 'fee_currency')
            base = self.safe_currency_code(baseId)
            quote = self.safe_currency_code(quoteId)
            feeCurrency = self.safe_currency_code(feeCurrencyId)
            settleId = None
            settle = None
            symbol = base + '/' + quote
            type = 'spot'
            contractSize = None
            linear = None
            inverse = None
            if contract:
                contractSize = self.parse_number('1')
                settleId = feeCurrencyId
                settle = self.safe_currency_code(settleId)
                linear = ((quote is not None) and (quote == settle))
                inverse = not linear
                symbol = symbol + ':' + settle
                if future:
                    symbol = symbol + '-' + expiry
                    type = 'future'
                else:
                    type = 'swap'
            lotString = self.safe_string(market, 'quantity_increment')
            stepString = self.safe_string(market, 'tick_size')
            lot = self.parse_number(lotString)
            step = self.parse_number(stepString)
            taker = self.safe_number(market, 'take_rate')
            maker = self.safe_number(market, 'make_rate')
            result.append({
                'id': id,
                'symbol': symbol,
                'base': base,
                'quote': quote,
                'settle': settle,
                'baseId': baseId,
                'quoteId': quoteId,
                'settleId': settleId,
                'type': type,
                'spot': spot,
                'margin': margin,
                'swap': swap,
                'future': future,
                'option': option,
                'contract': contract,
                'linear': linear,
                'inverse': inverse,
                'taker': taker,
                'maker': maker,
                'contractSize': contractSize,
                'active': True,
                'expiry': expiry,
                'expiryDatetime': None,
                'strike': None,
                'optionType': None,
                'feeCurrency': feeCurrency,
                'precision': {
                    'price': step,
                    'amount': lot,
                },
                'limits': {
                    'leverage': {
                        'min': 1,
                        'max': self.safe_number(market, 'max_initial_leverage', 1),
                    },
                    'amount': {
                        'min': lot,
                        'max': None,
                    },
                    'price': {
                        'min': step,
                        'max': None,
                    },
                    'cost': {
                        'min': self.parse_number(Precise.string_mul(lotString, stepString)),
                        'max': None,
                    },
                },
                'info': market,
            })
        return result

    async def fetch_currencies(self, params={}):
        response = await self.publicGetPublicCurrency(params)
        #
        #     {
        #       "WEALTH": {
        #         "full_name": "ConnectWealth",
        #         "payin_enabled": False,
        #         "payout_enabled": False,
        #         "transfer_enabled": True,
        #         "precision_transfer": "0.001",
        #         "networks": [
        #           {
        #             "network": "ETH",
        #             "protocol": "ERC20",
        #             "default": True,
        #             "payin_enabled": False,
        #             "payout_enabled": False,
        #             "precision_payout": "0.001",
        #             "payout_fee": "0.016800000000",
        #             "payout_is_payment_id": False,
        #             "payin_payment_id": False,
        #             "payin_confirmations": "2"
        #           }
        #         ]
        #       }
        #     }
        #
        result = {}
        currencies = list(response.keys())
        for i in range(0, len(currencies)):
            currencyId = currencies[i]
            code = self.safe_currency_code(currencyId)
            entry = response[currencyId]
            name = self.safe_string(entry, 'full_name')
            precision = self.safe_number(entry, 'precision_transfer')
            payinEnabled = self.safe_value(entry, 'payin_enabled', False)
            payoutEnabled = self.safe_value(entry, 'payout_enabled', False)
            transferEnabled = self.safe_value(entry, 'transfer_enabled', False)
            active = payinEnabled and payoutEnabled and transferEnabled
            rawNetworks = self.safe_value(entry, 'networks', [])
            networks = {}
            fee = None
            for j in range(0, len(rawNetworks)):
                rawNetwork = rawNetworks[j]
                networkId = self.safe_string(rawNetwork, 'protocol')
                if len(networkId) == 0:
                    networkId = self.safe_string(rawNetwork, 'network')
                network = self.safe_network(networkId)
                fee = self.safe_number(rawNetwork, 'payout_fee')
                precision = self.safe_number(rawNetwork, 'precision_payout')
                payinEnabledNetwork = self.safe_value(entry, 'payin_enabled', False)
                payoutEnabledNetwork = self.safe_value(entry, 'payout_enabled', False)
                transferEnabledNetwork = self.safe_value(entry, 'transfer_enabled', False)
                active = payinEnabledNetwork and payoutEnabledNetwork and transferEnabledNetwork
                networks[network] = {
                    'info': rawNetwork,
                    'id': networkId,
                    'network': network,
                    'fee': fee,
                    'active': active,
                    'precision': precision,
                    'limits': {
                        'withdraw': {
                            'min': None,
                            'max': None,
                        },
                    },
                }
            networksKeys = list(networks.keys())
            networksLength = len(networksKeys)
            result[code] = {
                'info': entry,
                'code': code,
                'id': currencyId,
                'precision': precision,
                'name': name,
                'active': active,
                'networks': networks,
                'fee': fee if (networksLength <= 1) else None,
                'limits': {
                    'amount': {
                        'min': None,
                        'max': None,
                    },
                },
            }
        return result

    def safe_network(self, networkId):
        if networkId is None:
            return None
        else:
            return networkId.upper()

    async def fetch_deposit_address(self, code, params={}):
        await self.load_markets()
        currency = self.currency(code)
        request = {
            'currency': currency['id'],
        }
        network = self.safe_string_upper(params, 'network')
        if (network is not None) and (code == 'USDT'):
            networks = self.safe_value(self.options, 'networks')
            parsedNetwork = self.safe_string(networks, network)
            if parsedNetwork is not None:
                request['currency'] = parsedNetwork
            params = self.omit(params, 'network')
        response = await self.privateGetWalletCryptoAddress(self.extend(request, params))
        #
        #  [{"currency":"ETH","address":"0xd0d9aea60c41988c3e68417e2616065617b7afd3"}]
        #
        firstAddress = self.safe_value(response, 0)
        address = self.safe_string(firstAddress, 'address')
        currencyId = self.safe_string(firstAddress, 'currency')
        tag = self.safe_string(firstAddress, 'payment_id')
        parsedCode = self.safe_currency_code(currencyId)
        return {
            'info': response,
            'address': address,
            'tag': tag,
            'code': parsedCode,
            'network': None,
        }

    def parse_balance(self, response):
        result = {'info': response}
        for i in range(0, len(response)):
            entry = response[i]
            currencyId = self.safe_string(entry, 'currency')
            code = self.safe_currency_code(currencyId)
            account = self.account()
            account['free'] = self.safe_string(entry, 'available')
            account['used'] = self.safe_string(entry, 'reserved')
            result[code] = account
        return self.safe_balance(result)

    async def fetch_balance(self, params={}):
        type = self.safe_string_lower(params, 'type', 'spot')
        params = self.omit(params, ['type'])
        accountsByType = self.safe_value(self.options, 'accountsByType', {})
        account = self.safe_string(accountsByType, type)
        response = None
        if account == 'wallet':
            response = await self.privateGetWalletBalance(params)
        elif account == 'spot':
            response = await self.privateGetSpotBalance(params)
        elif account == 'derivatives':
            response = await self.privateGetFuturesBalance(params)
        else:
            keys = list(accountsByType.keys())
            raise BadRequest(self.id + ' fetchBalance() type parameter must be one of ' + ', '.join(keys))
        #
        #     [
        #       {
        #         "currency": "PAXG",
        #         "available": "0",
        #         "reserved": "0",
        #         "reserved_margin": "0",
        #       },
        #       ...
        #     ]
        #
        return self.parse_balance(response)

    async def fetch_ticker(self, symbol, params={}):
        response = await self.fetch_tickers([symbol], params)
        return self.safe_value(response, symbol)

    async def fetch_tickers(self, symbols=None, params={}):
        await self.load_markets()
        request = {}
        if symbols is not None:
            marketIds = self.market_ids(symbols)
            delimited = ','.join(marketIds)
            request['symbols'] = delimited
        response = await self.publicGetPublicTicker(self.extend(request, params))
        #
        #     {
        #       "BTCUSDT": {
        #         "ask": "63049.06",
        #         "bid": "63046.41",
        #         "last": "63048.36",
        #         "low": "62010.00",
        #         "high": "66657.99",
        #         "open": "64839.75",
        #         "volume": "15272.13278",
        #         "volume_quote": "976312127.6277998",
        #         "timestamp": "2021-10-22T04:25:47.573Z"
        #       }
        #     }
        #
        result = {}
        keys = list(response.keys())
        for i in range(0, len(keys)):
            marketId = keys[i]
            market = self.safe_market(marketId)
            symbol = market['symbol']
            entry = response[marketId]
            result[symbol] = self.parse_ticker(entry, market)
        return self.filter_by_array(result, 'symbol', symbols)

    def parse_ticker(self, ticker, market=None):
        #
        #     {
        #       "ask": "62756.01",
        #       "bid": "62754.09",
        #       "last": "62755.87",
        #       "low": "62010.00",
        #       "high": "66657.99",
        #       "open": "65089.27",
        #       "volume": "16719.50366",
        #       "volume_quote": "1063422878.8156828",
        #       "timestamp": "2021-10-22T07:29:14.585Z"
        #     }
        #
        timestamp = self.parse8601(ticker['timestamp'])
        symbol = self.safe_symbol(None, market)
        baseVolume = self.safe_number(ticker, 'volume')
        quoteVolume = self.safe_number(ticker, 'volume_quote')
        open = self.safe_number(ticker, 'open')
        last = self.safe_number(ticker, 'last')
        vwap = self.vwap(baseVolume, quoteVolume)
        return self.safe_ticker({
            'symbol': symbol,
            'timestamp': timestamp,
            'datetime': self.iso8601(timestamp),
            'high': self.safe_number(ticker, 'high'),
            'low': self.safe_number(ticker, 'low'),
            'bid': self.safe_number(ticker, 'bid'),
            'bidVolume': None,
            'ask': self.safe_number(ticker, 'ask'),
            'askVolume': None,
            'vwap': vwap,
            'open': open,
            'close': last,
            'last': last,
            'previousClose': None,
            'change': None,
            'percentage': None,
            'average': None,
            'baseVolume': baseVolume,
            'quoteVolume': quoteVolume,
            'info': ticker,
        }, market)

    async def fetch_trades(self, symbol, since=None, limit=None, params={}):
        await self.load_markets()
        market = None
        request = {}
        if symbol is not None:
            market = self.market(symbol)
            # symbol is optional for hitbtc fetchTrades
            request['symbols'] = market['id']
        if limit is not None:
            request['limit'] = limit
        if since is not None:
            request['since'] = since
        response = await self.publicGetPublicTrades(self.extend(request, params))
        marketIds = list(response.keys())
        trades = []
        for i in range(0, len(marketIds)):
            marketId = marketIds[i]
            market = self.market(marketId)
            rawTrades = response[marketId]
            parsed = self.parse_trades(rawTrades, market)
            trades = self.array_concat(trades, parsed)
        return trades

    async def fetch_my_trades(self, symbol=None, since=None, limit=None, params={}):
        await self.load_markets()
        market = None
        request = {}
        if symbol is not None:
            market = self.market(symbol)
            request['symbol'] = market['id']
        if limit is not None:
            request['limit'] = limit
        if since is not None:
            request['since'] = since
        response = await self.privateGetSpotHistoryTrade(self.extend(request, params))
        return self.parse_trades(response, market, since, limit)

    def parse_trade(self, trade, market=None):
        # createMarketOrder
        #
        #  {      fee: "0.0004644",
        #           id:  386394956,
        #        price: "0.4644",
        #     quantity: "1",
        #    timestamp: "2018-10-25T16:41:44.780Z"}
        #
        # fetchTrades
        #
        # {id: 974786185,
        #   price: '0.032462',
        #   qty: '0.3673',
        #   side: 'buy',
        #   timestamp: '2020-10-16T12:57:39.846Z'}
        #
        # fetchMyTrades
        #
        # {id: 277210397,
        #   clientOrderId: '6e102f3e7f3f4e04aeeb1cdc95592f1a',
        #   orderId: 28102855393,
        #   symbol: 'ETHBTC',
        #   side: 'sell',
        #   quantity: '0.002',
        #   price: '0.073365',
        #   fee: '0.000000147',
        #   timestamp: '2018-04-28T18:39:55.345Z',
        #   taker: True}
        #
        timestamp = self.parse8601(trade['timestamp'])
        marketId = self.safe_string(trade, 'symbol')
        market = self.safe_market(marketId, market)
        symbol = market['symbol']
        fee = None
        feeCost = self.safe_number(trade, 'fee')
        taker = self.safe_value(trade, 'taker')
        takerOrMaker = None
        if taker is not None:
            takerOrMaker = 'taker' if taker else 'maker'
        if feeCost is not None:
            info = self.safe_value(market, 'info', {})
            feeCurrency = self.safe_string(info, 'fee_currency')
            feeCurrencyCode = self.safe_currency_code(feeCurrency)
            fee = {
                'cost': feeCost,
                'currency': feeCurrencyCode,
            }
        # we use clientOrderId as the order id with self exchange intentionally
        # because most of their endpoints will require clientOrderId
        # explained here: https://github.com/ccxt/ccxt/issues/5674
        orderId = self.safe_string(trade, 'clientOrderId')
        priceString = self.safe_string(trade, 'price')
        amountString = self.safe_string_2(trade, 'quantity', 'qty')
        price = self.parse_number(priceString)
        amount = self.parse_number(amountString)
        cost = self.parse_number(Precise.string_mul(priceString, amountString))
        side = self.safe_string(trade, 'side')
        id = self.safe_string(trade, 'id')
        return {
            'info': trade,
            'id': id,
            'order': orderId,
            'timestamp': timestamp,
            'datetime': self.iso8601(timestamp),
            'symbol': symbol,
            'type': None,
            'side': side,
            'takerOrMaker': takerOrMaker,
            'price': price,
            'amount': amount,
            'cost': cost,
            'fee': fee,
        }

    async def fetch_transactions_helper(self, types, code, since, limit, params):
        await self.load_markets()
        request = {
            'types': types,
        }
        currency = None
        if code is not None:
            currency = self.currency(code)
            request['currencies'] = currency['id']
        if since is not None:
            request['from'] = self.iso8601(since)
        if limit is not None:
            request['limit'] = limit
        response = await self.privateGetWalletTransactions(self.extend(request, params))
        #
        #     [
        #       {
        #         "id": "101609495",
        #         "created_at": "2018-03-06T22:05:06.507Z",
        #         "updated_at": "2018-03-06T22:11:45.03Z",
        #         "status": "SUCCESS",
        #         "type": "DEPOSIT",
        #         "subtype": "BLOCKCHAIN",
        #         "native": {
        #           "tx_id": "e20b0965-4024-44d0-b63f-7fb8996a6706",
        #           "index": "881652766",
        #           "currency": "ETH",
        #           "amount": "0.01418088",
        #           "hash": "d95dbbff3f9234114f1211ab0ba2a94f03f394866fd5749d74a1edab80e6c5d3",
        #           "address": "0xd9259302c32c0a0295d86a39185c9e14f6ba0a0d",
        #           "confirmations": "20",
        #           "senders": [
        #             "0x243bec9256c9a3469da22103891465b47583d9f1"
        #           ]
        #         }
        #       }
        #     ]
        #
        return self.parse_transactions(response, currency, since, limit, params)

    def parse_transaction_status(self, status):
        statuses = {
            'PENDING': 'pending',
            'FAILED': 'failed',
            'SUCCESS': 'ok',
        }
        return self.safe_string(statuses, status, status)

    def parse_transaction_type(self, type):
        types = {
            'DEPOSIT': 'deposit',
            'WITHDRAW': 'withdrawal',
        }
        return self.safe_string(types, type, type)

    def parse_transaction(self, transaction, currency=None):
        #
        #     {
        #       "id": "101609495",
        #       "created_at": "2018-03-06T22:05:06.507Z",
        #       "updated_at": "2018-03-06T22:11:45.03Z",
        #       "status": "SUCCESS",
        #       "type": "DEPOSIT",
        #       "subtype": "BLOCKCHAIN",
        #       "native": {
        #         "tx_id": "e20b0965-4024-44d0-b63f-7fb8996a6706",
        #         "index": "881652766",
        #         "currency": "ETH",
        #         "amount": "0.01418088",
        #         "hash": "d95dbbff3f9234114f1211ab0ba2a94f03f394866fd5749d74a1edab80e6c5d3",
        #         "address": "0xd9259302c32c0a0295d86a39185c9e14f6ba0a0d",
        #         "confirmations": "20",
        #         "senders": [
        #           "0x243bec9256c9a3469da22103891465b47583d9f1"
        #         ]
        #       }
        #     }
        #
        #     {
        #       "id": "102703545",
        #       "created_at": "2018-03-30T21:39:17.854Z",
        #       "updated_at": "2018-03-31T00:23:19.067Z",
        #       "status": "SUCCESS",
        #       "type": "WITHDRAW",
        #       "subtype": "BLOCKCHAIN",
        #       "native": {
        #         "tx_id": "5ecd7a85-ce5d-4d52-a916-b8b755e20926",
        #         "index": "918286359",
        #         "currency": "OMG",
        #         "amount": "2.45",
        #         "fee": "1.22",
        #         "hash": "0x1c621d89e7a0841342d5fb3b3587f60b95351590161e078c4a1daee353da4ca9",
        #         "address": "0x50227da7644cea0a43258a2e2d7444d01b43dcca",
        #         "confirmations": "0"
        #       }
        #     }
        #
        id = self.safe_string(transaction, 'id')
        timestamp = self.parse8601(self.safe_string(transaction, 'created_at'))
        updated = self.parse8601(self.safe_string(transaction, 'updated_at'))
        type = self.parse_transaction_type(self.safe_string(transaction, 'type'))
        status = self.parse_transaction_status(self.safe_string(transaction, 'status'))
        native = self.safe_value(transaction, 'native')
        currencyId = self.safe_string(native, 'currency')
        code = self.safe_currency_code(currencyId)
        txhash = self.safe_string(native, 'hash')
        address = self.safe_string(native, 'address')
        addressTo = address
        tag = self.safe_string(native, 'payment_id')
        tagTo = tag
        sender = self.safe_value(native, 'senders')
        addressFrom = self.safe_string(sender, 0)
        amount = self.safe_number(native, 'amount')
        fee = None
        feeCost = self.safe_number(native, 'fee')
        if feeCost is not None:
            fee = {
                'code': code,
                'cost': feeCost,
            }
        return {
            'info': transaction,
            'id': id,
            'txid': txhash,
            'code': code,
            'amount': amount,
            'network': None,
            'address': address,
            'addressFrom': addressFrom,
            'addressTo': addressTo,
            'tag': tag,
            'tagFrom': None,
            'tagTo': tagTo,
            'timestamp': timestamp,
            'datetime': self.iso8601(timestamp),
            'updated': updated,
            'status': status,
            'type': type,
            'fee': fee,
        }

    async def fetch_transactions(self, code=None, since=None, limit=None, params={}):
        return await self.fetch_transactions_helper('DEPOSIT,WITHDRAW', code, since, limit, params)

    async def fetch_deposits(self, code=None, since=None, limit=None, params={}):
        return await self.fetch_transactions_helper('DEPOSIT', code, since, limit, params)

    async def fetch_withdrawals(self, code=None, since=None, limit=None, params={}):
        return await self.fetch_transactions_helper('WITHDRAW', code, since, limit, params)

    async def fetch_order_books(self, symbols=None, limit=None, params={}):
        await self.load_markets()
        request = {}
        if symbols is not None:
            marketIds = self.market_ids(symbols)
            request['symbols'] = ','.join(marketIds)
        if limit is not None:
            request['depth'] = limit
        response = await self.publicGetPublicOrderbook(self.extend(request, params))
        result = {}
        marketIds = list(response.keys())
        for i in range(0, len(marketIds)):
            marketId = marketIds[i]
            orderbook = response[marketId]
            symbol = self.safe_symbol(marketId)
            timestamp = self.parse8601(self.safe_string(orderbook, 'timestamp'))
            result[symbol] = self.parse_order_book(response[marketId], symbol, timestamp, 'bid', 'ask')
        return result

    async def fetch_order_book(self, symbol, limit=None, params={}):
        result = await self.fetch_order_books([symbol], limit, params)
        return result[symbol]

    def parse_trading_fee(self, fee, market=None):
        #
        #     {
        #         "symbol":"ARVUSDT",  # returned from fetchTradingFees only
        #         "take_rate":"0.0009",
        #         "make_rate":"0.0009"
        #     }
        #
        taker = self.safe_number(fee, 'take_rate')
        maker = self.safe_number(fee, 'make_rate')
        marketId = self.safe_string(fee, 'symbol')
        symbol = self.safe_symbol(marketId, market)
        return {
            'info': fee,
            'symbol': symbol,
            'taker': taker,
            'maker': maker,
        }

    async def fetch_trading_fee(self, symbol, params={}):
        await self.load_markets()
        market = self.market(symbol)
        request = {
            'symbol': market['id'],
        }
        response = await self.privateGetSpotFeeSymbol(self.extend(request, params))
        #
        #     {
        #         "take_rate":"0.0009",
        #         "make_rate":"0.0009"
        #     }
        #
        return self.parse_trading_fee(response, market)

    async def fetch_trading_fees(self, symbols=None, params={}):
        await self.load_markets()
        response = await self.privateGetSpotFee(params)
        #
        #     [
        #         {
        #             "symbol":"ARVUSDT",
        #             "take_rate":"0.0009",
        #             "make_rate":"0.0009"
        #         }
        #     ]
        #
        result = {}
        for i in range(0, len(response)):
            fee = self.parse_trading_fee(response[i])
            symbol = fee['symbol']
            result[symbol] = fee
        return result

    async def fetch_ohlcv(self, symbol, timeframe='1m', since=None, limit=None, params={}):
        await self.load_markets()
        market = self.market(symbol)
        request = {
            'symbols': market['id'],
            'period': self.timeframes[timeframe],
        }
        if since is not None:
            request['from'] = self.iso8601(since)
        if limit is not None:
            request['limit'] = limit
        response = await self.publicGetPublicCandles(self.extend(request, params))
        #
        #     {
        #       "ETHUSDT": [
        #         {
        #           "timestamp": "2021-10-25T07:38:00.000Z",
        #           "open": "4173.391",
        #           "close": "4170.923",
        #           "min": "4170.923",
        #           "max": "4173.986",
        #           "volume": "0.1879",
        #           "volume_quote": "784.2517846"
        #         }
        #       ]
        #     }
        #
        ohlcvs = self.safe_value(response, market['id'])
        return self.parse_ohlcvs(ohlcvs, market, timeframe, since, limit)

    def parse_ohlcv(self, ohlcv, market=None):
        #
        #     {
        #         "timestamp":"2015-08-20T19:01:00.000Z",
        #         "open":"0.006",
        #         "close":"0.006",
        #         "min":"0.006",
        #         "max":"0.006",
        #         "volume":"0.003",
        #         "volume_quote":"0.000018"
        #     }
        #
        return [
            self.parse8601(self.safe_string(ohlcv, 'timestamp')),
            self.safe_number(ohlcv, 'open'),
            self.safe_number(ohlcv, 'max'),
            self.safe_number(ohlcv, 'min'),
            self.safe_number(ohlcv, 'close'),
            self.safe_number(ohlcv, 'volume'),
        ]

    async def fetch_closed_orders(self, symbol=None, since=None, limit=None, params={}):
        await self.load_markets()
        market = None
        request = {}
        if symbol is not None:
            market = self.market(symbol)
            request['symbol'] = market['id']
        if since is not None:
            request['from'] = self.iso8601(since)
        if limit is not None:
            request['limit'] = limit
        response = await self.privateGetSpotHistoryOrder(self.extend(request, params))
        parsed = self.parse_orders(response, market, since, limit)
        return self.filter_by_array(parsed, 'status', ['closed', 'canceled'], False)

    async def fetch_order(self, id, symbol=None, params={}):
        await self.load_markets()
        market = None
        if symbol is not None:
            market = self.market(symbol)
        request = {
            'client_order_id': id,
        }
        response = await self.privateGetSpotHistoryOrder(self.extend(request, params))
        #
        #     [
        #       {
        #         "id": "685965182082",
        #         "client_order_id": "B3CBm9uGg9oYQlw96bBSEt38-6gbgBO0",
        #         "symbol": "BTCUSDT",
        #         "side": "buy",
        #         "status": "new",
        #         "type": "limit",
        #         "time_in_force": "GTC",
        #         "quantity": "0.00010",
        #         "quantity_cumulative": "0",
        #         "price": "50000.00",
        #         "price_average": "0",
        #         "created_at": "2021-10-26T11:40:09.287Z",
        #         "updated_at": "2021-10-26T11:40:09.287Z"
        #       }
        #     ]
        #
        order = self.safe_value(response, 0)
        return self.parse_order(order, market)

    async def fetch_order_trades(self, id, symbol=None, since=None, limit=None, params={}):
        await self.load_markets()
        market = None
        if symbol is not None:
            market = self.market(symbol)
        request = {
            'order_id': id,  # exchange assigned order id as oppose to the client order id
        }
        response = await self.privateGetSpotHistoryTrade(self.extend(request, params))
        #
        #     [
        #       {
        #         "id": 1393448977,
        #         "order_id": 653496804534,
        #         "client_order_id": "065f6f0ff9d54547848454182263d7b4",
        #         "symbol": "DICEETH",
        #         "side": "buy",
        #         "quantity": "1.4",
        #         "price": "0.00261455",
        #         "fee": "0.000003294333",
        #         "timestamp": "2021-09-19T05:35:56.601Z",
        #         "taker": True
        #       }
        #     ]
        #
        return self.parse_trades(response, market, since, limit)

    async def fetch_open_orders(self, symbol=None, since=None, limit=None, params={}):
        await self.load_markets()
        market = None
        request = {}
        if symbol is not None:
            market = self.market(symbol)
            request['symbol'] = market['id']
        response = await self.privateGetSpotOrder(self.extend(request, params))
        #
        #     [
        #       {
        #         "id": "488953123149",
        #         "client_order_id": "103ad305301e4c3590045b13de15b36e",
        #         "symbol": "BTCUSDT",
        #         "side": "buy",
        #         "status": "new",
        #         "type": "limit",
        #         "time_in_force": "GTC",
        #         "quantity": "0.00001",
        #         "quantity_cumulative": "0",
        #         "price": "0.01",
        #         "post_only": False,
        #         "created_at": "2021-04-13T13:06:16.567Z",
        #         "updated_at": "2021-04-13T13:06:16.567Z"
        #       }
        #     ]
        #
        return self.parse_orders(response, market, since, limit)

    async def fetch_open_order(self, id, symbol=None, params={}):
        await self.load_markets()
        market = None
        if symbol is not None:
            market = self.market(symbol)
        request = {
            'client_order_id': id,
        }
        response = await self.privateGetSpotOrderClientOrderId(self.extend(request, params))
        return self.parse_order(response, market)

    async def cancel_all_orders(self, symbol=None, params={}):
        await self.load_markets()
        market = None
        request = {}
        if symbol is not None:
            market = self.market(symbol)
            request['symbol'] = market['id']
        response = await self.privateDeleteSpotOrder(self.extend(request, params))
        return self.parse_orders(response, market)

    async def cancel_order(self, id, symbol=None, params={}):
        await self.load_markets()
        market = None
        request = {
            'client_order_id': id,
        }
        if symbol is not None:
            market = self.market(symbol)
        response = await self.privateDeleteSpotOrderClientOrderId(self.extend(request, params))
        return self.parse_order(response, market)

    async def edit_order(self, id, symbol, type, side, amount, price=None, params={}):
        await self.load_markets()
        market = None
        request = {
            'client_order_id': id,
            'quantity': self.amount_to_precision(symbol, amount),
        }
        if (type == 'limit') or (type == 'stopLimit'):
            if price is None:
                raise ExchangeError(self.id + ' limit order requires price')
            request['price'] = self.price_to_precision(symbol, price)
        if symbol is not None:
            market = self.market(symbol)
        response = await self.privatePatchSpotOrderClientOrderId(self.extend(request, params))
        return self.parse_order(response, market)

    async def create_order(self, symbol, type, side, amount, price=None, params={}):
        await self.load_markets()
        market = self.market(symbol)
        request = {
            'type': type,
            'side': side,
            'quantity': self.amount_to_precision(symbol, amount),
            'symbol': market['id'],
        }
        if (type == 'limit') or (type == 'stopLimit'):
            if price is None:
                raise ExchangeError(self.id + ' limit order requires price')
            request['price'] = self.price_to_precision(symbol, price)
        response = await self.privatePostSpotOrder(self.extend(request, params))
        return self.parse_order(response, market)

    def parse_order_status(self, status):
        statuses = {
            'new': 'open',
            'suspended': 'open',
            'partiallyFilled': 'open',
            'filled': 'closed',
            'canceled': 'canceled',
            'expired': 'failed',
        }
        return self.safe_string(statuses, status, status)

    def parse_order(self, order, market=None):
        #
        # limit
        #     {
        #       "id": 488953123149,
        #       "client_order_id": "103ad305301e4c3590045b13de15b36e",
        #       "symbol": "BTCUSDT",
        #       "side": "buy",
        #       "status": "new",
        #       "type": "limit",
        #       "time_in_force": "GTC",
        #       "quantity": "0.00001",
        #       "quantity_cumulative": "0",
        #       "price": "0.01",
        #       "price_average": "0.01",
        #       "post_only": False,
        #       "created_at": "2021-04-13T13:06:16.567Z",
        #       "updated_at": "2021-04-13T13:06:16.567Z"
        #     }
        #
        # market
        #     {
        #       "id": "685877626834",
        #       "client_order_id": "Yshl7G-EjaREyXQYaGbsmdtVbW-nzQwu",
        #       "symbol": "BTCUSDT",
        #       "side": "buy",
        #       "status": "filled",
        #       "type": "market",
        #       "time_in_force": "GTC",
        #       "quantity": "0.00010",
        #       "quantity_cumulative": "0.00010",
        #       "post_only": False,
        #       "created_at": "2021-10-26T08:55:55.1Z",
        #       "updated_at": "2021-10-26T08:55:55.1Z",
        #       "trades": [
        #         {
        #           "id": "1437229630",
        #           "position_id": "0",
        #           "quantity": "0.00010",
        #           "price": "62884.78",
        #           "fee": "0.005659630200",
        #           "timestamp": "2021-10-26T08:55:55.1Z",
        #           "taker": True
        #         }
        #       ]
        #     }
        #
        id = self.safe_string(order, 'client_order_id')
        # we use clientOrderId as the order id with self exchange intentionally
        # because most of their endpoints will require clientOrderId
        # explained here: https://github.com/ccxt/ccxt/issues/5674
        side = self.safe_string(order, 'side')
        type = self.safe_string(order, 'type')
        amount = self.safe_string(order, 'quantity')
        price = self.safe_string(order, 'price')
        average = self.safe_string(order, 'price_average')
        created = self.safe_string(order, 'created_at')
        timestamp = self.parse8601(created)
        updated = self.safe_string(order, 'updated_at')
        lastTradeTimestamp = None
        if updated != created:
            lastTradeTimestamp = self.parse8601(updated)
        filled = self.safe_string(order, 'quantity_cumulative')
        status = self.parse_order_status(self.safe_string(order, 'status'))
        marketId = self.safe_string(order, 'symbol')
        market = self.safe_market(marketId, market)
        symbol = market['symbol']
        postOnly = self.safe_value(order, 'post_only')
        timeInForce = self.safe_string(order, 'time_in_force')
        rawTrades = self.safe_value(order, 'trades')
        return self.safe_order({
            'info': order,
            'id': id,
            'clientOrderId': id,
            'timestamp': timestamp,
            'datetime': self.iso8601(timestamp),
            'lastTradeTimestamp': lastTradeTimestamp,
            'symbol': symbol,
            'price': price,
            'amount': amount,
            'type': type,
            'side': side,
            'timeInForce': timeInForce,
            'postOnly': postOnly,
            'filled': filled,
            'remaining': None,
            'cost': None,
            'status': status,
            'average': average,
            'trades': rawTrades,
            'fee': None,
        }, market)

    async def transfer(self, code, amount, fromAccount, toAccount, params={}):
        # account can be "spot", "wallet", or "derivatives"
        await self.load_markets()
        currency = self.currency(code)
        requestAmount = self.currency_to_precision(code, amount)
        accountsByType = self.safe_value(self.options, 'accountsByType', {})
        fromAccount = fromAccount.lower()
        toAccount = toAccount.lower()
        fromId = self.safe_string(accountsByType, fromAccount)
        toId = self.safe_string(accountsByType, toAccount)
        keys = list(accountsByType.keys())
        if fromId is None:
            raise ArgumentsRequired(self.id + ' transfer() fromAccount argument must be one of ' + ', '.join(keys))
        if toId is None:
            raise ArgumentsRequired(self.id + ' transfer() toAccount argument must be one of ' + ', '.join(keys))
        if fromId == toId:
            raise BadRequest(self.id + ' transfer() fromAccount and toAccount arguments cannot be the same account')
        request = {
            'currency': currency['id'],
            'amount': requestAmount,
            'source': fromId,
            'destination': toId,
        }
        response = await self.privatePostWalletTransfer(self.extend(request, params))
        # ['2db6ebab-fb26-4537-9ef8-1a689472d236']
        id = self.safe_string(response, 0)
        return {
            'info': response,
            'id': id,
            'timestamp': None,
            'datetime': None,
            'amount': self.parse_number(requestAmount),
            'currency': code,
            'fromAccount': fromAccount,
            'toAccount': toAccount,
            'status': None,
        }

    async def convert_currency_network(self, code, amount, fromNetwork, toNetwork, params):
        await self.load_markets()
        if code != 'USDT':
            raise ExchangeError(self.id + ' convertCurrencyNetwork() only supports USDT currently')
        networks = self.safe_value(self.options, 'networks', {})
        fromNetwork = fromNetwork.upper()
        toNetwork = toNetwork.upper()
        fromNetwork = self.safe_string(networks, fromNetwork)  # handle ETH>ERC20 alias
        toNetwork = self.safe_string(networks, toNetwork)  # handle ETH>ERC20 alias
        if fromNetwork == toNetwork:
            raise BadRequest(self.id + ' fromNetwork cannot be the same as toNetwork')
        if (fromNetwork is None) or (toNetwork is None):
            keys = list(networks.keys())
            raise ArgumentsRequired(self.id + ' convertCurrencyNetwork() requires a fromNetwork parameter and a toNetwork parameter, supported networks are ' + ', '.join(keys))
        request = {
            'from_currency': fromNetwork,
            'to_currency': toNetwork,
            'amount': self.currency_to_precision(code, amount),
        }
        response = await self.privatePostWalletConvert(self.extend(request, params))
        # {"result":["587a1868-e62d-4d8e-b27c-dbdb2ee96149","e168df74-c041-41f2-b76c-e43e4fed5bc7"]}
        return {
            'info': response,
        }

    async def withdraw(self, code, amount, address, tag=None, params={}):
        tag, params = self.handle_withdraw_tag_and_params(tag, params)
        await self.load_markets()
        self.check_address(address)
        currency = self.currency(code)
        request = {
            'currency': currency['id'],
            'amount': amount,
            'address': address,
        }
        if tag is not None:
            request['payment_id'] = tag
        networks = self.safe_value(self.options, 'networks', {})
        network = self.safe_string_upper(params, 'network')
        if (network is not None) and (code == 'USDT'):
            parsedNetwork = self.safe_string(networks, network)
            if parsedNetwork is not None:
                request['currency'] = parsedNetwork
            params = self.omit(params, 'network')
        response = await self.privatePostWalletCryptoWithdraw(self.extend(request, params))
        # {"id":"084cfcd5-06b9-4826-882e-fdb75ec3625d"}
        id = self.safe_string(response, 'id')
        return {
            'info': response,
            'id': id,
        }

    async def fetch_funding_rate_history(self, symbol=None, since=None, limit=None, params={}):
        await self.load_markets()
        market = None
        request = {
            # all arguments are optional
            # 'symbols': Comma separated list of symbol codes,
            # 'sort': 'DESC' or 'ASC'
            # 'from': 'Datetime or Number',
            # 'till': 'Datetime or Number',
            # 'limit': 100,
            # 'offset': 0,
        }
        if symbol is not None:
            market = self.market(symbol)
            request['symbols'] = market['id']
        if since is not None:
            request['from'] = since
        if limit is not None:
            request['limit'] = limit
        response = await self.publicGetPublicFuturesHistoryFunding(self.extend(request, params))
        #
        #    {
        #        "BTCUSDT_PERP": [
        #            {
        #                "timestamp": "2021-07-29T16:00:00.271Z",
        #                "funding_rate": "0.0001",
        #                "avg_premium_index": "0.000061858585213222",
        #                "next_funding_time": "2021-07-30T00:00:00.000Z",
        #                "interest_rate": "0.0001"
        #            },
        #            ...
        #        ],
        #        ...
        #    }
        #
        contracts = list(response.keys())
        rates = []
        for i in range(0, len(contracts)):
            marketId = contracts[i]
            market = self.safe_market(marketId)
            fundingRateData = response[marketId]
            for i in range(0, len(fundingRateData)):
                entry = fundingRateData[i]
                symbol = self.safe_symbol(market['symbol'])
                fundingRate = self.safe_number(entry, 'funding_rate')
                datetime = self.safe_string(entry, 'timestamp')
                rates.append({
                    'info': entry,
                    'symbol': symbol,
                    'fundingRate': fundingRate,
                    'timestamp': self.parse8601(datetime),
                    'datetime': datetime,
                })
        sorted = self.sort_by(rates, 'timestamp')
        return self.filter_by_symbol_since_limit(sorted, symbol, since, limit)

    def handle_errors(self, code, reason, url, method, headers, body, response, requestHeaders, requestBody):
        #
        #     {
        #       "error": {
        #         "code": 20001,
        #         "message": "Insufficient funds",
        #         "description": "Check that the funds are sufficient, given commissions"
        #       }
        #     }
        #
        #     {
        #       "error": {
        #         "code": "600",
        #         "message": "Action not allowed"
        #       }
        #     }
        #
        error = self.safe_value(response, 'error')
        errorCode = self.safe_string(error, 'code')
        if errorCode is not None:
            feedback = self.id + ' ' + body
            message = self.safe_string_2(error, 'message', 'description')
            self.throw_exactly_matched_exception(self.exceptions['exact'], errorCode, feedback)
            self.throw_broadly_matched_exception(self.exceptions['broad'], message, feedback)
            raise ExchangeError(feedback)

    def sign(self, path, api='public', method='GET', params={}, headers=None, body=None):
        query = self.omit(params, self.extract_params(path))
        implodedPath = self.implode_params(path, params)
        url = self.urls['api'][api] + '/' + implodedPath
        getRequest = None
        keys = list(query.keys())
        queryLength = len(keys)
        headers = {
            'Content-Type': 'application/json',
        }
        if method == 'GET':
            if queryLength:
                getRequest = '?' + self.urlencode(query)
                url = url + getRequest
        else:
            body = self.json(params)
        if api == 'private':
            self.check_required_credentials()
            timestamp = str(self.nonce())
            payload = [method, '/api/3/' + implodedPath]
            if method == 'GET':
                if getRequest is not None:
                    payload.append(getRequest)
            else:
                payload.append(body)
            payload.append(timestamp)
            payloadString = ''.join(payload)
            signature = self.hmac(self.encode(payloadString), self.encode(self.secret), hashlib.sha256, 'hex')
            secondPayload = self.apiKey + ':' + signature + ':' + timestamp
            encoded = self.decode(self.string_to_base64(secondPayload))
            headers['Authorization'] = 'HS256 ' + encoded
        return {'url': url, 'method': method, 'body': body, 'headers': headers}
