<?php

namespace ccxt;

// 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\abstract\bitstamp as Exchange;

class bitstamp extends Exchange {

    public function describe(): mixed {
        return $this->deep_extend(parent::describe(), array(
            'id' => 'bitstamp',
            'name' => 'Bitstamp',
            'countries' => array( 'GB' ),
            // 8000 requests per 10 minutes = 8000 / 600 = 13.33333333 requests per second => 1000ms / 13.33333333 = 75ms between requests on average
            'rateLimit' => 75,
            'version' => 'v2',
            'userAgent' => $this->userAgents['chrome'],
            'pro' => true,
            'has' => array(
                'CORS' => true,
                'spot' => true,
                'margin' => false,
                'swap' => false,
                'future' => false,
                'option' => false,
                'addMargin' => false,
                'borrowCrossMargin' => false,
                'borrowIsolatedMargin' => false,
                'borrowMargin' => false,
                'cancelAllOrders' => true,
                'cancelOrder' => true,
                'closeAllPositions' => false,
                'closePosition' => false,
                'createOrder' => true,
                'createOrderWithTakeProfitAndStopLoss' => false,
                'createOrderWithTakeProfitAndStopLossWs' => false,
                'createReduceOnlyOrder' => false,
                'createStopLimitOrder' => false,
                'createStopMarketOrder' => false,
                'createStopOrder' => false,
                'fetchBalance' => true,
                'fetchBorrowInterest' => false,
                'fetchBorrowRate' => false,
                'fetchBorrowRateHistories' => false,
                'fetchBorrowRateHistory' => false,
                'fetchBorrowRates' => false,
                'fetchBorrowRatesPerSymbol' => false,
                'fetchCrossBorrowRate' => false,
                'fetchCrossBorrowRates' => false,
                'fetchCurrencies' => true,
                'fetchDepositAddress' => true,
                'fetchDepositAddresses' => false,
                'fetchDepositAddressesByNetwork' => false,
                'fetchDepositsWithdrawals' => true,
                'fetchDepositWithdrawFee' => 'emulated',
                'fetchDepositWithdrawFees' => true,
                'fetchFundingHistory' => false,
                'fetchFundingInterval' => false,
                'fetchFundingIntervals' => false,
                'fetchFundingRate' => false,
                'fetchFundingRateHistory' => false,
                'fetchFundingRates' => false,
                'fetchGreeks' => false,
                'fetchIndexOHLCV' => false,
                'fetchIsolatedBorrowRate' => false,
                'fetchIsolatedBorrowRates' => false,
                'fetchIsolatedPositions' => false,
                'fetchLedger' => true,
                'fetchLeverage' => false,
                'fetchLeverages' => false,
                'fetchLeverageTiers' => false,
                'fetchLiquidations' => false,
                'fetchLongShortRatio' => false,
                'fetchLongShortRatioHistory' => false,
                'fetchMarginAdjustmentHistory' => false,
                'fetchMarginMode' => false,
                'fetchMarginModes' => false,
                'fetchMarketLeverageTiers' => false,
                'fetchMarkets' => true,
                'fetchMarkOHLCV' => false,
                'fetchMarkPrices' => false,
                'fetchMyLiquidations' => false,
                'fetchMySettlementHistory' => false,
                'fetchMyTrades' => true,
                'fetchOHLCV' => true,
                'fetchOpenInterest' => false,
                'fetchOpenInterestHistory' => false,
                'fetchOpenInterests' => false,
                'fetchOpenOrders' => true,
                'fetchOption' => false,
                'fetchOptionChain' => false,
                'fetchOrder' => true,
                'fetchOrderBook' => true,
                'fetchPosition' => false,
                'fetchPositionHistory' => false,
                'fetchPositionMode' => false,
                'fetchPositions' => false,
                'fetchPositionsForSymbol' => false,
                'fetchPositionsHistory' => false,
                'fetchPositionsRisk' => false,
                'fetchPremiumIndexOHLCV' => false,
                'fetchSettlementHistory' => false,
                'fetchTicker' => true,
                'fetchTickers' => true,
                'fetchTrades' => true,
                'fetchTradingFee' => true,
                'fetchTradingFees' => true,
                'fetchTransactionFees' => true,
                'fetchTransactions' => 'emulated',
                'fetchVolatilityHistory' => false,
                'fetchWithdrawals' => true,
                'reduceMargin' => false,
                'repayCrossMargin' => false,
                'repayIsolatedMargin' => false,
                'setLeverage' => false,
                'setMargin' => false,
                'setMarginMode' => false,
                'setPositionMode' => false,
                'transfer' => true,
                'withdraw' => true,
            ),
            'urls' => array(
                'logo' => 'https://github.com/user-attachments/assets/d5480572-1fee-43cb-b900-d38c522d0024',
                'api' => array(
                    'public' => 'https://www.bitstamp.net/api',
                    'private' => 'https://www.bitstamp.net/api',
                ),
                'www' => 'https://www.bitstamp.net',
                'doc' => 'https://www.bitstamp.net/api',
            ),
            'timeframes' => array(
                '1m' => '60',
                '3m' => '180',
                '5m' => '300',
                '15m' => '900',
                '30m' => '1800',
                '1h' => '3600',
                '2h' => '7200',
                '4h' => '14400',
                '6h' => '21600',
                '12h' => '43200',
                '1d' => '86400',
                '1w' => '259200',
            ),
            'requiredCredentials' => array(
                'apiKey' => true,
                'secret' => true,
            ),
            'api' => array(
                'public' => array(
                    'get' => array(
                        'ohlc/{pair}/' => 1,
                        'order_book/{pair}/' => 1,
                        'ticker/' => 1,
                        'ticker_hour/{pair}/' => 1,
                        'ticker/{pair}/' => 1,
                        'transactions/{pair}/' => 1,
                        'trading-pairs-info/' => 1,
                        'currencies/' => 1,
                        'eur_usd/' => 1,
                        'travel_rule/vasps/' => 1,
                    ),
                ),
                'private' => array(
                    'get' => array(
                        'travel_rule/contacts/' => 1,
                        'contacts/{contact_uuid}/' => 1,
                        'earn/subscriptions/' => 1,
                        'earn/transactions/' => 1,
                    ),
                    'post' => array(
                        'account_balances/' => 1,
                        'account_balances/{currency}/' => 1,
                        'balance/' => 1,
                        'balance/{pair}/' => 1,
                        'bch_withdrawal/' => 1,
                        'bch_address/' => 1,
                        'user_transactions/' => 1,
                        'user_transactions/{pair}/' => 1,
                        'crypto-transactions/' => 1,
                        'open_order' => 1,
                        'open_orders/all/' => 1,
                        'open_orders/{pair}/' => 1,
                        'order_status/' => 1,
                        'cancel_order/' => 1,
                        'cancel_all_orders/' => 1,
                        'cancel_all_orders/{pair}/' => 1,
                        'buy/{pair}/' => 1,
                        'buy/market/{pair}/' => 1,
                        'buy/instant/{pair}/' => 1,
                        'sell/{pair}/' => 1,
                        'sell/market/{pair}/' => 1,
                        'sell/instant/{pair}/' => 1,
                        'transfer-to-main/' => 1,
                        'transfer-from-main/' => 1,
                        'my_trading_pairs/' => 1,
                        'fees/trading/' => 1,
                        'fees/trading/{market_symbol}' => 1,
                        'fees/withdrawal/' => 1,
                        'fees/withdrawal/{currency}/' => 1,
                        'withdrawal-requests/' => 1,
                        'withdrawal/open/' => 1,
                        'withdrawal/status/' => 1,
                        'withdrawal/cancel/' => 1,
                        'liquidation_address/new/' => 1,
                        'liquidation_address/info/' => 1,
                        'btc_unconfirmed/' => 1,
                        'websockets_token/' => 1,
                        // individual coins
                        'btc_withdrawal/' => 1,
                        'btc_address/' => 1,
                        'ripple_withdrawal/' => 1,
                        'ripple_address/' => 1,
                        'ltc_withdrawal/' => 1,
                        'ltc_address/' => 1,
                        'eth_withdrawal/' => 1,
                        'eth_address/' => 1,
                        'xrp_withdrawal/' => 1,
                        'xrp_address/' => 1,
                        'xlm_withdrawal/' => 1,
                        'xlm_address/' => 1,
                        'pax_withdrawal/' => 1,
                        'pax_address/' => 1,
                        'link_withdrawal/' => 1,
                        'link_address/' => 1,
                        'usdc_withdrawal/' => 1,
                        'usdc_address/' => 1,
                        'omg_withdrawal/' => 1,
                        'omg_address/' => 1,
                        'dai_withdrawal/' => 1,
                        'dai_address/' => 1,
                        'knc_withdrawal/' => 1,
                        'knc_address/' => 1,
                        'mkr_withdrawal/' => 1,
                        'mkr_address/' => 1,
                        'zrx_withdrawal/' => 1,
                        'zrx_address/' => 1,
                        'gusd_withdrawal/' => 1,
                        'gusd_address/' => 1,
                        'aave_withdrawal/' => 1,
                        'aave_address/' => 1,
                        'bat_withdrawal/' => 1,
                        'bat_address/' => 1,
                        'uma_withdrawal/' => 1,
                        'uma_address/' => 1,
                        'snx_withdrawal/' => 1,
                        'snx_address/' => 1,
                        'uni_withdrawal/' => 1,
                        'uni_address/' => 1,
                        'yfi_withdrawal/' => 1,
                        'yfi_address/' => 1,
                        'audio_withdrawal/' => 1,
                        'audio_address/' => 1,
                        'crv_withdrawal/' => 1,
                        'crv_address/' => 1,
                        'algo_withdrawal/' => 1,
                        'algo_address/' => 1,
                        'comp_withdrawal/' => 1,
                        'comp_address/' => 1,
                        'grt_withdrawal/' => 1,
                        'grt_address/' => 1,
                        'usdt_withdrawal/' => 1,
                        'usdt_address/' => 1,
                        'eurt_withdrawal/' => 1,
                        'eurt_address/' => 1,
                        'matic_withdrawal/' => 1,
                        'matic_address/' => 1,
                        'sushi_withdrawal/' => 1,
                        'sushi_address/' => 1,
                        'chz_withdrawal/' => 1,
                        'chz_address/' => 1,
                        'enj_withdrawal/' => 1,
                        'enj_address/' => 1,
                        'alpha_withdrawal/' => 1,
                        'alpha_address/' => 1,
                        'ftt_withdrawal/' => 1,
                        'ftt_address/' => 1,
                        'storj_withdrawal/' => 1,
                        'storj_address/' => 1,
                        'axs_withdrawal/' => 1,
                        'axs_address/' => 1,
                        'sand_withdrawal/' => 1,
                        'sand_address/' => 1,
                        'hbar_withdrawal/' => 1,
                        'hbar_address/' => 1,
                        'rgt_withdrawal/' => 1,
                        'rgt_address/' => 1,
                        'fet_withdrawal/' => 1,
                        'fet_address/' => 1,
                        'skl_withdrawal/' => 1,
                        'skl_address/' => 1,
                        'cel_withdrawal/' => 1,
                        'cel_address/' => 1,
                        'sxp_withdrawal/' => 1,
                        'sxp_address/' => 1,
                        'ada_withdrawal/' => 1,
                        'ada_address/' => 1,
                        'slp_withdrawal/' => 1,
                        'slp_address/' => 1,
                        'ftm_withdrawal/' => 1,
                        'ftm_address/' => 1,
                        'perp_withdrawal/' => 1,
                        'perp_address/' => 1,
                        'dydx_withdrawal/' => 1,
                        'dydx_address/' => 1,
                        'gala_withdrawal/' => 1,
                        'gala_address/' => 1,
                        'shib_withdrawal/' => 1,
                        'shib_address/' => 1,
                        'amp_withdrawal/' => 1,
                        'amp_address/' => 1,
                        'sgb_withdrawal/' => 1,
                        'sgb_address/' => 1,
                        'avax_withdrawal/' => 1,
                        'avax_address/' => 1,
                        'wbtc_withdrawal/' => 1,
                        'wbtc_address/' => 1,
                        'ctsi_withdrawal/' => 1,
                        'ctsi_address/' => 1,
                        'cvx_withdrawal/' => 1,
                        'cvx_address/' => 1,
                        'imx_withdrawal/' => 1,
                        'imx_address/' => 1,
                        'nexo_withdrawal/' => 1,
                        'nexo_address/' => 1,
                        'ust_withdrawal/' => 1,
                        'ust_address/' => 1,
                        'ant_withdrawal/' => 1,
                        'ant_address/' => 1,
                        'gods_withdrawal/' => 1,
                        'gods_address/' => 1,
                        'rad_withdrawal/' => 1,
                        'rad_address/' => 1,
                        'band_withdrawal/' => 1,
                        'band_address/' => 1,
                        'inj_withdrawal/' => 1,
                        'inj_address/' => 1,
                        'rly_withdrawal/' => 1,
                        'rly_address/' => 1,
                        'rndr_withdrawal/' => 1,
                        'rndr_address/' => 1,
                        'vega_withdrawal/' => 1,
                        'vega_address/' => 1,
                        '1inch_withdrawal/' => 1,
                        '1inch_address/' => 1,
                        'ens_withdrawal/' => 1,
                        'ens_address/' => 1,
                        'mana_withdrawal/' => 1,
                        'mana_address/' => 1,
                        'lrc_withdrawal/' => 1,
                        'lrc_address/' => 1,
                        'ape_withdrawal/' => 1,
                        'ape_address/' => 1,
                        'mpl_withdrawal/' => 1,
                        'mpl_address/' => 1,
                        'euroc_withdrawal/' => 1,
                        'euroc_address/' => 1,
                        'sol_withdrawal/' => 1,
                        'sol_address/' => 1,
                        'dot_withdrawal/' => 1,
                        'dot_address/' => 1,
                        'near_withdrawal/' => 1,
                        'near_address/' => 1,
                        'doge_withdrawal/' => 1,
                        'doge_address/' => 1,
                        'flr_withdrawal/' => 1,
                        'flr_address/' => 1,
                        'dgld_withdrawal/' => 1,
                        'dgld_address/' => 1,
                        'ldo_withdrawal/' => 1,
                        'ldo_address/' => 1,
                        'travel_rule/contacts/' => 1,
                        'earn/subscribe/' => 1,
                        'earn/subscriptions/setting/' => 1,
                        'earn/unsubscribe' => 1,
                        'wecan_withdrawal/' => 1,
                        'wecan_address/' => 1,
                        'trac_withdrawal/' => 1,
                        'trac_address/' => 1,
                        'eurcv_withdrawal/' => 1,
                        'eurcv_address/' => 1,
                        'pyusd_withdrawal/' => 1,
                        'pyusd_address/' => 1,
                        'lmwr_withdrawal/' => 1,
                        'lmwr_address/' => 1,
                        'pepe_withdrawal/' => 1,
                        'pepe_address/' => 1,
                        'blur_withdrawal/' => 1,
                        'blur_address/' => 1,
                        'vext_withdrawal/' => 1,
                        'vext_address/' => 1,
                        'cspr_withdrawal/' => 1,
                        'cspr_address/' => 1,
                        'vchf_withdrawal/' => 1,
                        'vchf_address/' => 1,
                        'veur_withdrawal/' => 1,
                        'veur_address/' => 1,
                        'truf_withdrawal/' => 1,
                        'truf_address/' => 1,
                        'wif_withdrawal/' => 1,
                        'wif_address/' => 1,
                        'smt_withdrawal/' => 1,
                        'smt_address/' => 1,
                        'sui_withdrawal/' => 1,
                        'sui_address/' => 1,
                        'jup_withdrawal/' => 1,
                        'jup_address/' => 1,
                        'ondo_withdrawal/' => 1,
                        'ondo_address/' => 1,
                        'boba_withdrawal/' => 1,
                        'boba_address/' => 1,
                        'pyth_withdrawal/' => 1,
                        'pyth_address/' => 1,
                    ),
                ),
            ),
            'fees' => array(
                'trading' => array(
                    'tierBased' => true,
                    'percentage' => true,
                    'taker' => $this->parse_number('0.004'),
                    'maker' => $this->parse_number('0.004'),
                    'tiers' => array(
                        'taker' => array(
                            array( $this->parse_number('0'), $this->parse_number('0.004') ),
                            array( $this->parse_number('10000'), $this->parse_number('0.003') ),
                            array( $this->parse_number('100000'), $this->parse_number('0.002') ),
                            array( $this->parse_number('500000'), $this->parse_number('0.0018') ),
                            array( $this->parse_number('1500000'), $this->parse_number('0.0016') ),
                            array( $this->parse_number('5000000'), $this->parse_number('0.0012') ),
                            array( $this->parse_number('20000000'), $this->parse_number('0.001') ),
                            array( $this->parse_number('50000000'), $this->parse_number('0.0008') ),
                            array( $this->parse_number('100000000'), $this->parse_number('0.0006') ),
                            array( $this->parse_number('250000000'), $this->parse_number('0.0005') ),
                            array( $this->parse_number('1000000000'), $this->parse_number('0.0003') ),
                        ),
                        'maker' => array(
                            array( $this->parse_number('0'), $this->parse_number('0.003') ),
                            array( $this->parse_number('10000'), $this->parse_number('0.002') ),
                            array( $this->parse_number('100000'), $this->parse_number('0.001') ),
                            array( $this->parse_number('500000'), $this->parse_number('0.0008') ),
                            array( $this->parse_number('1500000'), $this->parse_number('0.0006') ),
                            array( $this->parse_number('5000000'), $this->parse_number('0.0003') ),
                            array( $this->parse_number('20000000'), $this->parse_number('0.002') ),
                            array( $this->parse_number('50000000'), $this->parse_number('0.0001') ),
                            array( $this->parse_number('100000000'), $this->parse_number('0') ),
                            array( $this->parse_number('250000000'), $this->parse_number('0') ),
                            array( $this->parse_number('1000000000'), $this->parse_number('0') ),
                        ),
                    ),
                ),
                'funding' => array(
                    'tierBased' => false,
                    'percentage' => false,
                    'withdraw' => array(),
                    'deposit' => array(
                        'BTC' => 0,
                        'BCH' => 0,
                        'LTC' => 0,
                        'ETH' => 0,
                        'XRP' => 0,
                        'XLM' => 0,
                        'PAX' => 0,
                        'USD' => 7.5,
                        'EUR' => 0,
                    ),
                ),
            ),
            'precisionMode' => TICK_SIZE,
            'commonCurrencies' => array(
                'UST' => 'USTC',
            ),
            // exchange-specific options
            'options' => array(
                'networksById' => array(
                    'bitcoin-cash' => 'BCH',
                    'bitcoin' => 'BTC',
                    'ethereum' => 'ERC20',
                    'litecoin' => 'LTC',
                    'stellar' => 'XLM',
                    'xrpl' => 'XRP',
                    'tron' => 'TRC20',
                    'algorand' => 'ALGO',
                    'flare' => 'FLR',
                    'hedera' => 'HBAR',
                    'cardana' => 'ADA',
                    'songbird' => 'FLR',
                    'avalanche-c-chain' => 'AVAX',
                    'solana' => 'SOL',
                    'polkadot' => 'DOT',
                    'near' => 'NEAR',
                    'doge' => 'DOGE',
                    'sui' => 'SUI',
                    'casper' => 'CSRP',
                ),
            ),
            'exceptions' => array(
                'exact' => array(
                    'No permission found' => '\\ccxt\\PermissionDenied',
                    'API key not found' => '\\ccxt\\AuthenticationError',
                    'IP address not allowed' => '\\ccxt\\PermissionDenied',
                    'Invalid nonce' => '\\ccxt\\InvalidNonce',
                    'Invalid signature' => '\\ccxt\\AuthenticationError',
                    'Authentication failed' => '\\ccxt\\AuthenticationError',
                    'Missing key, signature and nonce parameters' => '\\ccxt\\AuthenticationError',
                    'Wrong API key format' => '\\ccxt\\AuthenticationError',
                    'Your account is frozen' => '\\ccxt\\PermissionDenied',
                    'Please update your profile with your FATCA information, before using API.' => '\\ccxt\\PermissionDenied',
                    'Order not found.' => '\\ccxt\\OrderNotFound',
                    'Price is more than 20% below market price.' => '\\ccxt\\InvalidOrder',
                    "Bitstamp.net is under scheduled maintenance. We'll be back soon." => '\\ccxt\\OnMaintenance', // array( "error" => "Bitstamp.net is under scheduled maintenance. We'll be back soon." )
                    'Order could not be placed.' => '\\ccxt\\ExchangeNotAvailable', // Order could not be placed (perhaps due to internal error or trade halt). Please retry placing order.
                    'Invalid offset.' => '\\ccxt\\BadRequest',
                    'Trading is currently unavailable for your account.' => '\\ccxt\\AccountSuspended', // array("status" => "error", "reason" => array("__all__" => ["Trading is currently unavailable for your account."]), "response_code" => "403.004")
                ),
                'broad' => array(
                    'Minimum order size is' => '\\ccxt\\InvalidOrder', // Minimum order size is 5.0 EUR.
                    'Check your account balance for details.' => '\\ccxt\\InsufficientFunds', // You have only 0.00100000 BTC available. Check your account balance for details.
                    'Ensure this value has at least' => '\\ccxt\\InvalidAddress', // Ensure this value has at least 25 characters (it has 4).
                    'Ensure that there are no more than' => '\\ccxt\\InvalidOrder', // array("status" => "error", "reason" => array("amount" => ["Ensure that there are no more than 0 decimal places."], "__all__" => [""]))
                ),
            ),
            'features' => array(
                'spot' => array(
                    'sandbox' => false,
                    'createOrder' => array(
                        'marginMode' => false,
                        'triggerPrice' => false,
                        'triggerPriceType' => null,
                        'triggerDirection' => false,
                        'stopLossPrice' => false,
                        'takeProfitPrice' => false,
                        'attachedStopLossTakeProfit' => null,
                        'timeInForce' => array(
                            'IOC' => true,
                            'FOK' => true,
                            'PO' => true,
                            'GTD' => true,
                        ),
                        'hedged' => false,
                        'trailing' => false,
                        'leverage' => false,
                        'marketBuyByCost' => false,
                        'marketBuyRequiresPrice' => false,
                        'selfTradePrevention' => false,
                        'iceberg' => false,
                    ),
                    'createOrders' => null,
                    'fetchMyTrades' => array(
                        'marginMode' => false,
                        'limit' => 1000,
                        'daysBack' => null,
                        'untilDays' => 30,
                        'symbolRequired' => false,
                    ),
                    '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,
                    'fetchOHLCV' => array(
                        'limit' => 1000,
                    ),
                ),
                'swap' => array(
                    'linear' => null,
                    'inverse' => null,
                ),
                'future' => array(
                    'linear' => null,
                    'inverse' => null,
                ),
            ),
        ));
    }

