<?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\ArgumentsRequired;
use ccxt\BadRequest;
use ccxt\NotSupported;
use ccxt\ChecksumError;
use ccxt\Precise;
use \React\Async;
use \React\Promise\PromiseInterface;

class gate extends \ccxt\async\gate {

    public function describe(): mixed {
        return $this->deep_extend(parent::describe(), array(
            'has' => array(
                'ws' => true,
                'cancelAllOrdersWs' => true,
                'cancelOrderWs' => true,
                'createMarketBuyOrderWithCostWs' => true,
                'createMarketOrderWs' => true,
                'createMarketOrderWithCostWs' => false,
                'createMarketSellOrderWithCostWs' => false,
                'createOrderWs' => true,
                'createOrdersWs' => true,
                'createPostOnlyOrderWs' => true,
                'createReduceOnlyOrderWs' => true,
                'createStopLimitOrderWs' => true,
                'createStopLossOrderWs' => true,
                'createStopMarketOrderWs' => false,
                'createStopOrderWs' => true,
                'createTakeProfitOrderWs' => true,
                'createTriggerOrderWs' => true,
                'editOrderWs' => true,
                'fetchOrderWs' => true,
                'fetchOrdersWs' => false,
                'fetchOpenOrdersWs' => true,
                'fetchClosedOrdersWs' => true,
                'watchOrderBook' => true,
                'watchBidsAsks' => true,
                'watchTicker' => true,
                'watchTickers' => true,
                'watchTrades' => true,
                'watchTradesForSymbols' => true,
                'watchMyTrades' => true,
                'watchOHLCV' => true,
                'watchBalance' => true,
                'watchOrders' => true,
                'watchLiquidations' => false,
                'watchLiquidationsForSymbols' => false,
                'watchMyLiquidations' => true,
                'watchMyLiquidationsForSymbols' => true,
                'watchPositions' => true,
            ),
            'urls' => array(
                'api' => array(
                    'ws' => 'wss://ws.gate.io/v4',
                    'spot' => 'wss://api.gateio.ws/ws/v4/',
                    'swap' => array(
                        'usdt' => 'wss://fx-ws.gateio.ws/v4/ws/usdt',
                        'btc' => 'wss://fx-ws.gateio.ws/v4/ws/btc',
                    ),
                    'future' => array(
                        'usdt' => 'wss://fx-ws.gateio.ws/v4/ws/delivery/usdt',
                        'btc' => 'wss://fx-ws.gateio.ws/v4/ws/delivery/btc',
                    ),
                    'option' => array(
                        'usdt' => 'wss://op-ws.gateio.live/v4/ws/usdt',
                        'btc' => 'wss://op-ws.gateio.live/v4/ws/btc',
                    ),
                ),
                'test' => array(
                    'swap' => array(
                        'usdt' => 'wss://fx-ws-testnet.gateio.ws/v4/ws/usdt',
                        'btc' => 'wss://fx-ws-testnet.gateio.ws/v4/ws/btc',
                    ),
                    'future' => array(
                        'usdt' => 'wss://fx-ws-testnet.gateio.ws/v4/ws/usdt',
                        'btc' => 'wss://fx-ws-testnet.gateio.ws/v4/ws/btc',
                    ),
                    'option' => array(
                        'usdt' => 'wss://op-ws-testnet.gateio.live/v4/ws/usdt',
                        'btc' => 'wss://op-ws-testnet.gateio.live/v4/ws/btc',
                    ),
                ),
            ),
            'options' => array(
                'tradesLimit' => 1000,
                'OHLCVLimit' => 1000,
                'watchTradesSubscriptions' => array(),
                'watchTickerSubscriptions' => array(),
                'watchOrderBookSubscriptions' => array(),
                'watchTicker' => array(
                    'name' => 'tickers', // or book_ticker
                ),
                'watchOrderBook' => array(
                    'interval' => '100ms',
                    'snapshotDelay' => 10, // how many deltas to cache before fetching a snapshot
                    'snapshotMaxRetries' => 3,
                    'checksum' => true,
                ),
                'watchBalance' => array(
                    'settle' => 'usdt', // or btc
                    'spot' => 'spot.balances', // spot.margin_balances, spot.funding_balances or spot.cross_balances
                ),
                'watchPositions' => array(
                    'fetchPositionsSnapshot' => true, // or false
                    'awaitPositionsSnapshot' => true, // whether to wait for the positions snapshot before providing updates
                ),
            ),
            'exceptions' => array(
                'ws' => array(
                    'exact' => array(
                        '1' => '\\ccxt\\BadRequest',
                        '2' => '\\ccxt\\BadRequest',
                        '4' => '\\ccxt\\AuthenticationError',
                        '6' => '\\ccxt\\AuthenticationError',
                        '11' => '\\ccxt\\AuthenticationError',
                    ),
                    'broad' => array(),
                ),
            ),
        ));
    }

    public function create_order_ws(string $symbol, string $type, string $side, float $amount, ?float $price = null, $params = array ()) {
        return Async\async(function () use ($symbol, $type, $side, $amount, $price, $params) {
            /**
             *
             * @see https://www.gate.io/docs/developers/apiv4/ws/en/#$order-place
             * @see https://www.gate.io/docs/developers/futures/ws/en/#$order-place
             *
             * Create an $order on the exchange
             * @param {string} $symbol Unified CCXT $market $symbol
             * @param {string} $type 'limit' or 'market' *"market" is contract only*
             * @param {string} $side 'buy' or 'sell'
             * @param {float} $amount the $amount of currency to trade
             * @param {float} [$price] *ignored in "market" orders* the $price at which the $order is to be fulfilled at in units of the quote currency
             * @param {array} [$params]  extra parameters specific to the exchange API endpoint
             * @param {float} [$params->stopPrice] The $price at which a trigger $order is triggered at
             * @param {string} [$params->timeInForce] "GTC", "IOC", or "PO"
             * @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->marginMode] 'cross' or 'isolated' - marginMode for margin trading if not provided $this->options['defaultMarginMode'] is used
             * @param {int} [$params->iceberg] Amount to display for the iceberg $order, Null or 0 for normal orders, Set to -1 to hide the $order completely
             * @param {string} [$params->text] User defined information
             * @param {string} [$params->account] *spot and margin only* "spot", "margin" or "cross_margin"
             * @param {bool} [$params->auto_borrow] *margin only* Used in margin or cross margin trading to allow automatic loan of insufficient $amount if balance is not enough
             * @param {string} [$params->settle] *contract only* Unified Currency Code for settle currency
             * @param {bool} [$params->reduceOnly] *contract only* Indicates if this $order is to reduce the size of a position
             * @param {bool} [$params->close] *contract only* Set to close the position, with size set to 0
             * @param {bool} [$params->auto_size] *contract only* Set $side to close dual-mode position, close_long closes the long $side, while close_short the short one, size also needs to be set to 0
             * @param {int} [$params->price_type] *contract only* 0 latest deal $price, 1 mark $price, 2 index $price
             * @param {float} [$params->cost] *spot $market buy only* the quote quantity that can be used alternative for the $amount
             * @return {array|null} ~@link https://docs.ccxt.com/#/?id=$order-structure An $order structure~
             */
            Async\await($this->load_markets());
            $market = $this->market($symbol);
            $symbol = $market['symbol'];
            $messageType = $this->get_type_by_market($market);
            $channel = $messageType . '.order_place';
            $url = $this->get_url_by_market($market);
            $params['textIsRequired'] = true;
            $request = $this->create_order_request($symbol, $type, $side, $amount, $price, $params);
            Async\await($this->authenticate($url, $messageType));
            $rawOrder = Async\await($this->request_private($url, $request, $channel));
            $order = $this->parse_order($rawOrder, $market);
            return $order;
        }) ();
    }

    public function create_orders_ws(array $orders, $params = array ()) {
        return Async\async(function () use ($orders, $params) {
            /**
             * create a list of trade $orders
             *
             * @see https://www.gate.io/docs/developers/futures/ws/en/#order-batch-place
             *
             * @param {Array} $orders list of $orders to create, each object should contain the parameters required by createOrder, namely symbol, type, side, amount, price and $params
             * @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());
            $request = $this->createOrdersRequest ($orders, $params);
            $firstOrder = $orders[0];
            $market = $this->market($firstOrder['symbol']);
            if ($market['swap'] !== true) {
                throw new NotSupported($this->id . ' createOrdersWs is not supported for swap markets');
            }
            $messageType = $this->get_type_by_market($market);
            $channel = $messageType . '.order_batch_place';
            $url = $this->get_url_by_market($market);
            Async\await($this->authenticate($url, $messageType));
            $rawOrders = Async\await($this->request_private($url, $request, $channel));
            return $this->parse_orders($rawOrders, $market);
        }) ();
    }

    public function cancel_all_orders_ws(?string $symbol = null, $params = array ()) {
        return Async\async(function () use ($symbol, $params) {
            /**
             * cancel all open orders
             *
             * @see https://www.gate.io/docs/developers/futures/ws/en/#cancel-all-open-orders-matched
             * @see https://www.gate.io/docs/developers/apiv4/ws/en/#order-cancel-all-with-specified-currency-pair
             *
             * @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 exchange API endpoint
             * @param {string} [$params->channel] the $channel to use, defaults to spot.order_cancel_cp or futures.order_cancel_cp
             * @return {array[]} a list of ~@link https://docs.ccxt.com/#/?id=order-structure order structures~
             */
            Async\await($this->load_markets());
            $market = ($symbol === null) ? null : $this->market($symbol);
            $trigger = $this->safe_bool_2($params, 'stop', 'trigger');
            $messageType = $this->get_type_by_market($market);
            $channel = $messageType . '.order_cancel_cp';
            list($channel, $params) = $this->handle_option_and_params($params, 'cancelAllOrdersWs', 'channel', $channel);
            $url = $this->get_url_by_market($market);
            $params = $this->omit($params, array( 'stop', 'trigger' ));
            list($type, $query) = $this->handle_market_type_and_params('cancelAllOrders', $market, $params);
            list($request, $requestParams) = ($type === 'spot') ? $this->multiOrderSpotPrepareRequest ($market, $trigger, $query) : $this->prepareRequest ($market, $type, $query);
            Async\await($this->authenticate($url, $messageType));
            $rawOrders = Async\await($this->request_private($url, $this->extend($request, $requestParams), $channel));
            return $this->parse_orders($rawOrders, $market);
        }) ();
    }

