'use strict';

//  ---------------------------------------------------------------------------

const Exchange = require ('./base/Exchange');
const { ExchangeError, InvalidOrder, AuthenticationError, InsufficientFunds, BadSymbol, OrderNotFound, InvalidAddress, BadRequest } = require ('./base/errors');
const { TRUNCATE, ROUND, TICK_SIZE } = require ('./base/functions/number');

//  ---------------------------------------------------------------------------

module.exports = class gopax extends Exchange {
    describe () {
        return this.deepExtend (super.describe (), {
            'id': 'gopax',
            'name': 'GOPAX',
            'countries': [ 'KR' ], // South Korea
            'version': 'v1',
            'rateLimit': 50,
            'hostname': 'gopax.co.kr', // or 'gopax.com'
            'certified': true,
            'pro': true,
            'has': {
                'cancelOrder': true,
                'createMarketOrder': true,
                'createOrder': true,
                'fetchBalance': true,
                'fetchCurrencies': true,
                'fetchDepositAddress': 'emulated',
                'fetchDepositAddresses': true,
                'fetchMarkets': true,
                'fetchMyTrades': true,
                'fetchOHLCV': true,
                'fetchOpenOrders': true,
                'fetchOrder': true,
                'fetchOrderBook': true,
                'fetchOrders': true,
                'fetchTicker': true,
                'fetchTickers': true,
                'fetchTime': true,
                'fetchTrades': true,
                'fetchTransactions': true,
            },
            'timeframes': {
                '1m': '1',
                '5m': '5',
                '30m': '30',
                '1d': '1440',
            },
            'urls': {
                'logo': 'https://user-images.githubusercontent.com/1294454/102897212-ae8a5e00-4478-11eb-9bab-91507c643900.jpg',
                'api': {
                    'public': 'https://api.{hostname}', // or 'https://api.gopax.co.kr'
                    'private': 'https://api.{hostname}',
                },
                'www': 'https://www.gopax.co.kr',
                'doc': 'https://gopax.github.io/API/index.en.html',
                'fees': 'https://www.gopax.com/feeinfo',
            },
            'api': {
                'public': {
                    'get': [
                        'notices',
                        'assets',
                        'price-tick-size',
                        'trading-pairs',
                        'trading-pairs/{tradingPair}/ticker',
                        'trading-pairs/{tradingPair}/book',
                        'trading-pairs/{tradingPair}/trades',
                        'trading-pairs/{tradingPair}/stats',
                        'trading-pairs/{tradingPair}/price-tick-size',
                        'trading-pairs/stats',
                        'trading-pairs/{tradingPair}/candles',
                        'time',
                    ],
                },
                'private': {
                    'get': [
                        'balances',
                        'balances/{assetName}',
                        'orders',
                        'orders/{orderId}',
                        'orders/clientOrderId/{clientOrderId}',
                        'trades',
                        'deposit-withdrawal-status',
                        'crypto-deposit-addresses',
                        'crypto-withdrawal-addresses',
                    ],
                    'post': [
                        'orders',
                    ],
                    'delete': [
                        'orders/{orderId}',
                        'orders/clientOrderId/{clientOrderId}',
                    ],
                },
            },
            'fees': {
                'trading': {
                    'percentage': true,
                    'tierBased': false,
                    'maker': 0.04 / 100,
                    'taker': 0.04 / 100,
                },
            },
            'exceptions': {
                'broad': {
                    'ERROR_INVALID_ORDER_TYPE': InvalidOrder,
                    'ERROR_INVALID_AMOUNT': InvalidOrder,
                    'ERROR_INVALID_TRADING_PAIR': BadSymbol, // Unlikely to be triggered, due to ccxt.gopax.js implementation
                    'No such order ID': OrderNotFound, // {"errorMessage":"No such order ID","errorCode":202,"errorData":"Order server error: 202"}
                    // 'Not enough amount': InsufficientFunds, // {"errorMessage":"Not enough amount, try increasing your order amount","errorCode":10212,"errorData":{}}
                    'Forbidden order type': InvalidOrder,
                    'the client order ID will be reusable which order has already been completed or canceled': InvalidOrder,
                    'ERROR_NO_SUCH_TRADING_PAIR': BadSymbol, // Unlikely to be triggered, due to ccxt.gopax.js implementation
                    'ERROR_INVALID_ORDER_SIDE': InvalidOrder,
                    'ERROR_NOT_HEDGE_TOKEN_USER': InvalidOrder,
                    'ORDER_EVENT_ERROR_NOT_ALLOWED_BID_ORDER': InvalidOrder, // Triggered only when the exchange is locked
                    'ORDER_EVENT_ERROR_INSUFFICIENT_BALANCE': InsufficientFunds,
                    'Invalid option combination': InvalidOrder,
                    'No such client order ID': OrderNotFound,
                },
                'exact': {
                    '100': BadSymbol, // Invalid asset name
                    '101': BadSymbol, // Invalid trading pair
                    '103': InvalidOrder, // Invalid order type
                    '104': BadSymbol, // Invalid trading pair
                    '105': BadSymbol, // Trading pair temporarily disabled
                    '106': BadSymbol, // Invalid asset name
                    '107': InvalidOrder, // Invalid order amount
                    '108': InvalidOrder, // Invalid order price
                    '111': InvalidOrder, // Invalid event type
                    '201': InsufficientFunds, // Not enough balance
                    '202': InvalidOrder, // Invalid order ID
                    '203': InvalidOrder, // Order amount X order price too large
                    '204': InvalidOrder, // Bid order temporarily unavailable
                    '205': InvalidOrder, // Invalid side
                    '206': InvalidOrder, // Invalid order option combination
                    '10004': AuthenticationError, // Not authorized
                    // '10004': ExchangeError, // API key not exist
                    // '10004': ExchangeError, // User KYC not approved
                    // '10004': ExchangeError, // User account is frozen
                    // '10004': ExchangeError, // User is under deactivation process
                    // '10004': ExchangeError, // 2FA is not enabled
                    // '10004': ExchangeError, // Invalid signature
                    '10041': BadRequest, // Invalid exchange
                    '10056': BadRequest, // No registered asset
                    '10057': BadSymbol, // No registered trading pair
                    '10059': BadSymbol, // Invalid trading pair
                    '10062': BadRequest, // Invalid chart interval
                    '10069': OrderNotFound, // {"errorMessage":"No such order ID: 73152094","errorCode":10069,"errorData":"73152094"}
                    '10155': AuthenticationError, // {"errorMessage":"Invalid API key","errorCode":10155}
                    '10166': BadRequest, // Invalid chart range
                    '10212': InvalidOrder, // {"errorMessage":"Not enough amount, try increasing your order amount","errorCode":10212,"errorData":{}}
                    '10221': OrderNotFound, // No such client order ID
                    '10222': InvalidOrder, // Client order ID being used
                    '10223': InvalidOrder, // Soon the client order ID will be reusable which order has already been completed or canceled
                    '10227': InvalidOrder, // Invalid client order ID format
                    '10319': BadRequest, // Pagination is required as you have too many orders
                    '10358': InvalidOrder, // Invalid order type
                    '10359': InvalidOrder, // Invalid order side
                    '10360': InvalidOrder, // Invalid order status
                    '10361': InvalidOrder, // Invalid order time in force
                    '10362': InvalidOrder, // Invalid order protection
                    '10363': InvalidOrder, // Invalid forced completion reason
                },
            },
            'options': {
                'createMarketBuyOrderRequiresPrice': true,
            },
        });
    }

    async fetchTime (params = {}) {
        const response = await this.publicGetTime (params);
        //
        //     {"serverTime":1608327726656}
        //
        return this.safeInteger (response, 'serverTime');
    }

    async fetchMarkets (params = {}) {
        const response = await this.publicGetTradingPairs (params);
        //
        //     [
        //         {
        //             "id":1,
        //             "name":"ETH-KRW",
        //             "baseAsset":"ETH",
        //             "quoteAsset":"KRW",
        //             "baseAssetScale":8,
        //             "quoteAssetScale":0,
        //             "priceMin":1,
        //             "restApiOrderAmountMin":{
        //                 "limitAsk":{"amount":10000,"unit":"KRW"},
        //                 "limitBid":{"amount":10000,"unit":"KRW"},
        //                 "marketAsk":{"amount":0.001,"unit":"ETH"},
        //                 "marketBid":{"amount":10000,"unit":"KRW"},
        //             },
        //             "makerFeePercent":0.2,
        //             "takerFeePercent":0.2,
        //         },
        //     ]
        //
        const result = [];
        for (let i = 0; i < response.length; i++) {
            const market = response[i];
            const id = this.safeString (market, 'name');
            const numericId = this.safeInteger (market, 'id');
            const baseId = this.safeString (market, 'baseAsset');
            const quoteId = this.safeString (market, 'quoteAsset');
            const base = this.safeCurrencyCode (baseId);
            const quote = this.safeCurrencyCode (quoteId);
            const symbol = base + '/' + quote;
            const precision = {
                'price': this.safeInteger (market, 'quoteAssetScale'),
                'amount': this.safeInteger (market, 'baseAssetScale'),
            };
            const minimums = this.safeValue (market, 'restApiOrderAmountMin', {});
            const marketAsk = this.safeValue (minimums, 'marketAsk', {});
            const marketBid = this.safeValue (minimums, 'marketBid', {});
            const takerFeePercent = this.safeFloat (market, 'takerFeePercent');
            const makerFeePercent = this.safeFloat (market, 'makerFeePercent');
            const taker = parseFloat (this.decimalToPrecision (takerFeePercent / 100, ROUND, 0.00000001, TICK_SIZE));
            const maker = parseFloat (this.decimalToPrecision (makerFeePercent / 100, ROUND, 0.00000001, TICK_SIZE));
            result.push ({
                'id': id,
                'info': market,
                'numericId': numericId,
                'symbol': symbol,
                'base': base,
                'quote': quote,
                'baseId': this.safeString (market, 'baseAsset'),
                'quoteId': this.safeString (market, 'quoteAsset'),
                'active': true,
                'taker': taker,
                'maker': maker,
                'precision': precision,
                'limits': {
                    'amount': {
                        'min': this.safeFloat (marketAsk, 'amount'),
                        'max': undefined,
                    },
                    'price': {
                        'min': this.safeFloat (market, 'priceMin'),
                        'max': undefined,
                    },
                    'cost': {
                        'min': this.safeFloat (marketBid, 'amount'),
                        'max': undefined,
                    },
                },
            });
        }
        return result;
    }

    async fetchCurrencies (params = {}) {
        const response = await this.publicGetAssets (params);
        //
        //     [
        //         {
        //             "id":"KRW",
        //             "name":"대한민국 원",
        //             "scale":0,
        //             "withdrawalFee":1000,
        //             "withdrawalAmountMin":5000
        //         },
        //         {
        //             "id":"ETH",
        //             "name":"이더리움",
        //             "scale":8,
        //             "withdrawalFee":0.03,
        //             "withdrawalAmountMin":0.015
        //         },
        //     ]
        //
        const result = {};
        for (let i = 0; i < response.length; i++) {
            const currency = response[i];
            const id = this.safeString (currency, 'id');
            const code = this.safeCurrencyCode (id);
            const name = this.safeString (currency, 'name');
            const fee = this.safeFloat (currency, 'withdrawalFee');
            const precision = this.safeFloat (currency, 'scale');
            result[code] = {
                'id': id,
                'info': currency,
                'code': code,
                'name': name,
                'active': true,
                'fee': fee,
                'precision': precision,
                'limits': {
                    'amount': {
                        'min': undefined,
                        'max': undefined,
                    },
                    'price': {
                        'min': undefined,
                        'max': undefined,
                    },
                    'cost': {
                        'min': undefined,
                        'max': undefined,
                    },
                    'withdraw': {
                        'min': this.safeFloat (currency, 'withdrawalAmountMin'),
                        'max': undefined,
                    },
                },
            };
        }
        return result;
    }

    async fetchOrderBook (symbol, limit = undefined, params = {}) {
        await this.loadMarkets ();
        const market = this.market (symbol);
        const request = {
            'tradingPair': market['id'],
            // 'level': 3, // 1 best bidask, 2 top 50 bidasks, 3 all bidasks
        };
        const response = await this.publicGetTradingPairsTradingPairBook (this.extend (request, params));
        //
        //     {
        //         "sequence":17691957,
        //         "bid":[
        //             ["17690499",25019000,0.00008904,"1608326468921"],
        //             ["17691894",25010000,0.4295,"1608326499940"],
        //             ["17691895",25009000,0.2359,"1608326499953"],
        //         ],
        //         "ask":[
        //             ["17689176",25024000,0.000098,"1608326442006"],
        //             ["17691351",25031000,0.206,"1608326490418"],
        //             ["17691571",25035000,0.3996,"1608326493742"],
        //         ]
        //     }
        //
        const nonce = this.safeInteger (response, 'sequence');
        const result = this.parseOrderBook (response, undefined, 'bid', 'ask', 1, 2);
        result['nonce'] = nonce;
        return result;
    }

    parseTicker (ticker, market = undefined) {
        //
        // fetchTicker
        //
        //     {
        //         "price":25087000,
        //         "ask":25107000,
        //         "askVolume":0.05837704,
        //         "bid":25087000,
        //         "bidVolume":0.00398628,
        //         "volume":350.09171591,
        //         "quoteVolume":8721016926.06529,
        //         "time":"2020-12-18T21:42:13.774Z",
        //     }
        //
        // fetchTickers
        //
        //     {
        //         "name":"ETH-KRW",
        //         "open":690500,
        //         "high":719500,
        //         "low":681500,
        //         "close":709500,
        //         "volume":2784.6081544,
        //         "time":"2020-12-18T21:54:50.795Z"
        //     }
        //
        const marketId = this.safeString (ticker, 'name');
        const symbol = this.safeSymbol (marketId, market, '-');
        const timestamp = this.parse8601 (this.safeString (ticker, 'time'));
        const open = this.safeFloat (ticker, 'open');
        const last = this.safeFloat2 (ticker, 'price', 'close');
        let change = undefined;
        let percentage = undefined;
        let average = undefined;
        if ((last !== undefined) && (open !== undefined)) {
            average = this.sum (last, open) / 2;
            change = last - open;
            if (open > 0) {
                percentage = change / open * 100;
            }
        }
        const baseVolume = this.safeFloat (ticker, 'volume');
        const quoteVolume = this.safeFloat (ticker, 'quoteVolume');
        const vwap = this.vwap (baseVolume, quoteVolume);
        return {
            'symbol': symbol,
            'info': ticker,
            'timestamp': timestamp,
            'datetime': this.iso8601 (timestamp),
            'high': this.safeFloat (ticker, 'high'),
            'low': this.safeFloat (ticker, 'low'),
            'bid': this.safeFloat (ticker, 'bid'),
            'bidVolume': this.safeFloat (ticker, 'bidVolume'),
            'ask': this.safeFloat (ticker, 'ask'),
            'askVolume': this.safeFloat (ticker, 'askVolume'),
            'vwap': vwap,
            'open': open,
            'close': last,
            'last': last,
            'previousClose': undefined,
            'change': change,
            'percentage': percentage,
            'average': average,
            'baseVolume': baseVolume,
            'quoteVolume': quoteVolume,
        };
    }

    async fetchTicker (symbol, params = {}) {
        await this.loadMarkets ();
        const market = this.market (symbol);
        const request = {
            'tradingPair': market['id'],
        };
        const response = await this.publicGetTradingPairsTradingPairTicker (this.extend (request, params));
        //
        //     {
        //         "price":25087000,
        //         "ask":25107000,
        //         "askVolume":0.05837704,
        //         "bid":25087000,
        //         "bidVolume":0.00398628,
        //         "volume":350.09171591,
        //         "quoteVolume":8721016926.06529,
        //         "time":"2020-12-18T21:42:13.774Z",
        //     }
        //
        return this.parseTicker (response, market);
    }

    parseTickers (rawTickers, symbols = undefined) {
        const tickers = [];
        for (let i = 0; i < rawTickers.length; i++) {
            tickers.push (this.parseTicker (rawTickers[i]));
        }
        return this.filterByArray (tickers, 'symbol', symbols);
    }

    async fetchTickers (symbols = undefined, params = {}) {
        await this.loadMarkets ();
        const response = await this.publicGetTradingPairsStats (params);
        //
        //     [
        //         {
        //             "name":"ETH-KRW",
        //             "open":690500,
        //             "high":719500,
        //             "low":681500,
        //             "close":709500,
        //             "volume":2784.6081544,
        //             "time":"2020-12-18T21:54:50.795Z"
        //         }
        //     ]
        //
        return this.parseTickers (response, symbols);
    }

    parsePublicTrade (trade, market = undefined) {
        const timestamp = this.parse8601 (this.safeString (trade, 'time'));
        const price = this.safeFloat (trade, 'price');
        const amount = this.safeFloat (trade, 'amount');
        let symbol = undefined;
        if ('symbol' in market) {
            symbol = this.safeString (market, 'symbol');
        }
        return {
            'info': trade,
            'id': this.safeString (trade, 'id'),
            'timestamp': timestamp,
            'datetime': this.iso8601 (timestamp),
            'symbol': symbol,
            'order': undefined, // Not mandatory to specify
            'type': undefined, // Not mandatory to specify
            'side': this.safeString (trade, 'side'),
            'takerOrMaker': undefined,
            'price': price,
            'amount': amount,
            'cost': price * amount,
            'fee': undefined,
        };
    }

    parsePrivateTrade (trade, market = undefined) {
        const timestamp = this.parse8601 (this.safeString (trade, 'timestamp'));
        const symbol = this.safeString (trade, 'tradingPairName').replace ('-', '/');
        const side = this.safeString (trade, 'side');
        const price = this.safeFloat (trade, 'price');
        const amount = this.safeFloat (trade, 'baseAmount');
        let feeCurrency = symbol.slice (0, 3);
        if (side === 'sell') {
            feeCurrency = symbol.slice (4);
        }
        const fee = {
            'cost': this.safeFloat (trade, 'fee'),
            'currency': feeCurrency,
            'rate': undefined,
        };
        return {
            'info': trade,
            'id': this.safeString (trade, 'id'),
            'timestamp': timestamp,
            'datetime': this.iso8601 (timestamp),
            'symbol': symbol,
            'order': this.safeInteger (trade, 'orderId'),
            'type': undefined,
            'side': side,
            'takerOrMaker': this.safeString (trade, 'position'),
            'price': price,
            'amount': amount,
            'cost': price * amount,
            'fee': fee,
        };
    }

    parseTrade (trade, market = undefined) {
        //
        // public fetchTrades
        //
        //     {
        //         "time":"2020-12-19T12:17:43.000Z",
        //         "date":1608380263,
        //         "id":23903608,
        //         "price":25155000,
        //         "amount":0.0505,
        //         "side":"sell",
        //     }
        //
        // private fetchMyTrades
        //
        //     {
        //         "id": 73953,                             // trading event ID
        //         "orderId": 453324,                       // order ID
        //         "baseAmount": 3,                         // traded base asset amount
        //         "quoteAmount": 3000000,                  // traded quote asset amount
        //         "fee": 0.0012,                           // fee
        //         "price": 1000000,                        // price
        //         "timestamp": "2020-09-25T04:06:30.000Z", // trading time
        //         "side": "buy",                           // buy, sell
        //         "tradingPairName": "ZEC-KRW",            // order book
        //         "position": "maker"                      // maker, taker
        //     }
        //
        //     {
        //         "tradeId": 74072,            // trade ID
        //         "orderId": 453529,           // order ID
        //         "side": 2,                   // 1(bid), 2(ask)
        //         "type": 1,                   // 1(limit), 2(market)
        //         "baseAmount": 0.01,          // filled base asset amount (in ZEC for this case)
        //         "quoteAmount": 1,            // filled quote asset amount (in KRW for this case)
        //         "fee": 0.0004,               // fee
        //         "price": 100,                // price
        //         "isSelfTrade": false,        // whether both of matching orders are yours
        //         "occurredAt": 1603932107,    // trade occurrence time
        //         "tradingPairName": "ZEC-KRW" // order book
        //     }
        //
        const id = this.safeString2 (trade, 'id', 'tradeId');
        const orderId = this.safeInteger (trade, 'orderId');
        let timestamp = this.parse8601 (this.safeString2 (trade, 'time', 'timestamp'));
        timestamp = this.safeTimestamp (trade, 'occuredAt', timestamp);
        const marketId = this.safeString (trade, 'tradingPairName');
        market = this.safeMarket (marketId, market, '-');
        const symbol = market['symbol'];
        let side = this.safeString (trade, 'side');
        if (side === '1') {
            side = 'buy';
        } else if (side === '2') {
            side = 'sell';
        }
        let type = this.safeString (trade, 'type');
        if (type === '1') {
            type = 'limit';
        } else if (type === '2') {
            type = 'market';
        }
        const price = this.safeFloat (trade, 'price');
        const amount = this.safeFloat2 (trade, 'amount', 'baseAmount');
        let cost = this.safeFloat (trade, 'quoteAmount');
        if (cost === undefined) {
            if ((price !== undefined) && (amount !== undefined)) {
                cost = price * amount;
            }
        }
        const feeCost = this.safeFloat (trade, 'fee');
        let fee = undefined;
        if (feeCost !== undefined) {
            fee = {
                'cost': feeCost,
                'currency': market['base'],
            };
        }
        const takerOrMaker = this.safeString (trade, 'position');
        return {
            'info': trade,
            'id': id,
            'timestamp': timestamp,
            'datetime': this.iso8601 (timestamp),
            'symbol': symbol,
            'order': orderId,
            'type': undefined,
            'side': side,
            'takerOrMaker': takerOrMaker,
            'price': price,
            'amount': amount,
            'cost': cost,
            'fee': fee,
        };
    }

    async fetchTrades (symbol, since = undefined, limit = undefined, params = {}) {
        await this.loadMarkets ();
        const market = this.market (symbol);
        const request = {
            'tradingPair': market['id'],
            // 'limit': limit,
            // 'pastmax': id, // read data older than this ID
            // 'latestmin': id, // read data newer than this ID
            // 'after': parseInt (since / 1000),
            // 'before': this.seconds (),
        };
        if (since !== undefined) {
            request['after'] = parseInt (since / 1000);
        }
        if (limit !== undefined) {
            request['limit'] = limit;
        }
        const response = await this.publicGetTradingPairsTradingPairTrades (this.extend (request, params));
        //
        //     [
        //         {"time":"2020-12-19T12:17:43.000Z","date":1608380263,"id":23903608,"price":25155000,"amount":0.0505,"side":"sell"},
        //         {"time":"2020-12-19T12:17:13.000Z","date":1608380233,"id":23903604,"price":25140000,"amount":0.019,"side":"sell"},
        //         {"time":"2020-12-19T12:16:49.000Z","date":1608380209,"id":23903599,"price":25140000,"amount":0.0072,"side":"sell"},
        //     ]
        //
        return this.parseTrades (response, market, since, limit);
    }

    parseOHLCV (ohlcv, market = undefined) {
        //
        //     [
        //         1606780800000, // timestamp
        //         21293000,      // low
        //         21300000,      // high
        //         21294000,      // open
        //         21300000,      // close
        //         1.019126,      // volume
        //     ]
        //
        return [
            this.safeInteger (ohlcv, 0),
            this.safeFloat (ohlcv, 3),
            this.safeFloat (ohlcv, 2),
            this.safeFloat (ohlcv, 1),
            this.safeFloat (ohlcv, 4),
            this.safeFloat (ohlcv, 5),
        ];
    }

    async fetchOHLCV (symbol, timeframe = '1m', since = undefined, limit = undefined, params = {}) {
        await this.loadMarkets ();
        const market = this.market (symbol);
        limit = (limit === undefined) ? 1024 : limit; // default 1024
        const request = {
            'tradingPair': market['id'],
            // 'start': since,
            // 'end': this.milliseconds (),
            'interval': this.timeframes[timeframe],
        };
        const duration = this.parseTimeframe (timeframe);
        if (since === undefined) {
            const end = this.milliseconds ();
            request['end'] = end;
            request['start'] = end - limit * duration * 1000;
        } else {
            request['start'] = since;
            request['end'] = this.sum (since, limit * duration * 1000);
        }
        const response = await this.publicGetTradingPairsTradingPairCandles (this.extend (request, params));
        //
        //     [
        //         [1606780800000,21293000,21300000,21294000,21300000,1.019126],
        //         [1606780860000,21237000,21293000,21293000,21263000,0.96800057],
        //         [1606780920000,21240000,21240000,21240000,21240000,0.11068715],
        //     ]
        //
        return this.parseOHLCVs (response, market, timeframe, since, limit);
    }

    parseBalanceResponse (response) {
        const result = { 'info': response };
        for (let i = 0; i < response.length; i++) {
            const balance = response[i];
            const currencyId = this.safeString2 (balance, 'asset', 'isoAlpha3');
            const code = this.safeCurrencyCode (currencyId);
            const hold = this.safeFloat (balance, 'hold');
            const pendingWithdrawal = this.safeFloat (balance, 'pendingWithdrawal');
            const account = this.account ();
            account['free'] = this.safeFloat (balance, 'avail');
            account['used'] = this.sum (hold, pendingWithdrawal);
            result[code] = account;
        }
        return this.parseBalance (result);
    }

    async fetchBalance (params = {}) {
        await this.loadMarkets ();
        const response = await this.privateGetBalances (params);
        //
        //     [
        //         {
        //             "asset": "KRW",                   // asset name
        //             "avail": 1759466.76,              // available amount to place order
        //             "hold": 16500,                    // outstanding amount on order books
        //             "pendingWithdrawal": 0,           // amount being withdrawn
        //             "lastUpdatedAt": "1600684352032", // balance last update time
        //         },
        //     ]
        //
        return this.parseBalanceResponse (response);
    }

    parseOrderStatus (status) {
        const statuses = {
            'placed': 'open',
            'cancelled': 'canceled',
            'completed': 'closed',
            'updated': 'open',
            'reserved': 'open',
        };
        return this.safeString (statuses, status, status);
    }

    parseOrder (order, market = undefined) {
        //
        // cancelOrder
        //
        //     {} // empty object
        //
        // fetchOrder, fetchOrders, fetchOpenOrders, createOrder
        //
        //     {
        //         "id": "453324",                          // order ID
        //         "clientOrderId": "zeckrw23456",          // client order ID (showed only when it exists)
        //         "status": "updated",                     // placed, cancelled, completed, updated, reserved
        //         "forcedCompletionReason": undefined,     // the reason in case it was canceled in the middle (protection or timeInForce)
        //         "tradingPairName": "ZEC-KRW",            // order book
        //         "side": "buy",                           // buy, sell
        //         "type": "limit",                         // limit, market
        //         "price": 1000000,                        // price
        //         "stopPrice": undefined,                  // stop price (showed only for stop orders)
        //         "amount": 4,                             // initial amount
        //         "remaining": 1,                          // outstanding amount
        //         "protection": "yes",                     // whether protection is activated (yes or no)
        //         "timeInForce": "gtc",                    // limit order's time in force (gtc/po/ioc/fok)
        //         "createdAt": "2020-09-25T04:06:20.000Z", // order placement time
        //         "updatedAt": "2020-09-25T04:06:29.000Z", // order last update time
        //         "balanceChange": {
        //             "baseGross": 3,                      // base asset balance's gross change (in ZEC for this case)
        //             "baseFee": {
        //                 "taking": 0,                     // base asset fee imposed as taker
        //                 "making": -0.0012                // base asset fee imposed as maker
        //             },
        //             "baseNet": 2.9988,                   // base asset balance's net change (in ZEC for this case)
        //             "quoteGross": -3000000,              // quote asset balance's gross change (in KRW for
        //             "quoteFee": {
        //                 "taking": 0,                     // quote asset fee imposed as taker
        //                 "making": 0                      // quote asset fee imposed as maker
        //             },
        //             "quoteNet": -3000000                 // quote asset balance's net change (in KRW for this case)
        //         }
        //     }
        //
        const id = this.safeString (order, 'id');
        const clientOrderId = this.safeString (order, 'clientOrderId');
        const timestamp = this.parse8601 (this.safeString (order, 'createdAt'));
        const type = this.safeString (order, 'type');
        const side = this.safeString (order, 'side');
        const timeInForce = this.safeStringUpper (order, 'timeInForce');
        const price = this.safeFloat (order, 'price');
        const amount = this.safeFloat (order, 'amount');
        const stopPrice = this.safeFloat (order, 'stopPrice');
        const remaining = this.safeFloat (order, 'remaining');
        const marketId = this.safeString (order, 'tradingPairName');
        market = this.safeMarket (marketId, market, '-');
        const status = this.parseOrderStatus (this.safeString (order, 'status'));
        const balanceChange = this.safeValue (order, 'balanceChange', {});
        let filled = this.safeFloat (balanceChange, 'baseNet');
        let cost = this.safeFloat (balanceChange, 'quoteNet');
        if (cost !== undefined) {
            cost = Math.abs (cost);
        }
        let updated = undefined;
        if ((filled === undefined) && (amount !== undefined) && (remaining !== undefined)) {
            filled = Math.max (0, amount - remaining);
        }
        if ((filled !== undefined) && (filled > 0)) {
            updated = this.parse8601 (this.safeString (order, 'updatedAt'));
        }
        if ((cost === undefined) && (price !== undefined) && (filled !== undefined)) {
            cost = filled * price;
        }
        let fee = undefined;
        if (side === 'buy') {
            const baseFee = this.safeValue (balanceChange, 'baseFee', {});
            const taking = this.safeFloat (baseFee, 'taking');
            const making = this.safeFloat (baseFee, 'making');
            fee = {
                'currency': market['base'],
                'cost': this.sum (taking, making),
            };
        } else {
            const quoteFee = this.safeValue (balanceChange, 'quoteFee', {});
            const taking = this.safeFloat (quoteFee, 'taking');
            const making = this.safeFloat (quoteFee, 'making');
            fee = {
                'currency': market['quote'],
                'cost': this.sum (taking, making),
            };
        }
        let postOnly = undefined;
        if (timeInForce !== undefined) {
            postOnly = (timeInForce === 'PO');
        }
        return {
            'id': id,
            'clientOrderId': clientOrderId,
            'datetime': this.iso8601 (timestamp),
            'timestamp': timestamp,
            'lastTradeTimestamp': updated,
            'status': status,
            'symbol': market['symbol'],
            'type': type,
            'timeInForce': timeInForce,
            'postOnly': postOnly,
            'side': side,
            'price': price,
            'stopPrice': stopPrice,
            'average': undefined,
            'amount': amount,
            'filled': filled,
            'remaining': remaining,
            'cost': cost,
            'trades': undefined,
            'fee': fee,
            'info': order,
        };
    }

    async fetchOrder (id, symbol = undefined, params = {}) {
        await this.loadMarkets ();
        let method = undefined;
        const clientOrderId = this.safeString (params, 'clientOrderId');
        params = this.omit (params, 'clientOrderId');
        const request = {};
        if (clientOrderId === undefined) {
            method = 'privateGetOrdersOrderId';
            request['orderId'] = id;
        } else {
            method = 'privateGetOrdersClientOrderIdClientOrderId';
            request['clientOrderId'] = clientOrderId;
        }
        const response = await this[method] (this.extend (request, params));
        //
        //     {
        //         "id": "453324",                          // order ID
        //         "clientOrderId": "zeckrw23456",          // client order ID (showed only when it exists)
        //         "status": "updated",                     // placed, cancelled, completed, updated, reserved
        //         "forcedCompletionReason": undefined,     // the reason in case it was canceled in the middle (protection or timeInForce)
        //         "tradingPairName": "ZEC-KRW",            // order book
        //         "side": "buy",                           // buy, sell
        //         "type": "limit",                         // limit, market
        //         "price": 1000000,                        // price
        //         "stopPrice": undefined,                  // stop price (showed only for stop orders)
        //         "amount": 4,                             // initial amount
        //         "remaining": 1,                          // outstanding amount
        //         "protection": "yes",                     // whether protection is activated (yes or no)
        //         "timeInForce": "gtc",                    // limit order's time in force (gtc/po/ioc/fok)
        //         "createdAt": "2020-09-25T04:06:20.000Z", // order placement time
        //         "updatedAt": "2020-09-25T04:06:29.000Z", // order last update time
        //         "balanceChange": {
        //             "baseGross": 3,                      // base asset balance's gross change (in ZEC for this case)
        //             "baseFee": {
        //                 "taking": 0,                     // base asset fee imposed as taker
        //                 "making": -0.0012                // base asset fee imposed as maker
        //             },
        //             "baseNet": 2.9988,                   // base asset balance's net change (in ZEC for this case)
        //             "quoteGross": -3000000,              // quote asset balance's gross change (in KRW for
        //             "quoteFee": {
        //                 "taking": 0,                     // quote asset fee imposed as taker
        //                 "making": 0                      // quote asset fee imposed as maker
        //             },
        //             "quoteNet": -3000000                 // quote asset balance's net change (in KRW for this case)
        //         }
        //     }
        //
        return this.parseOrder (response);
    }

    async fetchOrders (symbol = undefined, since = undefined, limit = undefined, params = {}) {
        await this.loadMarkets ();
        const request = {
            'includePast': 'true', // if true, completed and canceled orders are included as the result, they are accessible for one hour only from its completion or cancellation time
            // 'pagination': 'false', // if the result is more than 3,000 orders, set this value as true to access 1000 orders at max per each page
        };
        let market = undefined;
        if (symbol !== undefined) {
            market = this.market (symbol);
        }
        const response = await this.privateGetOrders (this.extend (request, params));
        //
        //     [
        //         {
        //             "id": "453324",                          // order ID
        //             "clientOrderId": "zeckrw23456",          // client order ID (showed only when it exists)
        //             "status": "updated",                     // placed, cancelled, completed, updated, reserved
        //             "forcedCompletionReason": undefined,     // the reason in case it was canceled in the middle (protection or timeInForce)
        //             "tradingPairName": "ZEC-KRW",            // order book
        //             "side": "buy",                           // buy, sell
        //             "type": "limit",                         // limit, market
        //             "price": 1000000,                        // price
        //             "stopPrice": undefined,                  // stop price (showed only for stop orders)
        //             "amount": 4,                             // initial amount
        //             "remaining": 1,                          // outstanding amount
        //             "protection": "yes",                     // whether protection is activated (yes or no)
        //             "timeInForce": "gtc",                    // limit order's time in force (gtc/po/ioc/fok)
        //             "createdAt": "2020-09-25T04:06:20.000Z", // order placement time
        //             "updatedAt": "2020-09-25T04:06:29.000Z", // order last update time
        //             "balanceChange": {
        //                 "baseGross": 3,                      // base asset balance's gross change (in ZEC for this case)
        //                 "baseFee": {
        //                     "taking": 0,                     // base asset fee imposed as taker
        //                     "making": -0.0012                // base asset fee imposed as maker
        //                 },
        //                 "baseNet": 2.9988,                   // base asset balance's net change (in ZEC for this case)
        //                 "quoteGross": -3000000,              // quote asset balance's gross change (in KRW for
        //                 "quoteFee": {
        //                     "taking": 0,                     // quote asset fee imposed as taker
        //                     "making": 0                      // quote asset fee imposed as maker
        //                 },
        //                 "quoteNet": -3000000                 // quote asset balance's net change (in KRW for this case)
        //             }
        //         },
        //     ]
        //
        return this.parseOrders (response, market, since, limit);
    }

    async fetchOpenOrders (symbol = undefined, since = undefined, limit = undefined, params = {}) {
        const request = {
            'includePast': 'false',
        };
        return await this.fetchOrders (symbol, since, limit, this.extend (request, params));
    }

    async createOrder (symbol, type, side, amount, price = undefined, params = {}) {
        await this.loadMarkets ();
        const market = this.market (symbol);
        const request = {
            // 'clientOrderId': 'test4321', // max 20 characters of [a-zA-Z0-9_-]
            'tradingPairName': market['id'],
            'side': side, // buy, sell
            'type': type, // limit, market
            // 'price': this.priceToPrecision (symbol, price),
            // 'stopPrice': this.priceToPrecision (symbol, stopPrice), // optional, becomes a stop order if set
            // 'amount': this.amountToPrecision (symbol, amount),
            // 'protection': 'no', // whether protection is activated
            // 'timeInForce': 'gtc', // gtc, po, ioc, fok
        };
        if (type === 'limit') {
            request['price'] = this.priceToPrecision (symbol, price);
            request['amount'] = this.amountToPrecision (symbol, amount);
        } else if (type === 'market') {
            // for market buy it requires the amount of quote currency to spend
            if (side === 'buy') {
                let total = amount;
                const createMarketBuyOrderRequiresPrice = this.safeValue (this.options, 'createMarketBuyOrderRequiresPrice', true);
                if (createMarketBuyOrderRequiresPrice) {
                    if (price === undefined) {
                        throw new InvalidOrder (this.id + " createOrder() requires the price argument with market buy orders to calculate total order cost (amount to spend), where cost = amount * price. Supply a price argument to createOrder() call if you want the cost to be calculated for you from price and amount, or, alternatively, add .options['createMarketBuyOrderRequiresPrice'] = false and supply the total cost value in the 'amount' argument");
                    }
                    total = price * amount;
                }
                const precision = market['precision']['price'];
                request['amount'] = this.decimalToPrecision (total, TRUNCATE, precision, this.precisionMode);
            } else {
                request['amount'] = this.amountToPrecision (symbol, amount);
            }
        }
        const clientOrderId = this.safeString (params, 'clientOrderId');
        if (clientOrderId !== undefined) {
            request['clientOrderId'] = clientOrderId;
            params = this.omit (params, 'clientOrderId');
        }
        const stopPrice = this.safeFloat (params, 'stopPrice');
        if (stopPrice !== undefined) {
            request['stopPrice'] = this.priceToPrecision (symbol, stopPrice);
            params = this.omit (params, 'stopPrice');
        }
        const timeInForce = this.safeStringLower (params, 'timeInForce');
        if (timeInForce !== undefined) {
            request['timeInForce'] = timeInForce;
            params = this.omit (params, 'timeInForce');
        }
        const response = await this.privatePostOrders (this.extend (request, params));
        //
        //     {
        //         "id": "453327",                          // order ID
        //         "clientOrderId": "test4321",             // client order ID (showed only when it exists)
        //         "status": "reserved",                    // placed, cancelled, completed, updated, reserved
        //         "forcedCompletionReason": undefined,     // the reason in case it was canceled in the middle (protection or timeInForce)
        //         "tradingPairName": "BCH-KRW",            // order book
        //         "side": "sell",                          // buy, sell
        //         "type": "limit",                         // limit, market
        //         "price": 11000000,                       // price
        //         "stopPrice": 12000000,                   // stop price (showed only for stop orders)
        //         "amount": 0.5,                           // initial amount
        //         "remaining": 0.5,                        // outstanding amount
        //         "protection": "no",                      // whether protection is activated (yes or no)
        //         "timeInForce": "gtc",                    // limit order's time in force (gtc/po/ioc/fok)
        //         "createdAt": "2020-09-25T04:51:31.000Z", // order placement time
        //         "balanceChange": {
        //             "baseGross": 0,                      // base asset balance's gross change (in BCH for this case)
        //             "baseFee": {
        //                 "taking": 0,                     // base asset fee imposed as taker
        //                 "making": 0                      // base asset fee imposed as maker
        //             },
        //             "baseNet": 0,                        // base asset balance's net change (in BCH for this case)
        //             "quoteGross": 0,                     // quote asset balance's gross change (in KRW for
        //             "quoteFee": {
        //                 "taking": 0,                     // quote asset fee imposed as taker
        //                 "making": 0                      // quote asset fee imposed as maker
        //             },
        //             "quoteNet": 0                        // quote asset balance's net change (in KRW for this case)
        //         }
        //     }
        //
        return this.parseOrder (response, market);
    }

    async cancelOrder (id, symbol = undefined, params = {}) {
        await this.loadMarkets ();
        const request = {};
        const clientOrderId = this.safeString (params, 'clientOrderId');
        let method = undefined;
        if (clientOrderId === undefined) {
            method = 'privateDeleteOrdersOrderId';
            request['orderId'] = id;
        } else {
            method = 'privateDeleteOrdersClientOrderIdClientOrderId';
            request['clientOrderId'] = clientOrderId;
            params = this.omit (params, 'clientOrderId');
        }
        const response = await this[method] (this.extend (request, params));
        //
        //     {}
        //
        const order = this.parseOrder (response);
        return this.extend (order, { 'id': id });
    }

    async fetchMyTrades (symbol = undefined, since = undefined, limit = undefined, params = {}) {
        await this.loadMarkets ();
        const request = {
            // 'limit': limit, // max 100
            // 'pastmax': id, // read data older than this id
            // 'latestmin': id, // read data newer than this id
            // 'after': parseInt (since / 1000), // Read data after this timestamp in seconds
            // 'before': this.seconds (), // Read data before this timestamp in seconds
            'deepSearch': 'true', // read data older than one month ago are inclusively looked up only when it is "true"
        };
        if (since !== undefined) {
            request['after'] = parseInt (since / 1000);
        }
        if (limit !== undefined) {
            request['limit'] = limit;
        }
        const response = await this.privateGetTrades (this.extend (request, params));
        //
        //     [
        //         {
        //             "id": 73953,                             // trading event ID
        //             "orderId": 453324,                       // order ID
        //             "baseAmount": 3,                         // traded base asset amount
        //             "quoteAmount": 3000000,                  // traded quote asset amount
        //             "fee": 0.0012,                           // fee
        //             "price": 1000000,                        // price
        //             "timestamp": "2020-09-25T04:06:30.000Z", // trading time
        //             "side": "buy",                           // buy, sell
        //             "tradingPairName": "ZEC-KRW",            // order book
        //             "position": "maker"                      // maker, taker
        //         },
        //     ]
        //
        let market = undefined;
        if (symbol !== undefined) {
            market = this.market (symbol);
        }
        return this.parseTrades (response, market, since, limit);
    }

    parseDepositAddress (depositAddress, currency = undefined) {
        //
        //     {
        //         "asset": "BTC",                                  // asset name
        //         "address": "1CwC2cMFu1jRQUBtw925cENbT1kctJBMdm", // deposit address
        //         "memoId": null,                                  // memo ID (showed only for assets using memo ID)
        //         "createdAt": 1594802312                          // deposit address creation time
        //     }
        //
        const address = this.safeString (depositAddress, 'address');
        const tag = this.safeString (depositAddress, 'memoId');
        const currencyId = this.safeString (depositAddress, 'asset');
        const code = this.safeCurrencyCode (currencyId);
        this.checkAddress (address);
        return {
            'currency': code,
            'address': address,
            'tag': tag,
            'info': depositAddress,
        };
    }

    parseDepositAddresses (addresses, codes = undefined) {
        let result = [];
        for (let i = 0; i < addresses.length; i++) {
            const address = this.parseDepositAddress (addresses[i]);
            result.push (address);
        }
        if (codes) {
            result = this.filterByArray (result, 'currency', codes);
        }
        return this.indexBy (result, 'currency');
    }

    async fetchDepositAddresses (codes = undefined, params = {}) {
        await this.loadMarkets ();
        const response = await this.privateGetCryptoDepositAddresses (params);
        //
        //     [
        //         {
        //             "asset": "BTC",                                  // asset name
        //             "address": "1CwC2cMFu1jRQUBtw925cENbT1kctJBMdm", // deposit address
        //             "memoId": null,                                  // memo ID (showed only for assets using memo ID)
        //             "createdAt": 1594802312                          // deposit address creation time
        //         },
        //     ]
        //
        return this.parseDepositAddresses (response, codes);
    }

    async fetchDepositAddress (code, params = {}) {
        await this.loadMarkets ();
        const response = await this.fetchDepositAddresses (undefined, params);
        const address = this.safeValue (response, code);
        if (address === undefined) {
            throw new InvalidAddress (this.id + ' fetchDepositAddress() ' + code + ' address not found');
        }
        return address;
    }

    parseTransactionStatus (status) {
        const statuses = {
            'reviewing': 'pending',
            'rejected': 'rejected',
            'processing': 'pending',
            'failed': 'failed',
            'completed': 'ok',
        };
        return this.safeString (statuses, status, status);
    }

    parseTransaction (transaction, currency = undefined) {
        //
        //     {
        //         "id": 640,                     // deposit/withdrawal event ID
        //         "asset": "BTC",                // asset name
        //         "type": "crypto_withdrawal",   // fiat_withdrawal, fiat_deposit, crypto_withdrawal, crypto_deposit
        //         "netAmount": 0.0001,           // amount
        //         "feeAmount": 0.0005,           // fee (null if there is no imposed fee)
        //         "status": "completed",         // reviewing, rejected, processing, failed, completed
        //         "reviewStartedAt": 1595556218, // request time
        //         "completedAt": 1595556902,     // completion time (showed only in case of completed)
        //         "txId": "eaca5ad3...",         // tx ID
        //         "sourceAddress": null,         // sender address (showed only in case of crypto_deposit)
        //         "destinationAddress: "3H8...", // recipient address (showed only in case of crypto_withdrawal)
        //         "destinationMemoId": null      // recipient address's memo ID
        //     }
        //
        const id = this.safeString (transaction, 'id');
        const txid = this.safeString (transaction, 'txId');
        const currencyId = this.safeString (transaction, 'asset');
        const code = this.safeCurrencyCode (currencyId, currency);
        let type = this.safeString (transaction, 'type');
        if ((type === 'crypto_withdrawal') || (type === 'fiat_withdrawal')) {
            type = 'withdrawal';
        } else if ((type === 'crypto_deposit' || type === 'fiat_deposit')) {
            type = 'deposit';
        }
        const amount = this.safeFloat (transaction, 'netAmount');
        const feeCost = this.safeFloat (transaction, 'feeAmount');
        let fee = undefined;
        if (feeCost !== undefined) {
            fee = {
                'cost': feeCost,
                'currency': code,
            };
        }
        const timestamp = this.safeTimestamp (transaction, 'reviewStartedAt');
        const updated = this.safeTimestamp (transaction, 'completedAt');
        const addressFrom = this.safeString (transaction, 'sourceAddress');
        const addressTo = this.safeString (transaction, 'destinationAddress');
        const tagFrom = this.safeString (transaction, 'sourceMemoId');
        const tagTo = this.safeString (transaction, 'destinationMemoId');
        const status = this.parseTransactionStatus (this.safeString (transaction, 'status'));
        return {
            'info': transaction,
            'id': id,
            'txid': txid,
            'timestamp': timestamp,
            'datetime': this.iso8601 (timestamp),
            'addressFrom': addressFrom,
            'address': addressTo,
            'addressTo': addressTo,
            'tagFrom': tagFrom,
            'tag': tagTo,
            'tagTo': tagTo,
            'type': type,
            'amount': amount,
            'currency': code,
            'status': status,
            'updated': updated,
            'comment': undefined,
            'fee': fee,
        };
    }

    async fetchTransactions (code = undefined, since = undefined, limit = undefined, params = {}) {
        await this.loadMarkets ();
        const request = {
            // 'limit': limit, // max 20
            // 'latestmin': limit, // read data older than this id
            // 'after': this.milliseconds (),
            // 'before': since,
            // 'completedOnly': 'no',
        };
        if (since !== undefined) {
            request['before'] = since;
        }
        if (limit !== undefined) {
            request['limit'] = limit;
        }
        const response = await this.privateGetDepositWithdrawalStatus (this.extend (request, params));
        //
        //     [
        //         {
        //             "id": 640,                     // deposit/withdrawal event ID
        //             "asset": "BTC",                // asset name
        //             "type": "crypto_withdrawal",   // fiat_withdrawal, fiat_deposit, crypto_withdrawal, crypto_deposit
        //             "netAmount": 0.0001,           // amount
        //             "feeAmount": 0.0005,           // fee (null if there is no imposed fee)
        //             "status": "completed",         // reviewing, rejected, processing, failed, completed
        //             "reviewStartedAt": 1595556218, // request time
        //             "completedAt": 1595556902,     // completion time (showed only in case of completed)
        //             "txId": "eaca5ad3...",         // tx ID
        //             "sourceAddress": null,         // sender address (showed only in case of crypto_deposit)
        //             "destinationAddress: "3H8...", // recipient address (showed only in case of crypto_withdrawal)
        //             "destinationMemoId": null      // recipient address's memo ID
        //         },
        //     ]
        //
        let currency = undefined;
        if (code !== undefined) {
            currency = this.currency (code);
        }
        return this.parseTransactions (response, currency, since, limit, params);
    }

    nonce () {
        return this.milliseconds ();
    }

    sign (path, api = 'public', method = 'GET', params = {}, headers = undefined, body = undefined) { // for authentication in private API calls
        const endpoint = '/' + this.implodeParams (path, params);
        let url = this.implodeParams (this.urls['api'][api], { 'hostname': this.hostname }) + endpoint;
        const query = this.omit (params, this.extractParams (path));
        if (api === 'public') {
            if (Object.keys (query).length) {
                url += '?' + this.urlencode (query);
            }
        } else if (api === 'private') {
            this.checkRequiredCredentials ();
            const timestamp = this.nonce ().toString ();
            let auth = 't' + timestamp + method + endpoint;
            headers = {
                'api-key': this.apiKey,
                'timestamp': timestamp,
            };
            if (method === 'POST') {
                headers['Content-Type'] = 'application/json';
                body = this.json (params);
                auth += body;
            } else if (endpoint === '/orders') {
                if (Object.keys (query).length) {
                    const urlQuery = '?' + this.urlencode (query);
                    auth += urlQuery;
                    url += urlQuery;
                }
            } else if (method === 'GET') {
                if (Object.keys (query).length) {
                    url += '?' + this.urlencode (query);
                }
            }
            const rawSecret = this.base64ToBinary (this.secret);
            const signature = this.hmac (this.encode (auth), rawSecret, 'sha512', 'base64');
            headers['signature'] = signature;
        }
        return { 'url': url, 'method': method, 'body': body, 'headers': headers };
    }

    handleErrors (code, reason, url, method, headers, body, response, requestHeaders, requestBody) {
        if (response === undefined) {
            return;
        }
        //
        //     {"errorMessage":"Invalid API key","errorCode":10155}
        //
        if (!Array.isArray (response)) {
            const errorCode = this.safeString (response, 'errorCode');
            const errorMessage = this.safeString (response, 'errorMessage');
            const feedback = this.id + ' ' + body;
            if (errorMessage !== undefined) {
                this.throwBroadlyMatchedException (this.exceptions['broad'], body, feedback);
            }
            if (errorCode !== undefined) {
                this.throwExactlyMatchedException (this.exceptions['exact'], errorCode, feedback);
            }
            if ((errorCode !== undefined) || (errorMessage !== undefined)) {
                throw new ExchangeError (feedback);
            }
        }
    }
};
