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

class hyperliquid extends \ccxt\async\hyperliquid {

    public function describe(): mixed {
        return $this->deep_extend(parent::describe(), array(
            'has' => array(
                'ws' => true,
                'cancelOrderWs' => true,
                'cancelOrdersWs' => true,
                'createOrderWs' => true,
                'createOrdersWs' => true,
                'editOrderWs' => true,
                'watchBalance' => false,
                'watchMyTrades' => true,
                'watchOHLCV' => true,
                'watchOrderBook' => true,
                'watchOrders' => true,
                'watchTicker' => true,
                'watchTickers' => true,
                'watchTrades' => true,
                'watchTradesForSymbols' => false,
                'watchPosition' => false,
            ),
            'urls' => array(
                'api' => array(
                    'ws' => array(
                        'public' => 'wss://api.hyperliquid.xyz/ws',
                    ),
                ),
                'test' => array(
                    'ws' => array(
                        'public' => 'wss://api.hyperliquid-testnet.xyz/ws',
                    ),
                ),
            ),
            'options' => array(
            ),
            'streaming' => array(
                'ping' => array($this, 'ping'),
                'keepAlive' => 20000,
            ),
            'exceptions' => array(
                'ws' => array(
                    'exact' => array(
                    ),
                ),
            ),
        ));
    }

    public function create_orders_ws(array $orders, $params = array ()) {
        return Async\async(function () use ($orders, $params) {
            /**
             * create a list of trade $orders using WebSocket post $request
             *
             * @see https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/exchange-endpoint#place-an-order
             *
             * @param {Array} $orders list of $orders to create, each object should contain the parameters required by createOrder, namely symbol, type, side, amount, price and $params
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @return {array} an ~@link https://docs.ccxt.com/#/?id=order-structure order structure~
             */
            Async\await($this->load_markets());
            $url = $this->urls['api']['ws']['public'];
            $ordersRequest = $this->createOrdersRequest ($orders, $params);
            $wrapped = $this->wrap_as_post_action($ordersRequest);
            $request = $this->safe_dict($wrapped, 'request', array());
            $requestId = $this->safe_string($wrapped, 'requestId');
            $response = Async\await($this->watch($url, $requestId, $request, $requestId));
            $responseOjb = $this->safe_dict($response, 'response', array());
            $data = $this->safe_dict($responseOjb, 'data', array());
            $statuses = $this->safe_list($data, 'statuses', array());
            return $this->parse_orders($statuses, null);
        }) ();
    }

    public function create_order_ws(string $symbol, string $type, string $side, float $amount, ?float $price = null, $params = array ()) {
        return Async\async(function () use ($symbol, $type, $side, $amount, $price, $params) {
            /**
             * create a trade $order using WebSocket post request
             *
             * @see https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/exchange-endpoint#place-an-$order
             *
             * @param {string} $symbol unified $symbol of the market to create an $order in
             * @param {string} $type 'market' or 'limit'
             * @param {string} $side 'buy' or 'sell'
             * @param {float} $amount how much of currency you want to trade in units of base currency
             * @param {float} [$price] the $price at which the $order is to be fulfilled, in units of the quote currency, ignored in market $orders
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @param {string} [$params->timeInForce] 'Gtc', 'Ioc', 'Alo'
             * @param {bool} [$params->postOnly] true or false whether the $order is post-only
             * @param {bool} [$params->reduceOnly] true or false whether the $order is reduce-only
             * @param {float} [$params->triggerPrice] The $price at which a trigger $order is triggered at
             * @param {string} [$params->clientOrderId] client $order id, (optional 128 bit hex string e.g. 0x1234567890abcdef1234567890abcdef)
             * @param {string} [$params->slippage] the slippage for market $order
             * @param {string} [$params->vaultAddress] the vault address for $order
             * @return {array} an ~@link https://docs.ccxt.com/#/?id=$order-structure $order structure~
             */
            Async\await($this->load_markets());
            list($order, $globalParams) = $this->parseCreateEditOrderArgs (null, $symbol, $type, $side, $amount, $price, $params);
            $orders = Async\await($this->create_orders_ws(array( $order ), $globalParams));
            $ordersLength = count($orders);
            if ($ordersLength === 0) {
                // not sure why but it is happening sometimes
                return $this->safe_order(array());
            }
            $parsedOrder = $orders[0];
            return $parsedOrder;
        }) ();
    }