    public function cancel_order_ws(string $id, ?string $symbol = null, $params = array ()) {
        return Async\async(function () use ($id, $symbol, $params) {
            /**
             * Cancels an open order
             *
             * @see https://www.gate.io/docs/developers/apiv4/ws/en/#order-cancel
             * @see https://www.gate.io/docs/developers/futures/ws/en/#order-cancel
             *
             * @param {string} $id Order $id
             * @param {string} $symbol Unified $market $symbol
             * @param {array} [$params] Parameters specified by the exchange api
             * @param {bool} [$params->trigger] True if the order to be cancelled is a $trigger order
             * @return An ~@link https://docs.ccxt.com/#/?$id=order-structure order structure~
             */
            Async\await($this->load_markets());
            $market = ($symbol === null) ? null : $this->market($symbol);
            $trigger = $this->safe_value_n($params, array( 'is_stop_order', 'stop', 'trigger' ), false);
            $params = $this->omit($params, array( 'is_stop_order', 'stop', 'trigger' ));
            list($type, $query) = $this->handle_market_type_and_params('cancelOrder', $market, $params);
            list($request, $requestParams) = ($type === 'spot' || $type === 'margin') ? $this->spotOrderPrepareRequest ($market, $trigger, $query) : $this->prepareRequest ($market, $type, $query);
            $messageType = $this->get_type_by_market($market);
            $channel = $messageType . '.order_cancel';
            $url = $this->get_url_by_market($market);
            Async\await($this->authenticate($url, $messageType));
            $request['order_id'] = (string) $id;
            $res = Async\await($this->request_private($url, $this->extend($request, $requestParams), $channel));
            return $this->parse_order($res, $market);
        }) ();
    }

