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

class whitebit extends \ccxt\async\whitebit {

    public function describe(): mixed {
        return $this->deep_extend(parent::describe(), array(
            'has' => array(
                'ws' => true,
                'watchBalance' => true,
                'watchMyTrades' => true,
                'watchOHLCV' => true,
                'watchOrderBook' => true,
                'watchOrders' => true,
                'watchTicker' => true,
                'watchTickers' => true,
                'watchTrades' => true,
                'watchTradesForSymbols' => false,
            ),
            'urls' => array(
                'api' => array(
                    'ws' => 'wss://api.whitebit.com/ws',
                ),
            ),
            'options' => array(
                'timeframes' => array(
                    '1m' => '60',
                    '5m' => '300',
                    '15m' => '900',
                    '30m' => '1800',
                    '1h' => '3600',
                    '4h' => '14400',
                    '8h' => '28800',
                    '1d' => '86400',
                    '1w' => '604800',
                ),
                'watchOrderBook' => array(
                    'priceInterval' => 0, // "0" - no interval, available values - "0.00000001", "0.0000001", "0.000001", "0.00001", "0.0001", "0.001", "0.01", "0.1"
                ),
            ),
            'streaming' => array(
                'ping' => array($this, 'ping'),
            ),
            'exceptions' => array(
                'ws' => array(
                    'exact' => array(
                        '1' => '\\ccxt\\BadRequest', // array( error => array( code => 1, message => 'invalid argument' ), result => null, id => 1656404342 )
                        '2' => '\\ccxt\\BadRequest', // array( error => array( code => 2, message => 'internal error' ), result => null, id => 1656404075 )
                        '4' => '\\ccxt\\BadRequest', // array( error => array( code => 4, message => 'method not found' ), result => null, id => 1656404250 )
                        '6' => '\\ccxt\\AuthenticationError', // array( error => array( code => 6, message => 'require authentication' ), result => null, id => 1656404076 )
                    ),
                ),
            ),
        ));
    }

    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://docs.whitebit.com/public/websocket/#kline
             *
             * @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'];
            $timeframes = $this->safe_value($this->options, 'timeframes', array());
            $interval = $this->safe_integer($timeframes, $timeframe);
            $marketId = $market['id'];
            // currently there is no way of knowing
            // the $interval upon getting an update
            // so that can't be part of the message hash, and the user can only subscribe
            // to one $timeframe per $symbol
            $messageHash = 'candles:' . $symbol;
            $reqParams = array( $marketId, $interval );
            $method = 'candles_subscribe';
            $ohlcv = Async\await($this->watch_public($messageHash, $method, $reqParams, $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) {
        //
        // {
        //     "method" => "candles_update",
        //     "params" => array(
        //       array(
        //         1655204760,
        //         "22374.15",
        //         "22351.34",
        //         "22374.27",
        //         "22342.52",
        //         "30.213426",
        //         "675499.29718947",
        //         "BTC_USDT"
        //       )
        //     ),
        //     "id" => null
        // }
        //
        $params = $this->safe_value($message, 'params', array());
        for ($i = 0; $i < count($params); $i++) {
            $data = $params[$i];
            $marketId = $this->safe_string($data, 7);
            $market = $this->safe_market($marketId);
            $symbol = $market['symbol'];
            $messageHash = 'candles' . ':' . $symbol;
            $parsed = $this->parse_ohlcv($data, $market);
            // $this->ohlcvs[$symbol] = $this->safe_value($this->ohlcvs, $symbol);
            if (!(is_array($this->ohlcvs) && array_key_exists($symbol, $this->ohlcvs))) {
                $this->ohlcvs[$symbol] = array();
            }
            // $stored = $this->ohlcvs[$symbol]['unknown']; // we don't know the timeframe but we need to respect the type
            if (!(is_array($this->ohlcvs[$symbol]) && array_key_exists('unknown', $this->ohlcvs[$symbol]))) {
                $limit = $this->safe_integer($this->options, 'OHLCVLimit', 1000);
                $stored = new ArrayCacheByTimestamp ($limit);
                $this->ohlcvs[$symbol]['unknown'] = $stored;
            }
            $ohlcv = $this->ohlcvs[$symbol]['unknown'];
            $ohlcv->append ($parsed);
            $client->resolve ($ohlcv, $messageHash);
        }
        return $message;
    }

    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://docs.whitebit.com/public/websocket/#$market-depth
             *
             * @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);
            if ($limit === null) {
                $limit = 10; // max 100
            }
            $messageHash = 'orderbook' . ':' . $market['symbol'];
            $method = 'depth_subscribe';
            $options = $this->safe_value($this->options, 'watchOrderBook', array());
            $defaultPriceInterval = $this->safe_string($options, 'priceInterval', '0');
            $priceInterval = $this->safe_string($params, 'priceInterval', $defaultPriceInterval);
            $params = $this->omit($params, 'priceInterval');
            $reqParams = [
                $market['id'],
                $limit,
                $priceInterval,
                true, // true for allowing multiple subscriptions
            ];
            $orderbook = Async\await($this->watch_public($messageHash, $method, $reqParams, $params));
            return $orderbook->limit ();
        }) ();
    }

    public function handle_order_book(Client $client, $message) {
        //
        // {
        //     "method":"depth_update",
        //     "params":[
        //        true,
        //        array(
        //           "timestamp" => 1708679568.940867,
        //           "asks":[
        //              [ "21252.45","0.01957"],
        //              ["21252.55","0.126205"],
        //              ["21252.66","0.222689"],
        //              ["21252.76","0.185358"],
        //              ["21252.87","0.210077"],
        //              ["21252.98","0.303991"],
        //              ["21253.08","0.327909"],
        //              ["21253.19","0.399007"],
        //              ["21253.3","0.427695"],
        //              ["21253.4","0.492901"]
        //           ],
        //           "bids":[
        //              ["21248.82","0.22"],
        //              ["21248.73","0.000467"],
        //              ["21248.62","0.100864"],
        //              ["21248.51","0.061436"],
        //              ["21248.42","0.091"],
        //              ["21248.41","0.126839"],
        //              ["21248.3","0.063511"],
        //              ["21248.2","0.110547"],
        //              ["21248","0.25257"],
        //              ["21247.7","1.71813"]
        //           ]
        //        ),
        //        "BTC_USDT"
        //     ],
        //     "id":null
        //  }
        //
        $params = $this->safe_value($message, 'params', array());
        $isSnapshot = $this->safe_value($params, 0);
        $marketId = $this->safe_string($params, 2);
        $market = $this->safe_market($marketId);
        $symbol = $market['symbol'];
        $data = $this->safe_value($params, 1);
        $timestamp = $this->safe_timestamp($data, 'timestamp');
        if (!(is_array($this->orderbooks) && array_key_exists($symbol, $this->orderbooks))) {
            $ob = $this->order_book();
            $this->orderbooks[$symbol] = $ob;
        }
        $orderbook = $this->orderbooks[$symbol];
        $orderbook['timestamp'] = $timestamp;
        $orderbook['datetime'] = $this->iso8601($timestamp);
        if ($isSnapshot) {
            $snapshot = $this->parse_order_book($data, $symbol);
            $orderbook->reset ($snapshot);
        } else {
            $asks = $this->safe_value($data, 'asks', array());
            $bids = $this->safe_value($data, 'bids', array());
            $this->handle_deltas($orderbook['asks'], $asks);
            $this->handle_deltas($orderbook['bids'], $bids);
        }
        $messageHash = 'orderbook' . ':' . $symbol;
        $client->resolve ($orderbook, $messageHash);
    }

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

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

    public function watch_ticker(string $symbol, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbol, $params) {
            /**
             * watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific $market
             *
             * @see https://docs.whitebit.com/public/websocket/#$market-statistics
             *
             * @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'];
            $method = 'market_subscribe';
            $messageHash = 'ticker:' . $symbol;
            // every time we want to subscribe to another $market we have to "re-subscribe" sending it all again
            return Async\await($this->watch_multiple_subscription($messageHash, $method, $symbol, false, $params));
        }) ();
    }

    public function watch_tickers(?array $symbols = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbols, $params) {
            /**
             * watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for all markets of a specific list
             *
             * @see https://docs.whitebit.com/public/websocket/#$market-statistics
             *
             * @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);
            $method = 'market_subscribe';
            $url = $this->urls['api']['ws'];
            $id = $this->nonce();
            $messageHashes = array();
            $args = array();
            for ($i = 0; $i < count($symbols); $i++) {
                $market = $this->market($symbols[$i]);
                $messageHashes[] = 'ticker:' . $market['symbol'];
                $args[] = $market['id'];
            }
            $request = array(
                'id' => $id,
                'method' => $method,
                'params' => $args,
            );
            Async\await($this->watch_multiple($url, $messageHashes, $this->extend($request, $params), $messageHashes));
            return $this->filter_by_array($this->tickers, 'symbol', $symbols);
        }) ();
    }

    public function handle_ticker(Client $client, $message) {
        //
        //   {
        //       "method" => "market_update",
        //       "params" => array(
        //         "BTC_USDT",
        //         {
        //           "close" => "22293.86",
        //           "deal" => "1986990019.96552952",
        //           "high" => "24360.7",
        //           "last" => "22293.86",
        //           "low" => "20851.44",
        //           "open" => "24076.12",
        //           "period" => 86400,
        //           "volume" => "87016.995668"
        //         }
        //       ),
        //       "id" => null
        //   }
        //
        $tickers = $this->safe_value($message, 'params', array());
        $marketId = $this->safe_string($tickers, 0);
        $market = $this->safe_market($marketId, null);
        $symbol = $market['symbol'];
        $rawTicker = $this->safe_value($tickers, 1, array());
        $messageHash = 'ticker' . ':' . $symbol;
        $ticker = $this->parse_ticker($rawTicker, $market);
        $this->tickers[$symbol] = $ticker;
        // watchTicker
        $client->resolve ($ticker, $messageHash);
        // watchTickers
        $messageHashes = is_array($client->futures) ? array_keys($client->futures) : array();
        for ($i = 0; $i < count($messageHashes); $i++) {
            $currentMessageHash = $messageHashes[$i];
            if (mb_strpos($currentMessageHash, 'tickers') !== false && mb_strpos($currentMessageHash, $symbol) !== false) {
                // Example => user calls watchTickers with ['LTC/USDT', 'ETH/USDT']
                // the associated messagehash will be => 'tickers:LTC/USDT:ETH/USDT'
                // since we only have access to a single $symbol at a time
                // we have to do a reverse lookup into the $tickers hashes
                // and check if the current $symbol is a part of one or more
                // $tickers hashes and resolve them
                // user might have multiple watchTickers promises
                // watchTickers ( ['LTC/USDT', 'ETH/USDT'] ), watchTickers ( ['ETC/USDT', 'DOGE/USDT'] )
                // and we want to make sure we resolve only the correct ones
                $client->resolve ($ticker, $currentMessageHash);
            }
        }
        return $message;
    }

    public function watch_trades(string $symbol, ?int $since = null, ?int $limit = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbol, $since, $limit, $params) {
            /**
             * get the list of most recent $trades for a particular $symbol
             *
             * @see https://docs.whitebit.com/public/websocket/#$market-$trades
             *
             * @param {string} $symbol unified $symbol of the $market to fetch $trades for
             * @param {int} [$since] timestamp in ms of the earliest trade to fetch
             * @param {int} [$limit] the maximum amount of $trades to fetch
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @return {array[]} a list of ~@link https://docs.ccxt.com/#/?id=public-$trades trade structures~
             */
            Async\await($this->load_markets());
            $market = $this->market($symbol);
            $symbol = $market['symbol'];
            $messageHash = 'trades' . ':' . $symbol;
            $method = 'trades_subscribe';
            // every time we want to subscribe to another $market we have to 're-subscribe' sending it all again
            $trades = Async\await($this->watch_multiple_subscription($messageHash, $method, $symbol, false, $params));
            if ($this->newUpdates) {
                $limit = $trades->getLimit ($symbol, $limit);
            }
            return $this->filter_by_since_limit($trades, $since, $limit, 'timestamp', true);
        }) ();
    }

    public function handle_trades(Client $client, $message) {
        //
        //    {
        //        "method":"trades_update",
        //        "params":array(
        //           "BTC_USDT",
        //           array(
        //              array(
        //                 "id":1900632398,
        //                 "time":1656320231.404343,
        //                 "price":"21443.04",
        //                 "amount":"0.072844",
        //                 "type":"buy"
        //              ),
        //              {
        //                 "id":1900632397,
        //                 "time":1656320231.400001,
        //                 "price":"21443.15",
        //                 "amount":"0.060757",
        //                 "type":"buy"
        //              }
        //           )
        //        )
        //    }
        //
        $params = $this->safe_value($message, 'params', array());
        $marketId = $this->safe_string($params, 0);
        $market = $this->safe_market($marketId);
        $symbol = $market['symbol'];
        $stored = $this->safe_value($this->trades, $symbol);
        if ($stored === null) {
            $limit = $this->safe_integer($this->options, 'tradesLimit', 1000);
            $stored = new ArrayCache ($limit);
            $this->trades[$symbol] = $stored;
        }
        $data = $this->safe_value($params, 1, array());
        $parsedTrades = $this->parse_trades($data, $market);
        for ($j = 0; $j < count($parsedTrades); $j++) {
            $stored->append ($parsedTrades[$j]);
        }
        $messageHash = 'trades:' . $market['symbol'];
        $client->resolve ($stored, $messageHash);
    }

    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 $trades made by the user
             *
             * @see https://docs.whitebit.com/private/websocket/#deals
             *
             * @param {str} $symbol unified $market $symbol
             * @param {int} [$since] the earliest time in ms to fetch $trades for
             * @param {int} [$limit] the maximum number of $trades structures to retrieve
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @return {array[]} a list of ~@link https://docs.ccxt.com/#/?id=trade-structure trade structures~
             */
            if ($symbol === null) {
                throw new ArgumentsRequired($this->id . ' watchMyTrades() requires a $symbol argument');
            }
            Async\await($this->load_markets());
            Async\await($this->authenticate());
            $market = $this->market($symbol);
            $symbol = $market['symbol'];
            $messageHash = 'myTrades:' . $symbol;
            $method = 'deals_subscribe';
            $trades = Async\await($this->watch_multiple_subscription($messageHash, $method, $symbol, true, $params));
            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, $subscription = null) {
        //
        //   {
        //       "method" => "deals_update",
        //       "params" => array(
        //         1894994106,
        //         1656151427.729706,
        //         "LTC_USDT",
        //         96624037337,
        //         "56.78",
        //         "0.16717",
        //         "0.0094919126",
        //         ''
        //       ),
        //       "id" => null
        //   }
        //
        $trade = $this->safe_value($message, 'params');
        if ($this->myTrades === null) {
            $limit = $this->safe_integer($this->options, 'tradesLimit', 1000);
            $this->myTrades = new ArrayCache ($limit);
        }
        $stored = $this->myTrades;
        $parsed = $this->parse_ws_trade($trade);
        $stored->append ($parsed);
        $symbol = $parsed['symbol'];
        $messageHash = 'myTrades:' . $symbol;
        $client->resolve ($stored, $messageHash);
    }

    public function parse_ws_trade($trade, $market = null) {
        //
        //   array(
        //         1894994106, // $id
        //         1656151427.729706, // deal time
        //         "LTC_USDT", // symbol
        //         96624037337, // order $id
        //         "56.78", // $price
        //         "0.16717", // $amount
        //         "0.0094919126", // $fee
        //         '' // client order $id
        //    )
        //
        $orderId = $this->safe_string($trade, 3);
        $timestamp = $this->safe_timestamp($trade, 1);
        $id = $this->safe_string($trade, 0);
        $price = $this->safe_string($trade, 4);
        $amount = $this->safe_string($trade, 5);
        $marketId = $this->safe_string($trade, 2);
        $market = $this->safe_market($marketId, $market);
        $fee = null;
        $feeCost = $this->safe_string($trade, 6);
        if ($feeCost !== null) {
            $fee = array(
                'cost' => $feeCost,
                'currency' => $market['quote'],
            );
        }
        return $this->safe_trade(array(
            'id' => $id,
            'info' => $trade,
            'timestamp' => $timestamp,
            'datetime' => $this->iso8601($timestamp),
            'symbol' => $market['symbol'],
            'order' => $orderId,
            'type' => null,
            'side' => null,
            'takerOrMaker' => null,
            'price' => $price,
            'amount' => $amount,
            'cost' => null,
            'fee' => $fee,
        ), $market);
    }

    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://docs.whitebit.com/private/websocket/#orders-pending
             *
             * @param {string} $symbol unified $market $symbol of the $market orders were made in
             * @param {int} [$since] the earliest time in ms to fetch orders for
             * @param {int} [$limit] the maximum number of order structures to retrieve
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @return {array[]} a list of ~@link https://docs.ccxt.com/#/?id=order-structure order structures~
             */
            if ($symbol === null) {
                throw new ArgumentsRequired($this->id . ' watchOrders() requires a $symbol argument');
            }
            Async\await($this->load_markets());
            Async\await($this->authenticate());
            $market = $this->market($symbol);
            $symbol = $market['symbol'];
            $messageHash = 'orders:' . $symbol;
            $method = 'ordersPending_subscribe';
            $trades = Async\await($this->watch_multiple_subscription($messageHash, $method, $symbol, false, $params));
            if ($this->newUpdates) {
                $limit = $trades->getLimit ($symbol, $limit);
            }
            return $this->filter_by_symbol_since_limit($trades, $symbol, $since, $limit, true);
        }) ();
    }

    public function handle_order(Client $client, $message, $subscription = null) {
        //
        // {
        //     "method" => "ordersPending_update",
        //     "params" => array(
        //       1, // 1 = new, 2 = update 3 = cancel or execute
        //       {
        //         "id" => 96433622651,
        //         "market" => "LTC_USDT",
        //         "type" => 1,
        //         "side" => 2,
        //         "ctime" => 1656092215.39375,
        //         "mtime" => 1656092215.39375,
        //         "price" => "25",
        //         "amount" => "0.202",
        //         "taker_fee" => "0.001",
        //         "maker_fee" => "0.001",
        //         "left" => "0.202",
        //         "deal_stock" => "0",
        //         "deal_money" => "0",
        //         "deal_fee" => "0",
        //         "client_order_id" => ''
        //       }
        //     )
        //     "id" => null
        // }
        //
        $params = $this->safe_value($message, 'params', array());
        $data = $this->safe_value($params, 1);
        if ($this->orders === null) {
            $limit = $this->safe_integer($this->options, 'ordersLimit', 1000);
            $this->orders = new ArrayCacheBySymbolById ($limit);
        }
        $stored = $this->orders;
        $status = $this->safe_integer($params, 0);
        $parsed = $this->parse_ws_order($this->extend($data, array( 'status' => $status )));
        $stored->append ($parsed);
        $symbol = $parsed['symbol'];
        $messageHash = 'orders:' . $symbol;
        $client->resolve ($this->orders, $messageHash);
    }

    public function parse_ws_order($order, $market = null) {
        //
        //   {
        //         "id" => 96433622651,
        //         "market" => "LTC_USDT",
        //         "type" => 1,
        //         "side" => 2, //1- sell 2-buy
        //         "ctime" => 1656092215.39375,
        //         "mtime" => 1656092215.39375,
        //         "price" => "25",
        //         "amount" => "0.202",
        //         "taker_fee" => "0.001",
        //         "maker_fee" => "0.001",
        //         "left" => "0.202",
        //         "deal_stock" => "0",
        //         "deal_money" => "0",
        //         "deal_fee" => "0",
        //         "activation_price" => "40",
        //         "activation_condition" => "lte",
        //         "client_order_id" => ''
        //         "status" => 1, // 1 = new, 2 = update 3 = cancel or execute
        //    }
        //
        $status = $this->safe_integer($order, 'status');
        $marketId = $this->safe_string($order, 'market');
        $market = $this->safe_market($marketId, $market);
        $id = $this->safe_string($order, 'id');
        $clientOrderId = $this->omit_zero($this->safe_string($order, 'client_order_id'));
        $price = $this->safe_string($order, 'price');
        $filled = $this->safe_string($order, 'deal_stock');
        $cost = $this->safe_string($order, 'deal_money');
        $stopPrice = $this->safe_string($order, 'activation_price');
        $rawType = $this->safe_string($order, 'type');
        $type = $this->parse_ws_order_type($rawType);
        $amount = null;
        $remaining = null;
        if ($type === 'market') {
            $amount = $this->safe_string($order, 'deal_stock');
            $remaining = '0';
        } else {
            $remaining = $this->safe_string($order, 'left');
            $amount = $this->safe_string($order, 'amount');
        }
        $timestamp = $this->safe_timestamp($order, 'ctime');
        $lastTradeTimestamp = $this->safe_timestamp($order, 'mtime');
        $symbol = $market['symbol'];
        $rawSide = $this->safe_integer($order, 'side');
        $side = ($rawSide === 1) ? 'sell' : 'buy';
        $dealFee = $this->safe_string($order, 'deal_fee');
        $fee = null;
        if ($dealFee !== null) {
            $fee = array(
                'cost' => $this->parse_number($dealFee),
                'currency' => $market['quote'],
            );
        }
        $unifiedStatus = null;
        if (($status === 1) || ($status === 2)) {
            $unifiedStatus = 'open';
        } else {
            if (Precise::string_equals($remaining, '0')) {
                $unifiedStatus = 'closed';
            } else {
                $unifiedStatus = 'canceled';
            }
        }
        return $this->safe_order(array(
            'info' => $order,
            'symbol' => $symbol,
            'id' => $id,
            'clientOrderId' => $clientOrderId,
            'timestamp' => $timestamp,
            'datetime' => $this->iso8601($timestamp),
            'lastTradeTimestamp' => $lastTradeTimestamp,
            'type' => $type,
            'timeInForce' => null,
            'postOnly' => null,
            'side' => $side,
            'price' => $price,
            'stopPrice' => $stopPrice,
            'triggerPrice' => $stopPrice,
            'amount' => $amount,
            'cost' => $cost,
            'average' => null,
            'filled' => $filled,
            'remaining' => $remaining,
            'status' => $unifiedStatus,
            'fee' => $fee,
            'trades' => null,
        ), $market);
    }

    public function parse_ws_order_type($status) {
        $statuses = array(
            '1' => 'limit',
            '2' => 'market',
            '202' => 'market',
            '3' => 'limit',
            '4' => 'market',
            '5' => 'limit',
            '6' => 'market',
            '8' => 'limit',
            '10' => 'market',
        );
        return $this->safe_string($statuses, $status, $status);
    }

    public function watch_balance($params = array ()): PromiseInterface {
        return Async\async(function () use ($params) {
            /**
             * watch balance and get the amount of funds available for trading or funds locked in orders
             *
             * @see https://docs.whitebit.com/private/websocket/#balance-spot
             * @see https://docs.whitebit.com/private/websocket/#balance-margin
             *
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @param {str} [$params->type] spot or contract if not provided $this->options['defaultType'] is used
             * @return {array} a ~@link https://docs.ccxt.com/#/?id=balance-structure balance structure~
             */
            Async\await($this->load_markets());
            $type = null;
            list($type, $params) = $this->handle_market_type_and_params('watchBalance', null, $params);
            $messageHash = 'wallet:';
            $method = null;
            if ($type === 'spot') {
                $method = 'balanceSpot_subscribe';
                $messageHash .= 'spot';
            } else {
                $method = 'balanceMargin_subscribe';
                $messageHash .= 'margin';
            }
            $currencies = is_array($this->currencies) ? array_keys($this->currencies) : array();
            return Async\await($this->watch_private($messageHash, $method, $currencies, $params));
        }) ();
    }

    public function handle_balance(Client $client, $message) {
        //
        //   {
        //       "method":"balanceSpot_update",
        //       "params":array(
        //          {
        //             "LTC":{
        //                "available":"0.16587",
        //                "freeze":"0"
        //             }
        //          }
        //       ),
        //       "id":null
        //   }
        //
        $method = $this->safe_string($message, 'method');
        $data = $this->safe_value($message, 'params');
        $balanceDict = $this->safe_value($data, 0);
        $this->balance['info'] = $balanceDict;
        $keys = is_array($balanceDict) ? array_keys($balanceDict) : array();
        $currencyId = $this->safe_value($keys, 0);
        $rawBalance = $this->safe_value($balanceDict, $currencyId);
        $code = $this->safe_currency_code($currencyId);
        $account = $this->account();
        $account['free'] = $this->safe_string($rawBalance, 'available');
        $account['used'] = $this->safe_string($rawBalance, 'freeze');
        $this->balance[$code] = $account;
        $this->balance = $this->safe_balance($this->balance);
        $messageHash = 'wallet:';
        if (mb_strpos($method, 'Spot') !== false) {
            $messageHash .= 'spot';
        } else {
            $messageHash .= 'margin';
        }
        $client->resolve ($this->balance, $messageHash);
    }

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

    public function watch_multiple_subscription($messageHash, $method, $symbol, $isNested = false, $params = array ()) {
        return Async\async(function () use ($messageHash, $method, $symbol, $isNested, $params) {
            Async\await($this->load_markets());
            $url = $this->urls['api']['ws'];
            $id = $this->nonce();
            $client = $this->safe_value($this->clients, $url);
            $request = null;
            $marketIds = array();
            if ($client === null) {
                $subscription = array();
                $market = $this->market($symbol);
                $marketId = $market['id'];
                $subscription[$marketId] = true;
                $marketIds = array( $marketId );
                if ($isNested) {
                    $marketIds = array( $marketIds );
                }
                $request = array(
                    'id' => $id,
                    'method' => $method,
                    'params' => $marketIds,
                );
                $message = $this->extend($request, $params);
                return Async\await($this->watch($url, $messageHash, $message, $method, $subscription));
            } else {
                $subscription = $this->safe_value($client->subscriptions, $method, array());
                $hasSymbolSubscription = true;
                $market = $this->market($symbol);
                $marketId = $market['id'];
                $isSubscribed = $this->safe_bool($subscription, $marketId, false);
                if (!$isSubscribed) {
                    $subscription[$marketId] = true;
                    $hasSymbolSubscription = false;
                }
                if ($hasSymbolSubscription) {
                    // already subscribed to this $market(s)
                    return Async\await($this->watch($url, $messageHash, $request, $method, $subscription));
                } else {
                    // resubscribe
                    $marketIdsNew = array();
                    $marketIdsNew = is_array($subscription) ? array_keys($subscription) : array();
                    if ($isNested) {
                        $marketIdsNew = array( $marketIdsNew );
                    }
                    $resubRequest = array(
                        'id' => $id,
                        'method' => $method,
                        'params' => $marketIdsNew,
                    );
                    if (is_array($client->subscriptions) && array_key_exists($method, $client->subscriptions)) {
                        unset($client->subscriptions[$method]);
                    }
                    return Async\await($this->watch($url, $messageHash, $resubRequest, $method, $subscription));
                }
            }
        }) ();
    }

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

    public function authenticate($params = array ()) {
        return Async\async(function () use ($params) {
            $this->check_required_credentials();
            $url = $this->urls['api']['ws'];
            $messageHash = 'authenticated';
            $client = $this->client($url);
            $future = $client->reusableFuture ('authenticated');
            $authenticated = $this->safe_value($client->subscriptions, $messageHash);
            if ($authenticated === null) {
                $authToken = Async\await($this->v4PrivatePostProfileWebsocketToken ());
                //
                //   {
                //       "websocket_token" => "$2y$10$lxCvTXig/XrcTBFY1bdFseCKQmFTDtCpEzHNVnXowGplExFxPJp9y"
                //   }
                //
                $token = $this->safe_string($authToken, 'websocket_token');
                $id = $this->nonce();
                $request = array(
                    'id' => $id,
                    'method' => 'authorize',
                    'params' => array(
                        $token,
                        'public',
                    ),
                );
                $subscription = array(
                    'id' => $id,
                    'method' => array($this, 'handle_authenticate'),
                );
                try {
                    Async\await($this->watch($url, $messageHash, $request, $messageHash, $subscription));
                } catch (Exception $e) {
                    unset($client->subscriptions[$messageHash]);
                    $future->reject ($e);
                }
            }
            return Async\await($future);
        }) ();
    }

    public function handle_authenticate(Client $client, $message) {
        //
        //     array( error => null, result => array( status => "success" ), id => 1656084550 )
        //
        $future = $client->futures['authenticated'];
        $future->resolve (1);
        return $message;
    }

    public function handle_error_message(Client $client, $message): Bool {
        //
        //     {
        //         "error" => array( $code => 1, $message => "invalid argument" ),
        //         "result" => null,
        //         "id" => 1656090882
        //     }
        //
        $error = $this->safe_value($message, 'error');
        try {
            if ($error !== null) {
                $code = $this->safe_string($message, 'code');
                $feedback = $this->id . ' ' . $this->json($message);
                $this->throw_exactly_matched_exception($this->exceptions['ws']['exact'], $code, $feedback);
            }
        } catch (Exception $e) {
            if ($e instanceof AuthenticationError) {
                $client->reject ($e, 'authenticated');
                if (is_array($client->subscriptions) && array_key_exists('authenticated', $client->subscriptions)) {
                    unset($client->subscriptions['authenticated']);
                }
                return false;
            }
        }
        return $message;
    }

    public function handle_message(Client $client, $message) {
        //
        // auth
        //    array( error => null, $result => array( status => "success" ), $id => 1656084550 )
        //
        // pong
        //    array( error => null, $result => "pong", $id => 0 )
        //
        if (!$this->handle_error_message($client, $message)) {
            return;
        }
        $result = $this->safe_string($message, 'result');
        if ($result === 'pong') {
            $this->handle_pong($client, $message);
            return;
        }
        $id = $this->safe_integer($message, 'id');
        if ($id !== null) {
            $this->handle_subscription_status($client, $message, $id);
            return;
        }
        $methods = array(
            'market_update' => array($this, 'handle_ticker'),
            'trades_update' => array($this, 'handle_trades'),
            'depth_update' => array($this, 'handle_order_book'),
            'candles_update' => array($this, 'handle_ohlcv'),
            'ordersPending_update' => array($this, 'handle_order'),
            'ordersExecuted_update' => array($this, 'handle_order'),
            'balanceSpot_update' => array($this, 'handle_balance'),
            'balanceMargin_update' => array($this, 'handle_balance'),
            'deals_update' => array($this, 'handle_my_trades'),
        );
        $topic = $this->safe_value($message, 'method');
        $method = $this->safe_value($methods, $topic);
        if ($method !== null) {
            $method($client, $message);
        }
    }

    public function handle_subscription_status(Client $client, $message, $id) {
        // not every $method stores its $subscription
        // object so we can't do indeById here
        $subs = $client->subscriptions;
        $values = is_array($subs) ? array_values($subs) : array();
        for ($i = 0; $i < count($values); $i++) {
            $subscription = $values[$i];
            if ($subscription !== true) {
                $subId = $this->safe_integer($subscription, 'id');
                if (($subId !== null) && ($subId === $id)) {
                    $method = $this->safe_value($subscription, 'method');
                    if ($method !== null) {
                        $method($client, $message);
                        return;
                    }
                }
            }
        }
    }

    public function handle_pong(Client $client, $message) {
        $client->lastPong = $this->milliseconds();
        return $message;
    }

    public function ping(Client $client) {
        return array(
            'id' => 0,
            'method' => 'ping',
            'params' => array(),
        );
    }
}
