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

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

from ccxt.async_support.base.exchange import Exchange
import hashlib
import math
from ccxt.base.errors import ExchangeError
from ccxt.base.errors import AuthenticationError
from ccxt.base.errors import BadRequest
from ccxt.base.errors import InsufficientFunds
from ccxt.base.errors import InvalidAddress
from ccxt.base.errors import InvalidOrder
from ccxt.base.errors import NotSupported
from ccxt.base.errors import DDoSProtection
from ccxt.base.errors import ExchangeNotAvailable
from ccxt.base.decimal_to_precision import PAD_WITH_ZERO


class idex(Exchange):

    def describe(self):
        return self.deep_extend(super(idex, self).describe(), {
            'id': 'idex',
            'name': 'IDEX',
            'countries': ['US'],
            'rateLimit': 1500,
            'version': 'v2',
            'certified': True,
            'pro': True,
            'requiresWeb3': True,
            'has': {
                'cancelOrder': True,
                'createOrder': True,
                'fetchBalance': True,
                'fetchMarkets': True,
                'fetchCurrencies': True,
                'fetchMyTrades': True,
                'fetchOHLCV': True,
                'fetchOpenOrders': True,
                'fetchClosedOrders': True,
                'fetchOrders': False,
                'fetchOrder': True,
                'fetchOrderBook': True,
                'fetchTicker': True,
                'fetchTickers': True,
                'fetchTrades': True,
                'fetchTransactions': False,
                'fetchDeposits': True,
                'fetchWithdrawals': True,
                'withdraw': True,
            },
            'timeframes': {
                '1m': '1m',
                '5m': '5m',
                '15m': '15m',
                '30m': '30m',
                '1h': '1h',
                '6h': '6h',
                '1d': '1d',
            },
            'urls': {
                'test': {
                    'public': 'https://api-sandbox.idex.io',
                    'private': 'https://api-sandbox.idex.io',
                },
                'logo': 'https://user-images.githubusercontent.com/51840849/94481303-2f222100-01e0-11eb-97dd-bc14c5943a86.jpg',
                'api': {
                    'ETH': 'https://api-eth.idex.io',
                    'BSC': 'https://api-bsc.idex.io',
                },
                'www': 'https://idex.io',
                'doc': [
                    'https://docs.idex.io/',
                ],
            },
            'api': {
                'public': {
                    'get': [
                        'ping',
                        'time',
                        'exchange',
                        'assets',
                        'markets',
                        'tickers',
                        'candles',
                        'trades',
                        'orderbook',
                        'wsToken',
                    ],
                },
                'private': {
                    'get': [
                        'user',
                        'wallets',
                        'balances',
                        'orders',
                        'fills',
                        'deposits',
                        'withdrawals',
                    ],
                    'post': [
                        'wallets',
                        'orders',
                        'orders/test',
                        'withdrawals',
                    ],
                    'delete': [
                        'orders',
                    ],
                },
            },
            'options': {
                'defaultTimeInForce': 'gtc',
                'defaultSelfTradePrevention': 'cn',
                'network': 'ETH',  # also supports BSC
            },
            'exceptions': {
                'INVALID_ORDER_QUANTITY': InvalidOrder,
                'INSUFFICIENT_FUNDS': InsufficientFunds,
                'SERVICE_UNAVAILABLE': ExchangeNotAvailable,
                'EXCEEDED_RATE_LIMIT': DDoSProtection,
                'INVALID_PARAMETER': BadRequest,
                'WALLET_NOT_ASSOCIATED': InvalidAddress,
                'INVALID_WALLET_SIGNATURE': AuthenticationError,
            },
            'requiredCredentials': {
                'walletAddress': True,
                'privateKey': True,
                'apiKey': True,
                'secret': True,
            },
            'paddingMode': PAD_WITH_ZERO,
            'commonCurrencies': {},
            'requireCredentials': {
                'privateKey': True,
                'apiKey': True,
                'secret': True,
            },
        })

    async def fetch_markets(self, params={}):
        # [
        #   {
        #     market: 'DIL-ETH',
        #     status: 'active',
        #     baseAsset: 'DIL',
        #     baseAssetPrecision: 8,
        #     quoteAsset: 'ETH',
        #     quoteAssetPrecision: 8
        #   }, ...
        # ]
        response = await self.publicGetMarkets(params)
        result = []
        for i in range(0, len(response)):
            entry = response[i]
            marketId = self.safe_string(entry, 'market')
            baseId = self.safe_string(entry, 'baseAsset')
            quoteId = self.safe_string(entry, 'quoteAsset')
            base = self.safe_currency_code(baseId)
            quote = self.safe_currency_code(quoteId)
            symbol = base + '/' + quote
            basePrecision = self.safe_integer(entry, 'baseAssetPrecision')
            quotePrecision = self.safe_integer(entry, 'quoteAssetPrecision')
            status = self.safe_string(entry, 'status')
            active = status == 'active'
            precision = {
                'amount': basePrecision,
                'price': quotePrecision,
            }
            result.append({
                'symbol': symbol,
                'id': marketId,
                'base': base,
                'quote': quote,
                'baseId': baseId,
                'quoteId': quoteId,
                'active': active,
                'info': entry,
                'precision': precision,
                'limits': {
                    'amount': {
                        'min': math.pow(10, -precision['amount']),
                        'max': None,
                    },
                    'price': {
                        'min': None,
                        'max': None,
                    },
                    'cost': {
                        'min': None,
                        'max': None,
                    },
                },
            })
        return result

    async def fetch_ticker(self, symbol, params={}):
        await self.load_markets()
        market = self.market(symbol)
        request = {
            'market': market['id'],
        }
        # [
        #   {
        #     market: 'DIL-ETH',
        #     time: 1598367493008,
        #     open: '0.09695361',
        #     high: '0.10245881',
        #     low: '0.09572507',
        #     close: '0.09917079',
        #     closeQuantity: '0.71320950',
        #     baseVolume: '309.17380612',
        #     quoteVolume: '30.57633981',
        #     percentChange: '2.28',
        #     numTrades: 205,
        #     ask: '0.09910476',
        #     bid: '0.09688340',
        #     sequence: 3902
        #   }
        # ]
        response = await self.publicGetTickers(self.extend(request, params))
        ticker = self.safe_value(response, 0)
        return self.parse_ticker(ticker, market)

    async def fetch_tickers(self, symbols=None, params={}):
        await self.load_markets()
        # [
        #   {
        #     market: 'DIL-ETH',
        #     time: 1598367493008,
        #     open: '0.09695361',
        #     high: '0.10245881',
        #     low: '0.09572507',
        #     close: '0.09917079',
        #     closeQuantity: '0.71320950',
        #     baseVolume: '309.17380612',
        #     quoteVolume: '30.57633981',
        #     percentChange: '2.28',
        #     numTrades: 205,
        #     ask: '0.09910476',
        #     bid: '0.09688340',
        #     sequence: 3902
        #   }, ...
        # ]
        response = await self.publicGetTickers(params)
        return self.parse_tickers(response, symbols)

    def parse_tickers(self, rawTickers, symbols=None):
        tickers = []
        for i in range(0, len(rawTickers)):
            tickers.append(self.parse_ticker(rawTickers[i]))
        return self.filter_by_array(tickers, 'symbol', symbols)

    def parse_ticker(self, ticker, market=None):
        # {
        #   market: 'DIL-ETH',
        #   time: 1598367493008,
        #   open: '0.09695361',
        #   high: '0.10245881',
        #   low: '0.09572507',
        #   close: '0.09917079',
        #   closeQuantity: '0.71320950',
        #   baseVolume: '309.17380612',
        #   quoteVolume: '30.57633981',
        #   percentChange: '2.28',
        #   numTrades: 205,
        #   ask: '0.09910476',
        #   bid: '0.09688340',
        #   sequence: 3902
        # }
        marketId = self.safe_string(ticker, 'market')
        symbol = self.safe_symbol(marketId, market, '-')
        baseVolume = self.safe_float(ticker, 'baseVolume')
        quoteVolume = self.safe_float(ticker, 'quoteVolume')
        timestamp = self.safe_integer(ticker, 'time')
        open = self.safe_float(ticker, 'open')
        high = self.safe_float(ticker, 'high')
        low = self.safe_float(ticker, 'low')
        close = self.safe_float(ticker, 'close')
        ask = self.safe_float(ticker, 'ask')
        bid = self.safe_float(ticker, 'bid')
        percentage = self.safe_float(ticker, 'percentChange')
        if percentage is not None:
            percentage = 1 + percentage / 100
        change = None
        if (close is not None) and (open is not None):
            change = close - open
        return {
            'symbol': symbol,
            'timestamp': timestamp,
            'datetime': self.iso8601(timestamp),
            'high': high,
            'low': low,
            'bid': bid,
            'bidVolume': None,
            'ask': ask,
            'askVolume': None,
            'vwap': None,
            'open': open,
            'close': close,
            'last': close,
            'previousClose': None,
            'change': change,
            'percentage': percentage,
            'average': None,
            'baseVolume': baseVolume,
            'quoteVolume': quoteVolume,
            'info': ticker,
        }

    async def fetch_ohlcv(self, symbol, timeframe='1m', since=None, limit=None, params={}):
        await self.load_markets()
        market = self.market(symbol)
        request = {
            'market': market['id'],
            'interval': timeframe,
        }
        if since is not None:
            request['start'] = since
        if limit is not None:
            request['limit'] = limit
        response = await self.publicGetCandles(self.extend(request, params))
        if isinstance(response, list):
            # [
            #   {
            #     start: 1598345580000,
            #     open: '0.09771286',
            #     high: '0.09771286',
            #     low: '0.09771286',
            #     close: '0.09771286',
            #     volume: '1.45340410',
            #     sequence: 3853
            #   }, ...
            # ]
            return self.parse_ohlcvs(response, market, timeframe, since, limit)
        else:
            #  {"nextTime":1595536440000}
            return []

    def parse_ohlcv(self, ohlcv, market=None):
        # {
        #   start: 1598345580000,
        #   open: '0.09771286',
        #   high: '0.09771286',
        #   low: '0.09771286',
        #   close: '0.09771286',
        #   volume: '1.45340410',
        #   sequence: 3853
        # }
        timestamp = self.safe_integer(ohlcv, 'start')
        open = self.safe_float(ohlcv, 'open')
        high = self.safe_float(ohlcv, 'high')
        low = self.safe_float(ohlcv, 'low')
        close = self.safe_float(ohlcv, 'close')
        volume = self.safe_float(ohlcv, 'volume')
        return [timestamp, open, high, low, close, volume]

    async def fetch_trades(self, symbol, since=None, limit=None, params={}):
        await self.load_markets()
        market = self.market(symbol)
        request = {
            'market': market['id'],
        }
        if since is not None:
            request['start'] = since
        if limit is not None:
            request['limit'] = limit
        # [
        #   {
        #     fillId: 'b5467d00-b13e-3fa9-8216-dd66735550fc',
        #     price: '0.09771286',
        #     quantity: '1.45340410',
        #     quoteQuantity: '0.14201627',
        #     time: 1598345638994,
        #     makerSide: 'buy',
        #     sequence: 3853
        #   }, ...
        # ]
        response = await self.publicGetTrades(self.extend(request, params))
        return self.parse_trades(response, market, since, limit)

    def parse_trade(self, trade, market=None):
        # public trades
        # {
        #   fillId: 'b5467d00-b13e-3fa9-8216-dd66735550fc',
        #   price: '0.09771286',
        #   quantity: '1.45340410',
        #   quoteQuantity: '0.14201627',
        #   time: 1598345638994,
        #   makerSide: 'buy',
        #   sequence: 3853
        # }
        # private trades
        # {
        #   fillId: '48582d10-b9bb-3c4b-94d3-e67537cf2472',
        #   price: '0.09905990',
        #   quantity: '0.40000000',
        #   quoteQuantity: '0.03962396',
        #   time: 1598873478762,
        #   makerSide: 'sell',
        #   sequence: 5053,
        #   market: 'DIL-ETH',
        #   orderId: '7cdc8e90-eb7d-11ea-9e60-4118569f6e63',
        #   side: 'buy',
        #   fee: '0.00080000',
        #   feeAsset: 'DIL',
        #   gas: '0.00857497',
        #   liquidity: 'taker',
        #   txId: '0xeaa02b112c0b8b61bc02fa1776a2b39d6c614e287c1af90df0a2e591da573e65',
        #   txStatus: 'mined'
        # }
        id = self.safe_string(trade, 'fillId')
        price = self.safe_float(trade, 'price')
        amount = self.safe_float(trade, 'quantity')
        cost = self.safe_float(trade, 'quoteQuantity')
        timestamp = self.safe_integer(trade, 'time')
        marketId = self.safe_string(trade, 'market')
        symbol = self.safe_symbol(marketId, market, '-')
        # self code handles the duality of public vs private trades
        makerSide = self.safe_string(trade, 'makerSide')
        oppositeSide = 'sell' if (makerSide == 'buy') else 'buy'
        side = self.safe_string(trade, 'side', oppositeSide)
        takerOrMaker = self.safe_string(trade, 'liquidity', 'taker')
        feeCost = self.safe_float(trade, 'fee')
        fee = None
        if feeCost is not None:
            feeCurrencyId = self.safe_string(trade, 'feeAsset')
            fee = {
                'cost': feeCost,
                'currency': self.safe_currency_code(feeCurrencyId),
            }
        orderId = self.safe_string(trade, 'orderId')
        return {
            'info': trade,
            'timestamp': timestamp,
            'datetime': self.iso8601(timestamp),
            'symbol': symbol,
            'id': id,
            'order': orderId,
            'type': 'limit',
            'side': side,
            'takerOrMaker': takerOrMaker,
            'price': price,
            'amount': amount,
            'cost': cost,
            'fee': fee,
        }

    async def fetch_order_book(self, symbol, limit=None, params={}):
        await self.load_markets()
        market = self.market(symbol)
        request = {
            'market': market['id'],
            'level': 2,
        }
        if limit is not None:
            request['limit'] = limit
        # {
        #   sequence: 36416753,
        #   bids: [
        #     ['0.09672815', '8.22284267', 1],
        #     ['0.09672814', '1.83685554', 1],
        #     ['0.09672143', '4.10962617', 1],
        #     ['0.09658884', '4.03863759', 1],
        #     ['0.09653781', '3.35730684', 1],
        #     ['0.09624660', '2.54163586', 1],
        #     ['0.09617490', '1.93065030', 1]
        #   ],
        #   asks: [
        #     ['0.09910476', '3.22840154', 1],
        #     ['0.09940587', '3.39796593', 1],
        #     ['0.09948189', '4.25088898', 1],
        #     ['0.09958362', '2.42195784', 1],
        #     ['0.09974393', '4.25234367', 1],
        #     ['0.09995250', '3.40192141', 1]
        #   ]
        # }
        response = await self.publicGetOrderbook(self.extend(request, params))
        nonce = self.safe_integer(response, 'sequence')
        return {
            'timestamp': None,
            'datetime': None,
            'nonce': nonce,
            'bids': self.parse_side(response, 'bids'),
            'asks': self.parse_side(response, 'asks'),
        }

    def parse_side(self, book, side):
        bookSide = self.safe_value(book, side, [])
        result = []
        for i in range(0, len(bookSide)):
            order = bookSide[i]
            price = self.safe_float(order, 0)
            amount = self.safe_float(order, 1)
            orderCount = self.safe_integer(order, 2)
            result.append([price, amount, orderCount])
        descending = side == 'bids'
        return self.sort_by(result, 0, descending)

    async def fetch_currencies(self, params={}):
        # [
        #   {
        #     name: 'Ether',
        #     symbol: 'ETH',
        #     contractAddress: '0x0000000000000000000000000000000000000000',
        #     assetDecimals: 18,
        #     exchangeDecimals: 8
        #   }, ..
        # ]
        response = await self.publicGetAssets(params)
        result = {}
        for i in range(0, len(response)):
            entry = response[i]
            name = self.safe_string(entry, 'name')
            currencyId = self.safe_string(entry, 'symbol')
            precision = self.safe_integer(entry, 'exchangeDecimals')
            code = self.safe_currency_code(currencyId)
            lot = math.pow(-10, precision)
            result[code] = {
                'id': currencyId,
                'code': code,
                'info': entry,
                'type': None,
                'name': name,
                'active': None,
                'fee': None,
                'precision': precision,
                'limits': {
                    'amount': {'min': lot, 'max': None},
                    'price': {'min': lot, 'max': None},
                    'cost': {'min': None, 'max': None},
                    'withdraw': {'min': lot, 'max': None},
                },
            }
        return result

    async def fetch_balance(self, params={}):
        self.check_required_credentials()
        await self.load_markets()
        nonce1 = self.uuidv1()
        request = {
            'nonce': nonce1,
            'wallet': self.walletAddress,
        }
        # [
        #   {
        #     asset: 'DIL',
        #     quantity: '0.00000000',
        #     availableForTrade: '0.00000000',
        #     locked: '0.00000000',
        #     usdValue: null
        #   }, ...
        # ]
        extendedRequest = self.extend(request, params)
        if extendedRequest['wallet'] is None:
            raise BadRequest(self.id + ' wallet is None, set self.walletAddress or "address" in params')
        response = None
        try:
            response = await self.privateGetBalances(extendedRequest)
        except Exception as e:
            if isinstance(e, InvalidAddress):
                walletAddress = extendedRequest['wallet']
                await self.associate_wallet(walletAddress)
                response = await self.privateGetBalances(extendedRequest)
            else:
                raise e
        result = {
            'info': response,
        }
        for i in range(0, len(response)):
            entry = response[i]
            currencyId = self.safe_string(entry, 'asset')
            code = self.safe_currency_code(currencyId)
            total = self.safe_float(entry, 'quantity')
            free = self.safe_float(entry, 'availableForTrade')
            used = self.safe_float(entry, 'locked')
            result[code] = {
                'free': free,
                'used': used,
                'total': total,
            }
        return self.parse_balance(result)

    async def fetch_my_trades(self, symbol=None, since=None, limit=None, params={}):
        self.check_required_credentials()
        await self.load_markets()
        market = None
        request = {
            'nonce': self.uuidv1(),
            'wallet': self.walletAddress,
        }
        if symbol is not None:
            market = self.market(symbol)
            request['market'] = market['id']
        if since is not None:
            request['start'] = since
        if limit is not None:
            request['limit'] = limit
        # [
        #   {
        #     fillId: '48582d10-b9bb-3c4b-94d3-e67537cf2472',
        #     price: '0.09905990',
        #     quantity: '0.40000000',
        #     quoteQuantity: '0.03962396',
        #     time: 1598873478762,
        #     makerSide: 'sell',
        #     sequence: 5053,
        #     market: 'DIL-ETH',
        #     orderId: '7cdc8e90-eb7d-11ea-9e60-4118569f6e63',
        #     side: 'buy',
        #     fee: '0.00080000',
        #     feeAsset: 'DIL',
        #     gas: '0.00857497',
        #     liquidity: 'taker',
        #     txId: '0xeaa02b112c0b8b61bc02fa1776a2b39d6c614e287c1af90df0a2e591da573e65',
        #     txStatus: 'mined'
        #   }
        # ]
        extendedRequest = self.extend(request, params)
        if extendedRequest['wallet'] is None:
            raise BadRequest(self.id + ' walletAddress is None, set self.walletAddress or "address" in params')
        response = None
        try:
            response = await self.privateGetFills(extendedRequest)
        except Exception as e:
            if isinstance(e, InvalidAddress):
                walletAddress = extendedRequest['wallet']
                await self.associate_wallet(walletAddress)
                response = await self.privateGetFills(extendedRequest)
            else:
                raise e
        return self.parse_trades(response, market, since, limit)

    async def fetch_order(self, id, symbol=None, params={}):
        request = {
            'orderId': id,
        }
        return await self.fetch_orders_helper(symbol, None, None, self.extend(request, params))

    async def fetch_open_orders(self, symbol=None, since=None, limit=None, params={}):
        request = {
            'closed': False,
        }
        return await self.fetch_orders_helper(symbol, since, limit, self.extend(request, params))

    async def fetch_closed_orders(self, symbol=None, since=None, limit=None, params={}):
        request = {
            'closed': True,
        }
        return await self.fetch_orders_helper(symbol, since, limit, self.extend(request, params))

    async def fetch_orders_helper(self, symbol=None, since=None, limit=None, params={}):
        await self.load_markets()
        request = {
            'nonce': self.uuidv1(),
            'wallet': self.walletAddress,
        }
        market = None
        if symbol is not None:
            market = self.market(symbol)
            request['market'] = market['id']
        if since is not None:
            request['start'] = since
        if limit is not None:
            request['limit'] = limit
        response = await self.privateGetOrders(self.extend(request, params))
        # fetchClosedOrders / fetchOpenOrders
        # [
        #   {
        #     "market": "DIL-ETH",
        #     "orderId": "7cdc8e90-eb7d-11ea-9e60-4118569f6e63",
        #     "wallet": "0x0AB991497116f7F5532a4c2f4f7B1784488628e1",
        #     "time": 1598873478650,
        #     "status": "filled",
        #     "type": "limit",
        #     "side": "buy",
        #     "originalQuantity": "0.40000000",
        #     "executedQuantity": "0.40000000",
        #     "cumulativeQuoteQuantity": "0.03962396",
        #     "avgExecutionPrice": "0.09905990",
        #     "price": "1.00000000",
        #     "fills": [
        #       {
        #         "fillId": "48582d10-b9bb-3c4b-94d3-e67537cf2472",
        #         "price": "0.09905990",
        #         "quantity": "0.40000000",
        #         "quoteQuantity": "0.03962396",
        #         "time": 1598873478650,
        #         "makerSide": "sell",
        #         "sequence": 5053,
        #         "fee": "0.00080000",
        #         "feeAsset": "DIL",
        #         "gas": "0.00857497",
        #         "liquidity": "taker",
        #         "txId": "0xeaa02b112c0b8b61bc02fa1776a2b39d6c614e287c1af90df0a2e591da573e65",
        #         "txStatus": "mined"
        #       }
        #     ]
        #   }
        # ]
        # fetchOrder
        # {market: 'DIL-ETH',
        #   orderId: '7cdc8e90-eb7d-11ea-9e60-4118569f6e63',
        #   wallet: '0x0AB991497116f7F5532a4c2f4f7B1784488628e1',
        #   time: 1598873478650,
        #   status: 'filled',
        #   type: 'limit',
        #   side: 'buy',
        #   originalQuantity: '0.40000000',
        #   executedQuantity: '0.40000000',
        #   cumulativeQuoteQuantity: '0.03962396',
        #   avgExecutionPrice: '0.09905990',
        #   price: '1.00000000',
        #   fills:
        #    [{fillId: '48582d10-b9bb-3c4b-94d3-e67537cf2472',
        #        price: '0.09905990',
        #        quantity: '0.40000000',
        #        quoteQuantity: '0.03962396',
        #        time: 1598873478650,
        #        makerSide: 'sell',
        #        sequence: 5053,
        #        fee: '0.00080000',
        #        feeAsset: 'DIL',
        #        gas: '0.00857497',
        #        liquidity: 'taker',
        #        txId: '0xeaa02b112c0b8b61bc02fa1776a2b39d6c614e287c1af90df0a2e591da573e65',
        #        txStatus: 'mined'}]}
        if isinstance(response, list):
            return self.parse_orders(response, market, since, limit)
        else:
            return self.parse_order(response, market)

    def parse_order_status(self, status):
        # https://docs.idex.io/#order-states-amp-lifecycle
        statuses = {
            'active': 'open',
            'partiallyFilled': 'open',
            'rejected': 'canceled',
            'filled': 'closed',
        }
        return self.safe_string(statuses, status, status)

    def parse_order(self, order, market=None):
        #
        #     {
        #         "market": "DIL-ETH",
        #         "orderId": "7cdc8e90-eb7d-11ea-9e60-4118569f6e63",
        #         "wallet": "0x0AB991497116f7F5532a4c2f4f7B1784488628e1",
        #         "time": 1598873478650,
        #         "status": "filled",
        #         "type": "limit",
        #         "side": "buy",
        #         "originalQuantity": "0.40000000",
        #         "executedQuantity": "0.40000000",
        #         "cumulativeQuoteQuantity": "0.03962396",
        #         "avgExecutionPrice": "0.09905990",
        #         "price": "1.00000000",
        #         "fills": [
        #             {
        #             "fillId": "48582d10-b9bb-3c4b-94d3-e67537cf2472",
        #             "price": "0.09905990",
        #             "quantity": "0.40000000",
        #             "quoteQuantity": "0.03962396",
        #             "time": 1598873478650,
        #             "makerSide": "sell",
        #             "sequence": 5053,
        #             "fee": "0.00080000",
        #             "feeAsset": "DIL",
        #             "gas": "0.00857497",
        #             "liquidity": "taker",
        #             "txId": "0xeaa02b112c0b8b61bc02fa1776a2b39d6c614e287c1af90df0a2e591da573e65",
        #             "txStatus": "mined"
        #             }
        #         ]
        #     }
        #
        timestamp = self.safe_integer(order, 'time')
        fills = self.safe_value(order, 'fills', [])
        id = self.safe_string(order, 'orderId')
        clientOrderId = self.safe_string(order, 'clientOrderId')
        marketId = self.safe_string(order, 'market')
        side = self.safe_string(order, 'side')
        symbol = self.safe_symbol(marketId, market, '-')
        trades = self.parse_trades(fills, market)
        type = self.safe_string(order, 'type')
        amount = self.safe_float(order, 'originalQuantity')
        filled = self.safe_float(order, 'executedQuantity')
        remaining = None
        if (amount is not None) and (filled is not None):
            remaining = amount - filled
        average = self.safe_float(order, 'avgExecutionPrice')
        price = self.safe_float(order, 'price', average)  # for market orders
        cost = None
        if (amount is not None) and (price is not None):
            cost = amount * price
        rawStatus = self.safe_string(order, 'status')
        status = self.parse_order_status(rawStatus)
        fee = {
            'currency': None,
            'cost': None,
        }
        lastTrade = None
        for i in range(0, len(trades)):
            lastTrade = trades[i]
            fee['currency'] = lastTrade['fee']['currency']
            fee['cost'] = self.sum(fee['cost'], lastTrade['fee']['cost'])
        lastTradeTimestamp = self.safe_integer(lastTrade, 'timestamp')
        return {
            'info': order,
            'id': id,
            'clientOrderId': clientOrderId,
            'timestamp': timestamp,
            'datetime': self.iso8601(timestamp),
            'lastTradeTimestamp': lastTradeTimestamp,
            'symbol': symbol,
            'type': type,
            'timeInForce': None,
            'postOnly': None,
            'side': side,
            'price': price,
            'stopPrice': None,
            'amount': amount,
            'cost': cost,
            'average': average,
            'filled': filled,
            'remaining': remaining,
            'status': status,
            'fee': fee,
            'trades': trades,
        }

    async def associate_wallet(self, walletAddress, params={}):
        nonce = self.uuidv1()
        noPrefix = self.remove0x_prefix(walletAddress)
        byteArray = [
            self.base16_to_binary(nonce),
            self.base16_to_binary(noPrefix),
        ]
        binary = self.binary_concat_array(byteArray)
        hash = self.hash(binary, 'keccak', 'hex')
        signature = self.sign_message_string(hash, self.privateKey)
        # {
        #   address: '0x0AB991497116f7F5532a4c2f4f7B1784488628e1',
        #   totalPortfolioValueUsd: '0.00',
        #   time: 1598468353626
        # }
        request = {
            'parameters': {
                'nonce': nonce,
                'wallet': walletAddress,
            },
            'signature': signature,
        }
        result = await self.privatePostWallets(request)
        return result

    async def create_order(self, symbol, type, side, amount, price=None, params={}):
        # https://docs.idex.io/#create-order
        self.check_required_credentials()
        await self.load_markets()
        market = self.market(symbol)
        nonce = self.uuidv1()
        typeEnum = None
        stopLossTypeEnums = {
            'stopLoss': 3,
            'stopLossLimit': 4,
            'takeProfit': 5,
            'takeProfitLimit': 6,
        }
        stopPriceString = None
        if (type == 'stopLossLimit') or (type == 'takeProfitLimit') or ('stopPrice' in params):
            if not ('stopPrice' in params):
                raise BadRequest(self.id + ' stopPrice is a required parameter for ' + type + 'orders')
            stopPriceString = self.price_to_precision(symbol, params['stopPrice'])
        limitTypeEnums = {
            'limit': 1,
            'limitMaker': 2,
        }
        priceString = None
        typeLower = type.lower()
        limitOrder = typeLower.find('limit') > -1
        if type in limitTypeEnums:
            typeEnum = limitTypeEnums[type]
            priceString = self.price_to_precision(symbol, price)
        elif type in stopLossTypeEnums:
            typeEnum = stopLossTypeEnums[type]
            priceString = self.price_to_precision(symbol, price)
        elif type == 'market':
            typeEnum = 0
        else:
            raise BadRequest(self.id + ' ' + type + ' is not a valid order type')
        amountEnum = 0  # base quantity
        if 'quoteOrderQuantity' in params:
            if type != 'market':
                raise NotSupported(self.id + ' quoteOrderQuantity is not supported for ' + type + ' orders, only supported for market orders')
            amountEnum = 1
            amount = self.safe_float(params, 'quoteOrderQuantity')
        sideEnum = 0 if (side == 'buy') else 1
        walletBytes = self.remove0x_prefix(self.walletAddress)
        network = self.safe_string(self.options, 'network', 'ETH')
        orderVersion = 1 if (network == 'ETH') else 2
        amountString = self.amount_to_precision(symbol, amount)
        # https://docs.idex.io/#time-in-force
        timeInForceEnums = {
            'gtc': 0,
            'ioc': 2,
            'fok': 3,
        }
        defaultTimeInForce = self.safe_string(self.options, 'defaultTimeInForce', 'gtc')
        timeInForce = self.safe_string(params, 'timeInForce', defaultTimeInForce)
        timeInForceEnum = None
        if timeInForce in timeInForceEnums:
            timeInForceEnum = timeInForceEnums[timeInForce]
        else:
            allOptions = list(timeInForceEnums.keys())
            asString = ', '.join(allOptions)
            raise BadRequest(self.id + ' ' + timeInForce + ' is not a valid timeInForce, please choose one of ' + asString)
        # https://docs.idex.io/#self-trade-prevention
        selfTradePreventionEnums = {
            'dc': 0,
            'co': 1,
            'cn': 2,
            'cb': 3,
        }
        defaultSelfTradePrevention = self.safe_string(self.options, 'defaultSelfTradePrevention', 'cn')
        selfTradePrevention = self.safe_string(params, 'selfTradePrevention', defaultSelfTradePrevention)
        selfTradePreventionEnum = None
        if selfTradePrevention in selfTradePreventionEnums:
            selfTradePreventionEnum = selfTradePreventionEnums[selfTradePrevention]
        else:
            allOptions = list(selfTradePreventionEnums.keys())
            asString = ', '.join(allOptions)
            raise BadRequest(self.id + ' ' + selfTradePrevention + ' is not a valid selfTradePrevention, please choose one of ' + asString)
        byteArray = [
            self.number_to_be(orderVersion, 1),
            self.base16_to_binary(nonce),
            self.base16_to_binary(walletBytes),
            self.encode(market['id']),  # TODO: refactor to remove either encode or stringToBinary
            self.number_to_be(typeEnum, 1),
            self.number_to_be(sideEnum, 1),
            self.encode(amountString),
            self.number_to_be(amountEnum, 1),
        ]
        if limitOrder:
            encodedPrice = self.encode(priceString)
            byteArray.append(encodedPrice)
        if type in stopLossTypeEnums:
            encodedPrice = self.encode(stopPriceString or priceString)
            byteArray.append(encodedPrice)
        clientOrderId = self.safe_string(params, 'clientOrderId')
        if clientOrderId is not None:
            byteArray.append(self.encode(clientOrderId))
        after = [
            self.number_to_be(timeInForceEnum, 1),
            self.number_to_be(selfTradePreventionEnum, 1),
            self.number_to_be(0, 8),  # unused
        ]
        allBytes = self.array_concat(byteArray, after)
        binary = self.binary_concat_array(allBytes)
        hash = self.hash(binary, 'keccak', 'hex')
        signature = self.sign_message_string(hash, self.privateKey)
        request = {
            'parameters': {
                'nonce': nonce,
                'market': market['id'],
                'side': side,
                'type': type,
                'wallet': self.walletAddress,
                'timeInForce': timeInForce,
                'selfTradePrevention': selfTradePrevention,
            },
            'signature': signature,
        }
        if limitOrder:
            request['parameters']['price'] = priceString
        if type in stopLossTypeEnums:
            request['parameters']['stopPrice'] = stopPriceString or priceString
        if amountEnum == 0:
            request['parameters']['quantity'] = amountString
        else:
            request['parameters']['quoteOrderQuantity'] = amountString
        if clientOrderId is not None:
            request['parameters']['clientOrderId'] = clientOrderId
        # {
        #   market: 'DIL-ETH',
        #   orderId: '7cdc8e90-eb7d-11ea-9e60-4118569f6e63',
        #   wallet: '0x0AB991497116f7F5532a4c2f4f7B1784488628e1',
        #   time: 1598873478650,
        #   status: 'filled',
        #   type: 'limit',
        #   side: 'buy',
        #   originalQuantity: '0.40000000',
        #   executedQuantity: '0.40000000',
        #   cumulativeQuoteQuantity: '0.03962396',
        #   price: '1.00000000',
        #   fills: [
        #     {
        #       fillId: '48582d10-b9bb-3c4b-94d3-e67537cf2472',
        #       price: '0.09905990',
        #       quantity: '0.40000000',
        #       quoteQuantity: '0.03962396',
        #       time: 1598873478650,
        #       makerSide: 'sell',
        #       sequence: 5053,
        #       fee: '0.00080000',
        #       feeAsset: 'DIL',
        #       gas: '0.00857497',
        #       liquidity: 'taker',
        #       txStatus: 'pending'
        #     }
        #   ],
        #   avgExecutionPrice: '0.09905990'
        # }
        # we don't use self.extend here because it is a signed endpoint
        response = await self.privatePostOrders(request)
        return self.parse_order(response, market)

    async def withdraw(self, code, amount, address, tag=None, params={}):
        self.check_required_credentials()
        await self.load_markets()
        nonce = self.uuidv1()
        amountString = self.currency_to_precision(code, amount)
        currency = self.currency(code)
        walletBytes = self.remove0x_prefix(self.walletAddress)
        byteArray = [
            self.base16_to_binary(nonce),
            self.base16_to_binary(walletBytes),
            self.encode(currency['id']),
            self.encode(amountString),
            self.number_to_be(1, 1),  # bool set to True
        ]
        binary = self.binary_concat_array(byteArray)
        hash = self.hash(binary, 'keccak', 'hex')
        signature = self.sign_message_string(hash, self.privateKey)
        request = {
            'parameters': {
                'nonce': nonce,
                'wallet': address,
                'asset': currency['id'],
                'quantity': amountString,
            },
            'signature': signature,
        }
        # {
        #   withdrawalId: 'a61dcff0-ec4d-11ea-8b83-c78a6ecb3180',
        #   asset: 'ETH',
        #   assetContractAddress: '0x0000000000000000000000000000000000000000',
        #   quantity: '0.20000000',
        #   time: 1598962883190,
        #   fee: '0.00024000',
        #   txStatus: 'pending',
        #   txId: null
        # }
        response = await self.privatePostWithdrawals(request)
        id = self.safe_string(response, 'withdrawalId')
        return {
            'info': response,
            'id': id,
        }

    async def cancel_order(self, id, symbol=None, params={}):
        self.check_required_credentials()
        await self.load_markets()
        market = None
        if symbol is not None:
            market = self.market(symbol)
        nonce = self.uuidv1()
        walletBytes = self.remove0x_prefix(self.walletAddress)
        byteArray = [
            self.base16_to_binary(nonce),
            self.base16_to_binary(walletBytes),
            self.encode(id),
        ]
        binary = self.binary_concat_array(byteArray)
        hash = self.hash(binary, 'keccak', 'hex')
        signature = self.sign_message_string(hash, self.privateKey)
        request = {
            'parameters': {
                'nonce': nonce,
                'wallet': self.walletAddress,
                'orderId': id,
            },
            'signature': signature,
        }
        # [{orderId: '688336f0-ec50-11ea-9842-b332f8a34d0e'}]
        response = await self.privateDeleteOrders(self.extend(request, params))
        canceledOrder = self.safe_value(response, 0)
        return self.parse_order(canceledOrder, market)

    def handle_errors(self, code, reason, url, method, headers, body, response, requestHeaders, requestBody):
        errorCode = self.safe_string(response, 'code')
        message = self.safe_string(response, 'message')
        if errorCode in self.exceptions:
            Exception = self.exceptions[errorCode]
            raise Exception(self.id + ' ' + message)
        if errorCode is not None:
            raise ExchangeError(self.id + ' ' + message)

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

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

    async def fetch_transactions_helper(self, code=None, since=None, limit=None, params={}):
        await self.load_markets()
        nonce = self.uuidv1()
        request = {
            'nonce': nonce,
            'wallet': self.walletAddress,
        }
        currency = None
        if code is not None:
            currency = self.currency(code)
            request['asset'] = currency['id']
        if since is not None:
            request['start'] = since
        if limit is not None:
            request['limit'] = limit
        # [
        #   {
        #     depositId: 'e9970cc0-eb6b-11ea-9e89-09a5ebc1f98e',
        #     asset: 'ETH',
        #     quantity: '1.00000000',
        #     txId: '0xcd4aac3171d7131cc9e795568c67938675185ac17641553ef54c8a7c294c8142',
        #     txTime: 1598865853000,
        #     confirmationTime: 1598865930231
        #   }
        # ]
        method = params['method']
        params = self.omit(params, 'method')
        response = await getattr(self, method)(self.extend(request, params))
        return self.parse_transactions(response, currency, since, limit)

    def parse_transaction_status(self, status):
        statuses = {
            'mined': 'ok',
        }
        return self.safe_string(statuses, status, status)

    def parse_transaction(self, transaction, currency=None):
        # fetchDeposits
        # {
        #   depositId: 'e9970cc0-eb6b-11ea-9e89-09a5ebc1f98f',
        #   asset: 'ETH',
        #   quantity: '1.00000000',
        #   txId: '0xcd4aac3171d7131cc9e795568c67938675185ac17641553ef54c8a7c294c8142',
        #   txTime: 1598865853000,
        #   confirmationTime: 1598865930231
        # }
        # fetchWithdrwalas
        # {
        #   withdrawalId: 'a62d8760-ec4d-11ea-9fa6-47904c19499b',
        #   asset: 'ETH',
        #   assetContractAddress: '0x0000000000000000000000000000000000000000',
        #   quantity: '0.20000000',
        #   time: 1598962883288,
        #   fee: '0.00024000',
        #   txId: '0x305e9cdbaa85ad029f50578d13d31d777c085de573ed5334d95c19116d8c03ce',
        #   txStatus: 'mined'
        #  }
        type = None
        if 'depositId' in transaction:
            type = 'deposit'
        elif 'withdrawalId' in transaction:
            type = 'withdrawal'
        id = self.safe_string_2(transaction, 'depositId', 'withdrawId')
        code = self.safe_currency_code(self.safe_string(transaction, 'asset'), currency)
        amount = self.safe_float(transaction, 'quantity')
        txid = self.safe_string(transaction, 'txId')
        timestamp = self.safe_integer(transaction, 'txTime')
        fee = None
        if 'fee' in transaction:
            fee = {
                'cost': self.safe_float(transaction, 'fee'),
                'currency': 'ETH',
            }
        rawStatus = self.safe_string(transaction, 'txStatus')
        status = self.parse_transaction_status(rawStatus)
        updated = self.safe_integer(transaction, 'confirmationTime')
        return {
            'info': transaction,
            'id': id,
            'txid': txid,
            'timestamp': timestamp,
            'datetime': self.iso8601(timestamp),
            'address': None,
            'tag': None,
            'type': type,
            'amount': amount,
            'currency': code,
            'status': status,
            'updated': updated,
            'fee': fee,
        }

    def sign(self, path, api='public', method='GET', params={}, headers=None, body=None):
        network = self.safe_string(self.options, 'network', 'ETH')
        version = self.safe_string(self.options, 'version', 'v1')
        url = self.urls['api'][network] + '/' + version + '/' + path
        keys = list(params.keys())
        length = len(keys)
        query = None
        if length > 0:
            if method == 'GET':
                query = self.urlencode(params)
                url = url + '?' + query
            else:
                body = self.json(params)
        headers = {
            'Content-Type': 'application/json',
        }
        if self.apiKey is not None:
            headers['IDEX-API-Key'] = self.apiKey
        if api == 'private':
            payload = None
            if method == 'GET':
                payload = query
            else:
                payload = body
            headers['IDEX-HMAC-Signature'] = self.hmac(self.encode(payload), self.encode(self.secret), hashlib.sha256, 'hex')
        return {'url': url, 'method': method, 'body': body, 'headers': headers}
