# -*- coding: utf-8 -*-

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

from ccxt_zagrava.async_support.base.exchange import Exchange
from ccxt_zagrava.abstract.coinbaseexchange import ImplicitAPI
import hashlib
from ccxt_zagrava.base.types import Account, Balances, Currencies, Currency, Int, Market, Num, Order, OrderBook, OrderSide, OrderType, Str, Strings, Ticker, Tickers, Trade, TradingFees, Transaction
from typing import List
from ccxt_zagrava.base.errors import ExchangeError
from ccxt_zagrava.base.errors import AuthenticationError
from ccxt_zagrava.base.errors import PermissionDenied
from ccxt_zagrava.base.errors import ArgumentsRequired
from ccxt_zagrava.base.errors import InsufficientFunds
from ccxt_zagrava.base.errors import InvalidAddress
from ccxt_zagrava.base.errors import InvalidOrder
from ccxt_zagrava.base.errors import OrderNotFound
from ccxt_zagrava.base.errors import RateLimitExceeded
from ccxt_zagrava.base.errors import OnMaintenance
from ccxt_zagrava.base.decimal_to_precision import TICK_SIZE
from ccxt_zagrava.base.precise import Precise


class coinbaseexchange(Exchange, ImplicitAPI):

    def describe(self):
        return self.deep_extend(super(coinbaseexchange, self).describe(), {
            'id': 'coinbaseexchange',
            'name': 'Coinbase Exchange',
            'countries': ['US'],
            'rateLimit': 100,
            'userAgent': self.userAgents['chrome'],
            'pro': True,
            'has': {
                'CORS': True,
                'spot': True,
                'margin': False,
                'swap': False,
                'future': False,
                'option': False,
                'cancelAllOrders': True,
                'cancelOrder': True,
                'createDepositAddress': True,
                'createOrder': True,
                'createStopLimitOrder': True,
                'createStopMarketOrder': True,
                'createStopOrder': True,
                'fetchAccounts': True,
                'fetchBalance': True,
                'fetchClosedOrders': True,
                'fetchCurrencies': True,
                'fetchDepositAddress': False,  # the exchange does not have self method, only createDepositAddress, see https://github.com/ccxt_zagrava/ccxt_zagrava/pull/7405
                'fetchDeposits': True,
                'fetchDepositsWithdrawals': True,
                'fetchFundingRate': False,
                'fetchLedger': True,
                'fetchMarginMode': False,
                'fetchMarkets': True,
                'fetchMyTrades': True,
                'fetchOHLCV': True,
                'fetchOpenOrders': True,
                'fetchOrder': True,
                'fetchOrderBook': True,
                'fetchOrders': True,
                'fetchOrderTrades': True,
                'fetchPosition': False,
                'fetchPositionHistory': False,
                'fetchPositionMode': False,
                'fetchPositions': False,
                'fetchPositionsForSymbol': False,
                'fetchPositionsHistory': False,
                'fetchPositionsRisk': False,
                'fetchTicker': True,
                'fetchTickers': True,
                'fetchTime': True,
                'fetchTrades': True,
                'fetchTradingFee': False,
                'fetchTradingFees': True,
                'fetchTransactions': 'emulated',
                'fetchWithdrawals': True,
                'withdraw': True,
            },
            'timeframes': {
                '1m': 60,
                '5m': 300,
                '15m': 900,
                '1h': 3600,
                '6h': 21600,
                '1d': 86400,
            },
            'hostname': 'exchange.coinbase.com',
            'urls': {
                'test': {
                    'public': 'https://api-public.sandbox.exchange.coinbase.com',
                    'private': 'https://api-public.sandbox.exchange.coinbase.com',
                },
                'logo': 'https://github.com/ccxt_zagrava/ccxt_zagrava/assets/43336371/34a65553-88aa-4a38-a714-064bd228b97e',
                'api': {
                    'public': 'https://api.{hostname}',
                    'private': 'https://api.{hostname}',
                },
                'www': 'https://coinbase.com/',
                'doc': 'https://docs.cloud.coinbase.com/exchange/docs/',
                'fees': [
                    'https://docs.pro.coinbase.com/#fees',
                    'https://support.pro.coinbase.com/customer/en/portal/articles/2945310-fees',
                ],
            },
            'requiredCredentials': {
                'apiKey': True,
                'secret': True,
                'password': True,
            },
            'api': {
                'public': {
                    'get': [
                        'currencies',
                        'products',
                        'products/{id}',
                        'products/{id}/book',
                        'products/{id}/candles',
                        'products/{id}/stats',
                        'products/{id}/ticker',
                        'products/{id}/trades',
                        'time',
                        'products/spark-lines',  # experimental
                    ],
                },
                'private': {
                    'get': [
                        'address-book',
                        'accounts',
                        'accounts/{id}',
                        'accounts/{id}/holds',
                        'accounts/{id}/ledger',
                        'accounts/{id}/transfers',
                        'coinbase-accounts',
                        'fills',
                        'funding',
                        'fees',
                        'margin/profile_information',
                        'margin/buying_power',
                        'margin/withdrawal_power',
                        'margin/withdrawal_power_all',
                        'margin/exit_plan',
                        'margin/liquidation_history',
                        'margin/position_refresh_amounts',
                        'margin/status',
                        'oracle',
                        'orders',
                        'orders/{id}',
                        'orders/client:{client_oid}',
                        'otc/orders',
                        'payment-methods',
                        'position',
                        'profiles',
                        'profiles/{id}',
                        'reports/{report_id}',
                        'transfers',
                        'transfers/{transfer_id}',
                        'users/self/exchange-limits',
                        'users/self/hold-balances',
                        'users/self/trailing-volume',
                        'withdrawals/fee-estimate',
                        'conversions/{conversion_id}',
                        'conversions/fees',
                    ],
                    'post': [
                        'conversions',
                        'deposits/coinbase-account',
                        'deposits/payment-method',
                        'coinbase-accounts/{id}/addresses',
                        'funding/repay',
                        'orders',
                        'position/close',
                        'profiles/margin-transfer',
                        'profiles/transfer',
                        'reports',
                        'withdrawals/coinbase',
                        'withdrawals/coinbase-account',
                        'withdrawals/crypto',
                        'withdrawals/payment-method',
                    ],
                    'delete': [
                        'orders',
                        'orders/client:{client_oid}',
                        'orders/{id}',
                    ],
                },
            },
            'commonCurrencies': {
                'CGLD': 'CELO',
            },
            'precisionMode': TICK_SIZE,
            'fees': {
                'trading': {
                    'tierBased': True,  # complicated tier system per coin
                    'percentage': True,
                    'maker': self.parse_number('0.004'),  # highest fee of all tiers
                    'taker': self.parse_number('0.006'),  # highest fee of all tiers
                },
                'funding': {
                    'tierBased': False,
                    'percentage': False,
                    'withdraw': {
                        'BCH': 0,
                        'BTC': 0,
                        'LTC': 0,
                        'ETH': 0,
                        'EUR': 0.15,
                        'USD': 25,
                    },
                    'deposit': {
                        'BCH': 0,
                        'BTC': 0,
                        'LTC': 0,
                        'ETH': 0,
                        'EUR': 0.15,
                        'USD': 10,
                    },
                },
            },
            'exceptions': {
                'exact': {
                    'Insufficient funds': InsufficientFunds,
                    'NotFound': OrderNotFound,
                    'Invalid API Key': AuthenticationError,
                    'invalid signature': AuthenticationError,
                    'Invalid Passphrase': AuthenticationError,
                    'Invalid order id': InvalidOrder,
                    'Private rate limit exceeded': RateLimitExceeded,
                    'Trading pair not available': PermissionDenied,
                    'Product not found': InvalidOrder,
                },
                'broad': {
                    'Order already done': OrderNotFound,
                    'order not found': OrderNotFound,
                    'price too small': InvalidOrder,
                    'price too precise': InvalidOrder,
                    'under maintenance': OnMaintenance,
                    'size is too small': InvalidOrder,
                    'Cancel only mode': OnMaintenance,  # https://github.com/ccxt_zagrava/ccxt_zagrava/issues/7690
                },
            },
        })

    async def fetch_currencies(self, params={}) -> Currencies:
        """
        fetches all available currencies on an exchange
        :see: https://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_getcurrencies
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :returns dict: an associative dictionary of currencies
        """
        response = await self.publicGetCurrencies(params)
        #
        #     [
        #         {
        #             "id": "XTZ",
        #             "name": "Tezos",
        #             "min_size": "0.000001",
        #             "status": "online",
        #             "message": '',
        #             "max_precision": "0.000001",
        #             "convertible_to": [],
        #             "details": {
        #                 "type": "crypto",
        #                 "symbol": "Τ",
        #                 "network_confirmations": 60,
        #                 "sort_order": 53,
        #                 "crypto_address_link": "https://tzstats.com/{{address}}",
        #                 "crypto_transaction_link": "https://tzstats.com/{{txId}}",
        #                 "push_payment_methods": ["crypto"],
        #                 "group_types": [],
        #                 "display_name": '',
        #                 "processing_time_seconds": 0,
        #                 "min_withdrawal_amount": 1
        #             }
        #         }
        #     ]
        #
        result: dict = {}
        for i in range(0, len(response)):
            currency = response[i]
            id = self.safe_string(currency, 'id')
            name = self.safe_string(currency, 'name')
            code = self.safe_currency_code(id)
            details = self.safe_value(currency, 'details', {})
            status = self.safe_string(currency, 'status')
            active = (status == 'online')
            result[code] = {
                'id': id,
                'code': code,
                'info': currency,
                'type': self.safe_string(details, 'type'),
                'name': name,
                'active': active,
                'deposit': None,
                'withdraw': None,
                'fee': None,
                'precision': self.safe_number(currency, 'max_precision'),
                'limits': {
                    'amount': {
                        'min': self.safe_number(details, 'min_size'),
                        'max': None,
                    },
                    'withdraw': {
                        'min': self.safe_number(details, 'min_withdrawal_amount'),
                        'max': None,
                    },
                },
                'networks': {},
            }
        return result

    async def fetch_markets(self, params={}) -> List[Market]:
        """
        retrieves data on all markets for coinbaseexchange
        :see: https://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_getproducts
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :returns dict[]: an array of objects representing market data
        """
        response = await self.publicGetProducts(params)
        #
        #     [
        #         {
        #             "id": "BTCAUCTION-USD",
        #             "base_currency": "BTC",
        #             "quote_currency": "USD",
        #             "base_min_size": "0.000016",
        #             "base_max_size": "1500",
        #             "quote_increment": "0.01",
        #             "base_increment": "0.00000001",
        #             "display_name": "BTCAUCTION/USD",
        #             "min_market_funds": "1",
        #             "max_market_funds": "20000000",
        #             "margin_enabled": False,
        #             "fx_stablecoin": False,
        #             "max_slippage_percentage": "0.02000000",
        #             "post_only": False,
        #             "limit_only": False,
        #             "cancel_only": True,
        #             "trading_disabled": False,
        #             "status": "online",
        #             "status_message": '',
        #             "auction_mode": False
        #         },
        #         {
        #             "id": "BTC-USD",
        #             "base_currency": "BTC",
        #             "quote_currency": "USD",
        #             "base_min_size": "0.000016",
        #             "base_max_size": "1500",
        #             "quote_increment": "0.01",
        #             "base_increment": "0.00000001",
        #             "display_name": "BTC/USD",
        #             "min_market_funds": "1",
        #             "max_market_funds": "20000000",
        #             "margin_enabled": False,
        #             "fx_stablecoin": False,
        #             "max_slippage_percentage": "0.02000000",
        #             "post_only": False,
        #             "limit_only": False,
        #             "cancel_only": False,
        #             "trading_disabled": False,
        #             "status": "online",
        #             "status_message": '',
        #             "auction_mode": False
        #         }
        #     ]
        #
        result = []
        for i in range(0, len(response)):
            market = response[i]
            id = self.safe_string(market, 'id')
            baseId, quoteId = id.split('-')
            # BTCAUCTION-USD vs BTC-USD conflict workaround, see the output sample above
            # baseId = self.safe_string(market, 'base_currency')
            # quoteId = self.safe_string(market, 'quote_currency')
            base = self.safe_currency_code(baseId)
            quote = self.safe_currency_code(quoteId)
            status = self.safe_string(market, 'status')
            result.append(self.extend(self.fees['trading'], {
                'id': id,
                'symbol': base + '/' + quote,
                'base': base,
                'quote': quote,
                'settle': None,
                'baseId': baseId,
                'quoteId': quoteId,
                'settleId': None,
                'type': 'spot',
                'spot': True,
                'margin': self.safe_value(market, 'margin_enabled'),
                'swap': False,
                'future': False,
                'option': False,
                'active': (status == 'online'),
                'contract': False,
                'linear': None,
                'inverse': None,
                'contractSize': None,
                'expiry': None,
                'expiryDatetime': None,
                'strike': None,
                'optionType': None,
                'precision': {
                    'amount': self.safe_number(market, 'base_increment'),
                    'price': self.safe_number(market, 'quote_increment'),
                },
                'limits': {
                    'leverage': {
                        'min': None,
                        'max': None,
                    },
                    'amount': {
                        'min': None,
                        'max': None,
                    },
                    'price': {
                        'min': None,
                        'max': None,
                    },
                    'cost': {
                        'min': self.safe_number(market, 'min_market_funds'),
                        'max': None,
                    },
                },
                'created': None,
                'info': market,
            }))
        return result

    async def fetch_accounts(self, params={}) -> List[Account]:
        """
        fetch all the accounts associated with a profile
        :see: https://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_getaccounts
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :returns dict: a dictionary of `account structures <https://docs.ccxt_zagrava.com/#/?id=account-structure>` indexed by the account type
        """
        await self.load_markets()
        response = await self.privateGetAccounts(params)
        #
        #     [
        #         {
        #             "id": "4aac9c60-cbda-4396-9da4-4aa71e95fba0",
        #             "currency": "BTC",
        #             "balance": "0.0000000000000000",
        #             "available": "0",
        #             "hold": "0.0000000000000000",
        #             "profile_id": "b709263e-f42a-4c7d-949a-a95c83d065da"
        #         },
        #         {
        #             "id": "f75fa69a-1ad1-4a80-bd61-ee7faa6135a3",
        #             "currency": "USDC",
        #             "balance": "0.0000000000000000",
        #             "available": "0",
        #             "hold": "0.0000000000000000",
        #             "profile_id": "b709263e-f42a-4c7d-949a-a95c83d065da"
        #         },
        #     ]
        #
        return self.parse_accounts(response, params)

    def parse_account(self, account):
        #
        #     {
        #         "id": "4aac9c60-cbda-4396-9da4-4aa71e95fba0",
        #         "currency": "BTC",
        #         "balance": "0.0000000000000000",
        #         "available": "0",
        #         "hold": "0.0000000000000000",
        #         "profile_id": "b709263e-f42a-4c7d-949a-a95c83d065da"
        #     }
        #
        currencyId = self.safe_string(account, 'currency')
        return {
            'id': self.safe_string(account, 'id'),
            'type': None,
            'code': self.safe_currency_code(currencyId),
            'info': account,
        }

    def parse_balance(self, response) -> Balances:
        result: dict = {'info': response}
        for i in range(0, len(response)):
            balance = response[i]
            currencyId = self.safe_string(balance, 'currency')
            code = self.safe_currency_code(currencyId)
            account = self.account()
            account['free'] = self.safe_string(balance, 'available')
            account['used'] = self.safe_string(balance, 'hold')
            account['total'] = self.safe_string(balance, 'balance')
            result[code] = account
        return self.safe_balance(result)

    async def fetch_balance(self, params={}) -> Balances:
        """
        query for balance and get the amount of funds available for trading or funds locked in orders
        :see: https://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_getaccounts
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :returns dict: a `balance structure <https://docs.ccxt_zagrava.com/#/?id=balance-structure>`
        """
        await self.load_markets()
        response = await self.privateGetAccounts(params)
        return self.parse_balance(response)

    async def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook:
        """
        :see: https://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_getproductbook
        fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data
        :param str 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 dict [params]: extra parameters specific to the exchange API endpoint
        :returns dict: A dictionary of `order book structures <https://docs.ccxt_zagrava.com/#/?id=order-book-structure>` indexed by market symbols
        """
        await self.load_markets()
        # level 1 - only the best bid and ask
        # level 2 - top 50 bids and asks(aggregated)
        # level 3 - full order book(non aggregated)
        request: dict = {
            'id': self.market_id(symbol),
            'level': 2,  # 1 best bidask, 2 aggregated, 3 full
        }
        response = await self.publicGetProductsIdBook(self.extend(request, params))
        #
        #     {
        #         "sequence":1924393896,
        #         "bids":[
        #             ["0.01825","24.34811287",2],
        #             ["0.01824","72.5463",3],
        #             ["0.01823","424.54298049",6],
        #         ],
        #         "asks":[
        #             ["0.01826","171.10414904",4],
        #             ["0.01827","22.60427028",1],
        #             ["0.01828","397.46018784",7],
        #         ]
        #     }
        #
        orderbook = self.parse_order_book(response, symbol)
        orderbook['nonce'] = self.safe_integer(response, 'sequence')
        return orderbook

    def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker:
        #
        # fetchTickers
        #
        #      [
        #         1639472400,  # timestamp
        #         4.26,  # low
        #         4.38,  # high
        #         4.35,  # open
        #         4.27  # close
        #      ]
        #
        # fetchTicker
        #
        #     publicGetProductsIdTicker
        #
        #     {
        #         "trade_id":843439,
        #         "price":"0.997999",
        #         "size":"80.29769",
        #         "time":"2020-01-28T02:13:33.012523Z",
        #         "bid":"0.997094",
        #         "ask":"0.998",
        #         "volume":"1903188.03750000"
        #     }
        #
        #     publicGetProductsIdStats
        #
        #     {
        #         "open": "34.19000000",
        #         "high": "95.70000000",
        #         "low": "7.06000000",
        #         "volume": "2.41000000"
        #     }
        #
        timestamp = None
        bid = None
        ask = None
        last = None
        high = None
        low = None
        open = None
        volume = None
        symbol = None if (market is None) else market['symbol']
        if isinstance(ticker, list):
            last = self.safe_string(ticker, 4)
            timestamp = self.milliseconds()
        else:
            timestamp = self.parse8601(self.safe_value(ticker, 'time'))
            bid = self.safe_string(ticker, 'bid')
            ask = self.safe_string(ticker, 'ask')
            high = self.safe_string(ticker, 'high')
            low = self.safe_string(ticker, 'low')
            open = self.safe_string(ticker, 'open')
            last = self.safe_string_2(ticker, 'price', 'last')
            volume = self.safe_string(ticker, 'volume')
        return self.safe_ticker({
            'symbol': symbol,
            'timestamp': timestamp,
            'datetime': self.iso8601(timestamp),
            'high': high,
            'low': low,
            'bid': bid,
            'bidVolume': None,
            'ask': ask,
            'askVolume': None,
            'vwap': None,
            'open': open,
            'close': last,
            'last': last,
            'previousClose': None,
            'change': None,
            'percentage': None,
            'average': None,
            'baseVolume': volume,
            'quoteVolume': None,
            'info': ticker,
        }, market)

    async def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers:
        """
        fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market
        :see: https://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_getproduct
        :param str[]|None symbols: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :returns dict: a dictionary of `ticker structures <https://docs.ccxt_zagrava.com/#/?id=ticker-structure>`
        """
        await self.load_markets()
        symbols = self.market_symbols(symbols)
        request: dict = {}
        response = await self.publicGetProductsSparkLines(self.extend(request, params))
        #
        #     {
        #         YYY-USD: [
        #             [
        #                 1639472400,  # timestamp
        #                 4.26,  # low
        #                 4.38,  # high
        #                 4.35,  # open
        #                 4.27  # close
        #             ],
        #             [
        #                 1639468800,
        #                 4.31,
        #                 4.45,
        #                 4.35,
        #                 4.35
        #             ],
        #         ]
        #     }
        #
        result: dict = {}
        marketIds = list(response.keys())
        delimiter = '-'
        for i in range(0, len(marketIds)):
            marketId = marketIds[i]
            entry = self.safe_value(response, marketId, [])
            first = self.safe_value(entry, 0, [])
            market = self.safe_market(marketId, None, delimiter)
            symbol = market['symbol']
            result[symbol] = self.parse_ticker(first, market)
        return self.filter_by_array_tickers(result, 'symbol', symbols)

    async def fetch_ticker(self, symbol: str, params={}) -> Ticker:
        """
        :see: https://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_getproductticker
        fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market
        :param str symbol: unified symbol of the market to fetch the ticker for
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :returns dict: a `ticker structure <https://docs.ccxt_zagrava.com/#/?id=ticker-structure>`
        """
        await self.load_markets()
        market = self.market(symbol)
        request: dict = {
            'id': market['id'],
        }
        # publicGetProductsIdTicker or publicGetProductsIdStats
        method = self.safe_string(self.options, 'fetchTickerMethod', 'publicGetProductsIdTicker')
        response = await getattr(self, method)(self.extend(request, params))
        #
        # publicGetProductsIdTicker
        #
        #     {
        #         "trade_id":843439,
        #         "price":"0.997999",
        #         "size":"80.29769",
        #         "time":"2020-01-28T02:13:33.012523Z",
        #         "bid":"0.997094",
        #         "ask":"0.998",
        #         "volume":"1903188.03750000"
        #     }
        #
        # publicGetProductsIdStats
        #
        #     {
        #         "open": "34.19000000",
        #         "high": "95.70000000",
        #         "low": "7.06000000",
        #         "volume": "2.41000000"
        #     }
        #
        return self.parse_ticker(response, market)

    def parse_trade(self, trade: dict, market: Market = None) -> Trade:
        #
        #     {
        #         "type": "match",
        #         "trade_id": 82047307,
        #         "maker_order_id": "0f358725-2134-435e-be11-753912a326e0",
        #         "taker_order_id": "252b7002-87a3-425c-ac73-f5b9e23f3caf",
        #         "order_id": "d50ec984-77a8-460a-b958-66f114b0de9b",
        #         "side": "sell",
        #         "size": "0.00513192",
        #         "price": "9314.78",
        #         "product_id": "BTC-USD",
        #         "profile_id": "6244401d-c078-40d9-b305-7ad3551bc3b0",
        #         "sequence": 12038915443,
        #         "time": "2020-01-31T20:03:41.158814Z"
        #         "created_at": "2014-11-07T22:19:28.578544Z",
        #         "liquidity": "T",
        #         "fee": "0.00025",
        #         "settled": True,
        #         "usd_volume": "0.0924556000000000",
        #         "user_id": "595eb864313c2b02ddf2937d"
        #     }
        #
        timestamp = self.parse8601(self.safe_string_2(trade, 'time', 'created_at'))
        marketId = self.safe_string(trade, 'product_id')
        market = self.safe_market(marketId, market, '-')
        feeRate = None
        takerOrMaker = None
        cost = None
        feeCurrencyId = self.safe_string_lower(market, 'quoteId')
        if feeCurrencyId is not None:
            costField = feeCurrencyId + '_value'
            cost = self.safe_string(trade, costField)
            liquidity = self.safe_string(trade, 'liquidity')
            if liquidity is not None:
                takerOrMaker = 'taker' if (liquidity == 'T') else 'maker'
                feeRate = self.safe_string(market, takerOrMaker)
        feeCost = self.safe_string_2(trade, 'fill_fees', 'fee')
        fee = {
            'cost': feeCost,
            'currency': market['quote'],
            'rate': feeRate,
        }
        id = self.safe_string(trade, 'trade_id')
        side = 'sell' if (trade['side'] == 'buy') else 'buy'
        orderId = self.safe_string(trade, 'order_id')
        # Coinbase Pro returns inverted side to fetchMyTrades vs fetchTrades
        makerOrderId = self.safe_string(trade, 'maker_order_id')
        takerOrderId = self.safe_string(trade, 'taker_order_id')
        if (orderId is not None) or ((makerOrderId is not None) and (takerOrderId is not None)):
            side = 'buy' if (trade['side'] == 'buy') else 'sell'
        price = self.safe_string(trade, 'price')
        amount = self.safe_string(trade, 'size')
        return self.safe_trade({
            'id': id,
            'order': orderId,
            'info': trade,
            'timestamp': timestamp,
            'datetime': self.iso8601(timestamp),
            'symbol': market['symbol'],
            'type': None,
            'takerOrMaker': takerOrMaker,
            'side': side,
            'price': price,
            'amount': amount,
            'fee': fee,
            'cost': cost,
        }, market)

    async def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}):
        """
        :see: https://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_getfills
        fetch all trades made by the user
        :param str 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 dict [params]: extra parameters specific to the exchange API endpoint
        :param int [params.until]: the latest time in ms to fetch trades for
        :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt_zagrava/ccxt_zagrava/wiki/Manual#pagination-params)
        :returns Trade[]: a list of `trade structures <https://docs.ccxt_zagrava.com/#/?id=trade-structure>`
        """
        if symbol is None:
            raise ArgumentsRequired(self.id + ' fetchMyTrades() requires a symbol argument')
        paginate = False
        paginate, params = self.handle_option_and_params(params, 'fetchMyTrades', 'paginate')
        if paginate:
            return await self.fetch_paginated_call_dynamic('fetchMyTrades', symbol, since, limit, params, 100)
        await self.load_markets()
        market = self.market(symbol)
        request: dict = {
            'product_id': market['id'],
        }
        if limit is not None:
            request['limit'] = limit
        if since is not None:
            request['start_date'] = self.iso8601(since)
        until = self.safe_value_2(params, 'until', 'end_date')
        if until is not None:
            params = self.omit(params, ['until'])
            request['end_date'] = self.iso8601(until)
        response = await self.privateGetFills(self.extend(request, params))
        return self.parse_trades(response, market, since, limit)

    async def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]:
        """
        :see: https://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_getproducttrades
        get the list of most recent trades for a particular symbol
        :param str 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 dict [params]: extra parameters specific to the exchange API endpoint
        :returns Trade[]: a list of `trade structures <https://docs.ccxt_zagrava.com/#/?id=public-trades>`
        """
        await self.load_markets()
        market = self.market(symbol)
        request: dict = {
            'id': market['id'],  # fixes issue  #2
        }
        if limit is not None:
            request['limit'] = limit  # default 100
        response = await self.publicGetProductsIdTrades(self.extend(request, params))
        #
        #    [
        #        {
        #            "trade_id": "15035219",
        #            "side": "sell",
        #            "size": "0.27426731",
        #            "price": "25820.42000000",
        #            "time": "2023-09-10T13:47:41.447577Z"
        #        },
        #    ]
        #
        return self.parse_trades(response, market, since, limit)

    async def fetch_trading_fees(self, params={}) -> TradingFees:
        """
        fetch the trading fees for multiple markets
        :see: https://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_getfees
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :returns dict: a dictionary of `fee structures <https://docs.ccxt_zagrava.com/#/?id=fee-structure>` indexed by market symbols
        """
        await self.load_markets()
        response = await self.privateGetFees(params)
        #
        #    {
        #        "maker_fee_rate": "0.0050",
        #        "taker_fee_rate": "0.0050",
        #        "usd_volume": "43806.92"
        #    }
        #
        maker = self.safe_number(response, 'maker_fee_rate')
        taker = self.safe_number(response, 'taker_fee_rate')
        result: dict = {}
        for i in range(0, len(self.symbols)):
            symbol = self.symbols[i]
            result[symbol] = {
                'info': response,
                'symbol': symbol,
                'maker': maker,
                'taker': taker,
                'percentage': True,
                'tierBased': True,
            }
        return result

    def parse_ohlcv(self, ohlcv, market: Market = None) -> list:
        #
        #     [
        #         1591514160,
        #         0.02507,
        #         0.02507,
        #         0.02507,
        #         0.02507,
        #         0.02816506
        #     ]
        #
        return [
            self.safe_timestamp(ohlcv, 0),
            self.safe_number(ohlcv, 3),
            self.safe_number(ohlcv, 2),
            self.safe_number(ohlcv, 1),
            self.safe_number(ohlcv, 4),
            self.safe_number(ohlcv, 5),
        ]

    async def fetch_ohlcv(self, symbol: str, timeframe='1m', since: Int = None, limit: Int = None, params={}) -> List[list]:
        """
        :see: https://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_getproductcandles
        fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market
        :param str symbol: unified symbol of the market to fetch OHLCV data for
        :param str 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 dict [params]: extra parameters specific to the exchange API endpoint
        :param int [params.until]: the latest time in ms to fetch trades for
        :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt_zagrava/ccxt_zagrava/wiki/Manual#pagination-params)
        :returns int[][]: A list of candles ordered, open, high, low, close, volume
        """
        await self.load_markets()
        paginate = False
        paginate, params = self.handle_option_and_params(params, 'fetchOHLCV', 'paginate', False)
        if paginate:
            return await self.fetch_paginated_call_deterministic('fetchOHLCV', symbol, since, limit, timeframe, params, 300)
        market = self.market(symbol)
        parsedTimeframe = self.safe_integer(self.timeframes, timeframe)
        request: dict = {
            'id': market['id'],
        }
        if parsedTimeframe is not None:
            request['granularity'] = parsedTimeframe
        else:
            request['granularity'] = timeframe
        until = self.safe_value_2(params, 'until', 'end')
        params = self.omit(params, ['until'])
        if since is not None:
            request['start'] = self.iso8601(since)
            if limit is None:
                # https://docs.pro.coinbase.com/#get-historic-rates
                limit = 300  # max = 300
            else:
                limit = min(300, limit)
            if until is None:
                parsedTimeframeMilliseconds = parsedTimeframe * 1000
                if self.is_round_number(since % parsedTimeframeMilliseconds):
                    request['end'] = self.iso8601(self.sum((limit - 1) * parsedTimeframeMilliseconds, since))
                else:
                    request['end'] = self.iso8601(self.sum(limit * parsedTimeframeMilliseconds, since))
            else:
                request['end'] = self.iso8601(until)
        response = await self.publicGetProductsIdCandles(self.extend(request, params))
        #
        #     [
        #         [1591514160,0.02507,0.02507,0.02507,0.02507,0.02816506],
        #         [1591514100,0.02507,0.02507,0.02507,0.02507,1.63830323],
        #         [1591514040,0.02505,0.02507,0.02505,0.02507,0.19918178]
        #     ]
        #
        return self.parse_ohlcvs(response, market, timeframe, since, limit)

    async def fetch_time(self, params={}):
        """
        fetches the current integer timestamp in milliseconds from the exchange server
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :returns int: the current integer timestamp in milliseconds from the exchange server
        """
        response = await self.publicGetTime(params)
        #
        #     {
        #         "iso":"2020-05-12T08:00:51.504Z",
        #         "epoch":1589270451.504
        #     }
        #
        return self.safe_timestamp(response, 'epoch')

    def parse_order_status(self, status: Str):
        statuses: dict = {
            'pending': 'open',
            'active': 'open',
            'open': 'open',
            'done': 'closed',
            'canceled': 'canceled',
            'canceling': 'open',
        }
        return self.safe_string(statuses, status, status)

    def parse_order(self, order: dict, market: Market = None) -> Order:
        #
        # createOrder
        #
        #     {
        #         "id": "d0c5340b-6d6c-49d9-b567-48c4bfca13d2",
        #         "price": "0.10000000",
        #         "size": "0.01000000",
        #         "product_id": "BTC-USD",
        #         "side": "buy",
        #         "stp": "dc",
        #         "type": "limit",
        #         "time_in_force": "GTC",
        #         "post_only": False,
        #         "created_at": "2016-12-08T20:02:28.53864Z",
        #         "fill_fees": "0.0000000000000000",
        #         "filled_size": "0.00000000",
        #         "executed_value": "0.0000000000000000",
        #         "status": "pending",
        #         "settled": False
        #     }
        #
        timestamp = self.parse8601(self.safe_string(order, 'created_at'))
        marketId = self.safe_string(order, 'product_id')
        market = self.safe_market(marketId, market, '-')
        status = self.parse_order_status(self.safe_string(order, 'status'))
        doneReason = self.safe_string(order, 'done_reason')
        if (status == 'closed') and (doneReason == 'canceled'):
            status = 'canceled'
        price = self.safe_string(order, 'price')
        filled = self.safe_string(order, 'filled_size')
        amount = self.safe_string(order, 'size', filled)
        cost = self.safe_string(order, 'executed_value')
        feeCost = self.safe_number(order, 'fill_fees')
        fee = None
        if feeCost is not None:
            fee = {
                'cost': feeCost,
                'currency': market['quote'],
                'rate': None,
            }
        id = self.safe_string(order, 'id')
        type = self.safe_string(order, 'type')
        side = self.safe_string(order, 'side')
        timeInForce = self.safe_string(order, 'time_in_force')
        postOnly = self.safe_value(order, 'post_only')
        stopPrice = self.safe_number(order, 'stop_price')
        clientOrderId = self.safe_string(order, 'client_oid')
        return self.safe_order({
            'id': id,
            'clientOrderId': clientOrderId,
            'info': order,
            'timestamp': timestamp,
            'datetime': self.iso8601(timestamp),
            'lastTradeTimestamp': None,
            'status': status,
            'symbol': market['symbol'],
            'type': type,
            'timeInForce': timeInForce,
            'postOnly': postOnly,
            'side': side,
            'price': price,
            'stopPrice': stopPrice,
            'triggerPrice': stopPrice,
            'cost': cost,
            'amount': amount,
            'filled': filled,
            'remaining': None,
            'fee': fee,
            'average': None,
            'trades': None,
        }, market)

    async def fetch_order(self, id: str, symbol: Str = None, params={}):
        """
        :see: https://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_getorder
        fetches information on an order made by the user
        :param str symbol: not used by coinbaseexchange fetchOrder
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :returns dict: An `order structure <https://docs.ccxt_zagrava.com/#/?id=order-structure>`
        """
        await self.load_markets()
        request: dict = {}
        clientOrderId = self.safe_string_2(params, 'clientOrderId', 'client_oid')
        method = None
        if clientOrderId is None:
            method = 'privateGetOrdersId'
            request['id'] = id
        else:
            method = 'privateGetOrdersClientClientOid'
            request['client_oid'] = clientOrderId
            params = self.omit(params, ['clientOrderId', 'client_oid'])
        response = await getattr(self, method)(self.extend(request, params))
        return self.parse_order(response)

    async def fetch_order_trades(self, id: str, symbol: Str = None, since: Int = None, limit: Int = None, params={}):
        """
        fetch all the trades made from a single order
        :param str id: order id
        :param str 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 dict [params]: extra parameters specific to the exchange API endpoint
        :returns dict[]: a list of `trade structures <https://docs.ccxt_zagrava.com/#/?id=trade-structure>`
        """
        await self.load_markets()
        market = None
        if symbol is not None:
            market = self.market(symbol)
        request: dict = {
            'order_id': id,
        }
        response = await self.privateGetFills(self.extend(request, params))
        return self.parse_trades(response, market, since, limit)

    async def fetch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]:
        """
        :see: https://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_getorders
        fetches information on multiple orders made by the user
        :param str symbol: unified market symbol of the market orders were made in
        :param int [since]: the earliest time in ms to fetch orders for
        :param int [limit]: the maximum number of order structures to retrieve
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :param int [params.until]: the latest time in ms to fetch open orders for
        :returns Order[]: a list of `order structures <https://docs.ccxt_zagrava.com/#/?id=order-structure>`
        """
        request: dict = {
            'status': 'all',
        }
        return await self.fetch_open_orders(symbol, since, limit, self.extend(request, params))

    async def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]:
        """
        :see: https://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_getorders
        fetch all unfilled currently open orders
        :param str 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 dict [params]: extra parameters specific to the exchange API endpoint
        :param int [params.until]: the latest time in ms to fetch open orders for
        :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt_zagrava/ccxt_zagrava/wiki/Manual#pagination-params)
        :returns Order[]: a list of `order structures <https://docs.ccxt_zagrava.com/#/?id=order-structure>`
        """
        await self.load_markets()
        paginate = False
        paginate, params = self.handle_option_and_params(params, 'fetchOpenOrders', 'paginate')
        if paginate:
            return await self.fetch_paginated_call_dynamic('fetchOpenOrders', symbol, since, limit, params, 100)
        request: dict = {}
        market = None
        if symbol is not None:
            market = self.market(symbol)
            request['product_id'] = market['id']
        if limit is not None:
            request['limit'] = limit  # default 100
        if since is not None:
            request['start_date'] = self.iso8601(since)
        until = self.safe_value_2(params, 'until', 'end_date')
        if until is not None:
            params = self.omit(params, ['until'])
            request['end_date'] = self.iso8601(until)
        response = await self.privateGetOrders(self.extend(request, params))
        return self.parse_orders(response, market, since, limit)

    async def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]:
        """
        :see: https://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_getorders
        fetches information on multiple closed orders made by the user
        :param str symbol: unified market symbol of the market orders were made in
        :param int [since]: the earliest time in ms to fetch orders for
        :param int [limit]: the maximum number of order structures to retrieve
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :param int [params.until]: the latest time in ms to fetch open orders for
        :returns Order[]: a list of `order structures <https://docs.ccxt_zagrava.com/#/?id=order-structure>`
        """
        request: dict = {
            'status': 'done',
        }
        return await self.fetch_open_orders(symbol, since, limit, self.extend(request, params))

    async def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}):
        """
        :see: https://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_postorders
        create a trade order
        :param str symbol: unified symbol of the market to create an order in
        :param str type: 'market' or 'limit'
        :param str 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 dict [params]: extra parameters specific to the exchange API endpoint
        :returns dict: an `order structure <https://docs.ccxt_zagrava.com/#/?id=order-structure>`
        """
        await self.load_markets()
        market = self.market(symbol)
        request: dict = {
            # common params --------------------------------------------------
            # 'client_oid': clientOrderId,
            'type': type,
            'side': side,
            'product_id': market['id'],
            # 'size': self.amount_to_precision(symbol, amount),
            # 'stp': 'dc',  # self-trade prevention, dc = decrease and cancel, co = cancel oldest, cn = cancel newest, cb = cancel both
            # 'stop': 'loss',  # "loss" = stop loss below price, "entry" = take profit above price
            # 'stop_price': self.price_to_precision(symbol, price),
            # limit order params ---------------------------------------------
            # 'price': self.price_to_precision(symbol, price),
            # 'size': self.amount_to_precision(symbol, amount),
            # 'time_in_force': 'GTC',  # GTC, GTT, IOC, or FOK
            # 'cancel_after' [optional]* min, hour, day, requires time_in_force to be GTT
            # 'post_only': False,  # invalid when time_in_force is IOC or FOK
            # market order params --------------------------------------------
            # 'size': self.amount_to_precision(symbol, amount),
            # 'funds': self.cost_to_precision(symbol, amount),
        }
        clientOrderId = self.safe_string_2(params, 'clientOrderId', 'client_oid')
        if clientOrderId is not None:
            request['client_oid'] = clientOrderId
        stopPrice = self.safe_number_n(params, ['stopPrice', 'stop_price', 'triggerPrice'])
        if stopPrice is not None:
            request['stop_price'] = self.price_to_precision(symbol, stopPrice)
        timeInForce = self.safe_string_2(params, 'timeInForce', 'time_in_force')
        if timeInForce is not None:
            request['time_in_force'] = timeInForce
        postOnly = self.safe_value_2(params, 'postOnly', 'post_only', False)
        if postOnly:
            request['post_only'] = True
        params = self.omit(params, ['timeInForce', 'time_in_force', 'stopPrice', 'stop_price', 'clientOrderId', 'client_oid', 'postOnly', 'post_only', 'triggerPrice'])
        if type == 'limit':
            request['price'] = self.price_to_precision(symbol, price)
            request['size'] = self.amount_to_precision(symbol, amount)
        elif type == 'market':
            cost = self.safe_number_2(params, 'cost', 'funds')
            if cost is None:
                if price is not None:
                    cost = amount * price
            else:
                params = self.omit(params, ['cost', 'funds'])
            if cost is not None:
                request['funds'] = self.cost_to_precision(symbol, cost)
            else:
                request['size'] = self.amount_to_precision(symbol, amount)
        response = await self.privatePostOrders(self.extend(request, params))
        #
        #     {
        #         "id": "d0c5340b-6d6c-49d9-b567-48c4bfca13d2",
        #         "price": "0.10000000",
        #         "size": "0.01000000",
        #         "product_id": "BTC-USD",
        #         "side": "buy",
        #         "stp": "dc",
        #         "type": "limit",
        #         "time_in_force": "GTC",
        #         "post_only": False,
        #         "created_at": "2016-12-08T20:02:28.53864Z",
        #         "fill_fees": "0.0000000000000000",
        #         "filled_size": "0.00000000",
        #         "executed_value": "0.0000000000000000",
        #         "status": "pending",
        #         "settled": False
        #     }
        #
        return self.parse_order(response, market)

    async def cancel_order(self, id: str, symbol: Str = None, params={}):
        """
        :see: https://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_deleteorder
        cancels an open order
        :param str id: order id
        :param str symbol: unified symbol of the market the order was made in
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :returns dict: An `order structure <https://docs.ccxt_zagrava.com/#/?id=order-structure>`
        """
        await self.load_markets()
        request: dict = {
            # 'product_id': market['id'],  # the request will be more performant if you include it
        }
        clientOrderId = self.safe_string_2(params, 'clientOrderId', 'client_oid')
        method = None
        if clientOrderId is None:
            method = 'privateDeleteOrdersId'
            request['id'] = id
        else:
            method = 'privateDeleteOrdersClientClientOid'
            request['client_oid'] = clientOrderId
            params = self.omit(params, ['clientOrderId', 'client_oid'])
        market = None
        if symbol is not None:
            market = self.market(symbol)
            request['product_id'] = market['symbol']  # the request will be more performant if you include it
        return await getattr(self, method)(self.extend(request, params))

    async def cancel_all_orders(self, symbol: Str = None, params={}):
        """
        :see: https://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_deleteorders
        cancel all open orders
        :param str symbol: unified market symbol, only orders in the market of self symbol are cancelled when symbol is not None
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :returns dict[]: a list of `order structures <https://docs.ccxt_zagrava.com/#/?id=order-structure>`
        """
        await self.load_markets()
        request: dict = {}
        market = None
        if symbol is not None:
            market = self.market(symbol)
            request['product_id'] = market['symbol']  # the request will be more performant if you include it
        return await self.privateDeleteOrders(self.extend(request, params))

    async def fetch_payment_methods(self, params={}):
        return await self.privateGetPaymentMethods(params)

    async def withdraw(self, code: str, amount: float, address: str, tag=None, params={}):
        """
        make a withdrawal
        :see: https://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_postwithdrawpaymentmethod
        :see: https://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_postwithdrawcoinbaseaccount
        :param str code: unified currency code
        :param float amount: the amount to withdraw
        :param str address: the address to withdraw to
        :param str tag:
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :returns dict: a `transaction structure <https://docs.ccxt_zagrava.com/#/?id=transaction-structure>`
        """
        tag, params = self.handle_withdraw_tag_and_params(tag, params)
        self.check_address(address)
        await self.load_markets()
        currency = self.currency(code)
        request: dict = {
            'currency': currency['id'],
            'amount': amount,
        }
        method = 'privatePostWithdrawals'
        if 'payment_method_id' in params:
            method += 'PaymentMethod'
        elif 'coinbase_account_id' in params:
            method += 'CoinbaseAccount'
        else:
            method += 'Crypto'
            request['crypto_address'] = address
            if tag is not None:
                request['destination_tag'] = tag
        response = await getattr(self, method)(self.extend(request, params))
        if not response:
            raise ExchangeError(self.id + ' withdraw() error: ' + self.json(response))
        return self.parse_transaction(response, currency)

    def parse_ledger_entry_type(self, type):
        types: dict = {
            'transfer': 'transfer',  # Funds moved between portfolios
            'match': 'trade',       # Funds moved result of a trade
            'fee': 'fee',           # Fee result of a trade
            'rebate': 'rebate',     # Fee rebate
            'conversion': 'trade',  # Funds converted between fiat currency and a stablecoin
        }
        return self.safe_string(types, type, type)

    def parse_ledger_entry(self, item: dict, currency: Currency = None):
        #  {
        #      "id": "12087495079",
        #      "amount": "-0.0100000000000000",
        #      "balance": "0.0645419900000000",
        #      "created_at": "2021-10-28T17:14:32.593168Z",
        #      "type": "transfer",
        #      "details": {
        #          "from": "2f74edf7-1440-4586-86dc-ae58c5693691",
        #          "profile_transfer_id": "3ef093ad-2482-40d1-8ede-2f89cff5099e",
        #          "to": "dda99503-4980-4b60-9549-0b770ee51336"
        #      }
        #  },
        #  {
        #     "id": "11740725774",
        #     "amount": "-1.7565669701255000",
        #     "balance": "0.0016490047745000",
        #     "created_at": "2021-10-22T03:47:34.764122Z",
        #     "type": "fee",
        #     "details": {
        #         "order_id": "ad06abf4-95ab-432a-a1d8-059ef572e296",
        #         "product_id": "ETH-DAI",
        #         "trade_id": "1740617"
        #     }
        #  }
        id = self.safe_string(item, 'id')
        amountString = self.safe_string(item, 'amount')
        direction = None
        afterString = self.safe_string(item, 'balance')
        beforeString = Precise.string_sub(afterString, amountString)
        if Precise.string_lt(amountString, '0'):
            direction = 'out'
            amountString = Precise.string_abs(amountString)
        else:
            direction = 'in'
        amount = self.parse_number(amountString)
        after = self.parse_number(afterString)
        before = self.parse_number(beforeString)
        timestamp = self.parse8601(self.safe_value(item, 'created_at'))
        type = self.parse_ledger_entry_type(self.safe_string(item, 'type'))
        code = self.safe_currency_code(None, currency)
        details = self.safe_value(item, 'details', {})
        account = None
        referenceAccount = None
        referenceId = None
        if type == 'transfer':
            account = self.safe_string(details, 'from')
            referenceAccount = self.safe_string(details, 'to')
            referenceId = self.safe_string(details, 'profile_transfer_id')
        else:
            referenceId = self.safe_string(details, 'order_id')
        status = 'ok'
        return {
            'id': id,
            'currency': code,
            'account': account,
            'referenceAccount': referenceAccount,
            'referenceId': referenceId,
            'status': status,
            'amount': amount,
            'before': before,
            'after': after,
            'fee': None,
            'direction': direction,
            'timestamp': timestamp,
            'datetime': self.iso8601(timestamp),
            'type': type,
            'info': item,
        }

    async def fetch_ledger(self, code: Str = None, since: Int = None, limit: Int = None, params={}):
        """
        :see: https://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_getaccountledger
        fetch the history of changes, actions done by the user or operations that altered balance of the user
        :param str code: unified currency code, default is None
        :param int [since]: timestamp in ms of the earliest ledger entry, default is None
        :param int [limit]: max number of ledger entrys to return, default is None
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :param int [params.until]: the latest time in ms to fetch trades for
        :returns dict: a `ledger structure <https://docs.ccxt_zagrava.com/#/?id=ledger-structure>`
        """
        # https://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_getaccountledger
        if code is None:
            raise ArgumentsRequired(self.id + ' fetchLedger() requires a code param')
        await self.load_markets()
        await self.load_accounts()
        currency = self.currency(code)
        accountsByCurrencyCode = self.index_by(self.accounts, 'code')
        account = self.safe_value(accountsByCurrencyCode, code)
        if account is None:
            raise ExchangeError(self.id + ' fetchLedger() could not find account id for ' + code)
        request: dict = {
            'id': account['id'],
            # 'start_date': self.iso8601(since),
            # 'end_date': self.iso8601(self.milliseconds()),
            # 'before': 'cursor',  # sets start cursor to before date
            # 'after': 'cursor',  # sets end cursor to after date
            # 'limit': limit,  # default 100
            # 'profile_id': 'string'
        }
        if since is not None:
            request['start_date'] = self.iso8601(since)
        if limit is not None:
            request['limit'] = limit  # default 100
        until = self.safe_value_2(params, 'until', 'end_date')
        if until is not None:
            params = self.omit(params, ['until'])
            request['end_date'] = self.iso8601(until)
        response = await self.privateGetAccountsIdLedger(self.extend(request, params))
        for i in range(0, len(response)):
            response[i]['currency'] = code
        return self.parse_ledger(response, currency, since, limit)

    async def fetch_deposits_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]:
        """
        fetch history of deposits and withdrawals
        :see: https://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_gettransfers
        :see: https://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_getaccounttransfers
        :param str [code]: unified currency code for the currency of the deposit/withdrawals, default is None
        :param int [since]: timestamp in ms of the earliest deposit/withdrawal, default is None
        :param int [limit]: max number of deposit/withdrawals to return, default is None
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :param str [params.id]: account id, when defined, the endpoint used is '/accounts/{account_id}/transfers/' instead of '/transfers/'
        :returns dict: a list of `transaction structure <https://docs.ccxt_zagrava.com/#/?id=transaction-structure>`
        """
        await self.load_markets()
        await self.load_accounts()
        currency = None
        id = self.safe_string(params, 'id')  # account id
        if id is None:
            if code is not None:
                currency = self.currency(code)
                accountsByCurrencyCode = self.index_by(self.accounts, 'code')
                account = self.safe_value(accountsByCurrencyCode, code)
                if account is None:
                    raise ExchangeError(self.id + ' fetchDepositsWithdrawals() could not find account id for ' + code)
                id = account['id']
        request: dict = {}
        if id is not None:
            request['id'] = id
        if limit is not None:
            request['limit'] = limit
        response = None
        if id is None:
            response = await self.privateGetTransfers(self.extend(request, params))
            #
            #    [
            #        {
            #            "id": "bee6fd7c-afb2-4e47-8298-671d09997d16",
            #            "type": "deposit",
            #            "created_at": "2022-12-21 00:48:45.477503+00",
            #            "completed_at": null,
            #            "account_id": "sal3802-36bd-46be-a7b8-alsjf383sldak",
            #            "user_id": "6382048209f92as392039dlks2",
            #            "amount": "0.01000000",
            #            "details": {
            #                "network": "litecoin",
            #                "crypto_address": "MKemtnCFUYKsNWaf5EMYMpwSszcXWFDtTY",
            #                "coinbase_account_id": "fl2b6925-f6ba-403n-jj03-40fl435n430f",
            #                "coinbase_transaction_id": "63a25bb13cb5cf0001d2cf17",  # withdrawals only
            #                "crypto_transaction_hash": "752f35570736341e2a253f7041a34cf1e196fc56128c900fd03d99da899d94c1",
            #                "tx_service_transaction_id": "1873249104",
            #                "coinbase_payment_method_id": ""
            #            },
            #            "canceled_at": null,
            #            "processed_at": null,
            #            "user_nonce": null,
            #            "idem": "5e3201b0-e390-5k3k-a913-c32932049242",
            #            "profile_id": "k3k302a8-c4dk-4f49-9d39-3203923wpk39",
            #            "currency": "LTC"
            #        }
            #    ]
            #
            for i in range(0, len(response)):
                account_id = self.safe_string(response[i], 'account_id')
                account = self.safe_value(self.accountsById, account_id)
                codeInner = self.safe_string(account, 'code')
                response[i]['currency'] = codeInner
        else:
            response = await self.privateGetAccountsIdTransfers(self.extend(request, params))
            #
            #    [
            #        {
            #            "id": "bee6fd7c-afb2-4e47-8298-671d09997d16",
            #            "type": "deposit",
            #            "created_at": "2022-12-21 00:48:45.477503+00",
            #            "completed_at": null,
            #            "amount": "0.01000000",
            #            "details": {
            #                "network": "litecoin",
            #                "crypto_address": "MKemtnCFUYKsNWaf5EMYMpwSszcXWFDtTY",
            #                "coinbase_account_id": "fl2b6925-f6ba-403n-jj03-40fl435n430f",
            #                "coinbase_transaction_id": "63a25bb13cb5cf0001d2cf17",  # withdrawals only
            #                "crypto_transaction_hash": "752f35570736341e2a253f7041a34cf1e196fc56128c900fd03d99da899d94c1",
            #                "tx_service_transaction_id": "1873249104",
            #                "coinbase_payment_method_id": ""
            #            },
            #            "canceled_at": null,
            #            "processed_at": null,
            #            "user_nonce": null,
            #            "idem": "5e3201b0-e390-5k3k-a913-c32932049242",
            #            "profile_id": "k3k302a8-c4dk-4f49-9d39-3203923wpk39",
            #            "currency": "LTC"
            #        }
            #    ]
            #
            for i in range(0, len(response)):
                response[i]['currency'] = code
        return self.parse_transactions(response, currency, since, limit)

    async def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]:
        """
        fetch all deposits made to an account
        :see: https://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_gettransfers
        :see: https://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_getaccounttransfers
        :param str 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 dict [params]: extra parameters specific to the exchange API endpoint
        :returns dict[]: a list of `transaction structures <https://docs.ccxt_zagrava.com/#/?id=transaction-structure>`
        """
        return await self.fetch_deposits_withdrawals(code, since, limit, self.extend({'type': 'deposit'}, params))

    async def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]:
        """
        fetch all withdrawals made from an account
        :see: https://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_gettransfers
        :see: https://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_getaccounttransfers
        :param str 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 dict [params]: extra parameters specific to the exchange API endpoint
        :returns dict[]: a list of `transaction structures <https://docs.ccxt_zagrava.com/#/?id=transaction-structure>`
        """
        return await self.fetch_deposits_withdrawals(code, since, limit, self.extend({'type': 'withdraw'}, params))

    def parse_transaction_status(self, transaction):
        canceled = self.safe_value(transaction, 'canceled_at')
        if canceled:
            return 'canceled'
        processed = self.safe_value(transaction, 'processed_at')
        completed = self.safe_value(transaction, 'completed_at')
        if completed:
            return 'ok'
        elif processed and not completed:
            return 'failed'
        else:
            return 'pending'

    def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction:
        #
        # privateGetTransfers
        #
        #    [
        #        {
        #            "id": "bee6fd7c-afb2-4e47-8298-671d09997d16",
        #            "type": "deposit",
        #            "created_at": "2022-12-21 00:48:45.477503+00",
        #            "completed_at": null,
        #            "account_id": "sal3802-36bd-46be-a7b8-alsjf383sldak",     # only from privateGetTransfers
        #            "user_id": "6382048209f92as392039dlks2",                  # only from privateGetTransfers
        #            "amount": "0.01000000",
        #            "details": {
        #                "network": "litecoin",
        #                "crypto_address": "MKemtnCFUYKsNWaf5EMYMpwSszcXWFDtTY",
        #                "coinbase_account_id": "fl2b6925-f6ba-403n-jj03-40fl435n430f",
        #                "coinbase_transaction_id": "63a25bb13cb5cf0001d2cf17",  # withdrawals only
        #                "crypto_transaction_hash": "752f35570736341e2a253f7041a34cf1e196fc56128c900fd03d99da899d94c1",
        #                "tx_service_transaction_id": "1873249104",
        #                "coinbase_payment_method_id": ""
        #            },
        #            "canceled_at": null,
        #            "processed_at": null,
        #            "user_nonce": null,
        #            "idem": "5e3201b0-e390-5k3k-a913-c32932049242",
        #            "profile_id": "k3k302a8-c4dk-4f49-9d39-3203923wpk39",
        #            "currency": "LTC"
        #        }
        #    ]
        #
        details = self.safe_value(transaction, 'details', {})
        timestamp = self.parse8601(self.safe_string(transaction, 'created_at'))
        currencyId = self.safe_string(transaction, 'currency')
        code = self.safe_currency_code(currencyId, currency)
        amount = self.safe_number(transaction, 'amount')
        type = self.safe_string(transaction, 'type')
        address = self.safe_string(details, 'crypto_address')
        address = self.safe_string(transaction, 'crypto_address', address)
        fee = {
            'currency': None,
            'cost': None,
            'rate': None,
        }
        if type == 'withdraw':
            type = 'withdrawal'
            address = self.safe_string(details, 'sent_to_address', address)
            feeCost = self.safe_number(details, 'fee')
            if feeCost is not None:
                if amount is not None:
                    amount -= feeCost
                fee['cost'] = feeCost
                fee['currency'] = code
        networkId = self.safe_string(details, 'network')
        return {
            'info': transaction,
            'id': self.safe_string(transaction, 'id'),
            'txid': self.safe_string(details, 'crypto_transaction_hash'),
            'type': type,
            'currency': code,
            'network': self.network_id_to_code(networkId),
            'amount': amount,
            'status': self.parse_transaction_status(transaction),
            'timestamp': timestamp,
            'datetime': self.iso8601(timestamp),
            'address': address,
            'addressFrom': None,
            'addressTo': self.safe_string(details, 'crypto_address'),
            'tag': self.safe_string(details, 'destination_tag'),
            'tagFrom': None,
            'tagTo': None,
            'updated': self.parse8601(self.safe_string(transaction, 'processed_at')),
            'comment': None,
            'internal': False,
            'fee': fee,
        }

    async def create_deposit_address(self, code: str, params={}):
        """
        create a currency deposit address
        :see: https://docs.cloud.coinbase.com/exchange/reference/exchangerestapi_postcoinbaseaccountaddresses
        :param str code: unified currency code of the currency for the deposit address
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :returns dict: an `address structure <https://docs.ccxt_zagrava.com/#/?id=address-structure>`
        """
        await self.load_markets()
        currency = self.currency(code)
        accounts = self.safe_value(self.options, 'coinbaseAccounts')
        if accounts is None:
            accounts = await self.privateGetCoinbaseAccounts()
            self.options['coinbaseAccounts'] = accounts  # cache it
            self.options['coinbaseAccountsByCurrencyId'] = self.index_by(accounts, 'currency')
        currencyId = currency['id']
        account = self.safe_value(self.options['coinbaseAccountsByCurrencyId'], currencyId)
        if account is None:
            # eslint-disable-next-line quotes
            raise InvalidAddress(self.id + " createDepositAddress() could not find currency code " + code + " with id = " + currencyId + " in self.options['coinbaseAccountsByCurrencyId']")
        request: dict = {
            'id': account['id'],
        }
        response = await self.privatePostCoinbaseAccountsIdAddresses(self.extend(request, params))
        address = self.safe_string(response, 'address')
        tag = self.safe_string(response, 'destination_tag')
        return {
            'currency': code,
            'address': self.check_address(address),
            'tag': tag,
            'info': response,
        }

    def sign(self, path, api='public', method='GET', params={}, headers=None, body=None):
        request = '/' + self.implode_params(path, params)
        query = self.omit(params, self.extract_params(path))
        if method == 'GET':
            if query:
                request += '?' + self.urlencode(query)
        url = self.implode_hostname(self.urls['api'][api]) + request
        if api == 'private':
            self.check_required_credentials()
            nonce = str(self.nonce())
            payload = ''
            if method != 'GET':
                if query:
                    body = self.json(query)
                    payload = body
            what = nonce + method + request + payload
            secret = None
            try:
                secret = self.base64_to_binary(self.secret)
            except Exception as e:
                raise AuthenticationError(self.id + ' sign() invalid base64 secret')
            signature = self.hmac(self.encode(what), secret, hashlib.sha256, 'base64')
            headers = {
                'CB-ACCESS-KEY': self.apiKey,
                'CB-ACCESS-SIGN': signature,
                'CB-ACCESS-TIMESTAMP': nonce,
                'CB-ACCESS-PASSPHRASE': self.password,
                'Content-Type': 'application/json',
            }
        return {'url': url, 'method': method, 'body': body, 'headers': headers}

    def handle_errors(self, code: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody):
        if (code == 400) or (code == 404):
            if body[0] == '{':
                message = self.safe_string(response, 'message')
                feedback = self.id + ' ' + message
                self.throw_exactly_matched_exception(self.exceptions['exact'], message, feedback)
                self.throw_broadly_matched_exception(self.exceptions['broad'], message, feedback)
                raise ExchangeError(feedback)  # unknown message
            raise ExchangeError(self.id + ' ' + body)
        return None

    async def request(self, path, api='public', method='GET', params={}, headers=None, body=None, config={}):
        response = await self.fetch2(path, api, method, params, headers, body, config)
        if not isinstance(response, str):
            if 'message' in response:
                raise ExchangeError(self.id + ' ' + self.json(response))
        return response
