<?php

namespace ccxt\pro;

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

use Exception; // a common import
use ccxt\ExchangeError;
use ccxt\AuthenticationError;
use ccxt\NotSupported;
use \React\Async;
use \React\Promise\PromiseInterface;

class toobit extends \ccxt\async\toobit {

    public function describe(): mixed {
        return $this->deep_extend(parent::describe(), array(
            'has' => array(
                'ws' => true,
                'watchBalance' => true,
                'watchMyTrades' => true,
                'watchOHLCV' => true,
                'watchOHLCVForSymbols' => true,
                'watchOrderBook' => true,
                'watchOrderBookForSymbols' => true,
                'watchOrders' => true,
                'watchTicker' => true,
                'watchTickers' => true,
                'watchTrades' => true,
                'watchTradesForSymbols' => true,
                // 'watchPosition' => false,
            ),
            'urls' => array(
                'api' => array(
                    'ws' => array(
                        'common' => 'wss://stream.toobit.com',
                    ),
                ),
            ),
            'options' => array(
                'ws' => array(
                    'timeframes' => array(
                        '1m' => '1m',
                        '3m' => '3m',
                        '5m' => '5m',
                        '15m' => '15m',
                        '30m' => '30m',
                        '1h' => '1h',
                        '2h' => '2h',
                        '4h' => '4h',
                        '6h' => '6h',
                        '8h' => '8h',
                        '12h' => '12h',
                        '1d' => '1d',
                        '1w' => '1w',
                        '1M' => '1M',
                    ),
                    'watchOrderBook' => array(
                        'channel' => 'depth', // depth, diffDepth
                    ),
                    'listenKeyRefreshRate' => 1200000, // 20 mins
                ),
            ),
            'streaming' => array(
                'keepAlive' => (60 - 1) * 5 * 1000, // every 5 minutes
                'ping' => array($this, 'ping'),
            ),
            'exceptions' => array(
                'ws' => array(
                    'exact' => array(
                    ),
                ),
            ),
        ));
    }

    public function ping(Client $client) {
        return array(
            'ping' => $this->milliseconds(),
        );
    }

    public function handle_message(Client $client, $message) {
        //
        // public
        //
        //     {
        //         $topic => "trade",
        //         symbol => "DOGEUSDT",
        //         symbolName => "DOGEUSDT",
        //         params => array(
        //             realtimeInterval => "24h",
        //             binary => "false",
        //         ),
        //         data => array(
        //             array(
        //                 v => "4864732022868004630",
        //                 t => 1757243788405,
        //                 p => "0.21804",
        //                 q => "80",
        //                 m => true,
        //             ),
        //         ),
        //         f => true,  // initial first snapshot or not
        //         sendTime => 1757244002117,
        //         shared => false,
        //     }
        //
        // private
        //
        //     [
        //       {
        //         e => 'outboundContractAccountInfo',
        //         E => '1758228398234',
        //         T => true,
        //         W => true,
        //         D => true,
        //         B => [ [Object] ]
        //       }
        //     ]
        //
        $topic = $this->safe_string($message, 'topic');
        if ($this->handle_error_message($client, $message)) {
            return;
        }
        //
        // handle ping-pong => array( ping => 1758540450000 )
        //
        $pongTimestamp = $this->safe_integer($message, 'pong');
        if ($pongTimestamp !== null) {
            $this->handle_incoming_pong($client, $pongTimestamp);
            return;
        }
        $methods = array(
            'trade' => array($this, 'handle_trades'),
            'kline' => array($this, 'handle_ohlcv'),
            'realtimes' => array($this, 'handle_tickers'),
            'depth' => array($this, 'handle_order_book_partial_snapshot'),
            'diffDepth' => array($this, 'handle_order_book'),
            'outboundAccountInfo' => array($this, 'handle_balance'),
            'outboundContractAccountInfo' => array($this, 'handle_balance'),
            'executionReport' => array($this, 'handle_order'),
            'contractExecutionReport' => array($this, 'handle_order'),
            'ticketInfo' => array($this, 'handle_my_trade'),
            'outboundContractPositionInfo' => array($this, 'handle_positions'),
        );
        $method = $this->safe_value($methods, $topic);
        if ($method !== null) {
            $method($client, $message);
        } else {
            // check private streams
            for ($i = 0; $i < count($message); $i++) {
                $item = $message[$i];
                $event = $this->safe_string($item, 'e');
                $method2 = $this->safe_value($methods, $event);
                if ($method2 !== null) {
                    $method2($client, $item);
                }
            }
        }
    }

    public function handle_incoming_pong(Client $client, ?int $pongTimestamp) {
        $client->lastPong = $pongTimestamp;
    }

