<?php

namespace ccxt;

// 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

use Exception; // a common import
use ccxt\abstract\wavesexchange as Exchange;

class wavesexchange extends Exchange {

    public function describe(): mixed {
        return $this->deep_extend(parent::describe(), array(
            'id' => 'wavesexchange',
            'name' => 'Waves.Exchange',
            'countries' => array( 'CH' ), // Switzerland
            'certified' => false,
            'pro' => false,
            'dex' => true,
            'has' => array(
                'CORS' => null,
                'spot' => true,
                'margin' => false,
                'swap' => false,
                'future' => false,
                'option' => false,
                'addMargin' => false,
                'borrowCrossMargin' => false,
                'borrowIsolatedMargin' => false,
                'borrowMargin' => false,
                'cancelOrder' => true,
                'closeAllPositions' => false,
                'closePosition' => false,
                'createMarketOrder' => true,
                'createOrder' => true,
                'createReduceOnlyOrder' => false,
                'createStopLimitOrder' => false,
                'createStopMarketOrder' => false,
                'createStopOrder' => false,
                'fetchAllGreeks' => false,
                'fetchBalance' => true,
                'fetchBorrowInterest' => false,
                'fetchBorrowRate' => false,
                'fetchBorrowRateHistories' => false,
                'fetchBorrowRateHistory' => false,
                'fetchBorrowRates' => false,
                'fetchBorrowRatesPerSymbol' => false,
                'fetchClosedOrders' => true,
                'fetchCrossBorrowRate' => false,
                'fetchCrossBorrowRates' => false,
                'fetchCurrencies' => false,
                'fetchDepositAddress' => true,
                'fetchDepositAddresses' => null,
                'fetchDepositAddressesByNetwork' => null,
                'fetchDepositWithdrawFee' => 'emulated',
                'fetchDepositWithdrawFees' => true,
                'fetchFundingHistory' => false,
                'fetchFundingInterval' => false,
                'fetchFundingIntervals' => false,
                'fetchFundingRate' => false,
                'fetchFundingRateHistory' => false,
                'fetchFundingRates' => false,
                'fetchGreeks' => false,
                'fetchIndexOHLCV' => false,
                'fetchIsolatedBorrowRate' => false,
                'fetchIsolatedBorrowRates' => false,
                'fetchIsolatedPositions' => false,
                'fetchLeverage' => false,
                'fetchLeverages' => false,
                'fetchLeverageTiers' => false,
                'fetchLiquidations' => false,
                'fetchLongShortRatio' => false,
                'fetchLongShortRatioHistory' => false,
                'fetchMarginAdjustmentHistory' => false,
                'fetchMarginMode' => false,
                'fetchMarginModes' => false,
                'fetchMarketLeverageTiers' => false,
                'fetchMarkets' => true,
                'fetchMarkOHLCV' => false,
                'fetchMarkPrice' => false,
                'fetchMarkPrices' => false,
                'fetchMyLiquidations' => false,
                'fetchMySettlementHistory' => false,
                'fetchMyTrades' => true,
                'fetchOHLCV' => true,
                'fetchOpenInterest' => false,
                'fetchOpenInterestHistory' => false,
                'fetchOpenInterests' => false,
                'fetchOpenOrders' => true,
                'fetchOption' => false,
                'fetchOptionChain' => false,
                'fetchOrder' => true,
                'fetchOrderBook' => true,
                'fetchOrders' => true,
                'fetchPosition' => false,
                'fetchPositionHistory' => false,
                'fetchPositionMode' => false,
                'fetchPositions' => false,
                'fetchPositionsForSymbol' => false,
                'fetchPositionsHistory' => false,
                'fetchPositionsRisk' => false,
                'fetchPremiumIndexOHLCV' => false,
                'fetchSettlementHistory' => false,
                'fetchTicker' => true,
                'fetchTickers' => true,
                'fetchTrades' => true,
                'fetchTransfer' => false,
                'fetchTransfers' => false,
                'fetchUnderlyingAssets' => false,
                'fetchVolatilityHistory' => false,
                'reduceMargin' => false,
                'repayCrossMargin' => false,
                'repayIsolatedMargin' => false,
                'sandbox' => true,
                'setLeverage' => false,
                'setMargin' => false,
                'setMarginMode' => false,
                'setPositionMode' => false,
                'signIn' => true,
                'transfer' => false,
                'withdraw' => true,
                'ws' => false,
            ),
            'timeframes' => array(
                '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' => array(
                'logo' => 'https://user-images.githubusercontent.com/1294454/84547058-5fb27d80-ad0b-11ea-8711-78ac8b3c7f31.jpg',
                'test' => array(
                    'matcher' => 'https://matcher-testnet.wx.network',
                    'node' => 'https://nodes-testnet.wavesnodes.com',
                    'public' => 'https://api-testnet.wavesplatform.com/v0',
                    'private' => 'https://api-testnet.wx.network/v1',
                    'forward' => 'https://testnet.wx.network/api/v1/forward/matcher',
                    'market' => 'https://testnet.wx.network/api/v1/forward/marketdata/api/v1',
                ),
                'api' => array(
                    'matcher' => 'https://matcher.wx.network',
                    'node' => 'https://nodes.wx.network',
                    'public' => 'https://api.wavesplatform.com/v0',
                    'private' => 'https://api.wx.network/v1',
                    'forward' => 'https://wx.network/api/v1/forward/matcher',
                    'market' => 'https://wx.network/api/v1/forward/marketdata/api/v1',
                ),
                'doc' => array(
                    'https://docs.wx.network',
                    'https://docs.waves.tech',
                    'https://api.wavesplatform.com/v0/docs/',
                    'https://nodes.wavesnodes.com/api-docs/index.html',
                    'https://matcher.waves.exchange/api-docs/index.html',
                ),
                'www' => 'https://wx.network',
            ),
            'api' => array(
                'matcher' => array(
                    'get' => array(
                        'matcher',
                        'matcher/settings',
                        'matcher/settings/rates',
                        'matcher/balance/reserved/{publicKey}',
                        'matcher/debug/allSnashotOffsets',
                        'matcher/debug/currentOffset',
                        'matcher/debug/lastOffset',
                        'matcher/debug/oldestSnapshotOffset',
                        'matcher/debug/config',
                        'matcher/debug/address/{address}',
                        'matcher/debug/status',
                        'matcher/debug/address/{address}/check',
                        'matcher/orderbook',
                        'matcher/orderbook/{baseId}/{quoteId}',
                        '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}/tradableBalance/{address}',
                        'matcher/orderbook/{publicKey}',
                        'matcher/orderbook/{publicKey}/{orderId}',
                        'matcher/orders/{address}',
                        'matcher/orders/{address}/{orderId}',
                        'matcher/transactions/{orderId}',
                        'api/v1/orderbook/{baseId}/{quoteId}',
                    ),
                    'post' => array(
                        'matcher/orderbook',
                        'matcher/orderbook/market',
                        'matcher/orderbook/cancel',
                        'matcher/orderbook/{baseId}/{quoteId}/cancel',
                        'matcher/orderbook/{baseId}/{quoteId}/calculateFee',
                        'matcher/orderbook/{baseId}/{quoteId}/delete',
                        'matcher/orderbook/{baseId}/{quoteId}/cancelAll',
                        'matcher/debug/saveSnapshots',
                        'matcher/orders/{address}/cancel',
                        'matcher/orders/cancel/{orderId}',
                        'matcher/orders/serialize',
                    ),
                    'delete' => array(
                        'matcher/orderbook/{baseId}/{quoteId}',
                        'matcher/settings/rates/{assetId}',
                    ),
                    'put' => array(
                        'matcher/settings/rates/{assetId}',
                    ),
                ),
                'node' => array(
                    'get' => array(
                        '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' => array(
                        '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' => array(
                        'addresses/{address}',
                        'debug/rollback-to/{signature}',
                    ),
                ),
                'public' => array(
                    'get' => array(
                        'assets',
                        'pairs',
                        'candles/{baseId}/{quoteId}',
                        'transactions/exchange',
                    ),
                ),
                'private' => array(
                    'get' => array(
                        'deposit/addresses/{currency}',
                        'deposit/addresses/{currency}/{platform}',
                        'platforms',
                        'deposit/currencies',
                        'withdraw/currencies',
                        'withdraw/addresses/{currency}/{address}',
                    ),
                    'post' => array(
                        'oauth2/token',
                    ),
                ),
                'forward' => array(
                    'get' => array(
                        'matcher/orders/{address}',  // can't get the orders endpoint to work with the matcher api
                        'matcher/orders/{address}/{orderId}',
                    ),
                    'post' => array(
                        'matcher/orders/{wavesAddress}/cancel',
                    ),
                ),
                'market' => array(
                    'get' => array(
                        'tickers',
                    ),
                ),
            ),
            'currencies' => array(
                'WX' => $this->safe_currency_structure(array( 'id' => 'EMAMLxDnv3xiz8RXg8Btj33jcEw3wLczL3JKYYmuubpc', 'numericId' => null, 'code' => 'WX', 'precision' => $this->parse_number('1e-8') )),
            ),
            'precisionMode' => TICK_SIZE,
            'options' => array(
                'allowedCandles' => 1440,
                'accessToken' => null,
                'createMarketBuyOrderRequiresPrice' => true,
                'matcherPublicKey' => null,
                'quotes' => null,
                'createOrderDefaultExpiry' => 2419200000, // 60 * 60 * 24 * 28 * 1000
                'wavesAddress' => null,
                'withdrawFeeUSDN' => 7420,
                'withdrawFeeWAVES' => 100000,
                'wavesPrecision' => 1e-8,
                'messagePrefix' => 'W', // W for production, T for testnet
                'networks' => array(
                    'ERC20' => 'ETH',
                    'BEP20' => 'BSC',
                ),
            ),
            'features' => array(
                'spot' => array(
                    'sandbox' => true,
                    'createOrder' => array(
                        'marginMode' => false,
                        'triggerPrice' => true, // todo
                        'triggerDirection' => false,
                        'triggerPriceType' => null,
                        'stopLossPrice' => false, // todo
                        'takeProfitPrice' => false, // todo
                        'attachedStopLossTakeProfit' => null,
                        'timeInForce' => array(
                            'IOC' => false,
                            'FOK' => false,
                            'PO' => false,
                            'GTD' => true, // todo
                        ),
                        'hedged' => false,
                        'trailing' => false,
                        'leverage' => false,
                        'marketBuyByCost' => false, // todo
                        'marketBuyRequiresPrice' => true,
                        'selfTradePrevention' => false,
                        'iceberg' => false,
                    ),
                    'createOrders' => null,
                    'fetchMyTrades' => array(
                        'marginMode' => false,
                        'limit' => 100, // todo
                        'daysBack' => 100000, // todo
                        'untilDays' => 100000, // todo
                        'symbolRequired' => false,
                    ),
                    'fetchOrder' => array(
                        'marginMode' => false,
                        'trigger' => false,
                        'trailing' => false,
                        'symbolRequired' => false,
                    ),
                    'fetchOpenOrders' => array(
                        'marginMode' => false,
                        'limit' => 100, // todo
                        'trigger' => false,
                        'trailing' => false,
                        'symbolRequired' => false,
                    ),
                    'fetchOrders' => array(
                        'marginMode' => false,
                        'limit' => 100, // todo
                        'daysBack' => null,
                        'untilDays' => null,
                        'trigger' => false,
                        'trailing' => false,
                        'symbolRequired' => true,
                    ), // todo
                    'fetchClosedOrders' => array(
                        'marginMode' => false,
                        'limit' => 100,
                        'daysBack' => 100000, // todo
                        'daysBackCanceled' => 1, // todo
                        'untilDays' => 100000, // todo
                        'trigger' => false,
                        'trailing' => false,
                        'symbolRequired' => false,
                    ),
                    'fetchOHLCV' => array(
                        'limit' => null, // todo
                    ),
                ),
                'swap' => array(
                    'linear' => null,
                    'inverse' => null,
                ),
                'future' => array(
                    'linear' => null,
                    'inverse' => null,
                ),
            ),
            'commonCurrencies' => array(
                'EGG' => 'Waves Ducks',
            ),
            'requiresEddsa' => true,
            'exceptions' => array(
                '3147270' => '\\ccxt\\InsufficientFunds',  // https://github.com/wavesplatform/matcher/wiki/List-of-all-errors
                '112' => '\\ccxt\\InsufficientFunds',
                '4' => '\\ccxt\\ExchangeError',
                '13' => '\\ccxt\\ExchangeNotAvailable',
                '14' => '\\ccxt\\ExchangeNotAvailable',
                '3145733' => '\\ccxt\\AccountSuspended',
                '3148040' => '\\ccxt\\DuplicateOrderId',
                '3148801' => '\\ccxt\\AuthenticationError',
                '9440512' => '\\ccxt\\AuthenticationError',
                '9440771' => '\\ccxt\\BadSymbol',
                '9441026' => '\\ccxt\\InvalidOrder',
                '9441282' => '\\ccxt\\InvalidOrder',
                '9441286' => '\\ccxt\\InvalidOrder',
                '9441295' => '\\ccxt\\InvalidOrder',
                '9441540' => '\\ccxt\\InvalidOrder',
                '9441542' => '\\ccxt\\InvalidOrder',
                '106954752' => '\\ccxt\\AuthenticationError',
                '106954769' => '\\ccxt\\AuthenticationError',
                '106957828' => '\\ccxt\\AuthenticationError',
                '106960131' => '\\ccxt\\AuthenticationError',
                '106981137' => '\\ccxt\\AuthenticationError',
                '9437184' => '\\ccxt\\BadRequest', // array("error":9437184,"message":"The order is invalid => SpendAmount should be > 0","template":"The order is invalid => array({details})","params":array("details":"SpendAmount should be > 0"),"status":"OrderRejected","success":false)
                '9437193' => '\\ccxt\\OrderNotFound',
                '1048577' => '\\ccxt\\BadRequest',
                '1051904' => '\\ccxt\\AuthenticationError',
            ),
        ));
    }

    public function set_sandbox_mode($enabled) {
        $this->options['messagePrefix'] = $enabled ? 'T' : 'W';
        $this->options['sandboxMode'] = $enabled;
        parent::set_sandbox_mode($enabled);
    }

    public function get_fees_for_asset(string $symbol, $side, $amount, $price, $params = array ()) {
        $this->load_markets();
        $market = $this->market($symbol);
        $amount = $this->to_real_symbol_amount($symbol, $amount);
        $price = $this->to_real_symbol_price($symbol, $price);
        $request = $this->extend(array(
            'baseId' => $market['baseId'],
            'quoteId' => $market['quoteId'],
            'orderType' => $side,
            'amount' => $amount,
            'price' => $price,
        ), $params);
        return $this->matcherPostMatcherOrderbookBaseIdQuoteIdCalculateFee ($request);
    }

    public function custom_calculate_fee(string $symbol, $type, $side, $amount, $price, $takerOrMaker = 'taker', $params = array ()) {
        $response = $this->get_fees_for_asset($symbol, $side, $amount, $price);
        // {
        //     "base":array(
        //        "feeAssetId":"WAVES",
        //        "matcherFee":"1000000"
        //     ),
        //     "discount":{
        //        "feeAssetId":"EMAMLxDnv3xiz8RXg8Btj33jcEw3wLczL3JKYYmuubpc",
        //        "matcherFee":"4077612"
        //     }
        //  }
        $isDiscountFee = $this->safe_bool($params, 'isDiscountFee', false);
        $mode = null;
        if ($isDiscountFee) {
            $mode = $this->safe_value($response, 'discount');
        } else {
            $mode = $this->safe_value($response, 'base');
        }
        $matcherFee = $this->safe_string($mode, 'matcherFee');
        $feeAssetId = $this->safe_string($mode, 'feeAssetId');
        $feeAsset = $this->safe_currency_code($feeAssetId);
        $adjustedMatcherFee = $this->from_real_currency_amount($feeAsset, $matcherFee);
        $amountAsString = $this->number_to_string($amount);
        $priceAsString = $this->number_to_string($price);
        $feeCost = $this->fee_to_precision($symbol, $this->parse_number($adjustedMatcherFee));
        $feeRate = Precise::string_div($adjustedMatcherFee, Precise::string_mul($amountAsString, $priceAsString));
        return array(
            'type' => $takerOrMaker,
            'currency' => $feeAsset,
            'rate' => $this->parse_number($feeRate),
            'cost' => $this->parse_number($feeCost),
        );
    }

    public function get_quotes() {
        $quotes = $this->safe_value($this->options, 'quotes');
        if ($quotes) {
            return $quotes;
        } else {
            // currencies can have any name because you can create you own token
            // result someone can create a fake token called BTC
            // we use this mapping to determine the real tokens
            // https://docs.wx.network/en/waves-matcher/matcher-api#asset-pair
            $response = $this->matcherGetMatcherSettings ();
            // {
            //   "orderVersions" => array(
            //     1,
            //     2,
            //     3
            //   ),
            //   "success" => true,
            //   "matcherPublicKey" => "9cpfKN9suPNvfeUNphzxXMjcnn974eme8ZhWUjaktzU5",
            //   "orderFee" => {
            //     "dynamic" => {
            //       "baseFee" => 300000,
            //       "rates" => array(
            //         "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" => array(
            //     "Ft8X1v1LTa1ABafufpaCWyVj8KkaxUWE6xBhW6sNFJck",
            //     "DG2xFkPdDwKUoBkzGAhQtLpSGzfXLiCYPEzeKH2Ad24p",
            //     "34N9YcEETLWn93qYQ64EsP1x89tSruJU44RrEMSXXEPJ",
            //     "Gtb1WRznfchDnTh37ezoDTJ4wcoKaRsKqKjJjy7nm2zU",
            //     "2mX5DzVKWrAJw8iwdJnV2qtoeVG9h5nTDpTqC1wb1WEN",
            //     "8LQW8f7P5d5PZM7GtZEBgaqRPGSzS3DfPuiXrURJ4AJS",
            //     "WAVES",
            //     "474jTeYx2r2Va35794tCScAXWJG9hU2HcgxzMowaZUnu",
            //     "zMFqXuoyrn5w17PFurTqxB7GsS71fp9dfk6XFwxbPCy",
            //     "62LyMjcr2DtiyF5yVXFhoQ2q414VPPJXjsNYp72SuDCH",
            //     "HZk1mbfuJpmxU1Fs4AX5MWLVYtctsNcg6e2C6VKqK8zk",
            //     "B3uGHFRpSUuGEDWjqB9LWWxafQj8VTvpMucEyoxzws5H",
            //     "5WvPKSJXzVE2orvbkJ8wsQmmQKqTv9sGBPksV4adViw3",
            //     "BrjUWjndUanm5VsJkbUip8VRYy6LWJePtxya3FNv4TQa",
            //     "4LHHvYGNKJUg5hj65aGD5vgScvCBmLpdRFtjokvCjSL8"
            //   )
            // }
            $quotes = array();
            $priceAssets = $this->safe_value($response, 'priceAssets');
            for ($i = 0; $i < count($priceAssets); $i++) {
                $quotes[$priceAssets[$i]] = true;
            }
            $this->options['quotes'] = $quotes;
            return $quotes;
        }
    }

    public function fetch_markets($params = array ()): array {
        /**
         * retrieves data on all markets for wavesexchange
         * @param {array} [$params] extra parameters specific to the exchange API endpoint
         * @return {array[]} an array of objects representing market data
         */
        $response = $this->marketGetTickers ();
        //
        //   array(
        //       {
        //           "symbol" => "WAVES/BTC",
        //           "amountAssetID" => "WAVES",
        //           "amountAssetName" => "Waves",
        //           "amountAssetDecimals" => 8,
        //           "amountAssetTotalSupply" => "106908766.00000000",
        //           "amountAssetMaxSupply" => "106908766.00000000",
        //           "amountAssetCirculatingSupply" => "106908766.00000000",
        //           "priceAssetID" => "8LQW8f7P5d5PZM7GtZEBgaqRPGSzS3DfPuiXrURJ4AJS",
        //           "priceAssetName" => "WBTC",
        //           "priceAssetDecimals" => 8,
        //           "priceAssetTotalSupply" => "20999999.96007507",
        //           "priceAssetMaxSupply" => "20999999.96007507",
        //           "priceAssetCirculatingSupply" => "20999999.66019601",
        //           "24h_open" => "0.00032688",
        //           "24h_high" => "0.00033508",
        //           "24h_low" => "0.00032443",
        //           "24h_close" => "0.00032806",
        //           "24h_vwap" => "0.00032988",
        //           "24h_volume" => "42349.69440104",
        //           "24h_priceVolume" => "13.97037207",
        //           "timestamp":1640232379124
        //       }
        //       ...
        //   )
        //
        $result = array();
        for ($i = 0; $i < count($response); $i++) {
            $entry = $response[$i];
            $baseId = $this->safe_string($entry, 'amountAssetID');
            $quoteId = $this->safe_string($entry, 'priceAssetID');
            $id = $baseId . '/' . $quoteId;
            $marketId = $this->safe_string($entry, 'symbol');
            list($base, $quote) = explode('/', $marketId);
            $base = $this->safe_currency_code($base);
            $quote = $this->safe_currency_code($quote);
            $symbol = $base . '/' . $quote;
            $result[] = array(
                'id' => $id,
                'symbol' => $symbol,
                'base' => $base,
                'quote' => $quote,
                'settle' => null,
                'baseId' => $baseId,
                'quoteId' => $quoteId,
                'settleId' => null,
                'type' => 'spot',
                'spot' => true,
                'margin' => false,
                'swap' => false,
                'future' => false,
                'option' => false,
                'active' => null,
                'contract' => false,
                'linear' => null,
                'inverse' => null,
                'contractSize' => null,
                'expiry' => null,
                'expiryDatetime' => null,
                'strike' => null,
                'optionType' => null,
                'precision' => array(
                    'amount' => $this->parse_number($this->parse_precision($this->safe_string($entry, 'amountAssetDecimals'))),
                    'price' => $this->parse_number($this->parse_precision($this->safe_string($entry, 'priceAssetDecimals'))),
                ),
                'limits' => array(
                    'leverage' => array(
                        'min' => null,
                        'max' => null,
                    ),
                    'amount' => array(
                        'min' => null,
                        'max' => null,
                    ),
                    'price' => array(
                        'min' => null,
                        'max' => null,
                    ),
                    'cost' => array(
                        'min' => null,
                        'max' => null,
                    ),
                ),
                'created' => null,
                'info' => $entry,
            );
        }
        return $result;
    }

    public function fetch_order_book(string $symbol, ?int $limit = null, $params = array ()): array {
        /**
         * fetches information on open orders with bid (buy) and ask (sell) prices, volumes and other data
         *
         * @see https://matcher.waves.exchange/api-docs/index.html#/markets/getOrderBook
         *
         * @param {string} $symbol unified $symbol of the $market to fetch the order book for
         * @param {int} [$limit] the maximum amount of order book entries to return
         * @param {array} [$params] extra parameters specific to the exchange API endpoint
         * @return {array} A dictionary of ~@link https://docs.ccxt.com/#/?id=order-book-structure order book structures~ indexed by $market symbols
         */
        $this->load_markets();
        $market = $this->market($symbol);
        $request = $this->extend(array(
            'baseId' => $market['baseId'],
            'quoteId' => $market['quoteId'],
        ), $params);
        $response = $this->matcherGetMatcherOrderbookBaseIdQuoteId ($request);
        $timestamp = $this->safe_integer($response, 'timestamp');
        $bids = $this->parse_order_book_side($this->safe_value($response, 'bids'), $market, $limit);
        $asks = $this->parse_order_book_side($this->safe_value($response, 'asks'), $market, $limit);
        return array(
            'symbol' => $symbol,
            'bids' => $bids,
            'asks' => $asks,
            'timestamp' => $timestamp,
            'datetime' => $this->iso8601($timestamp),
            'nonce' => null,
        );
    }

    public function parse_order_book_side($bookSide, $market = null, ?int $limit = null) {
        $precision = $market['precision'];
        $wavesPrecision = $this->safe_string($this->options, 'wavesPrecision', '1e-8');
        $amountPrecisionString = $this->safe_string($precision, 'amount');
        $pricePrecisionString = $this->safe_string($precision, 'price');
        $difference = Precise::string_div($amountPrecisionString, $pricePrecisionString);
        $pricePrecision = Precise::string_div($wavesPrecision, $difference);
        $result = array();
        for ($i = 0; $i < count($bookSide); $i++) {
            $entry = $bookSide[$i];
            $entryPrice = $this->safe_string($entry, 'price', '0');
            $entryAmount = $this->safe_string($entry, 'amount', '0');
            $price = null;
            $amount = null;
            if (($pricePrecision !== null) && ($entryPrice !== null)) {
                $price = Precise::string_mul($entryPrice, $pricePrecision);
            }
            if (($amountPrecisionString !== null) && ($entryAmount !== null)) {
                $amount = Precise::string_mul($entryAmount, $amountPrecisionString);
            }
            if (($limit !== null) && ($i > $limit)) {
                break;
            }
            $result[] = array(
                $this->parse_number($price),
                $this->parse_number($amount),
            );
        }
        return $result;
    }

    public function check_required_keys() {
        if ($this->apiKey === null) {
            throw new AuthenticationError($this->id . ' requires apiKey credential');
        }
        if ($this->secret === null) {
            throw new AuthenticationError($this->id . ' requires secret credential');
        }
        $apiKeyBytes = null;
        $secretKeyBytes = null;
        try {
            $apiKeyBytes = $this->base58_to_binary($this->apiKey);
        } catch (Exception $e) {
            throw new AuthenticationError($this->id . ' apiKey must be a base58 encoded public key');
        }
        try {
            $secretKeyBytes = $this->base58_to_binary($this->secret);
        } catch (Exception $e) {
            throw new AuthenticationError($this->id . ' secret must be a base58 encoded private key');
        }
        $hexApiKeyBytes = bin2hex($apiKeyBytes);
        $hexSecretKeyBytes = bin2hex($secretKeyBytes);
        if (strlen($hexApiKeyBytes) !== 64) {
            throw new AuthenticationError($this->id . ' apiKey must be a base58 encoded public key');
        }
        if (strlen($hexSecretKeyBytes) !== 64) {
            throw new AuthenticationError($this->id . ' secret must be a base58 encoded private key');
        }
        return true;
    }

    public function sign($path, $api = 'public', $method = 'GET', $params = array (), $headers = null, $body = null) {
        $query = $this->omit($params, $this->extract_params($path));
        $isCancelOrder = $path === 'matcher/orders/{wavesAddress}/cancel';
        $path = $this->implode_params($path, $params);
        $url = $this->urls['api'][$api] . '/' . $path;
        $queryString = $this->urlencode_with_array_repeat($query);
        if (($api === 'private') || ($api === 'forward')) {
            $headers = array(
                'Accept' => 'application/json',
            );
            $accessToken = $this->safe_string($this->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 = $this->json([ $query['orderId'] ]);
                $queryString = '';
            }
            if (strlen($queryString) > 0) {
                $url .= '?' . $queryString;
            }
        } elseif ($api === 'matcher') {
            if ($method === 'POST') {
                $headers = array(
                    'Accept' => 'application/json',
                    'Content-Type' => 'application/json',
                );
                $body = $this->json($query);
            } else {
                $headers = $query;
            }
        } else {
            if ($method === 'POST') {
                $headers = array(
                    'content-type' => 'application/json',
                );
                $body = $this->json($query);
            } else {
                $headers = array(
                    'content-type' => 'application/x-www-form-urlencoded',
                );
                if (strlen($queryString) > 0) {
                    $url .= '?' . $queryString;
                }
            }
        }
        return array( 'url' => $url, 'method' => $method, 'body' => $body, 'headers' => $headers );
    }

    public function sign_in($params = array ()) {
        /**
         * sign in, must be called prior to using other authenticated methods
         *
         * @see https://docs.wx.network/en/api/auth/oauth2-token
         *
         * @param {array} [$params] extra parameters specific to the exchange API endpoint
         * @return $response from exchange
         */
        if (!$this->safe_string($this->options, 'accessToken')) {
            $prefix = 'ffffff01';
            $expiresDelta = 60 * 60 * 24 * 7;
            $seconds = $this->sum($this->seconds(), $expiresDelta);
            $seconds = (string) $seconds;
            $clientId = 'wx.network';
            // W for production, T for testnet
            $defaultMessagePrefix = $this->safe_string($this->options, 'messagePrefix', 'W');
            $message = $defaultMessagePrefix . ':' . $clientId . ':' . $seconds;
            $messageHex = bin2hex($this->encode($message));
            $payload = $prefix . $messageHex;
            $hexKey = bin2hex($this->base58_to_binary($this->secret));
            $signature = $this->axolotl($payload, $hexKey, 'ed25519');
            $request = array(
                'grant_type' => 'password',
                'scope' => 'general',
                'username' => $this->apiKey,
                'password' => $seconds . ':' . $signature,
                'client_id' => $clientId,
            );
            $response = $this->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" }
            $this->options['accessToken'] = $this->safe_string($response, 'access_token');
            return $this->options['accessToken'];
        }
        return null;
    }

    public function parse_ticker(array $ticker, ?array $market = null): array {
        //
        //       {
        //           "symbol" => "WAVES/BTC",
        //           "amountAssetID" => "WAVES",
        //           "amountAssetName" => "Waves",
        //           "amountAssetDecimals" => 8,
        //           "amountAssetTotalSupply" => "106908766.00000000",
        //           "amountAssetMaxSupply" => "106908766.00000000",
        //           "amountAssetCirculatingSupply" => "106908766.00000000",
        //           "priceAssetID" => "8LQW8f7P5d5PZM7GtZEBgaqRPGSzS3DfPuiXrURJ4AJS",
        //           "priceAssetName" => "WBTC",
        //           "priceAssetDecimals" => 8,
        //           "priceAssetTotalSupply" => "20999999.96007507",
        //           "priceAssetMaxSupply" => "20999999.96007507",
        //           "priceAssetCirculatingSupply" => "20999999.66019601",
        //           "24h_open" => "0.00032688",
        //           "24h_high" => "0.00033508",
        //           "24h_low" => "0.00032443",
        //           "24h_close" => "0.00032806",
        //           "24h_vwap" => "0.00032988",
        //           "24h_volume" => "42349.69440104",
        //           "24h_priceVolume" => "13.97037207",
        //           "timestamp":1640232379124
        //       }
        //
        //  fetch $ticker
        //
        //       {
        //           "firstPrice" => "21749",
        //           "lastPrice" => "22000",
        //           "volume" => "0.73747149",
        //           "quoteVolume" => "16409.44564928645471",
        //           "high" => "23589.999941",
        //           "low" => "21010.000845",
        //           "weightedAveragePrice" => "22250.955964",
        //           "txsCount" => "148",
        //           "volumeWaves" => "0.0000000000680511203072"
        //       }
        //
        $timestamp = $this->safe_integer($ticker, 'timestamp');
        $marketId = $this->safe_string($ticker, 'symbol');
        $market = $this->safe_market($marketId, $market, '/');
        $symbol = $market['symbol'];
        $last = $this->safe_string_2($ticker, '24h_close', 'lastPrice');
        $low = $this->safe_string_2($ticker, '24h_low', 'low');
        $high = $this->safe_string_2($ticker, '24h_high', 'high');
        $vwap = $this->safe_string_2($ticker, '24h_vwap', 'weightedAveragePrice');
        $baseVolume = $this->safe_string_2($ticker, '24h_volume', 'volume');
        $quoteVolume = $this->safe_string_2($ticker, '24h_priceVolume', 'quoteVolume');
        $open = $this->safe_string_2($ticker, '24h_open', 'firstPrice');
        return $this->safe_ticker(array(
            'symbol' => $symbol,
            'timestamp' => $timestamp,
            'datetime' => $this->iso8601($timestamp),
            'high' => $high,
            'low' => $low,
            'bid' => null,
            'bidVolume' => null,
            'ask' => null,
            'askVolume' => null,
            'vwap' => $vwap,
            'open' => $open,
            'close' => $last,
            'last' => $last,
            'previousClose' => null,
            'change' => null,
            'percentage' => null,
            'average' => null,
            'baseVolume' => $baseVolume,
            'quoteVolume' => $quoteVolume,
            'info' => $ticker,
        ), $market);
    }

    public function fetch_ticker(string $symbol, $params = array ()): array {
        /**
         * fetches a price $ticker, a statistical calculation with the information calculated over the past 24 hours for a specific $market
         *
         * @see https://api.wavesplatform.com/v0/docs/#/pairs/getPairsListAll
         *
         * @param {string} $symbol unified $symbol of the $market to fetch the $ticker for
         * @param {array} [$params] extra parameters specific to the exchange API endpoint
         * @return {array} a ~@link https://docs.ccxt.com/#/?id=$ticker-structure $ticker structure~
         */
        $this->load_markets();
        $market = $this->market($symbol);
        $request = array(
            'pairs' => $market['id'],
        );
        $response = $this->publicGetPairs ($this->extend($request, $params));
        //
        //     {
        //         "__type":"list",
        //         "data":array(
        //             {
        //                 "__type":"pair",
        //                 "data":array(
        //                     "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 = $this->safe_value($response, 'data', array());
        $ticker = $this->safe_value($data, 0, array());
        $dataTicker = $this->safe_dict($ticker, 'data', array());
        return $this->parse_ticker($dataTicker, $market);
    }

    public function fetch_tickers(?array $symbols = null, $params = array ()): array {
        /**
         * fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market
         * @param {string[]} [$symbols] unified $symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned
         * @param {array} [$params] extra parameters specific to the exchange API endpoint
         * @return {array} a dictionary of ~@link https://docs.ccxt.com/#/?id=ticker-structure ticker structures~
         */
        $this->load_markets();
        $response = $this->marketGetTickers ($params);
        //
        //   array(
        //       {
        //           "symbol" => "WAVES/BTC",
        //           "amountAssetID" => "WAVES",
        //           "amountAssetName" => "Waves",
        //           "amountAssetDecimals" => 8,
        //           "amountAssetTotalSupply" => "106908766.00000000",
        //           "amountAssetMaxSupply" => "106908766.00000000",
        //           "amountAssetCirculatingSupply" => "106908766.00000000",
        //           "priceAssetID" => "8LQW8f7P5d5PZM7GtZEBgaqRPGSzS3DfPuiXrURJ4AJS",
        //           "priceAssetName" => "WBTC",
        //           "priceAssetDecimals" => 8,
        //           "priceAssetTotalSupply" => "20999999.96007507",
        //           "priceAssetMaxSupply" => "20999999.96007507",
        //           "priceAssetCirculatingSupply" => "20999999.66019601",
        //           "24h_open" => "0.00032688",
        //           "24h_high" => "0.00033508",
        //           "24h_low" => "0.00032443",
        //           "24h_close" => "0.00032806",
        //           "24h_vwap" => "0.00032988",
        //           "24h_volume" => "42349.69440104",
        //           "24h_priceVolume" => "13.97037207",
        //           "timestamp":1640232379124
        //       }
        //       ...
        //   )
        //
        return $this->parse_tickers($response, $symbols);
    }

    public function fetch_ohlcv(string $symbol, $timeframe = '1m', ?int $since = null, ?int $limit = null, $params = array ()): array {
        /**
         * fetches historical candlestick $data containing the $open, high, low, and close price, and the volume of a $market
         *
         * @see https://api.wavesplatform.com/v0/docs/#/candles/getCandles
         *
         * @param {string} $symbol unified $symbol of the $market to fetch OHLCV $data for
         * @param {string} $timeframe the $length of time each candle represents
         * @param {int} [$since] timestamp in ms of the earliest candle to fetch
         * @param {int} [$limit] the maximum amount of candles to fetch
         * @param {array} [$params] extra parameters specific to the exchange API endpoint
         * @param {int} [$params->until] timestamp in ms of the latest candle to fetch
         * @return {int[][]} A list of candles ordered, $open, high, low, close, volume
         */
        $this->load_markets();
        $market = $this->market($symbol);
        $request = array(
            'baseId' => $market['baseId'],
            'quoteId' => $market['quoteId'],
            'interval' => $this->safe_string($this->timeframes, $timeframe, $timeframe),
        );
        $allowedCandles = $this->safe_integer($this->options, 'allowedCandles', 1440);
        $until = $this->safe_integer($params, 'until');
        $untilIsDefined = $until !== null;
        if ($limit === null) {
            $limit = $allowedCandles;
        }
        $limit = min ($allowedCandles, $limit);
        $duration = $this->parse_timeframe($timeframe) * 1000;
        if ($since === null) {
            $now = $this->milliseconds();
            $timeEnd = $untilIsDefined ? $until : $now;
            $durationRoundedTimestamp = $this->parse_to_int($timeEnd / $duration) * $duration;
            $delta = ($limit - 1) * $duration;
            $timeStart = $durationRoundedTimestamp - $delta;
            $request['timeStart'] = (string) $timeStart;
            if ($untilIsDefined) {
                $request['timeEnd'] = (string) $until;
            }
        } else {
            $request['timeStart'] = (string) $since;
            if ($untilIsDefined) {
                $request['timeEnd'] = (string) $until;
            } else {
                $timeEnd = $this->sum($since, $duration * $limit);
                $request['timeEnd'] = (string) $timeEnd;
            }
        }
        $params = $this->omit($params, 'until');
        $response = $this->publicGetCandlesBaseIdQuoteId ($this->extend($request, $params));
        //
        //     {
        //         "__type" => "list",
        //         "data" => array(
        //             {
        //                 "__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 = $this->safe_value($response, 'data', array());
        $result = $this->parse_ohlcvs($data, $market, $timeframe, $since, $limit);
        $result = $this->filter_future_candles($result);
        $lastClose = null;
        $length = count($result);
        for ($i = 0; $i < count($result); $i++) {
            $j = $length - $i - 1;
            $entry = $result[$j];
            $open = $entry[1];
            if ($open === null) {
                $entry[1] = $lastClose;
                $entry[2] = $lastClose;
                $entry[3] = $lastClose;
                $entry[4] = $lastClose;
                $result[$j] = $entry;
            }
            $lastClose = $entry[4];
        }
        return $result;
    }

    public function filter_future_candles($ohlcvs) {
        $result = array();
        $timestamp = $this->milliseconds();
        for ($i = 0; $i < count($ohlcvs); $i++) {
            if ($ohlcvs[$i][0] > $timestamp) {
                // stop when getting data from the future
                break;
            }
            $result[] = $ohlcvs[$i];
        }
        return $result;
    }

    public function parse_ohlcv($ohlcv, ?array $market = null): array {
        //
        //     {
        //         "__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 = $this->safe_value($ohlcv, 'data', array());
        return array(
            $this->parse8601($this->safe_string($data, 'time')),
            $this->safe_number($data, 'open'),
            $this->safe_number($data, 'high'),
            $this->safe_number($data, 'low'),
            $this->safe_number($data, 'close'),
            $this->safe_number($data, 'volume', 0),
        );
    }

    public function fetch_deposit_address(string $code, $params = array ()): array {
        /**
         * fetch the deposit $address for a $currency associated with this account
         * @param {string} $code unified $currency $code
         * @param {array} [$params] extra parameters specific to the exchange API endpoint
         * @return {array} an ~@link https://docs.ccxt.com/#/?id=$address-structure $address structure~
         */
        $this->sign_in();
        $networks = $this->safe_value($this->options, 'networks', array());
        $rawNetwork = $this->safe_string_upper($params, 'network');
        $network = $this->safe_string($networks, $rawNetwork, $rawNetwork);
        $params = $this->omit($params, array( 'network' ));
        $supportedCurrencies = $this->privateGetPlatforms ();
        //
        //     {
        //       "type" => "list",
        //       "page_info" => array(
        //         "has_next_page" => false,
        //         "last_cursor" => null
        //       ),
        //       "items" => array(
        //         {
        //           "type" => "platform",
        //           "id" => "ETH",
        //           "name" => "Ethereum",
        //           "currencies" => array(
        //             "BAG",
        //             "BNT",
        //             "CRV",
        //             "EGG",
        //             "ETH",
        //             "EURN",
        //             "FL",
        //             "NSBT",
        //             "USDAP",
        //             "USDC",
        //             "USDFL",
        //             "USDN",
        //             "USDT",
        //             "WAVES"
        //           )
        //         }
        //       )
        //     }
        //
        $currencies = array();
        $networksByCurrency = array();
        $items = $this->safe_value($supportedCurrencies, 'items', array());
        for ($i = 0; $i < count($items); $i++) {
            $entry = $items[$i];
            $currencyId = $this->safe_string($entry, 'id');
            $innerCurrencies = $this->safe_value($entry, 'currencies', array());
            for ($j = 0; $j < count($innerCurrencies); $j++) {
                $currencyCode = $this->safe_string($innerCurrencies, $j);
                $currencies[$currencyCode] = true;
                if (!(is_array($networksByCurrency) && array_key_exists($currencyCode, $networksByCurrency))) {
                    $networksByCurrency[$currencyCode] = array();
                }
                $networksByCurrency[$currencyCode][$currencyId] = true;
            }
        }
        if (!(is_array($currencies) && array_key_exists($code, $currencies))) {
            $codes = is_array($currencies) ? array_keys($currencies) : array();
            throw new ExchangeError($this->id . ' fetchDepositAddress() ' . $code . ' not supported. Currency $code must be one of ' . implode(', ', $codes));
        }
        $response = null;
        if ($network === null) {
            $request = array(
                'currency' => $code,
            );
            $response = $this->privateGetDepositAddressesCurrency ($this->extend($request, $params));
        } else {
            $supportedNetworks = $networksByCurrency[$code];
            if (!(is_array($supportedNetworks) && array_key_exists($network, $supportedNetworks))) {
                $supportedNetworkKeys = is_array($supportedNetworks) ? array_keys($supportedNetworks) : array();
                throw new ExchangeError($this->id . ' ' . $network . ' $network ' . $code . ' deposit $address not supported. Network must be one of ' . implode(', ', $supportedNetworkKeys));
            }
            if ($network === 'WAVES') {
                $request = array(
                    'publicKey' => $this->apiKey,
                );
                $responseInner = $this->nodeGetAddressesPublicKeyPublicKey ($this->extend($request, $request));
                $addressInner = $this->safe_string($response, 'address');
                return array(
                    'info' => $responseInner,
                    'currency' => $code,
                    'network' => $network,
                    'address' => $addressInner,
                    'tag' => null,
                );
            } else {
                $request = array(
                    'currency' => $code,
                    'platform' => $network,
                );
                $response = $this->privateGetDepositAddressesCurrencyPlatform ($this->extend($request, $params));
            }
        }
        //
        // {
        //   "type" => "deposit_addresses",
        //   "currency" => {
        //     "type" => "deposit_currency",
        //     "id" => "ERGO",
        //     "waves_asset_id" => "5dJj4Hn9t2Ve3tRpNGirUHy4yBK6qdJRAJYV21yPPuGz",
        //     "platform_id" => "BSC",
        //     "decimals" => 9,
        //     "status" => "active",
        //     "allowed_amount" => array(
        //       "min" => 0.001,
        //       "max" => 100000
        //     ),
        //     "fees" => array(
        //       "flat" => 0,
        //       "rate" => 0
        //     }
        //   ),
        //   "deposit_addresses" => array(
        //     "9fRAAQjF8Yqg7qicQCL884zjimsRnuwsSavsM1rUdDaoG8mThku"
        //   )
        // }
        $currency = $this->safe_value($response, 'currency');
        $networkId = $this->safe_string($currency, 'platform_id');
        $networkByIds = $this->safe_value($this->options, 'networkByIds', array());
        $unifiedNetwork = $this->safe_string($networkByIds, $networkId, $networkId);
        $addresses = $this->safe_value($response, 'deposit_addresses');
        $address = $this->safe_string($addresses, 0);
        return array(
            'info' => $response,
            'currency' => $code,
            'network' => $unifiedNetwork,
            'address' => $address,
            'tag' => null,
        );
    }

    public function get_matcher_public_key() {
        // this method returns a single string
        $matcherPublicKey = $this->safe_string($this->options, 'matcherPublicKey');
        if ($matcherPublicKey) {
            return $matcherPublicKey;
        } else {
            $response = $this->matcherGetMatcher ();
            // remove trailing quotes from string $response
            $this->options['matcherPublicKey'] = mb_substr($response, 1, strlen($response) - 1 - 1);
            return $this->options['matcherPublicKey'];
        }
    }

    public function get_asset_bytes($currencyId) {
        if ($currencyId === 'WAVES') {
            return $this->number_to_be(0, 1);
        } else {
            return $this->binary_concat($this->number_to_be(1, 1), $this->base58_to_binary($currencyId));
        }
    }

    public function get_asset_id($currencyId) {
        if ($currencyId === 'WAVES') {
            return '';
        }
        return $currencyId;
    }

    public function to_real_currency_amount(string $code, float $amount, $networkCode = null) {
        $currency = $this->currency($code);
        $stringValue = Precise::string_div($this->number_to_string($amount), $this->safe_string($currency, 'precision'));
        return intval($stringValue);
    }

    public function from_real_currency_amount(string $code, string $amountString) {
        if (!(is_array($this->currencies) && array_key_exists($code, $this->currencies))) {
            return $amountString;
        }
        $currency = $this->currency($code);
        $precisionAmount = $this->safe_string($currency, 'precision');
        return Precise::string_mul($amountString, $precisionAmount);
    }

    public function to_real_symbol_price(string $symbol, float $price) {
        $market = $this->market($symbol);
        $stringValue = Precise::string_div($this->number_to_string($price), $this->safe_string($market['precision'], 'price'));
        return intval($stringValue);
    }

    public function from_real_symbol_price(string $symbol, string $priceString) {
        $market = $this->markets[$symbol];
        return Precise::string_mul($priceString, $this->safe_string($market['precision'], 'price'));
    }

    public function to_real_symbol_amount(string $symbol, float $amount) {
        $market = $this->market($symbol);
        $stringValue = Precise::string_div($this->number_to_string($amount), $this->safe_string($market['precision'], 'amount'));
        return intval($stringValue);
    }

    public function from_real_symbol_amount(string $symbol, string $amountString) {
        $market = $this->markets[$symbol];
        return Precise::string_mul($amountString, $market['precision']['amount']);
    }

    public function safe_get_dynamic($settings) {
        $orderFee = $this->safe_value($settings, 'orderFee');
        if (is_array($orderFee) && array_key_exists('dynamic', $orderFee)) {
            return $this->safe_value($orderFee, 'dynamic');
        } else {
            return $this->safe_value($orderFee['composite']['default'], 'dynamic');
        }
    }

    public function safe_get_rates($dynamic) {
        $rates = $this->safe_value($dynamic, 'rates');
        if ($rates === null) {
            return array( 'WAVES' => 1 );
        }
        return $rates;
    }

    public function create_order(string $symbol, string $type, string $side, float $amount, ?float $price = null, $params = array ()) {
        /**
         * create a trade order
         *
         * @see https://matcher.waves.exchange/api-docs/index.html#/serialize/serializeOrder
         *
         * @param {string} $symbol unified $symbol of the $market to create an order in
         * @param {string} $type 'market' or 'limit'
         * @param {string} $side 'buy' or 'sell'
         * @param {float} $amount how much of currency you want to trade in units of $base currency
         * @param {float} [$price] the $price at which the order is to be fulfilled, in units of the quote currency, ignored in $market orders
         * @param {array} [$params] extra parameters specific to the exchange API endpoint
         * @param {float} [$params->triggerPrice] The $price at which a stop order is triggered at
         * @return {array} an ~@link https://docs.ccxt.com/#/?id=order-structure order structure~
         */
        $this->check_required_dependencies();
        $this->check_required_keys();
        $this->load_markets();
        $market = $this->market($symbol);
        $matcherPublicKey = $this->get_matcher_public_key();
        $amountAsset = $this->get_asset_id($market['baseId']);
        $priceAsset = $this->get_asset_id($market['quoteId']);
        $isMarketOrder = ($type === 'market');
        $triggerPrice = $this->safe_float_2($params, 'triggerPrice', 'stopPrice');
        $isStopOrder = ($triggerPrice !== null);
        if (($isMarketOrder) && ($price === null)) {
            throw new InvalidOrder($this->id . ' createOrder() requires a $price argument for ' . $type . ' orders to determine the max $price for buy and the min $price for sell');
        }
        $timestamp = $this->milliseconds();
        $defaultExpiryDelta = null;
        list($defaultExpiryDelta, $params) = $this->handle_option_and_params($params, 'createOrder', 'defaultExpiry', $this->safe_integer($this->options, 'createOrderDefaultExpiry', 2419200000));
        $expiration = $this->sum($timestamp, $defaultExpiryDelta);
        $matcherFees = $this->get_fees_for_asset($symbol, $side, $amount, $price);
        // {
        //     "base":array(
        //        "feeAssetId":"WAVES", // varies depending on the trading pair
        //        "matcherFee":"1000000"
        //     ),
        //     "discount":{
        //        "feeAssetId":"EMAMLxDnv3xiz8RXg8Btj33jcEw3wLczL3JKYYmuubpc",
        //        "matcherFee":"4077612"
        //     }
        //  }
        $base = $this->safe_value_2($matcherFees, 'base', 'discount');
        $baseFeeAssetId = $this->safe_string($base, 'feeAssetId');
        $baseFeeAsset = $this->safe_currency_code($baseFeeAssetId);
        $baseMatcherFee = $this->safe_string($base, 'matcherFee');
        $discount = $this->safe_value($matcherFees, 'discount');
        $discountFeeAssetId = $this->safe_string($discount, 'feeAssetId');
        $discountFeeAsset = $this->safe_currency_code($discountFeeAssetId);
        $discountMatcherFee = $this->safe_string($discount, 'matcherFee');
        $matcherFeeAssetId = null;
        $matcherFee = null;
        // check first if user supplied asset fee is valid
        if ((is_array($params) && array_key_exists('feeAsset', $params)) || (is_array($this->options) && array_key_exists('feeAsset', $this->options))) {
            $feeAsset = $this->safe_string($params, 'feeAsset', $this->safe_string($this->options, 'feeAsset'));
            $feeCurrency = $this->currency($feeAsset);
            $matcherFeeAssetId = $this->safe_string($feeCurrency, 'id');
        }
        $balances = $this->fetch_balance();
        if ($matcherFeeAssetId !== null) {
            if ($baseFeeAssetId !== $matcherFeeAssetId && $discountFeeAssetId !== $matcherFeeAssetId) {
                throw new InvalidOrder($this->id . ' asset fee must be ' . $baseFeeAsset . ' or ' . $discountFeeAsset);
            }
            $matcherFeeAsset = $this->safe_currency_code($matcherFeeAssetId);
            $rawMatcherFee = ($matcherFeeAssetId === $baseFeeAssetId) ? $baseMatcherFee : $discountMatcherFee;
            $floatMatcherFee = floatval($this->from_real_currency_amount($matcherFeeAsset, $rawMatcherFee));
            if ((is_array($balances) && array_key_exists($matcherFeeAsset, $balances)) && ($balances[$matcherFeeAsset]['free'] >= $floatMatcherFee)) {
                $matcherFee = intval($rawMatcherFee);
            } else {
                throw new InsufficientFunds($this->id . ' not enough funds of the selected asset fee');
            }
        }
        $floatBaseMatcherFee = $this->from_real_currency_amount($baseFeeAsset, $baseMatcherFee);
        $floatDiscountMatcherFee = $this->from_real_currency_amount($discountFeeAsset, $discountMatcherFee);
        if ($matcherFeeAssetId === null) {
            // try to the pay the fee using the $base first then $discount asset
            if ((is_array($balances) && array_key_exists($baseFeeAsset, $balances)) && ($balances[$baseFeeAsset]['free'] >= floatval($floatBaseMatcherFee))) {
                $matcherFeeAssetId = $baseFeeAssetId;
                $matcherFee = intval($baseMatcherFee);
            } else {
                if ((is_array($balances) && array_key_exists($discountFeeAsset, $balances)) && ($balances[$discountFeeAsset]['free'] >= floatval($floatDiscountMatcherFee))) {
                    $matcherFeeAssetId = $discountFeeAssetId;
                    $matcherFee = intval($discountMatcherFee);
                }
            }
        }
        if ($matcherFeeAssetId === null) {
            throw new InsufficientFunds($this->id . ' not enough funds on none of the eligible asset fees => ' . $baseFeeAsset . ' ' . $floatBaseMatcherFee . ' or ' . $discountFeeAsset . ' ' . $floatDiscountMatcherFee);
        }
        $amount = $this->to_real_symbol_amount($symbol, $amount);
        $price = $this->to_real_symbol_price($symbol, $price);
        $assetPair = array(
            'amountAsset' => $amountAsset,
            'priceAsset' => $priceAsset,
        );
        $sandboxMode = $this->safe_bool($this->options, 'sandboxMode', false);
        $chainId = ($sandboxMode) ? 84 : 87;
        $body = array(
            'senderPublicKey' => $this->apiKey,
            'matcherPublicKey' => $matcherPublicKey,
            'assetPair' => $assetPair,
            'orderType' => $side,
            'price' => $price,
            'amount' => $amount,
            'timestamp' => $timestamp,
            'expiration' => $expiration,
            'matcherFee' => intval($matcherFee),
            'priceMode' => 'assetDecimals',
            'version' => 4,
            'chainId' => $chainId,
        );
        if ($isStopOrder) {
            //
            // {
            //     "v" => 1, // version (int)
            //     "c" => array( // condition (object)
            //         "t" => "sp", // condition $type-> for now only "stop-$price" (string)
            //         "v" => array( // $value (object)
            //             "p" => "123", // $price (long)
            //         ),
            //     ),
            // }
            //
            $attachment = array(
                'v' => 1,
                'c' => array(
                    't' => 'sp',
                    'v' => array(
                        'p' => $this->to_real_symbol_price($symbol, $triggerPrice),
                    ),
                ),
            );
            $body['attachment'] = $this->binary_to_base58($this->encode(json_encode ($attachment)));
        }
        if ($matcherFeeAssetId !== 'WAVES') {
            $body['matcherFeeAssetId'] = $matcherFeeAssetId;
        }
        $serializedOrder = $this->matcherPostMatcherOrdersSerialize ($body);
        if (($serializedOrder[0] === '"') && ($serializedOrder[(strlen($serializedOrder) - 1)] === '"')) {
            $serializedOrder = mb_substr($serializedOrder, 1, strlen($serializedOrder) - 1 - 1);
        }
        $signature = $this->axolotl(bin2hex($this->base58_to_binary($serializedOrder)), bin2hex($this->base58_to_binary($this->secret)), 'ed25519');
        $body['signature'] = $signature;
        //
        //     {
        //         "success" => true,
        //         "message" => array(
        //           "version" => 4,
        //           "id" => "8VR49dLZFaYcVwzx9TqVMTAZCSUoyB74kLUHrEPCSJgN",
        //           "sender" => "3MpEdBXtsRHRj2TvZURSb8uLDxzneVbYczW",
        //           "senderPublicKey" => "8aUTNqHGCBiubySBRhcS1N6NC5jLczhVcndRfMAuwtkY",
        //           "matcherPublicKey" => "8QUAqtTckM5B8gvcuP7mMswat9SjKUuafJMusEoSn1Gy",
        //           "assetPair" => array(
        //             "amountAsset" => "EMAMLxDnv3xiz8RXg8Btj33jcEw3wLczL3JKYYmuubpc",
        //             "priceAsset" => "25FEqEjRkqK6yCkiT7Lz6SAYz7gUFCtxfCChnrVFD5AT"
        //           ),
        //           "orderType" => "sell",
        //           "amount" => 100000,
        //           "price" => 480000,
        //           "timestamp" => 1690852043772,
        //           "expiration" => 1693271243772,
        //           "matcherFee" => 83327570,
        //           "signature" => "3QYDWQVSP4kdqpTLodCuboh8bpWd6GW5s1pQyKdce1JBDwX6t4kH5Xtuq35pqo94gxjo3cfG6k6Xuic2JaYLubkK",
        //           "proofs" => array(
        //             "3QYDWQVSP4kdqpTLodCuboh8bpWd6GW5s1pQyKdce1JBDwX6t4kH5Xtuq35pqo94gxjo3cfG6k6Xuic2JaYLubkK"
        //           ),
        //           "matcherFeeAssetId" => "EMAMLxDnv3xiz8RXg8Btj33jcEw3wLczL3JKYYmuubpc",
        //           "eip712Signature" => null,
        //           "priceMode" => "assetDecimals",
        //           "attachment" => "2PQ4akZHnMSZrQissuu5uudoXbgsipeDnFcRtXtjVgkdm1gUWEgGzp"
        //         ),
        //         "status" => "OrderAccepted"
        //     }
        //
        if ($isMarketOrder) {
            $response = $this->matcherPostMatcherOrderbookMarket ($this->extend($body, $params));
            $value = $this->safe_dict($response, 'message');
            return $this->parse_order($value, $market);
        } else {
            $response = $this->matcherPostMatcherOrderbook ($this->extend($body, $params));
            $value = $this->safe_dict($response, 'message');
            return $this->parse_order($value, $market);
        }
    }

    public function cancel_order(string $id, ?string $symbol = null, $params = array ()) {
        /**
         * cancels an open order
         *
         * @see https://matcher.waves.exchange/api-docs/index.html#/cancel/cancelOrdersByIdsWithKeyOrSignature
         *
         * @param {string} $id order $id
         * @param {string} $symbol unified $symbol of the market the order was made in
         * @param {array} [$params] extra parameters specific to the exchange API endpoint
         * @return {array} An ~@link https://docs.ccxt.com/#/?$id=order-structure order structure~
         */
        $this->check_required_dependencies();
        $this->check_required_keys();
        $this->sign_in();
        $wavesAddress = $this->get_waves_address();
        $response = $this->forwardPostMatcherOrdersWavesAddressCancel (array(
            'wavesAddress' => $wavesAddress,
            'orderId' => $id,
        ));
        //  {
        //    "success":true,
        //    "message":[[array("orderId":"EBpJeGM36KKFz5gTJAUKDBm89V8wqxKipSFBdU35AN3c","success":true,"status":"OrderCanceled")]],
        //    "status":"BatchCancelCompleted"
        //  }
        $message = $this->safe_value($response, 'message');
        $firstMessage = $this->safe_value($message, 0);
        $firstOrder = $this->safe_value($firstMessage, 0);
        $returnedId = $this->safe_string($firstOrder, 'orderId');
        return $this->safe_order(array(
            'info' => $response,
            'id' => $returnedId,
            'clientOrderId' => null,
            'timestamp' => null,
            'datetime' => null,
            'lastTradeTimestamp' => null,
            'symbol' => $symbol,
            'type' => null,
            'side' => null,
            'price' => null,
            'amount' => null,
            'cost' => null,
            'average' => null,
            'filled' => null,
            'remaining' => null,
            'status' => null,
            'fee' => null,
            'trades' => null,
        ));
    }

    public function fetch_order(string $id, ?string $symbol = null, $params = array ()) {
        /**
         * fetches information on an order made by the user
         *
         * @see https://matcher.waves.exchange/api-docs/index.html#/status/getOrderStatusByPKAndIdWithSig
         *
         * @param {string} $id order $id
         * @param {string} $symbol unified $symbol of the $market the order was made in
         * @param {array} [$params] extra parameters specific to the exchange API endpoint
         * @return {array} An ~@link https://docs.ccxt.com/#/?$id=order-structure order structure~
         */
        $this->check_required_dependencies();
        $this->check_required_keys();
        $this->load_markets();
        $market = null;
        if ($symbol !== null) {
            $market = $this->market($symbol);
        }
        $timestamp = $this->milliseconds();
        $byteArray = array(
            $this->base58_to_binary($this->apiKey),
            $this->number_to_be($timestamp, 8),
        );
        $binary = $this->binary_concat_array($byteArray);
        $hexSecret = bin2hex($this->base58_to_binary($this->secret));
        $signature = $this->axolotl(bin2hex($binary), $hexSecret, 'ed25519');
        $request = array(
            'Timestamp' => (string) $timestamp,
            'Signature' => $signature,
            'publicKey' => $this->apiKey,
            'orderId' => $id,
        );
        $response = $this->matcherGetMatcherOrderbookPublicKeyOrderId ($this->extend($request, $params));
        return $this->parse_order($response, $market);
    }

    public function fetch_orders(?string $symbol = null, ?int $since = null, ?int $limit = null, $params = array ()): array {
        /**
         * fetches information on multiple orders made by the user
         * @param {string} $symbol unified $market $symbol of the $market orders were made in
         * @param {int} [$since] the earliest time in ms to fetch orders for
         * @param {int} [$limit] the maximum number of order structures to retrieve
         * @param {array} [$params] extra parameters specific to the exchange API endpoint
         * @return {Order[]} a list of ~@link https://docs.ccxt.com/#/?id=order-structure order structures~
         */
        $this->check_required_dependencies();
        $this->check_required_keys();
        if ($symbol === null) {
            throw new ArgumentsRequired($this->id . ' fetchOrders() requires a $symbol argument');
        }
        $this->load_markets();
        $market = $this->market($symbol);
        $timestamp = $this->milliseconds();
        $byteArray = array(
            $this->base58_to_binary($this->apiKey),
            $this->number_to_be($timestamp, 8),
        );
        $binary = $this->binary_concat_array($byteArray);
        $hexSecret = bin2hex($this->base58_to_binary($this->secret));
        $signature = $this->axolotl(bin2hex($binary), $hexSecret, 'ed25519');
        $request = array(
            'Accept' => 'application/json',
            'Timestamp' => (string) $timestamp,
            'Signature' => $signature,
            'publicKey' => $this->apiKey,
            'baseId' => $market['baseId'],
            'quoteId' => $market['quoteId'],
        );
        $response = $this->matcherGetMatcherOrderbookBaseIdQuoteIdPublicKeyPublicKey ($this->extend($request, $params));
        // array( array( id => "3KicDeWayY2mdrRoYdCkP3gUAoUZUNT1AA6GAtWuPLfa",
        //     "type" => "sell",
        //     "orderType" => "limit",
        //     "amount" => 1,
        //     "fee" => 300000,
        //     "price" => 100000000,
        //     "timestamp" => 1591651254076,
        //     "filled" => 0,
        //     "filledFee" => 0,
        //     "feeAsset" => "WAVES",
        //     "status" => "Accepted",
        //     "assetPair":
        //      array( amountAsset => null,
        //        "priceAsset" => "8LQW8f7P5d5PZM7GtZEBgaqRPGSzS3DfPuiXrURJ4AJS" ),
        //     "avgWeighedPrice" => 0 ), ... )
        return $this->parse_orders($response, $market, $since, $limit);
    }

    public function fetch_open_orders(?string $symbol = null, ?int $since = null, ?int $limit = null, $params = array ()): array {
        /**
         * fetch all unfilled currently open orders
         * @param {string} $symbol unified $market $symbol
         * @param {int} [$since] the earliest time in ms to fetch open orders for
         * @param {int} [$limit] the maximum number of  open orders structures to retrieve
         * @param {array} [$params] extra parameters specific to the exchange API endpoint
         * @return {Order[]} a list of ~@link https://docs.ccxt.com/#/?id=order-structure order structures~
         */
        $this->load_markets();
        $this->sign_in();
        $market = null;
        if ($symbol !== null) {
            $market = $this->market($symbol);
        }
        $address = $this->get_waves_address();
        $request = array(
            'address' => $address,
            'activeOnly' => true,
        );
        $response = $this->forwardGetMatcherOrdersAddress ($request);
        return $this->parse_orders($response, $market, $since, $limit);
    }

    public function fetch_closed_orders(?string $symbol = null, ?int $since = null, ?int $limit = null, $params = array ()): array {
        /**
         * fetches information on multiple closed orders made by the user
         * @param {string} $symbol unified $market $symbol of the $market orders were made in
         * @param {int} [$since] the earliest time in ms to fetch orders for
         * @param {int} [$limit] the maximum number of order structures to retrieve
         * @param {array} [$params] extra parameters specific to the exchange API endpoint
         * @return {Order[]} a list of ~@link https://docs.ccxt.com/#/?id=order-structure order structures~
         */
        $this->load_markets();
        $this->sign_in();
        $market = null;
        if ($symbol !== null) {
            $market = $this->market($symbol);
        }
        $address = $this->get_waves_address();
        $request = array(
            'address' => $address,
            'closedOnly' => true,
        );
        $response = $this->forwardGetMatcherOrdersAddress ($request);
        // array(
        //   array(
        //     "id" => "9aXcxvXai73jbAm7tQNnqaQ2PwUjdmWuyjvRTKAHsw4f",
        //     "type" => "buy",
        //     "orderType" => "limit",
        //     "amount" => 23738330,
        //     "fee" => 300000,
        //     "price" => 3828348334,
        //     "timestamp" => 1591926905636,
        //     "filled" => 23738330,
        //     "filledFee" => 300000,
        //     "feeAsset" => "WAVES",
        //     "status" => "Filled",
        //     "assetPair" => array(
        //       "amountAsset" => "HZk1mbfuJpmxU1Fs4AX5MWLVYtctsNcg6e2C6VKqK8zk",
        //       "priceAsset" => null
        //     ),
        //     "avgWeighedPrice" => 3828348334
        //   ), ...
        // )
        return $this->parse_orders($response, $market, $since, $limit);
    }

    public function parse_order_status(?string $status) {
        $statuses = array(
            'Cancelled' => 'canceled',
            'Accepted' => 'open',
            'Filled' => 'closed',
            'PartiallyFilled' => 'open',
        );
        return $this->safe_string($statuses, $status, $status);
    }

    public function get_symbol_from_asset_pair($assetPair) {
        // a blank string or null can indicate WAVES
        $baseId = $this->safe_string($assetPair, 'amountAsset', 'WAVES');
        $quoteId = $this->safe_string($assetPair, 'priceAsset', 'WAVES');
        return $this->safe_currency_code($baseId) . '/' . $this->safe_currency_code($quoteId);
    }

    public function parse_order(array $order, ?array $market = null): array {
        //
        // createOrder
        //
        //     {
        //         "version" => 4,
        //         "id" => "BshyeHXDfJmTnjTdBYt371jD4yWaT3JTP6KpjpsiZepS",
        //         "sender" => "3P8VzLSa23EW5CVckHbV7d5BoN75fF1hhFH",
        //         "senderPublicKey" => "AHXn8nBA4SfLQF7hLQiSn16kxyehjizBGW1TdrmSZ1gF",
        //         "matcherPublicKey" => "9cpfKN9suPNvfeUNphzxXMjcnn974eme8ZhWUjaktzU5",
        //         "assetPair" => array(
        //             "amountAsset" => "474jTeYx2r2Va35794tCScAXWJG9hU2HcgxzMowaZUnu",
        //             "priceAsset" => "DG2xFkPdDwKUoBkzGAhQtLpSGzfXLiCYPEzeKH2Ad24p",
        //         ),
        //         "orderType" => "buy",
        //         "amount" => 10000,
        //         "price" => 400000000,
        //         "timestamp" => 1599848586891,
        //         "expiration" => 1602267786891,
        //         "matcherFee" => 3008,
        //         "matcherFeeAssetId" => "474jTeYx2r2Va35794tCScAXWJG9hU2HcgxzMowaZUnu",
        //         "signature" => "3D2h8ubrhuWkXbVn4qJ3dvjmZQxLoRNfjTqb9uNpnLxUuwm4fGW2qGH6yKFe2SQPrcbgkS3bDVe7SNtMuatEJ7qy",
        //         "proofs" => array(
        //             "3D2h8ubrhuWkXbVn4qJ3dvjmZQxLoRNfjTqb9uNpnLxUuwm4fGW2qGH6yKFe2SQPrcbgkS3bDVe7SNtMuatEJ7qy",
        //         ),
        //         "attachment":"77rnoyFX5BDr15hqZiUtgXKSN46zsbHHQjVNrTMLZcLz62mmFKr39FJ"
        //     }
        //
        //
        // fetchOrder, fetchOrders, fetchOpenOrders, fetchClosedOrders
        //
        //     {
        //         "id" => "81D9uKk2NfmZzfG7uaJsDtxqWFbJXZmjYvrL88h15fk8",
        //         "type" => "buy",
        //         "orderType" => "limit",
        //         "amount" => 30000000000,
        //         "filled" => 0,
        //         "price" => 1000000,
        //         "fee" => 300000,
        //         "filledFee" => 0,
        //         "feeAsset" => "WAVES",
        //         "timestamp" => 1594303779322,
        //         "status" => "Cancelled",
        //         "assetPair" => array(
        //             "amountAsset" => "474jTeYx2r2Va35794tCScAXWJG9hU2HcgxzMowaZUnu",
        //             "priceAsset" => "WAVES"
        //         ),
        //         "avgWeighedPrice" => 0,
        //         "version" => 4,
        //         "totalExecutedPriceAssets" => 0,  // in fetchOpenOrder/s
        //         "attachment":"77rnoyFX5BDr15hqZiUtgXKSN46zsbHHQjVNrTMLZcLz62mmFKr39FJ"
        //     }
        //
        $timestamp = $this->safe_integer($order, 'timestamp');
        $side = $this->safe_string_2($order, 'type', 'orderType');
        $type = 'limit';
        if (is_array($order) && array_key_exists('type', $order)) {
            // fetchOrders
            $type = $this->safe_string($order, 'orderType', $type);
        }
        $id = $this->safe_string($order, 'id');
        $filledString = $this->safe_string($order, 'filled');
        $priceString = $this->safe_string($order, 'price');
        $amountString = $this->safe_string($order, 'amount');
        $assetPair = $this->safe_value($order, 'assetPair');
        $symbol = null;
        if ($assetPair !== null) {
            $symbol = $this->get_symbol_from_asset_pair($assetPair);
        } elseif ($market !== null) {
            $symbol = $market['symbol'];
        }
        $amountCurrency = $this->safe_currency_code($this->safe_string($assetPair, 'amountAsset', 'WAVES'));
        $price = $this->from_real_symbol_price($symbol, $priceString);
        $amount = $this->from_real_currency_amount($amountCurrency, $amountString);
        $filled = $this->from_real_currency_amount($amountCurrency, $filledString);
        $average = $this->from_real_symbol_price($symbol, $this->safe_string($order, 'avgWeighedPrice'));
        $status = $this->parse_order_status($this->safe_string($order, 'status'));
        $fee = null;
        if (is_array($order) && array_key_exists('type', $order)) {
            $code = $this->safe_currency_code($this->safe_string($order, 'feeAsset'));
            $fee = array(
                'currency' => $code,
                'fee' => $this->parse_number($this->from_real_currency_amount($code, $this->safe_string($order, 'filledFee'))),
            );
        } else {
            $code = $this->safe_currency_code($this->safe_string($order, 'matcherFeeAssetId', 'WAVES'));
            $fee = array(
                'currency' => $code,
                'fee' => $this->parse_number($this->from_real_currency_amount($code, $this->safe_string($order, 'matcherFee'))),
            );
        }
        $triggerPrice = null;
        $attachment = $this->safe_string($order, 'attachment');
        if ($attachment !== null) {
            $decodedAttachment = $this->parse_json($this->decode($this->base58_to_binary($attachment)));
            if ($decodedAttachment !== null) {
                $c = $this->safe_value($decodedAttachment, 'c');
                if ($c !== null) {
                    $v = $this->safe_value($c, 'v');
                    if ($v !== null) {
                        $triggerPrice = $this->safe_string($v, 'p');
                    }
                }
            }
        }
        return $this->safe_order(array(
            'info' => $order,
            'id' => $id,
            'clientOrderId' => null,
            'timestamp' => $timestamp,
            'datetime' => $this->iso8601($timestamp),
            'lastTradeTimestamp' => null,
            'symbol' => $symbol,
            'type' => $type,
            'timeInForce' => null,
            'postOnly' => null,
            'side' => $side,
            'price' => $price,
            'triggerPrice' => $triggerPrice,
            'amount' => $amount,
            'cost' => null,
            'average' => $average,
            'filled' => $filled,
            'remaining' => null,
            'status' => $status,
            'fee' => $fee,
            'trades' => null,
        ), $market);
    }

    public function get_waves_address() {
        $cachedAddreess = $this->safe_string($this->options, 'wavesAddress');
        if ($cachedAddreess === null) {
            $request = array(
                'publicKey' => $this->apiKey,
            );
            $response = $this->nodeGetAddressesPublicKeyPublicKey ($request);
            $this->options['wavesAddress'] = $this->safe_string($response, 'address');
            return $this->options['wavesAddress'];
        } else {
            return $cachedAddreess;
        }
    }

    public function fetch_balance($params = array ()): array {
        /**
         * query for $balance and get the $amount of funds available for trading or funds locked in orders
         * @param {array} [$params] extra parameters specific to the exchange API endpoint
         * @return {array} a ~@link https://docs.ccxt.com/#/?id=$balance-structure $balance structure~
         */
        // 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
        $this->check_required_dependencies();
        $this->check_required_keys();
        $this->load_markets();
        $wavesAddress = $this->get_waves_address();
        $request = array(
            'address' => $wavesAddress,
        );
        $totalBalance = $this->nodeGetAssetsBalanceAddress ($request);
        // {
        //   "address" => "3P8VzLSa23EW5CVckHbV7d5BoN75fF1hhFH",
        //   "balances" => array(
        //     {
        //       "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" => array(
        //           "3HNpbVkgP69NWSeb9hGYauiQDaXrRXh3tXFzNsGwsAAXnFrA29SYGbLtziW9JLpXEq7qW1uytv5Fnm5XTUMB2BxU"
        //         ),
        //         "assetId" => "DG2xFkPdDwKUoBkzGAhQtLpSGzfXLiCYPEzeKH2Ad24p",
        //         "decimals" => 6,
        //         "name" => "USD-N",
        //         "id" => "DG2xFkPdDwKUoBkzGAhQtLpSGzfXLiCYPEzeKH2Ad24p",
        //         "timestamp" => 1574429393962
        //       }
        //     }
        //   )
        // }
        $balances = $this->safe_value($totalBalance, 'balances', array());
        $result = array();
        $timestamp = null;
        $assetIds = array();
        $nonStandardBalances = array();
        for ($i = 0; $i < count($balances); $i++) {
            $entry = $balances[$i];
            $entryTimestamp = $this->safe_integer($entry, 'timestamp');
            $timestamp = ($timestamp === null) ? $entryTimestamp : max ($timestamp, $entryTimestamp);
            $issueTransaction = $this->safe_value($entry, 'issueTransaction');
            $currencyId = $this->safe_string($entry, 'assetId');
            $balance = $this->safe_string($entry, 'balance');
            $currencyExists = (is_array($this->currencies_by_id) && array_key_exists($currencyId, $this->currencies_by_id));
            if ($currencyExists) {
                $code = $this->safe_currency_code($currencyId);
                $result[$code] = $this->account();
                $result[$code]['total'] = $this->from_real_currency_amount($code, $balance);
            } elseif ($issueTransaction === null) {
                $assetIds[] = $currencyId;
                $nonStandardBalances[] = $balance;
            }
        }
        $nonStandardAssets = count($assetIds);
        if ($nonStandardAssets) {
            $requestInner = array(
                'ids' => $assetIds,
            );
            $response = $this->publicGetAssets ($requestInner);
            $data = $this->safe_value($response, 'data', array());
            for ($i = 0; $i < count($data); $i++) {
                $entry = $data[$i];
                $balance = $nonStandardBalances[$i];
                $inner = $this->safe_value($entry, 'data');
                $precision = $this->parse_precision($this->safe_string($inner, 'precision'));
                $ticker = $this->safe_string($inner, 'ticker');
                $code = $this->safe_currency_code($ticker);
                $result[$code] = $this->account();
                $result[$code]['total'] = Precise::string_mul($balance, $precision);
            }
        }
        $currentTimestamp = $this->milliseconds();
        $byteArray = array(
            $this->base58_to_binary($this->apiKey),
            $this->number_to_be($currentTimestamp, 8),
        );
        $binary = $this->binary_concat_array($byteArray);
        $hexSecret = bin2hex($this->base58_to_binary($this->secret));
        $signature = $this->axolotl(bin2hex($binary), $hexSecret, 'ed25519');
        $matcherRequest = array(
            'publicKey' => $this->apiKey,
            'signature' => $signature,
            'timestamp' => (string) $currentTimestamp,
        );
        $reservedBalance = $this->matcherGetMatcherBalanceReservedPublicKey ($matcherRequest);
        // array( WAVES => 200300000 )
        $reservedKeys = is_array($reservedBalance) ? array_keys($reservedBalance) : array();
        for ($i = 0; $i < count($reservedKeys); $i++) {
            $currencyId = $reservedKeys[$i];
            $code = $this->safe_currency_code($currencyId);
            if (!(is_array($result) && array_key_exists($code, $result))) {
                $result[$code] = $this->account();
            }
            $amount = $this->safe_string($reservedBalance, $currencyId);
            $result[$code]['used'] = $this->from_real_currency_amount($code, $amount);
        }
        $wavesRequest = array(
            'address' => $wavesAddress,
        );
        $wavesTotal = $this->nodeGetAddressesBalanceAddress ($wavesRequest);
        // {
        //   "address" => "3P8VzLSa23EW5CVckHbV7d5BoN75fF1hhFH",
        //   "confirmations" => 0,
        //   "balance" => 909085978
        // }
        $result['WAVES'] = $this->safe_value($result, 'WAVES', $this->account());
        $result['WAVES']['total'] = $this->from_real_currency_amount('WAVES', $this->safe_string($wavesTotal, 'balance'));
        $result = $this->set_undefined_balances_to_zero($result);
        $result['timestamp'] = $timestamp;
        $result['datetime'] = $this->iso8601($timestamp);
        return $this->safe_balance($result);
    }

    public function set_undefined_balances_to_zero($balances, $key = 'used') {
        $codes = is_array($balances) ? array_keys($balances) : array();
        for ($i = 0; $i < count($codes); $i++) {
            $code = $codes[$i];
            if ($this->safe_value($balances[$code], 'used') === null) {
                $balances[$code][$key] = '0';
            }
        }
        return $balances;
    }

    public function fetch_my_trades(?string $symbol = null, ?int $since = null, ?int $limit = null, $params = array ()) {
        /**
         * fetch all trades made by the user
         *
         * @see https://api.wavesplatform.com/v0/docs/#/transactions/searchTxsExchange
         *
         * @param {string} $symbol unified $market $symbol
         * @param {int} [$since] the earliest time in ms to fetch trades for
         * @param {int} [$limit] the maximum number of trades structures to retrieve
         * @param {array} [$params] extra parameters specific to the exchange API endpoint
         * @return {Trade[]} a list of ~@link https://docs.ccxt.com/#/?id=trade-structure trade structures~
         */
        $this->load_markets();
        $address = $this->get_waves_address();
        $request = array(
            'sender' => $address,
        );
        $market = null;
        if ($symbol !== null) {
            $market = $this->market($symbol);
            $request['amountAsset'] = $market['baseId'];
            $request['priceAsset'] = $market['quoteId'];
        }
        $response = $this->publicGetTransactionsExchange ($request);
        $data = $this->safe_value($response, 'data');
        //
        //      {
        //          "__type":"list",
        //          "isLastPage":true,
        //          "lastCursor":"MzA2MjQ0MzAwMDI5OjpkZXNj",
        //          "data" => array(
        //              {
        //                  "__type":"transaction",
        //                  "data" => {
        //                      "id":"GbjPqco2wRP5QSrY5LimFrUyJaM535K9nhK5zaQ7J7Tx",
        //                      "timestamp":"2022-04-06T19:56:31.479Z",
        //                      "height":3062443,
        //                      "type":7,
        //                      "version":2,
        //                      "proofs":array(
        //                          "57mYrANw61eiArCTv2eYwzXm71jYC2KpZ5AeM9zHEstuRaYSAWSuSE7njAJYJu8zap6DMCm3nzqc6es3wQFDpRCN"
        //                      ),
        //                      "fee":0.003,
        //                      "applicationStatus":"succeeded",
        //                      "sender":"3PEjHv3JGjcWNpYEEkif2w8NXV4kbhnoGgu",
        //                      "senderPublicKey":"9cpfKN9suPNvfeUNphzxXMjcnn974eme8ZhWUjaktzU5",
        //                      "buyMatcherFee":0,
        //                      "sellMatcherFee":0.00141728,
        //                      "price":215.7431,
        //                      "amount":0.09,
        //                      "order1" => array(
        //                          "id":"49qiuQj5frdZ6zpTCEpMuKPMAh1EimwXpXWB4BeCw33h",
        //                          "senderPublicKey":"CjUfoH3dsDZsf5UuAjqqzpWHXgvKzBZpVG9YixF7L48K",
        //                          "matcherPublicKey":"9cpfKN9suPNvfeUNphzxXMjcnn974eme8ZhWUjaktzU5",
        //                          "assetPair" => array(
        //                              "amountAsset":"7TMu26hAs7B2oW6c5sfx45KSZT7GQA3TZNYuCav8Dcqt",
        //                              "priceAsset":"DG2xFkPdDwKUoBkzGAhQtLpSGzfXLiCYPEzeKH2Ad24p"
        //                          ),
        //                          "orderType":"buy",
        //                          "price":215.7431,
        //                          "sender":"3PR9WmaHV5ueVw2Wr9xsiCG3t4ySXzkkGLy",
        //                          "amount":0.36265477,
        //                          "timestamp":"2022-04-06T19:55:06.832Z",
        //                          "expiration":"2022-05-05T19:55:06.832Z",
        //                          "matcherFee":3.000334,
        //                          "signature":"2rBWhdeuRJNpQfXfTFtcR8x8Lpic8FUHPdLML9uxABRUuxe48YRJcZxbncwWAh9LWFCEUZiztv7RZBZfGMWfFxTs",
        //                          "matcherFeeAssetId":"DG2xFkPdDwKUoBkzGAhQtLpSGzfXLiCYPEzeKH2Ad24p"
        //                      ),
        //                      "order2" => array(
        //                          "id":"AkxiJqCuv6wm8K41TUSgFNwShZMnCbMDT78MqrcWpQ53",
        //                          "senderPublicKey":"72o7qNKyne5hthB1Ww6famE7uHrk5vTVB2ZfUMBEqL3Y",
        //                          "matcherPublicKey":"9cpfKN9suPNvfeUNphzxXMjcnn974eme8ZhWUjaktzU5",
        //                          "assetPair" => array(
        //                              "amountAsset":"7TMu26hAs7B2oW6c5sfx45KSZT7GQA3TZNYuCav8Dcqt",
        //                              "priceAsset":"DG2xFkPdDwKUoBkzGAhQtLpSGzfXLiCYPEzeKH2Ad24p"
        //                          ),
        //                          "orderType":"sell",
        //                          "price":210,
        //                          "sender":"3P3CzbjGgiqEyUBeKZYfgZtyaZfMG8fjoUD",
        //                          "amount":0.09,
        //                          "timestamp":"2022-04-06T19:56:18.535Z",
        //                          "expiration":"2022-05-04T19:56:18.535Z",
        //                          "matcherFee":0.00141728,
        //                          "signature":"5BZCjYn6QzVkMXBFDBnzcAUBdCZqhq9hQfRXFHfLUQCsbis4zeriw4sUqLa1BZRT2isC6iY4Z4HtekikPqZ461PT",
        //                          "matcherFeeAssetId":"7TMu26hAs7B2oW6c5sfx45KSZT7GQA3TZNYuCav8Dcqt"
        //                      }
        //                  }
        //              ),...
        //          )
        //      }
        //
        return $this->parse_trades($data, $market, $since, $limit);
    }

    public function fetch_trades(string $symbol, ?int $since = null, ?int $limit = null, $params = array ()): array {
        /**
         * get the list of most recent trades for a particular $symbol
         *
         * @see https://api.wavesplatform.com/v0/docs/#/transactions/searchTxsExchange
         *
         * @param {string} $symbol unified $symbol of the $market to fetch trades for
         * @param {int} [$since] timestamp in ms of the earliest trade to fetch
         * @param {int} [$limit] the maximum amount of trades to fetch
         * @param {array} [$params] extra parameters specific to the exchange API endpoint
         * @return {Trade[]} a list of ~@link https://docs.ccxt.com/#/?id=public-trades trade structures~
         */
        $this->load_markets();
        $market = $this->market($symbol);
        $request = array(
            'amountAsset' => $market['baseId'],
            'priceAsset' => $market['quoteId'],
        );
        if ($limit !== null) {
            $request['limit'] = min ($limit, 100);
        }
        if ($since !== null) {
            $request['timeStart'] = $since;
        }
        $response = $this->publicGetTransactionsExchange ($request);
        $data = $this->safe_value($response, 'data');
        //
        //      {
        //          "__type":"list",
        //          "isLastPage":false,
        //          "lastCursor":"MzA2MjM2MTAwMDU0OjpkZXNj",
        //          "data" => array(
        //              {
        //                  "__type":"transaction",
        //                  "data" => {
        //                      "id":"F42WsvSsyEzvpPLFjVhQKkSNuopooP4zMkjSUs47NeML",
        //                      "timestamp":"2022-04-06T18:39:49.145Z",
        //                      "height":3062361,
        //                      "type":7,
        //                      "version":2,
        //                      "proofs" => array(
        //                          "39iJv82kFi4pyuBxYeZpP45NXXjbrCXdVsHPAAvj32UMLmTXLjMTfV43PcmZDSAuS93HKSDo1aKJrin8UvkeE9Bs"
        //                      ),
        //                      "fee":0.003,
        //                      "applicationStatus":"succeeded",
        //                      "sender":"3PEjHv3JGjcWNpYEEkif2w8NXV4kbhnoGgu",
        //                      "senderPublicKey":"9cpfKN9suPNvfeUNphzxXMjcnn974eme8ZhWUjaktzU5",
        //                      "buyMatcherFee":0.02314421,
        //                      "sellMatcherFee":0,
        //                      "price":217.3893,
        //                      "amount":0.34523025,
        //                      "order1" => array(
        //                          "id":"HkM36PHGaeeZdDKT1mYgZXhaU9PRZ54RZiJc2K4YMT3Q",
        //                          "senderPublicKey":"7wYCaDcc6GX1Jx2uS7QgLHBypBKvrezTS1HfiW6Xe4Bk",
        //                          "matcherPublicKey":"9cpfKN9suPNvfeUNphzxXMjcnn974eme8ZhWUjaktzU5",
        //                          "assetPair" => array(
        //                              "amountAsset":"7TMu26hAs7B2oW6c5sfx45KSZT7GQA3TZNYuCav8Dcqt",
        //                              "priceAsset":"DG2xFkPdDwKUoBkzGAhQtLpSGzfXLiCYPEzeKH2Ad24p"
        //                          ),
        //                          "orderType":"buy",
        //                          "price":225.2693,
        //                          "sender":"3PLPc8f4DGYaF9C9bwJ2uVmHqRv3NCjg5VQ",
        //                          "amount":2.529,
        //                          "timestamp":"2022-04-06T18:39:48.796Z",
        //                          "expiration":"2022-05-05T18:39:48.796Z",
        //                          "matcherFee":0.17584444,
        //                          "signature":"2yQfJoomv86evQDw36fg1uiRkHvPDZtRp3qvxqTBWPvz4JLTHGQtEHJF5NGTvym6U93CtgNprngzmD9ecHBjxf6U",
        //                          "matcherFeeAssetId":"Atqv59EYzjFGuitKVnMRk6H8FukjoV3ktPorbEys25on"
        //                      ),
        //                      "order2" => array(
        //                          "id":"F7HKmeuzwWdk3wKitHLnVx5MuD4wBWPpphQ8kUGx4tT9",
        //                          "senderPublicKey":"CjUfoH3dsDZsf5UuAjqqzpWHXgvKzBZpVG9YixF7L48K",
        //                          "matcherPublicKey":"9cpfKN9suPNvfeUNphzxXMjcnn974eme8ZhWUjaktzU5",
        //                          "assetPair" => array(
        //                              "amountAsset":"7TMu26hAs7B2oW6c5sfx45KSZT7GQA3TZNYuCav8Dcqt",
        //                              "priceAsset":"DG2xFkPdDwKUoBkzGAhQtLpSGzfXLiCYPEzeKH2Ad24p"
        //                          ),
        //                          "orderType":"sell",
        //                          "price":217.3893,
        //                          "sender":"3PR9WmaHV5ueVw2Wr9xsiCG3t4ySXzkkGLy",
        //                          "amount":0.35767793,
        //                          "timestamp":"2022-04-06T18:32:01.390Z",
        //                          "expiration":"2022-05-05T18:32:01.390Z",
        //                          "matcherFee":0.0139168,
        //                          "signature":"34HgWVLPgeYWkiSvAc5ChVepGTYDQDug2dMTSincs6idEyoM7AtaZuH3mqQ5RJG2fcxxH2QSB723Qq3dgLQwQmKf",
        //                          "matcherFeeAssetId":"7TMu26hAs7B2oW6c5sfx45KSZT7GQA3TZNYuCav8Dcqt"
        //                      }
        //                  }
        //              ), ...
        //          )
        //      }
        //
        return $this->parse_trades($data, $market, $since, $limit);
    }

    public function parse_trade(array $trade, ?array $market = null): array {
        //
        // { __type => "transaction",
        //   "data":
        //    { $id => "HSdruioHqvYHeyn9hhyoHdRWPB2bFA8ujeCPZMK6992c",
        //      "timestamp" => "2020-06-09T19:34:51.897Z",
        //      "height" => 2099684,
        //      "type" => 7,
        //      "version" => 2,
        //      "proofs":
        //       array( "26teDHERQgwjjHqEn4REcDotNG8M21xjou3X42XuDuCvrRkQo6aPyrswByH3UrkWG8v27ZAaVNzoxDg4teNcLtde" ),
        //      "fee" => 0.003,
        //      "sender" => "3PEjHv3JGjcWNpYEEkif2w8NXV4kbhnoGgu",
        //      "senderPublicKey" => "9cpfKN9suPNvfeUNphzxXMjcnn974eme8ZhWUjaktzU5",
        //      "buyMatcherFee" => 0.00299999,
        //      "sellMatcherFee" => 0.00299999,
        //      "price" => 0.00012003,
        //      "amount" => 60.80421562,
        //      "order1":
        //       array( $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 = $this->safe_value($trade, 'data');
        $datetime = $this->safe_string($data, 'timestamp');
        $timestamp = $this->parse8601($datetime);
        $id = $this->safe_string($data, 'id');
        $priceString = $this->safe_string($data, 'price');
        $amountString = $this->safe_string($data, 'amount');
        $order1 = $this->safe_value($data, 'order1');
        $order2 = $this->safe_value($data, 'order2');
        $order = null;
        // at first, detect if response is from `fetch_my_trades`
        if ($this->safe_string($order1, 'senderPublicKey') === $this->apiKey) {
            $order = $order1;
        } elseif ($this->safe_string($order2, 'senderPublicKey') === $this->apiKey) {
            $order = $order2;
        } else {
            // response is from `fetch_trades`, so find only taker $order
            $date1 = $this->safe_string($order1, 'timestamp');
            $date2 = $this->safe_string($order2, 'timestamp');
            $ts1 = $this->parse8601($date1);
            $ts2 = $this->parse8601($date2);
            if ($ts1 > $ts2) {
                $order = $order1;
            } else {
                $order = $order2;
            }
        }
        $symbol = null;
        $assetPair = $this->safe_value($order, 'assetPair');
        if ($assetPair !== null) {
            $symbol = $this->get_symbol_from_asset_pair($assetPair);
        } elseif ($market !== null) {
            $symbol = $market['symbol'];
        }
        $side = $this->safe_string($order, 'orderType');
        $orderId = $this->safe_string($order, 'id');
        $fee = array(
            'cost' => $this->safe_string($order, 'matcherFee'),
            'currency' => $this->safe_currency_code($this->safe_string($order, 'matcherFeeAssetId', 'WAVES')),
        );
        return $this->safe_trade(array(
            'info' => $trade,
            'timestamp' => $timestamp,
            'datetime' => $datetime,
            'symbol' => $symbol,
            'id' => $id,
            'order' => $orderId,
            'type' => null,
            'side' => $side,
            'takerOrMaker' => null,
            'price' => $priceString,
            'amount' => $amountString,
            'cost' => null,
            'fee' => $fee,
        ), $market);
    }

    public function parse_deposit_withdraw_fees($response, ?array $codes = null, $currencyIdKey = null): mixed {
        $depositWithdrawFees = array();
        $codes = $this->market_codes($codes);
        for ($i = 0; $i < count($response); $i++) {
            $entry = $response[$i];
            $dictionary = $entry;
            $currencyId = $this->safe_string($dictionary, $currencyIdKey);
            $currency = $this->safe_value($this->currencies_by_id, $currencyId);
            $code = $this->safe_string($currency, 'code', $currencyId);
            if (($codes === null) || ($this->in_array($code, $codes))) {
                $depositWithdrawFee = $this->safe_value($depositWithdrawFees, $code);
                if ($depositWithdrawFee === null) {
                    $depositWithdrawFee = array(
                        'info' => array( $dictionary ),
                        'withdraw' => array(
                            'fee' => null,
                            'percentage' => null,
                        ),
                        'deposit' => array(
                            'fee' => null,
                            'percentage' => null,
                        ),
                        'networks' => array(),
                    );
                } else {
                    $depositWithdrawFee = $depositWithdrawFees[$code];
                    $depositWithdrawFee['info'] = $this->array_concat($depositWithdrawFee['info'], array( $dictionary ));
                }
                $networkId = $this->safe_string($dictionary, 'platform_id');
                $currencyCode = $this->safe_string($currency, 'code');
                $networkCode = $this->network_id_to_code($networkId, $currencyCode);
                $network = $this->safe_value($depositWithdrawFee['networks'], $networkCode);
                if ($network === null) {
                    $network = array(
                        'withdraw' => array(
                            'fee' => null,
                            'percentage' => null,
                        ),
                        'deposit' => array(
                            'fee' => null,
                            'percentage' => null,
                        ),
                    );
                }
                $feeType = $this->safe_string($dictionary, 'type');
                $fees = $this->safe_value($dictionary, 'fees');
                $networkKey = 'deposit';
                if ($feeType === 'withdrawal_currency') {
                    $networkKey = 'withdraw';
                }
                $network[$networkKey] = array( 'fee' => $this->safe_number($fees, 'flat'), 'percentage' => false );
                $depositWithdrawFee['networks'][$networkCode] = $network;
                $depositWithdrawFees[$code] = $depositWithdrawFee;
            }
        }
        $depositWithdrawFeesKeys = is_array($depositWithdrawFees) ? array_keys($depositWithdrawFees) : array();
        for ($i = 0; $i < count($depositWithdrawFeesKeys); $i++) {
            $code = $depositWithdrawFeesKeys[$i];
            $entry = $depositWithdrawFees[$code];
            $networks = $this->safe_value($entry, 'networks');
            $networkKeys = is_array($networks) ? array_keys($networks) : array();
            $networkKeysLength = count($networkKeys);
            if ($networkKeysLength === 1) {
                $network = $this->safe_value($networks, $networkKeys[0]);
                $depositWithdrawFees[$code]['withdraw'] = $this->safe_value($network, 'withdraw');
                $depositWithdrawFees[$code]['deposit'] = $this->safe_value($network, 'deposit');
            }
        }
        return $depositWithdrawFees;
    }

    public function fetch_deposit_withdraw_fees(?array $codes = null, $params = array ()) {
        /**
         * fetch deposit and withdraw fees
         *
         * @see https://docs.wx.network/en/api/gateways/deposit/currencies
         * @see https://docs.wx.network/en/api/gateways/withdraw/currencies
         *
         * @param {string[]|null} $codes list of unified currency $codes
         * @param {array} [$params] extra parameters specific to the exchange API endpoint
         * @return {array} a list of ~@link https://docs.ccxt.com/#/?id=fee-structure fee structures~
         */
        $this->load_markets();
        $data = array();
        $promises = array();
        $promises[] = $this->privateGetDepositCurrencies ($params);
        $promises[] = $this->privateGetWithdrawCurrencies ($params);
        $promises = $promises;
        //
        //    {
        //        "type" => "list",
        //        "page_info" => array(
        //          "has_next_page" => false,
        //          "last_cursor" => null
        //        ),
        //        "items" => array(
        //          {
        //            "type" => "deposit_currency",
        //            "id" => "WEST",
        //            "platform_id" => "WEST",
        //            "waves_asset_id" => "4LHHvYGNKJUg5hj65aGD5vgScvCBmLpdRFtjokvCjSL8",
        //            "platform_asset_id" => "WEST",
        //            "decimals" => 8,
        //            "status" => "active",
        //            "allowed_amount" => array(
        //              "min" => 0.1,
        //              "max" => 2000000
        //            ),
        //            "fees" => array(
        //              "flat" => 0,
        //              "rate" => 0
        //            }
        //          ),
        //        )
        //    }
        //
        //
        //    {
        //        "type" => "list",
        //        "page_info" => array(
        //          "has_next_page" => false,
        //          "last_cursor" => null
        //        ),
        //        "items" => array(
        //          {
        //            "type" => "withdrawal_currency",
        //            "id" => "BTC",
        //            "platform_id" => "BTC",
        //            "waves_asset_id" => "8LQW8f7P5d5PZM7GtZEBgaqRPGSzS3DfPuiXrURJ4AJS",
        //            "platform_asset_id" => "BTC",
        //            "decimals" => 8,
        //            "status" => "inactive",
        //            "allowed_amount" => array(
        //              "min" => 0.001,
        //              "max" => 10
        //            ),
        //            "fees" => array(
        //              "flat" => 0.001,
        //              "rate" => 0
        //            }
        //          ),
        //        )
        //    }
        //
        for ($i = 0; $i < count($promises); $i++) {
            $items = $this->safe_value($promises[$i], 'items');
            $data = $this->array_concat($data, $items);
        }
        return $this->parse_deposit_withdraw_fees($data, $codes, 'id');
    }

    public function handle_errors(int $code, string $reason, string $url, string $method, array $headers, string $body, $response, $requestHeaders, $requestBody) {
        $errorCode = $this->safe_string($response, 'error');
        $success = $this->safe_bool($response, 'success', true);
        $Exception = $this->safe_value($this->exceptions, $errorCode);
        if ($Exception !== null) {
            $messageInner = $this->safe_string($response, 'message');
            throw new $Exception($this->id . ' ' . $messageInner);
        }
        $message = $this->safe_string($response, 'message');
        if ($message === 'Validation Error') {
            throw new BadRequest($this->id . ' ' . $body);
        }
        if (!$success) {
            throw new ExchangeError($this->id . ' ' . $body);
        }
        return null;
    }

    public function withdraw(string $code, float $amount, string $address, ?string $tag = null, $params = array ()): array {
        /**
         * make a withdrawal
         * @param {string} $code unified $currency $code
         * @param {float} $amount the $amount to withdraw
         * @param {string} $address the $address to withdraw to
         * @param {string} $tag
         * @param {array} [$params] extra parameters specific to the exchange API endpoint
         * @return {array} a ~@link https://docs.ccxt.com/#/?id=transaction-structure transaction structure~
         */
        list($tag, $params) = $this->handle_withdraw_tag_and_params($tag, $params);
        // currently only works for BTC and WAVES
        if ($code !== 'WAVES') {
            $supportedCurrencies = $this->privateGetWithdrawCurrencies ();
            $currencies = array();
            $items = $this->safe_value($supportedCurrencies, 'items', array());
            for ($i = 0; $i < count($items); $i++) {
                $entry = $items[$i];
                $currencyCode = $this->safe_string($entry, 'id');
                $currencies[$currencyCode] = true;
            }
            if (!(is_array($currencies) && array_key_exists($code, $currencies))) {
                $codes = is_array($currencies) ? array_keys($currencies) : array();
                throw new ExchangeError($this->id . ' withdraw() ' . $code . ' not supported. Currency $code must be one of ' . (string) $codes);
            }
        }
        $this->load_markets();
        $hexChars = array( '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' );
        $set = array();
        for ($i = 0; $i < count($hexChars); $i++) {
            $key = $hexChars[$i];
            $set[$key] = true;
        }
        $isErc20 = true;
        $noPrefix = $this->remove0x_prefix($address);
        $lower = strtolower($noPrefix);
        $stringLength = strlen($lower) * 1;
        for ($i = 0; $i < $stringLength; $i++) {
            $character = $lower[$i];
            if (!(is_array($set) && array_key_exists($character, $set))) {
                $isErc20 = false;
                break;
            }
        }
        $this->sign_in();
        $proxyAddress = null;
        if ($code === 'WAVES' && !$isErc20) {
            $proxyAddress = $address;
        } else {
            $withdrawAddressRequest = array(
                'address' => $address,
                'currency' => $code,
            );
            $withdrawAddress = $this->privateGetWithdrawAddressesCurrencyAddress ($withdrawAddressRequest);
            $currencyInner = $this->safe_value($withdrawAddress, 'currency');
            $allowedAmount = $this->safe_value($currencyInner, 'allowed_amount');
            $minimum = $this->safe_number($allowedAmount, 'min');
            if ($amount <= $minimum) {
                throw new BadRequest($this->id . ' ' . $code . ' withdraw failed, $amount ' . (string) $amount . ' must be greater than the $minimum allowed $amount of ' . (string) $minimum);
            }
            // {
            //   "type" => "withdrawal_addresses",
            //   "currency" => {
            //     "type" => "withdrawal_currency",
            //     "id" => "BTC",
            //     "waves_asset_id" => "8LQW8f7P5d5PZM7GtZEBgaqRPGSzS3DfPuiXrURJ4AJS",
            //     "decimals" => 8,
            //     "status" => "active",
            //     "allowed_amount" => array(
            //       "min" => 0.001,
            //       "max" => 20
            //     ),
            //     "fees" => array(
            //       "flat" => 0.001,
            //       "rate" => 0
            //     }
            //   ),
            //   "proxy_addresses" => array(
            //     "3P3qqmkiLwNHB7x1FeoE8bvkRtULwGpo9ga"
            //   )
            // }
            $proxyAddresses = $this->safe_value($withdrawAddress, 'proxy_addresses', array());
            $proxyAddress = $this->safe_string($proxyAddresses, 0);
        }
        $fee = $this->safe_integer($this->options, 'withdrawFeeWAVES', 100000);  // 0.001 WAVES
        $feeAssetId = 'WAVES';
        $type = 4;  // transfer
        $version = 2;
        $amountInteger = $this->to_real_currency_amount($code, $amount);
        $currency = $this->currency($code);
        $timestamp = $this->milliseconds();
        $byteArray = [
            $this->number_to_be(4, 1),
            $this->number_to_be(2, 1),
            $this->base58_to_binary($this->apiKey),
            $this->get_asset_bytes($currency['id']),
            $this->get_asset_bytes($feeAssetId),
            $this->number_to_be($timestamp, 8),
            $this->number_to_be($amountInteger, 8),
            $this->number_to_be($fee, 8),
            $this->base58_to_binary($proxyAddress),
            $this->number_to_be(0, 2),
        ];
        $binary = $this->binary_concat_array($byteArray);
        $hexSecret = bin2hex($this->base58_to_binary($this->secret));
        $signature = $this->axolotl(bin2hex($binary), $hexSecret, 'ed25519');
        $request = array(
            'senderPublicKey' => $this->apiKey,
            'amount' => $amountInteger,
            'fee' => $fee,
            'type' => $type,
            'version' => $version,
            'attachment' => '',
            'feeAssetId' => $this->get_asset_id($feeAssetId),
            'proofs' => array(
                $signature,
            ),
            'assetId' => $this->get_asset_id($currency['id']),
            'recipient' => $proxyAddress,
            'timestamp' => $timestamp,
            'signature' => $signature,
        );
        $result = $this->nodePostTransactionsBroadcast ($request);
        //
        //     {
        //         "id" => "string",
        //         "signature" => "string",
        //         "fee" => 0,
        //         "timestamp" => 1460678400000,
        //         "recipient" => "3P274YB5qseSE9DTTL3bpSjosZrYBPDpJ8k",
        //         "amount" => 0
        //     }
        //
        return $this->parse_transaction($result, $currency);
    }

    public function parse_transaction(array $transaction, ?array $currency = null): array {
        //
        // withdraw
        //
        //     {
        //         "id" => "string",
        //         "signature" => "string",
        //         "fee" => 0,
        //         "timestamp" => 1460678400000,
        //         "recipient" => "3P274YB5qseSE9DTTL3bpSjosZrYBPDpJ8k",
        //         "amount" => 0
        //     }
        //
        // withdraw new:
        //     {
        //         $type => "4",
        //         id => "2xnWTqG9ar7jEDrLxfbVyyspPZ6XZNrrw9ai9sQ81Eya",
        //         fee => "100000",
        //         $feeAssetId => null,
        //         $timestamp => "1715786263807",
        //         version => "2",
        //         sender => "3P81LLX1kk2CSJC9L8C2enxdHB7XvnSGAEE",
        //         senderPublicKey => "DdmzmXf9mty1FBE8AdVGnrncVLEAzP4gR4nWoTFAJoXz",
        //         proofs => array( "RyoKwdSYv3EqotJCYftfFM9JE2j1ZpDRxKwYfiRhLAFeyNp6VfJUXNDS884XfeCeHeNypNmTCZt5NYR1ekyjCX3", ),
        //         recipient => "3P9tXxu38a8tgewNEKFzourVxeqHd11ppOc",
        //         assetId => null,
        //         feeAsset => null,
        //         $amount => "2000000",
        //         attachment => "",
        //     }
        //
        $currency = $this->safe_currency(null, $currency);
        $code = $currency['code'];
        $typeRaw = $this->safe_string($transaction, 'type');
        $type = ($typeRaw === '4') ? 'withdraw' : 'deposit';
        $amount = $this->parse_number($this->from_real_currency_amount($code, $this->safe_string($transaction, 'amount')));
        $feeString = $this->safe_string($transaction, 'fee');
        $feeAssetId = $this->safe_string($transaction, 'feeAssetId', 'WAVES');
        $feeCode = $this->safe_currency_code($feeAssetId);
        $feeAmount = $this->parse_number($this->from_real_currency_amount($feeCode, $feeString));
        $timestamp = $this->safe_integer($transaction, 'timestamp');
        return array(
            'id' => $this->safe_string($transaction, 'id'),
            'txid' => null,
            'timestamp' => $timestamp,
            'datetime' => $this->iso8601($timestamp),
            'network' => null,
            'addressFrom' => $this->safe_string($transaction, 'sender'),
            'address' => null,
            'addressTo' => $this->safe_string($transaction, 'recipient'),
            'amount' => $amount,
            'type' => $type,
            'currency' => $currency['code'],
            'status' => null,
            'updated' => null,
            'tagFrom' => null,
            'tag' => null,
            'tagTo' => null,
            'comment' => null,
            'internal' => null,
            'fee' => array(
                'currency' => $feeCode,
                'cost' => $feeAmount,
            ),
            'info' => $transaction,
        );
    }
}