    public function fetch_markets($params = array ()): array {
        /**
         * retrieves data on all markets for bitstamp
         *
         * @see https://www.bitstamp.net/api/#tag/Market-info/operation/GetTradingPairsInfo
         *
         * @param {array} [$params] extra parameters specific to the exchange API endpoint
         * @return {array[]} an array of objects representing $market data
         */
        $response = $this->fetch_markets_from_cache($params);
        //
        //     array(
        //         {
        //             "trading" => "Enabled",
        //             "base_decimals" => 8,
        //             "url_symbol" => "btcusd",
        //             "name" => "BTC/USD",
        //             "instant_and_market_orders" => "Enabled",
        //             "minimum_order" => "20.0 USD",
        //             "counter_decimals" => 2,
        //             "description" => "Bitcoin / U.S. dollar"
        //         }
        //     )
        //
        $result = array();
        for ($i = 0; $i < count($response); $i++) {
            $market = $response[$i];
            $name = $this->safe_string($market, 'name');
            list($base, $quote) = explode('/', $name);
            $baseId = strtolower($base);
            $quoteId = strtolower($quote);
            $base = $this->safe_currency_code($base);
            $quote = $this->safe_currency_code($quote);
            $minimumOrder = $this->safe_string($market, 'minimum_order');
            $parts = explode(' ', $minimumOrder);
            $status = $this->safe_string($market, 'trading');
            $result[] = array(
                'id' => $this->safe_string($market, 'url_symbol'),
                'marketId' => $baseId . '_' . $quoteId,
                'symbol' => $base . '/' . $quote,
                'base' => $base,
                'quote' => $quote,
                'settle' => null,
                'baseId' => $baseId,
                'quoteId' => $quoteId,
                'settleId' => null,
                'type' => 'spot',
                'spot' => true,
                'margin' => false,
                'future' => false,
                'swap' => false,
                'option' => false,
                'active' => ($status === 'Enabled'),
                'contract' => false,
                'linear' => null,
                'inverse' => null,
                'contractSize' => null,
                'expiry' => null,
                'expiryDatetime' => null,
                'strike' => null,
                'optionType' => null,
                'precision' => array(
                    'amount' => $this->parse_number($this->parse_precision($this->safe_string($market, 'base_decimals'))),
                    'price' => $this->parse_number($this->parse_precision($this->safe_string($market, 'counter_decimals'))),
                ),
                'limits' => array(
                    'leverage' => array(
                        'min' => null,
                        'max' => null,
                    ),
                    'amount' => array(
                        'min' => null,
                        'max' => null,
                    ),
                    'price' => array(
                        'min' => null,
                        'max' => null,
                    ),
                    'cost' => array(
                        'min' => $this->safe_number($parts, 0),
                        'max' => null,
                    ),
                ),
                'created' => null,
                'info' => $market,
            );
        }
        return $result;
    }

