<?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\NetworkError;
use ccxt\ChecksumError;
use \React\Async;
use \React\Promise\PromiseInterface;

class cryptocom extends \ccxt\async\cryptocom {

    public function describe(): mixed {
        return $this->deep_extend(parent::describe(), array(
            'has' => array(
                'ws' => true,
                'watchBalance' => true,
                'watchTicker' => true,
                'watchTickers' => true,
                'watchBidsAsks' => true,
                'watchMyTrades' => true,
                'watchTrades' => true,
                'watchTradesForSymbols' => true,
                'watchOrderBook' => true,
                'watchOrderBookForSymbols' => true,
                'watchOrders' => true,
                'watchOHLCV' => true,
                'watchPositions' => true,
                'createOrderWs' => true,
                'cancelOrderWs' => true,
                'cancelAllOrders' => true,
                'editOrderWs' => true,
            ),
            'urls' => array(
                'api' => array(
                    'ws' => array(
                        'public' => 'wss://stream.crypto.com/exchange/v1/market',
                        'private' => 'wss://stream.crypto.com/exchange/v1/user',
                    ),
                ),
                'test' => array(
                    'public' => 'wss://uat-stream.3ona.co/exchange/v1/market',
                    'private' => 'wss://uat-stream.3ona.co/exchange/v1/user',
                ),
            ),
            'options' => array(
                'watchPositions' => array(
                    'fetchPositionsSnapshot' => true, // or false
                    'awaitPositionsSnapshot' => true, // whether to wait for the positions snapshot before providing updates
                ),
                'watchOrderBook' => array(
                    'checksum' => true,
                ),
            ),
            'streaming' => array(
            ),
        ));
    }

