<?php

namespace ccxt\async;

// 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\async\abstract\gemini as Exchange;
use ccxt\ExchangeError;
use ccxt\AuthenticationError;
use ccxt\ArgumentsRequired;
use ccxt\NotSupported;
use ccxt\Precise;
use \React\Async;
use \React\Promise;
use \React\Promise\PromiseInterface;

class gemini extends Exchange {

    public function describe(): mixed {
        return $this->deep_extend(parent::describe(), array(
            'id' => 'gemini',
            'name' => 'Gemini',
            'countries' => array( 'US' ),
            // 600 requests a minute = 10 requests per second => 1000ms / 10 = 100ms between requests (private endpoints)
            // 120 requests a minute = 2 requests per second => ( 1000ms / rateLimit ) / 2 = 5 (public endpoints)
            'rateLimit' => 100,
            'version' => 'v1',
            'pro' => true,
            'has' => array(
                'CORS' => null,
                'spot' => true,
                'margin' => false,
                'swap' => true,
                'future' => false,
                'option' => false,
                'addMargin' => false,
                'cancelOrder' => true,
                'closeAllPositions' => false,
                'closePosition' => false,
                'createDepositAddress' => true,
                'createMarketOrder' => false,
                'createOrder' => true,
                'createReduceOnlyOrder' => false,
                'fetchBalance' => true,
                'fetchBidsAsks' => false,
                'fetchBorrowRateHistories' => false,
                'fetchBorrowRateHistory' => false,
                'fetchClosedOrders' => false,
                'fetchCrossBorrowRate' => false,
                'fetchCrossBorrowRates' => false,
                'fetchCurrencies' => true,
                'fetchDepositAddress' => true,
                'fetchDepositAddresses' => false,
                'fetchDepositAddressesByNetwork' => true,
                'fetchDepositsWithdrawals' => true,
                'fetchFundingHistory' => false,
                'fetchFundingRate' => false,
                'fetchFundingRateHistory' => false,
                'fetchFundingRates' => false,
                'fetchIndexOHLCV' => false,
                'fetchIsolatedBorrowRate' => false,
                'fetchIsolatedBorrowRates' => false,
                'fetchLeverage' => false,
                'fetchLeverageTiers' => false,
                'fetchMarginMode' => false,
                'fetchMarkets' => true,
                'fetchMarkOHLCV' => false,
                'fetchMyTrades' => true,
                'fetchOHLCV' => true,
                'fetchOpenInterestHistory' => false,
                'fetchOpenOrders' => true,
                'fetchOrder' => true,
                'fetchOrderBook' => true,
                'fetchOrders' => false,
                'fetchPosition' => false,
                'fetchPositionMode' => false,
                'fetchPositions' => false,
                'fetchPositionsRisk' => false,
                'fetchPremiumIndexOHLCV' => false,
                'fetchTicker' => true,
                'fetchTickers' => true,
                'fetchTrades' => true,
                'fetchTradingFee' => false,
                'fetchTradingFees' => true,
                'fetchTransactions' => 'emulated',
                'postOnly' => true,
                'reduceMargin' => false,
                'sandbox' => true,
                'setLeverage' => false,
                'setMarginMode' => false,
                'setPositionMode' => false,
                'withdraw' => true,
            ),
            'urls' => array(
                'logo' => 'https://user-images.githubusercontent.com/1294454/27816857-ce7be644-6096-11e7-82d6-3c257263229c.jpg',
                'api' => array(
                    'public' => 'https://api.gemini.com',
                    'private' => 'https://api.gemini.com',
                    'web' => 'https://docs.gemini.com',
                    'webExchange' => 'https://exchange.gemini.com',
                ),
                'www' => 'https://gemini.com/',
                'doc' => array(
                    'https://docs.gemini.com/rest-api',
                    'https://docs.sandbox.gemini.com',
                ),
                'test' => array(
                    'public' => 'https://api.sandbox.gemini.com',
                    'private' => 'https://api.sandbox.gemini.com',
                    // use the true doc instead of the sandbox doc
                    // since they differ in parsing
                    // https://github.com/ccxt/ccxt/issues/7874
                    // https://github.com/ccxt/ccxt/issues/7894
                    'web' => 'https://docs.gemini.com',
                    'webExchange' => 'https://exchange.gemini.com',
                ),
                'fees' => array(
                    'https://gemini.com/api-fee-schedule',
                    'https://gemini.com/trading-fees',
                    'https://gemini.com/transfer-fees',
                ),
            ),
            'api' => array(
                'webExchange' => array(
                    'get' => array(
                        '',
                    ),
                ),
                'web' => array(
                    'get' => array(
                        'rest-api',
                    ),
                ),
                'public' => array(
                    'get' => array(
                        'v1/symbols' => 5,
                        'v1/symbols/details/{symbol}' => 5,
                        'v1/staking/rates' => 5,
                        'v1/pubticker/{symbol}' => 5,
                        'v2/ticker/{symbol}' => 5,
                        'v2/candles/{symbol}/{timeframe}' => 5,
                        'v1/trades/{symbol}' => 5,
                        'v1/auction/{symbol}' => 5,
                        'v1/auction/{symbol}/history' => 5,
                        'v1/pricefeed' => 5,
                        'v1/book/{symbol}' => 5,
                        'v1/earn/rates' => 5,
                    ),
                ),
                'private' => array(
                    'post' => array(
                        'v1/staking/unstake' => 1,
                        'v1/staking/stake' => 1,
                        'v1/staking/rewards' => 1,
                        'v1/staking/history' => 1,
                        'v1/order/new' => 1,
                        'v1/order/cancel' => 1,
                        'v1/wrap/{symbol}' => 1,
                        'v1/order/cancel/session' => 1,
                        'v1/order/cancel/all' => 1,
                        'v1/order/status' => 1,
                        'v1/orders' => 1,
                        'v1/mytrades' => 1,
                        'v1/notionalvolume' => 1,
                        'v1/tradevolume' => 1,
                        'v1/clearing/new' => 1,
                        'v1/clearing/status' => 1,
                        'v1/clearing/cancel' => 1,
                        'v1/clearing/confirm' => 1,
                        'v1/balances' => 1,
                        'v1/balances/staking' => 1,
                        'v1/notionalbalances/{currency}' => 1,
                        'v1/transfers' => 1,
                        'v1/addresses/{network}' => 1,
                        'v1/deposit/{network}/newAddress' => 1,
                        'v1/deposit/{currency}/newAddress' => 1,
                        'v1/withdraw/{currency}' => 1,
                        'v1/account/transfer/{currency}' => 1,
                        'v1/payments/addbank' => 1,
                        'v1/payments/methods' => 1,
                        'v1/payments/sen/withdraw' => 1,
                        'v1/balances/earn' => 1,
                        'v1/earn/interest' => 1,
                        'v1/earn/history' => 1,
                        'v1/approvedAddresses/{network}/request' => 1,
                        'v1/approvedAddresses/account/{network}' => 1,
                        'v1/approvedAddresses/{network}/remove' => 1,
                        'v1/account' => 1,
                        'v1/account/create' => 1,
                        'v1/account/list' => 1,
                        'v1/heartbeat' => 1,
                        'v1/roles' => 1,
                    ),
                ),
            ),
            'precisionMode' => TICK_SIZE,
            'fees' => array(
                'trading' => array(
                    'taker' => 0.004,
                    'maker' => 0.002,
                ),
            ),
            'httpExceptions' => array(
                '400' => '\\ccxt\\BadRequest', // Auction not open or paused, ineligible timing, market not open, or the request was malformed, in the case of a private API request, missing or malformed Gemini private API authentication headers
                '403' => '\\ccxt\\PermissionDenied', // The API key is missing the role necessary to access this private API endpoint
                '404' => '\\ccxt\\OrderNotFound', // Unknown API entry point or Order not found
                '406' => '\\ccxt\\InsufficientFunds', // Insufficient Funds
                '429' => '\\ccxt\\RateLimitExceeded', // Rate Limiting was applied
                '500' => '\\ccxt\\ExchangeError', // The server encountered an error
                '502' => '\\ccxt\\ExchangeNotAvailable', // Technical issues are preventing the request from being satisfied
                '503' => '\\ccxt\\OnMaintenance', // The exchange is down for maintenance
            ),
            'timeframes' => array(
                '1m' => '1m',
                '5m' => '5m',
                '15m' => '15m',
                '30m' => '30m',
                '1h' => '1hr',
                '6h' => '6hr',
                '1d' => '1day',
            ),
            'exceptions' => array(
                'exact' => array(
                    'AuctionNotOpen' => '\\ccxt\\BadRequest', // Failed to place an auction-only order because there is no current auction open for this symbol
                    'ClientOrderIdTooLong' => '\\ccxt\\BadRequest', // The Client Order ID must be under 100 characters
                    'ClientOrderIdMustBeString' => '\\ccxt\\BadRequest', // The Client Order ID must be a string
                    'ConflictingOptions' => '\\ccxt\\BadRequest', // New orders using a combination of order execution options are not supported
                    'EndpointMismatch' => '\\ccxt\\BadRequest', // The request was submitted to an endpoint different than the one in the payload
                    'EndpointNotFound' => '\\ccxt\\BadRequest', // No endpoint was specified
                    'IneligibleTiming' => '\\ccxt\\BadRequest', // Failed to place an auction order for the current auction on this symbol because the timing is not eligible, new orders may only be placed before the auction begins.
                    'InsufficientFunds' => '\\ccxt\\InsufficientFunds', // The order was rejected because of insufficient funds
                    'InvalidJson' => '\\ccxt\\BadRequest', // The JSON provided is invalid
                    'InvalidNonce' => '\\ccxt\\InvalidNonce', // The nonce was not greater than the previously used nonce, or was not present
                    'InvalidApiKey' => '\\ccxt\\AuthenticationError', // Invalid API key
                    'InvalidOrderType' => '\\ccxt\\InvalidOrder', // An unknown order type was provided
                    'InvalidPrice' => '\\ccxt\\InvalidOrder', // For new orders, the price was invalid
                    'InvalidQuantity' => '\\ccxt\\InvalidOrder', // A negative or otherwise invalid quantity was specified
                    'InvalidSide' => '\\ccxt\\InvalidOrder', // For new orders, and invalid side was specified
                    'InvalidSignature' => '\\ccxt\\AuthenticationError', // The signature did not match the expected signature
                    'InvalidSymbol' => '\\ccxt\\BadRequest', // An invalid symbol was specified
                    'InvalidTimestampInPayload' => '\\ccxt\\BadRequest', // The JSON payload contained a timestamp parameter with an unsupported value.
                    'Maintenance' => '\\ccxt\\OnMaintenance', // The system is down for maintenance
                    'MarketNotOpen' => '\\ccxt\\InvalidOrder', // The order was rejected because the market is not accepting new orders
                    'MissingApikeyHeader' => '\\ccxt\\AuthenticationError', // The X-GEMINI-APIKEY header was missing
                    'MissingOrderField' => '\\ccxt\\InvalidOrder', // A required order_id field was not specified
                    'MissingRole' => '\\ccxt\\AuthenticationError', // The API key used to access this endpoint does not have the required role assigned to it
                    'MissingPayloadHeader' => '\\ccxt\\AuthenticationError', // The X-GEMINI-PAYLOAD header was missing
                    'MissingSignatureHeader' => '\\ccxt\\AuthenticationError', // The X-GEMINI-SIGNATURE header was missing
                    'NoSSL' => '\\ccxt\\AuthenticationError', // You must use HTTPS to access the API
                    'OptionsMustBeArray' => '\\ccxt\\BadRequest', // The options parameter must be an array.
                    'OrderNotFound' => '\\ccxt\\OrderNotFound', // The order specified was not found
                    'RateLimit' => '\\ccxt\\RateLimitExceeded', // Requests were made too frequently. See Rate Limits below.
                    'System' => '\\ccxt\\ExchangeError', // We are experiencing technical issues
                    'UnsupportedOption' => '\\ccxt\\BadRequest', // This order execution option is not supported.
                ),
                'broad' => array(
                    'The Gemini Exchange is currently undergoing maintenance.' => '\\ccxt\\OnMaintenance', // The Gemini Exchange is currently undergoing maintenance. Please check https://status.gemini.com/ for more information.
                    'We are investigating technical issues with the Gemini Exchange.' => '\\ccxt\\ExchangeNotAvailable', // We are investigating technical issues with the Gemini Exchange. Please check https://status.gemini.com/ for more information.
                    'Internal Server Error' => '\\ccxt\\ExchangeNotAvailable',
                ),
            ),
            'options' => array(
                'fetchMarketsMethod' => 'fetch_markets_from_api', // fetch_markets_from_api, fetch_markets_from_web
                'fetchMarketFromWebRetries' => 10,
                'fetchMarketsFromAPI' => array(
                    'fetchDetailsForAllSymbols' => false,
                    'quoteCurrencies' => array( 'USDT', 'GUSD', 'USD', 'DAI', 'EUR', 'GBP', 'SGD', 'BTC', 'ETH', 'LTC', 'BCH', 'SOL', 'USDC' ),
                ),
                'fetchMarkets' => array(
                    'webApiEnable' => true, // fetches from WEB
                    'webApiRetries' => 10,
                ),
                'fetchUsdtMarkets' => array( 'btcusdt', 'ethusdt' ), // this is only used if markets-fetch is set from "web"; keep this list updated (not available trough web api)
                'fetchCurrencies' => array(
                    'webApiEnable' => true, // fetches from WEB
                    'webApiRetries' => 5,
                    'webApiMuteFailure' => true,
                ),
                'fetchTickerMethod' => 'fetchTickerV1', // fetchTickerV1, fetchTickerV2, fetchTickerV1AndV2
                'networks' => array(
                    'BTC' => 'bitcoin',
                    'ERC20' => 'ethereum',
                    'BCH' => 'bitcoincash',
                    'LTC' => 'litecoin',
                    'ZEC' => 'zcash',
                    'FIL' => 'filecoin',
                    'DOGE' => 'dogecoin',
                    'XTZ' => 'tezos',
                    'AVAXX' => 'avalanche',
                    'SOL' => 'solana',
                    'ATOM' => 'cosmos',
                    'DOT' => 'polkadot',
                ),
                'nonce' => 'milliseconds', // if getting a Network 400 error change to seconds,
                'conflictingMarkets' => array(
                    'paxgusd' => array(
                        'base' => 'PAXG',
                        'quote' => 'USD',
                    ),
                ),
                'brokenPairs' => array( 'efilusd', 'maticrlusd', 'maticusdc', 'eurusdc', 'maticgusd', 'maticusd', 'efilfil', 'eurusd' ),
            ),
            'features' => array(
                'default' => array(
                    'sandbox' => true,
                    'createOrder' => array(
                        'marginMode' => false,
                        'triggerPrice' => true,
                        'triggerPriceType' => null,
                        'triggerDirection' => false,
                        'stopLossPrice' => false, // todo
                        'takeProfitPrice' => false, // todo
                        'attachedStopLossTakeProfit' => null,
                        'timeInForce' => array(
                            'IOC' => true,
                            'FOK' => true,
                            'PO' => true,
                            'GTD' => false,
                        ),
                        'hedged' => false,
                        'trailing' => false,
                        'leverage' => false,
                        'marketBuyByCost' => true,
                        'marketBuyRequiresPrice' => false,
                        'selfTradePrevention' => false,
                        'iceberg' => false,
                    ),
                    'createOrders' => null,
                    'fetchMyTrades' => array(
                        'marginMode' => false,
                        'limit' => 500,
                        'daysBack' => null,
                        'untilDays' => null,
                        'symbolRequired' => true,
                    ),
                    'fetchOrder' => array(
                        'marginMode' => false,
                        'trigger' => false,
                        'trailing' => false,
                        'symbolRequired' => false,
                    ),
                    'fetchOpenOrders' => array(
                        'marginMode' => false,
                        'limit' => null,
                        'trigger' => false,
                        'trailing' => false,
                        'symbolRequired' => false,
                    ),
                    'fetchOrders' => null,
                    'fetchClosedOrders' => null, // todo => implement
                    'fetchOHLCV' => array(
                        'limit' => null,
                    ),
                ),
                'spot' => array(
                    'extends' => 'default',
                ),
                'swap' => array(
                    'linear' => array(
                        'extends' => 'default',
                    ),
                    'inverse' => null,
                ),
                'future' => array(
                    'linear' => null,
                    'inverse' => null,
                ),
            ),
        ));
    }

