<?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\bitso as Exchange;

class bitso extends Exchange {

    public function describe(): mixed {
        return $this->deep_extend(parent::describe(), array(
            'id' => 'bitso',
            'name' => 'Bitso',
            'countries' => array( 'MX' ), // Mexico
            'rateLimit' => 2000, // 30 requests per minute
            'version' => 'v3',
            'has' => array(
                'CORS' => null,
                'spot' => true,
                'margin' => false,
                'swap' => false,
                'future' => false,
                'option' => false,
                'addMargin' => false,
                'borrowCrossMargin' => false,
                'borrowIsolatedMargin' => false,
                'borrowMargin' => false,
                'cancelAllOrders' => true,
                'cancelOrder' => true,
                'cancelOrders' => true,
                'closeAllPositions' => false,
                'closePosition' => false,
                'createDepositAddress' => false,
                'createOrder' => true,
                'createOrderWithTakeProfitAndStopLoss' => false,
                'createOrderWithTakeProfitAndStopLossWs' => false,
                'createReduceOnlyOrder' => false,
                'fetchAccounts' => false,
                'fetchBalance' => true,
                'fetchBorrowInterest' => false,
                'fetchBorrowRate' => false,
                'fetchBorrowRateHistories' => false,
                'fetchBorrowRateHistory' => false,
                'fetchBorrowRates' => false,
                'fetchBorrowRatesPerSymbol' => false,
                'fetchCrossBorrowRate' => false,
                'fetchCrossBorrowRates' => false,
                'fetchCurrencies' => false,
                'fetchDeposit' => true,
                'fetchDepositAddress' => true,
                'fetchDepositAddresses' => false,
                'fetchDepositAddressesByNetwork' => false,
                'fetchDeposits' => true,
                'fetchDepositsWithdrawals' => false,
                '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,
                'fetchOrderTrades' => true,
                'fetchPosition' => false,
                'fetchPositionHistory' => false,
                'fetchPositionMode' => false,
                'fetchPositions' => false,
                'fetchPositionsForSymbol' => false,
                'fetchPositionsHistory' => false,
                'fetchPositionsRisk' => false,
                'fetchPremiumIndexOHLCV' => false,
                'fetchSettlementHistory' => false,
                'fetchTicker' => true,
                'fetchTickers' => false,
                'fetchTime' => false,
                'fetchTrades' => true,
                'fetchTradingFee' => false,
                'fetchTradingFees' => true,
                'fetchTransactionFee' => false,
                'fetchTransactionFees' => true,
                'fetchTransactions' => false,
                'fetchTransfer' => false,
                'fetchTransfers' => false,
                'fetchVolatilityHistory' => false,
                'reduceMargin' => false,
                'repayCrossMargin' => false,
                'repayIsolatedMargin' => false,
                'setLeverage' => false,
                'setMargin' => false,
                'setMarginMode' => false,
                'setPositionMode' => false,
                'transfer' => false,
                'withdraw' => true,
            ),
            'urls' => array(
                'logo' => 'https://github.com/user-attachments/assets/178c8e56-9054-4107-b192-5e5053d4f975',
                'api' => array(
                    'rest' => 'https://bitso.com/api',
                ),
                'test' => array(
                    'rest' => 'https://stage.bitso.com/api',
                ),
                'www' => 'https://bitso.com',
                'doc' => 'https://bitso.com/api_info',
                'fees' => 'https://bitso.com/fees',
                'referral' => 'https://bitso.com/?ref=itej',
            ),
            'precisionMode' => TICK_SIZE,
            'options' => array(
                'precision' => array(
                    'XRP' => 0.000001,
                    'MXN' => 0.01,
                    'TUSD' => 0.01,
                ),
                'defaultPrecision' => 0.00000001,
                'networks' => array(
                    'TRC20' => 'trx',
                    'ERC20' => 'erc20',
                    'BEP20' => 'bsc',
                    'BEP2' => 'bep2',
                ),
            ),
            'timeframes' => array(
                '1m' => '60',
                '5m' => '300',
                '15m' => '900',
                '30m' => '1800',
                '1h' => '3600',
                '4h' => '14400',
                '12h' => '43200',
                '1d' => '86400',
                '1w' => '604800',
            ),
            'api' => array(
                'public' => array(
                    'get' => array(
                        'available_books',
                        'ticker',
                        'order_book',
                        'trades',
                        'ohlc',
                    ),
                ),
                'private' => array(
                    'get' => array(
                        'account_status',
                        'balance',
                        'fees',
                        'fundings',
                        'fundings/{fid}',
                        'funding_destination',
                        'kyc_documents',
                        'ledger',
                        'ledger/trades',
                        'ledger/fees',
                        'ledger/fundings',
                        'ledger/withdrawals',
                        'mx_bank_codes',
                        'open_orders',
                        'order_trades/{oid}',
                        'orders/{oid}',
                        'user_trades',
                        'user_trades/{tid}',
                        'withdrawals/',
                        'withdrawals/{wid}',
                    ),
                    'post' => array(
                        'bitcoin_withdrawal',
                        'debit_card_withdrawal',
                        'ether_withdrawal',
                        'orders',
                        'phone_number',
                        'phone_verification',
                        'phone_withdrawal',
                        'spei_withdrawal',
                        'ripple_withdrawal',
                        'bcash_withdrawal',
                        'litecoin_withdrawal',
                    ),
                    'delete' => array(
                        'orders',
                        'orders/{oid}',
                        'orders/all',
                    ),
                ),
            ),
            'features' => array(
                'spot' => array(
                    'sandbox' => false,
                    'createOrder' => array(
                        'marginMode' => false,
                        'triggerPrice' => true, // todo implementation
                        'triggerPriceType' => null,
                        'triggerDirection' => null,
                        'stopLossPrice' => false, // todo
                        'takeProfitPrice' => false, // todo
                        'attachedStopLossTakeProfit' => null,
                        // todo => implementation for TIF
                        'timeInForce' => array(
                            'IOC' => true,
                            'FOK' => true,
                            'PO' => true,
                            'GTD' => false,
                        ),
                        'hedged' => false,
                        'trailing' => false,
                        'leverage' => false,
                        'marketBuyRequiresPrice' => false,
                        'marketBuyByCost' => false,
                        'selfTradePrevention' => false,
                        'iceberg' => false,
                    ),
                    'createOrders' => null,
                    'fetchMyTrades' => array(
                        'marginMode' => false,
                        'limit' => 100,
                        'daysBack' => null,
                        'untilDays' => null,
                        'symbolRequired' => true,
                    ),
                    'fetchOrder' => array(
                        'marginMode' => false,
                        'trigger' => false,
                        'trailing' => false,
                        'symbolRequired' => true,
                    ),
                    'fetchOpenOrders' => array(
                        'marginMode' => false,
                        'limit' => 500,
                        'trigger' => false,
                        'trailing' => false,
                        'symbolRequired' => true,
                    ),
                    'fetchOrders' => null,
                    'fetchClosedOrders' => null,
                    'fetchOHLCV' => array(
                        'limit' => 300,
                    ),
                ),
                'swap' => array(
                    'linear' => null,
                    'inverse' => null,
                ),
                'future' => array(
                    'linear' => null,
                    'inverse' => null,
                ),
            ),
            'exceptions' => array(
                '0201' => '\\ccxt\\AuthenticationError', // Invalid Nonce or Invalid Credentials
                '104' => '\\ccxt\\InvalidNonce', // Cannot perform request - nonce must be higher than 1520307203724237
                '0304' => '\\ccxt\\BadRequest', // array("success":false,"error":array("code":"0304","message":"The field time_bucket () is either invalid or missing"))
            ),
        ));
    }

    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
         * @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~
         */
        $request = array();
        if ($limit !== null) {
            $request['limit'] = $limit;
        }
        $response = $this->privateGetLedger ($this->extend($request, $params));
        //
        //     {
        //         "success" => true,
        //         "payload" => [{
        //             "eid" => "2510b3e2bc1c87f584500a18084f35ed",
        //             "created_at" => "2022-06-08T12:21:42+0000",
        //             "balance_updates" => [array(
        //                 "amount" => "0.00080000",
        //                 "currency" => "btc"
        //             )],
        //             "operation" => "funding",
        //             "details" => array(
        //                 "network" => "btc",
        //                 "method" => "btc",
        //                 "method_name" => "Bitcoin",
        //                 "asset" => "btc",
        //                 "protocol" => "btc",
        //                 "integration" => "bitgo-v2",
        //                 "fid" => "6112c6369100d6ecceb7f54f17cf0511"
        //             }
        //         )]
        //     }
        //
        $payload = $this->safe_value($response, 'payload', array());
        $currency = $this->safe_currency($code);
        return $this->parse_ledger($payload, $currency, $since, $limit);
    }

    public function parse_ledger_entry_type($type) {
        $types = array(
            'funding' => 'transaction',
            'withdrawal' => 'transaction',
            'trade' => 'trade',
            'fee' => 'fee',
        );
        return $this->safe_string($types, $type, $type);
    }

    public function parse_ledger_entry(array $item, ?array $currency = null): array {
        //
        //     {
        //         "eid" => "2510b3e2bc1c87f584500a18084f35ed",
        //         "created_at" => "2022-06-08T12:21:42+0000",
        //         "balance_updates" => [array(
        //             "amount" => "0.00080000",
        //             "currency" => "btc"
        //         )],
        //         "operation" => "funding",
        //         "details" => {
        //             "network" => "btc",
        //             "method" => "btc",
        //             "method_name" => "Bitcoin",
        //             "asset" => "btc",
        //             "protocol" => "btc",
        //             "integration" => "bitgo-v2",
        //             "fid" => "6112c6369100d6ecceb7f54f17cf0511"
        //         }
        //     }
        //
        //  trade
        //     {
        //         "eid" => "8976c6053f078f704f037d82a813678a",
        //         "created_at" => "2022-06-08T17:01:48+0000",
        //         "balance_updates" => [array(
        //                 "amount" => "59.21320500",
        //                 "currency" => "mxn"
        //             ),
        //             {
        //                 "amount" => "-0.00010000",
        //                 "currency" => "btc"
        //             }
        //         ],
        //         "operation" => "trade",
        //         "details" => {
        //             "tid" => "72145428",
        //             "oid" => "JO5TZmMZjzjlZDyT"
        //         }
        //     }
        //
        //  $fee
        //     {
        //         "eid" => "cbbb3c8d4e41723d25d2850dcb7c3c74",
        //         "created_at" => "2022-06-08T17:01:48+0000",
        //         "balance_updates" => [array(
        //             "amount" => "-0.38488583",
        //             "currency" => "mxn"
        //         )],
        //         "operation" => "fee",
        //         "details" => {
        //             "tid" => "72145428",
        //             "oid" => "JO5TZmMZjzjlZDyT"
        //         }
        //     }
        $operation = $this->safe_string($item, 'operation');
        $type = $this->parse_ledger_entry_type($operation);
        $balanceUpdates = $this->safe_value($item, 'balance_updates', array());
        $firstBalance = $this->safe_value($balanceUpdates, 0, array());
        $direction = null;
        $fee = null;
        $amount = $this->safe_string($firstBalance, 'amount');
        $currencyId = $this->safe_string($firstBalance, 'currency');
        $code = $this->safe_currency_code($currencyId, $currency);
        $currency = $this->safe_currency($currencyId, $currency);
        $details = $this->safe_value($item, 'details', array());
        $referenceId = $this->safe_string_2($details, 'fid', 'wid');
        if ($referenceId === null) {
            $referenceId = $this->safe_string($details, 'tid');
        }
        if ($operation === 'funding') {
            $direction = 'in';
        } elseif ($operation === 'withdrawal') {
            $direction = 'out';
        } elseif ($operation === 'trade') {
            $direction = null;
        } elseif ($operation === 'fee') {
            $direction = 'out';
            $cost = Precise::string_abs($amount);
            $fee = array(
                'cost' => $cost,
                'currency' => $currency,
            );
        }
        $timestamp = $this->parse8601($this->safe_string($item, 'created_at'));
        return $this->safe_ledger_entry(array(
            'info' => $item,
            'id' => $this->safe_string($item, 'eid'),
            'direction' => $direction,
            'account' => null,
            'referenceId' => $referenceId,
            'referenceAccount' => null,
            'type' => $type,
            'currency' => $code,
            'amount' => $amount,
            'timestamp' => $timestamp,
            'datetime' => $this->iso8601($timestamp),
            'before' => null,
            'after' => null,
            'status' => 'ok',
            'fee' => $fee,
        ), $currency);
    }

    public function fetch_markets($params = array ()): array {
        /**
         * retrieves data on all $markets for bitso
         *
         * @see https://docs.bitso.com/bitso-api/docs/list-available-books
         *
         * @param {array} [$params] extra parameters specific to the exchange API endpoint
         * @return {array[]} an array of objects representing $market data
         */
        $response = $this->publicGetAvailableBooks ($params);
        //
        //     {
        //         "success":true,
        //         "payload":array(
        //             {
        //                 "book":"btc_mxn",
        //                 "minimum_price":"500",
        //                 "maximum_price":"10000000",
        //                 "minimum_amount":"0.00005",
        //                 "maximum_amount":"500",
        //                 "minimum_value":"5",
        //                 "maximum_value":"10000000",
        //                 "tick_size":"0.01",
        //                 "fees":array(
        //                     "flat_rate":array("maker":"0.500","taker":"0.650"),
        //                     "structure":array(
        //                         array("volume":"1500000","maker":"0.00500","taker":"0.00650"),
        //                         array("volume":"2000000","maker":"0.00490","taker":"0.00637"),
        //                         array("volume":"5000000","maker":"0.00480","taker":"0.00624"),
        //                         array("volume":"7000000","maker":"0.00440","taker":"0.00572"),
        //                         array("volume":"10000000","maker":"0.00420","taker":"0.00546"),
        //                         array("volume":"15000000","maker":"0.00400","taker":"0.00520"),
        //                         array("volume":"35000000","maker":"0.00370","taker":"0.00481"),
        //                         array("volume":"50000000","maker":"0.00300","taker":"0.00390"),
        //                         array("volume":"150000000","maker":"0.00200","taker":"0.00260"),
        //                         array("volume":"250000000","maker":"0.00100","taker":"0.00130"),
        //                         array("volume":"9999999999","maker":"0.00000","taker":"0.00130"),
        //                     )
        //                 }
        //             ),
        //         )
        //     }
        $markets = $this->safe_value($response, 'payload', array());
        $result = array();
        for ($i = 0; $i < count($markets); $i++) {
            $market = $markets[$i];
            $id = $this->safe_string($market, 'book');
            list($baseId, $quoteId) = explode('_', $id);
            $base = strtoupper($baseId);
            $quote = strtoupper($quoteId);
            $base = $this->safe_currency_code($base);
            $quote = $this->safe_currency_code($quote);
            $fees = $this->safe_value($market, 'fees', array());
            $flatRate = $this->safe_value($fees, 'flat_rate', array());
            $takerString = $this->safe_string($flatRate, 'taker');
            $makerString = $this->safe_string($flatRate, 'maker');
            $taker = $this->parse_number(Precise::string_div($takerString, '100'));
            $maker = $this->parse_number(Precise::string_div($makerString, '100'));
            $feeTiers = $this->safe_value($fees, 'structure', array());
            $fee = array(
                'taker' => $taker,
                'maker' => $maker,
                'percentage' => true,
                'tierBased' => true,
            );
            $takerFees = array();
            $makerFees = array();
            for ($j = 0; $j < count($feeTiers); $j++) {
                $tier = $feeTiers[$j];
                $volume = $this->safe_number($tier, 'volume');
                $takerFee = $this->safe_number($tier, 'taker');
                $makerFee = $this->safe_number($tier, 'maker');
                $takerFees[] = array( $volume, $takerFee );
                $makerFees[] = array( $volume, $makerFee );
                if ($j === 0) {
                    $fee['taker'] = $takerFee;
                    $fee['maker'] = $makerFee;
                }
            }
            $tiers = array(
                'taker' => $takerFees,
                'maker' => $makerFees,
            );
            $fee['tiers'] = $tiers;
            // TODO => precisions can be also set from https://bitso.com/api/v3/catalogues ->available_currency_conversions->currencies (or ->currencies->metadata)  or https://bitso.com/api/v3/get_exchange_rates/mxn
            $defaultPricePrecision = $this->safe_number($this->options['precision'], $quote, $this->options['defaultPrecision']);
            $result[] = $this->extend(array(
                'id' => $id,
                'symbol' => $base . '/' . $quote,
                'base' => $base,
                'quote' => $quote,
                'settle' => null,
                'baseId' => $baseId,
                'quoteId' => $quoteId,
                'settleId' => null,
                'type' => 'spot',
                'spot' => true,
                'margin' => false,
                'swap' => false,
                'future' => false,
                'option' => false,
                'active' => null,
                'contract' => false,
                'linear' => null,
                'inverse' => null,
                'taker' => $taker,
                'maker' => $maker,
                'contractSize' => null,
                'expiry' => null,
                'expiryDatetime' => null,
                'strike' => null,
                'optionType' => null,
                'precision' => array(
                    'amount' => $this->safe_number($this->options['precision'], $base, $this->options['defaultPrecision']),
                    'price' => $this->safe_number($market, 'tick_size', $defaultPricePrecision),
                ),
                'limits' => array(
                    'leverage' => array(
                        'min' => null,
                        'max' => null,
                    ),
                    'amount' => array(
                        'min' => $this->safe_number($market, 'minimum_amount'),
                        'max' => $this->safe_number($market, 'maximum_amount'),
                    ),
                    'price' => array(
                        'min' => $this->safe_number($market, 'minimum_price'),
                        'max' => $this->safe_number($market, 'maximum_price'),
                    ),
                    'cost' => array(
                        'min' => $this->safe_number($market, 'minimum_value'),
                        'max' => $this->safe_number($market, 'maximum_value'),
                    ),
                ),
                'created' => null,
                'info' => $market,
            ), $fee);
        }
        return $result;
    }

    public function parse_balance($response): array {
        $payload = $this->safe_value($response, 'payload', array());
        $balances = $this->safe_value($payload, 'balances', array());
        $result = array(
            'info' => $response,
            'timestamp' => null,
            'datetime' => null,
        );
        for ($i = 0; $i < count($balances); $i++) {
            $balance = $balances[$i];
            $currencyId = $this->safe_string($balance, 'currency');
            $code = $this->safe_currency_code($currencyId);
            $account = $this->account();
            $account['free'] = $this->safe_string($balance, 'available');
            $account['used'] = $this->safe_string($balance, 'locked');
            $account['total'] = $this->safe_string($balance, 'total');
            $result[$code] = $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://docs.bitso.com/bitso-api/docs/get-account-balance
         *
         * @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->privateGetBalance ($params);
        //
        //     {
        //       "success" => true,
        //       "payload" => array(
        //         "balances" => array(
        //           array(
        //             "currency" => "bat",
        //             "available" => "0.00000000",
        //             "locked" => "0.00000000",
        //             "total" => "0.00000000",
        //             "pending_deposit" => "0.00000000",
        //             "pending_withdrawal" => "0.00000000"
        //           ),
        //           array(
        //             "currency" => "bch",
        //             "available" => "0.00000000",
        //             "locked" => "0.00000000",
        //             "total" => "0.00000000",
        //             "pending_deposit" => "0.00000000",
        //             "pending_withdrawal" => "0.00000000"
        //           ),
        //         ),
        //       ),
        //     }
        //
        return $this->parse_balance($response);
    }

    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://docs.bitso.com/bitso-api/docs/list-order-book
         *
         * @param {string} $symbol unified $symbol of the $market to fetch the order book for
         * @param {int} [$limit] the maximum amount of order book entries to return
         * @param {array} [$params] extra parameters specific to the exchange API endpoint
         * @return {array} A dictionary of ~@link https://docs.ccxt.com/#/?id=order-book-structure order book structures~ indexed by $market symbols
         */
        $this->load_markets();
        $market = $this->market($symbol);
        $request = array(
            'book' => $market['id'],
        );
        $response = $this->publicGetOrderBook ($this->extend($request, $params));
        $orderbook = $this->safe_value($response, 'payload');
        $timestamp = $this->parse8601($this->safe_string($orderbook, 'updated_at'));
        return $this->parse_order_book($orderbook, $market['symbol'], $timestamp, 'bids', 'asks', 'price', 'amount');
    }

    public function parse_ticker(array $ticker, ?array $market = null): array {
        //
        //     {
        //         "high":"37446.85",
        //         "last":"36599.54",
        //         "created_at":"2022-01-28T12:06:11+00:00",
        //         "book":"btc_usdt",
        //         "volume":"7.29075419",
        //         "vwap":"36579.1564400307",
        //         "low":"35578.52",
        //         "ask":"36574.76",
        //         "bid":"36538.22",
        //         "change_24":"-105.64"
        //     }
        //
        $symbol = $this->safe_symbol(null, $market);
        $timestamp = $this->parse8601($this->safe_string($ticker, 'created_at'));
        $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' => null,
            '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://docs.bitso.com/bitso-api/docs/ticker
         *
         * @param {string} $symbol unified $symbol of the $market to fetch the $ticker for
         * @param {array} [$params] extra parameters specific to the exchange API endpoint
         * @return {array} a ~@link https://docs.ccxt.com/#/?id=$ticker-structure $ticker structure~
         */
        $this->load_markets();
        $market = $this->market($symbol);
        $request = array(
            'book' => $market['id'],
        );
        $response = $this->publicGetTicker ($this->extend($request, $params));
        $ticker = $this->safe_value($response, 'payload');
        //
        //     {
        //         "success":true,
        //         "payload":{
        //             "high":"37446.85",
        //             "last":"37051.96",
        //             "created_at":"2022-01-28T17:03:29+00:00",
        //             "book":"btc_usdt",
        //             "volume":"6.16176186",
        //             "vwap":"36582.6293169472",
        //             "low":"35578.52",
        //             "ask":"37083.62",
        //             "bid":"37039.66",
        //             "change_24":"478.45"
        //         }
        //     }
        //
        return $this->parse_ticker($ticker, $market);
    }

    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
         * @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(
            'book' => $market['id'],
            'time_bucket' => $this->safe_string($this->timeframes, $timeframe, $timeframe),
        );
        if ($since !== null) {
            $request['start'] = $since;
            if ($limit !== null) {
                $duration = $this->parse_timeframe($timeframe);
                $request['end'] = $this->sum($since, $duration * $limit * 1000);
            }
        } elseif ($limit !== null) {
            $now = $this->milliseconds();
            $request['end'] = $now;
            $request['start'] = $now - $this->parse_timeframe($timeframe) * 1000 * $limit;
        }
        $response = $this->publicGetOhlc ($this->extend($request, $params));
        //
        //     {
        //         "success":true,
        //         "payload" => array(
        //             array(
        //                 "bucket_start_time":1648219140000,
        //                 "first_trade_time":1648219154990,
        //                 "last_trade_time":1648219189442,
        //                 "first_rate":"44958.60",
        //                 "last_rate":"44979.88",
        //                 "min_rate":"44957.33",
        //                 "max_rate":"44979.88",
        //                 "trade_count":8,
        //                 "volume":"0.00082814",
        //                 "vwap":"44965.02"
        //             ),
        //         )
        //     }
        //
        $payload = $this->safe_list($response, 'payload', array());
        return $this->parse_ohlcvs($payload, $market, $timeframe, $since, $limit);
    }

    public function parse_ohlcv($ohlcv, ?array $market = null): array {
        //
        //     array(
        //         "bucket_start_time":1648219140000,
        //         "first_trade_time":1648219154990,
        //         "last_trade_time":1648219189441,
        //         "first_rate":"44958.60",
        //         "last_rate":"44979.88",
        //         "min_rate":"44957.33",
        //         "max_rate":"44979.88",
        //         "trade_count":8,
        //         "volume":"0.00082814",
        //         "vwap":"44965.02"
        //     ),
        //
        return array(
            $this->safe_integer($ohlcv, 'bucket_start_time'),
            $this->safe_number($ohlcv, 'first_rate'),
            $this->safe_number($ohlcv, 'max_rate'),
            $this->safe_number($ohlcv, 'min_rate'),
            $this->safe_number($ohlcv, 'last_rate'),
            $this->safe_number($ohlcv, 'volume'),
        );
    }

    public function parse_trade(array $trade, ?array $market = null): array {
        //
        // fetchTrades (public)
        //
        //      {
        //          "book" => "btc_usdt",
        //          "created_at" => "2021-11-24T12:14:53+0000",
        //          "amount" => "0.00026562",
        //          "maker_side" => "sell",
        //          "price" => "56471.55",
        //          "tid" => "52557338"
        //      }
        //
        // fetchMyTrades (private)
        //
        //      {
        //          "book" => "btc_usdt",
        //          "created_at" => "2021-11-24T12:31:03+0000",
        //          "minor" => "11.30356000",
        //          "major" => "-0.00020000",
        //          "fees_amount" => "0.01119052",
        //          "fees_currency" => "usdt",
        //          "minor_currency" => "usdt",
        //          "major_currency" => "btc",
        //          "oid" => "djTzMIWx2Vi3iMjl",
        //          "tid" => "52559051",
        //          "price" => "56517.80",
        //          "side" => "sell",
        //          "maker_side" => "buy"
        //      }
        //
        // fetchOrderTrades (private)
        //
        //      {
        //          "book" => "btc_usdt",
        //          "created_at" => "2021-11-24T12:30:52+0000",
        //          "minor" => "-11.33047916",
        //          "major" => "0.00020020",
        //          "fees_amount" => "0.00000020",
        //          "fees_currency" => "btc",
        //          "minor_currency" => "usdt",
        //          "major_currency" => "btc",
        //          "oid" => "O0D2zcljjjQF5xlG",
        //          "tid" => "52559030",
        //          "price" => "56595.80",
        //          "side" => "buy",
        //          "maker_side" => "sell"
        //      }
        //
        $timestamp = $this->parse8601($this->safe_string($trade, 'created_at'));
        $marketId = $this->safe_string($trade, 'book');
        $symbol = $this->safe_symbol($marketId, $market, '_');
        $side = $this->safe_string($trade, 'side');
        $makerSide = $this->safe_string($trade, 'maker_side');
        $takerOrMaker = null;
        if ($side !== null) {
            if ($side === $makerSide) {
                $takerOrMaker = 'maker';
            } else {
                $takerOrMaker = 'taker';
            }
        } else {
            if ($makerSide === 'buy') {
                $side = 'sell';
            } else {
                $side = 'buy';
            }
        }
        $amount = $this->safe_string_2($trade, 'amount', 'major');
        if ($amount !== null) {
            $amount = Precise::string_abs($amount);
        }
        $fee = null;
        $feeCost = $this->safe_string($trade, 'fees_amount');
        if ($feeCost !== null) {
            $feeCurrencyId = $this->safe_string($trade, 'fees_currency');
            $feeCurrency = $this->safe_currency_code($feeCurrencyId);
            $fee = array(
                'cost' => $feeCost,
                'currency' => $feeCurrency,
            );
        }
        $cost = $this->safe_string($trade, 'minor');
        if ($cost !== null) {
            $cost = Precise::string_abs($cost);
        }
        $price = $this->safe_string($trade, 'price');
        $orderId = $this->safe_string($trade, 'oid');
        $id = $this->safe_string($trade, 'tid');
        return $this->safe_trade(array(
            'id' => $id,
            'info' => $trade,
            'timestamp' => $timestamp,
            'datetime' => $this->iso8601($timestamp),
            'symbol' => $symbol,
            'order' => $orderId,
            'type' => null,
            'side' => $side,
            'takerOrMaker' => $takerOrMaker,
            'price' => $price,
            'amount' => $amount,
            'cost' => $cost,
            '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://docs.bitso.com/bitso-api/docs/list-trades
         *
         * @param {string} $symbol unified $symbol of the $market to fetch trades for
         * @param {int} [$since] timestamp in ms of the earliest trade to fetch
         * @param {int} [$limit] the maximum amount of trades to fetch
         * @param {array} [$params] extra parameters specific to the exchange API endpoint
         * @return {Trade[]} a list of ~@link https://docs.ccxt.com/#/?id=public-trades trade structures~
         */
        $this->load_markets();
        $market = $this->market($symbol);
        $request = array(
            'book' => $market['id'],
        );
        $response = $this->publicGetTrades ($this->extend($request, $params));
        return $this->parse_trades($response['payload'], $market, $since, $limit);
    }

    public function fetch_trading_fees($params = array ()): array {
        /**
         * fetch the trading $fees for multiple markets
         *
         * @see https://docs.bitso.com/bitso-api/docs/list-$fees
         *
         * @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->privateGetFees ($params);
        //
        //    {
        //        "success" => true,
        //        "payload" => {
        //            "fees" => array(
        //                array(
        //                    "book" => "btc_mxn",
        //                    "fee_percent" => "0.6500",
        //                    "fee_decimal" => "0.00650000",
        //                    "taker_fee_percent" => "0.6500",
        //                    "taker_fee_decimal" => "0.00650000",
        //                    "maker_fee_percent" => "0.5000",
        //                    "maker_fee_decimal" => "0.00500000",
        //                    "volume_currency" => "mxn",
        //                    "current_volume" => "0.00",
        //                    "next_volume" => "1500000.00",
        //                    "next_maker_fee_percent" => "0.490",
        //                    "next_taker_fee_percent" => "0.637",
        //                    "nextVolume" => "1500000.00",
        //                    "nextFee" => "0.490",
        //                    "nextTakerFee" => "0.637"
        //                ),
        //                ...
        //            ),
        //            "deposit_fees" => array(
        //                array(
        //                    "currency" => "btc",
        //                    "method" => "rewards",
        //                    "fee" => "0.00",
        //                    "is_fixed" => false
        //                ),
        //                ...
        //            ),
        //            "withdrawal_fees" => {
        //                "ada" => "0.20958100",
        //                "bch" => "0.00009437",
        //                "ars" => "0",
        //                "btc" => "0.00001209",
        //                ...
        //            }
        //        }
        //    }
        //
        $payload = $this->safe_value($response, 'payload', array());
        $fees = $this->safe_value($payload, 'fees', array());
        $result = array();
        for ($i = 0; $i < count($fees); $i++) {
            $fee = $fees[$i];
            $marketId = $this->safe_string($fee, 'book');
            $symbol = $this->safe_symbol($marketId, null, '_');
            $result[$symbol] = array(
                'info' => $fee,
                'symbol' => $symbol,
                'maker' => $this->safe_number($fee, 'maker_fee_decimal'),
                'taker' => $this->safe_number($fee, 'taker_fee_decimal'),
                'percentage' => true,
                'tierBased' => true,
            );
        }
        return $result;
    }

    public function fetch_my_trades(?string $symbol = null, ?int $since = null, ?int $limit = 25, $params = array ()) {
        /**
         * fetch all trades made by the user
         *
         * @see https://docs.bitso.com/bitso-api/docs/user-trades
         *
         * @param {string} $symbol unified $market $symbol
         * @param {int} [$since] the earliest time in ms to fetch trades for
         * @param {int} [$limit] the maximum number of trades structures to retrieve
         * @param {array} [$params] extra parameters specific to the exchange API endpoint
         * @return {Trade[]} a list of ~@link https://docs.ccxt.com/#/?id=trade-structure trade structures~
         */
        $this->load_markets();
        $market = $this->market($symbol);
        // the don't support fetching trades starting from a date yet
        // use the `marker` extra param for that
        // this is not a typo, the variable name is 'marker' (don't confuse with 'market')
        $markerInParams = (is_array($params) && array_key_exists('marker', $params));
        // warn the user with an exception if the user wants to filter
        // starting from $since timestamp, but does not set the trade id with an extra 'marker' param
        if (($since !== null) && !$markerInParams) {
            throw new ExchangeError($this->id . ' fetchMyTrades() does not support fetching trades starting from a timestamp with the `$since` argument, use the `marker` extra param to filter starting from an integer trade id');
        }
        // convert it to an integer unconditionally
        if ($markerInParams) {
            $params = $this->extend($params, array(
                'marker' => intval($params['marker']),
            ));
        }
        $request = array(
            'book' => $market['id'],
            'limit' => $limit, // default = 25, max = 100
            // 'sort' => 'desc', // default = desc
            // 'marker' => id, // integer id to start from
        );
        $response = $this->privateGetUserTrades ($this->extend($request, $params));
        return $this->parse_trades($response['payload'], $market, $since, $limit);
    }

    public function create_order(string $symbol, string $type, string $side, float $amount, ?float $price = null, $params = array ()) {
        /**
         * create a trade order
         *
         * @see https://docs.bitso.com/bitso-api/docs/place-an-order
         *
         * @param {string} $symbol unified $symbol of the $market to create an order in
         * @param {string} $type 'market' or 'limit'
         * @param {string} $side 'buy' or 'sell'
         * @param {float} $amount how much of currency you want to trade in units of base currency
         * @param {float} [$price] the $price at which the order is to be fulfilled, in units of the quote currency, ignored in $market orders
         * @param {array} [$params] extra parameters specific to the exchange API endpoint
         * @return {array} an ~@link https://docs.ccxt.com/#/?$id=order-structure order structure~
         */
        $this->load_markets();
        $market = $this->market($symbol);
        $request = array(
            'book' => $market['id'],
            'side' => $side,
            'type' => $type,
            'major' => $this->amount_to_precision($market['symbol'], $amount),
        );
        if ($type === 'limit') {
            $request['price'] = $this->price_to_precision($market['symbol'], $price);
        }
        $response = $this->privatePostOrders ($this->extend($request, $params));
        $id = $this->safe_string($response['payload'], 'oid');
        return $this->safe_order(array(
            'info' => $response,
            'id' => $id,
        ), $market);
    }

    public function cancel_order(string $id, ?string $symbol = null, $params = array ()) {
        /**
         * cancels an open order
         *
         * @see https://docs.bitso.com/bitso-api/docs/cancel-an-order
         *
         * @param {string} $id order $id
         * @param {string} $symbol not used by bitso cancelOrder ()
         * @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(
            'oid' => $id,
        );
        $response = $this->privateDeleteOrdersOid ($this->extend($request, $params));
        //
        //     {
        //         "success" => true,
        //         "payload" => ["yWTQGxDMZ0VimZgZ"]
        //     }
        //
        $payload = $this->safe_list($response, 'payload', array());
        $orderId = $this->safe_string($payload, 0);
        return $this->safe_order(array(
            'info' => $response,
            'id' => $orderId,
        ));
    }

    public function cancel_orders($ids, ?string $symbol = null, $params = array ()): array {
        /**
         * cancel multiple $orders
         *
         * @see https://docs.bitso.com/bitso-api/docs/cancel-an-order
         *
         * @param {string[]} $ids order $ids
         * @param {string} $symbol unified $market $symbol
         * @param {array} [$params] extra parameters specific to the exchange API endpoint
         * @return {array} an list of ~@link https://docs.ccxt.com/#/?$id=order-structure order structures~
         */
        if (gettype($ids) !== 'array' || array_keys($ids) !== array_keys(array_keys($ids))) {
            throw new ArgumentsRequired($this->id . ' cancelOrders() $ids argument should be an array');
        }
        $market = null;
        if ($symbol !== null) {
            $market = $this->market($symbol);
        }
        $oids = implode(',', $ids);
        $request = array(
            'oids' => $oids,
        );
        $response = $this->privateDeleteOrders ($this->extend($request, $params));
        //
        //     {
        //         "success" => true,
        //         "payload" => ["yWTQGxDMZ0VimZgZ"]
        //     }
        //
        $payload = $this->safe_value($response, 'payload', array());
        $orders = array();
        for ($i = 0; $i < count($payload); $i++) {
            $id = $payload[$i];
            $orders[] = $this->parse_order($id, $market);
        }
        return $orders;
    }

    public function cancel_all_orders(?string $symbol = null, $params = array ()): array {
        /**
         * cancel all open orders
         *
         * @see https://docs.bitso.com/bitso-api/docs/cancel-an-$order
         *
         * @param {null} $symbol bitso does not support canceling orders for only a specific market
         * @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~
         */
        if ($symbol !== null) {
            throw new NotSupported($this->id . ' cancelAllOrders() deletes all orders for user, it does not support filtering by $symbol->');
        }
        $response = $this->privateDeleteOrdersAll ($params);
        //
        //     {
        //         "success" => true,
        //         "payload" => ["NWUZUYNT12ljwzDT", "kZUkZmQ2TTjkkYTY"]
        //     }
        //
        $payload = $this->safe_value($response, 'payload', array());
        $canceledOrders = array();
        for ($i = 0; $i < count($payload); $i++) {
            $order = $this->parse_order($payload[$i]);
            $canceledOrders[] = $order;
        }
        return $canceledOrders;
    }

    public function parse_order_status(?string $status) {
        $statuses = array(
            'partial-fill' => 'open', // this is a common substitution in ccxt
            'partially filled' => 'open',
            'queued' => 'open',
            'completed' => 'closed',
        );
        return $this->safe_string($statuses, $status, $status);
    }

    public function parse_order(array $order, ?array $market = null): array {
        //
        //
        // canceledOrder
        // yWTQGxDMZ0VimZgZ
        //
        $id = null;
        if (gettype($order) === 'string') {
            $id = $order;
        } else {
            $id = $this->safe_string($order, 'oid');
        }
        $side = $this->safe_string($order, 'side');
        $status = $this->parse_order_status($this->safe_string($order, 'status'));
        $marketId = $this->safe_string($order, 'book');
        $symbol = $this->safe_symbol($marketId, $market, '_');
        $orderType = $this->safe_string($order, 'type');
        $timestamp = $this->parse8601($this->safe_string($order, 'created_at'));
        $price = $this->safe_string($order, 'price');
        $amount = $this->safe_string($order, 'original_amount');
        $remaining = $this->safe_string($order, 'unfilled_amount');
        $clientOrderId = $this->safe_string($order, 'client_id');
        return $this->safe_order(array(
            'info' => $order,
            'id' => $id,
            'clientOrderId' => $clientOrderId,
            'timestamp' => $timestamp,
            'datetime' => $this->iso8601($timestamp),
            'lastTradeTimestamp' => null,
            'symbol' => $symbol,
            'type' => $orderType,
            'timeInForce' => null,
            'postOnly' => null,
            'side' => $side,
            'price' => $price,
            'triggerPrice' => null,
            'amount' => $amount,
            'cost' => null,
            'remaining' => $remaining,
            'filled' => null,
            'status' => $status,
            'fee' => null,
            'average' => null,
            'trades' => null,
        ), $market);
    }

    public function fetch_open_orders(?string $symbol = null, ?int $since = null, ?int $limit = 25, $params = array ()): array {
        /**
         * fetch all unfilled currently open $orders
         *
         * @see https://docs.bitso.com/bitso-api/docs/list-open-$orders
         *
         * @param {string} $symbol unified $market $symbol
         * @param {int} [$since] the earliest time in ms to fetch open $orders for
         * @param {int} [$limit] the maximum number of  open $orders structures to retrieve
         * @param {array} [$params] extra parameters specific to the exchange API endpoint
         * @return {Order[]} a list of ~@link https://docs.ccxt.com/#/?id=order-structure order structures~
         */
        $this->load_markets();
        $market = $this->market($symbol);
        // the don't support fetching trades starting from a date yet
        // use the `marker` extra param for that
        // this is not a typo, the variable name is 'marker' (don't confuse with 'market')
        $markerInParams = (is_array($params) && array_key_exists('marker', $params));
        // warn the user with an exception if the user wants to filter
        // starting from $since timestamp, but does not set the trade id with an extra 'marker' param
        if (($since !== null) && !$markerInParams) {
            throw new ExchangeError($this->id . ' fetchOpenOrders() does not support fetching $orders starting from a timestamp with the `$since` argument, use the `marker` extra param to filter starting from an integer trade id');
        }
        // convert it to an integer unconditionally
        if ($markerInParams) {
            $params = $this->extend($params, array(
                'marker' => intval($params['marker']),
            ));
        }
        $request = array(
            'book' => $market['id'],
            'limit' => $limit, // default = 25, max = 100
            // 'sort' => 'desc', // default = desc
            // 'marker' => id, // integer id to start from
        );
        $response = $this->privateGetOpenOrders ($this->extend($request, $params));
        $orders = $this->parse_orders($response['payload'], $market, $since, $limit);
        return $orders;
    }

    public function fetch_order(string $id, ?string $symbol = null, $params = array ()) {
        /**
         * fetches information on an order made by the user
         *
         * @see https://docs.bitso.com/bitso-api/docs/look-up-orders
         *
         * @param {string} $id the order $id
         * @param {string} $symbol not used by bitso fetchOrder
         * @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();
        $response = $this->privateGetOrdersOid (array(
            'oid' => $id,
        ));
        $payload = $this->safe_value($response, 'payload');
        if (gettype($payload) === 'array' && array_keys($payload) === array_keys(array_keys($payload))) {
            $numOrders = count($response['payload']);
            if ($numOrders === 1) {
                return $this->parse_order($payload[0]);
            }
        }
        throw new OrderNotFound($this->id . ' => The order ' . $id . ' not found.');
    }

    public function fetch_order_trades(string $id, ?string $symbol = null, ?int $since = null, ?int $limit = null, $params = array ()) {
        /**
         * fetch all the trades made from a single order
         *
         * @see https://docs.bitso.com/bitso-api/docs/list-user-trades
         *
         * @param {string} $id order $id
         * @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 to retrieve
         * @param {array} [$params] extra parameters specific to the exchange API endpoint
         * @return {array[]} a list of ~@link https://docs.ccxt.com/#/?$id=trade-structure trade structures~
         */
        $this->load_markets();
        $market = $this->market($symbol);
        $request = array(
            'oid' => $id,
        );
        $response = $this->privateGetOrderTradesOid ($this->extend($request, $params));
        return $this->parse_trades($response['payload'], $market);
    }

    public function fetch_deposit(string $id, ?string $code = null, $params = array ()) {
        /**
         * fetch information on a deposit
         *
         * @see https://docs.bitso.com/bitso-payouts-funding/docs/fundings
         *
         * @param {string} $id deposit $id
         * @param {string} $code bitso does not support filtering by currency $code and will ignore this argument
         * @param {array} [$params] extra parameters specific to the exchange API endpoint
         * @return {array} a ~@link https://docs.ccxt.com/#/?$id=transaction-structure transaction structure~
         */
        $this->load_markets();
        $request = array(
            'fid' => $id,
        );
        $response = $this->privateGetFundingsFid ($this->extend($request, $params));
        //
        //     {
        //         "success" => true,
        //         "payload" => [{
        //             "fid" => "6112c6369100d6ecceb7f54f17cf0511",
        //             "status" => "complete",
        //             "created_at" => "2022-06-08T12:02:49+0000",
        //             "currency" => "btc",
        //             "method" => "btc",
        //             "method_name" => "Bitcoin",
        //             "amount" => "0.00080000",
        //             "asset" => "btc",
        //             "network" => "btc",
        //             "protocol" => "btc",
        //             "integration" => "bitgo-v2",
        //             "details" => array(
        //                 "receiving_address" => "3N2vbcYKhogs6RoTb4eYCUJ3beRSqLgSif",
        //                 "tx_hash" => "327f3838531f211485ec59f9d0a119fea1595591e274d942b2c10b9b8262eb1d",
        //                 "confirmations" => "4"
        //             }
        //         )]
        //     }
        //
        $transactions = $this->safe_value($response, 'payload', array());
        $first = $this->safe_dict($transactions, 0, array());
        return $this->parse_transaction($first);
    }

    public function fetch_deposits(?string $code = null, ?int $since = null, ?int $limit = null, $params = array ()): array {
        /**
         * fetch all deposits made to an account
         *
         * @see https://docs.bitso.com/bitso-payouts-funding/docs/fundings
         *
         * @param {string} $code unified $currency $code
         * @param {int} [$since] the earliest time in ms to fetch deposits for
         * @param {int} [$limit] the maximum number of deposits 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();
        $currency = null;
        if ($code !== null) {
            $currency = $this->currency($code);
        }
        $response = $this->privateGetFundings ($params);
        //
        //     {
        //         "success" => true,
        //         "payload" => [{
        //             "fid" => "6112c6369100d6ecceb7f54f17cf0511",
        //             "status" => "complete",
        //             "created_at" => "2022-06-08T12:02:49+0000",
        //             "currency" => "btc",
        //             "method" => "btc",
        //             "method_name" => "Bitcoin",
        //             "amount" => "0.00080000",
        //             "asset" => "btc",
        //             "network" => "btc",
        //             "protocol" => "btc",
        //             "integration" => "bitgo-v2",
        //             "details" => array(
        //                 "receiving_address" => "3N2vbcYKhogs6RoTb4eYCUJ3beRSqLgSif",
        //                 "tx_hash" => "327f3838531f211485ec59f9d0a119fea1595591e274d942b2c10b9b8262eb1d",
        //                 "confirmations" => "4"
        //             }
        //         )]
        //     }
        //
        $transactions = $this->safe_list($response, 'payload', array());
        return $this->parse_transactions($transactions, $currency, $since, $limit, $params);
    }

    public function fetch_deposit_address(string $code, $params = array ()): array {
        /**
         * fetch the deposit $address for a $currency associated with this account
         * @param {string} $code unified $currency $code
         * @param {array} [$params] extra parameters specific to the exchange API endpoint
         * @return {array} an ~@link https://docs.ccxt.com/#/?id=$address-structure $address structure~
         */
        $this->load_markets();
        $currency = $this->currency($code);
        $request = array(
            'fund_currency' => $currency['id'],
        );
        $response = $this->privateGetFundingDestination ($this->extend($request, $params));
        $address = $this->safe_string($response['payload'], 'account_identifier');
        $tag = null;
        if (mb_strpos($address, '?dt=') !== false) {
            $parts = explode('?dt=', $address);
            $address = $this->safe_string($parts, 0);
            $tag = $this->safe_string($parts, 1);
        }
        $this->check_address($address);
        return array(
            'info' => $response,
            'currency' => $code,
            'network' => null,
            'address' => $address,
            'tag' => $tag,
        );
    }

    public function fetch_transaction_fees(?array $codes = null, $params = array ()) {
        /**
         * @deprecated
         * please use fetchDepositWithdrawFees instead
         *
         * @see https://docs.bitso.com/bitso-api/docs/list-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->privateGetFees ($params);
        //
        //    {
        //        "success" => true,
        //        "payload" => {
        //            "fees" => array(
        //                array(
        //                    "book" => "btc_mxn",
        //                    "fee_percent" => "0.6500",
        //                    "fee_decimal" => "0.00650000",
        //                    "taker_fee_percent" => "0.6500",
        //                    "taker_fee_decimal" => "0.00650000",
        //                    "maker_fee_percent" => "0.5000",
        //                    "maker_fee_decimal" => "0.00500000",
        //                    "volume_currency" => "mxn",
        //                    "current_volume" => "0.00",
        //                    "next_volume" => "1500000.00",
        //                    "next_maker_fee_percent" => "0.490",
        //                    "next_taker_fee_percent" => "0.637",
        //                    "nextVolume" => "1500000.00",
        //                    "nextFee" => "0.490",
        //                    "nextTakerFee" => "0.637"
        //                ),
        //                ...
        //            ),
        //            "deposit_fees" => array(
        //                array(
        //                    "currency" => "btc",
        //                    "method" => "rewards",
        //                    "fee" => "0.00",
        //                    "is_fixed" => false
        //                ),
        //                ...
        //            ),
        //            "withdrawal_fees" => {
        //                "ada" => "0.20958100",
        //                "bch" => "0.00009437",
        //                "ars" => "0",
        //                "btc" => "0.00001209",
        //                ...
        //            }
        //        }
        //    }
        //
        $result = array();
        $payload = $this->safe_value($response, 'payload', array());
        $depositFees = $this->safe_value($payload, 'deposit_fees', array());
        for ($i = 0; $i < count($depositFees); $i++) {
            $depositFee = $depositFees[$i];
            $currencyId = $this->safe_string($depositFee, 'currency');
            $code = $this->safe_currency_code($currencyId);
            if (($codes !== null) && !$this->in_array($code, $codes)) {
                continue;
            }
            $result[$code] = array(
                'deposit' => $this->safe_number($depositFee, 'fee'),
                'withdraw' => null,
                'info' => array(
                    'deposit' => $depositFee,
                    'withdraw' => null,
                ),
            );
        }
        $withdrawalFees = $this->safe_value($payload, 'withdrawal_fees', array());
        $currencyIds = is_array($withdrawalFees) ? array_keys($withdrawalFees) : array();
        for ($i = 0; $i < count($currencyIds); $i++) {
            $currencyId = $currencyIds[$i];
            $code = $this->safe_currency_code($currencyId);
            if (($codes !== null) && !$this->in_array($code, $codes)) {
                continue;
            }
            $result[$code] = array(
                'deposit' => $this->safe_value($result[$code], 'deposit'),
                'withdraw' => $this->safe_number($withdrawalFees, $currencyId),
                'info' => array(
                    'deposit' => $this->safe_value($result[$code]['info'], 'deposit'),
                    'withdraw' => $this->safe_number($withdrawalFees, $currencyId),
                ),
            );
        }
        return $result;
    }

    public function fetch_deposit_withdraw_fees(?array $codes = null, $params = array ()) {
        /**
         * fetch deposit and withdraw fees
         *
         * @see https://docs.bitso.com/bitso-api/docs/list-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->privateGetFees ($params);
        //
        //    {
        //        "success" => true,
        //        "payload" => {
        //            "fees" => array(
        //                array(
        //                    "book" => "btc_mxn",
        //                    "fee_percent" => "0.6500",
        //                    "fee_decimal" => "0.00650000",
        //                    "taker_fee_percent" => "0.6500",
        //                    "taker_fee_decimal" => "0.00650000",
        //                    "maker_fee_percent" => "0.5000",
        //                    "maker_fee_decimal" => "0.00500000",
        //                    "volume_currency" => "mxn",
        //                    "current_volume" => "0.00",
        //                    "next_volume" => "1500000.00",
        //                    "next_maker_fee_percent" => "0.490",
        //                    "next_taker_fee_percent" => "0.637",
        //                    "nextVolume" => "1500000.00",
        //                    "nextFee" => "0.490",
        //                    "nextTakerFee" => "0.637"
        //                ),
        //                ...
        //            ),
        //            "deposit_fees" => array(
        //                array(
        //                    "currency" => "btc",
        //                    "method" => "rewards",
        //                    "fee" => "0.00",
        //                    "is_fixed" => false
        //                ),
        //                ...
        //            ),
        //            "withdrawal_fees" => {
        //                "ada" => "0.20958100",
        //                "bch" => "0.00009437",
        //                "ars" => "0",
        //                "btc" => "0.00001209",
        //                ...
        //            }
        //        }
        //    }
        //
        $payload = $this->safe_list($response, 'payload', array());
        return $this->parse_deposit_withdraw_fees($payload, $codes);
    }

    public function parse_deposit_withdraw_fees($response, $codes = null, $currencyIdKey = null) {
        //
        //    {
        //        "fees" => array(
        //            array(
        //                "book" => "btc_mxn",
        //                "fee_percent" => "0.6500",
        //                "fee_decimal" => "0.00650000",
        //                "taker_fee_percent" => "0.6500",
        //                "taker_fee_decimal" => "0.00650000",
        //                "maker_fee_percent" => "0.5000",
        //                "maker_fee_decimal" => "0.00500000",
        //                "volume_currency" => "mxn",
        //                "current_volume" => "0.00",
        //                "next_volume" => "1500000.00",
        //                "next_maker_fee_percent" => "0.490",
        //                "next_taker_fee_percent" => "0.637",
        //                "nextVolume" => "1500000.00",
        //                "nextFee" => "0.490",
        //                "nextTakerFee" => "0.637"
        //            ),
        //            ...
        //        ),
        //        "deposit_fees" => array(
        //            array(
        //                "currency" => "btc",
        //                "method" => "rewards",
        //                "fee" => "0.00",
        //                "is_fixed" => false
        //            ),
        //            ...
        //        ),
        //        "withdrawal_fees" => {
        //            "ada" => "0.20958100",
        //            "bch" => "0.00009437",
        //            "ars" => "0",
        //            "btc" => "0.00001209",
        //            ...
        //        }
        //    }
        //
        $result = array();
        $depositResponse = $this->safe_value($response, 'deposit_fees', array());
        $withdrawalResponse = $this->safe_value($response, 'withdrawal_fees', array());
        for ($i = 0; $i < count($depositResponse); $i++) {
            $entry = $depositResponse[$i];
            $currencyId = $this->safe_string($entry, 'currency');
            $code = $this->safe_currency_code($currencyId);
            if (($codes === null) || (is_array($codes) && array_key_exists($code, $codes))) {
                $result[$code] = array(
                    'deposit' => array(
                        'fee' => $this->safe_number($entry, 'fee'),
                        'percentage' => !$this->safe_value($entry, 'is_fixed'),
                    ),
                    'withdraw' => array(
                        'fee' => null,
                        'percentage' => null,
                    ),
                    'networks' => array(),
                    'info' => $entry,
                );
            }
        }
        $withdrawalKeys = is_array($withdrawalResponse) ? array_keys($withdrawalResponse) : array();
        for ($i = 0; $i < count($withdrawalKeys); $i++) {
            $currencyId = $withdrawalKeys[$i];
            $code = $this->safe_currency_code($currencyId);
            if (($codes === null) || (is_array($codes) && array_key_exists($code, $codes))) {
                $withdrawFee = $this->parse_number($withdrawalResponse[$currencyId]);
                $resultValue = $this->safe_value($result, $code);
                if ($resultValue === null) {
                    $result[$code] = $this->deposit_withdraw_fee(array());
                }
                $result[$code]['withdraw']['fee'] = $withdrawFee;
                $result[$code]['info'][$code] = $withdrawFee;
            }
        }
        return $result;
    }

    public function withdraw(string $code, float $amount, string $address, ?string $tag = null, $params = array ()): array {
        /**
         * make a withdrawal
         * @param {string} $code unified $currency $code
         * @param {float} $amount the $amount to withdraw
         * @param {string} $address the $address to withdraw to
         * @param {string} $tag
         * @param {array} [$params] extra parameters specific to the exchange API endpoint
         * @return {array} a ~@link https://docs.ccxt.com/#/?id=transaction-structure transaction structure~
         */
        list($tag, $params) = $this->handle_withdraw_tag_and_params($tag, $params);
        $this->check_address($address);
        $this->load_markets();
        $methods = array(
            'BTC' => 'Bitcoin',
            'ETH' => 'Ether',
            'XRP' => 'Ripple',
            'BCH' => 'Bcash',
            'LTC' => 'Litecoin',
        );
        $currency = $this->currency($code);
        $method = (is_array($methods) && array_key_exists($code, $methods)) ? $methods[$code] : null;
        if ($method === null) {
            throw new ExchangeError($this->id . ' not valid withdraw coin => ' . $code);
        }
        $request = array(
            'amount' => $amount,
            'address' => $address,
            'destination_tag' => $tag,
        );
        $classMethod = 'privatePost' . $method . 'Withdrawal';
        $response = $this->$classMethod ($this->extend($request, $params));
        //
        //     {
        //         "success" => true,
        //         "payload" => array(
        //             {
        //                 "wid" => "c5b8d7f0768ee91d3b33bee648318688",
        //                 "status" => "pending",
        //                 "created_at" => "2016-04-08T17:52:31.000+00:00",
        //                 "currency" => "btc",
        //                 "method" => "Bitcoin",
        //                 "amount" => "0.48650929",
        //                 "details" => array(
        //                     "withdrawal_address" => "18MsnATiNiKLqUHDTRKjurwMg7inCrdNEp",
        //                     "tx_hash" => "d4f28394693e9fb5fffcaf730c11f32d1922e5837f76ca82189d3bfe30ded433"
        //                 }
        //             ),
        //         )
        //     }
        //
        $payload = $this->safe_value($response, 'payload', array());
        $first = $this->safe_dict($payload, 0);
        return $this->parse_transaction($first, $currency);
    }

    public function parse_transaction(array $transaction, ?array $currency = null): array {
        //
        // deposit
        //     {
        //         "fid" => "6112c6369100d6ecceb7f54f17cf0511",
        //         "status" => "complete",
        //         "created_at" => "2022-06-08T12:02:49+0000",
        //         "currency" => "btc",
        //         "method" => "btc",
        //         "method_name" => "Bitcoin",
        //         "amount" => "0.00080000",
        //         "asset" => "btc",
        //         "network" => "btc",
        //         "protocol" => "btc",
        //         "integration" => "bitgo-v2",
        //         "details" => {
        //             "receiving_address" => "3NmvbcYKhogs6RoTb4eYCUJ3beRSqLgSif",
        //             "tx_hash" => "327f3838531f611485ec59f9d0a119fea1595591e274d942b2c10b9b8262eb1d",
        //             "confirmations" => "4"
        //         }
        //     }
        //
        // withdraw
        //
        //     {
        //         "wid" => "c5b8d7f0768ee91d3b33bee648318688",
        //         "status" => "pending",
        //         "created_at" => "2016-04-08T17:52:31.000+00:00",
        //         "currency" => "btc",
        //         "method" => "Bitcoin",
        //         "amount" => "0.48650929",
        //         "details" => {
        //             "withdrawal_address" => "18MsnATiNiKLqUHDTRKjurwMg7inCrdNEp",
        //             "tx_hash" => "d4f28394693e9fb5fffcaf730c11f32d1922e5837f76ca82189d3bfe30ded433"
        //         }
        //     }
        //
        $currencyId = $this->safe_string_2($transaction, 'currency', 'asset');
        $currency = $this->safe_currency($currencyId, $currency);
        $details = $this->safe_value($transaction, 'details', array());
        $datetime = $this->safe_string($transaction, 'created_at');
        $withdrawalAddress = $this->safe_string($details, 'withdrawal_address');
        $receivingAddress = $this->safe_string($details, 'receiving_address');
        $networkId = $this->safe_string_2($transaction, 'network', 'method');
        $status = $this->safe_string($transaction, 'status');
        $withdrawId = $this->safe_string($transaction, 'wid');
        $networkCode = $this->network_id_to_code($networkId);
        $networkCodeUpper = ($networkCode !== null) ? strtoupper($networkCode) : null;
        return array(
            'id' => $this->safe_string_2($transaction, 'wid', 'fid'),
            'txid' => $this->safe_string($details, 'tx_hash'),
            'timestamp' => $this->parse8601($datetime),
            'datetime' => $datetime,
            'network' => $networkCodeUpper,
            'addressFrom' => $receivingAddress,
            'address' => ($withdrawalAddress !== null) ? $withdrawalAddress : $receivingAddress,
            'addressTo' => $withdrawalAddress,
            'amount' => $this->safe_number($transaction, 'amount'),
            'type' => ($withdrawId === null) ? 'deposit' : 'withdrawal',
            'currency' => $this->safe_currency_code($currencyId, $currency),
            'status' => $this->parse_transaction_status($status),
            'updated' => null,
            'tagFrom' => null,
            'tag' => null,
            'tagTo' => null,
            'comment' => null,
            'internal' => null,
            'fee' => null,
            'info' => $transaction,
        );
    }

    public function parse_transaction_status(?string $status) {
        $statuses = array(
            'pending' => 'pending',
            'in_progress' => 'pending',
            'complete' => 'ok',
            'failed' => '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) {
        $endpoint = '/' . $this->version . '/' . $this->implode_params($path, $params);
        $query = $this->omit($params, $this->extract_params($path));
        if ($method === 'GET' || $method === 'DELETE') {
            if ($query) {
                $endpoint .= '?' . $this->urlencode($query);
            }
        }
        $url = $this->urls['api']['rest'] . $endpoint;
        if ($api === 'private') {
            $this->check_required_credentials();
            $nonce = (string) $this->nonce();
            $endpoint = '/api' . $endpoint;
            $request = implode('', array($nonce, $method, $endpoint));
            if ($method !== 'GET' && $method !== 'DELETE') {
                if ($query) {
                    $body = $this->json($query);
                    $request .= $body;
                }
            }
            $signature = $this->hmac($this->encode($request), $this->encode($this->secret), 'sha256');
            $auth = $this->apiKey . ':' . $nonce . ':' . $signature;
            $headers = array(
                'Authorization' => 'Bitso ' . $auth,
                // 'Content-Type' => 'application/json',
            );
        }
        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; // fallback to default $error handler
        }
        if (is_array($response) && array_key_exists('success', $response)) {
            //
            //     array("success":false,"error":array("code":104,"message":"Cannot perform request - nonce must be higher than 1520307203724237"))
            //
            $success = $this->safe_bool($response, 'success', false);
            if (gettype($success) === 'string') {
                if (($success === 'true') || ($success === '1')) {
                    $success = true;
                } else {
                    $success = false;
                }
            }
            if (!$success) {
                $feedback = $this->id . ' ' . $this->json($response);
                $error = $this->safe_value($response, 'error');
                if ($error === null) {
                    throw new ExchangeError($feedback);
                }
                $code = $this->safe_string($error, 'code');
                $this->throw_exactly_matched_exception($this->exceptions, $code, $feedback);
                throw new ExchangeError($feedback);
            }
        }
        return null;
    }
}