    public function construct_currency_object($id, $code, $name, $precision, $minCost, $originalPayload) {
        $currencyType = 'crypto';
        $description = $this->describe();
        if ($this->is_fiat($code)) {
            $currencyType = 'fiat';
        }
        $tickSize = $this->parse_number($this->parse_precision($this->number_to_string($precision)));
        return array(
            'id' => $id,
            'code' => $code,
            'info' => $originalPayload, // the original payload
            'type' => $currencyType,
            'name' => $name,
            'active' => true,
            'deposit' => null,
            'withdraw' => null,
            'fee' => $this->safe_number($description['fees']['funding']['withdraw'], $code),
            'precision' => $tickSize,
            'limits' => array(
                'amount' => array(
                    'min' => $tickSize,
                    'max' => null,
                ),
                'price' => array(
                    'min' => $tickSize,
                    'max' => null,
                ),
                'cost' => array(
                    'min' => $minCost,
                    'max' => null,
                ),
                'withdraw' => array(
                    'min' => null,
                    'max' => null,
                ),
            ),
            'networks' => array(),
        );
    }

    public function fetch_markets_from_cache($params = array ()) {
        // this method is $now redundant
        // currencies are $now fetched before markets
        $options = $this->safe_value($this->options, 'fetchMarkets', array());
        $timestamp = $this->safe_integer($options, 'timestamp');
        $expires = $this->safe_integer($options, 'expires', 1000);
        $now = $this->milliseconds();
        if (($timestamp === null) || (($now - $timestamp) > $expires)) {
            $response = $this->publicGetTradingPairsInfo ($params);
            $this->options['fetchMarkets'] = $this->extend($options, array(
                'response' => $response,
                'timestamp' => $now,
            ));
        }
        return $this->safe_value($this->options['fetchMarkets'], 'response');
    }

    public function fetch_currencies($params = array ()): ?array {
        /**
         * fetches all available currencies on an exchange
         *
         * @see https://www.bitstamp.net/api/#tag/Market-info/operation/GetTradingPairsInfo
         *
         * @param {array} [$params] extra parameters specific to the exchange API endpoint
         * @return {array} an associative dictionary of currencies
         */
        $response = $this->fetch_markets_from_cache($params);
        //
        //     array(
        //         array(
        //             "trading" => "Enabled",
        //             "base_decimals" => 8,
        //             "url_symbol" => "btcusd",
        //             "name" => "BTC/USD",
        //             "instant_and_market_orders" => "Enabled",
        //             "minimum_order" => "20.0 USD",
        //             "counter_decimals" => 2,
        //             "description" => "Bitcoin / U.S. dollar"
        //         ),
        //     )
        //
        $result = array();
        for ($i = 0; $i < count($response); $i++) {
            $market = $response[$i];
            $name = $this->safe_string($market, 'name');
            list($base, $quote) = explode('/', $name);
            $baseId = strtolower($base);
            $quoteId = strtolower($quote);
            $base = $this->safe_currency_code($base);
            $quote = $this->safe_currency_code($quote);
            $description = $this->safe_string($market, 'description');
            list($baseDescription, $quoteDescription) = explode(' / ', $description);
            $minimumOrder = $this->safe_string($market, 'minimum_order');
            $parts = explode(' ', $minimumOrder);
            $cost = $parts[0];
            if (!(is_array($result) && array_key_exists($base, $result))) {
                $baseDecimals = $this->safe_integer($market, 'base_decimals');
                $result[$base] = $this->construct_currency_object($baseId, $base, $baseDescription, $baseDecimals, null, $market);
            }
            if (!(is_array($result) && array_key_exists($quote, $result))) {
                $counterDecimals = $this->safe_integer($market, 'counter_decimals');
                $result[$quote] = $this->construct_currency_object($quoteId, $quote, $quoteDescription, $counterDecimals, $this->parse_number($cost), $market);
            }
        }
        return $result;
    }

    public function fetch_order_book(string $symbol, ?int $limit = null, $params = array ()): array {
        /**
         * fetches information on open orders with bid (buy) and ask (sell) prices, volumes and other data
         *
         * @see https://www.bitstamp.net/api/#tag/Order-book/operation/GetOrderBook
         *
         * @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
         */
        $this->load_markets();
        $market = $this->market($symbol);
        $request = array(
            'pair' => $market['id'],
        );
        $response = $this->publicGetOrderBookPair ($this->extend($request, $params));
        //
        //     {
        //         "timestamp" => "1583652948",
        //         "microtimestamp" => "1583652948955826",
        //         "bids" => array(
        //             array( "8750.00", "1.33685271" ),
        //             array( "8749.39", "0.07700000" ),
        //             array( "8746.98", "0.07400000" ),
        //         )
        //         "asks" => array(
        //             array( "8754.10", "1.51995636" ),
        //             array( "8754.71", "1.40000000" ),
        //             array( "8754.72", "2.50000000" ),
        //         )
        //     }
        //
        $microtimestamp = $this->safe_integer($response, 'microtimestamp');
        $timestamp = $this->parse_to_int($microtimestamp / 1000);
        $orderbook = $this->parse_order_book($response, $market['symbol'], $timestamp);
        $orderbook['nonce'] = $microtimestamp;
        return $orderbook;
    }