    public function watch_trades(string $symbol, ?int $since = null, ?int $limit = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbol, $since, $limit, $params) {
            /**
             * watches information on multiple trades made in a market
             *
             * @see https://toobit-docs.github.io/apidocs/spot/v1/en/#trade-streams
             *
             * @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~
             */
            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 list of $symbols
             *
             * @see https://toobit-docs.github.io/apidocs/spot/v1/en/#trade-streams
             *
             * @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
             * @param {string} [$params->name] the name of the method to call, 'trade' or 'aggTrade', default is 'trade'
             * @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, null, false);
            $messageHashes = array();
            $subParams = array();
            for ($i = 0; $i < count($symbols); $i++) {
                $symbol = $symbols[$i];
                $market = $this->market($symbol);
                $messageHashes[] = 'trade::' . $symbol;
                $rawHash = $market['id'];
                $subParams[] = $rawHash;
            }
            $marketIds = $this->market_ids($symbols);
            $url = $this->urls['api']['ws']['common'] . '/quote/ws/v1';
            $request = array(
                'symbol' => implode(',', $marketIds),
                'topic' => 'trade',
                'event' => 'sub',
            );
            $trades = Async\await($this->watch_multiple($url, $messageHashes, $this->extend($request, $params), $messageHashes));
            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 handle_trades(Client $client, $message) {
        //
        //     {
        //         $symbol => "DOGEUSDT",
        //         symbolName => "DOGEUSDT",
        //         topic => "trade",
        //         params => array(
        //             realtimeInterval => "24h",
        //             binary => "false",
        //         ),
        //         $data => array(
        //             array(
        //                 v => "4864732022868004630",
        //                 t => 1757243788405,
        //                 p => "0.21804",
        //                 q => "80",
        //                 m => true,
        //             ),
        //         ),
        //         f => true,  // initial first snapshot or not
        //         sendTime => 1757244002117,
        //         shared => false,
        //     }
        //
        $marketId = $this->safe_string($message, 'symbol');
        $symbol = $this->safe_symbol($marketId);
        if (!(is_array($this->trades) && array_key_exists($symbol, $this->trades))) {
            $limit = $this->safe_integer($this->options, 'tradesLimit', 1000);
            $this->trades[$symbol] = new ArrayCache ($limit);
        }
        $stored = $this->trades[$symbol];
        $data = $this->safe_list($message, 'data', array());
        $parsed = $this->parse_ws_trades($data);
        for ($i = 0; $i < count($parsed); $i++) {
            $trade = $parsed[$i];
            $trade['symbol'] = $symbol;
            $stored->append ($trade);
        }
        $messageHash = 'trade::' . $symbol;
        $client->resolve ($stored, $messageHash);
    }

    public function parse_ws_trade(array $trade, ?array $market = null): array {
        return $this->parse_trade($trade, $market);
    }

