<?php

namespace ccxt\pro;

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

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

class coinbase extends \ccxt\async\coinbase {

    public function describe(): mixed {
        return $this->deep_extend(parent::describe(), array(
            'has' => array(
                'ws' => true,
                'cancelAllOrdersWs' => false,
                'cancelOrdersWs' => false,
                'cancelOrderWs' => false,
                'createOrderWs' => false,
                'editOrderWs' => false,
                'fetchBalanceWs' => false,
                'fetchOpenOrdersWs' => false,
                'fetchOrderWs' => false,
                'fetchTradesWs' => false,
                'watchBalance' => false,
                'watchMyTrades' => false,
                'watchOHLCV' => false,
                'watchOrderBook' => true,
                'watchOrderBookForSymbols' => true,
                'watchOrders' => true,
                'watchTicker' => true,
                'watchTickers' => true,
                'watchTrades' => true,
                'watchTradesForSymbols' => true,
                'unWatchTicker' => true,
                'unWatchTickers' => true,
                'unWatchTrades' => true,
                'unWatchOrders' => true,
                'unWatchTradesForSymbols' => true,
            ),
            'urls' => array(
                'api' => array(
                    'ws' => 'wss://advanced-trade-ws.coinbase.com',
                ),
            ),
            'options' => array(
                'tradesLimit' => 1000,
                'ordersLimit' => 1000,
                'myTradesLimit' => 1000,
                'sides' => array(
                    'bid' => 'bids',
                    'offer' => 'asks',
                ),
            ),
        ));
    }

    public function subscribe(string $name, bool $isPrivate, $symbol = null, $params = array ()) {
        return Async\async(function () use ($name, $isPrivate, $symbol, $params) {
            /**
             * @ignore
             * subscribes to a websocket channel
             *
             * @see https://docs.cloud.coinbase.com/advanced-trade-api/docs/ws-overview#$subscribe
             *
             * @param {string} $name the $name of the channel
             * @param {boolean} $isPrivate whether the channel is private or not
             * @param {string} [$symbol] unified $market $symbol
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @return {array} subscription to a websocket channel
             */
            Async\await($this->load_markets());
            $market = null;
            $messageHash = $name;
            $productIds = array();
            if (gettype($symbol) === 'array' && array_keys($symbol) === array_keys(array_keys($symbol))) {
                $symbols = $this->market_symbols($symbol);
                $marketIds = $this->market_ids($symbols);
                $productIds = $marketIds;
                $messageHash = $messageHash . '::' . implode(',', $symbol);
            } elseif ($symbol !== null) {
                $market = $this->market($symbol);
                $messageHash = $name . '::' . $symbol;
                $productIds = [ $market['id'] ];
            }
            $url = $this->urls['api']['ws'];
            $subscribe = array(
                'type' => 'subscribe',
                'product_ids' => $productIds,
                'channel' => $name,
                // 'api_key' => $this->apiKey,
                // 'timestamp' => timestamp,
                // 'signature' => $this->hmac($this->encode(auth), $this->encode($this->secret), 'sha256'),
            );
            if ($isPrivate) {
                $subscribe = $this->extend($subscribe, $this->create_ws_auth($name, $productIds));
            }
            return Async\await($this->watch($url, $messageHash, $subscribe, $messageHash));
        }) ();
    }