    public function parse_ticker(array $ticker, ?array $market = null): array {
        //
        // {
        //     "timestamp" => "1686068944",
        //     "high" => "26252",
        //     "last" => "26216",
        //     "bid" => "26208",
        //     "vwap" => "25681",
        //     "volume" => "3563.13819902",
        //     "low" => "25350",
        //     "ask" => "26211",
        //     "open" => "25730",
        //     "open_24" => "25895",
        //     "percent_change_24" => "1.24",
        //     "pair" => "BTC/USD"
        // }
        //
        $marketId = $this->safe_string($ticker, 'pair');
        $symbol = $this->safe_symbol($marketId, $market, null);
        $timestamp = $this->safe_timestamp($ticker, 'timestamp');
        $vwap = $this->safe_string($ticker, 'vwap');
        $baseVolume = $this->safe_string($ticker, 'volume');
        $quoteVolume = Precise::string_mul($baseVolume, $vwap);
        $last = $this->safe_string($ticker, 'last');
        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' => $vwap,
            'open' => $this->safe_string($ticker, 'open'),
            'close' => $last,
            'last' => $last,
            'previousClose' => null,
            'change' => null,
            'percentage' => null,
            'average' => null,
            'baseVolume' => $baseVolume,
            'quoteVolume' => $quoteVolume,
            'info' => $ticker,
        ), $market);
    }

    public function fetch_ticker(string $symbol, $params = array ()): array {
        /**
         * fetches a price $ticker, a statistical calculation with the information calculated over the past 24 hours for a specific $market
         *
         * @see https://www.bitstamp.net/api/#tag/Tickers/operation/GetMarketTicker
         *
         * @param {string} $symbol unified $symbol of the $market to fetch the $ticker for
         * @param {array} [$params] extra parameters specific to the exchange API endpoint
         * @return {array} a ~@link https://docs.ccxt.com/#/?id=$ticker-structure $ticker structure~
         */
        $this->load_markets();
        $market = $this->market($symbol);
        $request = array(
            'pair' => $market['id'],
        );
        $ticker = $this->publicGetTickerPair ($this->extend($request, $params));
        //
        // {
        //     "timestamp" => "1686068944",
        //     "high" => "26252",
        //     "last" => "26216",
        //     "bid" => "26208",
        //     "vwap" => "25681",
        //     "volume" => "3563.13819902",
        //     "low" => "25350",
        //     "ask" => "26211",
        //     "open" => "25730",
        //     "open_24" => "25895",
        //     "percent_change_24" => "1.24"
        // }
        //
        return $this->parse_ticker($ticker, $market);
    }

    public function fetch_tickers(?array $symbols = null, $params = array ()): array {
        /**
         * fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market
         *
         * @see https://www.bitstamp.net/api/#tag/Tickers/operation/GetCurrencyPairTickers
         *
         * @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~
         */
        $this->load_markets();
        $response = $this->publicGetTicker ($params);
        //
        // {
        //     "timestamp" => "1686068944",
        //     "high" => "26252",
        //     "last" => "26216",
        //     "bid" => "26208",
        //     "vwap" => "25681",
        //     "volume" => "3563.13819902",
        //     "low" => "25350",
        //     "ask" => "26211",
        //     "open" => "25730",
        //     "open_24" => "25895",
        //     "percent_change_24" => "1.24",
        //     "pair" => "BTC/USD"
        // }
        //
        return $this->parse_tickers($response, $symbols);
    }

    public function get_currency_id_from_transaction($transaction) {
        //
        //     {
        //         "fee" => "0.00000000",
        //         "btc_usd" => "0.00",
        //         "datetime" => XXX,
        //         "usd" => 0.0,
        //         "btc" => 0.0,
        //         "eth" => "0.05000000",
        //         "type" => "0",
        //         "id" => XXX,
        //         "eur" => 0.0
        //     }
        //
        $currencyId = $this->safe_string_lower($transaction, 'currency');
        if ($currencyId !== null) {
            return $currencyId;
        }
        $transaction = $this->omit($transaction, array(
            'fee',
            'price',
            'datetime',
            'type',
            'status',
            'id',
        ));
        $ids = is_array($transaction) ? array_keys($transaction) : array();
        for ($i = 0; $i < count($ids); $i++) {
            $id = $ids[$i];
            if (mb_strpos($id, '_') === false) {
                $value = $this->safe_integer($transaction, $id);
                if (($value !== null) && ($value !== 0)) {
                    return $id;
                }
            }
        }
        return null;
    }

    public function get_market_from_trade($trade) {
        $trade = $this->omit($trade, array(
            'fee',
            'price',
            'datetime',
            'tid',
            'type',
            'order_id',
            'side',
        ));
        $currencyIds = is_array($trade) ? array_keys($trade) : array();
        $numCurrencyIds = count($currencyIds);
        if ($numCurrencyIds > 2) {
            throw new ExchangeError($this->id . ' getMarketFromTrade() too many keys => ' . $this->json($currencyIds) . ' in the $trade => ' . $this->json($trade));
        }
        if ($numCurrencyIds === 2) {
            $marketId = $currencyIds[0] . $currencyIds[1];
            if (is_array($this->markets_by_id) && array_key_exists($marketId, $this->markets_by_id)) {
                return $this->safe_market($marketId);
            }
            $marketId = $currencyIds[1] . $currencyIds[0];
            if (is_array($this->markets_by_id) && array_key_exists($marketId, $this->markets_by_id)) {
                return $this->safe_market($marketId);
            }
        }
        return null;
    }

    public function parse_trade(array $trade, ?array $market = null): array {
        //
        // fetchTrades (public)
        //
        //      {
        //          "date" => "1637845199",
        //          "tid" => "209895701",
        //          "amount" => "0.00500000",
        //          "type" => "0",             // Transaction $type => 0 - buy; 1 - sell
        //          "price" => "4451.25"
        //      }
        //
        // fetchMyTrades, trades returned within fetchOrder (private)
        //
        //      {
        //          "fee" => "0.11128",
        //          "eth_usdt" =>  4451.25,
        //          "datetime" => "2021-11-25 12:59:59.322000",
        //          "usdt" => "-22.26",
        //          "order_id" =>  1429545880227846,
        //          "usd" =>  0,
        //          "btc" =>  0,
        //          "eth" => "0.00500000",
        //          "type" => "2",                    // Transaction $type => 0 - deposit; 1 - withdrawal; 2 - $market $trade; 14 - sub account transfer; 25 - credited with staked assets; 26 - sent assets to staking; 27 - staking reward; 32 - referral reward; 35 - inter account transfer.
        //          "id" =>  209895701,
        //          "eur" =>  0
        //      }
        //
        // from fetchOrder (private)
        //
        //      {
        //          "fee" => "0.11128",
        //          "price" => "4451.25000000",
        //          "datetime" => "2021-11-25 12:59:59.322000",
        //          "usdt" => "22.25625000",
        //          "tid" => 209895701,
        //          "eth" => "0.00500000",
        //          "type" => 2                       // Transaction $type => 0 - deposit; 1 - withdrawal; 2 - $market $trade
        //      }
        //
        $id = $this->safe_string_2($trade, 'id', 'tid');
        $symbol = null;
        $side = null;
        $priceString = $this->safe_string($trade, 'price');
        $amountString = $this->safe_string($trade, 'amount');
        $orderId = $this->safe_string($trade, 'order_id');
        $type = null;
        $costString = $this->safe_string($trade, 'cost');
        $rawMarketId = null;
        if ($market === null) {
            $keys = is_array($trade) ? array_keys($trade) : array();
            for ($i = 0; $i < count($keys); $i++) {
                $currentKey = $keys[$i];
                if ($currentKey !== 'order_id' && mb_strpos($currentKey, '_') !== false) {
                    $rawMarketId = $currentKey;
                    $market = $this->safe_market($rawMarketId, $market, '_');
                }
            }
        }
        // if the $market is still not defined
        // try to deduce it from used $keys
        if ($market === null) {
            $market = $this->get_market_from_trade($trade);
        }
        $feeCostString = $this->safe_string($trade, 'fee');
        $feeCurrency = $market['quote'];
        $priceId = ($rawMarketId !== null) ? $rawMarketId : $market['marketId'];
        $priceString = $this->safe_string($trade, $priceId, $priceString);
        $amountString = $this->safe_string($trade, $market['baseId'], $amountString);
        $costString = $this->safe_string($trade, $market['quoteId'], $costString);
        $symbol = $market['symbol'];
        $datetimeString = $this->safe_string_2($trade, 'date', 'datetime');
        $timestamp = null;
        if ($datetimeString !== null) {
            if (mb_strpos($datetimeString, ' ') !== false) {
                // iso8601
                $timestamp = $this->parse8601($datetimeString);
            } else {
                // string unix epoch in seconds
                $timestamp = intval($datetimeString);
                $timestamp = $timestamp * 1000;
            }
        }
        // if it is a private $trade
        if (is_array($trade) && array_key_exists('id', $trade)) {
            if ($amountString !== null) {
                $isAmountNeg = Precise::string_lt($amountString, '0');
                if ($isAmountNeg) {
                    $side = 'sell';
                    $amountString = Precise::string_neg($amountString);
                } else {
                    $side = 'buy';
                }
            }
        } else {
            $side = $this->safe_string($trade, 'type');
            if ($side === '1') {
                $side = 'sell';
            } elseif ($side === '0') {
                $side = 'buy';
            } else {
                $side = null;
            }
        }
        if ($costString !== null) {
            $costString = Precise::string_abs($costString);
        }
        $fee = null;
        if ($feeCostString !== null) {
            $fee = array(
                'cost' => $feeCostString,
                'currency' => $feeCurrency,
            );
        }
        return $this->safe_trade(array(
            'id' => $id,
            'info' => $trade,
            'timestamp' => $timestamp,
            'datetime' => $this->iso8601($timestamp),
            'symbol' => $symbol,
            'order' => $orderId,
            'type' => $type,
            'side' => $side,
            'takerOrMaker' => null,
            'price' => $priceString,
            'amount' => $amountString,
            'cost' => $costString,
            'fee' => $fee,
        ), $market);
    }

    public function fetch_trades(string $symbol, ?int $since = null, ?int $limit = null, $params = array ()): array {
        /**
         * get the list of most recent trades for a particular $symbol
         *
         * @see https://www.bitstamp.net/api/#tag/Transactions-public/operation/GetTransactions
         *
         * @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~
         */
        $this->load_markets();
        $market = $this->market($symbol);
        $request = array(
            'pair' => $market['id'],
            'time' => 'hour',
        );
        $response = $this->publicGetTransactionsPair ($this->extend($request, $params));
        //
        //     array(
        //         array(
        //             "date" => "1551814435",
        //             "tid" => "83581898",
        //             "price" => "0.03532850",
        //             "type" => "1",
        //             "amount" => "0.85945907"
        //         ),
        //         array(
        //             "date" => "1551814434",
        //             "tid" => "83581896",
        //             "price" => "0.03532851",
        //             "type" => "1",
        //             "amount" => "11.34130961"
        //         ),
        //     )
        //
        return $this->parse_trades($response, $market, $since, $limit);
    }

    public function parse_ohlcv($ohlcv, ?array $market = null): array {
        //
        //     {
        //         "high" => "9064.77",
        //         "timestamp" => "1593961440",
        //         "volume" => "18.49436608",
        //         "low" => "9040.87",
        //         "close" => "9064.77",
        //         "open" => "9040.87"
        //     }
        //
        return array(
            $this->safe_timestamp($ohlcv, 'timestamp'),
            $this->safe_number($ohlcv, 'open'),
            $this->safe_number($ohlcv, 'high'),
            $this->safe_number($ohlcv, 'low'),
            $this->safe_number($ohlcv, 'close'),
            $this->safe_number($ohlcv, 'volume'),
        );
    }

    public function fetch_ohlcv(string $symbol, $timeframe = '1m', ?int $since = null, ?int $limit = null, $params = array ()): array {
        /**
         * fetches historical candlestick $data containing the open, high, low, and close price, and the volume of a $market
         *
         * @see https://www.bitstamp.net/api/#tag/Market-info/operation/GetOHLCData
         *
         * @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
         */
        $this->load_markets();
        $market = $this->market($symbol);
        $request = array(
            'pair' => $market['id'],
            'step' => $this->safe_string($this->timeframes, $timeframe, $timeframe),
        );
        $duration = $this->parse_timeframe($timeframe);
        if ($limit === null) {
            if ($since === null) {
                $request['limit'] = 1000; // we need to specify an allowed amount of `$limit` if no `$since` is set and there is no default $limit by exchange
            } else {
                $limit = 1000;
                $start = $this->parse_to_int($since / 1000);
                $request['start'] = $start;
                $request['end'] = $this->sum($start, $duration * ($limit - 1));
                $request['limit'] = $limit;
            }
        } else {
            if ($since !== null) {
                $start = $this->parse_to_int($since / 1000);
                $request['start'] = $start;
                $request['end'] = $this->sum($start, $duration * ($limit - 1));
            }
            $request['limit'] = min ($limit, 1000); // min 1, max 1000
        }
        $response = $this->publicGetOhlcPair ($this->extend($request, $params));
        //
        //     {
        //         "data" => {
        //             "pair" => "BTC/USD",
        //             "ohlc" => array(
        //                 array("high" => "9064.77", "timestamp" => "1593961440", "volume" => "18.49436608", "low" => "9040.87", "close" => "9064.77", "open" => "9040.87"),
        //                 array("high" => "9071.59", "timestamp" => "1593961500", "volume" => "3.48631711", "low" => "9058.76", "close" => "9061.07", "open" => "9064.66"),
        //                 array("high" => "9067.33", "timestamp" => "1593961560", "volume" => "0.04142833", "low" => "9061.94", "close" => "9061.94", "open" => "9067.33"),
        //             ),
        //         }
        //     }
        //
        $data = $this->safe_value($response, 'data', array());
        $ohlc = $this->safe_list($data, 'ohlc', array());
        return $this->parse_ohlcvs($ohlc, $market, $timeframe, $since, $limit);
    }

    public function parse_balance($response): array {
        $result = array(
            'info' => $response,
            'timestamp' => null,
            'datetime' => null,
        );
        if ($response === null) {
            $response = array();
        }
        for ($i = 0; $i < count($response); $i++) {
            $currencyBalance = $response[$i];
            $currencyId = $this->safe_string($currencyBalance, 'currency');
            $currencyCode = $this->safe_currency_code($currencyId);
            $account = $this->account();
            $account['free'] = $this->safe_string($currencyBalance, 'available');
            $account['used'] = $this->safe_string($currencyBalance, 'reserved');
            $account['total'] = $this->safe_string($currencyBalance, 'total');
            $result[$currencyCode] = $account;
        }
        return $this->safe_balance($result);
    }

    public function fetch_balance($params = array ()): array {
        /**
         * query for balance and get the amount of funds available for trading or funds locked in orders
         *
         * @see https://www.bitstamp.net/api/#tag/Account-balances/operation/GetAccountBalances
         *
         * @param {array} [$params] extra parameters specific to the exchange API endpoint
         * @return {array} a ~@link https://docs.ccxt.com/#/?id=balance-structure balance structure~
         */
        $this->load_markets();
        $response = $this->privatePostAccountBalances ($params);
        //
        //     array(
        //         array(
        //             "currency" => "usdt",
        //             "total" => "7.00000",
        //             "available" => "7.00000",
        //             "reserved" => "0.00000"
        //         ),
        //         ...
        //     )
        //
        return $this->parse_balance($response);
    }

    public function fetch_trading_fee(string $symbol, $params = array ()): array {
        /**
         * fetch the trading fees for a $market
         *
         * @see https://www.bitstamp.net/api/#tag/Fees/operation/GetTradingFeesForCurrency
         *
         * @param {string} $symbol unified $market $symbol
         * @param {array} [$params] extra parameters specific to the exchange API endpoint
         * @return {array} a ~@link https://docs.ccxt.com/#/?id=fee-structure fee structure~
         */
        $this->load_markets();
        $market = $this->market($symbol);
        $request = array(
            'market_symbol' => $market['id'],
        );
        $response = $this->privatePostFeesTrading ($this->extend($request, $params));
        //
        //     array(
        //         {
        //             "currency_pair" => "btcusd",
        //             "fees":
        //                 array(
        //                     "maker" => "0.15000",
        //                     "taker" => "0.16000"
        //                 ),
        //             "market" => "btcusd"
        //         }
        //         ...
        //     )
        //
        $tradingFeesByMarketId = $this->index_by($response, 'currency_pair');
        $tradingFee = $this->safe_dict($tradingFeesByMarketId, $market['id']);
        return $this->parse_trading_fee($tradingFee, $market);
    }

    public function parse_trading_fee(array $fee, ?array $market = null): array {
        $marketId = $this->safe_string($fee, 'market');
        $fees = $this->safe_dict($fee, 'fees', array());
        return array(
            'info' => $fee,
            'symbol' => $this->safe_symbol($marketId, $market),
            'maker' => $this->safe_number($fees, 'maker'),
            'taker' => $this->safe_number($fees, 'taker'),
            'percentage' => null,
            'tierBased' => null,
        );
    }

    public function parse_trading_fees($fees) {
        $result = array( 'info' => $fees );
        for ($i = 0; $i < count($fees); $i++) {
            $fee = $this->parse_trading_fee($fees[$i]);
            $symbol = $fee['symbol'];
            $result[$symbol] = $fee;
        }
        return $result;
    }

    public function fetch_trading_fees($params = array ()): array {
        /**
         * fetch the trading fees for multiple markets
         *
         * @see https://www.bitstamp.net/api/#tag/Fees/operation/GetAllTradingFees
         *
         * @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
         */
        $this->load_markets();
        $response = $this->privatePostFeesTrading ($params);
        //
        //     array(
        //         {
        //             "currency_pair" => "btcusd",
        //             "fees":
        //                 array(
        //                     "maker" => "0.15000",
        //                     "taker" => "0.16000"
        //                 ),
        //             "market" => "btcusd"
        //         }
        //         ...
        //     )
        //
        return $this->parse_trading_fees($response);
    }

    public function fetch_transaction_fees(?array $codes = null, $params = array ()) {
        /**
         * @deprecated
         * please use fetchDepositWithdrawFees instead
         *
         * @see https://www.bitstamp.net/api/#tag/Fees
         *
         * @param {string[]|null} $codes list of unified currency $codes
         * @param {array} [$params] extra parameters specific to the exchange API endpoint
         * @return {array[]} a list of ~@link https://docs.ccxt.com/#/?id=fee-structure fee structures~
         */
        $this->load_markets();
        $response = $this->privatePostFeesWithdrawal ($params);
        //
        //     array(
        //         {
        //             "currency" => "btc",
        //             "fee" => "0.00015000",
        //             "network" => "bitcoin"
        //         }
        //         ...
        //     )
        //
        return $this->parse_transaction_fees($response);
    }

    public function parse_transaction_fees($response, $codes = null) {
        $result = array();
        $currencies = $this->index_by($response, 'currency');
        $ids = is_array($currencies) ? array_keys($currencies) : array();
        for ($i = 0; $i < count($ids); $i++) {
            $id = $ids[$i];
            $fees = $this->safe_value($response, $i, array());
            $code = $this->safe_currency_code($id);
            if (($codes !== null) && !$this->in_array($code, $codes)) {
                continue;
            }
            $result[$code] = array(
                'withdraw_fee' => $this->safe_number($fees, 'fee'),
                'deposit' => array(),
                'info' => $this->safe_dict($currencies, $id),
            );
        }
        return $result;
    }

    public function fetch_deposit_withdraw_fees($codes = null, $params = array ()) {
        /**
         * fetch deposit and withdraw fees
         *
         * @see https://www.bitstamp.net/api/#tag/Fees/operation/GetAllWithdrawalFees
         *
         * @param {string[]|null} $codes list of unified currency $codes
         * @param {array} [$params] extra parameters specific to the exchange API endpoint
         * @return {array[]} a list of ~@link https://docs.ccxt.com/#/?id=fee-structure fee structures~
         */
        $this->load_markets();
        $response = $this->privatePostFeesWithdrawal ($params);
        //
        //     array(
        //         {
        //             "currency" => "btc",
        //             "fee" => "0.00015000",
        //             "network" => "bitcoin"
        //         }
        //         ...
        //     )
        //
        $responseByCurrencyId = $this->group_by($response, 'currency');
        return $this->parse_deposit_withdraw_fees($responseByCurrencyId, $codes);
    }

    public function parse_deposit_withdraw_fee($fee, $currency = null) {
        $result = $this->deposit_withdraw_fee($fee);
        for ($j = 0; $j < count($fee); $j++) {
            $networkEntry = $fee[$j];
            $networkId = $this->safe_string($networkEntry, 'network');
            $networkCode = $this->network_id_to_code($networkId);
            $withdrawFee = $this->safe_number($networkEntry, 'fee');
            $result['withdraw'] = array(
                'fee' => $withdrawFee,
                'percentage' => null,
            );
            $result['networks'][$networkCode] = array(
                'withdraw' => array(
                    'fee' => $withdrawFee,
                    'percentage' => null,
                ),
                'deposit' => array(
                    'fee' => null,
                    'percentage' => null,
                ),
            );
        }
        return $result;
    }

    public function create_order(string $symbol, string $type, string $side, float $amount, ?float $price = null, $params = array ()) {
        /**
         * create a trade $order
         *
         * @see https://www.bitstamp.net/api/#tag/Orders/operation/OpenInstantBuyOrder
         * @see https://www.bitstamp.net/api/#tag/Orders/operation/OpenMarketBuyOrder
         * @see https://www.bitstamp.net/api/#tag/Orders/operation/OpenLimitBuyOrder
         * @see https://www.bitstamp.net/api/#tag/Orders/operation/OpenInstantSellOrder
         * @see https://www.bitstamp.net/api/#tag/Orders/operation/OpenMarketSellOrder
         * @see https://www.bitstamp.net/api/#tag/Orders/operation/OpenLimitSellOrder
         *
         * @param {string} $symbol unified $symbol of the $market to create an $order in
         * @param {string} $type 'market' or 'limit'
         * @param {string} $side 'buy' or 'sell'
         * @param {float} $amount how much of currency you want to trade in units of base currency
         * @param {float} [$price] the $price at which the $order is to be fulfilled, in units of the quote currency, ignored in $market orders
         * @param {array} [$params] extra parameters specific to the exchange API endpoint
         * @return {array} an ~@link https://docs.ccxt.com/#/?id=$order-structure $order structure~
         */
        $this->load_markets();
        $market = $this->market($symbol);
        $request = array(
            'pair' => $market['id'],
            'amount' => $this->amount_to_precision($symbol, $amount),
        );
        $clientOrderId = $this->safe_string_2($params, 'client_order_id', 'clientOrderId');
        if ($clientOrderId !== null) {
            $request['client_order_id'] = $clientOrderId;
            $params = $this->omit($params, array( 'clientOrderId' ));
        }
        $response = null;
        $capitalizedSide = $this->capitalize($side);
        if ($type === 'market') {
            if ($capitalizedSide === 'Buy') {
                $response = $this->privatePostBuyMarketPair ($this->extend($request, $params));
            } else {
                $response = $this->privatePostSellMarketPair ($this->extend($request, $params));
            }
        } elseif ($type === 'instant') {
            if ($capitalizedSide === 'Buy') {
                $response = $this->privatePostBuyInstantPair ($this->extend($request, $params));
            } else {
                $response = $this->privatePostSellInstantPair ($this->extend($request, $params));
            }
        } else {
            $request['price'] = $this->price_to_precision($symbol, $price);
            if ($capitalizedSide === 'Buy') {
                $response = $this->privatePostBuyPair ($this->extend($request, $params));
            } else {
                $response = $this->privatePostSellPair ($this->extend($request, $params));
            }
        }
        $order = $this->parse_order($response, $market);
        $order['type'] = $type;
        return $order;
    }

    public function cancel_order(string $id, ?string $symbol = null, $params = array ()) {
        /**
         * cancels an open order
         *
         * @see https://www.bitstamp.net/api/#tag/Orders/operation/CancelOrder
         *
         * @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~
         */
        $this->load_markets();
        $request = array(
            'id' => $id,
        );
        $response = $this->privatePostCancelOrder ($this->extend($request, $params));
        //
        //    {
        //        "id" => 1453282316578816,
        //        "amount" => "0.02035278",
        //        "price" => "2100.45",
        //        "type" => 0,
        //        "market" => "BTC/USD"
        //    }
        //
        return $this->parse_order($response);
    }

    public function cancel_all_orders(?string $symbol = null, $params = array ()) {
        /**
         * cancel all open orders
         *
         * @see https://www.bitstamp.net/api/#tag/Orders/operation/CancelAllOrders
         * @see https://www.bitstamp.net/api/#tag/Orders/operation/CancelOrdersForMarket
         *
         * @param {string} $symbol unified $market $symbol, only orders in the $market of this $symbol are cancelled when $symbol is not null
         * @param {array} [$params] extra parameters specific to the exchange API endpoint
         * @return {array[]} a list of ~@link https://docs.ccxt.com/#/?id=order-structure order structures~
         */
        $this->load_markets();
        $market = null;
        $request = array();
        $response = null;
        if ($symbol !== null) {
            $market = $this->market($symbol);
            $request['pair'] = $market['id'];
            $response = $this->privatePostCancelAllOrdersPair ($this->extend($request, $params));
        } else {
            $response = $this->privatePostCancelAllOrders ($this->extend($request, $params));
        }
        //
        //    {
        //        "canceled" => array(
        //            {
        //                "id" => 1453282316578816,
        //                "amount" => "0.02035278",
        //                "price" => "2100.45",
        //                "type" => 0,
        //                "currency_pair" => "BTC/USD",
        //                "market" => "BTC/USD"
        //            }
        //        ),
        //        "success" => true
        //    }
        //
        $canceled = $this->safe_list($response, 'canceled');
        return $this->parse_orders($canceled);
    }

    public function parse_order_status(?string $status) {
        $statuses = array(
            'In Queue' => 'open',
            'Open' => 'open',
            'Finished' => 'closed',
            'Canceled' => 'canceled',
        );
        return $this->safe_string($statuses, $status, $status);
    }

    public function fetch_order_status(string $id, ?string $symbol = null, $params = array ()) {
        $this->load_markets();
        $clientOrderId = $this->safe_value_2($params, 'client_order_id', 'clientOrderId');
        $request = array();
        if ($clientOrderId !== null) {
            $request['client_order_id'] = $clientOrderId;
            $params = $this->omit($params, array( 'client_order_id', 'clientOrderId' ));
        } else {
            $request['id'] = $id;
        }
        $response = $this->privatePostOrderStatus ($this->extend($request, $params));
        return $this->parse_order_status($this->safe_string($response, 'status'));
    }

    public function fetch_order(string $id, ?string $symbol = null, $params = array ()) {
        /**
         * fetches information on an order made by the user
         *
         * @see https://www.bitstamp.net/api/#tag/Orders/operation/GetOrderStatus
         *
         * @param {string} $id the 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~
         */
        $this->load_markets();
        $market = null;
        if ($symbol !== null) {
            $market = $this->market($symbol);
        }
        $clientOrderId = $this->safe_value_2($params, 'client_order_id', 'clientOrderId');
        $request = array();
        if ($clientOrderId !== null) {
            $request['client_order_id'] = $clientOrderId;
            $params = $this->omit($params, array( 'client_order_id', 'clientOrderId' ));
        } else {
            $request['id'] = $id;
        }
        $response = $this->privatePostOrderStatus ($this->extend($request, $params));
        //
        //      {
        //          "status" => "Finished",
        //          "id" => 1429545880227846,
        //          "amount_remaining" => "0.00000000",
        //          "transactions" => array(
        //              {
        //                  "fee" => "0.11128",
        //                  "price" => "4451.25000000",
        //                  "datetime" => "2021-11-25 12:59:59.322000",
        //                  "usdt" => "22.25625000",
        //                  "tid" => 209895701,
        //                  "eth" => "0.00500000",
        //                  "type" => 2
        //              }
        //         )
        //     }
        //
        return $this->parse_order($response, $market);
    }

    public function fetch_my_trades(?string $symbol = null, ?int $since = null, ?int $limit = null, $params = array ()) {
        /**
         * fetch all trades made by the user
         *
         * @see https://www.bitstamp.net/api/#tag/Transactions-private/operation/GetUserTransactions
         * @see https://www.bitstamp.net/api/#tag/Transactions-private/operation/GetUserTransactionsForMarket
         *
         * @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~
         */
        $this->load_markets();
        $request = array();
        $method = 'privatePostUserTransactions';
        $market = null;
        if ($symbol !== null) {
            $market = $this->market($symbol);
            $request['pair'] = $market['id'];
            $method .= 'Pair';
        }
        if ($limit !== null) {
            $request['limit'] = $limit;
        }
        $response = $this->$method ($this->extend($request, $params));
        $result = $this->filter_by($response, 'type', '2');
        return $this->parse_trades($result, $market, $since, $limit);
    }

    public function fetch_deposits_withdrawals(?string $code = null, ?int $since = null, ?int $limit = null, $params = array ()): array {
        /**
         * fetch history of deposits and withdrawals
         *
         * @see https://www.bitstamp.net/api/#tag/Transactions-private/operation/GetUserTransactions
         *
         * @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~
         */
        $this->load_markets();
        $request = array();
        if ($limit !== null) {
            $request['limit'] = $limit;
        }
        $response = $this->privatePostUserTransactions ($this->extend($request, $params));
        //
        //     array(
        //         array(
        //             "fee" => "0.00000000",
        //             "btc_usd" => "0.00",
        //             "id" => 1234567894,
        //             "usd" => 0,
        //             "btc" => 0,
        //             "datetime" => "2018-09-08 09:00:31",
        //             "type" => "1",
        //             "xrp" => "-20.00000000",
        //             "eur" => 0,
        //         ),
        //         array(
        //             "fee" => "0.00000000",
        //             "btc_usd" => "0.00",
        //             "id" => 1134567891,
        //             "usd" => 0,
        //             "btc" => 0,
        //             "datetime" => "2018-09-07 18:47:52",
        //             "type" => "0",
        //             "xrp" => "20.00000000",
        //             "eur" => 0,
        //         ),
        //     )
        //
        $currency = null;
        if ($code !== null) {
            $currency = $this->currency($code);
        }
        $transactions = $this->filter_by_array($response, 'type', array( '0', '1' ), false);
        return $this->parse_transactions($transactions, $currency, $since, $limit);
    }

    public function fetch_withdrawals(?string $code = null, ?int $since = null, ?int $limit = null, $params = array ()): array {
        /**
         * fetch all withdrawals made from an account
         *
         * @see https://www.bitstamp.net/api/#tag/Withdrawals/operation/GetWithdrawalRequests
         *
         * @param {string} $code unified currency $code
         * @param {int} [$since] the earliest time in ms to fetch withdrawals for
         * @param {int} [$limit] the maximum number of withdrawals structures to retrieve
         * @param {array} [$params] extra parameters specific to the exchange API endpoint
         * @return {array[]} a list of ~@link https://docs.ccxt.com/#/?id=transaction-structure transaction structures~
         */
        $this->load_markets();
        $request = array();
        if ($since !== null) {
            $request['timedelta'] = $this->milliseconds() - $since;
        } else {
            $request['timedelta'] = 50000000; // use max bitstamp approved value
        }
        $response = $this->privatePostWithdrawalRequests ($this->extend($request, $params));
        //
        //     array(
        //         array(
        //             "status" => 2,
        //             "datetime" => "2018-10-17 10:58:13",
        //             "currency" => "BTC",
        //             "amount" => "0.29669259",
        //             "address" => "aaaaa",
        //             "type" => 1,
        //             "id" => 111111,
        //             "transaction_id" => "xxxx",
        //         ),
        //         array(
        //             "status" => 2,
        //             "datetime" => "2018-10-17 10:55:17",
        //             "currency" => "ETH",
        //             "amount" => "1.11010664",
        //             "address" => "aaaa",
        //             "type" => 16,
        //             "id" => 222222,
        //             "transaction_id" => "xxxxx",
        //         ),
        //     )
        //
        return $this->parse_transactions($response, null, $since, $limit);
    }

    public function parse_transaction(array $transaction, ?array $currency = null): array {
        //
        // fetchDepositsWithdrawals
        //
        //     {
        //         "fee" => "0.00000000",
        //         "btc_usd" => "0.00",
        //         "id" => 1234567894,
        //         "usd" => 0,
        //         "btc" => 0,
        //         "datetime" => "2018-09-08 09:00:31",
        //         "type" => "1",
        //         "xrp" => "-20.00000000",
        //         "eur" => 0,
        //     }
        //
        // fetchWithdrawals
        //
        //     {
        //         "status" => 2,
        //         "datetime" => "2018-10-17 10:58:13",
        //         "currency" => "BTC",
        //         "amount" => "0.29669259",
        //         "address" => "aaaaa",
        //         "type" => 1,
        //         "id" => 111111,
        //         "transaction_id" => "xxxx",
        //     }
        //
        //     {
        //         "id" => 3386432,
        //         "type" => 14,
        //         "amount" => "863.21332500",
        //         "status" => 2,
        //         "address" => "rE1sdh25BJQ3qFwngiTBwaq3zPGGYcrjp1?dt=1455",
        //         "currency" => "XRP",
        //         "datetime" => "2018-01-05 15:27:55",
        //         "transaction_id" => "001743B03B0C79BA166A064AC0142917B050347B4CB23BA2AB4B91B3C5608F4C"
        //     }
        //
        $timestamp = $this->parse8601($this->safe_string($transaction, 'datetime'));
        $currencyId = $this->get_currency_id_from_transaction($transaction);
        $code = $this->safe_currency_code($currencyId, $currency);
        $feeCost = $this->safe_string($transaction, 'fee');
        $feeCurrency = null;
        $amount = null;
        if (is_array($transaction) && array_key_exists('amount', $transaction)) {
            $amount = $this->safe_string($transaction, 'amount');
        } elseif ($currency !== null) {
            $amount = $this->safe_string($transaction, $currency['id'], $amount);
            $feeCurrency = $currency['code'];
        } elseif (($code !== null) && ($currencyId !== null)) {
            $amount = $this->safe_string($transaction, $currencyId, $amount);
            $feeCurrency = $code;
        }
        if ($amount !== null) {
            // withdrawals have a negative $amount
            $amount = Precise::string_abs($amount);
        }
        $status = 'ok';
        if (is_array($transaction) && array_key_exists('status', $transaction)) {
            $status = $this->parse_transaction_status($this->safe_string($transaction, 'status'));
        }
        $type = null;
        if (is_array($transaction) && array_key_exists('type', $transaction)) {
            // from fetchDepositsWithdrawals
            $rawType = $this->safe_string($transaction, 'type');
            if ($rawType === '0') {
                $type = 'deposit';
            } elseif ($rawType === '1') {
                $type = 'withdrawal';
            }
        } else {
            // from fetchWithdrawals
            $type = 'withdrawal';
        }
        $tag = null;
        $address = $this->safe_string($transaction, 'address');
        if ($address !== null) {
            // dt (destination $tag) is embedded into the $address field
            $addressParts = explode('?dt=', $address);
            $numParts = count($addressParts);
            if ($numParts > 1) {
                $address = $addressParts[0];
                $tag = $addressParts[1];
            }
        }
        $fee = array(
            'currency' => null,
            'cost' => null,
            'rate' => null,
        );
        if ($feeCost !== null) {
            $fee = array(
                'currency' => $feeCurrency,
                'cost' => $feeCost,
                'rate' => null,
            );
        }
        return array(
            'info' => $transaction,
            'id' => $this->safe_string($transaction, 'id'),
            'txid' => $this->safe_string($transaction, 'transaction_id'),
            'type' => $type,
            'currency' => $code,
            'network' => null,
            'amount' => $this->parse_number($amount),
            'status' => $status,
            'timestamp' => $timestamp,
            'datetime' => $this->iso8601($timestamp),
            'address' => $address,
            'addressFrom' => null,
            'addressTo' => $address,
            'tag' => $tag,
            'tagFrom' => null,
            'tagTo' => $tag,
            'updated' => null,
            'comment' => null,
            'internal' => null,
            'fee' => $fee,
        );
    }

    public function parse_transaction_status(?string $status) {
        //
        //   withdrawals:
        //   0 (open), 1 (in process), 2 (finished), 3 (canceled) or 4 (failed).
        //
        $statuses = array(
            '0' => 'pending', // Open
            '1' => 'pending', // In process
            '2' => 'ok', // Finished
            '3' => 'canceled', // Canceled
            '4' => 'failed', // Failed
        );
        return $this->safe_string($statuses, $status, $status);
    }

    public function parse_order(array $order, ?array $market = null): array {
        //
        //   from fetch $order:
        //     { $status => "Finished",
        //       "id" => 731693945,
        //       "client_order_id" => '',
        //       "transactions":
        //       array( { fee => "0.000019",
        //           "price" => "0.00015803",
        //           "datetime" => "2018-01-07 10:45:34.132551",
        //           "btc" => "0.0079015000000000",
        //           "tid" => 42777395,
        //           "type" => 2,
        //           "xrp" => "50.00000000" } ) }
        //
        //   partially filled $order:
        //     { "id" => 468646390,
        //       "client_order_id" => "",
        //       "status" => "Canceled",
        //       "transactions" => [array(
        //           "eth" => "0.23000000",
        //           "fee" => "0.09",
        //           "tid" => 25810126,
        //           "usd" => "69.8947000000000000",
        //           "type" => 2,
        //           "price" => "303.89000000",
        //           "datetime" => "2017-11-11 07:22:20.710567"
        //       )]}
        //
        //   from create $order response:
        //       {
        //           "price" => "0.00008012",
        //           "client_order_id" => '',
        //           "currency_pair" => "XRP/BTC",
        //           "datetime" => "2019-01-31 21:23:36",
        //           "amount" => "15.00000000",
        //           "type" => "0",
        //           "id" => "2814205012"
        //       }
        //
        // cancelOrder
        //
        //    {
        //        "id" => 1453282316578816,
        //        "amount" => "0.02035278",
        //        "price" => "2100.45",
        //        "type" => 0,
        //        "market" => "BTC/USD"
        //    }
        //
        $id = $this->safe_string($order, 'id');
        $clientOrderId = $this->safe_string($order, 'client_order_id');
        $side = $this->safe_string($order, 'type');
        if ($side !== null) {
            $side = ($side === '1') ? 'sell' : 'buy';
        }
        // there is no $timestamp from fetchOrder
        $timestamp = $this->parse8601($this->safe_string($order, 'datetime'));
        $marketId = $this->safe_string_lower($order, 'currency_pair');
        $symbol = $this->safe_symbol($marketId, $market, '/');
        $status = $this->parse_order_status($this->safe_string($order, 'status'));
        $amount = $this->safe_string($order, 'amount');
        $transactions = $this->safe_value($order, 'transactions', array());
        $price = $this->safe_string($order, 'price');
        return $this->safe_order(array(
            'id' => $id,
            'clientOrderId' => $clientOrderId,
            'datetime' => $this->iso8601($timestamp),
            'timestamp' => $timestamp,
            'lastTradeTimestamp' => null,
            'status' => $status,
            'symbol' => $symbol,
            'type' => null,
            'timeInForce' => null,
            'postOnly' => null,
            'side' => $side,
            'price' => $price,
            'triggerPrice' => null,
            'cost' => null,
            'amount' => $amount,
            'filled' => null,
            'remaining' => null,
            'trades' => $transactions,
            'fee' => null,
            'info' => $order,
            'average' => null,
        ), $market);
    }

    public function parse_ledger_entry_type($type) {
        $types = array(
            '0' => 'transaction',
            '1' => 'transaction',
            '2' => 'trade',
            '14' => 'transfer',
        );
        return $this->safe_string($types, $type, $type);
    }

    public function parse_ledger_entry(array $item, ?array $currency = null): array {
        //
        //     array(
        //         array(
        //             "fee" => "0.00000000",
        //             "btc_usd" => "0.00",
        //             "id" => 1234567894,
        //             "usd" => 0,
        //             "btc" => 0,
        //             "datetime" => "2018-09-08 09:00:31",
        //             "type" => "1",
        //             "xrp" => "-20.00000000",
        //             "eur" => 0,
        //         ),
        //         array(
        //             "fee" => "0.00000000",
        //             "btc_usd" => "0.00",
        //             "id" => 1134567891,
        //             "usd" => 0,
        //             "btc" => 0,
        //             "datetime" => "2018-09-07 18:47:52",
        //             "type" => "0",
        //             "xrp" => "20.00000000",
        //             "eur" => 0,
        //         ),
        //     )
        //
        $type = $this->parse_ledger_entry_type($this->safe_string($item, 'type'));
        if ($type === 'trade') {
            $parsedTrade = $this->parse_trade($item);
            $market = null;
            $keys = is_array($item) ? array_keys($item) : array();
            for ($i = 0; $i < count($keys); $i++) {
                if (mb_strpos($keys[$i], '_') !== false) {
                    $marketId = str_replace('_', '', $keys[$i]);
                    $market = $this->safe_market($marketId, $market);
                }
            }
            // if the $market is still not defined
            // try to deduce it from used $keys
            if ($market === null) {
                $market = $this->get_market_from_trade($item);
            }
            $direction = ($parsedTrade['side'] === 'buy') ? 'in' : 'out';
            return $this->safe_ledger_entry(array(
                'info' => $item,
                'id' => $parsedTrade['id'],
                'timestamp' => $parsedTrade['timestamp'],
                'datetime' => $parsedTrade['datetime'],
                'direction' => $direction,
                'account' => null,
                'referenceId' => $parsedTrade['order'],
                'referenceAccount' => null,
                'type' => $type,
                'currency' => $market['base'],
                'amount' => $parsedTrade['amount'],
                'before' => null,
                'after' => null,
                'status' => 'ok',
                'fee' => $parsedTrade['fee'],
            ), $currency);
        } else {
            $parsedTransaction = $this->parse_transaction($item, $currency);
            $direction = null;
            if (is_array($item) && array_key_exists('amount', $item)) {
                $amount = $this->safe_string($item, 'amount');
                $direction = Precise::string_gt($amount, '0') ? 'in' : 'out';
            } elseif ((is_array($parsedTransaction) && array_key_exists('currency', $parsedTransaction)) && $parsedTransaction['currency'] !== null) {
                $currencyCode = $this->safe_string($parsedTransaction, 'currency');
                $currency = $this->currency($currencyCode);
                $amount = $this->safe_string($item, $currency['id']);
                $direction = Precise::string_gt($amount, '0') ? 'in' : 'out';
            }
            return $this->safe_ledger_entry(array(
                'info' => $item,
                'id' => $parsedTransaction['id'],
                'timestamp' => $parsedTransaction['timestamp'],
                'datetime' => $parsedTransaction['datetime'],
                'direction' => $direction,
                'account' => null,
                'referenceId' => $parsedTransaction['txid'],
                'referenceAccount' => null,
                'type' => $type,
                'currency' => $parsedTransaction['currency'],
                'amount' => $parsedTransaction['amount'],
                'before' => null,
                'after' => null,
                'status' => $parsedTransaction['status'],
                'fee' => $parsedTransaction['fee'],
            ), $currency);
        }
    }

    public function fetch_ledger(?string $code = null, ?int $since = null, ?int $limit = null, $params = array ()): array {
        /**
         * fetch the history of changes, actions done by the user or operations that altered the balance of the user
         *
         * @see https://www.bitstamp.net/api/#tag/Transactions-private/operation/GetUserTransactions
         *
         * @param {string} [$code] unified $currency $code, default is null
         * @param {int} [$since] timestamp in ms of the earliest ledger entry, default is null
         * @param {int} [$limit] max number of ledger entries to return, default is null
         * @param {array} [$params] extra parameters specific to the exchange API endpoint
         * @return {array} a ~@link https://docs.ccxt.com/#/?id=ledger ledger structure~
         */
        $this->load_markets();
        $request = array();
        if ($limit !== null) {
            $request['limit'] = $limit;
        }
        $response = $this->privatePostUserTransactions ($this->extend($request, $params));
        $currency = null;
        if ($code !== null) {
            $currency = $this->currency($code);
        }
        return $this->parse_ledger($response, $currency, $since, $limit);
    }

    public function fetch_open_orders(?string $symbol = null, ?int $since = null, ?int $limit = null, $params = array ()): array {
        /**
         * fetch all unfilled currently open orders
         *
         * @see https://www.bitstamp.net/api/#tag/Orders/operation/GetAllOpenOrders
         * @see https://www.bitstamp.net/api/#tag/Orders/operation/GetOpenOrdersForMarket
         *
         * @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~
         */
        $market = null;
        $this->load_markets();
        if ($symbol !== null) {
            $market = $this->market($symbol);
        }
        $response = $this->privatePostOpenOrdersAll ($params);
        //
        //     array(
        //         {
        //             "price" => "0.00008012",
        //             "currency_pair" => "XRP/BTC",
        //             "client_order_id" => '',
        //             "datetime" => "2019-01-31 21:23:36",
        //             "amount" => "15.00000000",
        //             "type" => "0",
        //             "id" => "2814205012",
        //         }
        //     )
        //
        return $this->parse_orders($response, $market, $since, $limit, array(
            'status' => 'open',
            'type' => 'limit',
        ));
    }

    public function get_currency_name($code) {
        /**
         * @ignore
         * @param {string} $code Unified currency $code
         * @return {string} lowercase version of $code
         */
        return strtolower($code);
    }

    public function is_fiat($code) {
        return $code === 'USD' || $code === 'EUR' || $code === 'GBP';
    }

    public function fetch_deposit_address(string $code, $params = array ()): array {
        /**
         * fetch the deposit $address for a currency associated with this account
         *
         * @see https://www.bitstamp.net/api/#tag/Deposits/operation/GetCryptoDepositAddress
         *
         * @param {string} $code unified currency $code
         * @param {array} [$params] extra parameters specific to the exchange API endpoint
         * @return {array} an ~@link https://docs.ccxt.com/#/?id=$address-structure $address structure~
         */
        if ($this->is_fiat($code)) {
            throw new NotSupported($this->id . ' fiat fetchDepositAddress() for ' . $code . ' is not supported!');
        }
        $name = $this->get_currency_name($code);
        $method = 'privatePost' . $this->capitalize($name) . 'Address';
        $response = $this->$method ($params);
        $address = $this->safe_string($response, 'address');
        $tag = $this->safe_string_2($response, 'memo_id', 'destination_tag');
        $this->check_address($address);
        return array(
            'info' => $response,
            'currency' => $code,
            'network' => null,
            'address' => $address,
            'tag' => $tag,
        );
    }

    public function withdraw(string $code, float $amount, string $address, ?string $tag = null, $params = array ()): array {
        /**
         * make a withdrawal
         *
         * @see https://www.bitstamp.net/api/#tag/Withdrawals/operation/RequestFiatWithdrawal
         * @see https://www.bitstamp.net/api/#tag/Withdrawals/operation/RequestCryptoWithdrawal
         *
         * @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~
         */
        // For fiat withdrawals please provide all required additional parameters in the 'params'
        // Check https://www.bitstamp.net/api/ under 'Open bank withdrawal' for list and description.
        list($tag, $params) = $this->handle_withdraw_tag_and_params($tag, $params);
        $this->load_markets();
        $this->check_address($address);
        $request = array(
            'amount' => $amount,
        );
        $currency = null;
        $method = null;
        if (!$this->is_fiat($code)) {
            $name = $this->get_currency_name($code);
            $method = 'privatePost' . $this->capitalize($name) . 'Withdrawal';
            if ($code === 'XRP') {
                if ($tag !== null) {
                    $request['destination_tag'] = $tag;
                }
            } elseif ($code === 'XLM' || $code === 'HBAR') {
                if ($tag !== null) {
                    $request['memo_id'] = $tag;
                }
            }
            $request['address'] = $address;
        } else {
            $method = 'privatePostWithdrawalOpen';
            $currency = $this->currency($code);
            $request['iban'] = $address;
            $request['account_currency'] = $currency['id'];
        }
        $response = $this->$method ($this->extend($request, $params));
        return $this->parse_transaction($response, $currency);
    }

    public function transfer(string $code, float $amount, string $fromAccount, string $toAccount, $params = array ()): array {
        /**
         * $transfer $currency internally between wallets on the same account
         *
         * @see https://www.bitstamp.net/api/#tag/Sub-account/operation/TransferFromMainToSub
         * @see https://www.bitstamp.net/api/#tag/Sub-account/operation/TransferFromSubToMain
         *
         * @param {string} $code unified $currency $code
         * @param {float} $amount amount to $transfer
         * @param {string} $fromAccount account to $transfer from
         * @param {string} $toAccount account to $transfer to
         * @param {array} [$params] extra parameters specific to the exchange API endpoint
         * @return {array} a ~@link https://docs.ccxt.com/#/?id=$transfer-structure $transfer structure~
         */
        $this->load_markets();
        $currency = $this->currency($code);
        $request = array(
            'amount' => $this->parse_to_numeric($this->currency_to_precision($code, $amount)),
            'currency' => strtoupper($currency['id']),
        );
        $response = null;
        if ($fromAccount === 'main') {
            $request['subAccount'] = $toAccount;
            $response = $this->privatePostTransferFromMain ($this->extend($request, $params));
        } elseif ($toAccount === 'main') {
            $request['subAccount'] = $fromAccount;
            $response = $this->privatePostTransferToMain ($this->extend($request, $params));
        } else {
            throw new BadRequest($this->id . ' $transfer() only supports from or to main');
        }
        //
        //    array( status => 'ok' )
        //
        $transfer = $this->parse_transfer($response, $currency);
        $transfer['amount'] = $amount;
        $transfer['fromAccount'] = $fromAccount;
        $transfer['toAccount'] = $toAccount;
        return $transfer;
    }

    public function parse_transfer($transfer, $currency = null) {
        //
        //    array( $status => 'ok' )
        //
        $status = $this->safe_string($transfer, 'status');
        return array(
            'info' => $transfer,
            'id' => null,
            'timestamp' => null,
            'datetime' => null,
            'currency' => $currency['code'],
            'amount' => null,
            'fromAccount' => null,
            'toAccount' => null,
            'status' => $this->parse_transfer_status($status),
        );
    }

    public function parse_transfer_status(?string $status): ?string {
        $statuses = array(
            'ok' => 'ok',
            'error' => 'failed',
        );
        return $this->safe_string($statuses, $status, $status);
    }

    public function nonce() {
        return $this->milliseconds();
    }

    public function sign($path, $api = 'public', $method = 'GET', $params = array (), $headers = null, $body = null) {
        $url = $this->urls['api'][$api] . '/';
        $url .= $this->version . '/';
        $url .= $this->implode_params($path, $params);
        $query = $this->omit($params, $this->extract_params($path));
        if ($api === 'public') {
            if ($query) {
                $url .= '?' . $this->urlencode($query);
            }
        } else {
            $this->check_required_credentials();
            $xAuth = 'BITSTAMP ' . $this->apiKey;
            $xAuthNonce = $this->uuid();
            $xAuthTimestamp = (string) $this->milliseconds();
            $xAuthVersion = 'v2';
            $contentType = '';
            $headers = array(
                'X-Auth' => $xAuth,
                'X-Auth-Nonce' => $xAuthNonce,
                'X-Auth-Timestamp' => $xAuthTimestamp,
                'X-Auth-Version' => $xAuthVersion,
            );
            if ($method === 'POST') {
                if ($query) {
                    $body = $this->urlencode($query);
                    $contentType = 'application/x-www-form-urlencoded';
                    $headers['Content-Type'] = $contentType;
                } else {
                    // sending an empty POST request will trigger
                    // an API0020 error returned by the exchange
                    // therefore for empty requests we send a dummy object
                    // https://github.com/ccxt/ccxt/issues/6846
                    $body = $this->urlencode(array( 'foo' => 'bar' ));
                    $contentType = 'application/x-www-form-urlencoded';
                    $headers['Content-Type'] = $contentType;
                }
            }
            $authBody = $body ? $body : '';
            $auth = $xAuth . $method . str_replace('https://', '', $url) . $contentType . $xAuthNonce . $xAuthTimestamp . $xAuthVersion . $authBody;
            $signature = $this->hmac($this->encode($auth), $this->encode($this->secret), 'sha256');
            $headers['X-Auth-Signature'] = $signature;
        }
        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) {
            return null;
        }
        //
        //     array("error" => "No permission found") // fetchDepositAddress returns this on apiKeys that don't have the permission required
        //     array("status" => "error", "reason" => array("__all__" => ["Minimum order size is 5.0 EUR."]))
        //     reuse of a nonce gives => array( $status => 'error', $reason => 'Invalid nonce', $code => 'API0004' )
        //
        $status = $this->safe_string($response, 'status');
        $error = $this->safe_value($response, 'error');
        if (($status === 'error') || ($error !== null)) {
            $errors = array();
            if (gettype($error) === 'string') {
                $errors[] = $error;
            } elseif ($error !== null) {
                $keys = is_array($error) ? array_keys($error) : array();
                for ($i = 0; $i < count($keys); $i++) {
                    $key = $keys[$i];
                    $value = $this->safe_value($error, $key);
                    if (gettype($value) === 'array' && array_keys($value) === array_keys(array_keys($value))) {
                        $errors = $this->array_concat($errors, $value);
                    } else {
                        $errors[] = $value;
                    }
                }
            }
            $reasonInner = $this->safe_value($response, 'reason', array());
            if (gettype($reasonInner) === 'string') {
                $errors[] = $reasonInner;
            } else {
                $all = $this->safe_value($reasonInner, '__all__', array());
                for ($i = 0; $i < count($all); $i++) {
                    $errors[] = $all[$i];
                }
            }
            $code = $this->safe_string($response, 'code');
            if ($code === 'API0005') {
                throw new AuthenticationError($this->id . ' invalid signature, use the uid for the main account if you have subaccounts');
            }
            $feedback = $this->id . ' ' . $body;
            for ($i = 0; $i < count($errors); $i++) {
                $value = $errors[$i];
                $this->throw_exactly_matched_exception($this->exceptions['exact'], $value, $feedback);
                $this->throw_broadly_matched_exception($this->exceptions['broad'], $value, $feedback);
            }
            throw new ExchangeError($feedback);
        }
        return null;
    }
}