    public function edit_order_ws(string $id, string $symbol, string $type, string $side, ?float $amount = null, ?float $price = null, $params = array ()) {
        return Async\async(function () use ($id, $symbol, $type, $side, $amount, $price, $params) {
            /**
             * edit a trade order, gate currently only supports the modification of the $price or $amount fields
             *
             * @see https://www.gate.io/docs/developers/apiv4/ws/en/#order-amend
             * @see https://www.gate.io/docs/developers/futures/ws/en/#order-amend
             *
             * @param {string} $id 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 the currency you want to trade in units of the 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());
            $market = $this->market($symbol);
            $extendedRequest = $this->edit_order_request($id, $symbol, $type, $side, $amount, $price, $params);
            $messageType = $this->get_type_by_market($market);
            $channel = $messageType . '.order_amend';
            $url = $this->get_url_by_market($market);
            Async\await($this->authenticate($url, $messageType));
            $rawOrder = Async\await($this->request_private($url, $extendedRequest, $channel));
            return $this->parse_order($rawOrder, $market);
        }) ();
    }

    public function fetch_order_ws(string $id, ?string $symbol = null, $params = array ()) {
        return Async\async(function () use ($id, $symbol, $params) {
            /**
             * Retrieves information on an order
             *
             * @see https://www.gate.io/docs/developers/apiv4/ws/en/#order-status
             * @see https://www.gate.io/docs/developers/futures/ws/en/#order-status
             *
             * @param {string} $id Order $id
             * @param {string} $symbol Unified $market $symbol, *required for spot and margin*
             * @param {array} [$params] Parameters specified by the exchange api
             * @param {bool} [$params->trigger] True if the order being fetched is a trigger order
             * @param {string} [$params->marginMode] 'cross' or 'isolated' - marginMode for margin trading if not provided $this->options['defaultMarginMode'] is used
             * @param {string} [$params->type] 'spot', 'swap', or 'future', if not provided $this->options['defaultMarginMode'] is used
             * @param {string} [$params->settle] 'btc' or 'usdt' - settle currency for perpetual swap and future - $market settle currency is used if $symbol !== null, default="usdt" for swap and "btc" for future
             * @return An ~@link https://docs.ccxt.com/#/?$id=order-structure order structure~
             */
            Async\await($this->load_markets());
            $market = ($symbol === null) ? null : $this->market($symbol);
            list($request, $requestParams) = $this->fetchOrderRequest ($id, $symbol, $params);
            $messageType = $this->get_type_by_market($market);
            $channel = $messageType . '.order_status';
            $url = $this->get_url_by_market($market);
            Async\await($this->authenticate($url, $messageType));
            $rawOrder = Async\await($this->request_private($url, $this->extend($request, $requestParams), $channel));
            return $this->parse_order($rawOrder, $market);
        }) ();
    }

    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
             *
             * @see https://www.gate.io/docs/developers/futures/ws/en/#order-list
             *
             * @param {string} $symbol unified market $symbol
             * @param {int} [$since] the earliest time in ms to fetch open orders for
             * @param {int} [$limit] the maximum number of  open orders structures to retrieve
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @return {Order[]} a list of ~@link https://docs.ccxt.com/#/?id=order-structure order structures~
             */
            return Async\await($this->fetch_orders_by_status_ws('open', $symbol, $since, $limit, $params));
        }) ();
    }

    public function fetch_closed_orders_ws(?string $symbol = null, ?int $since = null, ?int $limit = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbol, $since, $limit, $params) {
            /**
             * fetches information on multiple closed orders made by the user
             *
             * @see https://www.gate.io/docs/developers/futures/ws/en/#order-list
             *
             * @param {string} $symbol unified market $symbol of the market orders were made in
             * @param {int} [$since] the earliest time in ms to fetch orders for
             * @param {int} [$limit] the maximum number of order structures to retrieve
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @return {Order[]} a list of ~@link https://docs.ccxt.com/#/?id=order-structure order structures~
             */
            return Async\await($this->fetch_orders_by_status_ws('finished', $symbol, $since, $limit, $params));
        }) ();
    }

    public function fetch_orders_by_status_ws(string $status, ?string $symbol = null, ?int $since = null, ?int $limit = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($status, $symbol, $since, $limit, $params) {
            /**
             *
             * @see https://www.gate.io/docs/developers/futures/ws/en/#order-list
             *
             * fetches information on multiple $orders made by the user by $status
             * @param {string} $status requested order $status
             * @param {string} $symbol unified $market $symbol of the $market $orders were made in
             * @param {int|null} [$since] the earliest time in ms to fetch $orders for
             * @param {int|null} [$limit] the maximum number of order structures to retrieve
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @param {int} [$params->orderId] order id to begin at
             * @param {int} [$params->limit] the maximum number of order structures to retrieve
             * @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'];
                if ($market['swap'] !== true) {
                    throw new NotSupported($this->id . ' fetchOrdersByStatusWs is only supported by swap markets. Use rest API for other markets');
                }
            }
            list($request, $requestParams) = $this->prepareOrdersByStatusRequest ($status, $symbol, $since, $limit, $params);
            $newRequest = $this->omit($request, array( 'settle' ));
            $messageType = $this->get_type_by_market($market);
            $channel = $messageType . '.order_list';
            $url = $this->get_url_by_market($market);
            Async\await($this->authenticate($url, $messageType));
            $rawOrders = Async\await($this->request_private($url, $this->extend($newRequest, $requestParams), $channel));
            $orders = $this->parse_orders($rawOrders, $market);
            return $this->filter_by_symbol_since_limit($orders, $symbol, $since, $limit);
        }) ();
    }

    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://www.gate.com/docs/developers/apiv4/ws/en/#order-book-$channel
             * @see https://www.gate.com/docs/developers/apiv4/ws/en/#order-book-v2-api
             * @see https://www.gate.com/docs/developers/futures/ws/en/#order-book-api
             * @see https://www.gate.com/docs/developers/futures/ws/en/#order-book-v2-api
             * @see https://www.gate.com/docs/developers/delivery/ws/en/#order-book-api
             *
             * @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'];
            $marketId = $market['id'];
            list($interval, $query) = $this->handle_option_and_params($params, 'watchOrderBook', 'interval', '100ms');
            $messageType = $this->get_type_by_market($market);
            $channel = $messageType . '.order_book_update';
            $messageHash = 'orderbook' . ':' . $symbol;
            $url = $this->get_url_by_market($market);
            $payload = array( $marketId, $interval );
            if ($limit === null) {
                $limit = 100; // max 100 atm
            }
            if ($market['contract']) {
                $stringLimit = (string) $limit;
                $payload[] = $stringLimit;
            }
            $subscription = array(
                'symbol' => $symbol,
                'limit' => $limit,
            );
            $orderbook = Async\await($this->subscribe_public($url, $messageHash, $payload, $channel, $query, $subscription));
            return $orderbook->limit ();
        }) ();
    }

    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
             * @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
             * @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'];
            $marketId = $market['id'];
            $interval = '100ms';
            list($interval, $params) = $this->handle_option_and_params($params, 'watchOrderBook', 'interval', $interval);
            $messageType = $this->get_type_by_market($market);
            $channel = $messageType . '.order_book_update';
            $subMessageHash = 'orderbook' . ':' . $symbol;
            $messageHash = 'unsubscribe:orderbook' . ':' . $symbol;
            $url = $this->get_url_by_market($market);
            $payload = array( $marketId, $interval );
            $limit = $this->safe_integer($params, 'limit', 100);
            if ($market['contract']) {
                $stringLimit = (string) $limit;
                $payload[] = $stringLimit;
            }
            return Async\await($this->un_subscribe_public_multiple($url, 'orderbook', array( $symbol ), array( $messageHash ), array( $subMessageHash ), $payload, $channel, $params));
        }) ();
    }

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

    public function handle_order_book(Client $client, $message) {
        //
        // spot
        //
        //     {
        //         "time" => 1650189272,
        //         "channel" => "spot.order_book_update",
        //         "event" => "update",
        //         "result" => {
        //             "t" => 1650189272515,
        //             "e" => "depthUpdate",
        //             "E" => 1650189272,
        //             "s" => "GMT_USDT",
        //             "U" => 140595902,
        //             "u" => 140595902,
        //             "b" => array(
        //                 array( '2.51518', "228.119" ),
        //                 array( '2.50587', "1510.11" ),
        //                 array( '2.49944', "67.6" ),
        //             ),
        //             "a" => array(
        //                 array( '2.5182', "4.199" ),
        //                 array( "2.51926", "1874" ),
        //                 array( '2.53528', "96.529" ),
        //             )
        //         }
        //     }
        //
        // swap
        //
        //     {
        //         "id" => null,
        //         "time" => 1650188898,
        //         "channel" => "futures.order_book_update",
        //         "event" => "update",
        //         "error" => null,
        //         "result" => {
        //             "t" => 1650188898938,
        //             "s" => "GMT_USDT",
        //             "U" => 1577718307,
        //             "u" => 1577719254,
        //             "b" => array(
        //                 array( p => "2.5178", s => 0 ),
        //                 array( p => "2.5179", s => 0 ),
        //                 array( p => "2.518", s => 0 ),
        //             ),
        //             "a" => array(
        //                 array( p => "2.52", s => 0 ),
        //                 array( p => "2.5201", s => 0 ),
        //                 array( p => "2.5203", s => 0 ),
        //             )
        //         }
        //     }
        //
        $channel = $this->safe_string($message, 'channel');
        $channelParts = explode('.', $channel);
        $rawMarketType = $this->safe_string($channelParts, 0);
        $isSpot = $rawMarketType === 'spot';
        $marketType = $isSpot ? 'spot' : 'contract';
        $delta = $this->safe_value($message, 'result');
        $deltaStart = $this->safe_integer($delta, 'U');
        $deltaEnd = $this->safe_integer($delta, 'u');
        $marketId = $this->safe_string($delta, 's');
        $symbol = $this->safe_symbol($marketId, null, '_', $marketType);
        $messageHash = 'orderbook:' . $symbol;
        $storedOrderBook = $this->safe_value($this->orderbooks, $symbol, $this->order_book(array()));
        $nonce = $this->safe_integer($storedOrderBook, 'nonce');
        if ($nonce === null) {
            $cacheLength = 0;
            if ($storedOrderBook !== null) {
                $cacheLength = count($storedOrderBook->cache);
            }
            $snapshotDelay = $this->handle_option('watchOrderBook', 'snapshotDelay', 10);
            $waitAmount = $isSpot ? $snapshotDelay : 0;
            if ($cacheLength === $waitAmount) {
                // max $limit is 100
                $subscription = $client->subscriptions[$messageHash];
                $limit = $this->safe_integer($subscription, 'limit');
                $this->spawn(array($this, 'load_order_book'), $client, $messageHash, $symbol, $limit, array()); // needed for c#, number of args needs to match
            }
            $storedOrderBook->cache[] = $delta;
            return;
        } elseif ($nonce >= $deltaEnd) {
            return;
        } elseif ($nonce >= $deltaStart - 1) {
            $this->handle_delta($storedOrderBook, $delta);
        } else {
            unset($client->subscriptions[$messageHash]);
            unset($this->orderbooks[$symbol]);
            $checksum = $this->handle_option('watchOrderBook', 'checksum', true);
            if ($checksum) {
                $error = new ChecksumError ($this->id . ' ' . $this->orderbook_checksum_message($symbol));
                $client->reject ($error, $messageHash);
            }
        }
        $client->resolve ($storedOrderBook, $messageHash);
    }

    public function get_cache_index($orderBook, $cache) {
        $nonce = $this->safe_integer($orderBook, 'nonce');
        $firstDelta = $cache[0];
        $firstDeltaStart = $this->safe_integer($firstDelta, 'U');
        if ($nonce < $firstDeltaStart) {
            return -1;
        }
        for ($i = 0; $i < count($cache); $i++) {
            $delta = $cache[$i];
            $deltaStart = $this->safe_integer($delta, 'U');
            $deltaEnd = $this->safe_integer($delta, 'u');
            if (($nonce >= $deltaStart - 1) && ($nonce < $deltaEnd)) {
                return $i;
            }
        }
        return count($cache);
    }

    public function handle_bid_asks($bookSide, $bidAsks) {
        for ($i = 0; $i < count($bidAsks); $i++) {
            $bidAsk = $bidAsks[$i];
            if (gettype($bidAsk) === 'array' && array_keys($bidAsk) === array_keys(array_keys($bidAsk))) {
                $bookSide->storeArray ($this->parse_bid_ask($bidAsk));
            } else {
                $price = $this->safe_float($bidAsk, 'p');
                $amount = $this->safe_float($bidAsk, 's');
                $bookSide->store ($price, $amount);
            }
        }
    }

    public function handle_delta($orderbook, $delta) {
        $timestamp = $this->safe_integer($delta, 't');
        $orderbook['timestamp'] = $timestamp;
        $orderbook['datetime'] = $this->iso8601($timestamp);
        $orderbook['nonce'] = $this->safe_integer($delta, 'u');
        $bids = $this->safe_value($delta, 'b', array());
        $asks = $this->safe_value($delta, 'a', array());
        $storedBids = $orderbook['bids'];
        $storedAsks = $orderbook['asks'];
        $this->handle_bid_asks($storedBids, $bids);
        $this->handle_bid_asks($storedAsks, $asks);
    }

    public function watch_ticker(string $symbol, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbol, $params) {
            /**
             *
             * @see https://www.gate.io/docs/developers/apiv4/ws/en/#tickers-channel
             *
             * watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific $market
             * @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);
            $symbol = $market['symbol'];
            $params['callerMethodName'] = 'watchTicker';
            $result = Async\await($this->watch_tickers(array( $symbol ), $params));
            return $this->safe_value($result, $symbol);
        }) ();
    }

    public function watch_tickers(?array $symbols = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbols, $params) {
            /**
             *
             * @see https://www.gate.io/docs/developers/apiv4/ws/en/#tickers-channel
             *
             * watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for all markets of a specific list
             * @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~
             */
            return Async\await($this->subscribe_watch_tickers_and_bids_asks($symbols, 'watchTickers', $this->extend(array( 'method' => 'tickers' ), $params)));
        }) ();
    }

    public function handle_ticker(Client $client, $message) {
        //
        //    {
        //        "time" => 1649326221,
        //        "channel" => "spot.tickers",
        //        "event" => "update",
        //        "result" => {
        //          "currency_pair" => "BTC_USDT",
        //          "last" => "43444.82",
        //          "lowest_ask" => "43444.82",
        //          "highest_bid" => "43444.81",
        //          "change_percentage" => "-4.0036",
        //          "base_volume" => "5182.5412425462",
        //          "quote_volume" => "227267634.93123952",
        //          "high_24h" => "47698",
        //          "low_24h" => "42721.03"
        //        }
        //    }
        //
        $this->handle_ticker_and_bid_ask('ticker', $client, $message);
    }

    public function watch_bids_asks(?array $symbols = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbols, $params) {
            /**
             *
             * @see https://www.gate.io/docs/developers/apiv4/ws/en/#best-bid-or-ask-price
             * @see https://www.gate.io/docs/developers/apiv4/ws/en/#order-book-channel
             *
             * 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~
             */
            return Async\await($this->subscribe_watch_tickers_and_bids_asks($symbols, 'watchBidsAsks', $this->extend(array( 'method' => 'book_ticker' ), $params)));
        }) ();
    }

    public function handle_bid_ask(Client $client, $message) {
        //
        //    {
        //        "time" => 1671363004,
        //        "time_ms" => 1671363004235,
        //        "channel" => "spot.book_ticker",
        //        "event" => "update",
        //        "result" => {
        //          "t" => 1671363004228,
        //          "u" => 9793320464,
        //          "s" => "BTC_USDT",
        //          "b" => "16716.8",
        //          "B" => "0.0134",
        //          "a" => "16716.9",
        //          "A" => "0.0353"
        //        }
        //    }
        //
        $this->handle_ticker_and_bid_ask('bidask', $client, $message);
    }

    public function subscribe_watch_tickers_and_bids_asks(?array $symbols = null, ?string $callerMethodName = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbols, $callerMethodName, $params) {
            Async\await($this->load_markets());
            list($callerMethodName, $params) = $this->handle_param_string($params, 'callerMethodName', $callerMethodName);
            $symbols = $this->market_symbols($symbols, null, false);
            $market = $this->market($symbols[0]);
            $messageType = $this->get_type_by_market($market);
            $marketIds = $this->market_ids($symbols);
            $channelName = null;
            list($channelName, $params) = $this->handle_option_and_params($params, $callerMethodName, 'method');
            $url = $this->get_url_by_market($market);
            $channel = $messageType . '.' . $channelName;
            $isWatchTickers = mb_strpos($callerMethodName, 'watchTicker') !== false;
            $prefix = $isWatchTickers ? 'ticker' : 'bidask';
            $messageHashes = array();
            for ($i = 0; $i < count($symbols); $i++) {
                $symbol = $symbols[$i];
                $messageHashes[] = $prefix . ':' . $symbol;
            }
            $tickerOrBidAsk = Async\await($this->subscribe_public_multiple($url, $messageHashes, $marketIds, $channel, $params));
            if ($this->newUpdates) {
                $items = array();
                $items[$tickerOrBidAsk['symbol']] = $tickerOrBidAsk;
                return $items;
            }
            $result = $isWatchTickers ? $this->tickers : $this->bidsasks;
            return $this->filter_by_array($result, 'symbol', $symbols, true);
        }) ();
    }

    public function handle_ticker_and_bid_ask(string $objectName, Client $client, $message) {
        $channel = $this->safe_string($message, 'channel');
        $parts = explode('.', $channel);
        $rawMarketType = $this->safe_string($parts, 0);
        $marketType = ($rawMarketType === 'futures') ? 'contract' : 'spot';
        $result = $this->safe_value($message, 'result');
        $results = array();
        if (gettype($result) === 'array' && array_keys($result) === array_keys(array_keys($result))) {
            $results = $this->safe_list($message, 'result', array());
        } else {
            $rawTicker = $this->safe_dict($message, 'result', array());
            $results = array( $rawTicker );
        }
        $isTicker = ($objectName === 'ticker'); // whether ticker or bid-ask
        for ($i = 0; $i < count($results); $i++) {
            $rawTicker = $results[$i];
            $marketId = $this->safe_string($rawTicker, 's');
            $market = $this->safe_market($marketId, null, '_', $marketType);
            $parsedItem = $this->parse_ticker($rawTicker, $market);
            $symbol = $parsedItem['symbol'];
            if ($isTicker) {
                $this->tickers[$symbol] = $parsedItem;
            } else {
                $this->bidsasks[$symbol] = $parsedItem;
            }
            $messageHash = $objectName . ':' . $symbol;
            $client->resolve ($parsedItem, $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
             * @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 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
             * @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);
            $marketIds = $this->market_ids($symbols);
            $market = $this->market($symbols[0]);
            $messageType = $this->get_type_by_market($market);
            $channel = $messageType . '.trades';
            $messageHashes = array();
            for ($i = 0; $i < count($symbols); $i++) {
                $symbol = $symbols[$i];
                $messageHashes[] = 'trades:' . $symbol;
            }
            $url = $this->get_url_by_market($market);
            $trades = Async\await($this->subscribe_public_multiple($url, $messageHashes, $marketIds, $channel, $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
             * @param {string[]} $symbols 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~
             */
            Async\await($this->load_markets());
            $symbols = $this->market_symbols($symbols);
            $marketIds = $this->market_ids($symbols);
            $market = $this->market($symbols[0]);
            $messageType = $this->get_type_by_market($market);
            $channel = $messageType . '.trades';
            $subMessageHashes = array();
            $messageHashes = array();
            for ($i = 0; $i < count($symbols); $i++) {
                $symbol = $symbols[$i];
                $subMessageHashes[] = 'trades:' . $symbol;
                $messageHashes[] = 'unsubscribe:trades:' . $symbol;
            }
            $url = $this->get_url_by_market($market);
            return Async\await($this->un_subscribe_public_multiple($url, 'trades', $symbols, $messageHashes, $subMessageHashes, $marketIds, $channel, $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
             * @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 handle_trades(Client $client, $message) {
        //
        // {
        //     "time" => 1648725035,
        //     "channel" => "spot.trades",
        //     "event" => "update",
        //     "result" => [array(
        //       "id" => 3130257995,
        //       "create_time" => 1648725035,
        //       "create_time_ms" => "1648725035923.0",
        //       "side" => "sell",
        //       "currency_pair" => "LTC_USDT",
        //       "amount" => "0.0116",
        //       "price" => "130.11"
        //     )]
        // }
        //
        $result = $this->safe_value($message, 'result');
        if (gettype($result) !== 'array' || array_keys($result) !== array_keys(array_keys($result))) {
            $result = array( $result );
        }
        $parsedTrades = $this->parse_trades($result);
        for ($i = 0; $i < count($parsedTrades); $i++) {
            $trade = $parsedTrades[$i];
            $symbol = $trade['symbol'];
            $cachedTrades = $this->safe_value($this->trades, $symbol);
            if ($cachedTrades === null) {
                $limit = $this->safe_integer($this->options, 'tradesLimit', 1000);
                $cachedTrades = new ArrayCache ($limit);
                $this->trades[$symbol] = $cachedTrades;
            }
            $cachedTrades->append ($trade);
            $hash = 'trades:' . $symbol;
            $client->resolve ($cachedTrades, $hash);
        }
    }

    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'];
            $marketId = $market['id'];
            $interval = $this->safe_string($this->timeframes, $timeframe, $timeframe);
            $messageType = $this->get_type_by_market($market);
            $channel = $messageType . '.candlesticks';
            $messageHash = 'candles:' . $interval . ':' . $market['symbol'];
            $url = $this->get_url_by_market($market);
            $payload = array( $interval, $marketId );
            $ohlcv = Async\await($this->subscribe_public($url, $messageHash, $payload, $channel, $params));
            if ($this->newUpdates) {
                $limit = $ohlcv->getLimit ($symbol, $limit);
            }
            return $this->filter_by_since_limit($ohlcv, $since, $limit, 0, true);
        }) ();
    }

    public function handle_ohlcv(Client $client, $message) {
        //
        // {
        //     "time" => 1606292600,
        //     "channel" => "spot.candlesticks",
        //     "event" => "update",
        //     "result" => {
        //       "t" => "1606292580", // total volume
        //       "v" => "2362.32035", // volume
        //       "c" => "19128.1", // close
        //       "h" => "19128.1", // high
        //       "l" => "19128.1", // low
        //       "o" => "19128.1", // open
        //       "n" => "1m_BTC_USDT" // sub
        //     }
        //   }
        //
        $channel = $this->safe_string($message, 'channel');
        $channelParts = explode('.', $channel);
        $rawMarketType = $this->safe_string($channelParts, 0);
        $marketType = ($rawMarketType === 'spot') ? 'spot' : 'contract';
        $result = $this->safe_value($message, 'result');
        if (gettype($result) !== 'array' || array_keys($result) !== array_keys(array_keys($result))) {
            $result = array( $result );
        }
        $marketIds = array();
        for ($i = 0; $i < count($result); $i++) {
            $ohlcv = $result[$i];
            $subscription = $this->safe_string($ohlcv, 'n', '');
            $parts = explode('_', $subscription);
            $timeframe = $this->safe_string($parts, 0);
            $timeframeId = $this->find_timeframe($timeframe);
            $prefix = $timeframe . '_';
            $marketId = str_replace($prefix, '', $subscription);
            $symbol = $this->safe_symbol($marketId, null, '_', $marketType);
            $parsed = $this->parse_ohlcv($ohlcv);
            $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][$timeframeId] = $stored;
            }
            $stored->append ($parsed);
            $marketIds[$symbol] = $timeframe;
        }
        $keys = is_array($marketIds) ? array_keys($marketIds) : array();
        for ($i = 0; $i < count($keys); $i++) {
            $symbol = $keys[$i];
            $timeframe = $marketIds[$symbol];
            $interval = $this->find_timeframe($timeframe);
            $hash = 'candles' . ':' . $interval . ':' . $symbol;
            $stored = $this->safe_value($this->ohlcvs[$symbol], $interval);
            $client->resolve ($stored, $hash);
        }
    }

    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~
             */
            Async\await($this->load_markets());
            $subType = null;
            $type = null;
            $marketId = '!' . 'all';
            $market = null;
            if ($symbol !== null) {
                $market = $this->market($symbol);
                $marketId = $market['id'];
            }
            list($type, $params) = $this->handle_market_type_and_params('watchMyTrades', $market, $params);
            list($subType, $params) = $this->handle_sub_type_and_params('watchMyTrades', $market, $params);
            $messageType = $this->get_supported_mapping($type, array(
                'spot' => 'spot',
                'margin' => 'spot',
                'future' => 'futures',
                'swap' => 'futures',
                'option' => 'options',
            ));
            $channel = $messageType . '.usertrades';
            $messageHash = 'myTrades';
            if ($symbol !== null) {
                $messageHash .= ':' . $symbol;
            }
            $isInverse = ($subType === 'inverse');
            $url = $this->get_url_by_market_type($type, $isInverse);
            $payload = array( $marketId );
            // uid required for non spot markets
            $requiresUid = ($type !== 'spot');
            $trades = Async\await($this->subscribe_private($url, $messageHash, $payload, $channel, $params, $requiresUid));
            if ($this->newUpdates) {
                $limit = $trades->getLimit ($symbol, $limit);
            }
            return $this->filter_by_symbol_since_limit($trades, $symbol, $since, $limit, true);
        }) ();
    }

    public function handle_my_trades(Client $client, $message) {
        //
        // {
        //     "time" => 1543205083,
        //     "channel" => "futures.usertrades",
        //     "event" => "update",
        //     "error" => null,
        //     "result" => array(
        //       {
        //         "id" => "3335259",
        //         "create_time" => 1628736848,
        //         "create_time_ms" => 1628736848321,
        //         "contract" => "BTC_USD",
        //         "order_id" => "4872460",
        //         "size" => 1,
        //         "price" => "40000.4",
        //         "role" => "maker"
        //       }
        //     )
        // }
        //
        $result = $this->safe_value($message, 'result', array());
        $tradesLength = count($result);
        if ($tradesLength === 0) {
            return;
        }
        $cachedTrades = $this->myTrades;
        if ($cachedTrades === null) {
            $limit = $this->safe_integer($this->options, 'tradesLimit', 1000);
            $cachedTrades = new ArrayCacheBySymbolById ($limit);
            $this->myTrades = $cachedTrades;
        }
        $parsed = $this->parse_trades($result);
        $marketIds = array();
        for ($i = 0; $i < count($parsed); $i++) {
            $trade = $parsed[$i];
            $cachedTrades->append ($trade);
            $symbol = $trade['symbol'];
            $marketIds[$symbol] = true;
        }
        $keys = is_array($marketIds) ? array_keys($marketIds) : array();
        for ($i = 0; $i < count($keys); $i++) {
            $market = $keys[$i];
            $hash = 'myTrades:' . $market;
            $client->resolve ($cachedTrades, $hash);
        }
        $client->resolve ($cachedTrades, 'myTrades');
    }

    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
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @return {array} a ~@link https://docs.ccxt.com/#/?id=balance-structure balance structure~
             */
            Async\await($this->load_markets());
            $type = null;
            $subType = null;
            list($type, $params) = $this->handle_market_type_and_params('watchBalance', null, $params);
            list($subType, $params) = $this->handle_sub_type_and_params('watchBalance', null, $params);
            $isInverse = ($subType === 'inverse');
            $url = $this->get_url_by_market_type($type, $isInverse);
            $requiresUid = ($type !== 'spot');
            $channelType = $this->get_supported_mapping($type, array(
                'spot' => 'spot',
                'margin' => 'spot',
                'future' => 'futures',
                'swap' => 'futures',
                'option' => 'options',
            ));
            $channel = $channelType . '.balances';
            $messageHash = $type . '.balance';
            return Async\await($this->subscribe_private($url, $messageHash, null, $channel, $params, $requiresUid));
        }) ();
    }

    public function handle_balance(Client $client, $message) {
        //
        // spot order fill
        //   {
        //       "time" => 1653664351,
        //       "channel" => "spot.balances",
        //       "event" => "update",
        //       "result" => array(
        //         {
        //           "timestamp" => "1653664351",
        //           "timestamp_ms" => "1653664351017",
        //           "user" => "10406147",
        //           "currency" => "LTC",
        //           "change" => "-0.0002000000000000",
        //           "total" => "0.09986000000000000000",
        //           "available" => "0.09986000000000000000"
        //         }
        //       )
        //   }
        //
        // $account transfer
        //
        //    {
        //        "id" => null,
        //        "time" => 1653665088,
        //        "channel" => "futures.balances",
        //        "event" => "update",
        //        "error" => null,
        //        "result" => array(
        //          {
        //            "balance" => 25.035008537,
        //            "change" => 25,
        //            "text" => "-",
        //            "time" => 1653665088,
        //            "time_ms" => 1653665088286,
        //            "type" => "dnw",
        //            "user" => "10406147"
        //          }
        //        )
        //   }
        //
        // swap order fill
        //   {
        //       "id" => null,
        //       "time" => 1653665311,
        //       "channel" => "futures.balances",
        //       "event" => "update",
        //       "error" => null,
        //       "result" => array(
        //         {
        //           "balance" => 20.031873037,
        //           "change" => -0.0031355,
        //           "text" => "LTC_USDT:165551103273",
        //           "time" => 1653665311,
        //           "time_ms" => 1653665311437,
        //           "type" => "fee",
        //           "user" => "10406147"
        //         }
        //       )
        //   }
        //
        $result = $this->safe_value($message, 'result', array());
        $timestamp = $this->safe_integer($message, 'time_ms');
        $this->balance['info'] = $result;
        $this->balance['timestamp'] = $timestamp;
        $this->balance['datetime'] = $this->iso8601($timestamp);
        for ($i = 0; $i < count($result); $i++) {
            $rawBalance = $result[$i];
            $account = $this->account();
            $currencyId = $this->safe_string($rawBalance, 'currency', 'USDT'); // when not present it is USDT
            $code = $this->safe_currency_code($currencyId);
            $account['free'] = $this->safe_string($rawBalance, 'available');
            $account['total'] = $this->safe_string_2($rawBalance, 'total', 'balance');
            $this->balance[$code] = $account;
        }
        $channel = $this->safe_string($message, 'channel');
        $parts = explode('.', $channel);
        $rawType = $this->safe_string($parts, 0);
        $channelType = $this->get_supported_mapping($rawType, array(
            'spot' => 'spot',
            'futures' => 'swap',
            'options' => 'option',
        ));
        $messageHash = $channelType . '.balance';
        $this->balance = $this->safe_balance($this->balance);
        $client->resolve ($this->balance, $messageHash);
    }

    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) {
            /**
             *
             * @see https://www.gate.io/docs/developers/futures/ws/en/#$positions-subscription
             * @see https://www.gate.io/docs/developers/delivery/ws/en/#$positions-subscription
             * @see https://www.gate.io/docs/developers/options/ws/en/#$positions-$channel
             *
             * watch all open $positions
             * @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());
            $market = null;
            $symbols = $this->market_symbols($symbols);
            $payload = array( '!' . 'all' );
            if (!$this->is_empty($symbols)) {
                $market = $this->get_market_from_symbols($symbols);
            }
            $type = null;
            $query = null;
            list($type, $query) = $this->handle_market_type_and_params('watchPositions', $market, $params);
            if ($type === 'spot') {
                $type = 'swap';
            }
            $typeId = $this->get_supported_mapping($type, array(
                'future' => 'futures',
                'swap' => 'futures',
                'option' => 'options',
            ));
            $messageHash = $type . ':positions';
            if (!$this->is_empty($symbols)) {
                $messageHash .= '::' . implode(',', $symbols);
            }
            $channel = $typeId . '.positions';
            $subType = null;
            list($subType, $query) = $this->handle_sub_type_and_params('watchPositions', $market, $query);
            $isInverse = ($subType === 'inverse');
            $url = $this->get_url_by_market_type($type, $isInverse);
            $client = $this->client($url);
            $this->set_positions_cache($client, $type, $symbols);
            $fetchPositionsSnapshot = $this->handle_option('watchPositions', 'fetchPositionsSnapshot', true);
            $awaitPositionsSnapshot = $this->handle_option('watchPositions', 'awaitPositionsSnapshot', true);
            $cache = $this->safe_value($this->positions, $type);
            if ($fetchPositionsSnapshot && $awaitPositionsSnapshot && $cache === null) {
                return Async\await($client->future ($type . ':fetchPositionsSnapshot'));
            }
            $positions = Async\await($this->subscribe_private($url, $messageHash, $payload, $channel, $query, true));
            if ($this->newUpdates) {
                return $positions;
            }
            return $this->filter_by_symbols_since_limit($this->positions[$type], $symbols, $since, $limit, true);
        }) ();
    }

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

    public function load_positions_snapshot($client, $messageHash, $type) {
        return Async\async(function () use ($client, $messageHash, $type) {
            $positions = Async\await($this->fetch_positions(null, array( 'type' => $type )));
            $this->positions[$type] = new ArrayCacheBySymbolBySide ();
            $cache = $this->positions[$type];
            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, $type . ':position');
        }) ();
    }

    public function handle_positions($client, $message) {
        //
        //    {
        //        time => 1693158497,
        //        time_ms => 1693158497204,
        //        channel => 'futures.positions',
        //        event => 'update',
        //        result => [array(
        //            contract => 'XRP_USDT',
        //            cross_leverage_limit => 0,
        //            entry_price => 0.5253,
        //            history_pnl => 0,
        //            history_point => 0,
        //            last_close_pnl => 0,
        //            leverage => 0,
        //            leverage_max => 50,
        //            liq_price => 0.0361,
        //            maintenance_rate => 0.01,
        //            margin => 4.89609962852,
        //            mode => 'single',
        //            realised_pnl => -0.0026265,
        //            realised_point => 0,
        //            risk_limit => 500000,
        //            size => 1,
        //            time => 1693158497,
        //            time_ms => 1693158497195,
        //            update_id => 1,
        //            user => '10444586'
        //        )]
        //    }
        //
        $type = $this->get_market_type_by_url($client->url);
        $data = $this->safe_value($message, 'result', array());
        $cache = $this->positions[$type];
        $newPositions = array();
        for ($i = 0; $i < count($data); $i++) {
            $rawPosition = $data[$i];
            $position = $this->parse_position($rawPosition);
            $symbol = $this->safe_string($position, 'symbol');
            $side = $this->safe_string($position, 'side');
            // Control when $position is closed no $side is returned
            if ($side === null) {
                $prevLongPosition = $this->safe_dict($cache, $symbol . 'long');
                if ($prevLongPosition !== null) {
                    $position['side'] = $prevLongPosition['side'];
                    $newPositions[] = $position;
                    $cache->append ($position);
                }
                $prevShortPosition = $this->safe_dict($cache, $symbol . 'short');
                if ($prevShortPosition !== null) {
                    $position['side'] = $prevShortPosition['side'];
                    $newPositions[] = $position;
                    $cache->append ($position);
                }
                // if no prev $position is found, default to long
                if ($prevLongPosition === null && $prevShortPosition === null) {
                    $position['side'] = 'long';
                    $newPositions[] = $position;
                    $cache->append ($position);
                }
            } else {
                $newPositions[] = $position;
                $cache->append ($position);
            }
        }
        $messageHashes = $this->find_message_hashes($client, $type . ':$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, $type . ':positions');
    }

    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
             * @param {string} [$params->type] spot, margin, swap, future, or option. Required if listening to all symbols.
             * @param {boolean} [$params->isInverse] if future, listen to inverse or linear contracts
             * @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'];
            }
            $type = null;
            $query = null;
            list($type, $query) = $this->handle_market_type_and_params('watchOrders', $market, $params);
            $typeId = $this->get_supported_mapping($type, array(
                'spot' => 'spot',
                'margin' => 'spot',
                'future' => 'futures',
                'swap' => 'futures',
                'option' => 'options',
            ));
            $channel = $typeId . '.orders';
            $messageHash = 'orders';
            $payload = array( '!' . 'all' );
            if ($symbol !== null) {
                $messageHash .= ':' . $market['id'];
                $payload = [ $market['id'] ];
            }
            $subType = null;
            list($subType, $query) = $this->handle_sub_type_and_params('watchOrders', $market, $query);
            $isInverse = ($subType === 'inverse');
            $url = $this->get_url_by_market_type($type, $isInverse);
            // uid required for non spot markets
            $requiresUid = ($type !== 'spot');
            $orders = Async\await($this->subscribe_private($url, $messageHash, $payload, $channel, $query, $requiresUid));
            if ($this->newUpdates) {
                $limit = $orders->getLimit ($symbol, $limit);
            }
            return $this->filter_by_since_limit($orders, $since, $limit, 'timestamp', true);
        }) ();
    }

    public function handle_order(Client $client, $message) {
        //
        // {
        //     "time" => 1605175506,
        //     "channel" => "spot.orders",
        //     "event" => "update",
        //     "result" => array(
        //       {
        //         "id" => "30784435",
        //         "user" => 123456,
        //         "text" => "t-abc",
        //         "create_time" => "1605175506",
        //         "create_time_ms" => "1605175506123",
        //         "update_time" => "1605175506",
        //         "update_time_ms" => "1605175506123",
        //         "event" => "put",
        //         "currency_pair" => "BTC_USDT",
        //         "type" => "limit",
        //         "account" => "spot",
        //         "side" => "sell",
        //         "amount" => "1",
        //         "price" => "10001",
        //         "time_in_force" => "gtc",
        //         "left" => "1",
        //         "filled_total" => "0",
        //         "fee" => "0",
        //         "fee_currency" => "USDT",
        //         "point_fee" => "0",
        //         "gt_fee" => "0",
        //         "gt_discount" => true,
        //         "rebated_fee" => "0",
        //         "rebated_fee_currency" => "USDT"
        //       }
        //     )
        // }
        //
        $orders = $this->safe_value($message, 'result', array());
        $limit = $this->safe_integer($this->options, 'ordersLimit', 1000);
        if ($this->orders === null) {
            $this->orders = new ArrayCacheBySymbolById ($limit);
        }
        $stored = $this->orders;
        $marketIds = array();
        $parsedOrders = $this->parse_orders($orders);
        for ($i = 0; $i < count($parsedOrders); $i++) {
            $parsed = $parsedOrders[$i];
            // inject order $status
            $info = $this->safe_value($parsed, 'info');
            $event = $this->safe_string($info, 'event');
            if ($event === 'put' || $event === 'update') {
                $parsed['status'] = 'open';
            } elseif ($event === 'finish') {
                $status = $this->safe_string($parsed, 'status');
                if ($status === null) {
                    $left = $this->safe_integer($info, 'left');
                    $parsed['status'] = ($left === 0) ? 'closed' : 'canceled';
                }
            }
            $stored->append ($parsed);
            $symbol = $parsed['symbol'];
            $market = $this->market($symbol);
            $marketIds[$market['id']] = true;
        }
        $keys = is_array($marketIds) ? array_keys($marketIds) : array();
        for ($i = 0; $i < count($keys); $i++) {
            $messageHash = 'orders:' . $keys[$i];
            $client->resolve ($this->orders, $messageHash);
        }
        $client->resolve ($this->orders, 'orders');
    }

    public function watch_my_liquidations(string $symbol, ?int $since = null, ?int $limit = null, $params = array ()): PromiseInterface {
        /**
         * watch the public liquidations of a trading pair
         *
         * @see https://www.gate.io/docs/developers/futures/ws/en/#liquidates-api
         * @see https://www.gate.io/docs/developers/delivery/ws/en/#liquidates-api
         * @see https://www.gate.io/docs/developers/options/ws/en/#liquidates-channel
         *
         * @param {string} $symbol unified CCXT market $symbol
         * @param {int} [$since] the earliest time in ms to fetch liquidations for
         * @param {int} [$limit] the maximum number of liquidation structures to retrieve
         * @param {array} [$params] exchange specific parameters for the bitmex api endpoint
         * @return {array} an array of {@link https://github.com/ccxt/ccxt/wiki/Manual#liquidation-structure liquidation structures}
         */
        return $this->watch_my_liquidations_for_symbols(array( $symbol ), $since, $limit, $params);
    }

    public function watch_my_liquidations_for_symbols(array $symbols, ?int $since = null, ?int $limit = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbols, $since, $limit, $params) {
            /**
             * watch the private liquidations of a trading pair
             *
             * @see https://www.gate.io/docs/developers/futures/ws/en/#liquidates-api
             * @see https://www.gate.io/docs/developers/delivery/ws/en/#liquidates-api
             * @see https://www.gate.io/docs/developers/options/ws/en/#liquidates-$channel
             *
             * @param {string[]} $symbols unified CCXT $market $symbols
             * @param {int} [$since] the earliest time in ms to fetch liquidations for
             * @param {int} [$limit] the maximum number of liquidation structures to retrieve
             * @param {array} [$params] exchange specific parameters for the gate api endpoint
             * @return {array} an array of {@link https://github.com/ccxt/ccxt/wiki/Manual#liquidation-structure liquidation structures}
             */
            Async\await($this->load_markets());
            $symbols = $this->market_symbols($symbols, null, true, true);
            $market = $this->get_market_from_symbols($symbols);
            $type = null;
            $query = null;
            list($type, $query) = $this->handle_market_type_and_params('watchMyLiquidationsForSymbols', $market, $params);
            $typeId = $this->get_supported_mapping($type, array(
                'future' => 'futures',
                'swap' => 'futures',
                'option' => 'options',
            ));
            $subType = null;
            list($subType, $query) = $this->handle_sub_type_and_params('watchMyLiquidationsForSymbols', $market, $query);
            $isInverse = ($subType === 'inverse');
            $url = $this->get_url_by_market_type($type, $isInverse);
            $payload = array();
            $messageHash = '';
            if ($this->is_empty($symbols)) {
                if ($typeId !== 'futures' && !$isInverse) {
                    throw new BadRequest($this->id . ' watchMyLiquidationsForSymbols() does not support listening to all $symbols, you must call watchMyLiquidations() instead for each symbol you wish to watch.');
                }
                $messageHash = 'myLiquidations';
                $payload[] = '!all';
            } else {
                $symbolsLength = count($symbols);
                if ($symbolsLength !== 1) {
                    throw new BadRequest($this->id . ' watchMyLiquidationsForSymbols() only allows one symbol at a time. To listen to several $symbols call watchMyLiquidationsForSymbols() several times.');
                }
                $messageHash = 'myLiquidations::' . $symbols[0];
                $payload[] = $market['id'];
            }
            $channel = $typeId . '.liquidates';
            $newLiquidations = Async\await($this->subscribe_private($url, $messageHash, $payload, $channel, $query, true));
            if ($this->newUpdates) {
                return $newLiquidations;
            }
            return $this->filter_by_symbols_since_limit($this->liquidations, $symbols, $since, $limit, true);
        }) ();
    }

    public function handle_liquidation(Client $client, $message) {
        //
        // future / delivery
        //     {
        //         "channel":"futures.liquidates",
        //         "event":"update",
        //         "time":1541505434,
        //         "time_ms":1541505434123,
        //         "result":array(
        //            {
        //               "entry_price":209,
        //               "fill_price":215.1,
        //               "left":0,
        //               "leverage":0.0,
        //               "liq_price":213,
        //               "margin":0.007816722941,
        //               "mark_price":213,
        //               "order_id":4093362,
        //               "order_price":215.1,
        //               "size":-124,
        //               "time":1541486601,
        //               "time_ms":1541486601123,
        //               "contract":"BTC_USD",
        //               "user":"1040xxxx"
        //            }
        //         )
        //     }
        // option
        //    {
        //        "channel":"options.liquidates",
        //        "event":"update",
        //        "time":1630654851,
        //        "result":array(
        //           {
        //              "user":"1xxxx",
        //              "init_margin":1190,
        //              "maint_margin":1042.5,
        //              "order_margin":0,
        //              "time":1639051907,
        //              "time_ms":1639051907000
        //           }
        //        )
        //    }
        //
        $rawLiquidations = $this->safe_list($message, 'result', array());
        $newLiquidations = array();
        for ($i = 0; $i < count($rawLiquidations); $i++) {
            $rawLiquidation = $rawLiquidations[$i];
            $liquidation = $this->parse_ws_liquidation($rawLiquidation);
            $symbol = $this->safe_string($liquidation, 'symbol');
            $liquidations = $this->safe_value($this->liquidations, $symbol);
            if ($liquidations === null) {
                $limit = $this->safe_integer($this->options, 'liquidationsLimit', 1000);
                $liquidations = new ArrayCache ($limit);
            }
            $liquidations->append ($liquidation);
            $this->liquidations[$symbol] = $liquidations;
            $client->resolve ($liquidations, 'myLiquidations::' . $symbol);
        }
        $client->resolve ($newLiquidations, 'myLiquidations');
    }

    public function parse_ws_liquidation($liquidation, $market = null) {
        //
        // future / delivery
        //    {
        //        "entry_price" => 209,
        //        "fill_price" => 215.1,
        //        "left" => 0,
        //        "leverage" => 0.0,
        //        "liq_price" => 213,
        //        "margin" => 0.007816722941,
        //        "mark_price" => 213,
        //        "order_id" => 4093362,
        //        "order_price" => 215.1,
        //        "size" => -124,
        //        "time" => 1541486601,
        //        "time_ms" => 1541486601123,
        //        "contract" => "BTC_USD",
        //        "user" => "1040xxxx"
        //    }
        // option
        //    {
        //        "user" => "1xxxx",
        //        "init_margin" => 1190,
        //        "maint_margin" => 1042.5,
        //        "order_margin" => 0,
        //        "time" => 1639051907,
        //        "time_ms" => 1639051907000
        //    }
        //
        $marketId = $this->safe_string($liquidation, 'contract');
        $market = $this->safe_market($marketId, $market);
        $timestamp = $this->safe_integer($liquidation, 'time_ms');
        $originalSize = $this->safe_string($liquidation, 'size');
        $left = $this->safe_string($liquidation, 'left');
        $amount = Precise::string_abs(Precise::string_sub($originalSize, $left));
        return $this->safe_liquidation(array(
            'info' => $liquidation,
            'symbol' => $this->safe_symbol($marketId, $market),
            'contracts' => $this->parse_number($amount),
            'contractSize' => $this->safe_number($market, 'contractSize'),
            'price' => $this->safe_number($liquidation, 'fill_price'),
            'baseValue' => null,
            'quoteValue' => null,
            'timestamp' => $timestamp,
            'datetime' => $this->iso8601($timestamp),
        ));
    }

    public function handle_error_message(Client $client, $message): Bool {
        //
        //    {
        //        "time" => 1647274664,
        //        "channel" => "futures.orders",
        //        "event" => "subscribe",
        //        "error" => array( $code => 2, $message => "unknown contract BTC_USDT_20220318" ),
        //    }
        //    {
        //      "time" => 1647276473,
        //      "channel" => "futures.orders",
        //      "event" => "subscribe",
        //      "error" => array(
        //        "code" => 4,
        //        "message" => "array("label":"INVALID_KEY","message":"Invalid key provided")\n"
        //      ),
        //      "result" => null
        //    }
        //    {
        //       header => array(
        //         response_time => '1718551891329',
        //         status => '400',
        //         $channel => 'spot.order_place',
        //         event => 'api',
        //         client_id => '81.34.68.6-0xc16375e2c0',
        //         conn_id => '9539116e0e09678f'
        //       ),
        //       $data => array( $errs => array( label => 'AUTHENTICATION_FAILED', $message => 'Not login' ) ),
        //       request_id => '10406147'
        //     }
        //     {
        //         "time" => 1739853211,
        //         "time_ms" => 1739853211201,
        //         "id" => 1,
        //         "conn_id" => "62f2c1dabbe186d7",
        //         "trace_id" => "cdb02a8c0b61086b2fe6f8fad2f98c54",
        //         "channel" => "spot.trades",
        //         "event" => "subscribe",
        //         "payload" => array(
        //             "LUNARLENS_USDT",
        //             "ETH_USDT"
        //         ),
        //         "error" => array(
        //             "code" => 2,
        //             "message" => "unknown currency pair => LUNARLENS_USDT"
        //         ),
        //         "result" => array(
        //             "status" => "fail"
        //         ),
        //         "requestId" => "cdb02a8c0b61086b2fe6f8fad2f98c54"
        //     }
        //
        $data = $this->safe_dict($message, 'data');
        $errs = $this->safe_dict($data, 'errs');
        $error = $this->safe_dict($message, 'error', $errs);
        $code = $this->safe_string_2($error, 'code', 'label');
        $id = $this->safe_string_n($message, array( 'id', 'requestId', 'request_id' ));
        if ($error !== null) {
            $messageHash = $this->safe_string($client->subscriptions, $id);
            try {
                $this->throw_exactly_matched_exception($this->exceptions['ws']['exact'], $code, $this->json($message));
                $this->throw_exactly_matched_exception($this->exceptions['exact'], $code, $this->json($errs));
                $errorMessage = $this->safe_string($error, 'message', $this->safe_string($errs, 'message'));
                $this->throw_broadly_matched_exception($this->exceptions['ws']['broad'], $errorMessage, $this->json($message));
                throw new ExchangeError($this->json($message));
            } catch (Exception $e) {
                $client->reject ($e, $messageHash);
                if (($messageHash !== null) && (is_array($client->subscriptions) && array_key_exists($messageHash, $client->subscriptions))) {
                    unset($client->subscriptions[$messageHash]);
                }
                // remove subscriptions for watchSymbols
                $channel = $this->safe_string($message, 'channel');
                if (($channel !== null) && (mb_strpos($channel, '.') > 0)) {
                    $parsedChannel = explode('.', $channel);
                    $payload = $this->safe_list($message, 'payload', array());
                    for ($i = 0; $i < count($payload); $i++) {
                        $marketType = $parsedChannel[0] === 'futures' ? 'swap' : $parsedChannel[0];
                        $symbol = $this->safe_symbol($payload[$i], null, '_', $marketType);
                        $messageHashSymbol = $parsedChannel[1] . ':' . $symbol;
                        if (($messageHashSymbol !== null) && (is_array($client->subscriptions) && array_key_exists($messageHashSymbol, $client->subscriptions))) {
                            unset($client->subscriptions[$messageHashSymbol]);
                        }
                    }
                }
            }
            if (($id !== null) && (is_array($client->subscriptions) && array_key_exists($id, $client->subscriptions))) {
                unset($client->subscriptions[$id]);
            }
            return true;
        }
        return false;
    }

    public function handle_balance_subscription(Client $client, $message, $subscription = null) {
        $this->balance = array();
    }

    public function handle_subscription_status(Client $client, $message) {
        $channel = $this->safe_string($message, 'channel');
        $methods = array(
            'balance' => array($this, 'handle_balance_subscription'),
            'spot.order_book_update' => array($this, 'handle_order_book_subscription'),
            'futures.order_book_update' => array($this, 'handle_order_book_subscription'),
        );
        $id = $this->safe_string($message, 'id');
        if (is_array($methods) && array_key_exists($channel, $methods)) {
            $subscriptionHash = $this->safe_string($client->subscriptions, $id);
            $subscription = $this->safe_value($client->subscriptions, $subscriptionHash);
            $method = $methods[$channel];
            $method($client, $message, $subscription);
        }
        if (is_array($client->subscriptions) && array_key_exists($id, $client->subscriptions)) {
            unset($client->subscriptions[$id]);
        }
    }

    public function handle_un_subscribe(Client $client, $message) {
        //
        // {
        //     "time":1725534679,
        //     "time_ms":1725534679786,
        //     "id":2,
        //     "conn_id":"fac539b443fd7002",
        //     "trace_id":"efe1d282b630b4aa266b84bee177791a",
        //     "channel":"spot.trades",
        //     "event":"unsubscribe",
        //     "payload":array(
        //        "LTC_USDT"
        //     ),
        //     "result":array(
        //        "status":"success"
        //     ),
        //     "requestId":"efe1d282b630b4aa266b84bee177791a"
        // }
        //
        $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);
            }
        }
    }

    public function clean_cache(array $subscription) {
        $topic = $this->safe_string($subscription, 'topic', '');
        $symbols = $this->safe_list($subscription, 'symbols', array());
        $symbolsLength = count($symbols);
        if ($topic === 'ohlcv') {
            $symbolsAndTimeFrames = $this->safe_list($subscription, 'symbolsAndTimeframes', array());
            for ($i = 0; $i < count($symbolsAndTimeFrames); $i++) {
                $symbolAndTimeFrame = $symbolsAndTimeFrames[$i];
                $symbol = $this->safe_string($symbolAndTimeFrame, 0);
                $timeframe = $this->safe_string($symbolAndTimeFrame, 1);
                unset($this->ohlcvs[$symbol][$timeframe]);
            }
        } elseif ($symbolsLength > 0) {
            for ($i = 0; $i < count($symbols); $i++) {
                $symbol = $symbols[$i];
                if (str_ends_with($topic, 'trades')) {
                    unset($this->trades[$symbol]);
                } elseif ($topic === 'orderbook') {
                    unset($this->orderbooks[$symbol]);
                } elseif ($topic === 'ticker') {
                    unset($this->tickers[$symbol]);
                }
            }
        } else {
            if (str_ends_with($topic, 'trades')) {
                // don't reset $this->myTrades directly here
                // because in c# we need to use a different object
                $keys = is_array($this->trades) ? array_keys($this->trades) : array();
                for ($i = 0; $i < count($keys); $i++) {
                    unset($this->trades[$keys[$i]]);
                }
            }
        }
    }

    public function handle_message(Client $client, $message) {
        //
        // subscribe
        //    {
        //        "time" => 1649062304,
        //        "id" => 1649062303,
        //        "channel" => "spot.candlesticks",
        //        "event" => "subscribe",
        //        "result" => array( status => "success" )
        //    }
        //
        // candlestick
        //    {
        //        "time" => 1649063328,
        //        "channel" => "spot.candlesticks",
        //        "event" => "update",
        //        "result" => {
        //          "t" => "1649063280",
        //          "v" => "58932.23174896",
        //          "c" => "45966.47",
        //          "h" => "45997.24",
        //          "l" => "45966.47",
        //          "o" => "45975.18",
        //          "n" => "1m_BTC_USDT",
        //          "a" => "1.281699"
        //        }
        //     }
        //
        //  orders
        //   {
        //       "time" => 1630654851,
        //       "channel" => "options.orders", or futures.orders or spot.orders
        //       "event" => "update",
        //       "result" => array(
        //          {
        //             "contract" => "BTC_USDT-20211130-65000-C",
        //             "create_time" => 1637897000,
        //               (...)
        //       )
        //   }
        // orderbook
        //   {
        //       "time" => 1649770525,
        //       "channel" => "spot.order_book_update",
        //       "event" => "update",
        //       "result" => {
        //         "t" => 1649770525653,
        //         "e" => "depthUpdate",
        //         "E" => 1649770525,
        //         "s" => "LTC_USDT",
        //         "U" => 2622525645,
        //         "u" => 2622525665,
        //         "b" => [
        //           [Array], [Array],
        //           [Array], [Array],
        //           [Array], [Array],
        //           [Array], [Array],
        //           [Array], [Array],
        //           [Array]
        //         ],
        //         "a" => [
        //           [Array], [Array],
        //           [Array], [Array],
        //           [Array], [Array],
        //           [Array], [Array],
        //           [Array], [Array],
        //           [Array]
        //         ]
        //       }
        //     }
        //
        // balance update
        //
        //    {
        //        "time" => 1653664351,
        //        "channel" => "spot.balances",
        //        "event" => "update",
        //        "result" => array(
        //          {
        //            "timestamp" => "1653664351",
        //            "timestamp_ms" => "1653664351017",
        //            "user" => "10406147",
        //            "currency" => "LTC",
        //            "change" => "-0.0002000000000000",
        //            "total" => "0.09986000000000000000",
        //            "available" => "0.09986000000000000000"
        //          }
        //        )
        //    }
        //
        if ($this->handle_error_message($client, $message)) {
            return;
        }
        $event = $this->safe_string($message, 'event');
        if ($event === 'subscribe') {
            $this->handle_subscription_status($client, $message);
            return;
        }
        if ($event === 'unsubscribe') {
            $this->handle_un_subscribe($client, $message);
            return;
        }
        $channel = $this->safe_string($message, 'channel', '');
        $channelParts = explode('.', $channel);
        $channelType = $this->safe_value($channelParts, 1);
        $v4Methods = array(
            'usertrades' => array($this, 'handle_my_trades'),
            'candlesticks' => array($this, 'handle_ohlcv'),
            'orders' => array($this, 'handle_order'),
            'positions' => array($this, 'handle_positions'),
            'tickers' => array($this, 'handle_ticker'),
            'book_ticker' => array($this, 'handle_bid_ask'),
            'trades' => array($this, 'handle_trades'),
            'order_book_update' => array($this, 'handle_order_book'),
            'balances' => array($this, 'handle_balance'),
            'liquidates' => array($this, 'handle_liquidation'),
        );
        $method = $this->safe_value($v4Methods, $channelType);
        if ($method !== null) {
            $method($client, $message);
        }
        $requestId = $this->safe_string($message, 'request_id');
        if ($requestId === 'authenticated') {
            $this->handle_authentication_message($client, $message);
            return;
        }
        if ($requestId !== null) {
            $data = $this->safe_dict($message, 'data');
            // use safeValue may be Array or an Object
            $result = $this->safe_value($data, 'result');
            $ack = $this->safe_bool($message, 'ack');
            if ($ack !== true) {
                $client->resolve ($result, $requestId);
            }
        }
    }

    public function get_url_by_market($market) {
        $baseUrl = $this->urls['api'][$market['type']];
        if ($market['contract']) {
            return $market['linear'] ? $baseUrl['usdt'] : $baseUrl['btc'];
        } else {
            return $baseUrl;
        }
    }

    public function get_type_by_market(array $market) {
        if ($market['spot']) {
            return 'spot';
        } elseif ($market['option']) {
            return 'options';
        } else {
            return 'futures';
        }
    }

    public function get_url_by_market_type(string $type, $isInverse = false) {
        $api = $this->urls['api'];
        $url = $api[$type];
        if (($type === 'swap') || ($type === 'future')) {
            return $isInverse ? $url['btc'] : $url['usdt'];
        } else {
            return $url;
        }
    }

    public function get_market_type_by_url(string $url) {
        $findBy = array(
            'op-' => 'option',
            'delivery' => 'future',
            'fx' => 'swap',
        );
        $keys = is_array($findBy) ? array_keys($findBy) : array();
        for ($i = 0; $i < count($keys); $i++) {
            $key = $keys[$i];
            $value = $findBy[$key];
            if (mb_strpos($url, $key) !== false) {
                return $value;
            }
        }
        return 'spot';
    }

    public function request_id() {
        // their support said that $reqid must be an int32, not documented
        $reqid = $this->sum($this->safe_integer($this->options, 'reqid', 0), 1);
        $this->options['reqid'] = $reqid;
        return $reqid;
    }

    public function subscribe_public($url, $messageHash, $payload, $channel, $params = array (), $subscription = null) {
        return Async\async(function () use ($url, $messageHash, $payload, $channel, $params, $subscription) {
            $requestId = $this->request_id();
            $time = $this->seconds();
            $request = array(
                'id' => $requestId,
                'time' => $time,
                'channel' => $channel,
                'event' => 'subscribe',
                'payload' => $payload,
            );
            if ($subscription !== null) {
                $client = $this->client($url);
                if (!(is_array($client->subscriptions) && array_key_exists($messageHash, $client->subscriptions))) {
                    $tempSubscriptionHash = (string) $requestId;
                    $client->subscriptions[$tempSubscriptionHash] = $messageHash;
                }
            }
            $message = $this->extend($request, $params);
            return Async\await($this->watch($url, $messageHash, $message, $messageHash, $subscription));
        }) ();
    }

    public function subscribe_public_multiple($url, $messageHashes, $payload, $channel, $params = array ()) {
        return Async\async(function () use ($url, $messageHashes, $payload, $channel, $params) {
            $requestId = $this->request_id();
            $time = $this->seconds();
            $request = array(
                'id' => $requestId,
                'time' => $time,
                'channel' => $channel,
                'event' => 'subscribe',
                'payload' => $payload,
            );
            $message = $this->extend($request, $params);
            return Async\await($this->watch_multiple($url, $messageHashes, $message, $messageHashes));
        }) ();
    }

    public function un_subscribe_public_multiple($url, $topic, $symbols, $messageHashes, $subMessageHashes, $payload, $channel, $params = array ()) {
        return Async\async(function () use ($url, $topic, $symbols, $messageHashes, $subMessageHashes, $payload, $channel, $params) {
            $requestId = $this->request_id();
            $time = $this->seconds();
            $request = array(
                'id' => $requestId,
                'time' => $time,
                'channel' => $channel,
                'event' => 'unsubscribe',
                'payload' => $payload,
            );
            $sub = array(
                'id' => (string) $requestId,
                'topic' => $topic,
                'unsubscribe' => true,
                'messageHashes' => $messageHashes,
                'subMessageHashes' => $subMessageHashes,
                'symbols' => $symbols,
            );
            $message = $this->extend($request, $params);
            return Async\await($this->watch_multiple($url, $messageHashes, $message, $messageHashes, $sub));
        }) ();
    }

    public function authenticate($url, $messageType) {
        return Async\async(function () use ($url, $messageType) {
            $channel = $messageType . '.login';
            $client = $this->client($url);
            $messageHash = 'authenticated';
            $future = $client->reusableFuture ($messageHash);
            $authenticated = $this->safe_value($client->subscriptions, $messageHash);
            if ($authenticated === null) {
                return Async\await($this->request_private($url, array(), $channel, $messageHash));
            }
            return $future;
        }) ();
    }

    public function handle_authentication_message(Client $client, $message) {
        $messageHash = 'authenticated';
        $future = $this->safe_value($client->futures, $messageHash);
        $future->resolve (true);
    }

    public function request_private($url, $reqParams, $channel, ?string $requestId = null) {
        return Async\async(function () use ($url, $reqParams, $channel, $requestId) {
            $this->check_required_credentials();
            // uid is required for some subscriptions only so it's not a part of required credentials
            $event = 'api';
            if ($requestId === null) {
                $reqId = $this->request_id();
                $requestId = (string) $reqId;
            }
            $messageHash = $requestId;
            $time = $this->seconds();
            // unfortunately, PHP demands double quotes for the escaped newline symbol
            $signatureString = implode("\n", array($event, $channel, $this->json($reqParams), (string) $time)); // eslint-disable-line quotes
            $signature = $this->hmac($this->encode($signatureString), $this->encode($this->secret), 'sha512', 'hex');
            $payload = array(
                'req_id' => $requestId,
                'timestamp' => (string) $time,
                'api_key' => $this->apiKey,
                'signature' => $signature,
                'req_param' => $reqParams,
            );
            if (($channel === 'spot.order_place') || ($channel === 'futures.order_place')) {
                $payload['req_header'] = array(
                    'X-Gate-Channel-Id' => 'ccxt',
                );
            }
            $request = array(
                'id' => $requestId,
                'time' => $time,
                'channel' => $channel,
                'event' => $event,
                'payload' => $payload,
            );
            return Async\await($this->watch($url, $messageHash, $request, $messageHash, $requestId));
        }) ();
    }

    public function subscribe_private($url, $messageHash, $payload, $channel, $params, $requiresUid = false) {
        return Async\async(function () use ($url, $messageHash, $payload, $channel, $params, $requiresUid) {
            $this->check_required_credentials();
            // uid is required for some subscriptions only so it's not a part of required credentials
            if ($requiresUid) {
                if ($this->uid === null || strlen($this->uid) === 0) {
                    throw new ArgumentsRequired($this->id . ' requires uid to subscribe');
                }
                $idArray = array( $this->uid );
                if ($payload === null) {
                    $payload = $idArray;
                } else {
                    $payload = $this->array_concat($idArray, $payload);
                }
            }
            $time = $this->seconds();
            $event = 'subscribe';
            $signaturePayload = 'channel=' . $channel . '&' . 'event=' . $event . '&' . 'time=' . (string) $time;
            $signature = $this->hmac($this->encode($signaturePayload), $this->encode($this->secret), 'sha512', 'hex');
            $auth = array(
                'method' => 'api_key',
                'KEY' => $this->apiKey,
                'SIGN' => $signature,
            );
            $requestId = $this->request_id();
            $request = array(
                'id' => $requestId,
                'time' => $time,
                'channel' => $channel,
                'event' => $event,
                'auth' => $auth,
            );
            if ($payload !== null) {
                $request['payload'] = $payload;
            }
            $client = $this->client($url);
            if (!(is_array($client->subscriptions) && array_key_exists($messageHash, $client->subscriptions))) {
                $tempSubscriptionHash = (string) $requestId;
                // in case of authenticationError we will throw
                $client->subscriptions[$tempSubscriptionHash] = $messageHash;
            }
            $message = $this->extend($request, $params);
            return Async\await($this->watch($url, $messageHash, $message, $messageHash, $messageHash));
        }) ();
    }
}
