<?php

namespace ccxt\pro;

// 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\ExchangeError;
use ccxt\AuthenticationError;
use ccxt\ArgumentsRequired;
use \React\Async;
use \React\Promise\PromiseInterface;

class bitvavo extends \ccxt\async\bitvavo {

    public function describe(): mixed {
        return $this->deep_extend(parent::describe(), array(
            'has' => array(
                'ws' => true,
                'cancelOrdersWs' => false,
                'fetchTradesWs' => false,
                'watchOrderBook' => true,
                'watchTrades' => true,
                'watchTicker' => true,
                'watchTickers' => true,
                'watchBidsAsks' => true,
                'watchOHLCV' => true,
                'watchOrders' => true,
                'watchMyTrades' => true,
                'cancelAllOrdersWs' => true,
                'cancelOrderWs' => true,
                'createOrderWs' => true,
                'createStopLimitOrderWs' => true,
                'createStopMarketOrderWs' => true,
                'createStopOrderWs' => true,
                'editOrderWs' => true,
                'fetchBalanceWs' => true,
                'fetchCurrenciesWS' => true,
                'fetchDepositAddressWs' => false,
                'fetchDepositsWs' => true,
                'fetchDepositWithdrawFeesWs' => false,
                'fetchMyTradesWs' => true,
                'fetchOHLCVWs' => true,
                'fetchOpenOrdersWs' => true,
                'fetchOrderWs' => true,
                'fetchOrderBookWs' => false,
                'fetchOrdersWs' => true,
                'fetchTickerWs' => false,
                'fetchTickersWs' => false,
                'fetchTimeWs' => false,
                'fetchTradingFeesWs' => true,
                'fetchWithdrawalsWs' => true,
                'withdrawWs' => true,
            ),
            'urls' => array(
                'api' => array(
                    'ws' => 'wss://ws.bitvavo.com/v2',
                ),
            ),
            'options' => array(
                'supressMultipleWsRequestsError' => false, // if true, will not throw an error when using the same messageHash for more than one request. By making false you may receive responses from different requests on the same action
                'tradesLimit' => 1000,
                'ordersLimit' => 1000,
                'OHLCVLimit' => 1000,
            ),
        ));
    }

    public function watch_public($name, $symbol, $params = array ()) {
        return Async\async(function () use ($name, $symbol, $params) {
            Async\await($this->load_markets());
            $market = $this->market($symbol);
            $messageHash = $name . '@' . $market['id'];
            $url = $this->urls['api']['ws'];
            $request = array(
                'action' => 'subscribe',
                'channels' => [
                    array(
                        'name' => $name,
                        'markets' => [
                            $market['id'],
                        ],
                    ),
                ],
            );
            $message = $this->extend($request, $params);
            return Async\await($this->watch($url, $messageHash, $message, $messageHash));
        }) ();
    }

    public function watch_public_multiple($methodName, string $channelName, $symbols, $params = array ()) {
        return Async\async(function () use ($methodName, $channelName, $symbols, $params) {
            Async\await($this->load_markets());
            $symbols = $this->market_symbols($symbols);
            $messageHashes = array( $methodName );
            $args = array();
            for ($i = 0; $i < count($symbols); $i++) {
                $market = $this->market($symbols[$i]);
                $args[] = $market['id'];
            }
            $url = $this->urls['api']['ws'];
            $request = array(
                'action' => 'subscribe',
                'channels' => array(
                    array(
                        'name' => $channelName,
                        'markets' => $args,
                    ),
                ),
            );
            $message = $this->extend($request, $params);
            return Async\await($this->watch_multiple($url, $messageHashes, $message, $messageHashes));
        }) ();
    }