    public function fetch_currencies($params = array ()): PromiseInterface {
        return Async\async(function () use ($params) {
            /**
             * fetches all available currencies on an exchange
             * @param {array} [$params] extra parameters specific to the endpoint
             * @return {array} an associative dictionary of currencies
             */
            return Async\await($this->fetch_currencies_from_web($params));
        }) ();
    }

    public function fetch_currencies_from_web($params = array ()) {
        return Async\async(function () use ($params) {
            /**
             * @ignore
             * fetches all available currencies on an exchange
             * @param {array} [$params] extra parameters specific to the endpoint
             * @return {array} an associative dictionary of currencies
             */
            $data = Async\await($this->fetch_web_endpoint('fetchCurrencies', 'webExchangeGet', true, '="currencyData">', '</script>'));
            if ($data === null) {
                return array();
            }
            //
            //    {
            //        "tradingPairs" => array( array( 'BTCUSD', 2, 8, '0.00001', 10, true ),  ... ),
            //        "currencies" => array(
            //            array( "ORCA", "Orca", 204, 6, 0, 6, 8, false, null, "solana" ), //, precisions seem to be the 5th index
            //            array( "ATOM", "Cosmos", 44, 6, 0, 6, 8, false, null, "cosmos" ),
            //            array( "ETH", "Ether", 2, 6, 0, 18, 8, false, null, "ethereum" ),
            //            array( "GBP", "Pound Sterling", 22, 2, 2, 2, 2, true, "£", null ),
            //            ...
            //        ),
            //        "networks" => array(
            //            array( "solana", "SOL", "Solana" ),
            //            array( "zcash", "ZEC", "Zcash" ),
            //            array( "tezos", "XTZ", "Tezos" ),
            //            array( "cosmos", "ATOM", "Cosmos" ),
            //            array( "ethereum", "ETH", "Ethereum" ),
            //            ...
            //        )
            //    }
            //
            $result = array();
            $this->options['tradingPairs'] = $this->safe_list($data, 'tradingPairs');
            $currenciesArray = $this->safe_value($data, 'currencies', array());
            for ($i = 0; $i < count($currenciesArray); $i++) {
                $currency = $currenciesArray[$i];
                $id = $this->safe_string($currency, 0);
                $code = $this->safe_currency_code($id);
                $type = $this->safe_string($currency, 7) ? 'fiat' : 'crypto';
                $precision = $this->parse_number($this->parse_precision($this->safe_string($currency, 5)));
                $networks = array();
                $networkId = $this->safe_string($currency, 9);
                $networkCode = null;
                if ($networkId !== null) {
                    $networkCode = $this->network_id_to_code($networkId);
                    $networks[$networkCode] = array(
                        'info' => $currency,
                        'id' => $networkId,
                        'network' => $networkCode,
                        'active' => null,
                        'deposit' => null,
                        'withdraw' => null,
                        'fee' => null,
                        'precision' => $precision,
                        'limits' => array(
                            'deposit' => array(
                                'min' => null,
                                'max' => null,
                            ),
                            'withdraw' => array(
                                'min' => null,
                                'max' => null,
                            ),
                        ),
                    );
                }
                $result[$code] = $this->safe_currency_structure(array(
                    'info' => $currency,
                    'id' => $id,
                    'code' => $code,
                    'name' => $this->safe_string($currency, 1),
                    'active' => null,
                    'deposit' => null,
                    'withdraw' => null,
                    'fee' => null,
                    'type' => $type,
                    'precision' => $precision,
                    'limits' => array(
                        'deposit' => array(
                            'min' => null,
                            'max' => null,
                        ),
                        'withdraw' => array(
                            'min' => null,
                            'max' => null,
                        ),
                    ),
                    'networks' => $networks,
                ));
            }
            return $result;
        }) ();
    }

