# -*- 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.base.exchange import Exchange
import math
from ccxt.base.errors import ExchangeError
from ccxt.base.errors import AuthenticationError
from ccxt.base.errors import ArgumentsRequired
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 NotSupported
from ccxt.base.errors import DDoSProtection
from ccxt.base.errors import InvalidNonce
from ccxt.base.decimal_to_precision import TICK_SIZE
from ccxt.base.precise import Precise


class liquid(Exchange):

    def describe(self):
        return self.deep_extend(super(liquid, self).describe(), {
            'id': 'liquid',
            'name': 'Liquid',
            'countries': ['JP', 'CN', 'TW'],
            'version': '2',
            'rateLimit': 1000,
            'has': {
                'cancelOrder': True,
                'CORS': None,
                'createOrder': True,
                'editOrder': True,
                'fetchBalance': True,
                'fetchClosedOrders': True,
                'fetchCurrencies': True,
                'fetchMarkets': True,
                'fetchMyTrades': True,
                'fetchOpenOrders': True,
                'fetchOrder': True,
                'fetchOrderBook': True,
                'fetchOrders': True,
                'fetchTicker': True,
                'fetchTickers': True,
                'fetchTrades': True,
                'fetchWithdrawals': True,
                'withdraw': True,
            },
            'urls': {
                'logo': 'https://user-images.githubusercontent.com/1294454/45798859-1a872600-bcb4-11e8-8746-69291ce87b04.jpg',
                'api': 'https://api.liquid.com',
                'www': 'https://www.liquid.com',
                'doc': [
                    'https://developers.liquid.com',
                ],
                'fees': 'https://help.liquid.com/getting-started-with-liquid/the-platform/fee-structure',
                'referral': 'https://www.liquid.com/sign-up/?affiliate=SbzC62lt30976',
            },
            'api': {
                'public': {
                    'get': [
                        'currencies',
                        'products',
                        'products/{id}',
                        'products/{id}/price_levels',
                        'executions',
                        'ir_ladders/{currency}',
                        'fees',  # add fetchFees, fetchTradingFees, fetchFundingFees
                    ],
                },
                'private': {
                    'get': [
                        'accounts',  # undocumented https://github.com/ccxt/ccxt/pull/7493
                        'accounts/balance',
                        'accounts/main_asset',
                        'accounts/{id}',
                        'accounts/{currency}/reserved_balance_details',
                        'crypto_accounts',  # add fetchAccounts
                        'crypto_withdrawal',
                        'crypto_withdrawals',
                        'crypto_withdrawals/crypto_networks',
                        'executions/me',
                        'fiat_accounts',  # add fetchAccounts
                        'fund_infos',  # add fetchDeposits
                        'loan_bids',
                        'loans',
                        'orders',
                        'orders/{id}',
                        'orders/{id}/trades',  # add fetchOrderTrades
                        'trades',
                        'trades/{id}/loans',
                        'trading_accounts',
                        'trading_accounts/{id}',
                        'transactions',
                        'withdrawals',  # add fetchWithdrawals
                        'user/fee_tier',
                        'user/fees',
                        'trading_accounts/{id}',
                        'bank_accounts',
                        'accounts/{currency}/reserved_balance_details',
                    ],
                    'post': [
                        'crypto_withdrawals',
                        'fund_infos',
                        'fiat_accounts',
                        'loan_bids',
                        'orders',
                        'withdrawals',
                        'fees/estimate',
                    ],
                    'put': [
                        'crypto_withdrawal/{id}/cancel',
                        'loan_bids/{id}/close',
                        'loans/{id}',
                        'orders/{id}',  # add editOrder
                        'orders/{id}/cancel',
                        'trades/{id}',
                        'trades/{id}/adjust_margin',
                        'trades/{id}/close',
                        'trades/close_all',
                        'trading_accounts/{id}',
                        'withdrawals/{id}/cancel',
                    ],
                },
            },
            'fees': {
                'trading': {
                    'tierBased': True,
                    'percentage': True,
                    'taker': 0.0030,
                    'maker': 0.0000,
                    'tiers': {
                        'perpetual': {
                            'maker': [
                                [0, 0.0000],
                                [25000, 0.0000],
                                [50000, -0.00025],
                                [100000, -0.00025],
                                [1000000, -0.00025],
                                [10000000, -0.00025],
                                [25000000, -0.00025],
                                [50000000, -0.00025],
                                [75000000, -0.00025],
                                [100000000, -0.00025],
                                [200000000, -0.00025],
                                [300000000, -0.00025],
                            ],
                            'taker': [
                                [0, 0.00120],
                                [25000, 0.00115],
                                [50000, 0.00110],
                                [100000, 0.00105],
                                [1000000, 0.00100],
                                [10000000, 0.00095],
                                [25000000, 0.00090],
                                [50000000, 0.00085],
                                [75000000, 0.00080],
                                [100000000, 0.00075],
                                [200000000, 0.00070],
                                [300000000, 0.00065],
                            ],
                        },
                        'spot': {
                            'taker': [
                                [0, 0.003],
                                [10000, 0.0029],
                                [20000, 0.0028],
                                [50000, 0.0026],
                                [100000, 0.0020],
                                [1000000, 0.0016],
                                [5000000, 0.0012],
                                [10000000, 0.0010],
                                [25000000, 0.0009],
                                [50000000, 0.0008],
                                [100000000, 0.0007],
                                [200000000, 0.0006],
                                [500000000, 0.0004],
                                [1000000000, 0.0003],
                            ],
                            'maker': [
                                [0, 0.0000],
                                [10000, 0.0020],
                                [20000, 0.0019],
                                [50000, 0.0018],
                                [100000, 0.0016],
                                [1000000, 0.0008],
                                [5000000, 0.0007],
                                [10000000, 0.0005],
                                [25000000, 0.0000],
                                [50000000, 0.0000],
                                [100000000, 0.0000],
                                [200000000, 0.0000],
                                [500000000, 0.0000],
                                [1000000000, 0.0000],
                            ],
                        },
                    },
                },
            },
            'precisionMode': TICK_SIZE,
            'exceptions': {
                'API rate limit exceeded. Please retry after 300s': DDoSProtection,
                'API Authentication failed': AuthenticationError,
                'Nonce is too small': InvalidNonce,
                'Order not found': OrderNotFound,
                'Can not update partially filled order': InvalidOrder,
                'Can not update non-live order': OrderNotFound,
                'not_enough_free_balance': InsufficientFunds,
                'must_be_positive': InvalidOrder,
                'less_than_order_size': InvalidOrder,
                'price_too_high': InvalidOrder,
                'price_too_small': InvalidOrder,  # {"errors":{"order":["price_too_small"]}}
                'product_disabled': BadSymbol,  # {"errors":{"order":["product_disabled"]}}
            },
            'commonCurrencies': {
                'HOT': 'HOT Token',
                'MIOTA': 'IOTA',  # https://github.com/ccxt/ccxt/issues/7487
                'TON': 'Tokamak Network',
                'BIFI': 'Bifrost Finance',
            },
            'options': {
                'cancelOrderException': True,
                'networks': {
                    'ETH': 'ERC20',
                    'TRX': 'TRC20',
                    'XLM': 'Stellar',
                    'ALGO': 'Algorand',
                },
            },
        })

    def fetch_currencies(self, params={}):
        response = self.publicGetCurrencies(params)
        #
        #     [
        #         {
        #             currency_type: 'fiat',
        #             currency: 'USD',
        #             symbol: '$',
        #             assets_precision: 2,
        #             quoting_precision: 5,
        #             minimum_withdrawal: '15.0',
        #             withdrawal_fee: 5,
        #             minimum_fee: null,
        #             minimum_order_quantity: null,
        #             display_precision: 2,
        #             depositable: True,
        #             withdrawable: True,
        #             discount_fee: 0.5,
        #             credit_card_fundable: False,
        #             lendable: False,
        #             position_fundable: True,
        #             has_memo: False,
        #             stable_currency: null,
        #             root_currency: 'USD',
        #             minimum_loan_bid_quantity: '0.0',
        #             maximum_order_taker_quantity: null,
        #             name: 'United States Dollar'
        #         },
        #     ]
        #
        result = {}
        for i in range(0, len(response)):
            currency = response[i]
            id = self.safe_string(currency, 'currency')
            code = self.safe_currency_code(id)
            name = self.safe_string(currency, 'name')
            active = currency['depositable'] and currency['withdrawable']
            amountPrecision = self.safe_integer(currency, 'assets_precision')
            result[code] = {
                'id': id,
                'code': code,
                'info': currency,
                'name': name,
                'active': active,
                'fee': self.safe_number(currency, 'withdrawal_fee'),
                'precision': amountPrecision,
                'limits': {
                    'amount': {
                        'min': math.pow(10, -amountPrecision),
                        'max': math.pow(10, amountPrecision),
                    },
                    'withdraw': {
                        'min': self.safe_number(currency, 'minimum_withdrawal'),
                        'max': None,
                    },
                },
            }
        return result

    def fetch_markets(self, params={}):
        spot = self.publicGetProducts(params)
        #
        #     [
        #         {
        #             "id":"637",
        #             "product_type":"CurrencyPair",
        #             "code":"CASH",
        #             "name":null,
        #             "market_ask":"0.00000797",
        #             "market_bid":"0.00000727",
        #             "indicator":null,
        #             "currency":"BTC",
        #             "currency_pair_code":"TFTBTC",
        #             "symbol":null,
        #             "btc_minimum_withdraw":null,
        #             "fiat_minimum_withdraw":null,
        #             "pusher_channel":"product_cash_tftbtc_637",
        #             "taker_fee":"0.0",
        #             "maker_fee":"0.0",
        #             "low_market_bid":"0.00000685",
        #             "high_market_ask":"0.00000885",
        #             "volume_24h":"3696.0755956",
        #             "last_price_24h":"0.00000716",
        #             "last_traded_price":"0.00000766",
        #             "last_traded_quantity":"1748.0377978",
        #             "average_price":null,
        #             "quoted_currency":"BTC",
        #             "base_currency":"TFT",
        #             "tick_size":"0.00000001",
        #             "disabled":false,
        #             "margin_enabled":false,
        #             "cfd_enabled":false,
        #             "perpetual_enabled":false,
        #             "last_event_timestamp":"1596962820.000797146",
        #             "timestamp":"1596962820.000797146",
        #             "multiplier_up":"9.0",
        #             "multiplier_down":"0.1",
        #             "average_time_interval":null
        #         },
        #     ]
        #
        perpetual = self.publicGetProducts({'perpetual': '1'})
        #
        #     [
        #         {
        #             "id":"604",
        #             "product_type":"Perpetual",
        #             "code":"CASH",
        #             "name":null,
        #             "market_ask":"11721.5",
        #             "market_bid":"11719.0",
        #             "indicator":null,
        #             "currency":"USD",
        #             "currency_pair_code":"P-BTCUSD",
        #             "symbol":"$",
        #             "btc_minimum_withdraw":null,
        #             "fiat_minimum_withdraw":null,
        #             "pusher_channel":"product_cash_p-btcusd_604",
        #             "taker_fee":"0.0012",
        #             "maker_fee":"0.0",
        #             "low_market_bid":"11624.5",
        #             "high_market_ask":"11859.0",
        #             "volume_24h":"0.271",
        #             "last_price_24h":"11621.5",
        #             "last_traded_price":"11771.5",
        #             "last_traded_quantity":"0.09",
        #             "average_price":"11771.5",
        #             "quoted_currency":"USD",
        #             "base_currency":"P-BTC",
        #             "tick_size":"0.5",
        #             "disabled":false,
        #             "margin_enabled":false,
        #             "cfd_enabled":false,
        #             "perpetual_enabled":true,
        #             "last_event_timestamp":"1596963309.418853092",
        #             "timestamp":"1596963309.418853092",
        #             "multiplier_up":null,
        #             "multiplier_down":"0.1",
        #             "average_time_interval":300,
        #             "index_price":"11682.8124",
        #             "mark_price":"11719.96781",
        #             "funding_rate":"0.00273",
        #             "fair_price":"11720.2745"
        #         },
        #     ]
        #
        currencies = self.fetch_currencies()
        currenciesByCode = self.index_by(currencies, 'code')
        result = []
        markets = self.array_concat(spot, perpetual)
        for i in range(0, len(markets)):
            market = markets[i]
            id = self.safe_string(market, 'id')
            baseId = self.safe_string(market, 'base_currency')
            quoteId = self.safe_string(market, 'quoted_currency')
            productType = self.safe_string(market, 'product_type')
            type = 'spot'
            spot = True
            swap = False
            if productType == 'Perpetual':
                spot = False
                swap = True
                type = 'swap'
            base = self.safe_currency_code(baseId)
            quote = self.safe_currency_code(quoteId)
            symbol = None
            if swap:
                symbol = self.safe_string(market, 'currency_pair_code')
            else:
                symbol = base + '/' + quote
            maker = self.fees['trading']['maker']
            taker = self.fees['trading']['taker']
            if type == 'swap':
                maker = self.safe_number(market, 'maker_fee', self.fees['trading']['maker'])
                taker = self.safe_number(market, 'taker_fee', self.fees['trading']['taker'])
            disabled = self.safe_value(market, 'disabled', False)
            active = not disabled
            baseCurrency = self.safe_value(currenciesByCode, base)
            precision = {
                'amount': 0.00000001,
                'price': self.safe_number(market, 'tick_size'),
            }
            minAmount = None
            if baseCurrency is not None:
                minAmount = self.safe_number(baseCurrency['info'], 'minimum_order_quantity')
            lastPrice = self.safe_number(market, 'last_traded_price')
            minPrice = None
            maxPrice = None
            if lastPrice:
                multiplierDown = self.safe_number(market, 'multiplier_down')
                multiplierUp = self.safe_number(market, 'multiplier_up')
                if multiplierDown is not None:
                    minPrice = lastPrice * multiplierDown
                if multiplierUp is not None:
                    maxPrice = lastPrice * multiplierUp
            limits = {
                'amount': {
                    'min': minAmount,
                    'max': None,
                },
                'price': {
                    'min': minPrice,
                    'max': maxPrice,
                },
                'cost': {
                    'min': None,
                    'max': None,
                },
            }
            result.append({
                'id': id,
                'symbol': symbol,
                'base': base,
                'quote': quote,
                'baseId': baseId,
                'quoteId': quoteId,
                'type': type,
                'spot': spot,
                'swap': swap,
                'maker': maker,
                'taker': taker,
                'limits': limits,
                'precision': precision,
                'active': active,
                'info': market,
            })
        return result

    def parse_balance(self, response):
        result = {
            'info': response,
            'timestamp': None,
            'datetime': None,
        }
        crypto = self.safe_value(response, 'crypto_accounts', [])
        fiat = self.safe_value(response, 'fiat_accounts', [])
        for i in range(0, len(crypto)):
            balance = crypto[i]
            currencyId = self.safe_string(balance, 'currency')
            code = self.safe_currency_code(currencyId)
            account = self.account()
            account['total'] = self.safe_string(balance, 'balance')
            account['used'] = self.safe_string(balance, 'reserved_balance')
            result[code] = account
        for i in range(0, len(fiat)):
            balance = fiat[i]
            currencyId = self.safe_string(balance, 'currency')
            code = self.safe_currency_code(currencyId)
            account = self.account()
            account['total'] = self.safe_string(balance, 'balance')
            account['used'] = self.safe_string(balance, 'reserved_balance')
            result[code] = account
        return self.safe_balance(result)

    def fetch_balance(self, params={}):
        self.load_markets()
        response = self.privateGetAccounts(params)
        #
        #     {
        #         crypto_accounts: [
        #             {
        #                 id: 2221179,
        #                 currency: 'USDT',
        #                 balance: '0.0',
        #                 reserved_balance: '0.0',
        #                 pusher_channel: 'user_xxxxx_account_usdt',
        #                 lowest_offer_interest_rate: null,
        #                 highest_offer_interest_rate: null,
        #                 address: '0',
        #                 currency_symbol: 'USDT',
        #                 minimum_withdraw: null,
        #                 currency_type: 'crypto'
        #             },
        #         ],
        #         fiat_accounts: [
        #             {
        #                 id: 1112734,
        #                 currency: 'USD',
        #                 balance: '0.0',
        #                 reserved_balance: '0.0',
        #                 pusher_channel: 'user_xxxxx_account_usd',
        #                 lowest_offer_interest_rate: null,
        #                 highest_offer_interest_rate: null,
        #                 currency_symbol: '$',
        #                 send_to_btc_address: null,
        #                 exchange_rate: '1.0',
        #                 currency_type: 'fiat'
        #             }
        #         ]
        #     }
        #
        return self.parse_balance(response)

    def fetch_order_book(self, symbol, limit=None, params={}):
        self.load_markets()
        request = {
            'id': self.market_id(symbol),
        }
        response = self.publicGetProductsIdPriceLevels(self.extend(request, params))
        return self.parse_order_book(response, symbol, None, 'buy_price_levels', 'sell_price_levels')

    def parse_ticker(self, ticker, market=None):
        timestamp = self.milliseconds()
        last = None
        if 'last_traded_price' in ticker:
            if ticker['last_traded_price']:
                length = len(ticker['last_traded_price'])
                if length > 0:
                    last = self.safe_number(ticker, 'last_traded_price')
        symbol = None
        if market is None:
            marketId = self.safe_string(ticker, 'id')
            if marketId in self.markets_by_id:
                market = self.markets_by_id[marketId]
            else:
                baseId = self.safe_string(ticker, 'base_currency')
                quoteId = self.safe_string(ticker, 'quoted_currency')
                if symbol in self.markets:
                    market = self.markets[symbol]
                else:
                    symbol = self.safe_currency_code(baseId) + '/' + self.safe_currency_code(quoteId)
        if market is not None:
            symbol = market['symbol']
        open = self.safe_number(ticker, 'last_price_24h')
        return self.safe_ticker({
            'symbol': symbol,
            'timestamp': timestamp,
            'datetime': self.iso8601(timestamp),
            'high': self.safe_number(ticker, 'high_market_ask'),
            'low': self.safe_number(ticker, 'low_market_bid'),
            'bid': self.safe_number(ticker, 'market_bid'),
            'bidVolume': None,
            'ask': self.safe_number(ticker, 'market_ask'),
            'askVolume': None,
            'vwap': None,
            'open': open,
            'close': last,
            'last': last,
            'previousClose': None,
            'change': None,
            'percentage': None,
            'average': None,
            'baseVolume': self.safe_number(ticker, 'volume_24h'),
            'quoteVolume': None,
            'info': ticker,
        }, market)

    def fetch_tickers(self, symbols=None, params={}):
        self.load_markets()
        response = self.publicGetProducts(params)
        result = {}
        for i in range(0, len(response)):
            ticker = self.parse_ticker(response[i])
            symbol = ticker['symbol']
            result[symbol] = ticker
        return self.filter_by_array(result, 'symbol', symbols)

    def fetch_ticker(self, symbol, params={}):
        self.load_markets()
        market = self.market(symbol)
        request = {
            'id': market['id'],
        }
        response = self.publicGetProductsId(self.extend(request, params))
        return self.parse_ticker(response, market)

    def parse_trade(self, trade, market=None):
        # {            id:  12345,
        #         quantity: "6.789",
        #            price: "98765.4321",
        #       taker_side: "sell",
        #       created_at:  1512345678,
        #          my_side: "buy"           }
        timestamp = self.safe_timestamp(trade, 'created_at')
        orderId = self.safe_string(trade, 'order_id')
        # 'taker_side' gets filled for both fetchTrades and fetchMyTrades
        takerSide = self.safe_string(trade, 'taker_side')
        # 'my_side' gets filled for fetchMyTrades only and may differ from 'taker_side'
        mySide = self.safe_string(trade, 'my_side')
        side = mySide if (mySide is not None) else takerSide
        takerOrMaker = None
        if mySide is not None:
            takerOrMaker = 'taker' if (takerSide == mySide) else 'maker'
        priceString = self.safe_string(trade, 'price')
        amountString = self.safe_string(trade, 'quantity')
        price = self.parse_number(priceString)
        amount = self.parse_number(amountString)
        cost = self.parse_number(Precise.string_mul(priceString, amountString))
        id = self.safe_string(trade, 'id')
        symbol = None
        if market is not None:
            symbol = market['symbol']
        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': None,
        }

    def fetch_trades(self, symbol, since=None, limit=None, params={}):
        self.load_markets()
        market = self.market(symbol)
        request = {
            'product_id': market['id'],
        }
        if limit is not None:
            request['limit'] = limit
        if since is not None:
            # timestamp should be in seconds, whereas we use milliseconds in since and everywhere
            request['timestamp'] = int(since / 1000)
        response = self.publicGetExecutions(self.extend(request, params))
        result = response if (since is not None) else response['models']
        return self.parse_trades(result, market, since, limit)

    def fetch_my_trades(self, symbol=None, since=None, limit=None, params={}):
        self.load_markets()
        market = self.market(symbol)
        # the `with_details` param is undocumented - it adds the order_id to the results
        request = {
            'product_id': market['id'],
            'with_details': True,
        }
        if limit is not None:
            request['limit'] = limit
        response = self.privateGetExecutionsMe(self.extend(request, params))
        return self.parse_trades(response['models'], market, since, limit)

    def create_order(self, symbol, type, side, amount, price=None, params={}):
        self.load_markets()
        clientOrderId = self.safe_string_2(params, 'clientOrderId', 'client_order_id')
        params = self.omit(params, ['clientOrderId', 'client_order_id'])
        request = {
            'order_type': type,
            'product_id': self.market_id(symbol),
            'side': side,
            'quantity': self.amount_to_precision(symbol, amount),
        }
        if clientOrderId is not None:
            request['client_order_id'] = clientOrderId
        if (type == 'limit') or (type == 'limit_post_only') or (type == 'market_with_range') or (type == 'stop'):
            request['price'] = self.price_to_precision(symbol, price)
        response = self.privatePostOrders(self.extend(request, params))
        #
        #     {
        #         "id": 2157474,
        #         "order_type": "limit",
        #         "quantity": "0.01",
        #         "disc_quantity": "0.0",
        #         "iceberg_total_quantity": "0.0",
        #         "side": "sell",
        #         "filled_quantity": "0.0",
        #         "price": "500.0",
        #         "created_at": 1462123639,
        #         "updated_at": 1462123639,
        #         "status": "live",
        #         "leverage_level": 1,
        #         "source_exchange": "QUOINE",
        #         "product_id": 1,
        #         "product_code": "CASH",
        #         "funding_currency": "USD",
        #         "currency_pair_code": "BTCUSD",
        #         "order_fee": "0.0",
        #         "client_order_id": null,
        #     }
        #
        return self.parse_order(response)

    def cancel_order(self, id, symbol=None, params={}):
        self.load_markets()
        request = {
            'id': id,
        }
        response = self.privatePutOrdersIdCancel(self.extend(request, params))
        order = self.parse_order(response)
        if order['status'] == 'closed':
            if self.options['cancelOrderException']:
                raise OrderNotFound(self.id + ' order closed already: ' + self.json(response))
        return order

    def edit_order(self, id, symbol, type, side, amount, price=None, params={}):
        self.load_markets()
        if price is None:
            raise ArgumentsRequired(self.id + ' editOrder() requires the price argument')
        request = {
            'order': {
                'quantity': self.amount_to_precision(symbol, amount),
                'price': self.price_to_precision(symbol, price),
            },
            'id': id,
        }
        response = self.privatePutOrdersId(self.extend(request, params))
        return self.parse_order(response)

    def parse_order_status(self, status):
        statuses = {
            'live': 'open',
            'filled': 'closed',
            'cancelled': 'canceled',
        }
        return self.safe_string(statuses, status, status)

    def parse_order(self, order, market=None):
        #
        # createOrder
        #
        #     {
        #         "id": 2157474,
        #         "order_type": "limit",
        #         "quantity": "0.01",
        #         "disc_quantity": "0.0",
        #         "iceberg_total_quantity": "0.0",
        #         "side": "sell",
        #         "filled_quantity": "0.0",
        #         "price": "500.0",
        #         "created_at": 1462123639,
        #         "updated_at": 1462123639,
        #         "status": "live",
        #         "leverage_level": 1,
        #         "source_exchange": "QUOINE",
        #         "product_id": 1,
        #         "product_code": "CASH",
        #         "funding_currency": "USD",
        #         "currency_pair_code": "BTCUSD",
        #         "order_fee": "0.0"
        #         "client_order_id": null,
        #     }
        #
        # fetchOrder, fetchOrders, fetchOpenOrders, fetchClosedOrders
        #
        #     {
        #         "id": 2157479,
        #         "order_type": "limit",
        #         "quantity": "0.01",
        #         "disc_quantity": "0.0",
        #         "iceberg_total_quantity": "0.0",
        #         "side": "sell",
        #         "filled_quantity": "0.01",
        #         "price": "500.0",
        #         "created_at": 1462123639,
        #         "updated_at": 1462123639,
        #         "status": "filled",
        #         "leverage_level": 2,
        #         "source_exchange": "QUOINE",
        #         "product_id": 1,
        #         "product_code": "CASH",
        #         "funding_currency": "USD",
        #         "currency_pair_code": "BTCUSD",
        #         "order_fee": "0.0",
        #         "executions": [
        #             {
        #                 "id": 4566133,
        #                 "quantity": "0.01",
        #                 "price": "500.0",
        #                 "taker_side": "buy",
        #                 "my_side": "sell",
        #                 "created_at": 1465396785
        #             }
        #         ]
        #     }
        #
        orderId = self.safe_string(order, 'id')
        timestamp = self.safe_timestamp(order, 'created_at')
        marketId = self.safe_string(order, 'product_id')
        market = self.safe_value(self.markets_by_id, marketId)
        status = self.parse_order_status(self.safe_string(order, 'status'))
        amount = self.safe_number(order, 'quantity')
        filled = self.safe_number(order, 'filled_quantity')
        price = self.safe_number(order, 'price')
        symbol = None
        feeCurrency = None
        if market is not None:
            symbol = market['symbol']
            feeCurrency = market['quote']
        type = self.safe_string(order, 'order_type')
        tradeCost = 0
        tradeFilled = 0
        average = self.safe_number(order, 'average_price')
        trades = self.parse_trades(self.safe_value(order, 'executions', []), market, None, None, {
            'order': orderId,
            'type': type,
        })
        numTrades = len(trades)
        for i in range(0, numTrades):
            # php copies values upon assignment, but not references them
            # todo rewrite self(shortly)
            trade = trades[i]
            trade['order'] = orderId
            trade['type'] = type
            tradeFilled = self.sum(tradeFilled, trade['amount'])
            tradeCost = self.sum(tradeCost, trade['cost'])
        cost = None
        lastTradeTimestamp = None
        if numTrades > 0:
            lastTradeTimestamp = trades[numTrades - 1]['timestamp']
            if not average and (tradeFilled > 0):
                average = tradeCost / tradeFilled
            if cost is None:
                cost = tradeCost
            if filled is None:
                filled = tradeFilled
        remaining = None
        if amount is not None and filled is not None:
            remaining = amount - filled
        side = self.safe_string(order, 'side')
        clientOrderId = self.safe_string(order, 'client_order_id')
        return {
            'id': orderId,
            'clientOrderId': clientOrderId,
            'timestamp': timestamp,
            'datetime': self.iso8601(timestamp),
            'lastTradeTimestamp': lastTradeTimestamp,
            'type': type,
            'timeInForce': None,
            'postOnly': None,
            'status': status,
            'symbol': symbol,
            'side': side,
            'price': price,
            'stopPrice': None,
            'amount': amount,
            'filled': filled,
            'cost': cost,
            'remaining': remaining,
            'average': average,
            'trades': trades,
            'fee': {
                'currency': feeCurrency,
                'cost': self.safe_number(order, 'order_fee'),
            },
            'info': order,
        }

    def fetch_order(self, id, symbol=None, params={}):
        self.load_markets()
        request = {
            'id': id,
        }
        response = self.privateGetOrdersId(self.extend(request, params))
        return self.parse_order(response)

    def fetch_orders(self, symbol=None, since=None, limit=None, params={}):
        self.load_markets()
        market = None
        request = {
            # 'funding_currency': market['quoteId'],  # filter orders based on "funding" currency(quote currency)
            # 'product_id': market['id'],
            # 'status': 'live',  # 'filled', 'cancelled'
            # 'trading_type': 'spot',  # 'margin', 'cfd'
            'with_details': 1,  # return full order details including executions
        }
        if symbol is not None:
            market = self.market(symbol)
            request['product_id'] = market['id']
        if limit is not None:
            request['limit'] = limit
        response = self.privateGetOrders(self.extend(request, params))
        #
        #     {
        #         "models": [
        #             {
        #                 "id": 2157474,
        #                 "order_type": "limit",
        #                 "quantity": "0.01",
        #                 "disc_quantity": "0.0",
        #                 "iceberg_total_quantity": "0.0",
        #                 "side": "sell",
        #                 "filled_quantity": "0.0",
        #                 "price": "500.0",
        #                 "created_at": 1462123639,
        #                 "updated_at": 1462123639,
        #                 "status": "live",
        #                 "leverage_level": 1,
        #                 "source_exchange": "QUOINE",
        #                 "product_id": 1,
        #                 "product_code": "CASH",
        #                 "funding_currency": "USD",
        #                 "currency_pair_code": "BTCUSD",
        #                 "order_fee": "0.0",
        #                 "executions": [],  # optional
        #             }
        #         ],
        #         "current_page": 1,
        #         "total_pages": 1
        #     }
        #
        orders = self.safe_value(response, 'models', [])
        return self.parse_orders(orders, market, since, limit)

    def fetch_open_orders(self, symbol=None, since=None, limit=None, params={}):
        request = {'status': 'live'}
        return self.fetch_orders(symbol, since, limit, self.extend(request, params))

    def fetch_closed_orders(self, symbol=None, since=None, limit=None, params={}):
        request = {'status': 'filled'}
        return self.fetch_orders(symbol, since, limit, self.extend(request, params))

    def withdraw(self, code, amount, address, tag=None, params={}):
        tag, params = self.handle_withdraw_tag_and_params(tag, params)
        self.check_address(address)
        self.load_markets()
        currency = self.currency(code)
        request = {
            # 'auth_code': '',  # optional 2fa code
            'crypto_withdrawal': {
                'currency': currency['id'],
                'address': address,
                'amount': amount,
                # 'payment_id': tag,  # for XRP only
                # 'memo_type': 'text',  # 'text', 'id' or 'hash', for XLM only
                # 'memo_value': tag,  # for XLM only
            },
        }
        if tag is not None:
            if code == 'XRP':
                request['crypto_withdrawal']['payment_id'] = tag
            elif code == 'XLM':
                request['crypto_withdrawal']['memo_type'] = 'text'  # overrideable via params
                request['crypto_withdrawal']['memo_value'] = tag
            else:
                raise NotSupported(self.id + ' withdraw() only supports a tag along the address for XRP or XLM')
        networks = self.safe_value(self.options, 'networks', {})
        paramsCwArray = self.safe_value(params, 'crypto_withdrawal', {})
        network = self.safe_string_upper(paramsCwArray, 'network')  # self line allows the user to specify either ERC20 or ETH
        network = self.safe_string(networks, network, network)  # handle ERC20>ETH alias
        if network is not None:
            request['crypto_withdrawal']['network'] = network
            params['crypto_withdrawal'] = self.omit(params['crypto_withdrawal'], 'network')
        response = self.privatePostCryptoWithdrawals(self.deep_extend(request, params))
        #
        #     {
        #         "id": 1353,
        #         "address": "1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN2",
        #         "amount": 1.0,
        #         "state": "pending",
        #         "currency": "BTC",
        #         "withdrawal_fee": 0.0,
        #         "created_at": 1568016450,
        #         "updated_at": 1568016450,
        #         "payment_id": null
        #     }
        #
        return self.parse_transaction(response, currency)

    def fetch_withdrawals(self, code=None, since=None, limit=None, params={}):
        self.load_markets()
        request = {
            # state: 'processed',  # optional: pending, filed, cancelled, processing, processed, reverted to_be_reviewed, declined, broadcasted
        }
        currency = None
        if code is not None:
            currency = self.currency(code)
        response = self.privateGetCryptoWithdrawals(self.extend(request, params))
        #
        #     {
        #         models: [
        #             {
        #                 id: '2',
        #                 address: '1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN2',
        #                 amount: '0.01',
        #                 state: 'processed',
        #                 currency: 'BTC',
        #                 withdrawal_fee: '0.0005',
        #                 created_at: '1614718276',
        #                 updated_at: '1614720926',
        #                 payment_id: null,
        #                 transaction_hash: 'xxxxxxxx...',
        #                 broadcasted_at: '1614720762',
        #                 wallet_label: 'btc',
        #                 chain_name: 'Bitcoin',
        #                 network: null
        #             },
        #         ],
        #         current_page: '1',
        #         total_pages: '1'
        #     }
        #
        transactions = self.safe_value(response, 'models', [])
        return self.parse_transactions(transactions, currency, since, limit)

    def parse_transaction_status(self, status):
        statuses = {
            'pending': 'pending',
            'cancelled': 'canceled',
            'approved': 'ok',
            'processing': 'pending',
            'processed': 'ok',
            'reverted': 'failed',
            'to_be_reviewed': 'pending',
            'declined': 'failed',
            'broadcasted': 'ok',
        }
        return self.safe_string(statuses, status, status)

    def parse_transaction(self, transaction, currency=None):
        #
        # withdraw
        #
        #     {
        #         id: '1',
        #         address: '1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN2',
        #         amount: '0.01',
        #         state: 'pending',
        #         currency: 'BTC',
        #         withdrawal_fee: '0.0007',
        #         created_at: '1626000533',
        #         updated_at: '1626000533',
        #         payment_id: null,
        #         transaction_hash: null,
        #         broadcasted_at: null,
        #         wallet_label: null,
        #         chain_name: 'Bitcoin',
        #         network: null
        #     },
        #
        # fetchWithdrawals
        #
        #     {
        #         id: '2',
        #         address: '1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN2',
        #         amount: '0.01',
        #         state: 'processed',
        #         currency: 'BTC',
        #         withdrawal_fee: '0.0005',
        #         created_at: '1614718276',
        #         updated_at: '1614720926',
        #         payment_id: '',
        #         transaction_hash: 'xxxxxxxx...',
        #         broadcasted_at: '1614720762',
        #         wallet_label: 'btc',
        #         chain_name: 'Bitcoin',
        #         network: null
        #     },
        #
        # fetchDeposits
        #
        #     ...
        #
        id = self.safe_string(transaction, 'id')
        address = self.safe_string(transaction, 'address')
        tag = self.safe_string_2(transaction, 'payment_id', 'memo_value')
        txid = self.safe_string(transaction, 'transaction_hash')
        currencyId = self.safe_string_2(transaction, 'currency', 'asset')
        code = self.safe_currency_code(currencyId, currency)
        timestamp = self.safe_timestamp(transaction, 'created_at')
        updated = self.safe_timestamp(transaction, 'updated_at')
        type = 'withdrawal'
        status = self.parse_transaction_status(self.safe_string(transaction, 'state'))
        amountString = self.safe_string(transaction, 'amount')
        feeCostString = self.safe_string(transaction, 'withdrawal_fee')
        amount = self.parse_number(Precise.string_sub(amountString, feeCostString))
        network = self.safe_string(transaction, 'chain_name')
        return {
            'info': transaction,
            'id': id,
            'txid': txid,
            'timestamp': timestamp,
            'datetime': self.iso8601(timestamp),
            'network': network,
            'address': address,
            'addressTo': None,
            'addressFrom': None,
            'tag': tag,
            'tagTo': None,
            'tagFrom': None,
            'type': type,
            'amount': amount,
            'currency': code,
            'status': status,
            'updated': updated,
            'fee': {
                'currency': code,
                'cost': self.parse_number(feeCostString),
            },
        }

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

    def sign(self, path, api='public', method='GET', params={}, headers=None, body=None):
        url = '/' + self.implode_params(path, params)
        query = self.omit(params, self.extract_params(path))
        headers = {
            'X-Quoine-API-Version': self.version,
            'Content-Type': 'application/json',
        }
        if api == 'private':
            self.check_required_credentials()
            if method == 'GET':
                if query:
                    url += '?' + self.urlencode(query)
            elif query:
                body = self.json(query)
            nonce = self.nonce()
            request = {
                'path': url,
                'token_id': self.apiKey,
                'iat': int(math.floor(nonce / 1000)),  # issued at
            }
            if not ('client_order_id' in query):
                request['nonce'] = nonce
            headers['X-Quoine-Auth'] = self.jwt(request, self.encode(self.secret))
        else:
            if query:
                url += '?' + self.urlencode(query)
        url = self.urls['api'] + url
        return {'url': url, 'method': method, 'body': body, 'headers': headers}

    def handle_errors(self, code, reason, url, method, headers, body, response, requestHeaders, requestBody):
        if code >= 200 and code < 300:
            return
        if code == 401:
            # expected non-json response
            self.throw_exactly_matched_exception(self.exceptions, body, body)
            return
        if code == 429:
            raise DDoSProtection(self.id + ' ' + body)
        if response is None:
            return
        feedback = self.id + ' ' + body
        message = self.safe_string(response, 'message')
        errors = self.safe_value(response, 'errors')
        if message is not None:
            #
            #  {"message": "Order not found"}
            #
            self.throw_exactly_matched_exception(self.exceptions, message, feedback)
        elif errors is not None:
            #
            #  {"errors": {"user": ["not_enough_free_balance"]}}
            #  {"errors": {"quantity": ["less_than_order_size"]}}
            #  {"errors": {"order": ["Can not update partially filled order"]}}
            #
            types = list(errors.keys())
            for i in range(0, len(types)):
                type = types[i]
                errorMessages = errors[type]
                for j in range(0, len(errorMessages)):
                    message = errorMessages[j]
                    self.throw_exactly_matched_exception(self.exceptions, message, feedback)
        else:
            raise ExchangeError(feedback)