    public function pong($client, $message) {
        return Async\async(function () use ($client, $message) {
            // {
            //     "id" => 1587523073344,
            //     "method" => "public/heartbeat",
            //     "code" => 0
            // }
            try {
                Async\await($client->send (array( 'id' => $this->safe_integer($message, 'id'), 'method' => 'public/respond-heartbeat' )));
            } catch (Exception $e) {
                $error = new NetworkError ($this->id . ' pong failed with $error ' . $this->json($e));
                $client->reset ($error);
            }
        }) ();
    }

    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
             *
             * @see https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#book-instrument_name
             *
             * @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
             * @param {string} [$params->bookSubscriptionType] The subscription type. Allowed values => SNAPSHOT full snapshot. This is the default if not specified. SNAPSHOT_AND_UPDATE delta updates
             * @param {int} [$params->bookUpdateFrequency] Book update interval in ms. Allowed values => 100 for snapshot subscription 10 for delta subscription
             * @return {array} A dictionary of ~@link https://docs.ccxt.com/#/?id=order-book-structure order book structures~ indexed by market symbols
             */
            return Async\await($this->watch_order_book_for_symbols(array( $symbol ), $limit, $params));
        }) ();
    }

    public function un_watch_order_book(string $symbol, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbol, $params) {
            /**
             * unWatches information on open orders with bid (buy) and ask (sell) prices, volumes and other data
             *
             * @see https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#book-instrument_name
             *
             * @param {string} $symbol unified $symbol of the market to fetch the order book for
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @param {string} [$params->bookSubscriptionType] The subscription type. Allowed values => SNAPSHOT full snapshot. This is the default if not specified. SNAPSHOT_AND_UPDATE delta updates
             * @param {int} [$params->bookUpdateFrequency] Book update interval in ms. Allowed values => 100 for snapshot subscription 10 for delta subscription
             * @return {array} A dictionary of ~@link https://docs.ccxt.com/#/?id=order-book-structure order book structures~ indexed by market symbols
             */
            return Async\await($this->un_watch_order_book_for_symbols(array( $symbol ), $params));
        }) ();
    }

    public function watch_order_book_for_symbols(array $symbols, ?int $limit = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbols, $limit, $params) {
            /**
             * watches information on open orders with bid (buy) and ask (sell) prices, volumes and other data
             *
             * @see https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#book-instrument_name
             *
             * @param {string[]} $symbols unified array of $symbols
             * @param {int} [$limit] the maximum amount of order book entries to return
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @param {string} [$params->bookSubscriptionType] The subscription type. Allowed values => SNAPSHOT full snapshot. This is the default if not specified. SNAPSHOT_AND_UPDATE delta updates
             * @param {int} [$params->bookUpdateFrequency] Book update interval in ms. Allowed values => 100 for snapshot subscription 10 for delta subscription
             * @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());
            $symbols = $this->market_symbols($symbols);
            $topics = array();
            $messageHashes = array();
            if (!$limit) {
                $limit = 50;
            }
            $topicParams = $this->safe_value($params, 'params');
            if ($topicParams === null) {
                $params['params'] = array();
            }
            $bookSubscriptionType = null;
            $bookSubscriptionType2 = null;
            list($bookSubscriptionType, $params) = $this->handle_option_and_params($params, 'watchOrderBook', 'bookSubscriptionType', 'SNAPSHOT_AND_UPDATE');
            list($bookSubscriptionType2, $params) = $this->handle_option_and_params($params, 'watchOrderBookForSymbols', 'bookSubscriptionType', $bookSubscriptionType);
            $params['params']['bookSubscriptionType'] = $bookSubscriptionType2;
            $bookUpdateFrequency = null;
            $bookUpdateFrequency2 = null;
            list($bookUpdateFrequency, $params) = $this->handle_option_and_params($params, 'watchOrderBook', 'bookUpdateFrequency');
            list($bookUpdateFrequency2, $params) = $this->handle_option_and_params($params, 'watchOrderBookForSymbols', 'bookUpdateFrequency', $bookUpdateFrequency);
            if ($bookUpdateFrequency2 !== null) {
                $params['params']['bookSubscriptionType'] = $bookUpdateFrequency2;
            }
            for ($i = 0; $i < count($symbols); $i++) {
                $symbol = $symbols[$i];
                $market = $this->market($symbol);
                $currentTopic = 'book' . '.' . $market['id'] . '.' . (string) $limit;
                $messageHash = 'orderbook:' . $market['symbol'];
                $messageHashes[] = $messageHash;
                $topics[] = $currentTopic;
            }
            $orderbook = Async\await($this->watch_public_multiple($messageHashes, $topics, $params));
            return $orderbook->limit ();
        }) ();
    }

    public function un_watch_order_book_for_symbols(array $symbols, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbols, $params) {
            /**
             * unWatches information on open orders with bid (buy) and ask (sell) prices, volumes and other data
             *
             * @see https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#book-instrument_name
             *
             * @param {string[]} $symbols unified array of $symbols
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @param {int} [$params->limit] orderbook $limit, default is 50
             * @param {string} [$params->bookSubscriptionType] The subscription type. Allowed values => SNAPSHOT full snapshot. This is the default if not specified. SNAPSHOT_AND_UPDATE delta updates
             * @param {int} [$params->bookUpdateFrequency] Book update interval in ms. Allowed values => 100 for snapshot subscription 10 for delta subscription
             * @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());
            $symbols = $this->market_symbols($symbols);
            $topics = array();
            $subMessageHashes = array();
            $messageHashes = array();
            $limit = $this->safe_integer($params, 'limit', 50);
            $topicParams = $this->safe_value($params, 'params');
            if ($topicParams === null) {
                $params['params'] = array();
            }
            $bookSubscriptionType = null;
            $bookSubscriptionType2 = null;
            list($bookSubscriptionType, $params) = $this->handle_option_and_params($params, 'watchOrderBook', 'bookSubscriptionType', 'SNAPSHOT_AND_UPDATE');
            list($bookSubscriptionType2, $params) = $this->handle_option_and_params($params, 'watchOrderBookForSymbols', 'bookSubscriptionType', $bookSubscriptionType);
            $params['params']['bookSubscriptionType'] = $bookSubscriptionType2;
            $bookUpdateFrequency = null;
            $bookUpdateFrequency2 = null;
            list($bookUpdateFrequency, $params) = $this->handle_option_and_params($params, 'watchOrderBook', 'bookUpdateFrequency');
            list($bookUpdateFrequency2, $params) = $this->handle_option_and_params($params, 'watchOrderBookForSymbols', 'bookUpdateFrequency', $bookUpdateFrequency);
            if ($bookUpdateFrequency2 !== null) {
                $params['params']['bookSubscriptionType'] = $bookUpdateFrequency2;
            }
            for ($i = 0; $i < count($symbols); $i++) {
                $symbol = $symbols[$i];
                $market = $this->market($symbol);
                $currentTopic = 'book' . '.' . $market['id'] . '.' . (string) $limit;
                $messageHash = 'orderbook:' . $market['symbol'];
                $subMessageHashes[] = $messageHash;
                $messageHashes[] = 'unsubscribe:' . $messageHash;
                $topics[] = $currentTopic;
            }
            return Async\await($this->un_watch_public_multiple('orderbook', $symbols, $messageHashes, $subMessageHashes, $topics, $params));
        }) ();
    }

    public function handle_delta($bookside, $delta) {
        $price = $this->safe_float($delta, 0);
        $amount = $this->safe_float($delta, 1);
        $count = $this->safe_integer($delta, 2);
        $bookside->storeArray (array( $price, $amount, $count ));
    }

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

    public function handle_order_book(Client $client, $message) {
        //
        // snapshot
        //    {
        //        "instrument_name":"LTC_USDT",
        //        "subscription":"book.LTC_USDT.150",
        //        "channel":"book",
        //        "depth":150,
        //        "data" => [
        //             {
        //                 "bids" => [
        //                     [122.21, 0.74041, 4]
        //                 ],
        //                 "asks" => [
        //                     [122.29, 0.00002, 1]
        //                 ]
        //                 "t" => 1648123943803,
        //                 "s":754560122
        //             }
        //         ]
        //    }
        //  update
        //    {
        //        "instrument_name":"BTC_USDT",
        //        "subscription":"book.BTC_USDT.50",
        //        "channel":"book.update",
        //        "depth":50,
        //        "data":array(
        //           {
        //              "update":array(
        //                 "asks":array(
        //                    array(
        //                       "43755.46",
        //                       "0.10000",
        //                       "1"
        //                    ),
        //                    ...
        //                 ),
        //                 "bids":array(
        //                    array(
        //                       "43737.46",
        //                       "0.14096",
        //                       "1"
        //                    ),
        //                    ...
        //                 )
        //              ),
        //              "t":1704484068898,
        //              "tt":1704484068892,
        //              "u":78795598253024,
        //              "pu":78795598162080,
        //              "cs":-781431132
        //           }
        //        )
        //    }
        //
        $marketId = $this->safe_string($message, 'instrument_name');
        $market = $this->safe_market($marketId);
        $symbol = $market['symbol'];
        $data = $this->safe_value($message, 'data');
        $data = $this->safe_value($data, 0);
        $timestamp = $this->safe_integer($data, 't');
        if (!(is_array($this->orderbooks) && array_key_exists($symbol, $this->orderbooks))) {
            $limit = $this->safe_integer($message, 'depth');
            $this->orderbooks[$symbol] = $this->counted_order_book(array(), $limit);
        }
        $orderbook = $this->orderbooks[$symbol];
        $channel = $this->safe_string($message, 'channel');
        $nonce = $this->safe_integer_2($data, 'u', 's');
        $books = $data;
        if ($channel === 'book') {  // snapshot
            $orderbook->reset (array());
            $orderbook['symbol'] = $symbol;
            $orderbook['timestamp'] = $timestamp;
            $orderbook['datetime'] = $this->iso8601($timestamp);
            $orderbook['nonce'] = $nonce;
        } else {
            $books = $this->safe_value($data, 'update', array());
            $previousNonce = $this->safe_integer($data, 'pu');
            $currentNonce = $orderbook['nonce'];
            if ($currentNonce !== $previousNonce) {
                $checksum = $this->handle_option('watchOrderBook', 'checksum', true);
                if ($checksum) {
                    throw new ChecksumError($this->id . ' ' . $this->orderbook_checksum_message($symbol));
                }
            }
        }
        $this->handle_deltas($orderbook['asks'], $this->safe_value($books, 'asks', array()));
        $this->handle_deltas($orderbook['bids'], $this->safe_value($books, 'bids', array()));
        $orderbook['nonce'] = $nonce;
        $this->orderbooks[$symbol] = $orderbook;
        $messageHash = 'orderbook:' . $symbol;
        $client->resolve ($orderbook, $messageHash);
    }

    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
             *
             * @see https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#trade-instrument_name
             *
             * @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~
             */
            return Async\await($this->watch_trades_for_symbols(array( $symbol ), $since, $limit, $params));
        }) ();
    }

    public function un_watch_trades(string $symbol, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbol, $params) {
            /**
             * get the list of most recent trades for a particular $symbol
             *
             * @see https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#trade-instrument_name
             *
             * @param {string} $symbol unified $symbol of the market to fetch trades for
             * @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~
             */
            return Async\await($this->un_watch_trades_for_symbols(array( $symbol ), $params));
        }) ();
    }

    public function watch_trades_for_symbols(array $symbols, ?int $since = null, ?int $limit = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbols, $since, $limit, $params) {
            /**
             * get the list of most recent $trades for a particular $symbol
             *
             * @see https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#trade-instrument_name
             *
             * @param {string[]} $symbols 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());
            $symbols = $this->market_symbols($symbols);
            $topics = array();
            for ($i = 0; $i < count($symbols); $i++) {
                $symbol = $symbols[$i];
                $market = $this->market($symbol);
                $currentTopic = 'trade' . '.' . $market['id'];
                $topics[] = $currentTopic;
            }
            $trades = Async\await($this->watch_public_multiple($topics, $topics, $params));
            if ($this->newUpdates) {
                $first = $this->safe_value($trades, 0);
                $tradeSymbol = $this->safe_string($first, 'symbol');
                $limit = $trades->getLimit ($tradeSymbol, $limit);
            }
            return $this->filter_by_since_limit($trades, $since, $limit, 'timestamp', true);
        }) ();
    }

    public function un_watch_trades_for_symbols(array $symbols, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbols, $params) {
            /**
             * get the list of most recent trades for a particular $symbol
             *
             * @see https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#trade-instrument_name
             *
             * @param {string[]} [$symbols] list of unified $market $symbols to unwatch trades for
             * @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());
            $symbols = $this->market_symbols($symbols);
            $topics = array();
            $messageHashes = array();
            for ($i = 0; $i < count($symbols); $i++) {
                $symbol = $symbols[$i];
                $market = $this->market($symbol);
                $currentTopic = 'trade' . '.' . $market['id'];
                $messageHashes[] = 'unsubscribe:trades:' . $market['symbol'];
                $topics[] = $currentTopic;
            }
            return Async\await($this->un_watch_public_multiple('trades', $symbols, $messageHashes, $topics, $topics, $params));
        }) ();
    }

    public function handle_trades(Client $client, $message) {
        //
        // {
        //     "code" => 0,
        //     "method" => "subscribe",
        //     "result" => {
        //       "instrument_name" => "BTC_USDT",
        //       "subscription" => "trade.BTC_USDT",
        //       "channel" => "trade",
        //       "data" => array(
        //             {
        //                 "dataTime":1648122434405,
        //                 "d":"2358394540212355488",
        //                 "s":"SELL",
        //                 "p":42980.85,
        //                 "q":0.002325,
        //                 "t":1648122434404,
        //                 "i":"BTC_USDT"
        //              }
        //              (...)
        //       )
        // }
        //
        $channel = $this->safe_string($message, 'channel');
        $marketId = $this->safe_string($message, 'instrument_name');
        $symbolSpecificMessageHash = $this->safe_string($message, 'subscription');
        $market = $this->safe_market($marketId);
        $symbol = $market['symbol'];
        $stored = $this->safe_value($this->trades, $symbol);
        if ($stored === null) {
            $limit = $this->safe_integer($this->options, 'tradesLimit', 1000);
            $stored = new ArrayCache ($limit);
            $this->trades[$symbol] = $stored;
        }
        $data = $this->safe_value($message, 'data', array());
        $dataLength = count($data);
        if ($dataLength === 0) {
            return;
        }
        $parsedTrades = $this->parse_trades($data, $market);
        for ($j = 0; $j < count($parsedTrades); $j++) {
            $stored->append ($parsedTrades[$j]);
        }
        $channelReplaced = str_replace('.' . $marketId, '', $channel);
        $client->resolve ($stored, $symbolSpecificMessageHash);
        $client->resolve ($stored, $channelReplaced);
    }

    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
             *
             * @see https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#user-trade-instrument_name
             *
             * @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~
             */
            Async\await($this->load_markets());
            $market = null;
            if ($symbol !== null) {
                $market = $this->market($symbol);
                $symbol = $market['symbol'];
            }
            $messageHash = 'user.trade';
            $messageHash = ($market !== null) ? ($messageHash . '.' . $market['id']) : $messageHash;
            $trades = Async\await($this->watch_private_subscribe($messageHash, $params));
            if ($this->newUpdates) {
                $limit = $trades->getLimit ($symbol, $limit);
            }
            return $this->filter_by_symbol_since_limit($trades, $symbol, $since, $limit, true);
        }) ();
    }

    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://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#ticker-instrument_name
             *
             * @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~
             */
            Async\await($this->load_markets());
            $market = $this->market($symbol);
            $messageHash = 'ticker' . '.' . $market['id'];
            return Async\await($this->watch_public($messageHash, $params));
        }) ();
    }

    public function un_watch_ticker(string $symbol, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbol, $params) {
            /**
             * unWatches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific $market
             *
             * @see https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#ticker-instrument_name
             *
             * @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~
             */
            Async\await($this->load_markets());
            $market = $this->market($symbol);
            $subMessageHash = 'ticker' . '.' . $market['id'];
            $messageHash = 'unsubscribe:ticker:' . $market['symbol'];
            return Async\await($this->un_watch_public_multiple('ticker', [ $market['symbol'] ], array( $messageHash ), array( $subMessageHash ), array( $subMessageHash ), $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://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#$ticker-instrument_name
             *
             * @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);
            $messageHashes = array();
            $marketIds = $this->market_ids($symbols);
            for ($i = 0; $i < count($marketIds); $i++) {
                $marketId = $marketIds[$i];
                $messageHashes[] = 'ticker.' . $marketId;
            }
            $url = $this->urls['api']['ws']['public'];
            $id = $this->nonce();
            $request = array(
                'method' => 'subscribe',
                'params' => array(
                    'channels' => $messageHashes,
                ),
                'nonce' => $id,
            );
            $ticker = Async\await($this->watch_multiple($url, $messageHashes, $this->extend($request, $params), $messageHashes));
            if ($this->newUpdates) {
                $result = array();
                $result[$ticker['symbol']] = $ticker;
                return $result;
            }
            return $this->filter_by_array($this->tickers, 'symbol', $symbols);
        }) ();
    }

    public function un_watch_tickers(?array $symbols = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbols, $params) {
            /**
             * unWatches a price ticker
             *
             * @see https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#ticker-instrument_name
             *
             * @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);
            $messageHashes = array();
            $subMessageHashes = array();
            $marketIds = $this->market_ids($symbols);
            for ($i = 0; $i < count($marketIds); $i++) {
                $marketId = $marketIds[$i];
                $symbol = $symbols[$i];
                $subMessageHashes[] = 'ticker.' . $marketId;
                $messageHashes[] = 'unsubscribe:ticker:' . $symbol;
            }
            return Async\await($this->un_watch_public_multiple('ticker', $symbols, $messageHashes, $subMessageHashes, $subMessageHashes, $params));
        }) ();
    }

    public function handle_ticker(Client $client, $message) {
        //
        //     {
        //       "instrument_name" => "ETHUSD-PERP",
        //       "subscription" => "ticker.ETHUSD-PERP",
        //       "channel" => "ticker",
        //       "data" => array(
        //         {
        //           "h" => "2400.20",
        //           "l" => "2277.10",
        //           "a" => "2335.25",
        //           "c" => "-0.0022",
        //           "b" => "2335.10",
        //           "bs" => "5.4000",
        //           "k" => "2335.16",
        //           "ks" => "1.9970",
        //           "i" => "ETHUSD-PERP",
        //           "v" => "1305697.6462",
        //           "vv" => "3058704939.17",
        //           "oi" => "161646.3614",
        //           "t" => 1726069647560
        //         }
        //       )
        //     }
        //
        $this->handle_bid_ask($client, $message);
        $messageHash = $this->safe_string($message, 'subscription');
        $marketId = $this->safe_string($message, 'instrument_name');
        $market = $this->safe_market($marketId);
        $data = $this->safe_value($message, 'data', array());
        for ($i = 0; $i < count($data); $i++) {
            $ticker = $data[$i];
            $parsed = $this->parse_ws_ticker($ticker, $market);
            $symbol = $parsed['symbol'];
            $this->tickers[$symbol] = $parsed;
            $client->resolve ($parsed, $messageHash);
        }
    }

    public function parse_ws_ticker(array $ticker, ?array $market = null): array {
        //
        //     {
        //       "h" => "2400.20",
        //       "l" => "2277.10",
        //       "a" => "2335.25",
        //       "c" => "-0.0022",
        //       "b" => "2335.10",
        //       "bs" => "5.4000",
        //       "k" => "2335.16",
        //       "ks" => "1.9970",
        //       "i" => "ETHUSD-PERP",
        //       "v" => "1305697.6462",
        //       "vv" => "3058704939.17",
        //       "oi" => "161646.3614",
        //       "t" => 1726069647560
        //     }
        //
        $timestamp = $this->safe_integer($ticker, 't');
        $marketId = $this->safe_string($ticker, 'i');
        $market = $this->safe_market($marketId, $market, '_');
        $quote = $this->safe_string($market, 'quote');
        $last = $this->safe_string($ticker, 'a');
        return $this->safe_ticker(array(
            'symbol' => $market['symbol'],
            'timestamp' => $timestamp,
            'datetime' => $this->iso8601($timestamp),
            'high' => $this->safe_number($ticker, 'h'),
            'low' => $this->safe_number($ticker, 'l'),
            'bid' => $this->safe_number($ticker, 'b'),
            'bidVolume' => $this->safe_number($ticker, 'bs'),
            'ask' => $this->safe_number($ticker, 'k'),
            'askVolume' => $this->safe_number($ticker, 'ks'),
            'vwap' => null,
            'open' => null,
            'close' => $last,
            'last' => $last,
            'previousClose' => null,
            'change' => null,
            'percentage' => $this->safe_string($ticker, 'c'),
            'average' => null,
            'baseVolume' => $this->safe_string($ticker, 'v'),
            'quoteVolume' => ($quote === 'USD') ? $this->safe_string($ticker, 'vv') : null,
            'info' => $ticker,
        ), $market);
    }

    public function watch_bids_asks(?array $symbols = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbols, $params) {
            /**
             *
             * @see https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#ticker-instrument_name
             *
             * watches best bid & ask for $symbols
             * @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);
            $messageHashes = array();
            $topics = array();
            $marketIds = $this->market_ids($symbols);
            for ($i = 0; $i < count($marketIds); $i++) {
                $marketId = $marketIds[$i];
                $messageHashes[] = 'bidask.' . $symbols[$i];
                $topics[] = 'ticker.' . $marketId;
            }
            $url = $this->urls['api']['ws']['public'];
            $id = $this->nonce();
            $request = array(
                'method' => 'subscribe',
                'params' => array(
                    'channels' => $topics,
                ),
                'nonce' => $id,
            );
            $newTickers = Async\await($this->watch_multiple($url, $messageHashes, $this->extend($request, $params), $messageHashes));
            if ($this->newUpdates) {
                $tickers = array();
                $tickers[$newTickers['symbol']] = $newTickers;
                return $tickers;
            }
            return $this->filter_by_array($this->bidsasks, 'symbol', $symbols);
        }) ();
    }

    public function handle_bid_ask(Client $client, $message) {
        $data = $this->safe_list($message, 'data', array());
        $ticker = $this->safe_dict($data, 0, array());
        $parsedTicker = $this->parse_ws_bid_ask($ticker);
        $symbol = $parsedTicker['symbol'];
        $this->bidsasks[$symbol] = $parsedTicker;
        $messageHash = 'bidask.' . $symbol;
        $client->resolve ($parsedTicker, $messageHash);
    }

    public function parse_ws_bid_ask($ticker, $market = null) {
        $marketId = $this->safe_string($ticker, 'i');
        $market = $this->safe_market($marketId, $market);
        $symbol = $this->safe_string($market, 'symbol');
        $timestamp = $this->safe_integer($ticker, 't');
        return $this->safe_ticker(array(
            'symbol' => $symbol,
            'timestamp' => $timestamp,
            'datetime' => $this->iso8601($timestamp),
            'ask' => $this->safe_string($ticker, 'k'),
            'askVolume' => $this->safe_string($ticker, 'ks'),
            'bid' => $this->safe_string($ticker, 'b'),
            'bidVolume' => $this->safe_string($ticker, 'bs'),
            'info' => $ticker,
        ), $market);
    }

    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
             *
             * @see https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#candlestick-time_frame-instrument_name
             *
             * @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'];
            $interval = $this->safe_string($this->timeframes, $timeframe, $timeframe);
            $messageHash = 'candlestick' . '.' . $interval . '.' . $market['id'];
            $ohlcv = Async\await($this->watch_public($messageHash, $params));
            if ($this->newUpdates) {
                $limit = $ohlcv->getLimit ($symbol, $limit);
            }
            return $this->filter_by_since_limit($ohlcv, $since, $limit, 0, true);
        }) ();
    }

    public function un_watch_ohlcv(string $symbol, $timeframe = '1m', $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbol, $timeframe, $params) {
            /**
             * unWatches historical candlestick data containing the open, high, low, and close price, and the volume of a $market
             *
             * @see https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#candlestick-time_frame-instrument_name
             *
             * @param {string} $symbol unified $symbol of the $market to fetch OHLCV data for
             * @param {string} $timeframe the length of time each candle represents
             * @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'];
            $interval = $this->safe_string($this->timeframes, $timeframe, $timeframe);
            $subMessageHash = 'candlestick' . '.' . $interval . '.' . $market['id'];
            $messageHash = 'unsubscribe:ohlcv:' . $market['symbol'] . ':' . $timeframe;
            $subExtend = array(
                'symbolsAndTimeframes' => [ [ $market['symbol'], $timeframe ] ],
            );
            return Async\await($this->un_watch_public_multiple('ohlcv', [ $market['symbol'] ], array( $messageHash ), array( $subMessageHash ), array( $subMessageHash ), $params, $subExtend));
        }) ();
    }

    public function handle_ohlcv(Client $client, $message) {
        //
        //  {
        //       "instrument_name" => "BTC_USDT",
        //       "subscription" => "candlestick.1m.BTC_USDT",
        //       "channel" => "candlestick",
        //       "depth" => 300,
        //       "interval" => "1m",
        //       "data" => [ [Object] ]
        //   }
        //
        $messageHash = $this->safe_string($message, 'subscription');
        $marketId = $this->safe_string($message, 'instrument_name');
        $market = $this->safe_market($marketId);
        $symbol = $market['symbol'];
        $interval = $this->safe_string($message, 'interval');
        $timeframe = $this->find_timeframe($interval);
        $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;
        }
        $data = $this->safe_value($message, 'data');
        for ($i = 0; $i < count($data); $i++) {
            $tick = $data[$i];
            $parsed = $this->parse_ohlcv($tick, $market);
            $stored->append ($parsed);
        }
        $client->resolve ($stored, $messageHash);
    }

    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
             *
             * @see https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#user-order-instrument_name
             *
             * @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~
             */
            Async\await($this->load_markets());
            $market = null;
            if ($symbol !== null) {
                $market = $this->market($symbol);
                $symbol = $market['symbol'];
            }
            $messageHash = 'user.order';
            $messageHash = ($market !== null) ? ($messageHash . '.' . $market['id']) : $messageHash;
            $orders = Async\await($this->watch_private_subscribe($messageHash, $params));
            if ($this->newUpdates) {
                $limit = $orders->getLimit ($symbol, $limit);
            }
            return $this->filter_by_symbol_since_limit($orders, $symbol, $since, $limit, true);
        }) ();
    }

    public function handle_orders(Client $client, $message, $subscription = null) {
        //
        //    {
        //        "method" => "subscribe",
        //        "result" => {
        //          "instrument_name" => "ETH_CRO",
        //          "subscription" => "user.order.ETH_CRO",
        //          "channel" => "user.order",
        //          "data" => array(
        //            {
        //              "status" => "ACTIVE",
        //              "side" => "BUY",
        //              "price" => 1,
        //              "quantity" => 1,
        //              "order_id" => "366455245775097673",
        //              "client_oid" => "my_order_0002",
        //              "create_time" => 1588758017375,
        //              "update_time" => 1588758017411,
        //              "type" => "LIMIT",
        //              "instrument_name" => "ETH_CRO",
        //              "cumulative_quantity" => 0,
        //              "cumulative_value" => 0,
        //              "avg_price" => 0,
        //              "fee_currency" => "CRO",
        //              "time_in_force":"GOOD_TILL_CANCEL"
        //            }
        //          ),
        //          "channel" => "user.order.ETH_CRO"
        //        }
        //    }
        //
        $channel = $this->safe_string($message, 'channel');
        $symbolSpecificMessageHash = $this->safe_string($message, 'subscription');
        $orders = $this->safe_value($message, 'data', array());
        $ordersLength = count($orders);
        if ($ordersLength > 0) {
            if ($this->orders === null) {
                $limit = $this->safe_integer($this->options, 'ordersLimit', 1000);
                $this->orders = new ArrayCacheBySymbolById ($limit);
            }
            $stored = $this->orders;
            $parsed = $this->parse_orders($orders);
            for ($i = 0; $i < count($parsed); $i++) {
                $stored->append ($parsed[$i]);
            }
            $client->resolve ($stored, $symbolSpecificMessageHash);
            // non-symbol specific
            $client->resolve ($stored, $channel); // $channel might have a symbol-specific suffix
            $client->resolve ($stored, 'user.order');
        }
    }

    public function watch_positions(?array $symbols = null, ?int $since = null, ?int $limit = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbols, $since, $limit, $params) {
            /**
             * watch all open positions
             *
             * @see https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#user-position_balance
             *
             * @param {string[]} [$symbols] list of unified market $symbols to watch positions for
             * @param {int} [$since] the earliest time in ms to fetch positions for
             * @param {int} [$limit] the maximum number of positions to retrieve
             * @param {array} $params extra parameters specific to the exchange API endpoint
             * @return {array[]} a list of {@link https://docs.ccxt.com/en/latest/manual.html#position-structure position structure}
             */
            Async\await($this->load_markets());
            Async\await($this->authenticate());
            $url = $this->urls['api']['ws']['private'];
            $id = $this->nonce();
            $request = array(
                'method' => 'subscribe',
                'params' => array(
                    'channels' => array( 'user.position_balance' ),
                ),
                'nonce' => $id,
            );
            $messageHash = 'positions';
            $symbols = $this->market_symbols($symbols);
            if (!$this->is_empty($symbols)) {
                $messageHash = '::' . implode(',', $symbols);
            }
            $client = $this->client($url);
            $this->set_positions_cache($client, $symbols);
            $fetchPositionsSnapshot = $this->handle_option('watchPositions', 'fetchPositionsSnapshot', true);
            $awaitPositionsSnapshot = $this->handle_option('watchPositions', 'awaitPositionsSnapshot', true);
            if ($fetchPositionsSnapshot && $awaitPositionsSnapshot && $this->positions === null) {
                $snapshot = Async\await($client->future ('fetchPositionsSnapshot'));
                return $this->filter_by_symbols_since_limit($snapshot, $symbols, $since, $limit, true);
            }
            $newPositions = Async\await($this->watch($url, $messageHash, $this->extend($request, $params)));
            if ($this->newUpdates) {
                return $newPositions;
            }
            return $this->filter_by_symbols_since_limit($this->positions, $symbols, $since, $limit, true);
        }) ();
    }

    public function set_positions_cache(Client $client, $type, ?array $symbols = null) {
        $fetchPositionsSnapshot = $this->handle_option('watchPositions', 'fetchPositionsSnapshot', false);
        if ($fetchPositionsSnapshot) {
            $messageHash = 'fetchPositionsSnapshot';
            if (!(is_array($client->futures) && array_key_exists($messageHash, $client->futures))) {
                $client->future ($messageHash);
                $this->spawn(array($this, 'load_positions_snapshot'), $client, $messageHash);
            }
        } else {
            $this->positions = new ArrayCacheBySymbolBySide ();
        }
    }

    public function load_positions_snapshot($client, $messageHash) {
        return Async\async(function () use ($client, $messageHash) {
            $positions = Async\await($this->fetch_positions());
            $this->positions = new ArrayCacheBySymbolBySide ();
            $cache = $this->positions;
            for ($i = 0; $i < count($positions); $i++) {
                $position = $positions[$i];
                $contracts = $this->safe_number($position, 'contracts', 0);
                if ($contracts > 0) {
                    $cache->append ($position);
                }
            }
            // don't remove the $future from the .futures $cache
            $future = $client->futures[$messageHash];
            $future->resolve ($cache);
            $client->resolve ($cache, 'positions');
        }) ();
    }

    public function handle_positions($client, $message) {
        //
        //    {
        //        "subscription" => "user.position_balance",
        //        "channel" => "user.position_balance",
        //        "data" => [array(
        //            "balances" => [array(
        //                "instrument_name" => "USD",
        //                "quantity" => "8.9979961950886",
        //                "update_timestamp_ms" => 1695598760597,
        //            )],
        //            "positions" => [array(
        //                "account_id" => "96a0edb1-afb5-4c7c-af89-5cb610319e2c",
        //                "instrument_name" => "LTCUSD-PERP",
        //                "type" => "PERPETUAL_SWAP",
        //                "quantity" => "1.8",
        //                "cost" => "114.766",
        //                "open_position_pnl" => "-0.0216206",
        //                "session_pnl" => "0.00962994",
        //                "update_timestamp_ms" => 1695598760597,
        //                "open_pos_cost" => "114.766",
        //            )],
        //        )],
        //    }
        //
        // each account is connected to a different endpoint
        // and has exactly one subscriptionhash which is the account type
        $data = $this->safe_value($message, 'data', array());
        $firstData = $this->safe_value($data, 0, array());
        $rawPositions = $this->safe_value($firstData, 'positions', array());
        if ($this->positions === null) {
            $this->positions = new ArrayCacheBySymbolBySide ();
        }
        $cache = $this->positions;
        $newPositions = array();
        for ($i = 0; $i < count($rawPositions); $i++) {
            $rawPosition = $rawPositions[$i];
            $position = $this->parse_position($rawPosition);
            $newPositions[] = $position;
            $cache->append ($position);
        }
        $messageHashes = $this->find_message_hashes($client, 'positions::');
        for ($i = 0; $i < count($messageHashes); $i++) {
            $messageHash = $messageHashes[$i];
            $parts = explode('::', $messageHash);
            $symbolsString = $parts[1];
            $symbols = explode(',', $symbolsString);
            $positions = $this->filter_by_array($newPositions, 'symbol', $symbols, false);
            if (!$this->is_empty($positions)) {
                $client->resolve ($positions, $messageHash);
            }
        }
        $client->resolve ($newPositions, 'positions');
    }

    public function watch_balance($params = array ()): PromiseInterface {
        return Async\async(function () use ($params) {
            /**
             * watch balance and get the amount of funds available for trading or funds locked in orders
             *
             * @see https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#user-balance
             *
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @return {array} a ~@link https://docs.ccxt.com/#/?id=balance-structure balance structure~
             */
            $messageHash = 'user.balance';
            return Async\await($this->watch_private_subscribe($messageHash, $params));
        }) ();
    }

    public function handle_balance(Client $client, $message) {
        //
        //     {
        //         "id" => 1,
        //         "method" => "subscribe",
        //         "code" => 0,
        //         "result" => {
        //             "subscription" => "user.balance",
        //             "channel" => "user.balance",
        //             "data" => array(
        //                 {
        //                     "total_available_balance" => "5.84684368",
        //                     "total_margin_balance" => "5.84684368",
        //                     "total_initial_margin" => "0",
        //                     "total_maintenance_margin" => "0",
        //                     "total_position_cost" => "0",
        //                     "total_cash_balance" => "6.44412101",
        //                     "total_collateral_value" => "5.846843685",
        //                     "total_session_unrealized_pnl" => "0",
        //                     "instrument_name" => "USD",
        //                     "total_session_realized_pnl" => "0",
        //                     "position_balances" => array(
        //                         array(
        //                             "quantity" => "0.0002119875",
        //                             "reserved_qty" => "0",
        //                             "collateral_weight" => "0.9",
        //                             "collateral_amount" => "5.37549592",
        //                             "market_value" => "5.97277325",
        //                             "max_withdrawal_balance" => "0.00021198",
        //                             "instrument_name" => "BTC",
        //                             "hourly_interest_rate" => "0"
        //                         ),
        //                     ),
        //                     "total_effective_leverage" => "0",
        //                     "position_limit" => "3000000",
        //                     "used_position_limit" => "0",
        //                     "total_borrow" => "0",
        //                     "margin_score" => "0",
        //                     "is_liquidating" => false,
        //                     "has_risk" => false,
        //                     "terminatable" => true
        //                 }
        //             )
        //         }
        //     }
        //
        $messageHash = $this->safe_string($message, 'subscription');
        $data = $this->safe_value($message, 'data', array());
        $positionBalances = $this->safe_value($data[0], 'position_balances', array());
        $this->balance['info'] = $data;
        for ($i = 0; $i < count($positionBalances); $i++) {
            $balance = $positionBalances[$i];
            $currencyId = $this->safe_string($balance, 'instrument_name');
            $code = $this->safe_currency_code($currencyId);
            $account = $this->account();
            $account['total'] = $this->safe_string($balance, 'quantity');
            $account['used'] = $this->safe_string($balance, 'reserved_qty');
            $this->balance[$code] = $account;
            $this->balance = $this->safe_balance($this->balance);
        }
        $client->resolve ($this->balance, $messageHash);
        $messageHashRequest = $this->safe_string($message, 'id');
        $client->resolve ($this->balance, $messageHashRequest);
    }

    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) {
            /**
             *
             * @see https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#private-create-order
             *
             * create a trade order
             * @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
             * @return {array} an ~@link https://docs.ccxt.com/#/?id=order-structure order structure~
             */
            Async\await($this->load_markets());
            $params = $this->create_order_request($symbol, $type, $side, $amount, $price, $params);
            $request = array(
                'method' => 'private/create-order',
                'params' => $params,
            );
            $messageHash = $this->nonce();
            return Async\await($this->watch_private_request($messageHash, $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://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#private-amend-order
             *
             * @param {string} $id order $id
             * @param {string} $symbol unified market $symbol of the order to edit
             * @param {string} [$type] not used by cryptocom editOrder
             * @param {string} [$side] not used by cryptocom editOrder
             * @param {float} $amount (mandatory) how much of the currency you want to trade in units of the base currency
             * @param {float} $price (mandatory) the $price for the order, in units of the quote currency, ignored in market orders
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @param {string} [$params->clientOrderId] the original client order $id of the order to edit, required if $id is not provided
             * @return {array} an ~@link https://docs.ccxt.com/#/?$id=order-structure order structure~
             */
            Async\await($this->load_markets());
            $params = $this->edit_order_request($id, $symbol, $amount, $price, $params);
            $request = array(
                'method' => 'private/amend-order',
                'params' => $params,
            );
            $messageHash = $this->nonce();
            return Async\await($this->watch_private_request($messageHash, $request));
        }) ();
    }

    public function handle_order(Client $client, $message) {
        //
        //    {
        //        "id" => 1,
        //        "method" => "private/create-$order",
        //        "code" => 0,
        //        "result" => {
        //            "client_oid" => "c5f682ed-7108-4f1c-b755-972fcdca0f02",
        //            "order_id" => "18342311"
        //        }
        //    }
        //
        $messageHash = $this->safe_string($message, 'id');
        $rawOrder = $this->safe_value($message, 'result', array());
        $order = $this->parse_order($rawOrder);
        $client->resolve ($order, $messageHash);
    }

    public function cancel_order_ws(string $id, ?string $symbol = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($id, $symbol, $params) {
            /**
             * cancels an open order
             *
             * @see https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#private-cancel-order
             *
             * @param {string} $id the order $id of the order to cancel
             * @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~
             */
            Async\await($this->load_markets());
            $params = $this->extend(array(
                'order_id' => $id,
            ), $params);
            $request = array(
                'method' => 'private/cancel-order',
                'params' => $params,
            );
            $messageHash = $this->nonce();
            return Async\await($this->watch_private_request($messageHash, $request));
        }) ();
    }

    public function cancel_all_orders_ws(?string $symbol = null, $params = array ()) {
        return Async\async(function () use ($symbol, $params) {
            /**
             * cancel all open orders
             *
             * @see https://exchange-docs.crypto.com/exchange/v1/rest-ws/index.html#private-cancel-all-orders
             *
             * @param {string} $symbol unified $market $symbol of the orders to cancel
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @return {array} Returns exchange raw message array(@link https://docs.ccxt.com/#/?id=order-structure)
             */
            Async\await($this->load_markets());
            $market = null;
            $request = array(
                'method' => 'private/cancel-all-orders',
                'params' => $this->extend(array(), $params),
            );
            if ($symbol !== null) {
                $market = $this->market($symbol);
                $request['params']['instrument_name'] = $market['id'];
            }
            $messageHash = $this->nonce();
            return Async\await($this->watch_private_request($messageHash, $request));
        }) ();
    }

    public function handle_cancel_all_orders(Client $client, $message) {
        //
        //    {
        //        "id" => 1688914586647,
        //        "method" => "private/cancel-all-orders",
        //        "code" => 0
        //    }
        //
        $messageHash = $this->safe_string($message, 'id');
        $client->resolve ($message, $messageHash);
    }

    public function watch_public($messageHash, $params = array ()) {
        return Async\async(function () use ($messageHash, $params) {
            $url = $this->urls['api']['ws']['public'];
            $id = $this->nonce();
            $request = array(
                'method' => 'subscribe',
                'params' => array(
                    'channels' => array( $messageHash ),
                ),
                'nonce' => $id,
            );
            $message = $this->extend($request, $params);
            return Async\await($this->watch($url, $messageHash, $message, $messageHash));
        }) ();
    }

    public function watch_public_multiple($messageHashes, $topics, $params = array ()) {
        return Async\async(function () use ($messageHashes, $topics, $params) {
            $url = $this->urls['api']['ws']['public'];
            $id = $this->nonce();
            $request = array(
                'method' => 'subscribe',
                'params' => array(
                    'channels' => $topics,
                ),
                'nonce' => $id,
            );
            $message = $this->deep_extend($request, $params);
            return Async\await($this->watch_multiple($url, $messageHashes, $message, $messageHashes));
        }) ();
    }

    public function un_watch_public_multiple(string $topic, array $symbols, array $messageHashes, array $subMessageHashes, array $topics, $params = array (), $subExtend = array ()) {
        return Async\async(function () use ($topic, $symbols, $messageHashes, $subMessageHashes, $topics, $params, $subExtend) {
            $url = $this->urls['api']['ws']['public'];
            $id = $this->nonce();
            $request = array(
                'method' => 'unsubscribe',
                'params' => array(
                    'channels' => $topics,
                ),
                'nonce' => $id,
                'id' => (string) $id,
            );
            $subscription = array(
                'id' => (string) $id,
                'topic' => $topic,
                'symbols' => $symbols,
                'subMessageHashes' => $subMessageHashes,
                'messageHashes' => $messageHashes,
            );
            $message = $this->deep_extend($request, $params);
            return Async\await($this->watch_multiple($url, $messageHashes, $message, $messageHashes, $this->extend($subscription, $subExtend)));
        }) ();
    }

    public function watch_private_request($nonce, $params = array ()) {
        return Async\async(function () use ($nonce, $params) {
            Async\await($this->authenticate());
            $url = $this->urls['api']['ws']['private'];
            $request = array(
                'id' => $nonce,
                'nonce' => $nonce,
            );
            $message = $this->extend($request, $params);
            return Async\await($this->watch($url, (string) $nonce, $message, true));
        }) ();
    }

    public function watch_private_subscribe($messageHash, $params = array ()) {
        return Async\async(function () use ($messageHash, $params) {
            Async\await($this->authenticate());
            $url = $this->urls['api']['ws']['private'];
            $id = $this->nonce();
            $request = array(
                'method' => 'subscribe',
                'params' => array(
                    'channels' => array( $messageHash ),
                ),
                'nonce' => $id,
            );
            $message = $this->extend($request, $params);
            return Async\await($this->watch($url, $messageHash, $message, $messageHash));
        }) ();
    }

    public function handle_error_message(Client $client, $message): Bool {
        //
        //    {
        //        "id" => 0,
        //        "code" => 10004,
        //        "method" => "subscribe",
        //        "message" => "invalid channel array("channels":["trade.BTCUSD-PERP"])"
        //    }
        //
        $id = $this->safe_string($message, 'id');
        $errorCode = $this->safe_string($message, 'code');
        try {
            if ($errorCode && $errorCode !== '0') {
                $feedback = $this->id . ' ' . $this->json($message);
                $this->throw_exactly_matched_exception($this->exceptions['exact'], $errorCode, $feedback);
                $messageString = $this->safe_value($message, 'message');
                if ($messageString !== null) {
                    $this->throw_broadly_matched_exception($this->exceptions['broad'], $messageString, $feedback);
                }
                throw new ExchangeError($feedback);
            }
            return false;
        } catch (Exception $e) {
            if ($e instanceof AuthenticationError) {
                $messageHash = 'authenticated';
                $client->reject ($e, $messageHash);
                if (is_array($client->subscriptions) && array_key_exists($messageHash, $client->subscriptions)) {
                    unset($client->subscriptions[$messageHash]);
                }
            } else {
                $client->reject ($e, $id);
            }
            return true;
        }
    }

    public function handle_subscribe(Client $client, $message) {
        $methods = array(
            'candlestick' => array($this, 'handle_ohlcv'),
            'ticker' => array($this, 'handle_ticker'),
            'trade' => array($this, 'handle_trades'),
            'book' => array($this, 'handle_order_book'),
            'book.update' => array($this, 'handle_order_book'),
            'user.order' => array($this, 'handle_orders'),
            'user.trade' => array($this, 'handle_trades'),
            'user.balance' => array($this, 'handle_balance'),
            'user.position_balance' => array($this, 'handle_positions'),
        );
        $result = $this->safe_value_2($message, 'result', 'info');
        $channel = $this->safe_string($result, 'channel');
        if (($channel !== null) && mb_strpos($channel, 'user.trade') > -1) {
            // $channel might be user.trade.BTC_USDT
            $this->handle_trades($client, $result);
        }
        if (($channel !== null) && str_starts_with($channel, 'user.order')) {
            // $channel might be user.order.BTC_USDT
            $this->handle_orders($client, $result);
        }
        $method = $this->safe_value($methods, $channel);
        if ($method !== null) {
            $method($client, $result);
        }
    }

    public function handle_message(Client $client, $message) {
        //
        // ping
        //    {
        //        "id" => 1587523073344,
        //        "method" => "public/heartbeat",
        //        "code" => 0
        //    }
        // auth
        //     array( id => 1648132625434, $method => "public/auth", code => 0 )
        // ohlcv
        //    {
        //        "code" => 0,
        //        "method" => "subscribe",
        //        "result" => {
        //          "instrument_name" => "BTC_USDT",
        //          "subscription" => "candlestick.1m.BTC_USDT",
        //          "channel" => "candlestick",
        //          "depth" => 300,
        //          "interval" => "1m",
        //          "data" => [ [Object] ]
        //        }
        //      }
        // ticker
        //    {
        //        "info":{
        //           "instrument_name":"BTC_USDT",
        //           "subscription":"ticker.BTC_USDT",
        //           "channel":"ticker",
        //           "data":array( array( ) )
        //
        // handle unsubscribe
        // array("id":1725448572836,"method":"unsubscribe","code":0)
        //
        if ($this->handle_error_message($client, $message)) {
            return;
        }
        $method = $this->safe_string($message, 'method');
        $methods = array(
            '' => array($this, 'handle_ping'),
            'public/heartbeat' => array($this, 'handle_ping'),
            'public/auth' => array($this, 'handle_authenticate'),
            'private/create-order' => array($this, 'handle_order'),
            'private/amend-order' => array($this, 'handle_order'),
            'private/cancel-order' => array($this, 'handle_order'),
            'private/cancel-all-orders' => array($this, 'handle_cancel_all_orders'),
            'private/close-position' => array($this, 'handle_order'),
            'subscribe' => array($this, 'handle_subscribe'),
            'unsubscribe' => array($this, 'handle_unsubscribe'),
        );
        $callMethod = $this->safe_value($methods, $method);
        if ($callMethod !== null) {
            $callMethod($client, $message);
        }
    }

    public function authenticate($params = array ()) {
        return Async\async(function () use ($params) {
            $this->check_required_credentials();
            $url = $this->urls['api']['ws']['private'];
            $client = $this->client($url);
            $messageHash = 'authenticated';
            $future = $client->reusableFuture ($messageHash);
            $authenticated = $this->safe_value($client->subscriptions, $messageHash);
            if ($authenticated === null) {
                $method = 'public/auth';
                $nonce = (string) $this->nonce();
                $auth = $method . $nonce . $this->apiKey . $nonce;
                $signature = $this->hmac($this->encode($auth), $this->encode($this->secret), 'sha256');
                $request = array(
                    'id' => $nonce,
                    'nonce' => $nonce,
                    'method' => $method,
                    'api_key' => $this->apiKey,
                    'sig' => $signature,
                );
                $message = $this->extend($request, $params);
                $this->watch($url, $messageHash, $message, $messageHash);
            }
            return Async\await($future);
        }) ();
    }

    public function handle_ping(Client $client, $message) {
        $this->spawn(array($this, 'pong'), $client, $message);
    }

    public function handle_authenticate(Client $client, $message) {
        //
        //  array( id => 1648132625434, method => "public/auth", code => 0 )
        //
        $future = $this->safe_value($client->futures, 'authenticated');
        $future->resolve (true);
    }

    public function handle_unsubscribe(Client $client, $message) {
        $id = $this->safe_string($message, 'id');
        $keys = is_array($client->subscriptions) ? array_keys($client->subscriptions) : array();
        for ($i = 0; $i < count($keys); $i++) {
            $messageHash = $keys[$i];
            if (!(is_array($client->subscriptions) && array_key_exists($messageHash, $client->subscriptions))) {
                continue;
                // the previous iteration can have deleted the $messageHash from the subscriptions
            }
            if (str_starts_with($messageHash, 'unsubscribe')) {
                $subscription = $client->subscriptions[$messageHash];
                $subId = $this->safe_string($subscription, 'id');
                if ($id !== $subId) {
                    continue;
                }
                $messageHashes = $this->safe_list($subscription, 'messageHashes', array());
                $subMessageHashes = $this->safe_list($subscription, 'subMessageHashes', array());
                for ($j = 0; $j < count($messageHashes); $j++) {
                    $unsubHash = $messageHashes[$j];
                    $subHash = $subMessageHashes[$j];
                    $this->clean_unsubscription($client, $subHash, $unsubHash);
                }
                $this->clean_cache($subscription);
            }
        }
    }
}
