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

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

from ccxt.base.exchange import Exchange
import math
from ccxt.base.errors import ExchangeError
from ccxt.base.errors import AuthenticationError
from ccxt.base.errors import AccountSuspended
from ccxt.base.errors import ArgumentsRequired
from ccxt.base.errors import BadRequest
from ccxt.base.errors import BadSymbol
from ccxt.base.errors import InsufficientFunds
from ccxt.base.errors import InvalidOrder
from ccxt.base.errors import OrderNotFound
from ccxt.base.errors import DuplicateOrderId
from ccxt.base.errors import ExchangeNotAvailable


class wavesexchange(Exchange):

    def describe(self):
        return self.deep_extend(super(wavesexchange, self).describe(), {
            'id': 'wavesexchange',
            'name': 'Waves.Exchange',
            'countries': ['CH'],  # Switzerland
            'rateLimit': 500,
            'certified': True,
            'pro': False,
            'has': {
                'cancelOrder': True,
                'createMarketOrder': False,
                'createOrder': True,
                'fetchBalance': True,
                'fetchClosedOrders': True,
                'fetchDepositAddress': True,
                'fetchMarkets': True,
                'fetchMyTrades': True,
                'fetchOHLCV': True,
                'fetchOpenOrders': True,
                'fetchOrderBook': True,
                'fetchOrders': True,
                'fetchTicker': True,
                'fetchTrades': True,
                'withdraw': True,
            },
            'timeframes': {
                '1m': '1m',
                '5m': '5m',
                '15m': '15m',
                '30m': '30m',
                '1h': '1h',
                '2h': '2h',
                '3h': '3h',
                '4h': '4h',
                '6h': '6h',
                '12h': '12h',
                '1d': '1d',
                '1w': '1w',
                '1M': '1M',
            },
            'urls': {
                'logo': 'https://user-images.githubusercontent.com/1294454/84547058-5fb27d80-ad0b-11ea-8711-78ac8b3c7f31.jpg',
                'api': {
                    'matcher': 'http://matcher.waves.exchange',
                    'node': 'https://nodes.waves.exchange',
                    'public': 'https://api.wavesplatform.com/v0',
                    'private': 'https://api.waves.exchange/v1',
                    'forward': 'https://waves.exchange/api/v1/forward/matcher',
                    'market': 'https://marketdata.wavesplatform.com/api/v1',
                },
                'doc': 'https://docs.waves.exchange',
                'www': 'https://waves.exchange',
            },
            'api': {
                'matcher': {
                    'get': [
                        'matcher',
                        'matcher/settings',
                        'matcher/settings/rates',
                        'matcher/balance/reserved/{publicKey}',
                        'matcher/debug/allSnashotOffsets',
                        'matcher/debug/currentOffset',
                        'matcher/debug/lastOffset',
                        'matcher/debug/oldestSnapshotOffset',
                        'matcher/orderbook',
                        'matcher/orderbook/{amountAsset}/{priceAsset}',
                        'matcher/orderbook/{baseId}/{quoteId}/publicKey/{publicKey}',
                        'matcher/orderbook/{baseId}/{quoteId}/{orderId}',
                        'matcher/orderbook/{baseId}/{quoteId}/info',
                        'matcher/orderbook/{baseId}/{quoteId}/status',
                        'matcher/orderbook/{baseId}/{quoteId}/tradeableBalance/{address}',
                        'matcher/orderbook/{publicKey}',
                        'matcher/orderbook/{publicKey}/{orderId}',
                        'matcher/orders/{address}',
                        'matcher/orders/{address}/{orderId}',
                        'matcher/transactions/{orderId}',
                    ],
                    'post': [
                        'matcher/orderbook',
                        'matcher/orderbook/market',
                        'matcher/orderbook/cancel',
                        'matcher/orderbook/{baseId}/{quoteId}/cancel',
                        'matcher/debug/saveSnapshots',
                        'matcher/orders/{address}/cancel',
                        'matcher/orders/cancel/{orderId}',
                    ],
                    'delete': [
                        'matcher/orderbook/{baseId}/{quoteId}',
                        'matcher/settings/rates/{assetId}',
                    ],
                    'put': [
                        'matcher/settings/rates/{assetId}',
                    ],
                },
                'node': {
                    'get': [
                        'addresses',
                        'addresses/balance/{address}',
                        'addresses/balance/{address}/{confirmations}',
                        'addresses/balance/details/{address}',
                        'addresses/data/{address}',
                        'addresses/data/{address}/{key}',
                        'addresses/effectiveBalance/{address}',
                        'addresses/effectiveBalance/{address}/{confirmations}',
                        'addresses/publicKey/{publicKey}',
                        'addresses/scriptInfo/{address}',
                        'addresses/scriptInfo/{address}/meta',
                        'addresses/seed/{address}',
                        'addresses/seq/{from}/{to}',
                        'addresses/validate/{address}',
                        'alias/by-address/{address}',
                        'alias/by-alias/{alias}',
                        'assets/{assetId}/distribution/{height}/{limit}',
                        'assets/balance/{address}',
                        'assets/balance/{address}/{assetId}',
                        'assets/details/{assetId}',
                        'assets/nft/{address}/limit/{limit}',
                        'blockchain/rewards',
                        'blockchain/rewards/height',
                        'blocks/address/{address}/{from}/{to}/',
                        'blocks/at/{height}',
                        'blocks/delay/{signature}/{blockNum}',
                        'blocks/first',
                        'blocks/headers/last',
                        'blocks/headers/seq/{from}/{to}',
                        'blocks/height',
                        'blocks/height/{signature}',
                        'blocks/last',
                        'blocks/seq/{from}/{to}',
                        'blocks/signature/{signature}',
                        'consensus/algo',
                        'consensus/basetarget',
                        'consensus/basetarget/{blockId}',
                        'consensus/{generatingbalance}/address',
                        'consensus/generationsignature',
                        'consensus/generationsignature/{blockId}',
                        'debug/balances/history/{address}',
                        'debug/blocks/{howMany}',
                        'debug/configInfo',
                        'debug/historyInfo',
                        'debug/info',
                        'debug/minerInfo',
                        'debug/portfolios/{address}',
                        'debug/state',
                        'debug/stateChanges/address/{address}',
                        'debug/stateChanges/info/{id}',
                        'debug/stateWaves/{height}',
                        'leasing/active/{address}',
                        'node/state',
                        'node/version',
                        'peers/all',
                        'peers/blacklisted',
                        'peers/connected',
                        'peers/suspended',
                        'transactions/address/{address}/limit/{limit}',
                        'transactions/info/{id}',
                        'transactions/status',
                        'transactions/unconfirmed',
                        'transactions/unconfirmed/info/{id}',
                        'transactions/unconfirmed/size',
                        'utils/seed',
                        'utils/seed/{length}',
                        'utils/time',
                        'wallet/seed',
                    ],
                    'post': [
                        'addresses',
                        'addresses/data/{address}',
                        'addresses/sign/{address}',
                        'addresses/signText/{address}',
                        'addresses/verify/{address}',
                        'addresses/verifyText/{address}',
                        'debug/blacklist',
                        'debug/print',
                        'debug/rollback',
                        'debug/validate',
                        'node/stop',
                        'peers/clearblacklist',
                        'peers/connect',
                        'transactions/broadcast',
                        'transactions/calculateFee',
                        'tranasctions/sign',
                        'transactions/sign/{signerAddress}',
                        'tranasctions/status',
                        'utils/hash/fast',
                        'utils/hash/secure',
                        'utils/script/compileCode',
                        'utils/script/compileWithImports',
                        'utils/script/decompile',
                        'utils/script/estimate',
                        'utils/sign/{privateKey}',
                        'utils/transactionsSerialize',
                    ],
                    'delete': [
                        'addresses/{address}',
                        'debug/rollback-to/{signature}',
                    ],
                },
                'public': {
                    'get': [
                        'pairs',
                        'candles/{baseId}/{quoteId}',
                        'transactions/exchange',
                    ],
                },
                'private': {
                    'get': [
                        'deposit/addresses/{code}',
                        'deposit/currencies',
                        'withdraw/currencies',
                        'withdraw/addresses/{currency}/{address}',
                    ],
                    'post': [
                        'oauth2/token',
                    ],
                },
                'forward': {
                    'get': [
                        'matcher/orders/{address}',  # can't get the orders endpoint to work with the matcher api
                        'matcher/orders/{address}/{orderId}',
                    ],
                    'post': [
                        'matcher/orders/{wavesAddress}/cancel',
                    ],
                },
                'market': {
                    'get': [
                        'tickers',
                    ],
                },
            },
            'options': {
                'allowedCandles': 1440,
                'accessToken': None,
                'matcherPublicKey': None,
                'quotes': None,
                'createOrderDefaultExpiry': 2419200000,  # 60 * 60 * 24 * 28 * 1000
                'wavesAddress': None,
                'withdrawFeeUSDN': 7420,
                'withdrawFeeWAVES': 100000,
                'wavesPrecision': 8,
            },
            'requiresEddsa': True,
            'exceptions': {
                '3147270': InsufficientFunds,  # https://github.com/wavesplatform/matcher/wiki/List-of-all-errors
                '112': InsufficientFunds,
                '4': ExchangeError,
                '13': ExchangeNotAvailable,
                '14': ExchangeNotAvailable,
                '3145733': AccountSuspended,
                '3148040': DuplicateOrderId,
                '3148801': AuthenticationError,
                '9440512': AuthenticationError,
                '9440771': BadSymbol,
                '9441026': InvalidOrder,
                '9441282': InvalidOrder,
                '9441286': InvalidOrder,
                '9441295': InvalidOrder,
                '9441540': InvalidOrder,
                '9441542': InvalidOrder,
                '106954752': AuthenticationError,
                '106954769': AuthenticationError,
                '106957828': AuthenticationError,
                '106960131': AuthenticationError,
                '106981137': AuthenticationError,
                '9437193': OrderNotFound,
                '1048577': BadRequest,
                '1051904': AuthenticationError,
            },
        })

    def get_quotes(self):
        quotes = self.safe_value(self.options, 'quotes')
        if quotes:
            return quotes
        else:
            # currencies can have any name because you can create you own token
            # as a result someone can create a fake token called BTC
            # we use self mapping to determine the real tokens
            # https://docs.waves.exchange/en/waves-matcher/matcher-api#asset-pair
            response = self.matcherGetMatcherSettings()
            # {
            #   "orderVersions": [
            #     1,
            #     2,
            #     3
            #   ],
            #   "success": True,
            #   "matcherPublicKey": "9cpfKN9suPNvfeUNphzxXMjcnn974eme8ZhWUjaktzU5",
            #   "orderFee": {
            #     "dynamic": {
            #       "baseFee": 300000,
            #       "rates": {
            #         "34N9YcEETLWn93qYQ64EsP1x89tSruJU44RrEMSXXEPJ": 1.22639597,
            #         "62LyMjcr2DtiyF5yVXFhoQ2q414VPPJXjsNYp72SuDCH": 0.00989643,
            #         "HZk1mbfuJpmxU1Fs4AX5MWLVYtctsNcg6e2C6VKqK8zk": 0.0395674,
            #         "8LQW8f7P5d5PZM7GtZEBgaqRPGSzS3DfPuiXrURJ4AJS": 0.00018814,
            #         "4LHHvYGNKJUg5hj65aGD5vgScvCBmLpdRFtjokvCjSL8": 26.19721262,
            #         "474jTeYx2r2Va35794tCScAXWJG9hU2HcgxzMowaZUnu": 0.00752978,
            #         "DG2xFkPdDwKUoBkzGAhQtLpSGzfXLiCYPEzeKH2Ad24p": 1.84575,
            #         "B3uGHFRpSUuGEDWjqB9LWWxafQj8VTvpMucEyoxzws5H": 0.02330273,
            #         "zMFqXuoyrn5w17PFurTqxB7GsS71fp9dfk6XFwxbPCy": 0.00721412,
            #         "5WvPKSJXzVE2orvbkJ8wsQmmQKqTv9sGBPksV4adViw3": 0.02659103,
            #         "WAVES": 1,
            #         "BrjUWjndUanm5VsJkbUip8VRYy6LWJePtxya3FNv4TQa": 0.03433583
            #       }
            #     }
            #   },
            #   "networkByte": 87,
            #   "matcherVersion": "2.1.3.5",
            #   "status": "SimpleResponse",
            #   "priceAssets": [
            #     "Ft8X1v1LTa1ABafufpaCWyVj8KkaxUWE6xBhW6sNFJck",
            #     "DG2xFkPdDwKUoBkzGAhQtLpSGzfXLiCYPEzeKH2Ad24p",
            #     "34N9YcEETLWn93qYQ64EsP1x89tSruJU44RrEMSXXEPJ",
            #     "Gtb1WRznfchDnTh37ezoDTJ4wcoKaRsKqKjJjy7nm2zU",
            #     "2mX5DzVKWrAJw8iwdJnV2qtoeVG9h5nTDpTqC1wb1WEN",
            #     "8LQW8f7P5d5PZM7GtZEBgaqRPGSzS3DfPuiXrURJ4AJS",
            #     "WAVES",
            #     "474jTeYx2r2Va35794tCScAXWJG9hU2HcgxzMowaZUnu",
            #     "zMFqXuoyrn5w17PFurTqxB7GsS71fp9dfk6XFwxbPCy",
            #     "62LyMjcr2DtiyF5yVXFhoQ2q414VPPJXjsNYp72SuDCH",
            #     "HZk1mbfuJpmxU1Fs4AX5MWLVYtctsNcg6e2C6VKqK8zk",
            #     "B3uGHFRpSUuGEDWjqB9LWWxafQj8VTvpMucEyoxzws5H",
            #     "5WvPKSJXzVE2orvbkJ8wsQmmQKqTv9sGBPksV4adViw3",
            #     "BrjUWjndUanm5VsJkbUip8VRYy6LWJePtxya3FNv4TQa",
            #     "4LHHvYGNKJUg5hj65aGD5vgScvCBmLpdRFtjokvCjSL8"
            #   ]
            # }
            quotes = {}
            priceAssets = self.safe_value(response, 'priceAssets')
            for i in range(0, len(priceAssets)):
                quotes[priceAssets[i]] = True
            self.options['quotes'] = quotes
            return quotes

    def fetch_markets(self, params={}):
        response = self.marketGetTickers()
        result = []
        for i in range(0, len(response)):
            entry = response[i]
            baseId = self.safe_string(entry, 'amountAssetID')
            quoteId = self.safe_string(entry, 'priceAssetID')
            id = baseId + '/' + quoteId
            marketId = self.safe_string(entry, 'symbol')
            base, quote = marketId.split('/')
            symbol = self.safe_currency_code(base) + '/' + self.safe_currency_code(quote)
            precision = {
                'amount': self.safe_integer(entry, 'amountAssetDecimals'),
                'price': self.safe_integer(entry, 'priceAssetDecimals'),
            }
            result.append({
                'symbol': symbol,
                'id': id,
                'base': base,
                'quote': quote,
                'baseId': baseId,
                'quoteId': quoteId,
                'info': entry,
                'precision': precision,
            })
        return result

    def fetch_order_book(self, symbol, limit=None, params={}):
        self.load_markets()
        market = self.market(symbol)
        request = self.extend({
            'amountAsset': market['baseId'],
            'priceAsset': market['quoteId'],
        }, params)
        response = self.matcherGetMatcherOrderbookAmountAssetPriceAsset(request)
        timestamp = self.safe_integer(response, 'timestamp')
        bids = self.parse_order_book_side(self.safe_value(response, 'bids'), market, limit)
        asks = self.parse_order_book_side(self.safe_value(response, 'asks'), market, limit)
        return {
            'bids': bids,
            'asks': asks,
            'timestamp': timestamp,
            'datetime': self.iso8601(timestamp),
            'nonce': None,
        }

    def parse_order_book_side(self, bookSide, market=None, limit=None):
        precision = market['precision']
        wavesPrecision = self.safe_integer(self.options, 'wavesPrecision', 8)
        amountPrecision = math.pow(10, precision['amount'])
        difference = precision['amount'] - precision['price']
        pricePrecision = math.pow(10, wavesPrecision - difference)
        result = []
        for i in range(0, len(bookSide)):
            entry = bookSide[i]
            price = self.safe_integer(entry, 'price', 0) / pricePrecision
            amount = self.safe_integer(entry, 'amount', 0) / amountPrecision
            if (limit is not None) and (i > limit):
                break
            result.append([price, amount])
        return result

    def check_required_keys(self):
        if self.apiKey is None:
            raise AuthenticationError(self.id + ' requires apiKey credential')
        if self.secret is None:
            raise AuthenticationError(self.id + ' requires secret credential')
        apiKeyBytes = None
        secretKeyBytes = None
        try:
            apiKeyBytes = self.base58_to_binary(self.apiKey)
        except Exception as e:
            raise AuthenticationError(self.id + ' apiKey must be a base58 encoded public key')
        try:
            secretKeyBytes = self.base58_to_binary(self.secret)
        except Exception as e:
            raise AuthenticationError(self.id + ' secret must be a base58 encoded private key')
        hexApiKeyBytes = self.binary_to_base16(apiKeyBytes)
        hexSecretKeyBytes = self.binary_to_base16(secretKeyBytes)
        if len(hexApiKeyBytes) != 64:
            raise AuthenticationError(self.id + ' apiKey must be a base58 encoded public key')
        if len(hexSecretKeyBytes) != 64:
            raise AuthenticationError(self.id + ' secret must be a base58 encoded private key')

    def sign(self, path, api='public', method='GET', params={}, headers=None, body=None):
        query = self.omit(params, self.extract_params(path))
        isCancelOrder = path == 'matcher/orders/{wavesAddress}/cancel'
        path = self.implode_params(path, params)
        url = self.urls['api'][api] + '/' + path
        queryString = self.urlencode(query)
        if (api == 'private') or (api == 'forward'):
            headers = {
                'Accept': 'application/json',
            }
            accessToken = self.safe_string(self.options, 'accessToken')
            if accessToken:
                headers['Authorization'] = 'Bearer ' + accessToken
            if method == 'POST':
                headers['content-type'] = 'application/json'
            else:
                headers['content-type'] = 'application/x-www-form-urlencoded'
            if isCancelOrder:
                body = self.json([query['orderId']])
                queryString = ''
            if len(queryString) > 0:
                url += '?' + queryString
        elif api == 'matcher':
            if method == 'POST':
                headers = {
                    'content-type': 'application/json',
                }
                body = self.json(query)
            else:
                headers = query
        else:
            if method == 'POST':
                headers = {
                    'content-type': 'application/json',
                }
                body = self.json(query)
            else:
                headers = {
                    'content-type': 'application/x-www-form-urlencoded',
                }
                if len(queryString) > 0:
                    url += '?' + queryString
        return {'url': url, 'method': method, 'body': body, 'headers': headers}

    def get_access_token(self):
        if not self.safe_string(self.options, 'accessToken'):
            prefix = 'ffffff01'
            expiresDelta = 60 * 60 * 24 * 7
            seconds = self.sum(self.seconds(), expiresDelta)
            seconds = str(seconds)
            clientId = 'waves.exchange'
            message = 'W:' + clientId + ':' + seconds
            messageHex = self.binary_to_base16(self.encode(message))
            payload = prefix + messageHex
            hexKey = self.binary_to_base16(self.base58_to_binary(self.secret))
            signature = self.eddsa(payload, hexKey, 'ed25519')
            request = {
                'grant_type': 'password',
                'scope': 'general',
                'username': self.apiKey,
                'password': seconds + ':' + signature,
                'client_id': clientId,
            }
            response = self.privatePostOauth2Token(request)
            # {access_token: 'eyJhbGciOXJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzaWciOiJiaTZiMVhMQlo0M1Q4QmRTSlVSejJBZGlQdVlpaFZQYVhhVjc4ZGVIOEpTM3M3NUdSeEU1VkZVOE5LRUI0UXViNkFHaUhpVFpuZ3pzcnhXdExUclRvZTgiLCJhIjoiM1A4VnpMU2EyM0VXNUNWY2tIYlY3ZDVCb043NWZGMWhoRkgiLCJuYiI6IlciLCJ1c2VyX25hbWUiOiJBSFhuOG5CQTRTZkxRRjdoTFFpU24xNmt4eWVoaml6QkdXMVRkcm1TWjFnRiIsInNjb3BlIjpbImdlbmVyYWwiXSwibHQiOjYwNDc5OSwicGsiOiJBSFhuOG5CQTRTZkxRRjdoTFFpU24xNmt4eWVoaml6QkdXMVRkcm1TWjFnRiIsImV4cCI6MTU5MTk3NTA1NywiZXhwMCI6MTU5MTk3NTA1NywianRpIjoiN2JhOTUxMTMtOGI2MS00NjEzLTlkZmYtNTEwYTc0NjlkOWI5IiwiY2lkIjoid2F2ZXMuZXhjaGFuZ2UifQ.B-XwexBnUAzbWknVN68RKT0ZP5w6Qk1SKJ8usL3OIwDEzCUUX9PjW-5TQHmiCRcA4oft8lqXEiCwEoNfsblCo_jTpRo518a1vZkIbHQk0-13Dm1K5ewGxfxAwBk0g49odcbKdjl64TN1yM_PO1VtLVuiTeZP-XF-S42Uj-7fcO-r7AulyQLuTE0uo-Qdep8HDCk47rduZwtJOmhFbCCnSgnLYvKWy3CVTeldsR77qxUY-vy8q9McqeP7Id-_MWnsob8vWXpkeJxaEsw1Fke1dxApJaJam09VU8EB3ZJWpkT7V8PdafIrQGeexx3jhKKxo7rRb4hDV8kfpVoCgkvFan',
            #   token_type: 'bearer',
            #   refresh_token: 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzaWciOiJiaTZiMVhMQlo0M1Q4QmRTSlVSejJBZGlQdVlpaFZQYVhhVjc4ZGVIOEpTM3M3NUdSeEU1VkZVOE5LRUI0UXViNkFHaUhpVFpuZ3pzcnhXdExUclRvZTgiLCJhIjoiM1A4VnpMU2EyM0VXNUNWY2tIYlY3ZDVCb043NWZGMWhoRkgiLCJuYiI6IlciLCJ1c2VyX25hbWUiOiJBSFhuOG5CQTRTZkxRRjdoTFFpU24xNmt4eWVoaml6QkdXMVRkcm1TWjFnRiIsInNjb3BlIjpbImdlbmVyYWwiXSwiYXRpIjoiN2JhOTUxMTMtOGI2MS00NjEzLTlkZmYtNTEwYTc0NjlkXWI5IiwibHQiOjYwNDc5OSwicGsiOiJBSFhuOG5CQTRTZkxRRjdoTFFpU24xNmt4eWVoaml6QkdXMVRkcm1TWjFnRiIsImV4cCI6MTU5Mzk2MjI1OCwiZXhwMCI6MTU5MTk3NTA1NywianRpIjoiM2MzZWRlMTktNjI5My00MTNlLWJmMWUtZTRlZDZlYzUzZTgzIiwiY2lkIjoid2F2ZXMuZXhjaGFuZ2UifQ.gD1Qj0jfqayfZpBvNY0t3ccMyK5hdbT7dY-_5L6LxwV0Knan4ndEtvygxlTOczmJUKtnA4T1r5GBFgNMZTvtViKZIbqZNysEg2OY8UxwDaF4VPeGJLg_QXEnn8wBeBQdyMafh9UQdwD2ci7x-saM4tOAGmncAygfTDxy80201gwDhfAkAGerb9kL00oWzSJScldxu--pNLDBUEHZt52MSEel10HGrzvZkkvvSh67vcQo5TOGb5KG6nh65UdJCwr41AVz4fbQPP-N2Nkxqy0TE_bqVzZxExXgvcS8TS0Z82T3ijJa_ct7B9wblpylBnvmyj3VycUzufD6uy8MUGq32D',
            #   expires_in: 604798,
            #   scope: 'general'}
            self.options['accessToken'] = self.safe_string(response, 'access_token')
            return self.options['accessToken']

    def parse_ticker(self, ticker, market=None):
        #
        #     {
        #         "__type":"pair",
        #         "data":{
        #             "firstPrice":0.00012512,
        #             "lastPrice":0.00012441,
        #             "low":0.00012167,
        #             "high":0.00012768,
        #             "weightedAveragePrice":0.000124710697407246,
        #             "volume":209554.26356614,
        #             "quoteVolume":26.1336583539951,
        #             "volumeWaves":209554.26356614,
        #             "txsCount":6655
        #         },
        #         "amountAsset":"WAVES",
        #         "priceAsset":"8LQW8f7P5d5PZM7GtZEBgaqRPGSzS3DfPuiXrURJ4AJS"
        #     }
        #
        timestamp = None
        baseId = self.safe_string(ticker, 'amountAsset')
        quoteId = self.safe_string(ticker, 'priceAsset')
        symbol = None
        if (baseId is not None) and (quoteId is not None):
            marketId = baseId + '/' + quoteId
            if marketId in self.markets_by_id:
                market = self.markets_by_id[marketId]
            else:
                base = self.safe_currency_code(baseId)
                quote = self.safe_currency_code(quoteId)
                symbol = base + '/' + quote
        if (symbol is None) and (market is not None):
            symbol = market['symbol']
        data = self.safe_value(ticker, 'data', {})
        last = self.safe_float(data, 'lastPrice')
        low = self.safe_float(data, 'low')
        high = self.safe_float(data, 'high')
        vwap = self.safe_float(data, 'weightedAveragePrice')
        baseVolume = self.safe_float(data, 'volume')
        quoteVolume = self.safe_float(data, 'quoteVolume')
        open = self.safe_value(data, 'firstPrice')
        change = None
        average = None
        percentage = None
        if last is not None and open is not None:
            change = last - open
            average = self.sum(last, open) / 2
            if open > 0:
                percentage = change / open * 100
        return {
            'symbol': symbol,
            'timestamp': timestamp,
            'datetime': self.iso8601(timestamp),
            'high': high,
            'low': low,
            'bid': None,
            'bidVolume': None,
            'ask': None,
            'askVolume': None,
            'vwap': vwap,
            'open': open,
            'close': last,
            'last': last,
            'previousClose': None,
            'change': change,
            'percentage': percentage,
            'average': average,
            'baseVolume': baseVolume,
            'quoteVolume': quoteVolume,
            'info': ticker,
        }

    def fetch_ticker(self, symbol, params={}):
        self.load_markets()
        market = self.market(symbol)
        request = {
            'pairs': market['id'],
        }
        response = self.publicGetPairs(self.extend(request, params))
        #
        #     {
        #         "__type":"list",
        #         "data":[
        #             {
        #                 "__type":"pair",
        #                 "data":{
        #                     "firstPrice":0.00012512,
        #                     "lastPrice":0.00012441,
        #                     "low":0.00012167,
        #                     "high":0.00012768,
        #                     "weightedAveragePrice":0.000124710697407246,
        #                     "volume":209554.26356614,
        #                     "quoteVolume":26.1336583539951,
        #                     "volumeWaves":209554.26356614,
        #                     "txsCount":6655
        #                 },
        #                 "amountAsset":"WAVES",
        #                 "priceAsset":"8LQW8f7P5d5PZM7GtZEBgaqRPGSzS3DfPuiXrURJ4AJS"
        #             }
        #         ]
        #     }
        #
        data = self.safe_value(response, 'data', [])
        ticker = self.safe_value(data, 0, {})
        return self.parse_ticker(ticker, market)

    def fetch_ohlcv(self, symbol, timeframe='1m', since=None, limit=None, params={}):
        self.load_markets()
        market = self.market(symbol)
        request = {
            'baseId': market['baseId'],
            'quoteId': market['quoteId'],
            'interval': self.timeframes[timeframe],
        }
        if since is not None:
            request['timeStart'] = str(since)
        else:
            allowedCandles = self.safe_integer(self.options, 'allowedCandles', 1440)
            timeframeUnix = self.parse_timeframe(timeframe) * 1000
            currentTime = int(math.floor(self.milliseconds()) / timeframeUnix) * timeframeUnix
            delta = (allowedCandles - 1) * timeframeUnix
            timeStart = currentTime - delta
            request['timeStart'] = str(timeStart)
        response = self.publicGetCandlesBaseIdQuoteId(self.extend(request, params))
        #
        #     {
        #         "__type": "list",
        #         "data": [
        #             {
        #                 "__type": "candle",
        #                 "data": {
        #                     "time": "2020-06-09T14:47:00.000Z",
        #                     "open": 0.0250385,
        #                     "close": 0.0250385,
        #                     "high": 0.0250385,
        #                     "low": 0.0250385,
        #                     "volume": 0.01033012,
        #                     "quoteVolume": 0.00025865,
        #                     "weightedAveragePrice": 0.0250385,
        #                     "maxHeight": 2099399,
        #                     "txsCount": 5,
        #                     "timeClose": "2020-06-09T14:47:59.999Z"
        #                 }
        #             }
        #         ]
        #     }
        #
        data = self.safe_value(response, 'data', [])
        result = self.parse_ohlcvs(data, market, timeframe, since, limit)
        lastClose = None
        length = len(result)
        for i in range(0, len(result)):
            j = length - i - 1
            entry = result[j]
            open = entry[1]
            if open is None:
                entry[1] = lastClose
                entry[2] = lastClose
                entry[3] = lastClose
                entry[4] = lastClose
                result[j] = entry
            lastClose = entry[4]
        return result

    def parse_ohlcv(self, ohlcv, market=None):
        #
        #     {
        #         __type: 'candle',
        #         data: {
        #             time: '2020-06-05T20:46:00.000Z',
        #             open: 240.573975,
        #             close: 240.573975,
        #             high: 240.573975,
        #             low: 240.573975,
        #             volume: 0.01278413,
        #             quoteVolume: 3.075528,
        #             weightedAveragePrice: 240.573975,
        #             maxHeight: 2093895,
        #             txsCount: 5,
        #             timeClose: '2020-06-05T20:46:59.999Z'
        #         }
        #     }
        #
        data = self.safe_value(ohlcv, 'data', {})
        return [
            self.parse8601(self.safe_string(data, 'time')),
            self.safe_float(data, 'open'),
            self.safe_float(data, 'high'),
            self.safe_float(data, 'low'),
            self.safe_float(data, 'close'),
            self.safe_float(data, 'volume', 0),
        ]

    def fetch_deposit_address(self, code, params={}):
        self.get_access_token()
        supportedCurrencies = self.privateGetDepositCurrencies()
        currencies = {}
        items = self.safe_value(supportedCurrencies, 'items', [])
        for i in range(0, len(items)):
            entry = items[i]
            currencyCode = self.safe_string(entry, 'id')
            currencies[currencyCode] = True
        if not (code in currencies):
            codes = list(currencies.keys())
            raise ExchangeError(self.id + ' fetch ' + code + ' deposit address not supported. Currency code must be one of ' + str(codes))
        request = self.extend({
            'code': code,
        }, params)
        response = self.privateGetDepositAddressesCode(request)
        # {
        #   "type": "deposit_addresses",
        #   "currency": {
        #     "type": "deposit_currency",
        #     "id": "ERGO",
        #     "waves_asset_id": "5dJj4Hn9t2Ve3tRpNGirUHy4yBK6qdJRAJYV21yPPuGz",
        #     "decimals": 9,
        #     "status": "active",
        #     "allowed_amount": {
        #       "min": 0.001,
        #       "max": 100000
        #     },
        #     "fees": {
        #       "flat": 0,
        #       "rate": 0
        #     }
        #   },
        #   "deposit_addresses": [
        #     "9fRAAQjF8Yqg7qicQCL884zjimsRnuwsSavsM1rUdDaoG8mThku"
        #   ]
        # }
        addresses = self.safe_value(response, 'deposit_addresses')
        address = self.safe_string(addresses, 0)
        return {
            'address': address,
            'code': code,
            'tag': None,
            'info': response,
        }

    def get_matcher_public_key(self):
        # self method returns a single string
        matcherPublicKey = self.safe_string(self.options, 'matcherPublicKey')
        if matcherPublicKey:
            return matcherPublicKey
        else:
            response = self.matcherGetMatcher()
            # remove trailing quotes from string response
            self.options['matcherPublicKey'] = response[1:len(response) - 1]
            return self.options['matcherPublicKey']

    def get_asset_bytes(self, currencyId):
        if currencyId == 'WAVES':
            return self.number_to_be(0, 1)
        else:
            return self.binary_concat(self.number_to_be(1, 1), self.base58_to_binary(currencyId))

    def get_asset_id(self, currencyId):
        if currencyId == 'WAVES':
            return ''
        return currencyId

    def price_to_precision(self, symbol, price):
        market = self.markets[symbol]
        wavesPrecision = self.safe_integer(self.options, 'wavesPrecision', 8)
        difference = market['precision']['amount'] - market['precision']['price']
        return int(float(self.to_wei(price, wavesPrecision - difference)))

    def amount_to_precision(self, symbol, amount):
        return int(float(self.to_wei(amount, self.markets[symbol]['precision']['amount'])))

    def currency_to_precision(self, currency, amount):
        return int(float(self.to_wei(amount, self.currencies[currency]['precision'])))

    def currency_from_precision(self, currency, amount):
        return self.from_wei(amount, self.currencies[currency]['precision'])

    def price_from_precision(self, symbol, price):
        market = self.markets[symbol]
        wavesPrecision = self.safe_integer(self.options, 'wavesPrecision', 8)
        difference = market['precision']['amount'] - market['precision']['price']
        return self.from_wei(price, wavesPrecision - difference)

    def get_default_expiry(self):
        expiry = self.safe_integer(self.options, 'createOrderDefaultExpiry')
        if expiry:
            return expiry
        else:
            self.options['createOrderDefaultExpiry'] = 60 * 60 * 24 * 28 * 1000
            return self.options['createOrderDefaultExpiry']

    def create_order(self, symbol, type, side, amount, price=None, params={}):
        self.check_required_dependencies()
        self.check_required_keys()
        self.load_markets()
        market = self.market(symbol)
        matcherPublicKey = self.get_matcher_public_key()
        amountAsset = self.get_asset_id(market['baseId'])
        priceAsset = self.get_asset_id(market['quoteId'])
        amount = self.amount_to_precision(symbol, amount)
        price = self.price_to_precision(symbol, price)
        orderType = 0 if (side == 'buy') else 1
        timestamp = self.milliseconds()
        expiration = self.sum(timestamp, self.get_default_expiry())
        settings = self.matcherGetMatcherSettings()
        # {
        #   "orderVersions": [
        #     1,
        #     2,
        #     3
        #   ],
        #   "success": True,
        #   "matcherPublicKey": "9cpfKN9suPNvfeUNphzxXMjcnn974eme8ZhWUjaktzU5",
        #   "orderFee": {
        #     "dynamic": {
        #       "baseFee": 300000,
        #       "rates": {
        #         "34N9YcEETLWn93qYQ64EsP1x89tSruJU44RrEMSXXEPJ": 1.0257813,
        #         "62LyMjcr2DtiyF5yVXFhoQ2q414VPPJXjsNYp72SuDCH": 0.01268146,
        #         "HZk1mbfuJpmxU1Fs4AX5MWLVYtctsNcg6e2C6VKqK8zk": 0.05232404,
        #         "8LQW8f7P5d5PZM7GtZEBgaqRPGSzS3DfPuiXrURJ4AJS": 0.00023985,
        #         "4LHHvYGNKJUg5hj65aGD5vgScvCBmLpdRFtjokvCjSL8": 19.5967716,
        #         "474jTeYx2r2Va35794tCScAXWJG9hU2HcgxzMowaZUnu": 0.00937073,
        #         "DG2xFkPdDwKUoBkzGAhQtLpSGzfXLiCYPEzeKH2Ad24p": 2.19825,
        #         "B3uGHFRpSUuGEDWjqB9LWWxafQj8VTvpMucEyoxzws5H": 0.03180264,
        #         "zMFqXuoyrn5w17PFurTqxB7GsS71fp9dfk6XFwxbPCy": 0.00996631,
        #         "5WvPKSJXzVE2orvbkJ8wsQmmQKqTv9sGBPksV4adViw3": 0.03254476,
        #         "WAVES": 1,
        #         "BrjUWjndUanm5VsJkbUip8VRYy6LWJePtxya3FNv4TQa": 0.03703704
        #       }
        #     }
        #   },
        #   "networkByte": 87,
        #   "matcherVersion": "2.1.4.8",
        #   "status": "SimpleResponse",
        #   "priceAssets": [
        #     "Ft8X1v1LTa1ABafufpaCWyVj8KkaxUWE6xBhW6sNFJck",
        #     "DG2xFkPdDwKUoBkzGAhQtLpSGzfXLiCYPEzeKH2Ad24p",
        #     "34N9YcEETLWn93qYQ64EsP1x89tSruJU44RrEMSXXEPJ",
        #     "Gtb1WRznfchDnTh37ezoDTJ4wcoKaRsKqKjJjy7nm2zU",
        #     "2mX5DzVKWrAJw8iwdJnV2qtoeVG9h5nTDpTqC1wb1WEN",
        #     "8LQW8f7P5d5PZM7GtZEBgaqRPGSzS3DfPuiXrURJ4AJS",
        #     "WAVES",
        #     "474jTeYx2r2Va35794tCScAXWJG9hU2HcgxzMowaZUnu",
        #     "zMFqXuoyrn5w17PFurTqxB7GsS71fp9dfk6XFwxbPCy",
        #     "62LyMjcr2DtiyF5yVXFhoQ2q414VPPJXjsNYp72SuDCH",
        #     "HZk1mbfuJpmxU1Fs4AX5MWLVYtctsNcg6e2C6VKqK8zk",
        #     "B3uGHFRpSUuGEDWjqB9LWWxafQj8VTvpMucEyoxzws5H",
        #     "5WvPKSJXzVE2orvbkJ8wsQmmQKqTv9sGBPksV4adViw3",
        #     "BrjUWjndUanm5VsJkbUip8VRYy6LWJePtxya3FNv4TQa",
        #     "4LHHvYGNKJUg5hj65aGD5vgScvCBmLpdRFtjokvCjSL8"
        #   ]
        # }
        orderFee = self.safe_value(settings, 'orderFee')
        dynamic = self.safe_value(orderFee, 'dynamic')
        baseMatcherFee = self.safe_integer(dynamic, 'baseFee')
        wavesMatcherFee = self.currency_from_precision('WAVES', baseMatcherFee)
        rates = self.safe_value(dynamic, 'rates')
        # choose sponsored assets from the list of priceAssets above
        priceAssets = list(rates.keys())
        matcherFeeAssetId = None
        matcherFee = None
        if 'feeAssetId' in params:
            matcherFeeAssetId = params['feeAssetId']
        elif 'feeAssetId' in self.options:
            matcherFeeAssetId = self.options['feeAssetId']
        else:
            balances = self.fetch_balance()
            if balances['WAVES']['free'] > wavesMatcherFee:
                matcherFeeAssetId = 'WAVES'
                matcherFee = baseMatcherFee
            else:
                for i in range(0, len(priceAssets)):
                    assetId = priceAssets[i]
                    code = self.safe_currency_code(assetId)
                    balance = self.safe_value(self.safe_value(balances, code, {}), 'free')
                    assetFee = rates[assetId] * wavesMatcherFee
                    if (balance is not None) and (balance > assetFee):
                        matcherFeeAssetId = assetId
                        break
        if matcherFeeAssetId is None:
            raise InsufficientFunds(self.id + ' not enough funds to cover the fee, specify feeAssetId in params or options, or buy some WAVES')
        if matcherFee is None:
            wavesPrecision = self.safe_integer(self.options, 'wavesPrecision', 8)
            rate = self.safe_float(rates, matcherFeeAssetId)
            code = self.safe_currency_code(matcherFeeAssetId)
            currency = self.currency(code)
            newPrecison = math.pow(10, wavesPrecision - currency['precision'])
            matcherFee = int(math.ceil(rate * baseMatcherFee / newPrecison))
        byteArray = [
            self.number_to_be(3, 1),
            self.base58_to_binary(self.apiKey),
            self.base58_to_binary(matcherPublicKey),
            self.get_asset_bytes(market['baseId']),
            self.get_asset_bytes(market['quoteId']),
            self.number_to_be(orderType, 1),
            self.number_to_be(price, 8),
            self.number_to_be(amount, 8),
            self.number_to_be(timestamp, 8),
            self.number_to_be(expiration, 8),
            self.number_to_be(matcherFee, 8),
            self.get_asset_bytes(matcherFeeAssetId),
        ]
        binary = self.binary_concat_array(byteArray)
        signature = self.eddsa(self.binary_to_base16(binary), self.binary_to_base16(self.base58_to_binary(self.secret)), 'ed25519')
        assetPair = {
            'amountAsset': amountAsset,
            'priceAsset': priceAsset,
        }
        body = {
            'senderPublicKey': self.apiKey,
            'matcherPublicKey': matcherPublicKey,
            'assetPair': assetPair,
            'orderType': side,
            'price': price,
            'amount': amount,
            'timestamp': timestamp,
            'expiration': expiration,
            'matcherFee': matcherFee,
            'signature': signature,
            'version': 3,
        }
        if matcherFeeAssetId != 'WAVES':
            body['matcherFeeAssetId'] = matcherFeeAssetId
        response = self.matcherPostMatcherOrderbook(body)
        # {success: True,
        #   message:
        #    {version: 3,
        #      id: 'Do7cDJMf2MJuFyorvxNNuzS42MXSGGEq1r1hGDn1PHiS',
        #      sender: '3P8VzLSa23EW5CVckHbV7d5BoN75fF1hhFH',
        #      senderPublicKey: 'AHXn8nBA4SfLQF7hLQiSn16kxyehjizBGW1TdrmSZ1gF',
        #      matcherPublicKey: '9cpfKN9suPNvfeUNphzxXMjcnn974eme8ZhWUjaktzU5',
        #      assetPair:
        #       {amountAsset: null,
        #         priceAsset: '8LQW8f7P5d5PZM7GtZEBgaqRPGSzS3DfPuiXrURJ4AJS'},
        #      orderType: 'sell',
        #      amount: 1,
        #      price: 100000000,
        #      timestamp: 1591593117995,
        #      expiration: 1594012317995,
        #      matcherFee: 300000,
        #      matcherFeeAssetId: null,
        #      signature: '2EG8zgE6Ze1X5EYA8DbfFiPXAtC7NniYBAMFbJUbzwVbHmmCKHornQfS5F32NwkHF4623KWq1U6K126h4TTqyVq',
        #      proofs:
        #       ['2EG8zgE6Ze1X5EYA8DbfFiPXAtC7NniYBAMFbJUbzwVbHmmCKHornQfS5F32NwkHF4623KWq1U6K126h4TTqyVq']},
        #   status: 'OrderAccepted'}
        value = self.safe_value(response, 'message')
        return self.parse_order(value, market)

    def cancel_order(self, id, symbol=None, params={}):
        self.check_required_dependencies()
        self.check_required_keys()
        self.get_access_token()
        wavesAddress = self.get_waves_address()
        response = self.forwardPostMatcherOrdersWavesAddressCancel({
            'wavesAddress': wavesAddress,
            'orderId': id,
        })
        #  {
        #    "success":true,
        #    "message":[[{"orderId":"EBpJeGM36KKFz5gTJAUKDBm89V8wqxKipSFBdU35AN3c","success":true,"status":"OrderCanceled"}]],
        #    "status":"BatchCancelCompleted"
        #  }
        message = self.safe_value(response, 'message')
        firstMessage = self.safe_value(message, 0)
        firstOrder = self.safe_value(firstMessage, 0)
        returnedId = self.safe_string(firstOrder, 'orderId')
        return {
            'info': response,
            'id': returnedId,
            'clientOrderId': None,
            'timestamp': None,
            'datetime': None,
            'lastTradeTimestamp': None,
            'symbol': symbol,
            'type': None,
            'side': None,
            'price': None,
            'amount': None,
            'cost': None,
            'average': None,
            'filled': None,
            'remaining': None,
            'status': None,
            'fee': None,
            'trades': None,
        }

    def fetch_orders(self, symbol=None, since=None, limit=None, params={}):
        self.check_required_dependencies()
        self.check_required_keys()
        if symbol is None:
            raise ArgumentsRequired(self.id + ' fetchOrders() requires symbol argument')
        self.load_markets()
        market = self.market(symbol)
        timestamp = self.milliseconds()
        byteArray = [
            self.base58_to_binary(self.apiKey),
            self.number_to_be(timestamp, 8),
        ]
        binary = self.binary_concat_array(byteArray)
        hexSecret = self.binary_to_base16(self.base58_to_binary(self.secret))
        signature = self.eddsa(self.binary_to_base16(binary), hexSecret, 'ed25519')
        request = {
            'Accept': 'application/json',
            'Timestamp': str(timestamp),
            'Signature': signature,
            'publicKey': self.apiKey,
            'baseId': market['baseId'],
            'quoteId': market['quoteId'],
        }
        response = self.matcherGetMatcherOrderbookBaseIdQuoteIdPublicKeyPublicKey(self.extend(request, params))
        # [{id: '3KicDeWayY2mdrRoYdCkP3gUAoUZUNT1AA6GAtWuPLfa',
        #     type: 'sell',
        #     orderType: 'limit',
        #     amount: 1,
        #     fee: 300000,
        #     price: 100000000,
        #     timestamp: 1591651254076,
        #     filled: 0,
        #     filledFee: 0,
        #     feeAsset: 'WAVES',
        #     status: 'Accepted',
        #     assetPair:
        #      {amountAsset: null,
        #        priceAsset: '8LQW8f7P5d5PZM7GtZEBgaqRPGSzS3DfPuiXrURJ4AJS'},
        #     avgWeighedPrice: 0}, ...]
        return self.parse_orders(response, market, since, limit)

    def fetch_open_orders(self, symbol=None, since=None, limit=None, params={}):
        self.load_markets()
        self.get_access_token()
        market = None
        if symbol is not None:
            market = self.market(symbol)
        address = self.get_waves_address()
        request = {
            'address': address,
            'activeOnly': True,
        }
        response = self.forwardGetMatcherOrdersAddress(request)
        return self.parse_orders(response, market, since, limit)

    def fetch_closed_orders(self, symbol=None, since=None, limit=None, params={}):
        self.load_markets()
        self.get_access_token()
        market = None
        if symbol is not None:
            market = self.market(symbol)
        address = self.get_waves_address()
        request = {
            'address': address,
            'closedOnly': True,
        }
        response = self.forwardGetMatcherOrdersAddress(request)
        # [
        #   {
        #     "id": "9aXcxvXai73jbAm7tQNnqaQ2PwUjdmWuyjvRTKAHsw4f",
        #     "type": "buy",
        #     "orderType": "limit",
        #     "amount": 23738330,
        #     "fee": 300000,
        #     "price": 3828348334,
        #     "timestamp": 1591926905636,
        #     "filled": 23738330,
        #     "filledFee": 300000,
        #     "feeAsset": "WAVES",
        #     "status": "Filled",
        #     "assetPair": {
        #       "amountAsset": "HZk1mbfuJpmxU1Fs4AX5MWLVYtctsNcg6e2C6VKqK8zk",
        #       "priceAsset": null
        #     },
        #     "avgWeighedPrice": 3828348334
        #   }, ...
        # ]
        return self.parse_orders(response, market, since, limit)

    def parse_order_status(self, status):
        statuses = {
            'Cancelled': 'canceled',
            'Accepted': 'open',
            'Filled': 'closed',
            'PartiallyFilled': 'open',
        }
        return self.safe_string(statuses, status, status)

    def get_symbol_from_asset_pair(self, assetPair):
        # a blank string or null can indicate WAVES
        baseId = self.safe_string(assetPair, 'amountAsset', 'WAVES')
        quoteId = self.safe_string(assetPair, 'priceAsset', 'WAVES')
        return self.safe_currency_code(baseId) + '/' + self.safe_currency_code(quoteId)

    def parse_order(self, order, market=None):
        #
        #     createOrder
        #
        #     {
        #         version: 3,
        #         id: 'BshyeHXDfJmTnjTdBYt371jD4yWaT3JTP6KpjpsiZepS',
        #         sender: '3P8VzLSa23EW5CVckHbV7d5BoN75fF1hhFH',
        #         senderPublicKey: 'AHXn8nBA4SfLQF7hLQiSn16kxyehjizBGW1TdrmSZ1gF',
        #         matcherPublicKey: '9cpfKN9suPNvfeUNphzxXMjcnn974eme8ZhWUjaktzU5',
        #         assetPair: {
        #             amountAsset: '474jTeYx2r2Va35794tCScAXWJG9hU2HcgxzMowaZUnu',
        #             priceAsset: 'DG2xFkPdDwKUoBkzGAhQtLpSGzfXLiCYPEzeKH2Ad24p'
        #         },
        #         orderType: 'buy',
        #         amount: 10000,
        #         price: 400000000,
        #         timestamp: 1599848586891,
        #         expiration: 1602267786891,
        #         matcherFee: 3008,
        #         matcherFeeAssetId: '474jTeYx2r2Va35794tCScAXWJG9hU2HcgxzMowaZUnu',
        #         signature: '3D2h8ubrhuWkXbVn4qJ3dvjmZQxLoRNfjTqb9uNpnLxUuwm4fGW2qGH6yKFe2SQPrcbgkS3bDVe7SNtMuatEJ7qy',
        #         proofs: [
        #             '3D2h8ubrhuWkXbVn4qJ3dvjmZQxLoRNfjTqb9uNpnLxUuwm4fGW2qGH6yKFe2SQPrcbgkS3bDVe7SNtMuatEJ7qy'
        #         ]
        #     }
        #
        #     fetchClosedOrders
        #
        #     {
        #         id: '81D9uKk2NfmZzfG7uaJsDtxqWFbJXZmjYvrL88h15fk8',
        #         type: 'buy',
        #         orderType: 'limit',
        #         amount: 30000000000,
        #         filled: 0,
        #         price: 1000000,
        #         fee: 300000,
        #         filledFee: 0,
        #         feeAsset: 'WAVES',
        #         timestamp: 1594303779322,
        #         status: 'Cancelled',
        #         assetPair: {
        #             amountAsset: '474jTeYx2r2Va35794tCScAXWJG9hU2HcgxzMowaZUnu',
        #             priceAsset: 'WAVES'
        #         },
        #         avgWeighedPrice: 0,
        #         version: 3
        #     }
        #
        timestamp = self.safe_integer(order, 'timestamp')
        side = self.safe_string_2(order, 'type', 'orderType')
        type = 'limit'
        if 'type' in order:
            # fetchOrders
            type = self.safe_string(order, 'orderType', type)
        id = self.safe_string(order, 'id')
        filled = self.safe_string(order, 'filled')
        price = self.safe_string(order, 'price')
        amount = self.safe_string(order, 'amount')
        assetPair = self.safe_value(order, 'assetPair')
        symbol = None
        if assetPair is not None:
            symbol = self.get_symbol_from_asset_pair(assetPair)
        elif market is not None:
            symbol = market['symbol']
        amountCurrency = self.safe_currency_code(self.safe_string(assetPair, 'amountAsset', 'WAVES'))
        price = self.price_from_precision(symbol, price)
        amount = self.currency_from_precision(amountCurrency, amount)
        cost = None
        if (price is not None) and (amount is not None):
            cost = price * amount
        filled = self.currency_from_precision(amountCurrency, filled)
        remaining = None
        if (filled is not None) and (amount is not None):
            remaining = amount - filled
        average = self.price_from_precision(symbol, self.safe_string(order, 'avgWeighedPrice'))
        status = self.parse_order_status(self.safe_string(order, 'status'))
        fee = None
        if 'type' in order:
            currency = self.safe_currency_code(self.safe_string(order, 'feeAsset'))
            fee = {
                'currency': currency,
                'fee': self.currency_from_precision(currency, self.safe_integer(order, 'filledFee')),
            }
        else:
            currency = self.safe_currency_code(self.safe_string(order, 'matcherFeeAssetId', 'WAVES'))
            fee = {
                'currency': currency,
                'fee': self.currency_from_precision(currency, self.safe_integer(order, 'matcherFee')),
            }
        return {
            'info': order,
            'id': id,
            'clientOrderId': None,
            'timestamp': timestamp,
            'datetime': self.iso8601(timestamp),
            'lastTradeTimestamp': None,
            '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': None,
        }

    def get_waves_address(self):
        cachedAddreess = self.safe_string(self.options, 'wavesAddress')
        if cachedAddreess is None:
            request = {
                'publicKey': self.apiKey,
            }
            response = self.nodeGetAddressesPublicKeyPublicKey(request)
            self.options['wavesAddress'] = self.safe_string(response, 'address')
            return self.options['wavesAddress']
        else:
            return cachedAddreess

    def fetch_balance(self, params={}):
        # makes a lot of different requests to get all the data
        # in particular:
        # fetchMarkets, getWavesAddress,
        # getTotalBalance(doesn't include waves), getReservedBalance(doesn't include waves)
        # getReservedBalance(includes WAVES)
        # I couldn't find another way to get all the data
        self.check_required_dependencies()
        self.check_required_keys()
        self.load_markets()
        wavesAddress = self.get_waves_address()
        request = {
            'address': wavesAddress,
        }
        totalBalance = self.nodeGetAssetsBalanceAddress(request)
        # {
        #   "address": "3P8VzLSa23EW5CVckHbV7d5BoN75fF1hhFH",
        #   "balances": [
        #     {
        #       "assetId": "DG2xFkPdDwKUoBkzGAhQtLpSGzfXLiCYPEzeKH2Ad24p",
        #       "balance": 1177200,
        #       "reissuable": False,
        #       "minSponsoredAssetFee": 7420,
        #       "sponsorBalance": 47492147189709,
        #       "quantity": 999999999775381400,
        #       "issueTransaction": {
        #         "senderPublicKey": "BRnVwSVctnV8pge5vRpsJdWnkjWEJspFb6QvrmZvu3Ht",
        #         "quantity": 1000000000000000000,
        #         "fee": 100400000,
        #         "description": "Neutrino USD",
        #         "type": 3,
        #         "version": 2,
        #         "reissuable": False,
        #         "script": null,
        #         "sender": "3PC9BfRwJWWiw9AREE2B3eWzCks3CYtg4yo",
        #         "feeAssetId": null,
        #         "chainId": 87,
        #         "proofs": [
        #           "3HNpbVkgP69NWSeb9hGYauiQDaXrRXh3tXFzNsGwsAAXnFrA29SYGbLtziW9JLpXEq7qW1uytv5Fnm5XTUMB2BxU"
        #         ],
        #         "assetId": "DG2xFkPdDwKUoBkzGAhQtLpSGzfXLiCYPEzeKH2Ad24p",
        #         "decimals": 6,
        #         "name": "USD-N",
        #         "id": "DG2xFkPdDwKUoBkzGAhQtLpSGzfXLiCYPEzeKH2Ad24p",
        #         "timestamp": 1574429393962
        #       }
        #     }
        #   ]
        # }
        balances = self.safe_value(totalBalance, 'balances')
        result = {}
        for i in range(0, len(balances)):
            entry = balances[i]
            issueTransaction = self.safe_value(entry, 'issueTransaction')
            decimals = self.safe_integer(issueTransaction, 'decimals')
            currencyId = self.safe_string(entry, 'assetId')
            balance = self.safe_float(entry, 'balance')
            code = None
            if currencyId in self.currencies_by_id:
                code = self.safe_currency_code(currencyId)
                result[code] = self.account()
                result[code]['total'] = self.from_wei(balance, decimals)
        timestamp = self.milliseconds()
        byteArray = [
            self.base58_to_binary(self.apiKey),
            self.number_to_be(timestamp, 8),
        ]
        binary = self.binary_concat_array(byteArray)
        hexSecret = self.binary_to_base16(self.base58_to_binary(self.secret))
        signature = self.eddsa(self.binary_to_base16(binary), hexSecret, 'ed25519')
        matcherRequest = {
            'publicKey': self.apiKey,
            'signature': signature,
            'timestamp': str(timestamp),
        }
        reservedBalance = self.matcherGetMatcherBalanceReservedPublicKey(matcherRequest)
        # {WAVES: 200300000}
        reservedKeys = list(reservedBalance.keys())
        for i in range(0, len(reservedKeys)):
            currencyId = reservedKeys[i]
            code = self.safe_currency_code(currencyId)
            if not (code in result):
                result[code] = self.account()
            amount = self.safe_float(reservedBalance, currencyId)
            result[code]['used'] = self.currency_from_precision(code, amount)
        wavesRequest = {
            'address': wavesAddress,
        }
        wavesTotal = self.nodeGetAddressesBalanceAddress(wavesRequest)
        # {
        #   "address": "3P8VzLSa23EW5CVckHbV7d5BoN75fF1hhFH",
        #   "confirmations": 0,
        #   "balance": 909085978
        # }
        result['WAVES'] = self.safe_value(result, 'WAVES', {})
        result['WAVES']['total'] = self.currency_from_precision('WAVES', self.safe_float(wavesTotal, 'balance'))
        codes = list(result.keys())
        for i in range(0, len(codes)):
            code = codes[i]
            if self.safe_value(result[code], 'used') is None:
                result[code]['used'] = 0.0
        return self.parse_balance(result)

    def fetch_my_trades(self, symbol=None, since=None, limit=None, params={}):
        self.load_markets()
        market = self.market(symbol)
        address = self.get_waves_address()
        request = {
            'sender': address,
            'amountAsset': market['baseId'],
            'priceAsset': market['quoteId'],
        }
        response = self.publicGetTransactionsExchange(request)
        data = self.safe_value(response, 'data')
        return self.parse_trades(data, market, since, limit)

    def fetch_trades(self, symbol, since=None, limit=None, params={}):
        self.load_markets()
        market = self.market(symbol)
        request = {
            'amountAsset': market['baseId'],
            'priceAsset': market['quoteId'],
        }
        if limit is not None:
            request['limit'] = limit
        if since is not None:
            request['timeStart'] = since
        response = self.publicGetTransactionsExchange(request)
        data = self.safe_value(response, 'data')
        return self.parse_trades(data, market, since, limit)

    def parse_trade(self, trade, market=None):
        # {__type: 'transaction',
        #   data:
        #    {id: 'HSdruioHqvYHeyn9hhyoHdRWPB2bFA8ujeCPZMK6992c',
        #      timestamp: '2020-06-09T19:34:51.897Z',
        #      height: 2099684,
        #      type: 7,
        #      version: 2,
        #      proofs:
        #       ['26teDHERQgwjjHqEn4REcDotNG8M21xjou3X42XuDuCvrRkQo6aPyrswByH3UrkWG8v27ZAaVNzoxDg4teNcLtde'],
        #      fee: 0.003,
        #      sender: '3PEjHv3JGjcWNpYEEkif2w8NXV4kbhnoGgu',
        #      senderPublicKey: '9cpfKN9suPNvfeUNphzxXMjcnn974eme8ZhWUjaktzU5',
        #      buyMatcherFee: 0.00299999,
        #      sellMatcherFee: 0.00299999,
        #      price: 0.00012003,
        #      amount: 60.80421562,
        #      order1:
        #       {id: 'CBRwP3ar4oMvvpUiGyfxc1syh41488SDi2GkrjuBDegv',
        #         senderPublicKey: 'DBXSHBz96NFsMu7xh4fi2eT9ZnyxefAHXsMxUayzgC6a',
        #         matcherPublicKey: '9cpfKN9suPNvfeUNphzxXMjcnn974eme8ZhWUjaktzU5',
        #         assetPair: [Object],
        #         orderType: 'buy',
        #         price: 0.00012003,
        #         sender: '3PJfFRgVuJ47UY4ckb74EGzEBzkHXtmG1LA',
        #         amount: 60.80424773,
        #         timestamp: '2020-06-09T19:34:51.885Z',
        #         expiration: '2020-06-10T12:31:31.885Z',
        #         matcherFee: 0.003,
        #         signature: '4cA3ZAb3XAEEXaFG7caqpto5TRbpR5PkhZpxoNQZ9ZReNvjuJQs5a3THnumv7rcqmVUiVtuHAgk2f67ANcqtKyJ8',
        #         matcherFeeAssetId: null},
        #      order2:
        #       {id: 'CHJSLQ6dfSPs6gu2mAegrMUcRiDEDqaj2GKfvptMjS3M',
        #         senderPublicKey: '3RUC4NGFZm9H8VJhSSjJyFLdiE42qNiUagDcZPwjgDf8',
        #         matcherPublicKey: '9cpfKN9suPNvfeUNphzxXMjcnn974eme8ZhWUjaktzU5',
        #         assetPair: [Object],
        #         orderType: 'sell',
        #         price: 0.00012003,
        #         sender: '3P9vKoQpMZtaSkHKpNh977YY9ZPzTuntLAq',
        #         amount: 60.80424773,
        #         timestamp: '2020-06-09T19:34:51.887Z',
        #         expiration: '2020-06-10T12:31:31.887Z',
        #         matcherFee: 0.003,
        #         signature: '3SFyrcqzou2ddZyNisnLYaGhLt5qRjKxH8Nw3s4T5U7CEKGX9DDo8dS27RgThPVGbYF1rYET1FwrWoQ2UFZ6SMTR',
        #         matcherFeeAssetId: null}}}
        data = self.safe_value(trade, 'data')
        datetime = self.safe_string(data, 'timestamp')
        timestamp = self.parse8601(datetime)
        id = self.safe_string(data, 'id')
        price = self.safe_float(data, 'price')
        amount = self.safe_float(data, 'amount')
        order1 = self.safe_value(data, 'order1')
        order2 = self.safe_value(data, 'order2')
        order = None
        # order2 arrived after order1
        if self.safe_string(order1, 'senderPublicKey') == self.apiKey:
            order = order1
        else:
            order = order2
        symbol = None
        assetPair = self.safe_value(order, 'assetPair')
        if assetPair is not None:
            symbol = self.get_symbol_from_asset_pair(assetPair)
        elif market is not None:
            symbol = market['symbol']
        side = self.safe_string(order, 'orderType')
        orderId = self.safe_string(order, 'id')
        cost = None
        if (price is not None) and (amount is not None):
            cost = price * amount
        fee = {
            'cost': self.safe_float(data, 'fee'),
            'currency': self.safe_currency_code(self.safe_string(order, 'matcherFeeAssetId', 'WAVES')),
        }
        return {
            'info': trade,
            'timestamp': timestamp,
            'datetime': datetime,
            'symbol': symbol,
            'id': id,
            'order': orderId,
            'type': None,
            'side': side,
            'takerOrMaker': None,
            'price': price,
            'amount': amount,
            'cost': cost,
            'fee': fee,
        }

    def handle_errors(self, code, reason, url, method, headers, body, response, requestHeaders, requestBody):
        errorCode = self.safe_string(response, 'error')
        success = self.safe_value(response, 'success', True)
        Exception = self.safe_value(self.exceptions, errorCode)
        if Exception is not None:
            message = self.safe_string(response, 'message')
            raise Exception(self.id + ' ' + message)
        message = self.safe_string(response, 'message')
        if message == 'Validation Error':
            raise BadRequest(self.id + ' ' + body)
        if not success:
            raise ExchangeError(self.id + ' ' + body)

    def withdraw(self, code, amount, address, tag=None, params={}):
        # currently only works for BTC and WAVES
        if code != 'WAVES':
            supportedCurrencies = self.privateGetWithdrawCurrencies()
            currencies = {}
            items = self.safe_value(supportedCurrencies, 'items', [])
            for i in range(0, len(items)):
                entry = items[i]
                currencyCode = self.safe_string(entry, 'id')
                currencies[currencyCode] = True
            if not (code in currencies):
                codes = list(currencies.keys())
                raise ExchangeError(self.id + ' fetch ' + code + ' withdrawals are not supported. Currency code must be one of ' + str(codes))
        self.load_markets()
        hexChars = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f']
        set = {}
        for i in range(0, len(hexChars)):
            key = hexChars[i]
            set[key] = True
        isErc20 = True
        noPrefix = self.remove0x_prefix(address)
        lower = noPrefix.lower()
        for i in range(0, len(lower)):
            character = lower[i]
            if not (character in set):
                isErc20 = False
                break
        self.get_access_token()
        proxyAddress = None
        if code == 'WAVES' and not isErc20:
            proxyAddress = address
        else:
            withdrawAddressRequest = {
                'address': address,
                'currency': code,
            }
            withdrawAddress = self.privateGetWithdrawAddressesCurrencyAddress(withdrawAddressRequest)
            currency = self.safe_value(withdrawAddress, 'currency')
            allowedAmount = self.safe_value(currency, 'allowed_amount')
            minimum = self.safe_float(allowedAmount, 'min')
            if amount <= minimum:
                raise BadRequest(self.id + ' ' + code + ' withdraw failed, amount ' + str(amount) + ' must be greater than the minimum allowed amount of ' + str(minimum))
            # {
            #   "type": "withdrawal_addresses",
            #   "currency": {
            #     "type": "withdrawal_currency",
            #     "id": "BTC",
            #     "waves_asset_id": "8LQW8f7P5d5PZM7GtZEBgaqRPGSzS3DfPuiXrURJ4AJS",
            #     "decimals": 8,
            #     "status": "active",
            #     "allowed_amount": {
            #       "min": 0.001,
            #       "max": 20
            #     },
            #     "fees": {
            #       "flat": 0.001,
            #       "rate": 0
            #     }
            #   },
            #   "proxy_addresses": [
            #     "3P3qqmkiLwNHB7x1FeoE8bvkRtULwGpo9ga"
            #   ]
            # }
            proxyAddresses = self.safe_value(withdrawAddress, 'proxy_addresses', [])
            proxyAddress = self.safe_string(proxyAddresses, 0)
        fee = self.safe_integer(self.options, 'withdrawFeeWAVES', 100000)  # 0.001 WAVES
        feeAssetId = 'WAVES'
        type = 4  # transfer
        version = 2
        amountInteger = self.currency_to_precision(code, amount)
        currency = self.currency(code)
        timestamp = self.milliseconds()
        byteArray = [
            self.number_to_be(4, 1),
            self.number_to_be(2, 1),
            self.base58_to_binary(self.apiKey),
            self.get_asset_bytes(currency['id']),
            self.get_asset_bytes(feeAssetId),
            self.number_to_be(timestamp, 8),
            self.number_to_be(amountInteger, 8),
            self.number_to_be(fee, 8),
            self.base58_to_binary(proxyAddress),
            self.number_to_be(0, 2),
        ]
        binary = self.binary_concat_array(byteArray)
        hexSecret = self.binary_to_base16(self.base58_to_binary(self.secret))
        signature = self.eddsa(self.binary_to_base16(binary), hexSecret, 'ed25519')
        request = {
            'senderPublicKey': self.apiKey,
            'amount': amountInteger,
            'fee': fee,
            'type': type,
            'version': version,
            'attachment': '',
            'feeAssetId': self.get_asset_id(feeAssetId),
            'proofs': [
                signature,
            ],
            'assetId': self.get_asset_id(currency['id']),
            'recipient': proxyAddress,
            'timestamp': timestamp,
            'signature': signature,
        }
        return self.nodePostTransactionsBroadcast(request)