    public function fetch_markets($params = array ()): PromiseInterface {
        return Async\async(function () use ($params) {
            /**
             * retrieves data on all markets for gemini
             *
             * @see https://docs.gemini.com/rest-api/#symbols
             *
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @return {array[]} an array of objects representing market data
             */
            $method = $this->safe_value($this->options, 'fetchMarketsMethod', 'fetch_markets_from_api');
            if ($method === 'fetch_markets_from_web') {
                $promises = array();
                $promises[] = $this->fetch_markets_from_web($params); // get usd markets
                $promises[] = $this->fetch_usdt_markets($params); // get usdt markets
                $promisesResult = Async\await(Promise\all($promises));
                return $this->array_concat($promisesResult[0], $promisesResult[1]);
            }
            return Async\await($this->fetch_markets_from_api($params));
        }) ();
    }

    public function fetch_markets_from_web($params = array ()) {
        return Async\async(function () use ($params) {
            $data = Async\await($this->fetch_web_endpoint('fetchMarkets', 'webGetRestApi', false, '<h1 id="symbols-and-minimums">Symbols and minimums</h1>'));
            $error = $this->id . ' fetchMarketsFromWeb() the API doc HTML markup has changed, breaking the parser of order limits and precision info for markets.';
            $tables = explode('tbody>', $data);
            $numTables = count($tables);
            if ($numTables < 2) {
                throw new NotSupported($error);
            }
            $rows = explode("\n<tr>\n", $tables[1]); // eslint-disable-line quotes
            $numRows = count($rows);
            if ($numRows < 2) {
                throw new NotSupported($error);
            }
            $result = array();
            // skip the first element (empty string)
            for ($i = 1; $i < $numRows; $i++) {
                $row = $rows[$i];
                $cells = explode("</td>\n", $row); // eslint-disable-line quotes
                $numCells = count($cells);
                if ($numCells < 5) {
                    throw new NotSupported($error);
                }
                //     array(
                //         '<td>btcusd', // currency
                //         '<td>0.00001 BTC (1e-5)', // min order size
                //         '<td>0.00000001 BTC (1e-8)', // tick size
                //         '<td>0.01 USD', // $quote currency price increment
                //         '</tr>'
                //     )
                $marketId = str_replace('<td>', '', $cells[0]);
                $marketId = str_replace('*', '', $marketId);
                // $base = $this->safe_currency_code($baseId);
                $minAmountString = str_replace('<td>', '', $cells[1]);
                $minAmountParts = explode(' ', $minAmountString);
                $minAmount = $this->safe_number($minAmountParts, 0);
                $amountPrecisionString = str_replace('<td>', '', $cells[2]);
                $amountPrecisionParts = explode(' ', $amountPrecisionString);
                $idLength = strlen($marketId) - 0;
                $startingIndex = $idLength - 3;
                $pricePrecisionString = str_replace('<td>', '', $cells[3]);
                $pricePrecisionParts = explode(' ', $pricePrecisionString);
                $quoteId = $this->safe_string_lower($pricePrecisionParts, 1, mb_substr($marketId, $startingIndex, $idLength - $startingIndex));
                $baseId = $this->safe_string_lower($amountPrecisionParts, 1, str_replace($quoteId, '', $marketId));
                $base = $this->safe_currency_code($baseId);
                $quote = $this->safe_currency_code($quoteId);
                $result[] = array(
                    'id' => $marketId,
                    'symbol' => $base . '/' . $quote,
                    'base' => $base,
                    'quote' => $quote,
                    'settle' => null,
                    'baseId' => $baseId,
                    'quoteId' => $quoteId,
                    'settleId' => null,
                    'type' => 'spot',
                    'spot' => true,
                    'margin' => false,
                    'swap' => false,
                    'future' => false,
                    'option' => false,
                    'active' => null,
                    'contract' => false,
                    'linear' => null,
                    'inverse' => null,
                    'contractSize' => null,
                    'expiry' => null,
                    'expiryDatetime' => null,
                    'strike' => null,
                    'optionType' => null,
                    'precision' => array(
                        'amount' => $this->safe_number($amountPrecisionParts, 0),
                        'price' => $this->safe_number($pricePrecisionParts, 0),
                    ),
                    'limits' => array(
                        'leverage' => array(
                            'min' => null,
                            'max' => null,
                        ),
                        'amount' => array(
                            'min' => $minAmount,
                            'max' => null,
                        ),
                        'price' => array(
                            'min' => null,
                            'max' => null,
                        ),
                        'cost' => array(
                            'min' => null,
                            'max' => null,
                        ),
                    ),
                    'created' => null,
                    'info' => $row,
                );
            }
            return $result;
        }) ();
    }

    public function parse_market_active($status) {
        $statuses = array(
            'open' => true,
            'closed' => false,
            'cancel_only' => true,
            'post_only' => true,
            'limit_only' => true,
        );
        if ($status === null) {
            return true; // below
        }
        return $this->safe_bool($statuses, $status, true);
    }

    public function fetch_usdt_markets($params = array ()) {
        return Async\async(function () use ($params) {
            // these markets can't be scrapped and fetchMarketsFrom api does an extra call
            // to load market ids which we don't need here
            if (is_array($this->urls) && array_key_exists('test', $this->urls)) {
                return array(); // sandbox does not have usdt markets
            }
            $fetchUsdtMarkets = $this->safe_value($this->options, 'fetchUsdtMarkets', array());
            $result = array();
            for ($i = 0; $i < count($fetchUsdtMarkets); $i++) {
                $marketId = $fetchUsdtMarkets[$i];
                $request = array(
                    'symbol' => $marketId,
                );
                // don't use Promise.all here, for some reason the exchange can't handle it and crashes
                $rawResponse = Async\await($this->publicGetV1SymbolsDetailsSymbol ($this->extend($request, $params)));
                $result[] = $this->parse_market($rawResponse);
            }
            return $result;
        }) ();
    }

    public function fetch_markets_from_api($params = array ()) {
        return Async\async(function () use ($params) {
            $marketIdsRaw = Async\await($this->publicGetV1Symbols ($params));
            //
            //     array(
            //         "btcusd",
            //         "linkusd",
            //         ...
            //     )
            //
            $result = array();
            $options = $this->safe_dict($this->options, 'fetchMarketsFromAPI', array());
            $brokenPairs = $this->safe_list($this->options, 'brokenPairs', array());
            $marketIds = array();
            for ($i = 0; $i < count($marketIdsRaw); $i++) {
                if (!$this->in_array($marketIdsRaw[$i], $brokenPairs)) {
                    $marketIds[] = $marketIdsRaw[$i];
                }
            }
            if ($this->safe_bool($options, 'fetchDetailsForAllSymbols', false)) {
                $promises = array();
                for ($i = 0; $i < count($marketIds); $i++) {
                    $marketId = $marketIds[$i];
                    $request = array(
                        'symbol' => $marketId,
                    );
                    $promises[] = $this->publicGetV1SymbolsDetailsSymbol ($this->extend($request, $params));
                    //
                    //     {
                    //         "symbol" => "BTCUSD",
                    //         "base_currency" => "BTC",
                    //         "quote_currency" => "USD",
                    //         "tick_size" => 1E-8,
                    //         "quote_increment" => 0.01,
                    //         "min_order_size" => "0.00001",
                    //         "status" => "open",
                    //         "wrap_enabled" => false
                    //     }
                    //
                }
                $responses = Async\await(Promise\all($promises));
                for ($i = 0; $i < count($responses); $i++) {
                    $result[] = $this->parse_market($responses[$i]);
                }
            } else {
                // use trading-pairs info, if it was fetched
                $tradingPairs = $this->safe_list($this->options, 'tradingPairs');
                if ($tradingPairs !== null) {
                    $indexedTradingPairs = $this->index_by($tradingPairs, 0);
                    for ($i = 0; $i < count($marketIds); $i++) {
                        $marketId = $marketIds[$i];
                        $pairInfo = $this->safe_list($indexedTradingPairs, strtoupper($marketId));
                        if ($pairInfo !== null && !$this->in_array($marketId, $brokenPairs)) {
                            $result[] = $this->parse_market($pairInfo);
                        }
                    }
                } else {
                    for ($i = 0; $i < count($marketIds); $i++) {
                        if (!$this->in_array($marketIds[$i], $brokenPairs)) {
                            $result[] = $this->parse_market($marketIds[$i]);
                        }
                    }
                }
            }
            return $result;
        }) ();
    }

