<?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 blockchaincom extends \ccxt\async\blockchaincom {

    public function describe(): mixed {
        return $this->deep_extend(parent::describe(), array(
            'has' => array(
                'ws' => true,
                'watchBalance' => true,
                'watchTicker' => true,
                'watchTickers' => false,
                'watchTrades' => true,
                'watchTradesForSymbols' => false,
                'watchMyTrades' => false,
                'watchOrders' => true,
                'watchOrderBook' => true,
                'watchOHLCV' => true,
            ),
            'urls' => array(
                'api' => array(
                    'ws' => 'wss://ws.blockchain.info/mercury-gateway/v1/ws',
                ),
            ),
            'options' => array(
                'ws' => array(
                    'options' => array(
                        'headers' => array(
                            'Origin' => 'https://exchange.blockchain.com',
                        ),
                    ),
                    'noOriginHeader' => false,
                ),
            ),
            'streaming' => array(
            ),
            'exceptions' => array(
            ),
            'timeframes' => array(
                '1m' => '60',
                '5m' => '300',
                '15m' => '900',
                '1h' => '3600',
                '6h' => '21600',
                '1d' => '86400',
            ),
        ));
    }

    public function watch_balance($params = array ()): PromiseInterface {
        return Async\async(function () use ($params) {
            /**
             * watch balance and get the amount of funds available for trading or funds locked in orders
             *
             * @see https://exchange.blockchain.com/api/#balances
             *
             * @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->authenticate($params));
            $messageHash = 'balance';
            $url = $this->urls['api']['ws'];
            $subscribe = array(
                'action' => 'subscribe',
                'channel' => 'balances',
            );
            $request = $this->deep_extend($subscribe, $params);
            return Async\await($this->watch($url, $messageHash, $request, $messageHash, $request));
        }) ();
    }

    public function handle_balance(Client $client, $message) {
        //
        //  subscribed
        //     {
        //         "seqnum" => 1,
        //         "event" => "subscribed",
        //         "channel" => "balances",
        //         "local_currency" => "USD",
        //         "batching" => false
        //     }
        //  snapshot
        //     {
        //         "seqnum" => 2,
        //         "event" => "snapshot",
        //         "channel" => "balances",
        //         "balances" => array(
        //           array(
        //             "currency" => "BTC",
        //             "balance" => 0.00366963,
        //             "available" => 0.00266963,
        //             "balance_local" => 38.746779155,
        //             "available_local" => 28.188009155,
        //             "rate" => 10558.77
        //           ),
        //            ...
        //         ),
        //         "total_available_local" => 65.477864168,
        //         "total_balance_local" => 87.696634168
        //     }
        //
        $event = $this->safe_string($message, 'event');
        if ($event === 'subscribed') {
            return;
        }
        $result = array( 'info' => $message );
        $balances = $this->safe_value($message, 'balances', array());
        for ($i = 0; $i < count($balances); $i++) {
            $entry = $balances[$i];
            $currencyId = $this->safe_string($entry, 'currency');
            $code = $this->safe_currency_code($currencyId);
            $account = $this->account();
            $account['free'] = $this->safe_string($entry, 'available');
            $account['total'] = $this->safe_string($entry, 'balance');
            $result[$code] = $account;
        }
        $messageHash = 'balance';
        $this->balance = $this->safe_balance($result);
        $client->resolve ($this->balance, $messageHash);
    }

    public function watch_ohlcv(string $symbol, $timeframe = '1m', ?int $since = null, ?int $limit = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbol, $timeframe, $since, $limit, $params) {
            /**
             * watches historical candlestick data containing the open, high, low, and close price, and the volume of a $market->
             *
             * @see https://exchange.blockchain.com/api/#prices
             *
             * @param {string} $symbol unified $symbol of the $market to fetch OHLCV data for
             * @param {string} $timeframe the length of time each candle represents. Allows '1m', '5m', '15m', '1h', '6h' '1d'. Can only watch one $timeframe per $symbol->
             * @param {int} [$since] timestamp in ms of the earliest candle to fetch
             * @param {int} [$limit] the maximum amount of candles to fetch
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @return {int[][]} A list of candles ordered, open, high, low, close, volume
             */
            Async\await($this->load_markets());
            $market = $this->market($symbol);
            $symbol = $market['symbol'];
            $interval = $this->safe_string($this->timeframes, $timeframe, $timeframe);
            $messageHash = 'ohlcv:' . $symbol;
            $request = array(
                'action' => 'subscribe',
                'channel' => 'prices',
                'symbol' => $market['id'],
                'granularity' => $this->parse_number($interval),
            );
            $request = $this->deep_extend($request, $params);
            $url = $this->urls['api']['ws'];
            $ohlcv = Async\await($this->watch($url, $messageHash, $request, $messageHash, $request));
            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) {
        //
        //  subscribed
        //     {
        //         "seqnum" => 0,
        //         "event" => "subscribed",
        //         "channel" => "prices",
        //         "symbol" => "BTC-USDT",
        //         "granularity" => 60
        //     }
        //
        //  updated
        //     {
        //         "seqnum" => 1,
        //         "event" => "updated",
        //         "channel" => "prices",
        //         "symbol" => "BTC-USD",
        //         "price" => array( 1660085580000, 23185.215, 23185.935, 23164.79, 23169.97, 0 )
        //     }
        //
        $event = $this->safe_string($message, 'event');
        if ($event === 'rejected') {
            $jsonMessage = $this->json($message);
            throw new ExchangeError($this->id . ' ' . $jsonMessage);
        } elseif ($event === 'updated') {
            $marketId = $this->safe_string($message, 'symbol');
            $symbol = $this->safe_symbol($marketId, null, '-');
            $messageHash = 'ohlcv:' . $symbol;
            $request = $this->safe_value($client->subscriptions, $messageHash);
            $timeframeId = $this->safe_number($request, 'granularity');
            $timeframe = $this->find_timeframe($timeframeId);
            $ohlcv = $this->safe_value($message, 'price', array());
            $this->ohlcvs[$symbol] = $this->safe_value($this->ohlcvs, $symbol, array());
            $stored = $this->safe_value($this->ohlcvs[$symbol], $timeframe);
            if ($stored === null) {
                $limit = $this->safe_integer($this->options, 'OHLCVLimit', 1000);
                $stored = new ArrayCacheByTimestamp ($limit);
                $this->ohlcvs[$symbol][$timeframe] = $stored;
            }
            $stored->append ($ohlcv);
            $client->resolve ($stored, $messageHash);
        } elseif ($event !== 'subscribed') {
            throw new NotSupported($this->id . ' ' . $this->json($message));
        }
    }

    public function watch_ticker(string $symbol, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbol, $params) {
            /**
             * watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific $market
             *
             * @see https://exchange.blockchain.com/api/#ticker
             *
             * @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'];
            $url = $this->urls['api']['ws'];
            $messageHash = 'ticker:' . $symbol;
            $request = array(
                'action' => 'subscribe',
                'channel' => 'ticker',
                'symbol' => $market['id'],
            );
            $request = $this->deep_extend($request, $params);
            return Async\await($this->watch($url, $messageHash, $request, $messageHash));
        }) ();
    }

    public function handle_ticker(Client $client, $message) {
        //
        //  subscribed
        //     {
        //         "seqnum" => 0,
        //         "event" => "subscribed",
        //         "channel" => "ticker",
        //         "symbol" => "BTC-USD"
        //     }
        //  snapshot
        //     {
        //         "seqnum" => 1,
        //         "event" => "snapshot",
        //         "channel" => "ticker",
        //         "symbol" => "BTC-USD",
        //         "price_24h" => 23071.4,
        //         "volume_24h" => 236.28398636,
        //         "last_trade_price" => 23936.4,
        //         "mark_price" => 23935.335240262
        //     }
        // update
        //     {
        //         "seqnum" => 2,
        //         "event" => "updated",
        //         "channel" => "ticker",
        //         "symbol" => "BTC-USD",
        //         "mark_price" => 23935.242443617
        //     }
        //
        $event = $this->safe_string($message, 'event');
        $marketId = $this->safe_string($message, 'symbol');
        $market = $this->safe_market($marketId);
        $symbol = $market['symbol'];
        $ticker = null;
        if ($event === 'subscribed') {
            return;
        } elseif ($event === 'snapshot') {
            $ticker = $this->parse_ticker($message, $market);
        } elseif ($event === 'updated') {
            $lastTicker = $this->safe_value($this->tickers, $symbol);
            $ticker = $this->parse_ws_updated_ticker($message, $lastTicker, $market);
        }
        $messageHash = 'ticker:' . $symbol;
        $this->tickers[$symbol] = $ticker;
        $client->resolve ($ticker, $messageHash);
    }

    public function parse_ws_updated_ticker($ticker, $lastTicker = null, $market = null) {
        //
        //     {
        //         "seqnum" => 2,
        //         "event" => "updated",
        //         "channel" => "ticker",
        //         "symbol" => "BTC-USD",
        //         "mark_price" => 23935.242443617
        //     }
        //
        $marketId = $this->safe_string($ticker, 'symbol');
        $symbol = $this->safe_symbol($marketId, null, '-');
        $last = $this->safe_string($ticker, 'mark_price');
        return $this->safe_ticker(array(
            'symbol' => $symbol,
            'timestamp' => null,
            'datetime' => null,
            'high' => null,
            'low' => null,
            'bid' => null,
            'bidVolume' => null,
            'ask' => null,
            'askVolume' => null,
            'vwap' => null,
            'open' => $this->safe_string($lastTicker, 'open'),
            'close' => null,
            'last' => $last,
            'previousClose' => $this->safe_string($lastTicker, 'close'),
            'change' => null,
            'percentage' => null,
            'average' => null,
            'baseVolume' => $this->safe_string($lastTicker, 'baseVolume'),
            'quoteVolume' => null,
            'info' => $this->extend($this->safe_value($lastTicker, 'info', array()), $ticker),
        ), $market);
    }

    public function watch_trades(string $symbol, ?int $since = null, ?int $limit = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbol, $since, $limit, $params) {
            /**
             * get the list of most recent $trades for a particular $symbol
             *
             * @see https://exchange.blockchain.com/api/#$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'];
            $url = $this->urls['api']['ws'];
            $messageHash = 'trades:' . $symbol;
            $request = array(
                'action' => 'subscribe',
                'channel' => 'trades',
                'symbol' => $market['id'],
            );
            $request = $this->deep_extend($request, $params);
            $trades = Async\await($this->watch($url, $messageHash, $request, $messageHash, $request));
            return $this->filter_by_since_limit($trades, $since, $limit, 'timestamp', true);
        }) ();
    }

    public function handle_trades(Client $client, $message) {
        //
        //  subscribed
        //     {
        //         "seqnum" => 0,
        //         "event" => "subscribed",
        //         "channel" => "trades",
        //         "symbol" => "BTC-USDT"
        //     }
        //  updates
        //     {
        //         "seqnum" => 1,
        //         "event" => "updated",
        //         "channel" => "trades",
        //         "symbol" => "BTC-USDT",
        //         "timestamp" => "2022-08-08T17:23:48.163096Z",
        //         "side" => "sell",
        //         "qty" => 0.083523,
        //         "price" => 23940.67,
        //         "trade_id" => "563078810223444"
        //     }
        //
        $event = $this->safe_string($message, 'event');
        if ($event !== 'updated') {
            return;
        }
        $marketId = $this->safe_string($message, 'symbol');
        $symbol = $this->safe_symbol($marketId);
        $market = $this->safe_market($marketId);
        $messageHash = 'trades:' . $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;
        }
        $parsed = $this->parse_ws_trade($message, $market);
        $stored->append ($parsed);
        $this->trades[$symbol] = $stored;
        $client->resolve ($this->trades[$symbol], $messageHash);
    }

    public function parse_ws_trade($trade, $market = null) {
        //
        //     {
        //         "seqnum" => 1,
        //         "event" => "updated",
        //         "channel" => "trades",
        //         "symbol" => "BTC-USDT",
        //         "timestamp" => "2022-08-08T17:23:48.163096Z",
        //         "side" => "sell",
        //         "qty" => 0.083523,
        //         "price" => 23940.67,
        //         "trade_id" => "563078810223444"
        //     }
        //
        $marketId = $this->safe_string($trade, 'symbol');
        $datetime = $this->safe_string($trade, 'timestamp');
        return $this->safe_trade(array(
            'id' => $this->safe_string($trade, 'trade_id'),
            'timestamp' => $this->parse8601($datetime),
            'datetime' => $datetime,
            'symbol' => $this->safe_symbol($marketId, $market, '-'),
            'order' => null,
            'type' => null,
            'side' => $this->safe_string($trade, 'side'),
            'takerOrMaker' => null,
            'price' => $this->safe_string($trade, 'price'),
            'amount' => $this->safe_string($trade, 'qty'),
            'cost' => null,
            'fee' => null,
            'info' => $trade,
        ), $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://exchange.blockchain.com/api/#mass-order-status-$request-ordermassstatusrequest
             *
             * @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());
            if ($symbol !== null) {
                $market = $this->market($symbol);
                $symbol = $market['symbol'];
            }
            $url = $this->urls['api']['ws'];
            $message = array(
                'action' => 'subscribe',
                'channel' => 'trading',
            );
            $messageHash = 'orders';
            $request = $this->deep_extend($message, $params);
            $orders = Async\await($this->watch($url, $messageHash, $request, $messageHash));
            if ($this->newUpdates) {
                $limit = $orders->getLimit ($symbol, $limit);
            }
            return $this->filter_by_symbol_since_limit($orders, $symbol, $since, $limit, true);
        }) ();
    }

    public function handle_orders(Client $client, $message) {
        //
        //     {
        //         "seqnum" => 1,
        //         "event" => "rejected",
        //         "channel" => "trading",
        //         "text" => "Not subscribed to channel"
        //     }
        //  snapshot
        //     {
        //         "seqnum" => 2,
        //         "event" => "snapshot",
        //         "channel" => "trading",
        //         "orders" => array(
        //           {
        //             "orderID" => "562965341621940",
        //             "gwOrderId" => 181011136260,
        //             "clOrdID" => "016caf67f7a94508webd",
        //             "symbol" => "BTC-USD",
        //             "side" => "sell",
        //             "ordType" => "limit",
        //             "orderQty" => 0.000675,
        //             "leavesQty" => 0.000675,
        //             "cumQty" => 0,
        //             "avgPx" => 0,
        //             "ordStatus" => "open",
        //             "timeInForce" => "GTC",
        //             "text" => "New $order",
        //             "execType" => "0",
        //             "execID" => "21415965325",
        //             "transactTime" => "2022-08-08T23:31:00.550795Z",
        //             "msgType" => 8,
        //             "lastPx" => 0,
        //             "lastShares" => 0,
        //             "tradeId" => "0",
        //             "fee" => 0,
        //             "price" => 30000,
        //             "marginOrder" => false,
        //             "closePositionOrder" => false
        //           }
        //         ),
        //         "positions" => array()
        //     }
        //  update
        //     {
        //         "seqnum" => 3,
        //         "event" => "updated",
        //         "channel" => "trading",
        //         "orderID" => "562965341621940",
        //         "gwOrderId" => 181011136260,
        //         "clOrdID" => "016caf67f7a94508webd",
        //         "symbol" => "BTC-USD",
        //         "side" => "sell",
        //         "ordType" => "limit",
        //         "orderQty" => 0.000675,
        //         "leavesQty" => 0.000675,
        //         "cumQty" => 0,
        //         "avgPx" => 0,
        //         "ordStatus" => "cancelled",
        //         "timeInForce" => "GTC",
        //         "text" => "Canceled by User",
        //         "execType" => "4",
        //         "execID" => "21416034921",
        //         "transactTime" => "2022-08-08T23:33:25.727785Z",
        //         "msgType" => 8,
        //         "lastPx" => 0,
        //         "lastShares" => 0,
        //         "tradeId" => "0",
        //         "fee" => 0,
        //         "price" => 30000,
        //         "marginOrder" => false,
        //         "closePositionOrder" => false
        //     }
        //
        $event = $this->safe_string($message, 'event');
        $messageHash = 'orders';
        $cachedOrders = $this->orders;
        if ($cachedOrders === null) {
            $limit = $this->safe_integer($this->options, 'ordersLimit', 1000);
            $this->orders = new ArrayCacheBySymbolById ($limit);
        }
        if ($event === 'subscribed') {
            return;
        } elseif ($event === 'rejected') {
            throw new ExchangeError($this->id . ' ' . $this->json($message));
        } elseif ($event === 'snapshot') {
            $orders = $this->safe_value($message, 'orders', array());
            for ($i = 0; $i < count($orders); $i++) {
                $order = $orders[$i];
                $parsedOrder = $this->parse_ws_order($order);
                $cachedOrders->append ($parsedOrder);
            }
        } elseif ($event === 'updated') {
            $parsedOrder = $this->parse_ws_order($message);
            $cachedOrders->append ($parsedOrder);
        }
        $this->orders = $cachedOrders;
        $client->resolve ($this->orders, $messageHash);
    }

    public function parse_ws_order($order, $market = null) {
        //
        //     {
        //         "seqnum" => 3,
        //         "event" => "updated",
        //         "channel" => "trading",
        //         "orderID" => "562965341621940",
        //         "gwOrderId" => 181011136260,
        //         "clOrdID" => "016caf67f7a94508webd",
        //         "symbol" => "BTC-USD",
        //         "side" => "sell",
        //         "ordType" => "limit",
        //         "orderQty" => 0.000675,
        //         "leavesQty" => 0.000675,
        //         "cumQty" => 0,
        //         "avgPx" => 0,
        //         "ordStatus" => "cancelled",
        //         "timeInForce" => "GTC",
        //         "text" => "Canceled by User",
        //         "execType" => "4",
        //         "execID" => "21416034921",
        //         "transactTime" => "2022-08-08T23:33:25.727785Z",
        //         "msgType" => 8,
        //         "lastPx" => 0,
        //         "lastShares" => 0,
        //         "tradeId" => "0",
        //         "fee" => 0,
        //         "price" => 30000,
        //         "marginOrder" => false,
        //         "closePositionOrder" => false
        //     }
        //
        $datetime = $this->safe_string($order, 'transactTime');
        $status = $this->safe_string($order, 'ordStatus');
        $marketId = $this->safe_string($order, 'symbol');
        $market = $this->safe_market($marketId, $market);
        $tradeId = $this->safe_string($order, 'tradeId');
        $trades = array();
        if ($tradeId !== '0') {
            $trades[] = array( 'id' => $tradeId );
        }
        return $this->safe_order(array(
            'id' => $this->safe_string($order, 'orderID'),
            'clientOrderId' => $this->safe_string($order, 'clOrdID'),
            'datetime' => $datetime,
            'timestamp' => $this->parse8601($datetime),
            'status' => $this->parse_ws_order_status($status),
            'symbol' => $this->safe_symbol($marketId, $market),
            'type' => $this->safe_string($order, 'ordType'), // limit, $market, stop, stopLimit, trailingStop, fillOrKill
            'timeInForce' => $this->safe_string($order, 'timeInForce'),
            'postOnly' => $this->safe_string($order, 'execInst') === 'ALO',
            'side' => $this->safe_string($order, 'side'),
            'price' => $this->safe_string($order, 'price'),
            'stopPrice' => $this->safe_string($order, 'stopPx'),
            'cost' => null,
            'amount' => $this->safe_string($order, 'orderQty'),
            'filled' => $this->safe_string($order, 'cumQty'),
            'remaining' => $this->safe_string($order, 'leavesQty'),
            'trades' => $trades,
            'fee' => array(
                'rate' => null,
                'cost' => $this->safe_number($order, 'fee'),
                'currency' => $this->safe_string($market, 'quote'),
            ),
            'info' => $order,
            'lastTradeTimestamp' => null,
            'average' => $this->safe_string($order, 'avgPx'),
        ), $market);
    }

    public function parse_ws_order_status($status) {
        $statuses = array(
            'pending' => 'open',
            'open' => 'open',
            'rejected' => 'rejected',
            'cancelled' => 'canceled',
            'filled' => 'closed',
            'partial' => 'open',
            'expired' => 'expired',
        );
        return $this->safe_string($statuses, $status, $status);
    }

    public function watch_order_book(string $symbol, ?int $limit = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbol, $limit, $params) {
            /**
             * watches information on open orders with bid (buy) and ask (sell) prices, volumes and other data
             *
             * @see https://exchange.blockchain.com/api/#l2-order-book
             *
             * @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 {arrayConstructor} [$params] extra parameters specific to the exchange API endpoint
             * @param {string} [$params->type] accepts l2 or l3 for level 2 or level 3 order book
             * @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);
            $url = $this->urls['api']['ws'];
            $type = $this->safe_string($params, 'type', 'l2');
            $params = $this->omit($params, 'type');
            $messageHash = 'orderbook:' . $symbol . ':' . $type;
            $subscribe = array(
                'action' => 'subscribe',
                'channel' => $type,
                'symbol' => $market['id'],
            );
            $request = $this->deep_extend($subscribe, $params);
            $orderbook = Async\await($this->watch($url, $messageHash, $request, $messageHash));
            return $orderbook->limit ();
        }) ();
    }

    public function handle_order_book(Client $client, $message) {
        //
        //  subscribe
        //     {
        //         "seqnum" => 0,
        //         "event" => "subscribed",
        //         "channel" => "l2",
        //         "symbol" => "BTC-USDT",
        //         "batching" => false
        //     }
        //  $snapshot
        //     {
        //         "seqnum" => 1,
        //         "event" => "snapshot",
        //         "channel" => "l2",
        //         "symbol" => "BTC-USDT",
        //         "bids" => array(
        //           array( num => 1, px => 0.01, qty => 22 ),
        //         ),
        //         "asks" => array(
        //           array( num => 1, px => 23840.26, qty => 0.25 ),
        //         ),
        //         "timestamp" => "2022-08-08T22:03:19.071870Z"
        //     }
        //  update
        //     {
        //         "seqnum" => 2,
        //         "event" => "updated",
        //         "channel" => "l2",
        //         "symbol" => "BTC-USDT",
        //         "bids" => array(),
        //         "asks" => array( array( num => 1, px => 23855.06, qty => 1.04786347 ) ),
        //         "timestamp" => "2022-08-08T22:03:19.014680Z"
        //     }
        //
        $event = $this->safe_string($message, 'event');
        if ($event === 'subscribed') {
            return;
        }
        $type = $this->safe_string($message, 'channel');
        $marketId = $this->safe_string($message, 'symbol');
        $symbol = $this->safe_symbol($marketId);
        $messageHash = 'orderbook:' . $symbol . ':' . $type;
        $datetime = $this->safe_string($message, 'timestamp');
        $timestamp = $this->parse8601($datetime);
        if ($this->safe_value($this->orderbooks, $symbol) === null) {
            $this->orderbooks[$symbol] = $this->counted_order_book();
        }
        $orderbook = $this->orderbooks[$symbol];
        if ($event === 'snapshot') {
            $snapshot = $this->parse_order_book($message, $symbol, $timestamp, 'bids', 'asks', 'px', 'qty', 'num');
            $orderbook->reset ($snapshot);
        } elseif ($event === 'updated') {
            $asks = $this->safe_list($message, 'asks', array());
            $bids = $this->safe_list($message, 'bids', array());
            $this->handle_deltas($orderbook['asks'], $asks);
            $this->handle_deltas($orderbook['bids'], $bids);
            $orderbook['timestamp'] = $timestamp;
            $orderbook['datetime'] = $datetime;
        } else {
            throw new NotSupported($this->id . ' watchOrderBook() does not support ' . $event . ' yet');
        }
        $client->resolve ($orderbook, $messageHash);
    }

    public function handle_delta($bookside, $delta) {
        $bookArray = $this->parse_bid_ask($delta, 'px', 'qty', 'num');
        $bookside->storeArray ($bookArray);
    }

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

    public function handle_message(Client $client, $message) {
        $channel = $this->safe_string($message, 'channel');
        $handlers = array(
            'ticker' => array($this, 'handle_ticker'),
            'trades' => array($this, 'handle_trades'),
            'prices' => array($this, 'handle_ohlcv'),
            'l2' => array($this, 'handle_order_book'),
            'l3' => array($this, 'handle_order_book'),
            'auth' => array($this, 'handle_authentication_message'),
            'balances' => array($this, 'handle_balance'),
            'trading' => array($this, 'handle_orders'),
        );
        $handler = $this->safe_value($handlers, $channel);
        if ($handler !== null) {
            $handler($client, $message);
            return;
        }
        throw new NotSupported($this->id . ' received an unsupported $message => ' . $this->json($message));
    }

    public function handle_authentication_message(Client $client, $message) {
        //
        //     {
        //         "seqnum" => 0,
        //         "event" => "subscribed",
        //         "channel" => "auth",
        //         "readOnly" => false
        //     }
        //
        $event = $this->safe_string($message, 'event');
        if ($event !== 'subscribed') {
            throw new AuthenticationError($this->id . ' received an authentication error => ' . $this->json($message));
        }
        $future = $this->safe_value($client->futures, 'authenticated');
        if ($future !== null) {
            $future->resolve (true);
        }
    }

    public function authenticate($params = array ()) {
        return Async\async(function () use ($params) {
            $url = $this->urls['api']['ws'];
            $client = $this->client($url);
            $messageHash = 'authenticated';
            $future = $client->reusableFuture ($messageHash);
            $isAuthenticated = $this->safe_value($client->subscriptions, $messageHash);
            if ($isAuthenticated === null) {
                $this->check_required_credentials();
                $request = array(
                    'action' => 'subscribe',
                    'channel' => 'auth',
                    'token' => $this->secret,
                );
                return $this->watch($url, $messageHash, $this->extend($request, $params), $messageHash);
            }
            return Async\await($future);
        }) ();
    }
}