    public function un_subscribe(string $topic, string $name, bool $isPrivate, $symbol = null) {
        return Async\async(function () use ($topic, $name, $isPrivate, $symbol) {
            /**
             * @ignore
             * unSubscribes to a websocket channel
             *
             * @see https://docs.cloud.coinbase.com/advanced-trade-api/docs/ws-overview#subscribe
             *
             * @param {string} $topic unified $topic
             * @param {string} $name the $name of the channel
             * @param {boolean} $isPrivate whether the channel is private or not
             * @param {string} [$symbol] unified $market $symbol
             * @return {array} $subscription to a websocket channel
             */
            Async\await($this->load_markets());
            if ($this->safe_bool($this->options, 'unSubscriptionPending', false)) {
                throw new ExchangeError($this->id . ' another unSubscription is pending, coinbase does not support concurrent unSubscriptions');
            }
            $this->options['unSubscriptionPending'] = true;
            $market = null;
            $watchMessageHash = $name;
            $unWatchMessageHash = 'unsubscribe:' . $name;
            $productIds = array();
            if (gettype($symbol) === 'array' && array_keys($symbol) === array_keys(array_keys($symbol))) {
                $symbols = $this->market_symbols($symbol);
                $marketIds = $this->market_ids($symbols);
                $productIds = $marketIds;
                $watchMessageHash = $watchMessageHash . '::' . implode(',', $symbol);
                $unWatchMessageHash = $unWatchMessageHash . '::' . implode(',', $symbol);
            } elseif ($symbol !== null) {
                $market = $this->market($symbol);
                $watchMessageHash = $name . '::' . $symbol;
                $unWatchMessageHash = $unWatchMessageHash . '::' . $symbol;
                $productIds = [ $market['id'] ];
            }
            $url = $this->urls['api']['ws'];
            // 'array("type" => "unsubscribe", "product_ids" => ["BTC-USD", "ETH-USD"], "channel" => "ticker")'
            $message = array(
                'type' => 'unsubscribe',
                'product_ids' => $productIds,
                'channel' => $name,
            );
            $subscription = array(
                'messageHashes' => array( $unWatchMessageHash ),
                'subMessageHashes' => array( $watchMessageHash ),
                'topic' => $topic,
                'unsubscribe' => true,
                'symbols' => array( $symbol ),
            );
            if ($isPrivate) {
                $message = $this->extend($message, $this->create_ws_auth($name, $productIds));
            }
            $this->options['unSubscription'] = $subscription;
            $res = Async\await($this->watch($url, $unWatchMessageHash, $message, $unWatchMessageHash, $subscription));
            $this->options['unSubscriptionPending'] = false;
            $this->options['unSubscription'] = null;
            return $res;
        }) ();
    }

    public function subscribe_multiple(string $name, bool $isPrivate, ?array $symbols = null, $params = array ()) {
        return Async\async(function () use ($name, $isPrivate, $symbols, $params) {
            /**
             * @ignore
             * subscribes to a websocket channel
             *
             * @see https://docs.cloud.coinbase.com/advanced-trade-api/docs/ws-overview#$subscribe
             *
             * @param {string} $name the $name of the channel
             * @param {boolean} $isPrivate whether the channel is private or not
             * @param {string[]} [$symbols] unified $market $symbol
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @return {array} subscription to a websocket channel
             */
            Async\await($this->load_markets());
            $productIds = array();
            $messageHashes = array();
            $symbols = $this->market_symbols($symbols, null, false);
            for ($i = 0; $i < count($symbols); $i++) {
                $symbol = $symbols[$i];
                $market = $this->market($symbol);
                $marketId = $market['id'];
                $productIds[] = $marketId;
                $messageHashes[] = $name . '::' . $symbol;
            }
            $url = $this->urls['api']['ws'];
            $subscribe = array(
                'type' => 'subscribe',
                'product_ids' => $productIds,
                'channel' => $name,
            );
            if ($isPrivate) {
                $subscribe = $this->extend($subscribe, $this->create_ws_auth($name, $productIds));
            }
            return Async\await($this->watch_multiple($url, $messageHashes, $subscribe, $messageHashes));
        }) ();
    }

    public function un_subscribe_multiple(string $topic, string $name, bool $isPrivate, ?array $symbols = null, $params = array ()) {
        return Async\async(function () use ($topic, $name, $isPrivate, $symbols, $params) {
            /**
             * @ignore
             * unsubscribes to a websocket channel
             *
             * @see https://docs.cloud.coinbase.com/advanced-trade-api/docs/ws-overview#subscribe
             *
             * @param {string} $topic unified $topic
             * @param {string} $name the $name of the channel
             * @param {boolean} $isPrivate whether the channel is private or not
             * @param {string[]} [$symbols] unified $market $symbol
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @return {array} $subscription to a websocket channel
             */
            if ($this->safe_bool($this->options, 'unSubscriptionPending', false)) {
                throw new ExchangeError($this->id . ' another unSubscription is pending, coinbase does not support concurrent unSubscriptions');
            }
            $this->options['unSubscriptionPending'] = true;
            Async\await($this->load_markets());
            $productIds = array();
            $watchMessageHashes = array();
            $unWatchMessageHashes = array();
            $symbols = $this->market_symbols($symbols, null, false);
            for ($i = 0; $i < count($symbols); $i++) {
                $symbol = $symbols[$i];
                $market = $this->market($symbol);
                $marketId = $market['id'];
                $productIds[] = $marketId;
                $watchMessageHashes[] = $name . '::' . $symbol;
                $unWatchMessageHashes[] = 'unsubscribe:' . $name . '::' . $symbol;
            }
            $url = $this->urls['api']['ws'];
            $message = array(
                'type' => 'unsubscribe',
                'product_ids' => $productIds,
                'channel' => $name,
            );
            if ($isPrivate) {
                $message = $this->extend($message, $this->create_ws_auth($name, $productIds));
            }
            $subscription = array(
                'messageHashes' => $unWatchMessageHashes,
                'subMessageHashes' => $watchMessageHashes,
                'topic' => $topic,
                'unsubscribe' => true,
                'symbols' => $symbols,
            );
            $this->options['unSubscription'] = $subscription;
            $res = Async\await($this->watch_multiple($url, $unWatchMessageHashes, $message, $unWatchMessageHashes, $subscription));
            $this->options['unSubscriptionPending'] = false;
            $this->options['unSubscription'] = null;
            return $res;
        }) ();
    }