    public function edit_order_ws(string $id, string $symbol, string $type, string $side, ?float $amount = null, ?float $price = null, $params = array ()) {
        return Async\async(function () use ($id, $symbol, $type, $side, $amount, $price, $params) {
            /**
             * edit a trade $order
             *
             * @see https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/exchange-endpoint#modify-multiple-orders
             *
             * @param {string} $id cancel $order $id
             * @param {string} $symbol unified $symbol of the $market to create an $order in
             * @param {string} $type 'market' or 'limit'
             * @param {string} $side 'buy' or 'sell'
             * @param {float} $amount how much of currency you want to trade in units of base currency
             * @param {float} [$price] the $price at which the $order is to be fulfilled, in units of the quote currency, ignored in $market orders
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @param {string} [$params->timeInForce] 'Gtc', 'Ioc', 'Alo'
             * @param {bool} [$params->postOnly] true or false whether the $order is post-only
             * @param {bool} [$params->reduceOnly] true or false whether the $order is reduce-only
             * @param {float} [$params->triggerPrice] The $price at which a trigger $order is triggered at
             * @param {string} [$params->clientOrderId] client $order $id, (optional 128 bit hex string e.g. 0x1234567890abcdef1234567890abcdef)
             * @param {string} [$params->vaultAddress] the vault address for $order
             * @return {array} an ~@link https://docs.ccxt.com/#/?$id=$order-structure $order structure~
             */
            Async\await($this->load_markets());
            $market = $this->market($symbol);
            $url = $this->urls['api']['ws']['public'];
            list($order, $globalParams) = $this->parseCreateEditOrderArgs ($id, $symbol, $type, $side, $amount, $price, $params);
            $postRequest = $this->editOrdersRequest (array( $order ), $globalParams);
            $wrapped = $this->wrap_as_post_action($postRequest);
            $request = $this->safe_dict($wrapped, 'request', array());
            $requestId = $this->safe_string($wrapped, 'requestId');
            $response = Async\await($this->watch($url, $requestId, $request, $requestId));
            // $response is the same array($this, 'edit_order')            $responseObject = $this->safe_dict($response, 'response', array());
            $dataObject = $this->safe_dict($responseObject, 'data', array());
            $statuses = $this->safe_list($dataObject, 'statuses', array());
            $first = $this->safe_dict($statuses, 0, array());
            $parsedOrder = $this->parse_order($first, $market);
            return $parsedOrder;
        }) ();
    }

    public function cancel_orders_ws(array $ids, ?string $symbol = null, $params = array ()) {
        return Async\async(function () use ($ids, $symbol, $params) {
            /**
             * cancel multiple $orders using WebSocket post $request
             *
             * @see https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/websocket/post-requests
             *
             * @param {string[]} $ids list of order $ids to cancel
             * @param {string} $symbol unified $symbol of the market the $orders were made in
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @param {string[]} [$params->clientOrderId] list of client order $ids to cancel instead of order $ids
             * @param {string} [$params->vaultAddress] the vault address for order cancellation
             * @return {array[]} a list of ~@link https://docs.ccxt.com/#/?id=order-structure order structures~
             */
            $this->check_required_credentials();
            Async\await($this->load_markets());
            $request = $this->cancelOrdersRequest ($ids, $symbol, $params);
            $url = $this->urls['api']['ws']['public'];
            $wrapped = $this->wrap_as_post_action($request);
            $wsRequest = $this->safe_dict($wrapped, 'request', array());
            $requestId = $this->safe_string($wrapped, 'requestId');
            $response = Async\await($this->watch($url, $requestId, $wsRequest, $requestId));
            $responseObj = $this->safe_dict($response, 'response', array());
            $data = $this->safe_dict($responseObj, 'data', array());
            $statuses = $this->safe_list($data, 'statuses', array());
            $orders = array();
            for ($i = 0; $i < count($statuses); $i++) {
                $status = $statuses[$i];
                $orders[] = $this->safe_order(array(
                    'info' => $status,
                    'status' => $status,
                ));
            }
            return $orders;
        }) ();
    }