    public function parse_market($response): array {
        //
        // $response might be:
        //
        //     btcusd
        //
        // or
        //
        //     array(
        //         'BTCUSD',   // $symbol
        //         2,          // tick precision (priceTickDecimalPlaces)
        //         8,          // amount precision (quantityTickDecimalPlaces)
        //         '0.00001',  // quantityMinimum
        //         10,         // quantityRoundDecimalPlaces
        //         true        // minimumsAreInclusive
        //     ),
        //
        // or
        //
        //     {
        //         "symbol" => "BTCUSD", // perpetuals have 'PERP' suffix, $i->e. DOGEUSDPERP
        //         "base_currency" => "BTC",
        //         "quote_currency" => "USD",
        //         "tick_size" => 1E-8,
        //         "quote_increment" => 0.01,
        //         "min_order_size" => "0.00001",
        //         "status" => "open",
        //         "wrap_enabled" => false
        //         "product_type" => "swap", // only in perps
        //         "contract_type" => "linear", // only in perps
        //         "contract_price_currency" => "GUSD"
        //     }
        //
        $marketId = null;
        $baseId = null;
        $quoteId = null;
        $settleId = null;
        $tickSize = null;
        $amountPrecision = null;
        $minSize = null;
        $status = null;
        $swap = false;
        $contractSize = null;
        $linear = null;
        $inverse = null;
        $isString = (gettype($response) === 'string');
        $isArray = (gettype($response) === 'array' && array_keys($response) === array_keys(array_keys($response)));
        if (!$isString && !$isArray) {
            $marketId = $this->safe_string_lower($response, 'symbol');
            $amountPrecision = $this->safe_number($response, 'tick_size'); // right, exchange has an imperfect naming and this turns out to be an amount-precision
            $tickSize = $this->safe_number($response, 'quote_increment'); // this is tick-size actually
            $minSize = $this->safe_number($response, 'min_order_size');
            $status = $this->parse_market_active($this->safe_string($response, 'status'));
            $baseId = $this->safe_string($response, 'base_currency');
            $quoteId = $this->safe_string($response, 'quote_currency');
            $settleId = $this->safe_string($response, 'contract_price_currency');
        } else {
            // if no detailed API was called, then parse either string or array
            if ($isString) {
                $marketId = $response;
            } else {
                $marketId = $this->safe_string_lower($response, 0);
                $tickSize = $this->parse_number($this->parse_precision($this->safe_string($response, 1))); // priceTickDecimalPlaces
                $amountPrecision = $this->parse_number($this->parse_precision($this->safe_string($response, 2))); // quantityTickDecimalPlaces
                $minSize = $this->safe_number($response, 3); // quantityMinimum
            }
            $marketIdUpper = strtoupper($marketId);
            $isPerp = (mb_strpos($marketIdUpper, 'PERP') !== false);
            $marketIdWithoutPerp = str_replace('PERP', '', $marketIdUpper);
            $conflictingMarkets = $this->safe_dict($this->options, 'conflictingMarkets', array());
            $lowerCaseId = strtolower($marketIdWithoutPerp);
            if (is_array($conflictingMarkets) && array_key_exists($lowerCaseId, $conflictingMarkets)) {
                $conflictingMarket = $conflictingMarkets[$lowerCaseId];
                $baseId = $conflictingMarket['base'];
                $quoteId = $conflictingMarket['quote'];
                if ($isPerp) {
                    $settleId = $conflictingMarket['quote'];
                }
            } else {
                $quoteCurrencies = $this->handle_option('fetchMarketsFromAPI', 'quoteCurrencies', array());
                for ($i = 0; $i < count($quoteCurrencies); $i++) {
                    $quoteCurrency = $quoteCurrencies[$i];
                    if (str_ends_with($marketIdWithoutPerp, $quoteCurrency)) {
                        $quoteLength = $this->parse_to_int(-1 * strlen($quoteCurrency));
                        $baseId = mb_substr($marketIdWithoutPerp, 0, $quoteLength - 0);
                        $quoteId = $quoteCurrency;
                        if ($isPerp) {
                            $settleId = $quoteCurrency; // always same
                        }
                        break;
                    }
                }
            }
        }
        $base = $this->safe_currency_code($baseId);
        $quote = $this->safe_currency_code($quoteId);
        $settle = $this->safe_currency_code($settleId);
        $symbol = $base . '/' . $quote;
        if ($settleId !== null) {
            $symbol = $symbol . ':' . $settle;
            $swap = true;
            $contractSize = $tickSize; // always same
            $linear = true; // always $linear
            $inverse = false;
        }
        $type = $swap ? 'swap' : 'spot';
        return array(
            'id' => $marketId,
            'symbol' => $symbol,
            'base' => $base,
            'quote' => $quote,
            'settle' => $settle,
            'baseId' => $baseId,
            'quoteId' => $quoteId,
            'settleId' => $settleId,
            'type' => $type,
            'spot' => !$swap,
            'margin' => false,
            'swap' => $swap,
            'future' => false,
            'option' => false,
            'active' => $status,
            'contract' => $swap,
            'linear' => $linear,
            'inverse' => $inverse,
            'contractSize' => $contractSize,
            'expiry' => null,
            'expiryDatetime' => null,
            'strike' => null,
            'optionType' => null,
            'precision' => array(
                'price' => $tickSize,
                'amount' => $amountPrecision,
            ),
            'limits' => array(
                'leverage' => array(
                    'min' => null,
                    'max' => null,
                ),
                'amount' => array(
                    'min' => $minSize,
                    'max' => null,
                ),
                'price' => array(
                    'min' => null,
                    'max' => null,
                ),
                'cost' => array(
                    'min' => null,
                    'max' => null,
                ),
            ),
            'created' => null,
            'info' => $response,
        );
    }