    public function watch_ohlcv(string $symbol, $timeframe = '1m', ?int $since = null, ?int $limit = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbol, $timeframe, $since, $limit, $params) {
            /**
             * watches historical candlestick data containing the open, high, low, and close price, and the volume of a market
             *
             * @see https://toobit-docs.github.io/apidocs/spot/v1/en/#kline-candlestick-streams
             *
             * @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
             */
            $params['callerMethodName'] = 'watchOHLCV';
            $result = Async\await($this->watch_ohlcv_for_symbols(array( array( $symbol, $timeframe ) ), $since, $limit, $params));
            return $result[$symbol][$timeframe];
        }) ();
    }

    public function watch_ohlcv_for_symbols(array $symbolsAndTimeframes, ?int $since = null, ?int $limit = null, $params = array ()) {
        return Async\async(function () use ($symbolsAndTimeframes, $since, $limit, $params) {
            /**
             * watches historical candlestick $data containing the open, high, low, and close price, and the volume of a $market
             *
             * @see https://toobit-docs.github.io/apidocs/spot/v1/en/#kline-candlestick-streams
             *
             * @param {string[][]} $symbolsAndTimeframes array of arrays containing unified symbols and $timeframes to fetch OHLCV $data for, example [['BTC/USDT', '1m'], ['LTC/USDT', '5m']]
             * @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 {array} A list of candles ordered, open, high, low, close, volume
             */
            Async\await($this->load_markets());
            $url = $this->urls['api']['ws']['common'] . '/quote/ws/v1';
            $messageHashes = array();
            $timeframes = $this->safe_dict($this->options['ws'], 'timeframes', array());
            $marketIds = array();
            $selectedTimeframe = null;
            for ($i = 0; $i < count($symbolsAndTimeframes); $i++) {
                $data = $symbolsAndTimeframes[$i];
                $symbolStr = $this->safe_string($data, 0);
                $market = $this->market($symbolStr);
                $marketId = $market['id'];
                $unfiedTimeframe = $this->safe_string($data, 1, '1m');
                $rawTimeframe = $this->safe_string($timeframes, $unfiedTimeframe, $unfiedTimeframe);
                if ($selectedTimeframe !== null && $selectedTimeframe !== $rawTimeframe) {
                    throw new NotSupported($this->id . ' watchOHLCVForSymbols() only supports a single $timeframe for all symbols');
                } else {
                    $selectedTimeframe = $rawTimeframe;
                }
                $marketIds[] = $marketId;
                $messageHashes[] = 'ohlcv::' . $symbolStr . '::' . $unfiedTimeframe;
            }
            $request = array(
                'symbol' => implode(',', $marketIds),
                'topic' => 'kline_' . $selectedTimeframe,
                'event' => 'sub',
            );
            list($symbol, $timeframe, $stored) = Async\await($this->watch_multiple($url, $messageHashes, $this->extend($request, $params), $messageHashes));
            if ($this->newUpdates) {
                $limit = $stored->getLimit ($symbol, $limit);
            }
            $filtered = $this->filter_by_since_limit($stored, $since, $limit, 0, true);
            return $this->create_ohlcv_object($symbol, $timeframe, $filtered);
        }) ();
    }

    public function handle_ohlcv(Client $client, $message) {
        //
        //     {
        //         $symbol => 'DOGEUSDT',
        //         symbolName => 'DOGEUSDT',
        //         klineType => '1m',
        //         topic => 'kline',
        //         $params => array( realtimeInterval => '24h', klineType => '1m', binary => 'false' ),
        //         $data => array(
        //             {
        //                 t => 1757251200000,
        //                 s => 'DOGEUSDT',
        //                 sn => 'DOGEUSDT',
        //                 c => '0.21889',
        //                 h => '0.21898',
        //                 l => '0.21889',
        //                 o => '0.21897',
        //                 v => '5247',
        //                 st => 0
        //             }
        //         ),
        //         f => true,
        //         sendTime => 1757251217643,
        //         shared => false
        //     }
        //
        $marketId = $this->safe_string($message, 'symbol');
        $market = $this->market($marketId);
        $symbol = $market['symbol'];
        $params = $this->safe_dict($message, 'params', array());
        $timeframeId = $this->safe_string($params, 'klineType');
        $timeframe = $this->find_timeframe($timeframeId);
        if (!(is_array($this->ohlcvs) && array_key_exists($symbol, $this->ohlcvs))) {
            $this->ohlcvs[$symbol] = array();
        }
        if (!(is_array($this->ohlcvs[$symbol]) && array_key_exists($timeframe, $this->ohlcvs[$symbol]))) {
            $limit = $this->safe_integer($this->options['ws'], 'OHLCVLimit', 1000);
            $this->ohlcvs[$symbol][$timeframe] = new ArrayCacheByTimestamp ($limit);
        }
        $stored = $this->ohlcvs[$symbol][$timeframe];
        $data = $this->safe_list($message, 'data', array());
        for ($i = 0; $i < count($data); $i++) {
            $parsed = $this->parse_ws_ohlcv($data[$i], $market);
            $stored->append ($parsed);
        }
        $messageHash = 'ohlcv::' . $symbol . '::' . $timeframe;
        $resolveData = array( $symbol, $timeframe, $stored );
        $client->resolve ($resolveData, $messageHash);
    }

    public function parse_ws_ohlcv($ohlcv, $market = null): array {
        //
        //             {
        //                 t => 1757251200000,
        //                 o => '0.21897',
        //                 h => '0.21898',
        //                 l => '0.21889',
        //                 c => '0.21889',
        //                 v => '5247',
        //                 s => 'DOGEUSDT',
        //                 sn => 'DOGEUSDT',
        //                 st => 0
        //             }
        //
        $parsed = $this->parse_ohlcv($ohlcv, $market);
        return $parsed;
    }

    public function watch_ticker(string $symbol, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbol, $params) {
            /**
             *
             * @see https://toobit-docs.github.io/apidocs/spot/v1/en/#individual-$symbol-ticker-streams
             *
             * 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());
            $symbol = $this->symbol($symbol);
            $tickers = Async\await($this->watch_tickers(array( $symbol ), $params));
            return $tickers[$symbol];
        }) ();
    }

    public function watch_tickers(?array $symbols = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbols, $params) {
            /**
             *
             * @see https://toobit-docs.github.io/apidocs/spot/v1/en/#individual-$symbol-$ticker-streams
             *
             * 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~
             */
            Async\await($this->load_markets());
            $symbols = $this->market_symbols($symbols, null, false);
            $messageHashes = array();
            $subParams = array();
            for ($i = 0; $i < count($symbols); $i++) {
                $symbol = $symbols[$i];
                $market = $this->market($symbol);
                $messageHashes[] = 'ticker::' . $symbol;
                $rawHash = $market['id'];
                $subParams[] = $rawHash;
            }
            $marketIds = $this->market_ids($symbols);
            $url = $this->urls['api']['ws']['common'] . '/quote/ws/v1';
            $request = array(
                'symbol' => implode(',', $marketIds),
                'topic' => 'realtimes',
                'event' => 'sub',
            );
            $ticker = Async\await($this->watch_multiple($url, $messageHashes, $this->extend($request, $params), $messageHashes));
            if ($this->newUpdates) {
                $result = array();
                $result[$ticker['symbol']] = $ticker;
                return $result;
            }
            return $this->filter_by_array($this->tickers, 'symbol', $symbols);
        }) ();
    }

    public function handle_tickers(Client $client, $message) {
        //
        //    {
        //        "symbol" => "DOGEUSDT",
        //        "symbolName" => "DOGEUSDT",
        //        "topic" => "realtimes",
        //        "params" => array(
        //            "realtimeInterval" => "24h"
        //        ),
        //        "data" => array(
        //            {
        //                "t" => 1757257643683,
        //                "s" => "DOGEUSDT",
        //                "o" => "0.21462",
        //                "h" => "0.22518",
        //                "l" => "0.21229",
        //                "c" => "0.2232",
        //                "v" => "283337017",
        //                "qv" => "62063771.42702",
        //                "sn" => "DOGEUSDT",
        //                "m" => "0.04",
        //                "e" => 301,
        //                "c24h" => "0.2232",
        //                "h24h" => "0.22518",
        //                "l24h" => "0.21229",
        //                "o24h" => "0.21462",
        //                "v24h" => "283337017",
        //                "qv24h" => "62063771.42702",
        //                "m24h" => "0.04"
        //            }
        //        ),
        //        "f" => false,
        //        "sendTime" => 1757257643751,
        //        "shared" => false
        //    }
        //
        $data = $this->safe_list($message, 'data');
        $newTickers = array();
        for ($i = 0; $i < count($data); $i++) {
            $ticker = $data[$i];
            $parsed = $this->parse_ws_ticker($ticker);
            $symbol = $parsed['symbol'];
            $this->tickers[$symbol] = $parsed;
            $newTickers[$symbol] = $parsed;
            $messageHash = 'ticker::' . $symbol;
            $client->resolve ($parsed, $messageHash);
        }
        $client->resolve ($newTickers, 'tickers');
    }

    public function parse_ws_ticker($ticker, $market = null) {
        return $this->parse_ticker($ticker, $market);
    }

    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://toobit-docs.github.io/apidocs/spot/v1/en/#partial-book-depth-streams
             *
             * @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
             */
            return Async\await($this->watch_order_book_for_symbols(array( $symbol ), $limit, $params));
        }) ();
    }

    public function watch_order_book_for_symbols(array $symbols, ?int $limit = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbols, $limit, $params) {
            /**
             * watches information on open orders with bid (buy) and ask (sell) prices, volumes and other data
             *
             * @see https://toobit-docs.github.io/apidocs/spot/v1/en/#partial-book-depth-streams
             *
             * @param {string[]} $symbols unified array of $symbols
             * @param {int} [$limit] the maximum amount of order book entries to return.
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @return {array} A dictionary of ~@link https://docs.ccxt.com/#/?id=order-book-structure order book structures~ indexed by $market $symbols
             */
            Async\await($this->load_markets());
            $symbols = $this->market_symbols($symbols, null, false);
            $channel = null;
            list($channel, $params) = $this->handle_option_and_params($params, 'watchOrderBook', 'channel', 'depth');
            $messageHashes = array();
            $subParams = array();
            for ($i = 0; $i < count($symbols); $i++) {
                $symbol = $symbols[$i];
                $market = $this->market($symbol);
                $messageHashes[] = 'orderBook::' . $symbol . '::' . $channel;
                $rawHash = $market['id'];
                $subParams[] = $rawHash;
            }
            $marketIds = $this->market_ids($symbols);
            $url = $this->urls['api']['ws']['common'] . '/quote/ws/v1';
            $request = array(
                'symbol' => implode(',', $marketIds),
                'topic' => $channel,
                'event' => 'sub',
            );
            $orderbook = Async\await($this->watch_multiple($url, $messageHashes, $this->extend($request, $params), $messageHashes));
            return $orderbook->limit ();
        }) ();
    }

    public function handle_order_book(Client $client, $message) {
        //
        //     {
        //         $symbol => 'DOGEUSDT',
        //         symbolName => 'DOGEUSDT',
        //         topic => 'depth',
        //         params => array( realtimeInterval => '24h' ),
        //         $data => [
        //             {
        //             e => 301,
        //             t => 1757304842860,
        //             v => '9814355_1E-18',
        //             b => [Array],
        //             a => [Array],
        //             o => 0
        //             }
        //         ],
        //         f => false,
        //         sendTime => 1757304843047,
        //         shared => false
        //     }
        //
        $isSnapshot = $this->safe_bool($message, 'f', false);
        if ($isSnapshot) {
            $this->set_order_book_snapshot($client, $message, 'diffDepth');
            return;
        }
        $marketId = $this->safe_string($message, 'symbol');
        $market = $this->safe_market($marketId);
        $symbol = $market['symbol'];
        $data = $this->safe_list($message, 'data', array());
        for ($i = 0; $i < count($data); $i++) {
            $entry = $data[$i];
            $messageHash = 'orderBook::' . $symbol . '::' . 'diffDepth';
            if (!(is_array($this->orderbooks) && array_key_exists($symbol, $this->orderbooks))) {
                $limit = $this->safe_integer($this->options['ws'], 'orderBookLimit', 1000);
                $this->orderbooks[$symbol] = $this->order_book(array(), $limit);
            }
            $orderBook = $this->orderbooks[$symbol];
            $timestamp = $this->safe_integer($entry, 't');
            $bids = $this->safe_list($entry, 'b', array());
            $asks = $this->safe_list($entry, 'a', array());
            $this->handle_deltas($orderBook['asks'], $asks);
            $this->handle_deltas($orderBook['bids'], $bids);
            $orderBook['timestamp'] = $timestamp;
            $this->orderbooks[$symbol] = $orderBook;
            $client->resolve ($orderBook, $messageHash);
        }
    }

    public function handle_delta($bookside, $delta) {
        $bidAsk = $this->parse_bid_ask($delta);
        $bookside->storeArray ($bidAsk);
    }

    public function handle_order_book_partial_snapshot(Client $client, $message) {
        //
        //     {
        //         symbol => 'DOGEUSDT',
        //         symbolName => 'DOGEUSDT',
        //         topic => 'depth',
        //         params => array( realtimeInterval => '24h' ),
        //         data => [
        //             {
        //             e => 301,
        //             s => 'DOGEUSDT',
        //             t => 1757304842860,
        //             v => '9814355_1E-18',
        //             b => [Array],
        //             a => [Array],
        //             o => 0
        //             }
        //         ],
        //         f => false,
        //         sendTime => 1757304843047,
        //         shared => false
        //     }
        //
        $this->set_order_book_snapshot($client, $message, 'depth');
    }

    public function set_order_book_snapshot(Client $client, $message, string $channel) {
        $data = $this->safe_list($message, 'data', array());
        $length = count($data);
        if ($length === 0) {
            return;
        }
        for ($i = 0; $i < $length; $i++) {
            $entry = $data[$i];
            $marketId = $this->safe_string($entry, 's');
            $symbol = $this->safe_symbol($marketId);
            $messageHash = 'orderBook::' . $symbol . '::' . $channel;
            if (!(is_array($this->orderbooks) && array_key_exists($symbol, $this->orderbooks))) {
                $limit = $this->safe_integer($this->options['ws'], 'orderBookLimit', 1000);
                $this->orderbooks[$symbol] = $this->order_book(array(), $limit);
            }
            $orderbook = $this->orderbooks[$symbol];
            $timestamp = $this->safe_integer($entry, 't');
            $snapshot = $this->parse_order_book($entry, $symbol, $timestamp, 'b', 'a');
            $orderbook->reset ($snapshot);
            $client->resolve ($orderbook, $messageHash);
        }
    }

    public function watch_balance($params = array ()): PromiseInterface {
        return Async\async(function () use ($params) {
            /**
             * query for balance and get the amount of funds available for trading or funds locked in orders
             *
             * @see https://toobit-docs.github.io/apidocs/spot/v1/en/#payload-account-update
             *
             * @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());
            Async\await($this->authenticate());
            $marketType = null;
            list($marketType, $params) = $this->handle_market_type_and_params('watchBalance', null, $params);
            $isSpot = ($marketType === 'spot');
            $type = $isSpot ? 'spot' : 'contract';
            $spotSubHash = 'spot:balance';
            $swapSubHash = 'contract:private';
            $spotMessageHash = 'spot:balance';
            $swapMessageHash = 'contract:balance';
            $messageHash = $isSpot ? $spotMessageHash : $swapMessageHash;
            $subscriptionHash = $isSpot ? $spotSubHash : $swapSubHash;
            $url = $this->get_user_stream_url();
            $client = $this->client($url);
            $this->set_balance_cache($client, $marketType, $subscriptionHash, $params);
            $client->future ($type . ':fetchBalanceSnapshot');
            return Async\await($this->watch($url, $messageHash, $params, $subscriptionHash));
        }) ();
    }

    public function set_balance_cache(Client $client, $marketType, ?string $subscriptionHash = null, $params = array ()) {
        if (is_array($client->subscriptions) && array_key_exists($subscriptionHash, $client->subscriptions)) {
            return;
        }
        $type = ($marketType === 'spot') ? 'spot' : 'contract';
        $messageHash = $type . ':fetchBalanceSnapshot';
        if (!(is_array($client->futures) && array_key_exists($messageHash, $client->futures))) {
            $client->future ($messageHash);
            $this->spawn(array($this, 'load_balance_snapshot'), $client, $messageHash, $marketType);
        }
    }

    public function handle_balance(Client $client, $message) {
        //
        // spot
        //
        // array(
        //     {
        //         e => 'outboundAccountInfo',
        //         E => '1758226989725',
        //         T => true,
        //         W => true,
        //         D => true,
        //         B => array(
        //             array(
        //               a => "USDT",
        //               f => "6.37242839",
        //               l => "0",
        //             ),
        //         )
        //     }
        // )
        //
        // contract
        //
        // [
        //     {
        //         e => 'outboundContractAccountInfo',
        //         E => '1758226989742',
        //         T => true,
        //         W => true,
        //         D => true,
        //         B => [ [Object] ]
        //     }
        // ]
        //
        $channel = $this->safe_string($message, 'e');
        $data = $this->safe_list($message, 'B', array());
        $timestamp = $this->safe_integer($message, 'E');
        $type = ($channel === 'outboundContractAccountInfo') ? 'contract' : 'spot';
        if (!(is_array($this->balance) && array_key_exists($type, $this->balance))) {
            $this->balance[$type] = array();
        }
        $this->balance[$type]['info'] = $data;
        $this->balance[$type]['timestamp'] = $timestamp;
        $this->balance[$type]['datetime'] = $this->iso8601($timestamp);
        for ($i = 0; $i < count($data); $i++) {
            $balance = $data[$i];
            $currencyId = $this->safe_string($balance, 'a');
            $code = $this->safe_currency_code($currencyId);
            $account = $this->account();
            $account['info'] = $balance;
            $account['used'] = $this->safe_string($balance, 'l');
            $account['free'] = $this->safe_string($balance, 'f');
            $this->balance[$type][$code] = $account;
        }
        $this->balance[$type] = $this->safe_balance($this->balance[$type]);
        $client->resolve ($this->balance[$type], $type . ':balance');
    }

    public function load_balance_snapshot($client, $messageHash, $marketType) {
        return Async\async(function () use ($client, $messageHash, $marketType) {
            $response = Async\await($this->fetch_balance(array( 'type' => $marketType )));
            $type = ($marketType === 'spot') ? 'spot' : 'contract';
            $this->balance[$type] = $this->extend($response, $this->safe_dict($this->balance, $type, array()));
            // don't remove the $future from the .futures cache
            $future = $client->futures[$messageHash];
            $future->resolve ();
            $client->resolve ($this->balance[$type], $type . ':fetchBalanceSnapshot');
            $client->resolve ($this->balance[$type], $type . ':balance'); // we should also resolve right away after snapshot, so user doesn't double-fetch balance
        }) ();
    }

    public function watch_orders(?string $symbol = null, ?int $since = null, ?int $limit = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbol, $since, $limit, $params) {
            /**
             * watches information on multiple $orders made by the user
             *
             * @see https://toobit-docs.github.io/apidocs/spot/v1/en/#payload-order-update
             *
             * @param {string} $symbol unified $market $symbol of the $market $orders were made in
             * @param {int} [$since] the earliest time in ms to fetch $orders for
             * @param {int} [$limit] the maximum number of order structures to retrieve
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @return {array[]} a list of ~@link https://docs.ccxt.com/#/?id=order-structure order structures~
             */
            Async\await($this->load_markets());
            Async\await($this->authenticate());
            $market = $this->market_or_null($symbol);
            $symbol = $this->safe_string($market, 'symbol', $symbol);
            $messageHash = 'orders';
            if ($symbol !== null) {
                $messageHash = $messageHash . ':' . $symbol;
            }
            $url = $this->get_user_stream_url();
            $orders = Async\await($this->watch($url, $messageHash, $params, $messageHash));
            if ($this->newUpdates) {
                $limit = $orders->getLimit ($symbol, $limit);
            }
            return $this->filter_by_symbol_since_limit($orders, $symbol, $since, $limit, true);
        }) ();
    }

    public function handle_order(Client $client, $message) {
        //
        //    {
        //        "e" => "executionReport",
        //        "E" => "1758311011844",
        //        "s" => "DOGEUSDT",
        //        "c" => "1758311011948",
        //        "S" => "BUY",
        //        "o" => "LIMIT",
        //        "f" => "GTC",
        //        "q" => "22",
        //        "p" => "0.23",
        //        "pt" => "INPUT",
        //        "X" => "NEW",
        //        "i" => "2043255292855185152",
        //        "l" => "0", // Last executed quantity
        //        "z" => "0", // Cumulative filled quantity
        //        "L" => "0", // Last executed price
        //        "n" => "0",
        //        "N" => "",
        //        "u" => true,
        //        "w" => true,
        //        "m" => false,
        //        "O" => "1758311011833",
        //        "U" => "1758311011841",
        //        "Z" => "0",
        //        "C" => false,
        //        "v" => "0",
        //        "rp" => "0",
        //        "td" => "0"
        //    }
        //
        if ($this->orders === null) {
            $limit = $this->safe_integer($this->options, 'ordersLimit', 1000);
            $this->orders = new ArrayCacheBySymbolById ($limit);
        }
        $orders = $this->orders;
        $order = $this->parse_ws_order($message);
        $orders->append ($order);
        $messageHash = 'orders';
        $client->resolve ($orders, $messageHash);
        $messageHash = 'orders:' . $this->safe_string($order, 'symbol');
        $client->resolve ($orders, $messageHash);
    }

    public function parse_ws_order($order, $market = null) {
        $timestamp = $this->safe_integer($order, 'O');
        $marketId = $this->safe_string($order, 's');
        $symbol = $this->safe_symbol($marketId, $market);
        $priceType = $this->safe_string_lower($order, 'pt');
        $rawOrderType = $this->safe_string_lower($order, 'o');
        $orderType = null;
        if ($priceType === 'market') {
            $orderType = 'market';
        } else {
            $orderType = $rawOrderType;
        }
        $feeCost = $this->safe_number($order, 'n');
        $fee = null;
        if ($feeCost !== null) {
            $fee = array(
                'cost' => $feeCost,
                'currency' => null,
            );
        }
        return $this->safe_order(array(
            'info' => $order,
            'id' => $this->safe_string($order, 'i'),
            'clientOrderId' => $this->safe_string($order, 'c'),
            'timestamp' => $timestamp,
            'datetime' => $this->iso8601($timestamp),
            'lastUpdateTimestamp' => $this->safe_integer_2($order, 'U', 'E'),
            'symbol' => $symbol,
            'type' => $orderType,
            'timeInForce' => $this->safe_string_upper($order, 'f'),
            'postOnly' => null,
            'side' => $this->safe_string_lower($order, 'S'),
            'price' => $this->safe_string($order, 'L'),
            'stopPrice' => null,
            'triggerPrice' => null,
            'amount' => $this->safe_string($order, 'q'),
            'cost' => null,
            'average' => $this->safe_string($order, 'p'),
            'filled' => $this->safe_string($order, 'z'),
            'remaining' => null,
            'status' => $this->parse_order_status($this->safe_string($order, 'X')),
            'fee' => $fee,
            'trades' => null,
        ), $market);
    }

    public function watch_my_trades(?string $symbol = null, ?int $since = null, ?int $limit = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbol, $since, $limit, $params) {
            /**
             * watches information on multiple $trades made by the user
             *
             * @see https://toobit-docs.github.io/apidocs/spot/v1/en/#payload-ticket-push
             *
             * @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
             * @param {boolean} [$params->unifiedMargin] use unified margin account
             * @return {array[]} a list of ~@link https://docs.ccxt.com/#/?id=trade-structure trade structures~
             */
            Async\await($this->load_markets());
            Async\await($this->authenticate());
            $market = $this->market_or_null($symbol);
            $symbol = $this->safe_string($market, 'symbol', $symbol);
            $messageHash = 'myTrades';
            if ($symbol !== null) {
                $messageHash = $messageHash . ':' . $symbol;
            }
            $url = $this->get_user_stream_url();
            $trades = Async\await($this->watch($url, $messageHash, $params, $messageHash));
            if ($this->newUpdates) {
                $limit = $trades->getLimit ($symbol, $limit);
            }
            return $this->filter_by_since_limit($trades, $since, $limit, 'timestamp', true);
        }) ();
    }

    public function handle_my_trade(Client $client, $message) {
        //
        //    {
        //        "e" => "ticketInfo",
        //        "E" => "1758314657847",
        //        "s" => "DOGEUSDT",
        //        "q" => "22.0",
        //        "t" => "1758314657842",
        //        "p" => "0.26667",
        //        "T" => "4864732022877055421",
        //        "o" => "2043285877770284800",
        //        "c" => "1758314657002",
        //        "a" => "1783404067076253952",
        //        "m" => false,
        //        "S" => "BUY"
        //    }
        //
        $myTrades = $this->myTrades;
        if ($myTrades === null) {
            $limit = $this->safe_integer($this->options, 'tradesLimit', 1000);
            $myTrades = new ArrayCacheBySymbolById ($limit);
        }
        $trade = $this->parse_my_trade($message);
        $myTrades->append ($trade);
        $messageHash = 'myTrades:' . $trade['symbol'];
        $client->resolve ($myTrades, $messageHash);
        $messageHash = 'myTrades';
        $client->resolve ($myTrades, $messageHash);
    }

    public function parse_my_trade($trade, $market = null) {
        $marketId = $this->safe_string($trade, 's');
        $ts = $this->safe_string($trade, 't');
        return $this->safe_trade(array(
            'info' => $trade,
            'id' => $this->safe_string($trade, 'T'),
            'timestamp' => $ts,
            'datetime' => $this->iso8601($ts),
            'symbol' => $this->safe_symbol($marketId, $market),
            'order' => $this->safe_string($trade, 'o'),
            'type' => null,
            'side' => $this->safe_string_lower($trade, 'S'),
            'takerOrMaker' => $this->safe_bool($trade, 'm') ? 'maker' : 'taker',
            'price' => $this->safe_string($trade, 'p'),
            'amount' => $this->safe_string($trade, 'q'),
            'cost' => null,
            'fee' => null,
        ), $market);
    }

    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://toobit-docs.github.io/apidocs/usdt_swap/v1/en/#event-position-update
             *
             * watch all open positions
             * @param {string[]} [$symbols] list of unified market $symbols
             * @param {int} [$since] the earliest time in ms to fetch positions for
             * @param {int} [$limit] the maximum number of positions to retrieve
             * @param {array} $params extra parameters specific to the exchange API endpoint
             * @return {array[]} a list of {@link https://docs.ccxt.com/en/latest/manual.html#position-structure position structure}
             */
            Async\await($this->load_markets());
            Async\await($this->authenticate());
            $messageHash = '';
            if (!$this->is_empty($symbols)) {
                $symbols = $this->market_symbols($symbols);
                $messageHash = '::' . implode(',', $symbols);
            }
            $url = $this->get_user_stream_url();
            $client = $this->client($url);
            Async\await($this->authenticate($url));
            $this->set_positions_cache($client, $symbols);
            $cache = $this->positions;
            if ($cache === null) {
                $snapshot = Async\await($client->future ('fetchPositionsSnapshot'));
                return $this->filter_by_symbols_since_limit($snapshot, $symbols, $since, $limit, true);
            }
            $newPositions = Async\await($this->watch($url, $messageHash, null, $messageHash));
            if ($this->newUpdates) {
                return $newPositions;
            }
            return $this->filter_by_symbols_since_limit($cache, $symbols, $since, $limit, true);
        }) ();
    }

    public function set_positions_cache(Client $client, $type, ?array $symbols = null, $isPortfolioMargin = false) {
        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, $isPortfolioMargin);
            }
        } else {
            $this->positions[$type] = new ArrayCacheBySymbolBySide ();
        }
    }

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

    public function handle_positions($client, $message) {
        //
        // array(
        //     {
        //         e => 'outboundContractPositionInfo',
        //         E => '1758316454554',
        //         A => '1783404067076253954',
        //         s => 'DOGE-SWAP-USDT',
        //         S => 'LONG',
        //         p => '0',
        //         P => '0',
        //         a => '0',
        //         f => '0.1228',
        //         m => '0',
        //         r => '0',
        //         up => '0',
        //         pr => '0',
        //         pv => '0',
        //         v => '3.0',
        //         mt => 'CROSS',
        //         mm => '0',
        //         mp => '0.265410000000000000'
        //     }
        // )
        //
        $subscriptions = is_array($client->subscriptions) ? array_keys($client->subscriptions) : array();
        $accountType = $subscriptions[0];
        if ($this->positions === null) {
            $this->positions = array();
        }
        if (!(is_array($this->positions) && array_key_exists($accountType, $this->positions))) {
            $this->positions[$accountType] = new ArrayCacheBySymbolBySide ();
        }
        $cache = $this->positions[$accountType];
        $newPositions = array();
        for ($i = 0; $i < count($message); $i++) {
            $rawPosition = $message[$i];
            $position = $this->parse_ws_position($rawPosition);
            $timestamp = $this->safe_integer($rawPosition, 'E');
            $position['timestamp'] = $timestamp;
            $position['datetime'] = $this->iso8601($timestamp);
            $newPositions[] = $position;
            $cache->append ($position);
        }
        $messageHashes = $this->find_message_hashes($client, $accountType . ':$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, $accountType . ':positions');
    }

    public function parse_ws_position($position, $market = null) {
        $marketId = $this->safe_string($position, 's');
        return $this->safe_position(array(
            'info' => $position,
            'id' => null,
            'symbol' => $this->safe_symbol($marketId, null),
            'notional' => $this->omit_zero($this->safe_string($position, 'pv')),
            'marginMode' => $this->safe_string_lower($position, 'mt'),
            'liquidationPrice' => $this->safe_string($position, 'f'),
            'entryPrice' => $this->safe_string($position, 'p'),
            'unrealizedPnl' => $this->safe_string($position, 'up'),
            'realizedPnl' => $this->safe_number($position, 'r'),
            'percentage' => null,
            'contracts' => null,
            'contractSize' => null,
            'markPrice' => $this->safe_string($position, 'mp'),
            'side' => $this->safe_string_lower($position, 'S'),
            'hedged' => null,
            'timestamp' => null,
            'datetime' => null,
            'maintenanceMargin' => $this->safe_string($position, 'mm'),
            'maintenanceMarginPercentage' => null,
            'collateral' => null,
            'initialMargin' => $this->omit_zero($this->safe_string($position, 'm')),
            'initialMarginPercentage' => null,
            'leverage' => $this->safe_string($position, 'v'),
            'marginRatio' => null,
        ));
    }

    public function authenticate($params = array ()) {
        return Async\async(function () use ($params) {
            $client = $this->client($this->get_user_stream_url());
            $messageHash = 'authenticated';
            $future = $client->reusableFuture ($messageHash);
            $authenticated = $this->safe_value($client->subscriptions, $messageHash);
            if ($authenticated === null) {
                $this->check_required_credentials();
                $time = $this->milliseconds();
                $lastAuthenticatedTime = $this->safe_integer($this->options['ws'], 'lastAuthenticatedTime', 0);
                $listenKeyRefreshRate = $this->safe_integer($this->options['ws'], 'listenKeyRefreshRate', 1200000);
                $delay = $this->sum($listenKeyRefreshRate, 10000);
                if ($time - $lastAuthenticatedTime > $delay) {
                    try {
                        $client->subscriptions[$messageHash] = true;
                        $response = Async\await($this->privatePostApiV1UserDataStream ($params));
                        $this->options['ws']['listenKey'] = $this->safe_string($response, 'listenKey');
                        $this->options['ws']['lastAuthenticatedTime'] = $time;
                        $future->resolve (true);
                        $this->delay($listenKeyRefreshRate, array($this, 'keep_alive_listen_key'), $params);
                    } catch (Exception $e) {
                        $err = new AuthenticationError ($this->id . ' ' . $this->json($e));
                        $client->reject ($err, $messageHash);
                        if (is_array($client->subscriptions) && array_key_exists($messageHash, $client->subscriptions)) {
                            unset($client->subscriptions[$messageHash]);
                        }
                    }
                }
            }
            return Async\await($future);
        }) ();
    }

    public function keep_alive_listen_key($params = array ()) {
        return Async\async(function () use ($params) {
            $options = $this->safe_value($this->options, 'ws', array());
            $listenKey = $this->safe_string($options, 'listenKey');
            if ($listenKey === null) {
                // A network $error happened => we can't renew a listen key that does not exist.
                return;
            }
            try {
                $response = Async\await($this->privatePostApiV1UserDataStream ($params));
                $this->options['ws']['listenKey'] = $this->safe_string($response, 'listenKey');
                $this->options['ws']['lastAuthenticatedTime'] = $this->milliseconds();
            } catch (Exception $error) {
                $url = $this->get_user_stream_url();
                $client = $this->client($url);
                $messageHashes = is_array($client->futures) ? array_keys($client->futures) : array();
                for ($i = 0; $i < count($messageHashes); $i++) {
                    $messageHash = $messageHashes[$i];
                    $client->reject ($error, $messageHash);
                }
                $this->options['ws']['listenKey'] = null;
                $this->options['ws']['lastAuthenticatedTime'] = 0;
                return;
            }
            // whether or not to schedule another $listenKey keepAlive request
            $listenKeyRefreshRate = $this->safe_integer($this->options, 'listenKeyRefreshRate', 1200000);
            $this->delay($listenKeyRefreshRate, array($this, 'keep_alive_listen_key'), $params);
        }) ();
    }

    public function get_user_stream_url() {
        return $this->urls['api']['ws']['common'] . '/api/v1/ws/' . $this->options['ws']['listenKey'];
    }

    public function handle_error_message(Client $client, $message): Bool {
        //
        //    {
        //        "code" => '-100010',
        //        "desc" => "Invalid Symbols!"
        //    }
        //
        $code = $this->safe_string($message, 'code');
        if ($code !== null) {
            $desc = $this->safe_string($message, 'desc');
            $msg = $this->id . ' $code => ' . $code . ' $message => ' . $desc;
            $exception = new ExchangeError ($msg); // c# fix
            $client->reject ($exception);
            return true;
        }
        return false;
    }
}