    public function cancel_order_ws(string $id, ?string $symbol = null, $params = array ()) {
        return Async\async(function () use ($id, $symbol, $params) {
            /**
             * cancel a single order using WebSocket post request
             *
             * @see https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/websocket/post-requests
             *
             * @param {string} $id order $id to cancel
             * @param {string} $symbol unified $symbol of the market the order was made in
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @param {string} [$params->clientOrderId] client order $id to cancel instead of order $id
             * @param {string} [$params->vaultAddress] the vault address for order cancellation
             * @return {array} an ~@link https://docs.ccxt.com/#/?$id=order-structure order structure~
             */
            $orders = Async\await($this->cancel_orders_ws(array( $id ), $symbol, $params));
            return $this->safe_dict($orders, 0);
        }) ();
    }

    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://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/websocket/subscriptions
             *
             * @param {string} $symbol unified $symbol of the $market to fetch the order book for
             * @param {int} [$limit] the maximum amount of order book entries to return
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @return {array} A dictionary of ~@link https://docs.ccxt.com/#/?id=order-book-structure order book structures~ indexed by $market symbols
             */
            Async\await($this->load_markets());
            $market = $this->market($symbol);
            $symbol = $market['symbol'];
            $messageHash = 'orderbook:' . $symbol;
            $url = $this->urls['api']['ws']['public'];
            $request = array(
                'method' => 'subscribe',
                'subscription' => array(
                    'type' => 'l2Book',
                    'coin' => $market['swap'] ? $market['baseName'] : $market['id'],
                ),
            );
            $message = $this->extend($request, $params);
            $orderbook = Async\await($this->watch($url, $messageHash, $message, $messageHash));
            return $orderbook->limit ();
        }) ();
    }

    public function un_watch_order_book(string $symbol, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbol, $params) {
            /**
             * unWatches information on open orders with bid (buy) and ask (sell) prices, volumes and other data
             *
             * @see https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/websocket/subscriptions
             *
             * @param {string} $symbol unified $symbol of the $market to fetch the order book for
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @return {array} A dictionary of ~@link https://docs.ccxt.com/#/?$id=order-book-structure order book structures~ indexed by $market symbols
             */
            Async\await($this->load_markets());
            $market = $this->market($symbol);
            $symbol = $market['symbol'];
            $subMessageHash = 'orderbook:' . $symbol;
            $messageHash = 'unsubscribe:' . $subMessageHash;
            $url = $this->urls['api']['ws']['public'];
            $id = (string) $this->nonce();
            $request = array(
                'id' => $id,
                'method' => 'unsubscribe',
                'subscription' => array(
                    'type' => 'l2Book',
                    'coin' => $market['swap'] ? $market['baseName'] : $market['id'],
                ),
            );
            $message = $this->extend($request, $params);
            return Async\await($this->watch($url, $messageHash, $message, $messageHash));
        }) ();
    }

    public function handle_order_book($client, $message) {
        //
        //     {
        //         "channel" => "l2Book",
        //         "data" => {
        //             "coin" => "BTC",
        //             "time" => 1710131872708,
        //             "levels" => array(
        //                 array(
        //                     {
        //                         "px" => "68674.0",
        //                         "sz" => "0.97139",
        //                         "n" => 4
        //                     }
        //                 ),
        //                 array(
        //                     {
        //                         "px" => "68675.0",
        //                         "sz" => "0.04396",
        //                         "n" => 1
        //                     }
        //                 )
        //             )
        //         }
        //     }
        //
        $entry = $this->safe_dict($message, 'data', array());
        $coin = $this->safe_string($entry, 'coin');
        $marketId = $this->coinToMarketId ($coin);
        $market = $this->market($marketId);
        $symbol = $market['symbol'];
        $rawData = $this->safe_list($entry, 'levels', array());
        $data = array(
            'bids' => $this->safe_list($rawData, 0, array()),
            'asks' => $this->safe_list($rawData, 1, array()),
        );
        $timestamp = $this->safe_integer($entry, 'time');
        $snapshot = $this->parse_order_book($data, $symbol, $timestamp, 'bids', 'asks', 'px', 'sz');
        if (!(is_array($this->orderbooks) && array_key_exists($symbol, $this->orderbooks))) {
            $ob = $this->order_book($snapshot);
            $this->orderbooks[$symbol] = $ob;
        }
        $orderbook = $this->orderbooks[$symbol];
        $orderbook->reset ($snapshot);
        $messageHash = 'orderbook:' . $symbol;
        $client->resolve ($orderbook, $messageHash);
    }

    public function watch_ticker(string $symbol, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbol, $params) {
            /**
             *
             * @see https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/websocket/subscriptions
             *
             * watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific $market
             * @param {string} $symbol unified $symbol of the $market to fetch the ticker for
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @return {array} a ~@link https://docs.ccxt.com/#/?id=ticker-structure ticker structure~
             */
            $market = $this->market($symbol);
            $symbol = $market['symbol'];
            $tickers = Async\await($this->watch_tickers(array( $symbol ), $params));
            return $tickers[$symbol];
        }) ();
    }

    public function watch_tickers(?array $symbols = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbols, $params) {
            /**
             * 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://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/websocket/subscriptions
             *
             * @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, true);
            $messageHash = 'tickers';
            $url = $this->urls['api']['ws']['public'];
            $request = array(
                'method' => 'subscribe',
                'subscription' => array(
                    'type' => 'webData2', // allMids
                    'user' => '0x0000000000000000000000000000000000000000',
                ),
            );
            $tickers = Async\await($this->watch($url, $messageHash, $this->extend($request, $params), $messageHash));
            if ($this->newUpdates) {
                return $this->filter_by_array_tickers($tickers, 'symbol', $symbols);
            }
            return $this->tickers;
        }) ();
    }

    public function un_watch_tickers(?array $symbols = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbols, $params) {
            /**
             * unWatches a price ticker, a statistical calculation with the information calculated over the past 24 hours for all markets of a specific list
             *
             * @see https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/websocket/subscriptions
             *
             * @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, true);
            $subMessageHash = 'tickers';
            $messageHash = 'unsubscribe:' . $subMessageHash;
            $url = $this->urls['api']['ws']['public'];
            $request = array(
                'method' => 'unsubscribe',
                'subscription' => array(
                    'type' => 'webData2', // allMids
                    'user' => '0x0000000000000000000000000000000000000000',
                ),
            );
            return Async\await($this->watch($url, $messageHash, $this->extend($request, $params), $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 information on multiple $trades made by the user
             *
             * @see https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/websocket/subscriptions
             *
             * @param {string} $symbol unified market $symbol of the market orders were made in
             * @param {int} [$since] the earliest time in ms to fetch orders for
             * @param {int} [$limit] the maximum number of order structures to retrieve
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @param {string} [$params->user] user address, will default to $this->walletAddress if not provided
             * @return {array[]} a list of ~@link https://docs.ccxt.com/#/?id=order-structure order structures~
             */
            $userAddress = null;
            list($userAddress, $params) = $this->handlePublicAddress ('watchMyTrades', $params);
            Async\await($this->load_markets());
            $messageHash = 'myTrades';
            if ($symbol !== null) {
                $symbol = $this->symbol($symbol);
                $messageHash .= ':' . $symbol;
            }
            $url = $this->urls['api']['ws']['public'];
            $request = array(
                'method' => 'subscribe',
                'subscription' => array(
                    'type' => 'userFills',
                    'user' => $userAddress,
                ),
            );
            $message = $this->extend($request, $params);
            $trades = Async\await($this->watch($url, $messageHash, $message, $messageHash));
            if ($this->newUpdates) {
                $limit = $trades->getLimit ($symbol, $limit);
            }
            return $this->filter_by_symbol_since_limit($trades, $symbol, $since, $limit, true);
        }) ();
    }

    public function handle_ws_tickers(Client $client, $message) {
        //
        //     {
        //         "channel" => "webData2",
        //         "data" => {
        //             "meta" => array(
        //                 "universe" => array(
        //                     array(
        //                         "szDecimals" => 5,
        //                         "name" => "BTC",
        //                         "maxLeverage" => 50,
        //                         "onlyIsolated" => false
        //                     ),
        //                     ...
        //                 ),
        //             ),
        //             "assetCtxs" => array(
        //                 array(
        //                     "funding" => "0.00003005",
        //                     "openInterest" => "2311.50778",
        //                     "prevDayPx" => "63475.0",
        //                     "dayNtlVlm" => "468043329.64289033",
        //                     "premium" => "0.00094264",
        //                     "oraclePx" => "64712.0",
        //                     "markPx" => "64774.0",
        //                     "midPx" => "64773.5",
        //                     "impactPxs" => array(
        //                         "64773.0",
        //                         "64774.0"
        //                     )
        //                 ),
        //                 ...
        //             ),
        //             "spotAssetCtxs" => array(
        //                 array(
        //                     "prevDayPx" => "0.20937",
        //                     "dayNtlVlm" => "11188888.61984999",
        //                     "markPx" => "0.19722",
        //                     "midPx" => "0.197145",
        //                     "circulatingSupply" => "598760557.12072003",
        //                     "coin" => "PURR/USDC"
        //                 ),
        //                 ...
        //             ),
        //         }
        //     }
        //
        // spot
        $rawData = $this->safe_dict($message, 'data', array());
        $spotAssets = $this->safe_list($rawData, 'spotAssetCtxs', array());
        $parsedTickers = array();
        for ($i = 0; $i < count($spotAssets); $i++) {
            $assetObject = $spotAssets[$i];
            $marketId = $this->safe_string($assetObject, 'coin');
            $market = $this->safe_market($marketId, null, null, 'spot');
            $ticker = $this->parse_ws_ticker($assetObject, $market);
            $parsedTickers[] = $ticker;
        }
        // perpetuals
        $meta = $this->safe_dict($rawData, 'meta', array());
        $universe = $this->safe_list($meta, 'universe', array());
        $assetCtxs = $this->safe_list($rawData, 'assetCtxs', array());
        for ($i = 0; $i < count($universe); $i++) {
            $data = $this->extend(
                $this->safe_dict($universe, $i, array()),
                $this->safe_dict($assetCtxs, $i, array())
            );
            $id = $data['name'] . '/USDC:USDC';
            $market = $this->safe_market($id, null, null, 'swap');
            $ticker = $this->parse_ws_ticker($data, $market);
            $parsedTickers[] = $ticker;
        }
        $tickers = $this->index_by($parsedTickers, 'symbol');
        $client->resolve ($tickers, 'tickers');
    }

    public function parse_ws_ticker($rawTicker, ?array $market = null): array {
        return $this->parse_ticker($rawTicker, $market);
    }

    public function handle_my_trades(Client $client, $message) {
        //
        //     {
        //         "channel" => "userFills",
        //         "data" => {
        //             "isSnapshot" => true,
        //             "user" => "0x15f43d1f2dee81424afd891943262aa90f22cc2a",
        //             "fills" => array(
        //                 {
        //                     "coin" => "BTC",
        //                     "px" => "72528.0",
        //                     "sz" => "0.11693",
        //                     "side" => "A",
        //                     "time" => 1710208712815,
        //                     "startPosition" => "0.11693",
        //                     "dir" => "Close Long",
        //                     "closedPnl" => "-0.81851",
        //                     "hash" => "0xc5adaf35f8402750c218040b0a7bc301130051521273b6f398b3caad3e1f3f5f",
        //                     "oid" => 7484888874,
        //                     "crossed" => true,
        //                     "fee" => "2.968244",
        //                     "liquidationMarkPx" => null,
        //                     "tid" => 567547935839686,
        //                     "cloid" => null
        //                 }
        //             )
        //         }
        //     }
        //
        $entry = $this->safe_dict($message, 'data', array());
        if ($this->myTrades === null) {
            $limit = $this->safe_integer($this->options, 'tradesLimit', 1000);
            $this->myTrades = new ArrayCacheBySymbolById ($limit);
        }
        $trades = $this->myTrades;
        $symbols = array();
        $data = $this->safe_list($entry, 'fills', array());
        $dataLength = count($data);
        if ($dataLength === 0) {
            return;
        }
        for ($i = 0; $i < count($data); $i++) {
            $rawTrade = $data[$i];
            $parsed = $this->parse_ws_trade($rawTrade);
            $symbol = $parsed['symbol'];
            $symbols[$symbol] = true;
            $trades->append ($parsed);
        }
        $keys = is_array($symbols) ? array_keys($symbols) : array();
        for ($i = 0; $i < count($keys); $i++) {
            $currentMessageHash = 'myTrades:' . $keys[$i];
            $client->resolve ($trades, $currentMessageHash);
        }
        // non-$symbol specific
        $messageHash = 'myTrades';
        $client->resolve ($trades, $messageHash);
    }

    public function watch_trades(string $symbol, ?int $since = null, ?int $limit = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbol, $since, $limit, $params) {
            // s
            // @method
            // @name hyperliquid#watchTrades
            // @description watches information on multiple $trades made in a $market
            // @see https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/websocket/subscriptions
            // @param {string} $symbol unified $market $symbol of the $market $trades were made in
            // @param {int} [$since] the earliest time in ms to fetch $trades for
            // @param {int} [$limit] the maximum number of trade structures to retrieve
            // @param {object} [$params] extra parameters specific to the exchange API endpoint
            // @returns {objectarray()} a list of ~@link https://docs.ccxt.com/#/?id=trade-structure trade structures~
            //
            Async\await($this->load_markets());
            $market = $this->market($symbol);
            $symbol = $market['symbol'];
            $messageHash = 'trade:' . $symbol;
            $url = $this->urls['api']['ws']['public'];
            $request = array(
                'method' => 'subscribe',
                'subscription' => array(
                    'type' => 'trades',
                    'coin' => $market['swap'] ? $market['baseName'] : $market['id'],
                ),
            );
            $message = $this->extend($request, $params);
            $trades = Async\await($this->watch($url, $messageHash, $message, $messageHash));
            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) {
            /**
             * unWatches information on multiple trades made in a $market
             *
             * @see https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/websocket/subscriptions
             *
             * @param {string} $symbol unified $market $symbol of the $market trades 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=trade-structure trade structures~
             */
            Async\await($this->load_markets());
            $market = $this->market($symbol);
            $symbol = $market['symbol'];
            $subMessageHash = 'trade:' . $symbol;
            $messageHash = 'unsubscribe:' . $subMessageHash;
            $url = $this->urls['api']['ws']['public'];
            $request = array(
                'method' => 'unsubscribe',
                'subscription' => array(
                    'type' => 'trades',
                    'coin' => $market['swap'] ? $market['baseName'] : $market['id'],
                ),
            );
            $message = $this->extend($request, $params);
            return Async\await($this->watch($url, $messageHash, $message, $messageHash));
        }) ();
    }

    public function handle_trades(Client $client, $message) {
        //
        //     {
        //         "channel" => "trades",
        //         "data" => array(
        //             {
        //                 "coin" => "BTC",
        //                 "side" => "A",
        //                 "px" => "68517.0",
        //                 "sz" => "0.005",
        //                 "time" => 1710125266669,
        //                 "hash" => "0xc872699f116e012186620407fc08a802015e0097c5cce74710697f7272e6e959",
        //                 "tid" => 981894269203506
        //             }
        //         )
        //     }
        //
        $entry = $this->safe_list($message, 'data', array());
        $first = $this->safe_dict($entry, 0, array());
        $coin = $this->safe_string($first, 'coin');
        $marketId = $this->coinToMarketId ($coin);
        $market = $this->market($marketId);
        $symbol = $market['symbol'];
        if (!(is_array($this->trades) && array_key_exists($symbol, $this->trades))) {
            $limit = $this->safe_integer($this->options, 'tradesLimit', 1000);
            $stored = new ArrayCache ($limit);
            $this->trades[$symbol] = $stored;
        }
        $trades = $this->trades[$symbol];
        for ($i = 0; $i < count($entry); $i++) {
            $data = $this->safe_dict($entry, $i);
            $trade = $this->parse_ws_trade($data);
            $trades->append ($trade);
        }
        $messageHash = 'trade:' . $symbol;
        $client->resolve ($trades, $messageHash);
    }

    public function parse_ws_trade(array $trade, ?array $market = null): array {
        //
        // fetchMyTrades
        //
        //     {
        //         "coin" => "BTC",
        //         "px" => "72528.0",
        //         "sz" => "0.11693",
        //         "side" => "A",
        //         "time" => 1710208712815,
        //         "startPosition" => "0.11693",
        //         "dir" => "Close Long",
        //         "closedPnl" => "-0.81851",
        //         "hash" => "0xc5adaf35f8402750c218040b0a7bc301130051521273b6f398b3caad3e1f3f5f",
        //         "oid" => 7484888874,
        //         "crossed" => true,
        //         "fee" => "2.968244",
        //         "liquidationMarkPx" => null,
        //         "tid" => 567547935839686,
        //         "cloid" => null
        //     }
        //
        // fetchTrades
        //
        //     {
        //         "coin" => "BTC",
        //         "side" => "A",
        //         "px" => "68517.0",
        //         "sz" => "0.005",
        //         "time" => 1710125266669,
        //         "hash" => "0xc872699f116e012186620407fc08a802015e0097c5cce74710697f7272e6e959",
        //         "tid" => 981894269203506
        //     }
        //
        $timestamp = $this->safe_integer($trade, 'time');
        $price = $this->safe_string($trade, 'px');
        $amount = $this->safe_string($trade, 'sz');
        $coin = $this->safe_string($trade, 'coin');
        $marketId = $this->coinToMarketId ($coin);
        $market = $this->safe_market($marketId, null);
        $symbol = $market['symbol'];
        $id = $this->safe_string($trade, 'tid');
        $side = $this->safe_string($trade, 'side');
        if ($side !== null) {
            $side = ($side === 'A') ? 'sell' : 'buy';
        }
        $fee = $this->safe_string($trade, 'fee');
        return $this->safe_trade(array(
            'info' => $trade,
            'timestamp' => $timestamp,
            'datetime' => $this->iso8601($timestamp),
            'symbol' => $symbol,
            'id' => $id,
            'order' => $this->safe_string($trade, 'oid'),
            'type' => null,
            'side' => $side,
            'takerOrMaker' => null,
            'price' => $price,
            'amount' => $amount,
            'cost' => null,
            'fee' => array( 'cost' => $fee, 'currency' => 'USDC' ),
        ), $market);
    }

    public function watch_ohlcv(string $symbol, $timeframe = '1m', ?int $since = null, ?int $limit = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbol, $timeframe, $since, $limit, $params) {
            /**
             * watches historical candlestick data containing the open, high, low, close price, and the volume of a $market
             *
             * @see https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/websocket/subscriptions
             *
             * @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'];
            $url = $this->urls['api']['ws']['public'];
            $request = array(
                'method' => 'subscribe',
                'subscription' => array(
                    'type' => 'candle',
                    'coin' => $market['swap'] ? $market['baseName'] : $market['id'],
                    'interval' => $timeframe,
                ),
            );
            $messageHash = 'candles:' . $timeframe . ':' . $symbol;
            $message = $this->extend($request, $params);
            $ohlcv = Async\await($this->watch($url, $messageHash, $message, $messageHash));
            if ($this->newUpdates) {
                $limit = $ohlcv->getLimit ($symbol, $limit);
            }
            return $this->filter_by_since_limit($ohlcv, $since, $limit, 0, true);
        }) ();
    }

    public function un_watch_ohlcv(string $symbol, $timeframe = '1m', $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbol, $timeframe, $params) {
            /**
             * watches historical candlestick data containing the open, high, low, close price, and the volume of a $market
             *
             * @see https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/websocket/subscriptions
             *
             * @param {string} $symbol unified $symbol of the $market to fetch OHLCV data for
             * @param {string} $timeframe the length of time each candle represents
             * @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'];
            $url = $this->urls['api']['ws']['public'];
            $request = array(
                'method' => 'unsubscribe',
                'subscription' => array(
                    'type' => 'candle',
                    'coin' => $market['swap'] ? $market['baseName'] : $market['id'],
                    'interval' => $timeframe,
                ),
            );
            $subMessageHash = 'candles:' . $timeframe . ':' . $symbol;
            $messagehash = 'unsubscribe:' . $subMessageHash;
            $message = $this->extend($request, $params);
            return Async\await($this->watch($url, $messagehash, $message, $messagehash));
        }) ();
    }

    public function handle_ohlcv(Client $client, $message) {
        //
        //     {
        //         channel => 'candle',
        //         $data => {
        //             t => 1710146280000,
        //             T => 1710146339999,
        //             s => 'BTC',
        //             i => '1m',
        //             o => '71400.0',
        //             c => '71411.0',
        //             h => '71422.0',
        //             l => '71389.0',
        //             v => '1.20407',
        //             n => 20
        //         }
        //     }
        //
        $data = $this->safe_dict($message, 'data', array());
        $base = $this->safe_string($data, 's');
        $marketId = $this->coinToMarketId ($base);
        $symbol = $this->safe_symbol($marketId);
        $timeframe = $this->safe_string($data, 'i');
        if (!(is_array($this->ohlcvs) && array_key_exists($symbol, $this->ohlcvs))) {
            $this->ohlcvs[$symbol] = array();
        }
        if (!(is_array($this->ohlcvs[$symbol]) && array_key_exists($timeframe, $this->ohlcvs[$symbol]))) {
            $limit = $this->safe_integer($this->options, 'OHLCVLimit', 1000);
            $stored = new ArrayCacheByTimestamp ($limit);
            $this->ohlcvs[$symbol][$timeframe] = $stored;
        }
        $ohlcv = $this->ohlcvs[$symbol][$timeframe];
        $parsed = $this->parse_ohlcv($data);
        $ohlcv->append ($parsed);
        $messageHash = 'candles:' . $timeframe . ':' . $symbol;
        $client->resolve ($ohlcv, $messageHash);
    }

    public function handle_ws_post(Client $client, array $message) {
        //    {
        //         channel => "post",
        //         $data => {
        //             $id => <number>,
        //             $response => {
        //                  type => "info" | "action" | "error",
        //                  $payload => array( ... )
        //         }
        //    }
        $data = $this->safe_dict($message, 'data');
        $id = $this->safe_string($data, 'id');
        $response = $this->safe_dict($data, 'response');
        $payload = $this->safe_dict($response, 'payload');
        $client->resolve ($payload, $id);
    }

    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://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/websocket/subscriptions
             *
             * @param {string} $symbol unified $market $symbol of the $market $orders were made in
             * @param {int} [$since] the earliest time in ms to fetch $orders for
             * @param {int} [$limit] the maximum number of order structures to retrieve
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @param {string} [$params->user] user address, will default to $this->walletAddress if not provided
             * @return {array[]} a list of ~@link https://docs.ccxt.com/#/?id=order-structure order structures~
             */
            Async\await($this->load_markets());
            $userAddress = null;
            list($userAddress, $params) = $this->handlePublicAddress ('watchOrders', $params);
            $market = null;
            $messageHash = 'order';
            if ($symbol !== null) {
                $market = $this->market($symbol);
                $symbol = $market['symbol'];
                $messageHash = $messageHash . ':' . $symbol;
            }
            $url = $this->urls['api']['ws']['public'];
            $request = array(
                'method' => 'subscribe',
                'subscription' => array(
                    'type' => 'orderUpdates',
                    'user' => $userAddress,
                ),
            );
            $message = $this->extend($request, $params);
            $orders = Async\await($this->watch($url, $messageHash, $message, $messageHash));
            if ($this->newUpdates) {
                $limit = $orders->getLimit ($symbol, $limit);
            }
            return $this->filter_by_symbol_since_limit($orders, $symbol, $since, $limit, true);
        }) ();
    }

    public function handle_order(Client $client, $message) {
        //
        //     {
        //         channel => 'orderUpdates',
        //         $data => array(
        //             {
        //                 $order => array(
        //                     coin => 'BTC',
        //                     side => 'B',
        //                     limitPx => '30000.0',
        //                     sz => '0.001',
        //                     oid => 7456484275,
        //                     timestamp => 1710163596492,
        //                     origSz => '0.001'
        //                 ),
        //                 status => 'open',
        //                 statusTimestamp => 1710163596492
        //             }
        //         )
        //     }
        //
        $data = $this->safe_list($message, 'data', array());
        if ($this->orders === null) {
            $limit = $this->safe_integer($this->options, 'ordersLimit', 1000);
            $this->orders = new ArrayCacheBySymbolById ($limit);
        }
        $dataLength = count($data);
        if ($dataLength === 0) {
            return;
        }
        $stored = $this->orders;
        $messageHash = 'order';
        $marketSymbols = array();
        for ($i = 0; $i < count($data); $i++) {
            $rawOrder = $data[$i];
            $order = $this->parse_order($rawOrder);
            $stored->append ($order);
            $symbol = $this->safe_string($order, 'symbol');
            $marketSymbols[$symbol] = true;
        }
        $keys = is_array($marketSymbols) ? array_keys($marketSymbols) : array();
        for ($i = 0; $i < count($keys); $i++) {
            $symbol = $keys[$i];
            $innerMessageHash = $messageHash . ':' . $symbol;
            $client->resolve ($stored, $innerMessageHash);
        }
        $client->resolve ($stored, $messageHash);
    }

    public function handle_error_message(Client $client, $message): Bool {
        //
        //    {
        //      "channel" => "post",
        //      "data" => {
        //        "id" => 1,
        //        "response" => {
        //          "type" => "action",
        //          "payload" => {
        //            "status" => "ok",
        //            "response" => {
        //              "type" => "order",
        //              "data" => {
        //                "statuses" => array(
        //                  {
        //                    "error" => "Order price cannot be more than 80% away from the reference price"
        //                  }
        //                )
        //              }
        //            }
        //          }
        //        }
        //      }
        //    }
        //
        //    {
        //         "channel" => "error",
        //         "data" => "Error parsing JSON into valid websocket request => array( \"type\" => \"allMids\" )"
        //     }
        //
        $channel = $this->safe_string($message, 'channel', '');
        if ($channel === 'error') {
            $ret_msg = $this->safe_string($message, 'data', '');
            $errorMsg = $this->id . ' ' . $ret_msg;
            $client->reject ($errorMsg);
            return true;
        }
        $data = $this->safe_dict($message, 'data', array());
        $id = $this->safe_string($message, 'id');
        if ($id === null) {
            $id = $this->safe_string($data, 'id');
        }
        $response = $this->safe_dict($data, 'response', array());
        $payload = $this->safe_dict($response, 'payload', array());
        $status = $this->safe_string($payload, 'status');
        if ($status !== null && $status !== 'ok') {
            $errorMsg = $this->id . ' ' . $this->json($payload);
            $client->reject ($errorMsg, $id);
            return true;
        }
        $type = $this->safe_string($payload, 'type');
        if ($type === 'error') {
            $error = $this->id . ' ' . $this->json($payload);
            $client->reject ($error, $id);
            return true;
        }
        try {
            $this->handle_errors(0, '', '', '', array(), $this->json($payload), $payload, array(), array());
        } catch (Exception $e) {
            $client->reject ($e, $id);
            return true;
        }
        return false;
    }

    public function handle_order_book_unsubscription(Client $client, array $subscription) {
        //
        //        "subscription":{
        //           "type":"l2Book",
        //           "coin":"BTC",
        //           "nSigFigs":5,
        //           "mantissa":null
        //        }
        //
        $coin = $this->safe_string($subscription, 'coin');
        $marketId = $this->coinToMarketId ($coin);
        $symbol = $this->safe_symbol($marketId);
        $subMessageHash = 'orderbook:' . $symbol;
        $messageHash = 'unsubscribe:' . $subMessageHash;
        $this->clean_unsubscription($client, $subMessageHash, $messageHash);
        if (is_array($this->orderbooks) && array_key_exists($symbol, $this->orderbooks)) {
            unset($this->orderbooks[$symbol]);
        }
    }

    public function handle_trades_unsubscription(Client $client, array $subscription) {
        //
        $coin = $this->safe_string($subscription, 'coin');
        $marketId = $this->coinToMarketId ($coin);
        $symbol = $this->safe_symbol($marketId);
        $subMessageHash = 'trade:' . $symbol;
        $messageHash = 'unsubscribe:' . $subMessageHash;
        $this->clean_unsubscription($client, $subMessageHash, $messageHash);
        if (is_array($this->trades) && array_key_exists($symbol, $this->trades)) {
            unset($this->trades[$symbol]);
        }
    }

    public function handle_tickers_unsubscription(Client $client, array $subscription) {
        //
        $subMessageHash = 'tickers';
        $messageHash = 'unsubscribe:' . $subMessageHash;
        $this->clean_unsubscription($client, $subMessageHash, $messageHash);
        $symbols = is_array($this->tickers) ? array_keys($this->tickers) : array();
        for ($i = 0; $i < count($symbols); $i++) {
            unset($this->tickers[$symbols[$i]]);
        }
    }

    public function handle_ohlcv_unsubscription(Client $client, array $subscription) {
        $coin = $this->safe_string($subscription, 'coin');
        $marketId = $this->coinToMarketId ($coin);
        $symbol = $this->safe_symbol($marketId);
        $interval = $this->safe_string($subscription, 'interval');
        $timeframe = $this->find_timeframe($interval);
        $subMessageHash = 'candles:' . $timeframe . ':' . $symbol;
        $messageHash = 'unsubscribe:' . $subMessageHash;
        $this->clean_unsubscription($client, $subMessageHash, $messageHash);
        if (is_array($this->ohlcvs) && array_key_exists($symbol, $this->ohlcvs)) {
            if (is_array($this->ohlcvs[$symbol]) && array_key_exists($timeframe, $this->ohlcvs[$symbol])) {
                unset($this->ohlcvs[$symbol][$timeframe]);
            }
        }
    }

    public function handle_subscription_response(Client $client, $message) {
        // {
        //     "channel":"subscriptionResponse",
        //     "data":{
        //        "method":"unsubscribe",
        //        "subscription":{
        //           "type":"l2Book",
        //           "coin":"BTC",
        //           "nSigFigs":5,
        //           "mantissa":null
        //        }
        //     }
        // }
        //
        //  {
        //      "channel":"subscriptionResponse",
        //      "data":{
        //         "method":"unsubscribe",
        //         "subscription":{
        //            "type":"trades",
        //            "coin":"PURR/USDC"
        //         }
        //      }
        //  }
        //
        $data = $this->safe_dict($message, 'data', array());
        $method = $this->safe_string($data, 'method');
        if ($method === 'unsubscribe') {
            $subscription = $this->safe_dict($data, 'subscription', array());
            $type = $this->safe_string($subscription, 'type');
            if ($type === 'l2Book') {
                $this->handle_order_book_unsubscription($client, $subscription);
            } elseif ($type === 'trades') {
                $this->handle_trades_unsubscription($client, $subscription);
            } elseif ($type === 'webData2') {
                $this->handle_tickers_unsubscription($client, $subscription);
            } elseif ($type === 'candle') {
                $this->handle_ohlcv_unsubscription($client, $subscription);
            }
        }
    }

    public function handle_message(Client $client, $message) {
        //
        // {
        //     "channel":"subscriptionResponse",
        //     "data":{
        //        "method":"unsubscribe",
        //        "subscription":{
        //           "type":"l2Book",
        //           "coin":"BTC",
        //           "nSigFigs":5,
        //           "mantissa":null
        //        }
        //     }
        // }
        //
        if ($this->handle_error_message($client, $message)) {
            return;
        }
        $topic = $this->safe_string($message, 'channel', '');
        $methods = array(
            'pong' => array($this, 'handle_pong'),
            'trades' => array($this, 'handle_trades'),
            'l2Book' => array($this, 'handle_order_book'),
            'candle' => array($this, 'handle_ohlcv'),
            'orderUpdates' => array($this, 'handle_order'),
            'userFills' => array($this, 'handle_my_trades'),
            'webData2' => array($this, 'handle_ws_tickers'),
            'post' => array($this, 'handle_ws_post'),
            'subscriptionResponse' => array($this, 'handle_subscription_response'),
        );
        $exacMethod = $this->safe_value($methods, $topic);
        if ($exacMethod !== null) {
            $exacMethod($client, $message);
            return;
        }
        $keys = is_array($methods) ? array_keys($methods) : array();
        for ($i = 0; $i < count($keys); $i++) {
            $key = $keys[$i];
            if (mb_strpos($topic, $keys[$i]) !== false) {
                $method = $methods[$key];
                $method($client, $message);
                return;
            }
        }
    }

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

    public function handle_pong(Client $client, $message) {
        //
        //   {
        //       "channel" => "pong"
        //   }
        //
        $client->lastPong = $this->safe_integer($message, 'pong');
        return $message;
    }

    public function request_id(): float {
        $requestId = $this->sum($this->safe_integer($this->options, 'requestId', 0), 1);
        $this->options['requestId'] = $requestId;
        return $requestId;
    }

    public function wrap_as_post_action(array $request): array {
        $requestId = $this->request_id();
        return array(
            'requestId' => $requestId,
            'request' => array(
                'method' => 'post',
                'id' => $requestId,
                'request' => array(
                    'type' => 'action',
                    'payload' => $request,
                ),
            ),
        );
    }
}