    public function fetch_order_book(string $symbol, ?int $limit = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbol, $limit, $params) {
            /**
             * fetches information on open orders with bid (buy) and ask (sell) prices, volumes and other data
             *
             * @see https://docs.gemini.com/rest-api/#current-order-book
             *
             * @param {string} $symbol unified $symbol of the $market to fetch the order book for
             * @param {int} [$limit] the maximum amount of order book entries to return
             * @param {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);
            $request = array(
                'symbol' => $market['id'],
            );
            if ($limit !== null) {
                $request['limit_bids'] = $limit;
                $request['limit_asks'] = $limit;
            }
            $response = Async\await($this->publicGetV1BookSymbol ($this->extend($request, $params)));
            return $this->parse_order_book($response, $market['symbol'], null, 'bids', 'asks', 'price', 'amount');
        }) ();
    }

    public function fetch_ticker_v1(string $symbol, $params = array ()) {
        return Async\async(function () use ($symbol, $params) {
            Async\await($this->load_markets());
            $market = $this->market($symbol);
            $request = array(
                'symbol' => $market['id'],
            );
            $response = Async\await($this->publicGetV1PubtickerSymbol ($this->extend($request, $params)));
            //
            //     {
            //         "bid":"9117.95",
            //         "ask":"9117.96",
            //         "volume":array(
            //             "BTC":"1615.46861748",
            //             "USD":"14727307.57545006088",
            //             "timestamp":1594982700000
            //         ),
            //         "last":"9115.23"
            //     }
            //
            return $this->parse_ticker($response, $market);
        }) ();
    }

    public function fetch_ticker_v2(string $symbol, $params = array ()) {
        return Async\async(function () use ($symbol, $params) {
            Async\await($this->load_markets());
            $market = $this->market($symbol);
            $request = array(
                'symbol' => $market['id'],
            );
            $response = Async\await($this->publicGetV2TickerSymbol ($this->extend($request, $params)));
            //
            //     {
            //         "symbol":"BTCUSD",
            //         "open":"9080.58",
            //         "high":"9184.53",
            //         "low":"9063.56",
            //         "close":"9116.08",
            //         // Hourly prices descending for past 24 hours
            //         "changes":["9117.33","9105.69","9106.23","9120.35","9098.57","9114.53","9113.55","9128.01","9113.63","9133.49","9133.49","9137.75","9126.73","9103.91","9119.33","9123.04","9124.44","9117.57","9114.22","9102.33","9076.67","9074.72","9074.97","9092.05"],
            //         "bid":"9115.86",
            //         "ask":"9115.87"
            //     }
            //
            return $this->parse_ticker($response, $market);
        }) ();
    }

    public function fetch_ticker_v1_and_v2(string $symbol, $params = array ()) {
        return Async\async(function () use ($symbol, $params) {
            $tickerPromiseA = $this->fetch_ticker_v1($symbol, $params);
            $tickerPromiseB = $this->fetch_ticker_v2($symbol, $params);
            list($tickerA, $tickerB) = Async\await(Promise\all(array( $tickerPromiseA, $tickerPromiseB )));
            return $this->deep_extend($tickerA, array(
                'open' => $tickerB['open'],
                'high' => $tickerB['high'],
                'low' => $tickerB['low'],
                'change' => $tickerB['change'],
                'percentage' => $tickerB['percentage'],
                'average' => $tickerB['average'],
                'info' => $tickerB['info'],
            ));
        }) ();
    }

    public function fetch_ticker(string $symbol, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbol, $params) {
            /**
             * fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market
             *
             * @see https://docs.gemini.com/rest-api/#ticker
             * @see https://docs.gemini.com/rest-api/#ticker-v2
             *
             * @param {string} $symbol unified $symbol of the market to fetch the ticker for
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @param {array} [$params->fetchTickerMethod] 'fetchTickerV2', 'fetchTickerV1' or 'fetchTickerV1AndV2' - 'fetchTickerV1' for original ccxt.gemini.fetchTicker - 'fetchTickerV1AndV2' for 2 api calls to get the result of both fetchTicker methods - default = 'fetchTickerV1'
             * @return {array} a ~@link https://docs.ccxt.com/#/?id=ticker-structure ticker structure~
             */
            $method = $this->safe_value($this->options, 'fetchTickerMethod', 'fetchTickerV1');
            if ($method === 'fetchTickerV1') {
                return Async\await($this->fetch_ticker_v1($symbol, $params));
            }
            if ($method === 'fetchTickerV2') {
                return Async\await($this->fetch_ticker_v2($symbol, $params));
            }
            return Async\await($this->fetch_ticker_v1_and_v2($symbol, $params));
        }) ();
    }

    public function parse_ticker(array $ticker, ?array $market = null): array {
        //
        // fetchTickers
        //
        //     {
        //         "pair" => "BATUSD",
        //         "price" => "0.20687",
        //         "percentChange24h" => "0.0146"
        //     }
        //
        // fetchTickerV1
        //
        //     {
        //         "bid":"9117.95",
        //         "ask":"9117.96",
        //         "volume":array(
        //             "BTC":"1615.46861748",
        //             "USD":"14727307.57545006088",
        //             "timestamp":1594982700000
        //         ),
        //         "last":"9115.23"
        //     }
        //
        // fetchTickerV2
        //
        //     {
        //         "symbol":"BTCUSD",
        //         "open":"9080.58",
        //         "high":"9184.53",
        //         "low":"9063.56",
        //         "close":"9116.08",
        //         // Hourly prices descending for past 24 hours
        //         "changes":["9117.33","9105.69","9106.23","9120.35","9098.57","9114.53","9113.55","9128.01","9113.63","9133.49","9133.49","9137.75","9126.73","9103.91","9119.33","9123.04","9124.44","9117.57","9114.22","9102.33","9076.67","9074.72","9074.97","9092.05"],
        //         "bid":"9115.86",
        //         "ask":"9115.87"
        //     }
        //
        $volume = $this->safe_value($ticker, 'volume', array());
        $timestamp = $this->safe_integer($volume, 'timestamp');
        $symbol = null;
        $marketId = $this->safe_string_lower($ticker, 'pair');
        $market = $this->safe_market($marketId, $market);
        $baseId = null;
        $quoteId = null;
        $base = null;
        $quote = null;
        if (($marketId !== null) && ($market === null)) {
            $idLength = strlen($marketId) - 0;
            if ($idLength === 7) {
                $baseId = mb_substr($marketId, 0, 4 - 0);
                $quoteId = mb_substr($marketId, 4, 7 - 4);
            } else {
                $baseId = mb_substr($marketId, 0, 3 - 0);
                $quoteId = mb_substr($marketId, 3, 6 - 3);
            }
            $base = $this->safe_currency_code($baseId);
            $quote = $this->safe_currency_code($quoteId);
            $symbol = $base . '/' . $quote;
        }
        if (($symbol === null) && ($market !== null)) {
            $symbol = $market['symbol'];
            $baseId = $this->safe_string_upper($market, 'baseId');
            $quoteId = $this->safe_string_upper($market, 'quoteId');
        }
        $price = $this->safe_string($ticker, 'price');
        $last = $this->safe_string_2($ticker, 'last', 'close', $price);
        $percentage = $this->safe_string($ticker, 'percentChange24h');
        $open = $this->safe_string($ticker, 'open');
        $baseVolume = $this->safe_string($volume, $baseId);
        $quoteVolume = $this->safe_string($volume, $quoteId);
        return $this->safe_ticker(array(
            'symbol' => $symbol,
            'timestamp' => $timestamp,
            'datetime' => $this->iso8601($timestamp),
            'high' => $this->safe_string($ticker, 'high'),
            'low' => $this->safe_string($ticker, 'low'),
            'bid' => $this->safe_string($ticker, 'bid'),
            'bidVolume' => null,
            'ask' => $this->safe_string($ticker, 'ask'),
            'askVolume' => null,
            'vwap' => null,
            'open' => $open,
            'close' => $last,
            'last' => $last,
            'previousClose' => null, // previous day close
            'change' => null,
            'percentage' => $percentage,
            'average' => null,
            'baseVolume' => $baseVolume,
            'quoteVolume' => $quoteVolume,
            'info' => $ticker,
        ), $market);
    }

    public function fetch_tickers(?array $symbols = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbols, $params) {
            /**
             * fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market
             *
             * @see https://docs.gemini.com/rest-api/#price-feed
             *
             * @param {string[]|null} $symbols unified $symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @return {array} a dictionary of ~@link https://docs.ccxt.com/#/?id=ticker-structure ticker structures~
             */
            Async\await($this->load_markets());
            $response = Async\await($this->publicGetV1Pricefeed ($params));
            //
            //     array(
            //         array(
            //             "pair" => "BATUSD",
            //             "price" => "0.20687",
            //             "percentChange24h" => "0.0146"
            //         ),
            //         array(
            //             "pair" => "LINKETH",
            //             "price" => "0.018",
            //             "percentChange24h" => "0.0000"
            //         ),
            //     )
            //
            $result = $this->parse_tickers($response, $symbols);
            $brokenPairs = $this->safe_list($this->options, 'brokenPairs', array());
            return $this->remove_keys_from_dict($result, $brokenPairs);
        }) ();
    }

    public function parse_trade(array $trade, ?array $market = null): array {
        //
        // public fetchTrades
        //
        //     {
        //         "timestamp":1601617445,
        //         "timestampms":1601617445144,
        //         "tid":14122489752,
        //         "price":"0.46476",
        //         "amount":"28.407209",
        //         "exchange":"gemini",
        //         "type":"buy"
        //     }
        //
        // private fetchTrades
        //
        //      {
        //          "price":"3900.00",
        //          "amount":"0.00996",
        //          "timestamp":1638891173,
        //          "timestampms":1638891173518,
        //          "type":"Sell",
        //          "aggressor":false,
        //          "fee_currency":"EUR",
        //          "fee_amount":"0.00",
        //          "tid":73621746145,
        //          "order_id":"73621746059",
        //          "exchange":"gemini",
        //          "is_auction_fill":false,
        //          "is_clearing_fill":false,
        //          "symbol":"ETHEUR",
        //          "client_order_id":"1638891171610"
        //      }
        //
        $timestamp = $this->safe_integer($trade, 'timestampms');
        $id = $this->safe_string($trade, 'tid');
        $orderId = $this->safe_string($trade, 'order_id');
        $feeCurrencyId = $this->safe_string($trade, 'fee_currency');
        $feeCurrencyCode = $this->safe_currency_code($feeCurrencyId);
        $fee = array(
            'cost' => $this->safe_string($trade, 'fee_amount'),
            'currency' => $feeCurrencyCode,
        );
        $priceString = $this->safe_string($trade, 'price');
        $amountString = $this->safe_string($trade, 'amount');
        $side = $this->safe_string_lower($trade, 'type');
        $symbol = $this->safe_symbol(null, $market);
        return $this->safe_trade(array(
            'id' => $id,
            'order' => $orderId,
            'info' => $trade,
            'timestamp' => $timestamp,
            'datetime' => $this->iso8601($timestamp),
            'symbol' => $symbol,
            'type' => null,
            'side' => $side,
            'takerOrMaker' => null,
            'price' => $priceString,
            'cost' => null,
            'amount' => $amountString,
            'fee' => $fee,
        ), $market);
    }

    public function fetch_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.gemini.com/rest-api/#trade-history
             *
             * @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 {Trade[]} a list of ~@link https://docs.ccxt.com/#/?id=public-trades trade structures~
             */
            Async\await($this->load_markets());
            $market = $this->market($symbol);
            $request = array(
                'symbol' => $market['id'],
            );
            if ($limit !== null) {
                $request['limit_trades'] = min ($limit, 500);
            }
            if ($since !== null) {
                $request['timestamp'] = $since;
            }
            $response = Async\await($this->publicGetV1TradesSymbol ($this->extend($request, $params)));
            //
            //     array(
            //         array(
            //             "timestamp":1601617445,
            //             "timestampms":1601617445144,
            //             "tid":14122489752,
            //             "price":"0.46476",
            //             "amount":"28.407209",
            //             "exchange":"gemini",
            //             "type":"buy"
            //         ),
            //     )
            //
            return $this->parse_trades($response, $market, $since, $limit);
        }) ();
    }

    public function parse_balance($response): array {
        $result = array( 'info' => $response );
        for ($i = 0; $i < count($response); $i++) {
            $balance = $response[$i];
            $currencyId = $this->safe_string($balance, 'currency');
            $code = $this->safe_currency_code($currencyId);
            $account = $this->account();
            $account['free'] = $this->safe_string($balance, 'available');
            $account['total'] = $this->safe_string($balance, 'amount');
            $result[$code] = $account;
        }
        return $this->safe_balance($result);
    }

    public function fetch_trading_fees($params = array ()): PromiseInterface {
        return Async\async(function () use ($params) {
            /**
             * fetch the trading fees for multiple markets
             *
             * @see https://docs.gemini.com/rest-api/#get-notional-volume
             *
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @return {array} a dictionary of ~@link https://docs.ccxt.com/#/?id=fee-structure fee structures~ indexed by market symbols
             */
            Async\await($this->load_markets());
            $response = Async\await($this->privatePostV1Notionalvolume ($params));
            //
            //      {
            //          "web_maker_fee_bps" => 25,
            //          "web_taker_fee_bps" => 35,
            //          "web_auction_fee_bps" => 25,
            //          "api_maker_fee_bps" => 10,
            //          "api_taker_fee_bps" => 35,
            //          "api_auction_fee_bps" => 20,
            //          "fix_maker_fee_bps" => 10,
            //          "fix_taker_fee_bps" => 35,
            //          "fix_auction_fee_bps" => 20,
            //          "block_maker_fee_bps" => 0,
            //          "block_taker_fee_bps" => 50,
            //          "notional_30d_volume" => 150.00,
            //          "last_updated_ms" => 1551371446000,
            //          "date" => "2019-02-28",
            //          "notional_1d_volume" => array(
            //              array(
            //                  "date" => "2019-02-22",
            //                  "notional_volume" => 75.00
            //              ),
            //              {
            //                  "date" => "2019-02-14",
            //                  "notional_volume" => 75.00
            //              }
            //          )
            //     }
            //
            $makerBps = $this->safe_string($response, 'api_maker_fee_bps');
            $takerBps = $this->safe_string($response, 'api_taker_fee_bps');
            $makerString = Precise::string_div($makerBps, '10000');
            $takerString = Precise::string_div($takerBps, '10000');
            $maker = $this->parse_number($makerString);
            $taker = $this->parse_number($takerString);
            $result = array();
            for ($i = 0; $i < count($this->symbols); $i++) {
                $symbol = $this->symbols[$i];
                $result[$symbol] = array(
                    'info' => $response,
                    'symbol' => $symbol,
                    'maker' => $maker,
                    'taker' => $taker,
                    'percentage' => true,
                    'tierBased' => true,
                );
            }
            return $result;
        }) ();
    }

    public function fetch_balance($params = array ()): PromiseInterface {
        return Async\async(function () use ($params) {
            /**
             * query for balance and get the amount of funds available for trading or funds locked in orders
             *
             * @see https://docs.gemini.com/rest-api/#get-available-balances
             *
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @return {array} a ~@link https://docs.ccxt.com/#/?id=balance-structure balance structure~
             */
            Async\await($this->load_markets());
            $response = Async\await($this->privatePostV1Balances ($params));
            return $this->parse_balance($response);
        }) ();
    }

    public function parse_order(array $order, ?array $market = null): array {
        //
        // createOrder (private)
        //
        //      {
        //          "order_id":"106027397702",
        //          "id":"106027397702",
        //          "symbol":"etheur",
        //          "exchange":"gemini",
        //          "avg_execution_price":"2877.48",
        //          "side":"sell",
        //          "type":"exchange limit",
        //          "timestamp":"1650398122",
        //          "timestampms":1650398122308,
        //          "is_live":false,
        //          "is_cancelled":false,
        //          "is_hidden":false,
        //          "was_forced":false,
        //          "executed_amount":"0.014434",
        //          "client_order_id":"1650398121695",
        //          "options":array(),
        //          "price":"2800.00",
        //          "original_amount":"0.014434",
        //          "remaining_amount":"0"
        //      }
        //
        // fetchOrder (private)
        //
        //      {
        //          "order_id":"106028543717",
        //          "id":"106028543717",
        //          "symbol":"etheur",
        //          "exchange":"gemini",
        //          "avg_execution_price":"0.00",
        //          "side":"buy",
        //          "type":"exchange limit",
        //          "timestamp":"1650398446",
        //          "timestampms":1650398446375,
        //          "is_live":true,
        //          "is_cancelled":false,
        //          "is_hidden":false,
        //          "was_forced":false,
        //          "executed_amount":"0",
        //          "client_order_id":"1650398445709",
        //          "options":array(),
        //          "price":"2000.00",
        //          "original_amount":"0.01",
        //          "remaining_amount":"0.01"
        //      }
        //
        // fetchOpenOrders (private)
        //
        //      {
        //          "order_id":"106028543717",
        //          "id":"106028543717",
        //          "symbol":"etheur",
        //          "exchange":"gemini",
        //          "avg_execution_price":"0.00",
        //          "side":"buy",
        //          "type":"exchange limit",
        //          "timestamp":"1650398446",
        //          "timestampms":1650398446375,
        //          "is_live":true,
        //          "is_cancelled":false,
        //          "is_hidden":false,
        //          "was_forced":false,
        //          "executed_amount":"0",
        //          "client_order_id":"1650398445709",
        //          "options":array(),
        //          "price":"2000.00",
        //          "original_amount":"0.01",
        //          "remaining_amount":"0.01"
        //      }
        //
        // cancelOrder (private)
        //
        //      {
        //          "order_id":"106028543717",
        //          "id":"106028543717",
        //          "symbol":"etheur",
        //          "exchange":"gemini",
        //          "avg_execution_price":"0.00",
        //          "side":"buy",
        //          "type":"exchange limit",
        //          "timestamp":"1650398446",
        //          "timestampms":1650398446375,
        //          "is_live":false,
        //          "is_cancelled":true,
        //          "is_hidden":false,
        //          "was_forced":false,
        //          "executed_amount":"0",
        //          "client_order_id":"1650398445709",
        //          "reason":"Requested",
        //          "options":array(),
        //          "price":"2000.00",
        //          "original_amount":"0.01",
        //          "remaining_amount":"0.01"
        //      }
        //
        $timestamp = $this->safe_integer($order, 'timestampms');
        $amount = $this->safe_string($order, 'original_amount');
        $remaining = $this->safe_string($order, 'remaining_amount');
        $filled = $this->safe_string($order, 'executed_amount');
        $status = 'closed';
        if ($order['is_live']) {
            $status = 'open';
        }
        if ($order['is_cancelled']) {
            $status = 'canceled';
        }
        $price = $this->safe_string($order, 'price');
        $average = $this->safe_string($order, 'avg_execution_price');
        $type = $this->safe_string($order, 'type');
        if ($type === 'exchange limit') {
            $type = 'limit';
        } elseif ($type === 'market buy' || $type === 'market sell') {
            $type = 'market';
        } else {
            $type = $order['type'];
        }
        $fee = null;
        $marketId = $this->safe_string($order, 'symbol');
        $symbol = $this->safe_symbol($marketId, $market);
        $id = $this->safe_string($order, 'order_id');
        $side = $this->safe_string_lower($order, 'side');
        $clientOrderId = $this->safe_string($order, 'client_order_id');
        $optionsArray = $this->safe_value($order, 'options', array());
        $option = $this->safe_string($optionsArray, 0);
        $timeInForce = 'GTC';
        $postOnly = false;
        if ($option !== null) {
            if ($option === 'immediate-or-cancel') {
                $timeInForce = 'IOC';
            } elseif ($option === 'fill-or-kill') {
                $timeInForce = 'FOK';
            } elseif ($option === 'maker-or-cancel') {
                $timeInForce = 'PO';
                $postOnly = true;
            }
        }
        return $this->safe_order(array(
            'id' => $id,
            'clientOrderId' => $clientOrderId,
            'info' => $order,
            'timestamp' => $timestamp,
            'datetime' => $this->iso8601($timestamp),
            'lastTradeTimestamp' => null,
            'status' => $status,
            'symbol' => $symbol,
            'type' => $type,
            'timeInForce' => $timeInForce, // default set to GTC
            'postOnly' => $postOnly,
            'side' => $side,
            'price' => $price,
            'triggerPrice' => null,
            'average' => $average,
            'cost' => null,
            'amount' => $amount,
            'filled' => $filled,
            'remaining' => $remaining,
            'fee' => $fee,
            'trades' => null,
        ), $market);
    }

    public function fetch_order(string $id, ?string $symbol = null, $params = array ()) {
        return Async\async(function () use ($id, $symbol, $params) {
            /**
             * fetches information on an order made by the user
             *
             * @see https://docs.gemini.com/rest-api/#order-status
             *
             * @param {string} $id order $id
             * @param {string} $symbol unified $symbol of the market the order was made in
             * @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());
            $request = array(
                'order_id' => $id,
            );
            $response = Async\await($this->privatePostV1OrderStatus ($this->extend($request, $params)));
            //
            //      {
            //          "order_id":"106028543717",
            //          "id":"106028543717",
            //          "symbol":"etheur",
            //          "exchange":"gemini",
            //          "avg_execution_price":"0.00",
            //          "side":"buy",
            //          "type":"exchange limit",
            //          "timestamp":"1650398446",
            //          "timestampms":1650398446375,
            //          "is_live":true,
            //          "is_cancelled":false,
            //          "is_hidden":false,
            //          "was_forced":false,
            //          "executed_amount":"0",
            //          "client_order_id":"1650398445701",
            //          "options":array(),
            //          "price":"2000.00",
            //          "original_amount":"0.01",
            //          "remaining_amount":"0.01"
            //      }
            //
            return $this->parse_order($response);
        }) ();
    }

    public function fetch_open_orders(?string $symbol = null, ?int $since = null, ?int $limit = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbol, $since, $limit, $params) {
            /**
             * fetch all unfilled currently open orders
             *
             * @see https://docs.gemini.com/rest-api/#get-active-orders
             *
             * @param {string} $symbol unified $market $symbol
             * @param {int} [$since] the earliest time in ms to fetch open orders for
             * @param {int} [$limit] the maximum number of  open orders structures to retrieve
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @return {Order[]} a list of ~@link https://docs.ccxt.com/#/?id=order-structure order structures~
             */
            Async\await($this->load_markets());
            $response = Async\await($this->privatePostV1Orders ($params));
            //
            //      array(
            //          {
            //              "order_id":"106028543717",
            //              "id":"106028543717",
            //              "symbol":"etheur",
            //              "exchange":"gemini",
            //              "avg_execution_price":"0.00",
            //              "side":"buy",
            //              "type":"exchange $limit",
            //              "timestamp":"1650398446",
            //              "timestampms":1650398446375,
            //              "is_live":true,
            //              "is_cancelled":false,
            //              "is_hidden":false,
            //              "was_forced":false,
            //              "executed_amount":"0",
            //              "client_order_id":"1650398445709",
            //              "options":array(),
            //              "price":"2000.00",
            //              "original_amount":"0.01",
            //              "remaining_amount":"0.01"
            //          }
            //      )
            //
            $market = null;
            if ($symbol !== null) {
                $market = $this->market($symbol); // throws on non-existent $symbol
            }
            return $this->parse_orders($response, $market, $since, $limit);
        }) ();
    }

    public function create_order(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
             *
             * @see https://docs.gemini.com/rest-api/#new-order
             *
             * @param {string} $symbol unified $symbol of the $market to create an order in
             * @param {string} $type must be '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
             * @return {array} an ~@link https://docs.ccxt.com/#/?id=order-structure order structure~
             */
            Async\await($this->load_markets());
            if ($type !== 'limit') {
                throw new ExchangeError($this->id . ' createOrder() allows limit orders only');
            }
            $clientOrderId = $this->safe_string_2($params, 'clientOrderId', 'client_order_id');
            $params = $this->omit($params, array( 'clientOrderId', 'client_order_id' ));
            if ($clientOrderId === null) {
                $clientOrderId = (string) $this->milliseconds();
            }
            $market = $this->market($symbol);
            $amountString = $this->amount_to_precision($symbol, $amount);
            $priceString = $this->price_to_precision($symbol, $price);
            $request = array(
                'client_order_id' => $clientOrderId,
                'symbol' => $market['id'],
                'amount' => $amountString,
                'price' => $priceString,
                'side' => $side,
                'type' => 'exchange limit', // gemini allows limit orders only
                // 'options' => array(), one of =>  maker-or-cancel, immediate-or-cancel, fill-or-kill, auction-only, indication-of-interest
            );
            $type = $this->safe_string($params, 'type', $type);
            $params = $this->omit($params, 'type');
            $triggerPrice = $this->safe_string_n($params, array( 'triggerPrice', 'stop_price', 'stopPrice' ));
            $params = $this->omit($params, array( 'triggerPrice', 'stop_price', 'stopPrice', 'type' ));
            if ($type === 'stopLimit') {
                throw new ArgumentsRequired($this->id . ' createOrder() requires a $triggerPrice parameter or a stop_price parameter for ' . $type . ' orders');
            }
            if ($triggerPrice !== null) {
                $request['stop_price'] = $this->price_to_precision($symbol, $triggerPrice);
                $request['type'] = 'exchange stop limit';
            } else {
                // No $options can be applied to stop-limit orders at this time.
                $timeInForce = $this->safe_string($params, 'timeInForce');
                $params = $this->omit($params, 'timeInForce');
                if ($timeInForce !== null) {
                    if (($timeInForce === 'IOC') || ($timeInForce === 'immediate-or-cancel')) {
                        $request['options'] = array( 'immediate-or-cancel' );
                    } elseif (($timeInForce === 'FOK') || ($timeInForce === 'fill-or-kill')) {
                        $request['options'] = array( 'fill-or-kill' );
                    } elseif ($timeInForce === 'PO') {
                        $request['options'] = array( 'maker-or-cancel' );
                    }
                }
                $postOnly = $this->safe_bool($params, 'postOnly', false);
                $params = $this->omit($params, 'postOnly');
                if ($postOnly) {
                    $request['options'] = array( 'maker-or-cancel' );
                }
                // allowing override for auction-only and indication-of-interest order $options
                $options = $this->safe_string($params, 'options');
                if ($options !== null) {
                    $request['options'] = array( $options );
                }
            }
            $response = Async\await($this->privatePostV1OrderNew ($this->extend($request, $params)));
            //
            //      {
            //          "order_id":"106027397702",
            //          "id":"106027397702",
            //          "symbol":"etheur",
            //          "exchange":"gemini",
            //          "avg_execution_price":"2877.48",
            //          "side":"sell",
            //          "type":"exchange limit",
            //          "timestamp":"1650398122",
            //          "timestampms":1650398122308,
            //          "is_live":false,
            //          "is_cancelled":false,
            //          "is_hidden":false,
            //          "was_forced":false,
            //          "executed_amount":"0.014434",
            //          "client_order_id":"1650398121695",
            //          "options":array(),
            //          "price":"2800.00",
            //          "original_amount":"0.014434",
            //          "remaining_amount":"0"
            //      }
            //
            return $this->parse_order($response);
        }) ();
    }

    public function cancel_order(string $id, ?string $symbol = null, $params = array ()) {
        return Async\async(function () use ($id, $symbol, $params) {
            /**
             * cancels an open order
             *
             * @see https://docs.gemini.com/rest-api/#cancel-order
             *
             * @param {string} $id order $id
             * @param {string} $symbol unified $symbol of the market the order was made in
             * @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());
            $request = array(
                'order_id' => $id,
            );
            $response = Async\await($this->privatePostV1OrderCancel ($this->extend($request, $params)));
            //
            //      {
            //          "order_id":"106028543717",
            //          "id":"106028543717",
            //          "symbol":"etheur",
            //          "exchange":"gemini",
            //          "avg_execution_price":"0.00",
            //          "side":"buy",
            //          "type":"exchange limit",
            //          "timestamp":"1650398446",
            //          "timestampms":1650398446375,
            //          "is_live":false,
            //          "is_cancelled":true,
            //          "is_hidden":false,
            //          "was_forced":false,
            //          "executed_amount":"0",
            //          "client_order_id":"1650398445709",
            //          "reason":"Requested",
            //          "options":array(),
            //          "price":"2000.00",
            //          "original_amount":"0.01",
            //          "remaining_amount":"0.01"
            //      }
            //
            return $this->parse_order($response);
        }) ();
    }

    public function fetch_my_trades(?string $symbol = null, ?int $since = null, ?int $limit = null, $params = array ()) {
        return Async\async(function () use ($symbol, $since, $limit, $params) {
            /**
             * fetch all trades made by the user
             *
             * @see https://docs.gemini.com/rest-api/#get-past-trades
             *
             * @param {string} $symbol unified $market $symbol
             * @param {int} [$since] the earliest time in ms to fetch trades for
             * @param {int} [$limit] the maximum number of trades structures to retrieve
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @return {Trade[]} a list of ~@link https://docs.ccxt.com/#/?id=trade-structure trade structures~
             */
            if ($symbol === null) {
                throw new ArgumentsRequired($this->id . ' fetchMyTrades() requires a $symbol argument');
            }
            Async\await($this->load_markets());
            $market = $this->market($symbol);
            $request = array(
                'symbol' => $market['id'],
            );
            if ($limit !== null) {
                $request['limit_trades'] = $limit;
            }
            if ($since !== null) {
                $request['timestamp'] = $this->parse_to_int($since / 1000);
            }
            $response = Async\await($this->privatePostV1Mytrades ($this->extend($request, $params)));
            return $this->parse_trades($response, $market, $since, $limit);
        }) ();
    }

    public function withdraw(string $code, float $amount, string $address, ?string $tag = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($code, $amount, $address, $tag, $params) {
            /**
             * make a withdrawal
             *
             * @see https://docs.gemini.com/rest-api/#withdraw-crypto-funds
             *
             * @param {string} $code unified $currency $code
             * @param {float} $amount the $amount to withdraw
             * @param {string} $address the $address to withdraw to
             * @param {string} $tag
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @return {array} a ~@link https://docs.ccxt.com/#/?id=transaction-structure transaction structure~
             */
            list($tag, $params) = $this->handle_withdraw_tag_and_params($tag, $params);
            $this->check_address($address);
            Async\await($this->load_markets());
            $currency = $this->currency($code);
            $request = array(
                'currency' => $currency['id'],
                'amount' => $amount,
                'address' => $address,
            );
            $response = Async\await($this->privatePostV1WithdrawCurrency ($this->extend($request, $params)));
            //
            //   for BTC
            //     {
            //         "address":"mi98Z9brJ3TgaKsmvXatuRahbFRUFKRUdR",
            //         "amount":"1",
            //         "withdrawalId":"02176a83-a6b1-4202-9b85-1c1c92dd25c4",
            //         "message":"You have requested a transfer of 1 BTC to mi98Z9brJ3TgaKsmvXatuRahbFRUFKRUdR. This withdrawal will be sent to the blockchain within the next 60 seconds."
            //     }
            //
            //   for ETH
            //     {
            //         "address":"0xA63123350Acc8F5ee1b1fBd1A6717135e82dBd28",
            //         "amount":"2.34567",
            //         "txHash":"0x28267179f92926d85c5516bqc063b2631935573d8915258e95d9572eedcc8cc"
            //     }
            //
            //   for error (other variations of error messages are also expected)
            //     {
            //         "result":"error",
            //         "reason":"CryptoAddressWhitelistsNotEnabled",
            //         "message":"Cryptocurrency withdrawal $address whitelists are not enabled for account 24. Please contact support@gemini.com for information on setting up a withdrawal $address whitelist."
            //     }
            //
            $result = $this->safe_string($response, 'result');
            if ($result === 'error') {
                throw new ExchangeError($this->id . ' withdraw() failed => ' . $this->json($response));
            }
            return $this->parse_transaction($response, $currency);
        }) ();
    }

    public function nonce() {
        $nonceMethod = $this->safe_string($this->options, 'nonce', 'milliseconds');
        if ($nonceMethod === 'milliseconds') {
            return $this->milliseconds();
        }
        return $this->seconds();
    }

    public function fetch_deposits_withdrawals(?string $code = null, ?int $since = null, ?int $limit = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($code, $since, $limit, $params) {
            /**
             * fetch history of deposits and withdrawals
             *
             * @see https://docs.gemini.com/rest-api/#transfers
             *
             * @param {string} [$code] unified currency $code for the currency of the deposit/withdrawals, default is null
             * @param {int} [$since] timestamp in ms of the earliest deposit/withdrawal, default is null
             * @param {int} [$limit] max number of deposit/withdrawals to return, default is null
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @return {array} a list of ~@link https://docs.ccxt.com/#/?id=transaction-structure transaction structure~
             */
            Async\await($this->load_markets());
            $request = array();
            if ($limit !== null) {
                $request['limit_transfers'] = $limit;
            }
            if ($since !== null) {
                $request['timestamp'] = $since;
            }
            $response = Async\await($this->privatePostV1Transfers ($this->extend($request, $params)));
            return $this->parse_transactions($response);
        }) ();
    }

    public function parse_transaction(array $transaction, ?array $currency = null): array {
        //
        // withdraw
        //
        //   for BTC
        //     {
        //         "address":"mi98Z9brJ3TgaKsmvXatuRahbFRUFKRUdR",
        //         "amount":"1",
        //         "withdrawalId":"02176a83-a6b1-4202-9b85-1c1c92dd25c4",
        //         "message":"You have requested a transfer of 1 BTC to mi98Z9brJ3TgaKsmvXatuRahbFRUFKRUdR. This withdrawal will be sent to the blockchain within the next 60 seconds."
        //     }
        //
        //   for ETH
        //     {
        //         "address":"0xA63123350Acc8F5ee1b1fBd1A6717135e82dBd28",
        //         "amount":"2.34567",
        //         "txHash":"0x28267179f92926d85c5516bqc063b2631935573d8915258e95d9572eedcc8cc"
        //     }
        //
        $timestamp = $this->safe_integer($transaction, 'timestampms');
        $currencyId = $this->safe_string($transaction, 'currency');
        $code = $this->safe_currency_code($currencyId, $currency);
        $address = $this->safe_string($transaction, 'destination');
        $type = $this->safe_string_lower($transaction, 'type');
        // if status field is available, then it's complete
        $statusRaw = $this->safe_string($transaction, 'status');
        $fee = null;
        $feeAmount = $this->safe_number($transaction, 'feeAmount');
        if ($feeAmount !== null) {
            $fee = array(
                'cost' => $feeAmount,
                'currency' => $code,
            );
        }
        return array(
            'info' => $transaction,
            'id' => $this->safe_string_2($transaction, 'eid', 'withdrawalId'),
            'txid' => $this->safe_string($transaction, 'txHash'),
            'timestamp' => $timestamp,
            'datetime' => $this->iso8601($timestamp),
            'network' => null,
            'address' => $address,
            'addressTo' => null,
            'addressFrom' => null,
            'tag' => null, // or is it defined?
            'tagTo' => null,
            'tagFrom' => null,
            'type' => $type, // direction of the $transaction, ('deposit' | 'withdraw')
            'amount' => $this->safe_number($transaction, 'amount'),
            'currency' => $code,
            'status' => $this->parse_transaction_status($statusRaw),
            'updated' => null,
            'internal' => null,
            'comment' => $this->safe_string($transaction, 'message'),
            'fee' => $fee,
        );
    }

    public function parse_transaction_status(?string $status) {
        $statuses = array(
            'Advanced' => 'ok',
            'Complete' => 'ok',
        );
        return $this->safe_string($statuses, $status, $status);
    }

    public function parse_deposit_address($depositAddress, ?array $currency = null) {
        //
        //      {
        //          "address" => "0xed6494Fe7c1E56d1bd6136e89268C51E32d9708B",
        //          "timestamp" => "1636813923098",
        //          "addressVersion" => "eV1"                                         }
        //      }
        //
        $address = $this->safe_string($depositAddress, 'address');
        $code = $this->safe_currency_code(null, $currency);
        return array(
            'currency' => $code,
            'network' => null,
            'address' => $address,
            'tag' => null,
            'info' => $depositAddress,
        );
    }

    public function fetch_deposit_address(string $code, $params = array ()): PromiseInterface {
        return Async\async(function () use ($code, $params) {
            /**
             *
             * @see https://docs.gemini.com/rest-api/#get-deposit-addresses
             *
             * fetch the deposit address for a currency associated with this account
             * @param {string} $code unified currency $code
             * @param {array} [$params] extra parameters specific to the endpoint
             * @param {string} [$params->network]  *required* The chain of currency
             * @return {array} an ~@link https://docs.ccxt.com/#/?id=address-structure address structure~
             */
            Async\await($this->load_markets());
            $groupedByNetwork = Async\await($this->fetch_deposit_addresses_by_network($code, $params));
            $networkCode = null;
            list($networkCode, $params) = $this->handle_network_code_and_params($params);
            $networkGroup = $this->index_by($this->safe_value($groupedByNetwork, $networkCode), 'currency');
            return $this->safe_value($networkGroup, $code);
        }) ();
    }

    public function fetch_deposit_addresses_by_network(string $code, $params = array ()): PromiseInterface {
        return Async\async(function () use ($code, $params) {
            /**
             * fetch a dictionary of addresses for a $currency, indexed by network
             *
             * @see https://docs.gemini.com/rest-api/#get-deposit-addresses
             *
             * @param {string} $code unified $currency $code of the $currency for the deposit address
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @param {string} [$params->network]  *required* The chain of $currency
             * @return {array} a dictionary of ~@link https://docs.ccxt.com/#/?id=address-structure address structures~ indexed by the network
             */
            Async\await($this->load_markets());
            $currency = $this->currency($code);
            $code = $currency['code'];
            $networkCode = null;
            list($networkCode, $params) = $this->handle_network_code_and_params($params);
            if ($networkCode === null) {
                throw new ArgumentsRequired($this->id . ' fetchDepositAddresses() requires a network parameter');
            }
            $networkId = $this->network_code_to_id($networkCode);
            $request = array(
                'network' => $networkId,
            );
            $response = Async\await($this->privatePostV1AddressesNetwork ($this->extend($request, $params)));
            $results = $this->parse_deposit_addresses($response, array( $code ), false, array( 'network' => $networkCode, 'currency' => $code ));
            return $this->group_by($results, 'network');
        }) ();
    }

    public function sign($path, $api = 'public', $method = 'GET', $params = array (), $headers = null, $body = null) {
        $url = '/' . $this->implode_params($path, $params);
        $query = $this->omit($params, $this->extract_params($path));
        if ($api === 'private') {
            $this->check_required_credentials();
            $apiKey = $this->apiKey;
            if (mb_strpos($apiKey, 'account') === false) {
                throw new AuthenticationError($this->id . ' sign() requires an account-key, master-keys are not-supported');
            }
            $nonce = (string) $this->nonce();
            $request = $this->extend(array(
                'request' => $url,
                'nonce' => $nonce,
            ), $query);
            $payload = $this->json($request);
            $payload = base64_encode($payload);
            $signature = $this->hmac($this->encode($payload), $this->encode($this->secret), 'sha384');
            $headers = array(
                'Content-Type' => 'text/plain',
                'X-GEMINI-APIKEY' => $this->apiKey,
                'X-GEMINI-PAYLOAD' => $payload,
                'X-GEMINI-SIGNATURE' => $signature,
            );
        } else {
            if ($query) {
                $url .= '?' . $this->urlencode($query);
            }
        }
        $url = $this->urls['api'][$api] . $url;
        if (($method === 'POST') || ($method === 'DELETE')) {
            $body = $this->json($query);
        }
        return array( 'url' => $url, 'method' => $method, 'body' => $body, 'headers' => $headers );
    }

    public function handle_errors(int $httpCode, string $reason, string $url, string $method, array $headers, string $body, $response, $requestHeaders, $requestBody) {
        if ($response === null) {
            if (gettype($body) === 'string') {
                $feedback = $this->id . ' ' . $body;
                $this->throw_broadly_matched_exception($this->exceptions['broad'], $body, $feedback);
            }
            return null; // fallback to default error handler
        }
        //
        //     {
        //         "result" => "error",
        //         "reason" => "BadNonce",
        //         "message" => "Out-of-sequence nonce <1234> precedes previously used nonce <2345>"
        //     }
        //
        $result = $this->safe_string($response, 'result');
        if ($result === 'error') {
            $reasonInner = $this->safe_string($response, 'reason');
            $message = $this->safe_string($response, 'message');
            $feedback = $this->id . ' ' . $message;
            $this->throw_exactly_matched_exception($this->exceptions['exact'], $reasonInner, $feedback);
            $this->throw_exactly_matched_exception($this->exceptions['exact'], $message, $feedback);
            $this->throw_broadly_matched_exception($this->exceptions['broad'], $message, $feedback);
            throw new ExchangeError($feedback); // unknown $message
        }
        return null;
    }

    public function create_deposit_address(string $code, $params = array ()): PromiseInterface {
        return Async\async(function () use ($code, $params) {
            /**
             * create a $currency deposit $address
             *
             * @see https://docs.gemini.com/rest-api/#new-deposit-$address
             *
             * @param {string} $code unified $currency $code of the $currency for the deposit $address
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @return {array} an ~@link https://docs.ccxt.com/#/?id=$address-structure $address structure~
             */
            Async\await($this->load_markets());
            $currency = $this->currency($code);
            $request = array(
                'currency' => $currency['id'],
            );
            $response = Async\await($this->privatePostV1DepositCurrencyNewAddress ($this->extend($request, $params)));
            $address = $this->safe_string($response, 'address');
            $this->check_address($address);
            return array(
                'currency' => $code,
                'address' => $address,
                'tag' => null,
                'network' => null,
                'info' => $response,
            );
        }) ();
    }

    public function fetch_ohlcv(string $symbol, $timeframe = '1m', ?int $since = null, ?int $limit = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbol, $timeframe, $since, $limit, $params) {
            /**
             * fetches historical candlestick data containing the open, high, low, and close price, and the volume of a $market
             *
             * @see https://docs.gemini.com/rest-api/#candles
             *
             * @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);
            $timeframeId = $this->safe_string($this->timeframes, $timeframe, $timeframe);
            $request = array(
                'timeframe' => $timeframeId,
                'symbol' => $market['id'],
            );
            $response = Async\await($this->publicGetV2CandlesSymbolTimeframe ($this->extend($request, $params)));
            //
            //     [
            //         [1591515000000,0.02509,0.02509,0.02509,0.02509,0],
            //         [1591514700000,0.02503,0.02509,0.02503,0.02509,44.6405],
            //         [1591514400000,0.02503,0.02503,0.02503,0.02503,0],
            //     ]
            //
            return $this->parse_ohlcvs($response, $market, $timeframe, $since, $limit);
        }) ();
    }
}