    public function create_ws_auth(string $name, array $productIds) {
        $subscribe = array();
        $timestamp = $this->number_to_string($this->seconds());
        $this->check_required_credentials();
        $isCloudAPiKey = (mb_strpos($this->apiKey, 'organizations/') !== false) || (str_starts_with($this->secret, '-----BEGIN'));
        $auth = $timestamp . $name . implode(',', $productIds);
        if (!$isCloudAPiKey) {
            $subscribe['api_key'] = $this->apiKey;
            $subscribe['timestamp'] = $timestamp;
            $subscribe['signature'] = $this->hmac($this->encode($auth), $this->encode($this->secret), 'sha256');
        } else {
            if (str_starts_with($this->apiKey, '-----BEGIN')) {
                throw new ArgumentsRequired($this->id . ' apiKey should contain the $name (eg => organizations/3b910e93....) and not the public key');
            }
            $currentToken = $this->safe_string($this->options, 'wsToken');
            $tokenTimestamp = $this->safe_integer($this->options, 'wsTokenTimestamp', 0);
            $seconds = $this->seconds();
            if ($currentToken === null || $tokenTimestamp + 120 < $seconds) {
                // we should generate new $token
                $token = $this->create_auth_token($seconds);
                $this->options['wsToken'] = $token;
                $this->options['wsTokenTimestamp'] = $seconds;
            }
            $subscribe['jwt'] = $this->safe_string($this->options, 'wsToken');
        }
        return $subscribe;
    }

    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.cloud.coinbase.com/advanced-trade-api/docs/ws-channels#ticker-channel
             *
             * @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());
            $name = 'ticker';
            return Async\await($this->subscribe($name, false, $symbol, $params));
        }) ();
    }

    public function un_watch_ticker(string $symbol, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbol, $params) {
            /**
             * stops watching a price ticker
             *
             * @see https://docs.cloud.coinbase.com/advanced-trade-api/docs/ws-channels#ticker-channel
             *
             * @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());
            $name = 'ticker';
            return Async\await($this->un_subscribe('ticker', $name, false, $symbol));
        }) ();
    }

    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 a specific market
             *
             * @see https://docs.cloud.coinbase.com/advanced-trade-api/docs/ws-channels#$ticker-batch-channel
             *
             * @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());
            if ($symbols === null) {
                $symbols = $this->symbols;
            }
            $name = 'ticker_batch';
            $ticker = Async\await($this->subscribe_multiple($name, false, $symbols, $params));
            if ($this->newUpdates) {
                $tickers = array();
                $symbol = $ticker['symbol'];
                $tickers[$symbol] = $ticker;
                return $tickers;
            }
            return $this->tickers;
        }) ();
    }

    public function un_watch_tickers(?array $symbols = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbols, $params) {
            /**
             * stop watching
             *
             * @see https://docs.cloud.coinbase.com/advanced-trade-api/docs/ws-channels#ticker-batch-channel
             *
             * @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());
            if ($symbols === null) {
                $symbols = $this->symbols;
            }
            return Async\await($this->un_subscribe_multiple('ticker', 'ticker_batch', false, $symbols));
        }) ();
    }

    public function handle_tickers($client, $message) {
        //
        //    {
        //        "channel" => "ticker",
        //        "client_id" => "",
        //        "timestamp" => "2023-02-09T20:30:37.167359596Z",
        //        "sequence_num" => 0,
        //        "events" => array(
        //            {
        //                "type" => "snapshot",
        //                "tickers" => array(
        //                    {
        //                        "type" => "ticker",
        //                        "product_id" => "BTC-USD",
        //                        "price" => "21932.98",
        //                        "volume_24_h" => "16038.28770938",
        //                        "low_24_h" => "21835.29",
        //                        "high_24_h" => "23011.18",
        //                        "low_52_w" => "15460",
        //                        "high_52_w" => "48240",
        //                        "price_percent_chg_24_h" => "-4.15775596190603"
        // new 2024-04-12
        //                        "best_bid":"21835.29",
        //                        "best_bid_quantity" => "0.02000000",
        //                        "best_ask":"23011.18",
        //                        "best_ask_quantity" => "0.01500000"
        //                    }
        //                )
        //            }
        //        )
        //    }
        //
        //    {
        //        "channel" => "ticker_batch",
        //        "client_id" => "",
        //        "timestamp" => "2023-03-01T12:15:18.382173051Z",
        //        "sequence_num" => 0,
        //        "events" => array(
        //            {
        //                "type" => "snapshot",
        //                "tickers" => array(
        //                    {
        //                        "type" => "ticker",
        //                        "product_id" => "DOGE-USD",
        //                        "price" => "0.08212",
        //                        "volume_24_h" => "242556423.3",
        //                        "low_24_h" => "0.07989",
        //                        "high_24_h" => "0.08308",
        //                        "low_52_w" => "0.04908",
        //                        "high_52_w" => "0.1801",
        //                        "price_percent_chg_24_h" => "0.50177456859626"
        // new 2024-04-12
        //                        "best_bid":"0.07989",
        //                        "best_bid_quantity" => "500.0",
        //                        "best_ask":"0.08308",
        //                        "best_ask_quantity" => "300.0"
        //                    }
        //                )
        //            }
        //        )
        //    }
        //
        // note! seems coinbase might also send empty data like:
        //
        //    {
        //        "channel" => "ticker_batch",
        //        "client_id" => "",
        //        "timestamp" => "2024-05-24T18:22:24.546809523Z",
        //        "sequence_num" => 1,
        //        "events" => array(
        //            {
        //                "type" => "snapshot",
        //                "tickers" => array(
        //                    {
        //                        "type" => "ticker",
        //                        "product_id" => "",
        //                        "price" => "",
        //                        "volume_24_h" => "",
        //                        "low_24_h" => "",
        //                        "high_24_h" => "",
        //                        "low_52_w" => "",
        //                        "high_52_w" => "",
        //                        "price_percent_chg_24_h" => ""
        //                    }
        //                )
        //            }
        //        )
        //    }
        //
        //
        $channel = $this->safe_string($message, 'channel');
        $events = $this->safe_list($message, 'events', array());
        $datetime = $this->safe_string($message, 'timestamp');
        $timestamp = $this->parse8601($datetime);
        $newTickers = array();
        for ($i = 0; $i < count($events); $i++) {
            $tickersObj = $events[$i];
            $tickers = $this->safe_list($tickersObj, 'tickers', array());
            for ($j = 0; $j < count($tickers); $j++) {
                $ticker = $tickers[$j];
                $wsMarketId = $this->safe_string($ticker, 'product_id');
                if ($wsMarketId === null) {
                    continue;
                }
                $result = $this->parse_ws_ticker($ticker);
                $result['timestamp'] = $timestamp;
                $result['datetime'] = $datetime;
                $symbol = $result['symbol'];
                $this->tickers[$symbol] = $result;
                $newTickers[] = $result;
                $messageHash = $channel . '::' . $symbol;
                $client->resolve ($result, $messageHash);
                $this->try_resolve_usdc($client, $messageHash, $result);
            }
        }
    }

    public function parse_ws_ticker($ticker, $market = null) {
        //
        //     {
        //         "type" => "ticker",
        //         "product_id" => "DOGE-USD",
        //         "price" => "0.08212",
        //         "volume_24_h" => "242556423.3",
        //         "low_24_h" => "0.07989",
        //         "high_24_h" => "0.08308",
        //         "low_52_w" => "0.04908",
        //         "high_52_w" => "0.1801",
        //         "price_percent_chg_24_h" => "0.50177456859626"
        // new 2024-04-12
        //         "best_bid":"0.07989",
        //         "best_bid_quantity" => "500.0",
        //         "best_ask":"0.08308",
        //         "best_ask_quantity" => "300.0"
        //     }
        //
        $marketId = $this->safe_string($ticker, 'product_id');
        $timestamp = null;
        $last = $this->safe_number($ticker, 'price');
        return $this->safe_ticker(array(
            'info' => $ticker,
            'symbol' => $this->safe_symbol($marketId, $market, '-'),
            'timestamp' => $timestamp,
            'datetime' => $this->iso8601($timestamp),
            'high' => $this->safe_string($ticker, 'high_24_h'),
            'low' => $this->safe_string($ticker, 'low_24_h'),
            'bid' => $this->safe_string($ticker, 'best_bid'),
            'bidVolume' => $this->safe_string($ticker, 'best_bid_quantity'),
            'ask' => $this->safe_string($ticker, 'best_ask'),
            'askVolume' => $this->safe_string($ticker, 'best_ask_quantity'),
            'vwap' => null,
            'open' => null,
            'close' => $last,
            'last' => $last,
            'previousClose' => null,
            'change' => null,
            'percentage' => $this->safe_string($ticker, 'price_percent_chg_24_h'),
            'average' => null,
            'baseVolume' => $this->safe_string($ticker, 'volume_24_h'),
            'quoteVolume' => null,
        ));
    }

    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.cloud.coinbase.com/advanced-trade-api/docs/ws-channels#market-$trades-channel
             *
             * @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());
            $symbol = $this->symbol($symbol);
            $name = 'market_trades';
            $trades = Async\await($this->subscribe($name, false, $symbol, $params));
            if ($this->newUpdates) {
                $limit = $trades->getLimit ($symbol, $limit);
            }
            return $this->filter_by_since_limit($trades, $since, $limit, 'timestamp', true);
        }) ();
    }

    public function un_watch_trades(string $symbol, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbol, $params) {
            /**
             * stops watching the list of most recent trades for a particular $symbol
             *
             * @see https://docs.cloud.coinbase.com/advanced-trade-api/docs/ws-channels#market-trades-channel
             *
             * @param {string} $symbol unified $symbol of the market to fetch trades for
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @return {array[]} a list of ~@link https://docs.ccxt.com/#/?id=public-trades trade structures~
             */
            Async\await($this->load_markets());
            $name = 'market_trades';
            return Async\await($this->un_subscribe('trades', $name, false, $symbol));
        }) ();
    }

    public function watch_trades_for_symbols(array $symbols, ?int $since = null, ?int $limit = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbols, $since, $limit, $params) {
            /**
             * get the list of most recent $trades for a particular symbol
             *
             * @see https://docs.cloud.coinbase.com/advanced-trade-api/docs/ws-channels#market-$trades-channel
             *
             * @param {string[]} $symbols unified symbol of the market to fetch $trades for
             * @param {int} [$since] timestamp in ms of the earliest trade to fetch
             * @param {int} [$limit] the maximum amount of $trades to fetch
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @return {array[]} a list of ~@link https://docs.ccxt.com/#/?id=public-$trades trade structures~
             */
            Async\await($this->load_markets());
            $name = 'market_trades';
            $trades = Async\await($this->subscribe_multiple($name, false, $symbols, $params));
            if ($this->newUpdates) {
                $first = $this->safe_dict($trades, 0);
                $tradeSymbol = $this->safe_string($first, 'symbol');
                $limit = $trades->getLimit ($tradeSymbol, $limit);
            }
            return $this->filter_by_since_limit($trades, $since, $limit, 'timestamp', true);
        }) ();
    }

    public function un_watch_trades_for_symbols(array $symbols, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbols, $params) {
            /**
             * get the list of most recent trades for a particular symbol
             *
             * @see https://docs.cloud.coinbase.com/advanced-trade-api/docs/ws-channels#market-trades-channel
             *
             * @param {string[]} $symbols unified symbol of the market to fetch trades for
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @return {array[]} a list of ~@link https://docs.ccxt.com/#/?id=public-trades trade structures~
             */
            Async\await($this->load_markets());
            $name = 'market_trades';
            return Async\await($this->un_subscribe_multiple('trades', $name, false, $symbols, $params));
        }) ();
    }

    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.cloud.coinbase.com/advanced-trade-api/docs/ws-channels#user-channel
             *
             * @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());
            $name = 'user';
            $orders = Async\await($this->subscribe($name, true, $symbol, $params));
            if ($this->newUpdates) {
                $limit = $orders->getLimit ($symbol, $limit);
            }
            return $this->filter_by_since_limit($orders, $since, $limit, 'timestamp', true);
        }) ();
    }

    public function un_watch_orders(?string $symbol = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbol, $params) {
            /**
             * stops watching information on multiple orders made by the user
             *
             * @see https://docs.cloud.coinbase.com/advanced-trade-api/docs/ws-channels#user-channel
             *
             * @param {string} [$symbol] unified market $symbol of the market orders were made in
             * @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());
            $name = 'user';
            return Async\await($this->un_subscribe('orders', $name, true, $this->symbol($symbol)));
        }) ();
    }

    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.cloud.coinbase.com/advanced-trade-api/docs/ws-channels#level2-channel
             *
             * @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());
            $name = 'level2';
            $market = $this->market($symbol);
            $symbol = $market['symbol'];
            $orderbook = Async\await($this->subscribe($name, false, $symbol, $params));
            return $orderbook->limit ();
        }) ();
    }

    public function un_watch_order_book(string $symbol, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbol, $params) {
            /**
             * stops watching information on open orders with bid (buy) and ask (sell) prices, volumes and other data
             *
             * @see https://docs.cloud.coinbase.com/advanced-trade-api/docs/ws-channels#level2-channel
             *
             * @param {string} $symbol unified $symbol of the market to fetch the order book for
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @return {array} A dictionary of ~@link https://docs.ccxt.com/#/?id=order-book-structure order book structures~ indexed by market symbols
             */
            Async\await($this->load_markets());
            $symbol = $this->symbol($symbol);
            $name = 'level2';
            return Async\await($this->un_subscribe('orderbook', $name, false, $symbol));
        }) ();
    }

    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://docs.cloud.coinbase.com/advanced-trade-api/docs/ws-channels#level2-channel
             *
             * @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());
            $name = 'level2';
            $orderbook = Async\await($this->subscribe_multiple($name, false, $symbols, $params));
            return $orderbook->limit ();
        }) ();
    }

    public function handle_trade($client, $message) {
        //
        //    {
        //        "channel" => "market_trades",
        //        "client_id" => "",
        //        "timestamp" => "2023-02-09T20:19:35.39625135Z",
        //        "sequence_num" => 0,
        //        "events" => array(
        //            {
        //                "type" => "snapshot",
        //                "trades" => array(
        //                    {
        //                        "trade_id" => "000000000",
        //                        "product_id" => "ETH-USD",
        //                        "price" => "1260.01",
        //                        "size" => "0.3",
        //                        "side" => "BUY",
        //                        "time" => "2019-08-14T20:42:27.265Z",
        //                    }
        //                )
        //            }
        //        )
        //    }
        //
        $events = $this->safe_list($message, 'events');
        $event = $this->safe_value($events, 0);
        $trades = $this->safe_list($event, 'trades');
        $trade = $this->safe_dict($trades, 0);
        $marketId = $this->safe_string($trade, 'product_id');
        $symbol = $this->safe_symbol($marketId);
        $messageHash = 'market_trades::' . $symbol;
        $tradesArray = $this->safe_value($this->trades, $symbol);
        if ($tradesArray === null) {
            $tradesLimit = $this->safe_integer($this->options, 'tradesLimit', 1000);
            $tradesArray = new ArrayCacheBySymbolById ($tradesLimit);
            $this->trades[$symbol] = $tradesArray;
        }
        for ($i = 0; $i < count($events); $i++) {
            $currentEvent = $events[$i];
            $currentTrades = $this->safe_list($currentEvent, 'trades');
            for ($j = 0; $j < count($currentTrades); $j++) {
                $item = $currentTrades[$i];
                $tradesArray->append ($this->parse_trade($item));
            }
        }
        $client->resolve ($tradesArray, $messageHash);
        $this->try_resolve_usdc($client, $messageHash, $tradesArray);
    }

    public function handle_order($client, $message) {
        //
        //    {
        //        "channel" => "user",
        //        "client_id" => "",
        //        "timestamp" => "2023-02-09T20:33:57.609931463Z",
        //        "sequence_num" => 0,
        //        "events" => array(
        //            {
        //                "type" => "snapshot",
        //                "orders" => array(
        //                    array(
        //                        "order_id" => "XXX",
        //                        "client_order_id" => "YYY",
        //                        "cumulative_quantity" => "0",
        //                        "leaves_quantity" => "0.000994",
        //                        "avg_price" => "0",
        //                        "total_fees" => "0",
        //                        "status" => "OPEN",
        //                        "product_id" => "BTC-USD",
        //                        "creation_time" => "2022-12-07T19:42:18.719312Z",
        //                        "order_side" => "BUY",
        //                        "order_type" => "Limit"
        //                    ),
        //                )
        //            }
        //        )
        //    }
        //
        $events = $this->safe_list($message, 'events');
        $marketIds = array();
        if ($this->orders === null) {
            $limit = $this->safe_integer($this->options, 'ordersLimit', 1000);
            $this->orders = new ArrayCacheBySymbolById ($limit);
        }
        for ($i = 0; $i < count($events); $i++) {
            $event = $events[$i];
            $responseOrders = $this->safe_list($event, 'orders');
            for ($j = 0; $j < count($responseOrders); $j++) {
                $responseOrder = $responseOrders[$j];
                $parsed = $this->parse_ws_order($responseOrder);
                $cachedOrders = $this->orders;
                $marketId = $this->safe_string($responseOrder, 'product_id');
                if (!(is_array($marketIds) && array_key_exists($marketId, $marketIds))) {
                    $marketIds[] = $marketId;
                }
                $cachedOrders->append ($parsed);
            }
        }
        for ($i = 0; $i < count($marketIds); $i++) {
            $marketId = $marketIds[$i];
            $symbol = $this->safe_symbol($marketId);
            $messageHash = 'user::' . $symbol;
            $client->resolve ($this->orders, $messageHash);
            $this->try_resolve_usdc($client, $messageHash, $this->orders);
        }
        $client->resolve ($this->orders, 'user');
    }

    public function parse_ws_order($order, $market = null) {
        //
        //    {
        //        "order_id" => "XXX",
        //        "client_order_id" => "YYY",
        //        "cumulative_quantity" => "0",
        //        "leaves_quantity" => "0.000994",
        //        "avg_price" => "0",
        //        "total_fees" => "0",
        //        "status" => "OPEN",
        //        "product_id" => "BTC-USD",
        //        "creation_time" => "2022-12-07T19:42:18.719312Z",
        //        "order_side" => "BUY",
        //        "order_type" => "Limit"
        //    }
        //
        $id = $this->safe_string($order, 'order_id');
        $clientOrderId = $this->safe_string($order, 'client_order_id');
        $marketId = $this->safe_string($order, 'product_id');
        $datetime = $this->safe_string($order, 'time');
        $market = $this->safe_market($marketId, $market);
        return $this->safe_order(array(
            'info' => $order,
            'symbol' => $this->safe_string($market, 'symbol'),
            'id' => $id,
            'clientOrderId' => $clientOrderId,
            'timestamp' => $this->parse8601($datetime),
            'datetime' => $datetime,
            'lastTradeTimestamp' => null,
            'type' => $this->safe_string($order, 'order_type'),
            'timeInForce' => null,
            'postOnly' => null,
            'side' => $this->safe_string($order, 'side'),
            'price' => null,
            'stopPrice' => null,
            'triggerPrice' => null,
            'amount' => null,
            'cost' => null,
            'average' => $this->safe_string($order, 'avg_price'),
            'filled' => $this->safe_string($order, 'cumulative_quantity'),
            'remaining' => $this->safe_string($order, 'leaves_quantity'),
            'status' => $this->safe_string_lower($order, 'status'),
            'fee' => array(
                'amount' => $this->safe_string($order, 'total_fees'),
                'currency' => $this->safe_string($market, 'quote'),
            ),
            'trades' => null,
        ));
    }

    public function handle_order_book_helper($orderbook, $updates) {
        for ($i = 0; $i < count($updates); $i++) {
            $trade = $updates[$i];
            $sideId = $this->safe_string($trade, 'side');
            $side = $this->safe_string($this->options['sides'], $sideId);
            $price = $this->safe_number($trade, 'price_level');
            $amount = $this->safe_number($trade, 'new_quantity');
            $orderbookSide = $orderbook[$side];
            $orderbookSide->store ($price, $amount);
        }
    }

    public function handle_order_book($client, $message) {
        //
        //    {
        //        "channel" => "l2_data",
        //        "client_id" => "",
        //        "timestamp" => "2023-02-09T20:32:50.714964855Z",
        //        "sequence_num" => 0,
        //        "events" => array(
        //            {
        //                "type" => "snapshot",
        //                "product_id" => "BTC-USD",
        //                "updates" => array(
        //                    array(
        //                        "side" => "bid",
        //                        "event_time" => "1970-01-01T00:00:00Z",
        //                        "price_level" => "21921.74",
        //                        "new_quantity" => "0.06317902"
        //                    ),
        //                    array(
        //                        "side" => "bid",
        //                        "event_time" => "1970-01-01T00:00:00Z",
        //                        "price_level" => "21921.3",
        //                        "new_quantity" => "0.02"
        //                    ),
        //                )
        //            }
        //        )
        //    }
        //
        $events = $this->safe_list($message, 'events');
        $datetime = $this->safe_string($message, 'timestamp');
        for ($i = 0; $i < count($events); $i++) {
            $event = $events[$i];
            $updates = $this->safe_list($event, 'updates', array());
            $marketId = $this->safe_string($event, 'product_id');
            // sometimes we subscribe to BTC/USDC and coinbase returns BTC/USD, are aliases
            $market = $this->safe_market($marketId);
            $symbol = $market['symbol'];
            $messageHash = 'level2::' . $symbol;
            $subscription = $this->safe_value($client->subscriptions, $messageHash, array());
            $limit = $this->safe_integer($subscription, 'limit');
            $type = $this->safe_string($event, 'type');
            if ($type === 'snapshot') {
                $this->orderbooks[$symbol] = $this->order_book(array(), $limit);
            }
            // unknown bug, can't reproduce, but sometimes $orderbook is null
            if (!(is_array($this->orderbooks) && array_key_exists($symbol, $this->orderbooks)) && $this->orderbooks[$symbol] === null) {
                continue;
            }
            $orderbook = $this->orderbooks[$symbol];
            $this->handle_order_book_helper($orderbook, $updates);
            $orderbook['timestamp'] = $this->parse8601($datetime);
            $orderbook['datetime'] = $datetime;
            $orderbook['symbol'] = $symbol;
            $client->resolve ($orderbook, $messageHash);
            $this->try_resolve_usdc($client, $messageHash, $orderbook);
        }
    }

    public function try_resolve_usdc($client, $messageHash, $result) {
        if (str_ends_with($messageHash, '/USD') || str_ends_with($messageHash, '-USD')) {
            $client->resolve ($result, $messageHash . 'C'); // when subscribing to BTC/USDC and coinbase returns BTC/USD, so resolve USDC too
        }
    }

    public function handle_subscription_status($client, $message) {
        //
        //     {
        //         "type" => "subscriptions",
        //         "channels" => array(
        //             {
        //                 "name" => "level2",
        //                 "product_ids" => array( "ETH-BTC" )
        //             }
        //         )
        //     }
        //
        //
        //      {
        //        channel => 'subscriptions',
        //        client_id => '',
        //        timestamp => '2025-09-15T17:02:49.90120868Z',
        //        sequence_num => 3,
        //        $events => array( array( subscriptions => array() ) )
        //      }
        //
        $events = $this->safe_list($message, 'events', array());
        $firstEvent = $this->safe_value($events, 0, array());
        $isUnsub = (is_array($firstEvent) && array_key_exists('subscriptions', $firstEvent));
        $subKeys = is_array($firstEvent['subscriptions']) ? array_keys($firstEvent['subscriptions']) : array();
        $subKeysLength = count($subKeys);
        if ($isUnsub && $subKeysLength === 0) {
            $unSubObject = $this->safe_dict($this->options, 'unSubscription', array());
            $messageHashes = $this->safe_list($unSubObject, 'messageHashes', array());
            $subMessageHashes = $this->safe_list($unSubObject, 'subMessageHashes', array());
            for ($i = 0; $i < count($messageHashes); $i++) {
                $messageHash = $messageHashes[$i];
                $subHash = $subMessageHashes[$i];
                $this->clean_unsubscription($client, $subHash, $messageHash);
            }
            $this->clean_cache($unSubObject);
        }
        return $message;
    }

    public function handle_heartbeats($client, $message) {
        // although the subscription takes a product_ids parameter (i.e. symbol),
        // there is no (clear) way of mapping the $message back to the symbol.
        //
        //     {
        //         "channel" => "heartbeats",
        //         "client_id" => "",
        //         "timestamp" => "2023-06-23T20:31:26.122969572Z",
        //         "sequence_num" => 0,
        //         "events" => array(
        //           {
        //               "current_time" => "2023-06-23 20:31:56.121961769 +0000 UTC m=+91717.525857105",
        //               "heartbeat_counter" => "3049"
        //           }
        //         )
        //     }
        //
        return $message;
    }

    public function handle_message($client, $message) {
        $channel = $this->safe_string($message, 'channel');
        $methods = array(
            'subscriptions' => array($this, 'handle_subscription_status'),
            'ticker' => array($this, 'handle_tickers'),
            'ticker_batch' => array($this, 'handle_tickers'),
            'market_trades' => array($this, 'handle_trade'),
            'user' => array($this, 'handle_order'),
            'l2_data' => array($this, 'handle_order_book'),
            'heartbeats' => array($this, 'handle_heartbeats'),
        );
        $type = $this->safe_string($message, 'type');
        if ($type === 'error') {
            $errorMessage = $this->safe_string($message, 'message');
            throw new ExchangeError($errorMessage);
        }
        $method = $this->safe_value($methods, $channel);
        if ($method) {
            $method($client, $message);
        }
    }
}