    public function watch_ticker(string $symbol, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbol, $params) {
            /**
             * watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market
             *
             * @see https://docs.bitvavo.com/#tag/Market-data-subscription-WebSocket/paths/~1subscribeTicker24h/post
             *
             * @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~
             */
            return Async\await($this->watch_public('ticker24h', $symbol, $params));
        }) ();
    }

    public function watch_tickers(?array $symbols = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbols, $params) {
            /**
             * watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for all markets of a specific list
             *
             * @see https://docs.bitvavo.com/#tag/Market-data-subscription-WebSocket/paths/~1subscribeTicker24h/post
             *
             * @param {string[]} [$symbols] 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~
             */
            Async\await($this->load_markets());
            $symbols = $this->market_symbols($symbols, null, false);
            $channel = 'ticker24h';
            $tickers = Async\await($this->watch_public_multiple($channel, $channel, $symbols, $params));
            return $this->filter_by_array($tickers, 'symbol', $symbols);
        }) ();
    }

    public function handle_ticker(Client $client, $message) {
        //
        //     {
        //         "event" => "ticker24h",
        //         "data" => array(
        //             {
        //                 "market" => "ETH-EUR",
        //                 "open" => "193.5",
        //                 "high" => "202.72",
        //                 "low" => "192.46",
        //                 "last" => "199.01",
        //                 "volume" => "3587.05020246",
        //                 "volumeQuote" => "708030.17",
        //                 "bid" => "199.56",
        //                 "bidSize" => "4.14730802",
        //                 "ask" => "199.57",
        //                 "askSize" => "6.13642074",
        //                 "timestamp" => 1590770885217
        //             }
        //         )
        //     }
        //
        $this->handle_bid_ask($client, $message);
        $event = $this->safe_string($message, 'event');
        $tickers = $this->safe_value($message, 'data', array());
        $result = array();
        for ($i = 0; $i < count($tickers); $i++) {
            $data = $tickers[$i];
            $marketId = $this->safe_string($data, 'market');
            $market = $this->safe_market($marketId, null, '-');
            $messageHash = $event . '@' . $marketId;
            $ticker = $this->parse_ticker($data, $market);
            $symbol = $ticker['symbol'];
            $this->tickers[$symbol] = $ticker;
            $result[] = $ticker;
            $client->resolve ($ticker, $messageHash);
        }
        $client->resolve ($result, $event);
    }

    public function watch_bids_asks(?array $symbols = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbols, $params) {
            /**
             * watches best bid & ask for $symbols
             *
             * @see https://docs.bitvavo.com/#tag/Market-data-subscription-WebSocket/paths/~1subscribeTicker24h/post
             *
             * @param {string[]} $symbols 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~
             */
            Async\await($this->load_markets());
            $symbols = $this->market_symbols($symbols, null, false);
            $channel = 'ticker24h';
            $tickers = Async\await($this->watch_public_multiple('bidask', $channel, $symbols, $params));
            return $this->filter_by_array($tickers, 'symbol', $symbols);
        }) ();
    }

    public function handle_bid_ask(Client $client, $message) {
        $event = 'bidask';
        $tickers = $this->safe_value($message, 'data', array());
        $result = array();
        for ($i = 0; $i < count($tickers); $i++) {
            $data = $tickers[$i];
            $ticker = $this->parse_ws_bid_ask($data);
            $symbol = $ticker['symbol'];
            $this->bidsasks[$symbol] = $ticker;
            $result[] = $ticker;
            $messageHash = $event . ':' . $symbol;
            $client->resolve ($ticker, $messageHash);
        }
        $client->resolve ($result, $event);
    }

    public function parse_ws_bid_ask($ticker, $market = null) {
        $marketId = $this->safe_string($ticker, 'market');
        $market = $this->safe_market($marketId, null, '-');
        $symbol = $this->safe_string($market, 'symbol');
        $timestamp = $this->safe_integer($ticker, 'timestamp');
        return $this->safe_ticker(array(
            'symbol' => $symbol,
            'timestamp' => $timestamp,
            'datetime' => $this->iso8601($timestamp),
            'ask' => $this->safe_number($ticker, 'ask'),
            'askVolume' => $this->safe_number($ticker, 'askSize'),
            'bid' => $this->safe_number($ticker, 'bid'),
            'bidVolume' => $this->safe_number($ticker, 'bidSize'),
            'info' => $ticker,
        ), $market);
    }

    public function watch_trades(string $symbol, ?int $since = null, ?int $limit = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbol, $since, $limit, $params) {
            /**
             * get the list of most recent $trades for a particular $symbol
             * @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 {array[]} a list of ~@link https://docs.ccxt.com/#/?id=public-$trades trade structures~
             */
            Async\await($this->load_markets());
            $symbol = $this->symbol($symbol);
            $trades = Async\await($this->watch_public('trades', $symbol, $params));
            if ($this->newUpdates) {
                $limit = $trades->getLimit ($symbol, $limit);
            }
            return $this->filter_by_since_limit($trades, $since, $limit, 'timestamp', true);
        }) ();
    }

    public function handle_trade(Client $client, $message) {
        //
        //     {
        //         "event" => "trade",
        //         "timestamp" => 1590779594547,
        //         "market" => "ETH-EUR",
        //         "id" => "450c3298-f082-4461-9e2c-a0262cc7cc2e",
        //         "amount" => "0.05026233",
        //         "price" => "198.46",
        //         "side" => "buy"
        //     }
        //
        $marketId = $this->safe_string($message, 'market');
        $market = $this->safe_market($marketId, null, '-');
        $symbol = $market['symbol'];
        $name = 'trades';
        $messageHash = $name . '@' . $marketId;
        $trade = $this->parse_trade($message, $market);
        $tradesArray = $this->safe_value($this->trades, $symbol);
        if ($tradesArray === null) {
            $limit = $this->safe_integer($this->options, 'tradesLimit', 1000);
            $tradesArray = new ArrayCache ($limit);
        }
        $tradesArray->append ($trade);
        $this->trades[$symbol] = $tradesArray;
        $client->resolve ($tradesArray, $messageHash);
    }

    public function watch_ohlcv(string $symbol, $timeframe = '1m', ?int $since = null, ?int $limit = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbol, $timeframe, $since, $limit, $params) {
            /**
             * watches historical candlestick data containing the open, high, low, and close price, and the volume of a $market
             * @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
             * @return {int[][]} A list of candles ordered, open, high, low, close, volume
             */
            Async\await($this->load_markets());
            $market = $this->market($symbol);
            $symbol = $market['symbol'];
            $name = 'candles';
            $marketId = $market['id'];
            $interval = $this->safe_string($this->timeframes, $timeframe, $timeframe);
            $messageHash = $name . '@' . $marketId . '_' . $interval;
            $url = $this->urls['api']['ws'];
            $request = array(
                'action' => 'subscribe',
                'channels' => array(
                    array(
                        'name' => 'candles',
                        'interval' => array( $interval ),
                        'markets' => array( $marketId ),
                    ),
                ),
            );
            $message = $this->extend($request, $params);
            $ohlcv = Async\await($this->watch($url, $messageHash, $message, $messageHash));
            if ($this->newUpdates) {
                $limit = $ohlcv->getLimit ($symbol, $limit);
            }
            return $this->filter_by_since_limit($ohlcv, $since, $limit, 0, true);
        }) ();
    }

    public function handle_fetch_ohlcv(Client $client, $message) {
        //
        //    {
        //        action => 'getCandles',
        //        $response => [
        //            [1690325820000, '26453', '26453', '26436', '26447', '0.01626246'],
        //            [1690325760000, '26454', '26454', '26453', '26453', '0.00037707']
        //        ]
        //    }
        //
        $response = $this->safe_value($message, 'response');
        $ohlcv = $this->parse_ohlcvs($response, null, null, null);
        $messageHash = $this->safe_string($message, 'requestId');
        $client->resolve ($ohlcv, $messageHash);
    }

    public function handle_ohlcv(Client $client, $message) {
        //
        //     {
        //         "event" => "candle",
        //         "market" => "BTC-EUR",
        //         "interval" => "1m",
        //         "candle" => array(
        //             array(
        //                 1590797160000,
        //                 "8480.9",
        //                 "8480.9",
        //                 "8480.9",
        //                 "8480.9",
        //                 "0.01038628"
        //             )
        //         )
        //     }
        //
        $name = 'candles';
        $marketId = $this->safe_string($message, 'market');
        $market = $this->safe_market($marketId, null, '-');
        $symbol = $market['symbol'];
        $interval = $this->safe_string($message, 'interval');
        // use a reverse lookup in a static map instead
        $timeframe = $this->find_timeframe($interval);
        $messageHash = $name . '@' . $marketId . '_' . $interval;
        $candles = $this->safe_value($message, 'candle');
        $this->ohlcvs[$symbol] = $this->safe_value($this->ohlcvs, $symbol, array());
        $stored = $this->safe_value($this->ohlcvs[$symbol], $timeframe);
        if ($stored === null) {
            $limit = $this->safe_integer($this->options, 'OHLCVLimit', 1000);
            $stored = new ArrayCacheByTimestamp ($limit);
            $this->ohlcvs[$symbol][$timeframe] = $stored;
        }
        for ($i = 0; $i < count($candles); $i++) {
            $candle = $candles[$i];
            $parsed = $this->parse_ohlcv($candle, $market);
            $stored->append ($parsed);
        }
        $client->resolve ($stored, $messageHash);
    }

    public function watch_order_book(string $symbol, ?int $limit = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbol, $limit, $params) {
            /**
             * watches information on open orders with bid (buy) and ask (sell) prices, volumes and other data
             * @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
             */
            Async\await($this->load_markets());
            $market = $this->market($symbol);
            $symbol = $market['symbol'];
            $name = 'book';
            $messageHash = $name . '@' . $market['id'];
            $url = $this->urls['api']['ws'];
            $request = array(
                'action' => 'subscribe',
                'channels' => [
                    array(
                        'name' => $name,
                        'markets' => [
                            $market['id'],
                        ],
                    ),
                ],
            );
            $subscription = array(
                'messageHash' => $messageHash,
                'name' => $name,
                'symbol' => $symbol,
                'marketId' => $market['id'],
                'method' => array($this, 'handle_order_book_subscription'),
                'limit' => $limit,
                'params' => $params,
            );
            $message = $this->extend($request, $params);
            $orderbook = Async\await($this->watch($url, $messageHash, $message, $messageHash, $subscription));
            return $orderbook->limit ();
        }) ();
    }

    public function handle_delta($bookside, $delta) {
        $price = $this->safe_float($delta, 0);
        $amount = $this->safe_float($delta, 1);
        $bookside->store ($price, $amount);
    }

    public function handle_deltas($bookside, $deltas) {
        for ($i = 0; $i < count($deltas); $i++) {
            $this->handle_delta($bookside, $deltas[$i]);
        }
    }

    public function handle_order_book_message(Client $client, $message, $orderbook) {
        //
        //     {
        //         "event" => "book",
        //         "market" => "BTC-EUR",
        //         "nonce" => 36947383,
        //         "bids" => array(
        //             array( "8477.8", "0" )
        //         ),
        //         "asks" => array(
        //             array( "8550.9", "0" )
        //         )
        //     }
        //
        $nonce = $this->safe_integer($message, 'nonce');
        if ($nonce > $orderbook['nonce']) {
            $this->handle_deltas($orderbook['asks'], $this->safe_value($message, 'asks', array()));
            $this->handle_deltas($orderbook['bids'], $this->safe_value($message, 'bids', array()));
            $orderbook['nonce'] = $nonce;
        }
        return $orderbook;
    }

    public function handle_order_book(Client $client, $message) {
        //
        //     {
        //         "event" => "book",
        //         "market" => "BTC-EUR",
        //         "nonce" => 36729561,
        //         "bids" => array(
        //             array( "8513.3", "0" ),
        //             array( '8518.8', "0.64236203" ),
        //             array( '8513.6', "0.32435481" ),
        //         ),
        //         "asks" => array()
        //     }
        //
        $event = $this->safe_string($message, 'event');
        $marketId = $this->safe_string($message, 'market');
        $market = $this->safe_market($marketId, null, '-');
        $symbol = $market['symbol'];
        $messageHash = $event . '@' . $market['id'];
        $orderbook = $this->safe_value($this->orderbooks, $symbol);
        if ($orderbook === null) {
            return;
        }
        if ($orderbook['nonce'] === null) {
            $subscription = $this->safe_value($client->subscriptions, $messageHash, array());
            $watchingOrderBookSnapshot = $this->safe_value($subscription, 'watchingOrderBookSnapshot');
            if ($watchingOrderBookSnapshot === null) {
                $subscription['watchingOrderBookSnapshot'] = true;
                $client->subscriptions[$messageHash] = $subscription;
                $options = $this->safe_value($this->options, 'watchOrderBookSnapshot', array());
                $delay = $this->safe_integer($options, 'delay', $this->rateLimit);
                // fetch the snapshot in a separate async call after a warmup $delay
                $this->delay($delay, array($this, 'watch_order_book_snapshot'), $client, $message, $subscription);
            }
            $orderbook->cache[] = $message;
        } else {
            $this->handle_order_book_message($client, $message, $orderbook);
            $client->resolve ($orderbook, $messageHash);
        }
    }

    public function watch_order_book_snapshot($client, $message, $subscription) {
        return Async\async(function () use ($client, $message, $subscription) {
            $params = $this->safe_value($subscription, 'params');
            $marketId = $this->safe_string($subscription, 'marketId');
            $name = 'getBook';
            $messageHash = $name . '@' . $marketId;
            $url = $this->urls['api']['ws'];
            $request = array(
                'action' => $name,
                'market' => $marketId,
            );
            $orderbook = Async\await($this->watch($url, $messageHash, $this->extend($request, $params), $messageHash, $subscription));
            return $orderbook->limit ();
        }) ();
    }

    public function handle_order_book_snapshot(Client $client, $message) {
        //
        //     {
        //         "action" => "getBook",
        //         "response" => {
        //             "market" => "BTC-EUR",
        //             "nonce" => 36946120,
        //             "bids" => array(
        //                 array( '8494.9', "0.24399521" ),
        //                 array( '8494.8', "0.34884085" ),
        //                 array( '8493.9', "0.14535128" ),
        //             ),
        //             "asks" => array(
        //                 array( "8495", "0.46982463" ),
        //                 array( '8495.1', "0.12178267" ),
        //                 array( '8496.2', "0.21924143" ),
        //             )
        //         }
        //     }
        //
        $response = $this->safe_value($message, 'response');
        if ($response === null) {
            return;
        }
        $marketId = $this->safe_string($response, 'market');
        $symbol = $this->safe_symbol($marketId, null, '-');
        $name = 'book';
        $messageHash = $name . '@' . $marketId;
        $orderbook = $this->orderbooks[$symbol];
        $snapshot = $this->parse_order_book($response, $symbol);
        $snapshot['nonce'] = $this->safe_integer($response, 'nonce');
        $orderbook->reset ($snapshot);
        // unroll the accumulated deltas
        $messages = $orderbook->cache;
        for ($i = 0; $i < count($messages); $i++) {
            $messageItem = $messages[$i];
            $this->handle_order_book_message($client, $messageItem, $orderbook);
        }
        $this->orderbooks[$symbol] = $orderbook;
        $client->resolve ($orderbook, $messageHash);
    }

    public function handle_order_book_subscription(Client $client, $message, $subscription) {
        $symbol = $this->safe_string($subscription, 'symbol');
        $limit = $this->safe_integer($subscription, 'limit');
        if (is_array($this->orderbooks) && array_key_exists($symbol, $this->orderbooks)) {
            unset($this->orderbooks[$symbol]);
        }
        $this->orderbooks[$symbol] = $this->order_book(array(), $limit);
    }

    public function handle_order_book_subscriptions(Client $client, $message, $marketIds) {
        $name = 'book';
        for ($i = 0; $i < count($marketIds); $i++) {
            $marketId = $this->safe_string($marketIds, $i);
            $symbol = $this->safe_symbol($marketId, null, '-');
            $messageHash = $name . '@' . $marketId;
            if (!(is_array($this->orderbooks) && array_key_exists($symbol, $this->orderbooks))) {
                $subscription = $this->safe_value($client->subscriptions, $messageHash);
                $method = $this->safe_value($subscription, 'method');
                if ($method !== null) {
                    $method($client, $message, $subscription);
                }
            }
        }
    }

    public function watch_orders(?string $symbol = null, ?int $since = null, ?int $limit = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbol, $since, $limit, $params) {
            /**
             * watches 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 {array[]} a list of ~@link https://docs.ccxt.com/#/?id=order-structure order structures~
             */
            if ($symbol === null) {
                throw new ArgumentsRequired($this->id . ' watchOrders() requires a $symbol argument');
            }
            Async\await($this->load_markets());
            Async\await($this->authenticate());
            $market = $this->market($symbol);
            $symbol = $market['symbol'];
            $marketId = $market['id'];
            $url = $this->urls['api']['ws'];
            $name = 'account';
            $messageHash = 'order:' . $symbol;
            $request = array(
                'action' => 'subscribe',
                'channels' => array(
                    array(
                        'name' => $name,
                        'markets' => array( $marketId ),
                    ),
                ),
            );
            $orders = Async\await($this->watch($url, $messageHash, $request, $messageHash));
            if ($this->newUpdates) {
                $limit = $orders->getLimit ($symbol, $limit);
            }
            return $this->filter_by_symbol_since_limit($orders, $symbol, $since, $limit, true);
        }) ();
    }

    public function watch_my_trades(?string $symbol = null, ?int $since = null, ?int $limit = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbol, $since, $limit, $params) {
            /**
             * watches information on multiple $trades made by the user
             * @param {string} $symbol unified $market $symbol of the $market $trades were made in
             * @param {int} [$since] the earliest time in ms to fetch $trades for
             * @param {int} [$limit] the maximum number of trade structures to retrieve
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @return {array[]} a list of ~@link https://docs.ccxt.com/#/?id=trade-structure trade structures~
             */
            if ($symbol === null) {
                throw new ArgumentsRequired($this->id . ' watchMyTrades() requires a $symbol argument');
            }
            Async\await($this->load_markets());
            Async\await($this->authenticate());
            $market = $this->market($symbol);
            $symbol = $market['symbol'];
            $marketId = $market['id'];
            $url = $this->urls['api']['ws'];
            $name = 'account';
            $messageHash = 'myTrades:' . $symbol;
            $request = array(
                'action' => 'subscribe',
                'channels' => array(
                    array(
                        'name' => $name,
                        'markets' => array( $marketId ),
                    ),
                ),
            );
            $trades = Async\await($this->watch($url, $messageHash, $request, $messageHash));
            if ($this->newUpdates) {
                $limit = $trades->getLimit ($symbol, $limit);
            }
            return $this->filter_by_symbol_since_limit($trades, $symbol, $since, $limit, true);
        }) ();
    }

    public function create_order_ws(string $symbol, string $type, string $side, float $amount, ?float $price = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbol, $type, $side, $amount, $price, $params) {
            /**
             * create a trade order
             *
             * @see https://docs.bitvavo.com/#tag/Orders/paths/~1order/post
             *
             * @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 bitvavo api endpoint
             * @param {string} [$params->timeInForce] "GTC", "IOC", or "PO"
             * @param {float} [$params->stopPrice] The $price at which a trigger order is triggered at
             * @param {float} [$params->triggerPrice] The $price at which a trigger order is triggered at
             * @param {bool} [$params->postOnly] If true, the order will only be posted to the order book and not executed immediately
             * @param {float} [$params->stopLossPrice] The $price at which a stop loss order is triggered at
             * @param {float} [$params->takeProfitPrice] The $price at which a take profit order is triggered at
             * @param {string} [$params->triggerType] "price"
             * @param {string} [$params->triggerReference] "lastTrade", "bestBid", "bestAsk", "midPrice" Only for stop orders => Use this to determine which parameter will trigger the order
             * @param {string} [$params->selfTradePrevention] "decrementAndCancel", "cancelOldest", "cancelNewest", "cancelBoth"
             * @param {bool} [$params->disableMarketProtection] don't cancel if the next fill $price is 10% worse than the best fill $price
             * @param {bool} [$params->responseRequired] Set this to 'false' when only an acknowledgement of success or failure is required, this is faster.
             * @return {array} an ~@link https://docs.ccxt.com/#/?id=order-structure order structure~
             */
            Async\await($this->load_markets());
            Async\await($this->authenticate());
            $request = $this->create_order_request($symbol, $type, $side, $amount, $price, $params);
            return Async\await($this->watch_request('privateCreateOrder', $request));
        }) ();
    }

    public function edit_order_ws(string $id, string $symbol, string $type, string $side, ?float $amount = null, ?float $price = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($id, $symbol, $type, $side, $amount, $price, $params) {
            /**
             * edit a trade order
             *
             * @see https://docs.bitvavo.com/#tag/Orders/paths/~1order/put
             *
             * @param {string} $id cancel order $id
             * @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 bitvavo api endpoint
             * @return {array} an ~@link https://docs.ccxt.com/#/?$id=order-structure order structure~
             */
            Async\await($this->load_markets());
            Async\await($this->authenticate());
            $request = $this->edit_order_request($id, $symbol, $type, $side, $amount, $price, $params);
            return Async\await($this->watch_request('privateUpdateOrder', $request));
        }) ();
    }

    public function cancel_order_ws(string $id, ?string $symbol = null, $params = array ()) {
        return Async\async(function () use ($id, $symbol, $params) {
            /**
             *
             * @see https://docs.bitvavo.com/#tag/Orders/paths/~1order/delete
             *
             * cancels an open order
             * @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 bitvavo api endpoint
             * @return {array} An ~@link https://docs.ccxt.com/#/?$id=order-structure order structure~
             */
            Async\await($this->load_markets());
            Async\await($this->authenticate());
            $request = $this->cancel_order_request($id, $symbol, $params);
            return Async\await($this->watch_request('privateCancelOrder', $request));
        }) ();
    }

    public function cancel_all_orders_ws(?string $symbol = null, $params = array ()) {
        return Async\async(function () use ($symbol, $params) {
            /**
             *
             * @see https://docs.bitvavo.com/#tag/Orders/paths/~1orders/delete
             *
             * cancel all open orders
             * @param {string} $symbol unified $market $symbol, only orders in the $market of this $symbol are cancelled when $symbol is not null
             * @param {array} [$params] extra parameters specific to the bitvavo api endpoint
             * @return {array[]} a list of ~@link https://docs.ccxt.com/#/?id=order-structure order structures~
             */
            Async\await($this->load_markets());
            Async\await($this->authenticate());
            $request = array();
            $market = null;
            if ($symbol !== null) {
                $market = $this->market($symbol);
                $request['market'] = $market['id'];
            }
            return Async\await($this->watch_request('privateCancelOrders', $this->extend($request, $params)));
        }) ();
    }

    public function handle_multiple_orders(Client $client, $message) {
        //
        //    {
        //        $action => 'privateCancelOrders',
        //        $response => [array(
        //            orderId => 'd71df826-1130-478a-8741-d219128675b0'
        //        )]
        //    }
        //
        // $action = $this->safe_string($message, 'action');
        $response = $this->safe_list($message, 'response');
        // $firstRawOrder = $this->safe_value($response, 0, array());
        // $marketId = $this->safe_string($firstRawOrder, 'market');
        $orders = $this->parse_orders($response);
        // $messageHash = $this->build_message_hash($action, array( 'market' => $marketId ));
        // $client->resolve ($orders, $messageHash);
        // $messageHash = $this->build_message_hash($action, $message);
        $messageHash = $this->safe_string($message, 'requestId');
        $client->resolve ($orders, $messageHash);
    }

    public function fetch_order_ws(string $id, ?string $symbol = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($id, $symbol, $params) {
            /**
             *
             * @see https://docs.bitvavo.com/#tag/General/paths/~1assets/get
             *
             * fetches information on an order made by the user
             * @param {string} $id the order $id
             * @param {string} $symbol unified $symbol of the $market the order was made in
             * @param {array} [$params] extra parameters specific to the bitvavo api endpoint
             * @return {array} An ~@link https://docs.ccxt.com/#/?$id=order-structure order structure~
             */
            if ($symbol === null) {
                throw new ArgumentsRequired($this->id . ' fetchOrder() requires a $symbol argument');
            }
            Async\await($this->load_markets());
            Async\await($this->authenticate());
            $market = $this->market($symbol);
            $request = array(
                'orderId' => $id,
                'market' => $market['id'],
            );
            return Async\await($this->watch_request('privateGetOrder', $this->extend($request, $params)));
        }) ();
    }

    public function fetch_orders_ws(?string $symbol = null, ?int $since = null, ?int $limit = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbol, $since, $limit, $params) {
            /**
             *
             * @see https://docs.bitvavo.com/#tag/Orders/paths/~1orders/get
             *
             * 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  orde structures to retrieve
             * @param {array} [$params] extra parameters specific to the bitvavo api endpoint
             * @return {Order[]} a list of ~@link https://docs.ccxt.com/#/?id=order-structure order structures~
             */
            if ($symbol === null) {
                throw new ArgumentsRequired($this->id . ' fetchOrdersWs() requires a $symbol argument');
            }
            Async\await($this->load_markets());
            Async\await($this->authenticate());
            $request = $this->fetchOrdersRequest ($symbol, $since, $limit, $params);
            $orders = Async\await($this->watch_request('privateGetOrders', $request));
            return $this->filter_by_symbol_since_limit($orders, $symbol, $since, $limit);
        }) ();
    }

    public function request_id() {
        $ts = (string) $this->milliseconds();
        $randomNumber = $this->rand_number(4);
        $randomPart = (string) $randomNumber;
        return intval($ts . $randomPart);
    }

    public function watch_request($action, $request) {
        return Async\async(function () use ($action, $request) {
            $messageHash = $this->request_id();
            $messageHashStr = (string) $messageHash;
            $request['action'] = $action;
            $request['requestId'] = $messageHash;
            $url = $this->urls['api']['ws'];
            return Async\await($this->watch($url, $messageHashStr, $request, $messageHashStr));
        }) ();
    }

    public function fetch_open_orders_ws(?string $symbol = null, ?int $since = null, ?int $limit = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbol, $since, $limit, $params) {
            /**
             * 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 bitvavo api endpoint
             * @return {Order[]} a list of ~@link https://docs.ccxt.com/#/?id=order-structure order structures~
             */
            Async\await($this->load_markets());
            Async\await($this->authenticate());
            $request = array(
                // 'market' => $market['id'], // rate $limit 25 without a $market, 1 with $market specified
            );
            $market = null;
            if ($symbol !== null) {
                $market = $this->market($symbol);
                $request['market'] = $market['id'];
            }
            $orders = Async\await($this->watch_request('privateGetOrdersOpen', $this->extend($request, $params)));
            return $this->filter_by_symbol_since_limit($orders, $symbol, $since, $limit);
        }) ();
    }

    public function fetch_my_trades_ws(?string $symbol = null, ?int $since = null, ?int $limit = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbol, $since, $limit, $params) {
            /**
             *
             * @see https://docs.bitvavo.com/#tag/Trades
             *
             * fetch all trades made by the user
             * @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 bitvavo api endpoint
             * @return {Trade[]} a list of ~@link https://docs.ccxt.com/#/?id=trade-structure trade structures~
             */
            if ($symbol === null) {
                throw new ArgumentsRequired($this->id . ' fetchMyTradesWs() requires a $symbol argument');
            }
            Async\await($this->load_markets());
            Async\await($this->authenticate());
            $request = $this->fetchMyTradesRequest ($symbol, $since, $limit, $params);
            $myTrades = Async\await($this->watch_request('privateGetTrades', $request));
            return $this->filter_by_symbol_since_limit($myTrades, $symbol, $since, $limit);
        }) ();
    }

    public function handle_my_trades(Client $client, $message) {
        //
        //    {
        //        $action => 'privateGetTrades',
        //        $response => array(
        //            {
        //                "id" => "108c3633-0276-4480-a902-17a01829deae",
        //                "orderId" => "1d671998-3d44-4df4-965f-0d48bd129a1b",
        //                "timestamp" => 1542967486256,
        //                "market" => "BTC-EUR",
        //                "side" => "buy",
        //                "amount" => "0.005",
        //                "price" => "5000.1",
        //                "taker" => true,
        //                "fee" => "0.03",
        //                "feeCurrency" => "EUR",
        //                "settled" => true
        //            }
        //        )
        //    }
        //
        //
        // $action = $this->safe_string($message, 'action');
        $response = $this->safe_list($message, 'response');
        // $marketId = $this->safe_string(firstRawTrade, 'market');
        $trades = $this->parse_trades($response, null, null, null);
        // $messageHash = $this->build_message_hash($action, array( 'market' => $marketId ));
        $messageHash = $this->safe_string($message, 'requestId');
        $client->resolve ($trades, $messageHash);
    }

    public function withdraw_ws(string $code, float $amount, string $address, ?string $tag = null, $params = array ()) {
        return Async\async(function () use ($code, $amount, $address, $tag, $params) {
            /**
             * 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 bitvavo 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);
            $this->check_address($address);
            Async\await($this->load_markets());
            Async\await($this->authenticate());
            $request = $this->withdrawRequest ($code, $amount, $address, $tag, $params);
            return Async\await($this->watch_request('privateWithdrawAssets', $request));
        }) ();
    }

    public function handle_withdraw(Client $client, $message) {
        //
        //    {
        //        $action => 'privateWithdrawAssets',
        //        $response => {
        //         "success" => true,
        //         "symbol" => "BTC",
        //         "amount" => "1.5"
        //        }
        //    }
        //
        // $action = $this->safe_string($message, 'action');
        // $messageHash = $this->build_message_hash($action, $message);
        $messageHash = $this->safe_string($message, 'requestId');
        $response = $this->safe_value($message, 'response');
        $withdraw = $this->parse_transaction($response);
        $client->resolve ($withdraw, $messageHash);
    }

    public function fetch_withdrawals_ws(?string $code = null, ?int $since = null, ?int $limit = null, $params = array ()) {
        return Async\async(function () use ($code, $since, $limit, $params) {
            /**
             *
             * @see https://docs.bitvavo.com/#tag/Account/paths/~1withdrawalHistory/get
             *
             * fetch all withdrawals made from an account
             * @param {string} $code unified currency $code
             * @param {int} [$since] the earliest time in ms to fetch withdrawals for
             * @param {int} [$limit] the maximum number of withdrawals structures to retrieve
             * @param {array} [$params] extra parameters specific to the bitvavo api endpoint
             * @return {array[]} a list of ~@link https://docs.ccxt.com/#/?id=transaction-structure transaction structures~
             */
            Async\await($this->load_markets());
            Async\await($this->authenticate());
            $request = $this->fetchWithdrawalsRequest ($code, $since, $limit, $params);
            $withdraws = Async\await($this->watch_request('privateGetWithdrawalHistory', $request));
            return $this->filter_by_currency_since_limit($withdraws, $code, $since, $limit);
        }) ();
    }

    public function handle_withdraws(Client $client, $message) {
        //
        //    {
        //        $action => 'privateGetWithdrawalHistory',
        //        $response => [array(
        //                timestamp => 1689792085000,
        //                symbol => 'BTC',
        //                amount => '0.0009',
        //                fee => '0',
        //                status => 'completed',
        //                txId => '7dbadc658d7d59c129de1332c55ee8e08d0ab74432faae03b417b9809c819d1f'
        //            ),
        //            ...
        //        ]
        //    }
        //
        // $action = $this->safe_string($message, 'action');
        // $messageHash = $this->build_message_hash($action, $message);
        $response = $this->safe_list($message, 'response');
        $messageHash = $this->safe_string($message, 'requestId');
        $withdrawals = $this->parse_transactions($response, null, null, null, array( 'type' => 'withdrawal' ));
        $client->resolve ($withdrawals, $messageHash);
    }

    public function fetch_ohlcv_ws(string $symbol, $timeframe = '1m', ?int $since = null, ?int $limit = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbol, $timeframe, $since, $limit, $params) {
            /**
             *
             * @see https://docs.bitvavo.com/#tag/Market-Data/paths/{1}market~~1candles/get
             *
             * fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market
             * @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 bitvavo api endpoint
             * @return {int[][]} A list of candles ordered, open, high, low, close, volume
             */
            Async\await($this->load_markets());
            $request = $this->fetchOHLCVRequest ($symbol, $timeframe, $since, $limit, $params);
            $action = 'getCandles';
            $ohlcv = Async\await($this->watch_request($action, $request));
            return $this->filter_by_since_limit($ohlcv, $since, $limit, 0, true);
        }) ();
    }

    public function fetch_deposits_ws(?string $code = null, ?int $since = null, ?int $limit = null, $params = array ()) {
        return Async\async(function () use ($code, $since, $limit, $params) {
            /**
             *
             * @see https://docs.bitvavo.com/#tag/Account/paths/~1depositHistory/get
             *
             * fetch all $deposits made to an account
             * @param {string} $code unified currency $code
             * @param {int} [$since] the earliest time in ms to fetch $deposits for
             * @param {int} [$limit] the maximum number of $deposits structures to retrieve
             * @param {array} [$params] extra parameters specific to the bitvavo api endpoint
             * @return {array[]} a list of ~@link https://docs.ccxt.com/#/?id=transaction-structure transaction structures~
             */
            Async\await($this->load_markets());
            Async\await($this->authenticate());
            $request = $this->fetchDepositsRequest ($code, $since, $limit, $params);
            $deposits = Async\await($this->watch_request('privateGetDepositHistory', $request));
            return $this->filter_by_currency_since_limit($deposits, $code, $since, $limit);
        }) ();
    }

    public function handle_deposits(Client $client, $message) {
        //
        //    {
        //        action => 'privateGetDepositHistory',
        //        $response => [array(
        //                timestamp => 1689792085000,
        //                symbol => 'BTC',
        //                amount => '0.0009',
        //                fee => '0',
        //                status => 'completed',
        //                txId => '7dbadc658d7d59c129de1332c55ee8e08d0ab74432faae03b417b9809c819d1f'
        //            ),
        //            ...
        //        ]
        //    }
        //
        $response = $this->safe_value($message, 'response');
        $deposits = $this->parse_transactions($response, null, null, null, array( 'type' => 'deposit' ));
        $messageHash = $this->safe_string($message, 'requestId');
        $client->resolve ($deposits, $messageHash);
    }

    public function fetch_trading_fees_ws($params = array ()): PromiseInterface {
        return Async\async(function () use ($params) {
            /**
             *
             * @see https://docs.bitvavo.com/#tag/Account/paths/~1account/get
             *
             * fetch the trading fees for multiple markets
             * @param {array} [$params] extra parameters specific to the bitvavo api endpoint
             * @return {array} a dictionary of ~@link https://docs.ccxt.com/#/?id=fee-structure fee structures~ indexed by market symbols
             */
            Async\await($this->load_markets());
            Async\await($this->authenticate());
            return Async\await($this->watch_request('privateGetAccount', $params));
        }) ();
    }

    public function fetch_markets_ws($params = array ()) {
        return Async\async(function () use ($params) {
            /**
             *
             * @see https://docs.bitvavo.com/#tag/General/paths/~1markets/get
             *
             * retrieves data on all markets for bitvavo
             * @param {array} [$params] extra parameters specific to the exchange api endpoint
             * @return {array[]} an array of objects representing market data
             */
            return Async\await($this->watch_request('getMarkets', $params));
        }) ();
    }

    public function fetch_currencies_ws($params = array ()) {
        return Async\async(function () use ($params) {
            /**
             *
             * @see https://docs.bitvavo.com/#tag/General/paths/~1assets/get
             *
             * fetches all available currencies on an exchange
             * @param {array} [$params] extra parameters specific to the bitvavo api endpoint
             * @return {array} an associative dictionary of currencies
             */
            Async\await($this->load_markets());
            return Async\await($this->watch_request('getAssets', $params));
        }) ();
    }

    public function handle_fetch_currencies(Client $client, $message) {
        //
        //    {
        //        action => 'getAssets',
        //        $response => [array(
        //                symbol => '1INCH',
        //                name => '1inch',
        //                decimals => 8,
        //                depositFee => '0',
        //                depositConfirmations => 64,
        //                depositStatus => 'OK',
        //                withdrawalFee => '13',
        //                withdrawalMinAmount => '13',
        //                withdrawalStatus => 'OK',
        //                networks => [Array],
        //                $message => ''
        //            ),
        //            ...
        //        ]
        //    }
        //
        $messageHash = $this->safe_string($message, 'requestId');
        $response = $this->safe_value($message, 'response');
        $currencies = $this->parse_currencies($response);
        $client->resolve ($currencies, $messageHash);
    }

    public function handle_trading_fees($client, $message) {
        //
        //    {
        //        action => 'privateGetAccount',
        //        $response => {
        //            $fees => {
        //                taker => '0.0025',
        //                maker => '0.0015',
        //                volume => '1693.74'
        //            }
        //        }
        //    }
        //
        $messageHash = $this->safe_string($message, 'requestId');
        $response = $this->safe_value($message, 'response');
        $fees = $this->parse_trading_fees($response);
        $client->resolve ($fees, $messageHash);
    }

    public function fetch_balance_ws($params = array ()): PromiseInterface {
        return Async\async(function () use ($params) {
            /**
             *
             * @see https://docs.bitvavo.com/#tag/Account/paths/~1balance/get
             *
             * 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 bitvavo api endpoint
             * @return {array} a ~@link https://docs.ccxt.com/en/latest/manual.html?#balance-structure balance structure~
             */
            Async\await($this->load_markets());
            Async\await($this->authenticate());
            return Async\await($this->watch_request('privateGetBalance', $params));
        }) ();
    }

    public function handle_fetch_balance(Client $client, $message) {
        //
        //    {
        //        action => 'privateGetBalance',
        //        $response => [array(
        //                symbol => 'ADA',
        //                available => '0',
        //                inOrder => '0'
        //            ),
        //            ...
        //        ]
        //    }
        //
        $messageHash = $this->safe_string($message, 'requestId');
        $response = $this->safe_value($message, 'response', array());
        $balance = $this->parse_balance($response);
        $client->resolve ($balance, $messageHash);
    }

    public function handle_single_order(Client $client, $message) {
        //
        //    {
        //        action => 'privateCreateOrder',
        //        $response => {
        //            orderId => 'd71df826-1130-478a-8741-d219128675b0',
        //            market => 'BTC-EUR',
        //            created => 1689792749748,
        //            updated => 1689792749748,
        //            status => 'new',
        //            side => 'sell',
        //            orderType => 'limit',
        //            amount => '0.0002',
        //            amountRemaining => '0.0002',
        //            price => '37000',
        //            onHold => '0.0002',
        //            onHoldCurrency => 'BTC',
        //            filledAmount => '0',
        //            filledAmountQuote => '0',
        //            feePaid => '0',
        //            feeCurrency => 'EUR',
        //            fills => array(),
        //            selfTradePrevention => 'decrementAndCancel',
        //            visible => true,
        //            timeInForce => 'GTC',
        //            postOnly => false
        //        }
        //    }
        //
        $response = $this->safe_value($message, 'response', array());
        $order = $this->parse_order($response);
        $messageHash = $this->safe_string($message, 'requestId');
        $client->resolve ($order, $messageHash);
    }

    public function handle_markets(Client $client, $message) {
        //
        //    {
        //        action => 'getMarkets',
        //        $response => [array(
        //                market => '1INCH-EUR',
        //                status => 'trading',
        //                base => '1INCH',
        //                quote => 'EUR',
        //                pricePrecision => 5,
        //                minOrderInBaseAsset => '2',
        //                minOrderInQuoteAsset => '5',
        //                maxOrderInBaseAsset => '1000000000',
        //                maxOrderInQuoteAsset => '1000000000',
        //                orderTypes => [Array]
        //            ),
        //            ...
        //        ]
        //    }
        //
        $response = $this->safe_value($message, 'response', array());
        $markets = $this->parse_markets($response);
        $messageHash = $this->safe_string($message, 'requestId');
        $client->resolve ($markets, $messageHash);
    }

    public function build_message_hash($action, $params = array ()) {
        $methods = array(
            'privateCreateOrder' => array($this, 'action_and_market_message_hash'),
            'privateUpdateOrder' => array($this, 'action_and_order_id_message_hash'),
            'privateCancelOrder' => array($this, 'action_and_order_id_message_hash'),
            'privateGetOrder' => array($this, 'action_and_order_id_message_hash'),
            'privateGetTrades' => array($this, 'action_and_market_message_hash'),
        );
        $method = $this->safe_value($methods, $action);
        $messageHash = $action;
        if ($method !== null) {
            $messageHash = $method($action, $params);
        }
        return $messageHash;
    }

    public function action_and_market_message_hash($action, $params = array ()) {
        $symbol = $this->safe_string($params, 'market', '');
        return $action . $symbol;
    }

    public function action_and_order_id_message_hash($action, $params = array ()) {
        $orderId = $this->safe_string($params, 'orderId');
        if ($orderId === null) {
            throw new ExchangeError($this->id . ' privateUpdateOrderMessageHash requires a $orderId parameter');
        }
        return $action . $orderId;
    }

    public function handle_order(Client $client, $message) {
        //
        //     {
        //         "event" => "order",
        //         "orderId" => "f0e5180f-9497-4d05-9dc2-7056e8a2de9b",
        //         "market" => "ETH-EUR",
        //         "created" => 1590948500319,
        //         "updated" => 1590948500319,
        //         "status" => "new",
        //         "side" => "sell",
        //         "orderType" => "limit",
        //         "amount" => "0.1",
        //         "amountRemaining" => "0.1",
        //         "price" => "300",
        //         "onHold" => "0.1",
        //         "onHoldCurrency" => "ETH",
        //         "selfTradePrevention" => "decrementAndCancel",
        //         "visible" => true,
        //         "timeInForce" => "GTC",
        //         "postOnly" => false
        //     }
        //
        $marketId = $this->safe_string($message, 'market');
        $market = $this->safe_market($marketId, null, '-');
        $symbol = $market['symbol'];
        $messageHash = 'order:' . $symbol;
        $order = $this->parse_order($message, $market);
        if ($this->orders === null) {
            $limit = $this->safe_integer($this->options, 'ordersLimit', 1000);
            $this->orders = new ArrayCacheBySymbolById ($limit);
        }
        $orders = $this->orders;
        $orders->append ($order);
        $client->resolve ($this->orders, $messageHash);
    }

    public function handle_my_trade(Client $client, $message) {
        //
        //     {
        //         "event" => "fill",
        //         "timestamp" => 1590964470132,
        //         "market" => "ETH-EUR",
        //         "orderId" => "85d082e1-eda4-4209-9580-248281a29a9a",
        //         "fillId" => "861d2da5-aa93-475c-8d9a-dce431bd4211",
        //         "side" => "sell",
        //         "amount" => "0.1",
        //         "price" => "211.46",
        //         "taker" => true,
        //         "fee" => "0.056",
        //         "feeCurrency" => "EUR"
        //     }
        //
        $marketId = $this->safe_string($message, 'market');
        $market = $this->safe_market($marketId, null, '-');
        $symbol = $market['symbol'];
        $messageHash = 'myTrades:' . $symbol;
        $trade = $this->parse_trade($message, $market);
        if ($this->myTrades === null) {
            $limit = $this->safe_integer($this->options, 'tradesLimit', 1000);
            $this->myTrades = new ArrayCache ($limit);
        }
        $tradesArray = $this->myTrades;
        $tradesArray->append ($trade);
        $client->resolve ($tradesArray, $messageHash);
    }

    public function handle_subscription_status(Client $client, $message) {
        //
        //     {
        //         "event" => "subscribed",
        //         "subscriptions" => {
        //             "book" => array( "BTC-EUR" )
        //         }
        //     }
        //
        $subscriptions = $this->safe_value($message, 'subscriptions', array());
        $methods = array(
            'book' => array($this, 'handle_order_book_subscriptions'),
        );
        $names = is_array($subscriptions) ? array_keys($subscriptions) : array();
        for ($i = 0; $i < count($names); $i++) {
            $name = $names[$i];
            $method = $this->safe_value($methods, $name);
            if ($method !== null) {
                $subscription = $this->safe_value($subscriptions, $name);
                $method($client, $message, $subscription);
            }
        }
        return $message;
    }

    public function authenticate($params = array ()) {
        return Async\async(function () use ($params) {
            $url = $this->urls['api']['ws'];
            $client = $this->client($url);
            $messageHash = 'authenticated';
            $future = $this->safe_value($client->subscriptions, $messageHash);
            if ($future === null) {
                $timestamp = $this->milliseconds();
                $stringTimestamp = (string) $timestamp;
                $auth = $stringTimestamp . 'GET/' . $this->version . '/websocket';
                $signature = $this->hmac($this->encode($auth), $this->encode($this->secret), 'sha256');
                $action = 'authenticate';
                $request = array(
                    'action' => $action,
                    'key' => $this->apiKey,
                    'signature' => $signature,
                    'timestamp' => $timestamp,
                );
                $message = $this->extend($request, $params);
                $future = Async\await($this->watch($url, $messageHash, $message, $messageHash));
                $client->subscriptions[$messageHash] = $future;
            }
            return $future;
        }) ();
    }

    public function handle_authentication_message(Client $client, $message) {
        //
        //     {
        //         "event" => "authenticate",
        //         "authenticated" => true
        //     }
        //
        $messageHash = 'authenticated';
        $authenticated = $this->safe_bool($message, 'authenticated', false);
        if ($authenticated) {
            // we resolve the future here permanently so authentication only happens once
            $client->resolve ($message, $messageHash);
        } else {
            $error = new AuthenticationError ($this->json($message));
            $client->reject ($error, $messageHash);
            // allows further authentication attempts
            if (is_array($client->subscriptions) && array_key_exists($messageHash, $client->subscriptions)) {
                unset($client->subscriptions[$messageHash]);
            }
        }
    }

    public function handle_error_message(Client $client, $message): Bool {
        //
        //    {
        //        $action => 'privateCreateOrder',
        //        market => 'BTC-EUR',
        //        errorCode => 217,
        //        $error => 'Minimum order size in quote currency is 5 EUR or 0.001 BTC.'
        //    }
        //    {
        //        $action => 'privateCreateOrder',
        //        requestId => '17317539426571916',
        //        market => 'USDT-EUR',
        //        errorCode => 216,
        //        $error => 'You do not have sufficient balance to complete this operation.'
        //    }
        //
        $error = $this->safe_string($message, 'error');
        $code = $this->safe_integer($error, 'errorCode');
        $action = $this->safe_string($message, 'action');
        $buildMessage = $this->build_message_hash($action, $message);
        $messageHash = $this->safe_string($message, 'requestId', $buildMessage);
        $rejected = false;
        try {
            $this->handle_errors($code, $error, $client->url, '', array(), $error, $message, array(), array());
        } catch (Exception $e) {
            $rejected = true;
            $client->reject ($e, $messageHash);
        }
        if (!$rejected) {
            $client->reject ($message, $messageHash);
            return true;
        }
        return null;
    }

    public function handle_message(Client $client, $message) {
        //
        //     {
        //         "event" => "subscribed",
        //         "subscriptions" => {
        //             "book" => array( "BTC-EUR" )
        //         }
        //     }
        //
        //     {
        //         "event" => "book",
        //         "market" => "BTC-EUR",
        //         "nonce" => 36729561,
        //         "bids" => array(
        //             array( "8513.3", "0" ),
        //             array( '8518.8', "0.64236203" ),
        //             array( '8513.6', "0.32435481" ),
        //         ),
        //         "asks" => array()
        //     }
        //
        //     {
        //         "action" => "getBook",
        //         "response" => {
        //             "market" => "BTC-EUR",
        //             "nonce" => 36946120,
        //             "bids" => array(
        //                 array( '8494.9', "0.24399521" ),
        //                 array( '8494.8', "0.34884085" ),
        //                 array( '8493.9', "0.14535128" ),
        //             ),
        //             "asks" => array(
        //                 array( "8495", "0.46982463" ),
        //                 array( '8495.1', "0.12178267" ),
        //                 array( '8496.2', "0.21924143" ),
        //             )
        //         }
        //     }
        //
        //     {
        //         "event" => "authenticate",
        //         "authenticated" => true
        //     }
        //
        $error = $this->safe_string($message, 'error');
        if ($error !== null) {
            $this->handle_error_message($client, $message);
        }
        $methods = array(
            'subscribed' => array($this, 'handle_subscription_status'),
            'book' => array($this, 'handle_order_book'),
            'getBook' => array($this, 'handle_order_book_snapshot'),
            'trade' => array($this, 'handle_trade'),
            'candle' => array($this, 'handle_ohlcv'),
            'ticker24h' => array($this, 'handle_ticker'),
            'authenticate' => array($this, 'handle_authentication_message'),
            'order' => array($this, 'handle_order'),
            'fill' => array($this, 'handle_my_trade'),
            'privateCreateOrder' => array($this, 'handle_single_order'),
            'privateUpdateOrder' => array($this, 'handle_single_order'),
            'privateGetBalance' => array($this, 'handle_fetch_balance'),
            'privateCancelOrders' => array($this, 'handle_multiple_orders'),
            'privateGetOrders' => array($this, 'handle_multiple_orders'),
            'privateGetOrder' => array($this, 'handle_single_order'),
            'privateCancelOrder' => array($this, 'handle_single_order'),
            'privateGetOrdersOpen' => array($this, 'handle_multiple_orders'),
            'privateGetAccount' => array($this, 'handle_trading_fees'),
            'privateGetDepositHistory' => array($this, 'handle_deposits'),
            'privateGetWithdrawalHistory' => array($this, 'handle_withdraws'),
            'privateWithdrawAssets' => array($this, 'handle_withdraw'),
            'privateGetTrades' => array($this, 'handle_my_trades'),
            'getAssets' => array($this, 'handle_fetch_currencies'),
            'getCandles' => array($this, 'handle_fetch_ohlcv'),
            'getMarkets' => array($this, 'handle_markets'),
        );
        $event = $this->safe_string($message, 'event');
        $method = $this->safe_value($methods, $event);
        if ($method === null) {
            $action = $this->safe_string($message, 'action');
            $method = $this->safe_value($methods, $action);
            if ($method !== null) {
                $method($client, $message);
            }
        } else {
            $method($client, $message);
        }
    }
}
