package coinbase

import (
	"bytes"
	"context"
	"crypto/ecdsa"
	"crypto/rand"
	"crypto/sha256"
	"crypto/x509"
	"encoding/base64"
	"encoding/pem"
	"errors"
	"fmt"
	"math/big"
	"net/http"
	"net/url"
	"strconv"
	"strings"
	"time"

	"github.com/gofrs/uuid"
	"github.com/thrasher-corp/gocryptotrader/common"
	"github.com/thrasher-corp/gocryptotrader/common/key"
	"github.com/thrasher-corp/gocryptotrader/currency"
	"github.com/thrasher-corp/gocryptotrader/encoding/json"
	exchange "github.com/thrasher-corp/gocryptotrader/exchanges"
	"github.com/thrasher-corp/gocryptotrader/exchanges/kline"
	"github.com/thrasher-corp/gocryptotrader/exchanges/order"
	"github.com/thrasher-corp/gocryptotrader/exchanges/request"
	"github.com/thrasher-corp/gocryptotrader/types"
)

const (
	apiURL                   = "https://api.coinbase.com"
	v1APIURL                 = "https://api.exchange.coinbase.com/"
	sandboxAPIURL            = "https://api-sandbox.coinbase.com"
	tradeBaseURL             = "https://www.coinbase.com/advanced-trade/spot/"
	v3Path                   = "/api/v3/brokerage/"
	accountsPath             = "accounts"
	convertPath              = "convert"
	tradePath                = "trade"
	quotePath                = "quote"
	keyPermissionsPath       = "key_permissions"
	transactionSummaryPath   = "transaction_summary"
	futuresPath              = "cfm" // Coinbase Financial Markets is the legal name for the Coinbase futures company
	sweepsPath               = "sweeps"
	intradayPath             = "intraday"
	currentMarginWindowPath  = "current_margin_window"
	balanceSummaryPath       = "balance_summary"
	positionsPath            = "positions"
	marginSettingPath        = "margin_setting"
	schedulePath             = "schedule"
	ordersPath               = "orders"
	batchCancelpath          = "batch_cancel"
	closePositionPath        = "close_position"
	editPath                 = "edit"
	editPreviewPath          = "edit_preview"
	historicalPath           = "historical"
	fillsPath                = "fills"
	batchPath                = "batch"
	bestBidAskPath           = "best_bid_ask"
	productBookPath          = "product_book"
	productsPath             = "products"
	candlesPath              = "candles"
	tickerPath               = "ticker"
	previewPath              = "preview"
	portfoliosPath           = "portfolios"
	moveFundsPath            = "move_funds"
	intxPath                 = "intx"
	balancesPath             = "balances"
	multiAssetCollateralPath = "multi_asset_collateral"
	allocatePath             = "allocate"
	portfolioPath            = "portfolio"
	paymentMethodsPath       = "payment_methods"
	v2Path                   = "/v2/"
	userPath                 = "user"
	addressesPath            = "addresses"
	transactionsPath         = "transactions"
	depositsPath             = "deposits"
	commitPath               = "commit"
	withdrawalsPath          = "withdrawals"
	currenciesPath           = "currencies"
	cryptoPath               = "crypto"
	exchangeRatesPath        = "exchange-rates"
	pricesPath               = "prices"
	timePath                 = "time"
	volumeSummaryPath        = "volume-summary"
	bookPath                 = "book"
	statsPath                = "stats"
	tradesPath               = "trades"
	wrappedAssetsPath        = "wrapped-assets"
	conversionRatePath       = "conversion-rate"
	marketPath               = "market"

	startDateString = "start_date"
	endDateString   = "end_date"

	defaultOrderFillCount = 3000       // Largest number of fills the exchange will let one retrieve in a request, found through experimentation
	defaultOrderCount     = 2147483647 // int32 limit, largest number of orders the exchange will let one retrieve in a request, found through experimentation
)

// Constants defining whether a transfer is a deposit or withdrawal, used to simplify interactions with a few endpoints
const (
	FiatDeposit    FiatTransferType = false
	FiatWithdrawal FiatTransferType = true
)

// While the exchange's fee pages say the worst taker/maker fees are lower than the ones listed here, the data returned by the GetTransactionsSummary endpoint are consistent with these worst case scenarios. The best case scenarios are untested, and assumed to be in line with the fee pages
const (
	WorstCaseTakerFee           = 0.012
	WorstCaseMakerFee           = 0.006
	BestCaseTakerFee            = 0.0005
	BestCaseMakerFee            = 0
	StablePairMakerFee          = 0
	WorstCaseStablePairTakerFee = 0.000045
)

var (
	errAccountIDEmpty           = errors.New("account id cannot be empty")
	errProductIDEmpty           = errors.New("product id cannot be empty")
	errCancelLimitExceeded      = errors.New("100 order cancel limit exceeded")
	errSizeAndPriceZero         = errors.New("size and price cannot both be 0")
	errCurrWalletConflict       = errors.New("exactly one of walletID and currency must be specified")
	errWalletIDEmpty            = errors.New("wallet id cannot be empty")
	errAddressIDEmpty           = errors.New("address id cannot be empty")
	errTransactionTypeEmpty     = errors.New("transaction type cannot be empty")
	errToEmpty                  = errors.New("to cannot be empty")
	errTransactionIDEmpty       = errors.New("transaction id cannot be empty")
	errPaymentMethodEmpty       = errors.New("payment method cannot be empty")
	errDepositIDEmpty           = errors.New("deposit id cannot be empty")
	errInvalidPriceType         = errors.New("price type must be spot, buy, or sell")
	errInvalidOrderType         = errors.New("order type must be market, limit, or stop")
	errEndTimeInPast            = errors.New("end time cannot be in the past")
	errNoMatchingWallets        = errors.New("no matching wallets returned")
	errOrderModFailNoRet        = errors.New("order modification failed but no error returned")
	errNameEmpty                = errors.New("name cannot be empty")
	errPortfolioIDEmpty         = errors.New("portfolio id cannot be empty")
	errFeeTypeNotSupported      = errors.New("fee type not supported")
	errDecodingPrivateKey       = errors.New("error decoding private key")
	errNoWalletForCurrency      = errors.New("no wallet found for currency, address creation impossible")
	errChannelNameUnknown       = errors.New("unknown channel name")
	errNoWalletsReturned        = errors.New("no wallets returned")
	errPayMethodNotFound        = errors.New("payment method not found")
	errUnknownL2DataType        = errors.New("unknown l2update data type")
	errOrderFailedToCancel      = errors.New("failed to cancel order")
	errWrappedAssetEmpty        = errors.New("wrapped asset cannot be empty")
	errUnrecognisedStrategyType = errors.New("unrecognised strategy type")
	errEndpointPathInvalid      = errors.New("endpoint path invalid, should start with https://")
	errPairsDisabledOrErrored   = errors.New("pairs are either disabled or errored")
	errDateLabelEmpty           = errors.New("date label cannot be empty")
	errMarginProfileTypeEmpty   = errors.New("margin profile type cannot be empty")
	errSettingEmpty             = errors.New("setting cannot be empty")
	errUnknownTransferType      = errors.New("unknown transfer type")
	errOutOfSequence            = errors.New("out of order sequence number")

	closedStatuses = []string{"FILLED", "CANCELLED", "EXPIRED", "FAILED"}
	openStatus     = []string{"OPEN"}

	allowedGranularities = map[kline.Interval]string{
		kline.OneMin:     "ONE_MINUTE",
		kline.FiveMin:    "FIVE_MINUTE",
		kline.FifteenMin: "FIFTEEN_MINUTE",
		kline.ThirtyMin:  "THIRTY_MINUTE",
		kline.OneHour:    "ONE_HOUR",
		kline.TwoHour:    "TWO_HOUR",
		kline.SixHour:    "SIX_HOUR",
		kline.OneDay:     "ONE_DAY",
	}
)

// GetAccountByID returns information for a single account
func (e *Exchange) GetAccountByID(ctx context.Context, accountID string) (*Account, error) {
	if accountID == "" {
		return nil, errAccountIDEmpty
	}
	path := v3Path + accountsPath + "/" + accountID
	resp := struct {
		Account Account `json:"account"`
	}{}
	return &resp.Account, e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, path, nil, nil, true, &resp)
}

// ListAccounts returns information on all trading accounts associated with the API key
func (e *Exchange) ListAccounts(ctx context.Context, limit uint8, cursor int64) (*AllAccountsResponse, error) {
	vals := url.Values{}
	if limit != 0 {
		vals.Set("limit", strconv.FormatUint(uint64(limit), 10))
	}
	if cursor != 0 {
		vals.Set("cursor", strconv.FormatInt(cursor, 10))
	}
	var resp *AllAccountsResponse
	return resp, e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, v3Path+accountsPath, vals, nil, true, &resp)
}

// CommitConvertTrade commits a conversion between two currencies, using the trade_id returned from CreateConvertQuote
func (e *Exchange) CommitConvertTrade(ctx context.Context, tradeID, from, to string) (*ConvertResponse, error) {
	if tradeID == "" {
		return nil, errTransactionIDEmpty
	}
	if from == "" || to == "" {
		return nil, errAccountIDEmpty
	}
	path := v3Path + convertPath + "/" + tradePath + "/" + tradeID
	var resp ConvertWrapper
	return &resp.Trade, e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, path, nil, convertTradeReqBase{FromAccount: from, ToAccount: to}, true, &resp)
}

// CreateConvertQuote creates a quote for a conversion between two currencies. The trade_id returned can be used to commit the trade, but that must be done within 10 minutes of the quote's creation
func (e *Exchange) CreateConvertQuote(ctx context.Context, from, to, userIncentiveID, codeVal string, amount float64) (*ConvertResponse, error) {
	if from == "" || to == "" {
		return nil, errAccountIDEmpty
	}
	if amount <= 0 {
		return nil, order.ErrAmountIsInvalid
	}
	path := v3Path + convertPath + "/" + quotePath
	req := convertQuoteReqBase{
		FromAccount: from,
		ToAccount:   to,
		Amount:      amount,
		Metadata: tradeIncentiveMetadata{
			UserIncentiveID: userIncentiveID,
			CodeVal:         codeVal,
		},
	}
	var resp ConvertWrapper
	return &resp.Trade, e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, path, nil, req, true, &resp)
}

// GetConvertTradeByID returns information on a conversion between two currencies
func (e *Exchange) GetConvertTradeByID(ctx context.Context, tradeID, from, to string) (*ConvertResponse, error) {
	if tradeID == "" {
		return nil, errTransactionIDEmpty
	}
	if from == "" || to == "" {
		return nil, errAccountIDEmpty
	}
	path := v3Path + convertPath + "/" + tradePath + "/" + tradeID
	var resp ConvertWrapper
	return &resp.Trade, e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, path, nil, convertTradeReqBase{FromAccount: from, ToAccount: to}, true, &resp)
}

// GetPermissions returns the permissions associated with the API key
func (e *Exchange) GetPermissions(ctx context.Context) (*PermissionsResponse, error) {
	var resp PermissionsResponse
	return &resp, e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, v3Path+keyPermissionsPath, nil, nil, true, &resp)
}

// GetTransactionSummary returns a summary of transactions with fee tiers, total volume, and fees
func (e *Exchange) GetTransactionSummary(ctx context.Context, startDate, endDate time.Time, productVenue, productType, contractExpiryType string) (*TransactionSummary, error) {
	vals, err := urlValsFromDateRange(startDate, endDate, startDateString, endDateString)
	if err != nil {
		return nil, err
	}
	if contractExpiryType != "" {
		vals.Set("contract_expiry_type", contractExpiryType)
	}
	if productType != "" {
		vals.Set("product_type", productType)
	}
	if productVenue != "" {
		vals.Set("product_venue", productVenue)
	}
	var resp *TransactionSummary
	return resp, e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, v3Path+transactionSummaryPath, vals, nil, true, &resp)
}

// CancelPendingFuturesSweep cancels a pending sweep request
func (e *Exchange) CancelPendingFuturesSweep(ctx context.Context) (bool, error) {
	path := v3Path + futuresPath + "/" + sweepsPath
	var resp SuccessResp
	return resp.Success, e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodDelete, path, nil, nil, true, &resp)
}

// GetCurrentMarginWindow returns the futures current margin window
func (e *Exchange) GetCurrentMarginWindow(ctx context.Context, marginProfileType string) (*CurrentMarginWindow, error) {
	if marginProfileType == "" {
		return nil, errMarginProfileTypeEmpty
	}
	vals := url.Values{}
	if marginProfileType != "" {
		vals.Set("margin_profile_type", marginProfileType)
	}
	path := v3Path + futuresPath + "/" + intradayPath + "/" + currentMarginWindowPath
	var resp *CurrentMarginWindow
	return resp, e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, path, vals, nil, true, &resp)
}

// GetFuturesBalanceSummary returns information on balances related to Coinbase Financial Markets futures trading
func (e *Exchange) GetFuturesBalanceSummary(ctx context.Context) (*FuturesBalanceSummary, error) {
	resp := struct {
		BalanceSummary FuturesBalanceSummary `json:"balance_summary"`
	}{}
	path := v3Path + futuresPath + "/" + balanceSummaryPath
	return &resp.BalanceSummary, e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, path, nil, nil, true, &resp)
}

// GetFuturesPositionByID returns information on an open futures position
func (e *Exchange) GetFuturesPositionByID(ctx context.Context, productID currency.Pair) (*FuturesPosition, error) {
	if productID.IsEmpty() {
		return nil, errProductIDEmpty
	}
	path := v3Path + futuresPath + "/" + positionsPath + "/" + productID.String()
	resp := struct {
		Position FuturesPosition `json:"position"`
	}{}
	return &resp.Position, e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, path, nil, nil, true, &resp)
}

// GetIntradayMarginSetting returns the futures intraday margin setting
func (e *Exchange) GetIntradayMarginSetting(ctx context.Context) (string, error) {
	resp := struct {
		Setting string `json:"setting"`
	}{}
	path := v3Path + futuresPath + "/" + intradayPath + "/" + marginSettingPath
	return resp.Setting, e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, path, nil, nil, true, &resp)
}

// ListFuturesPositions returns a list of all open futures positions
func (e *Exchange) ListFuturesPositions(ctx context.Context) ([]FuturesPosition, error) {
	resp := struct {
		Positions []FuturesPosition `json:"positions"`
	}{}
	path := v3Path + futuresPath + "/" + positionsPath
	return resp.Positions, e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, path, nil, nil, true, &resp)
}

// ListFuturesSweeps returns information on pending and/or processing requests to sweep funds
func (e *Exchange) ListFuturesSweeps(ctx context.Context) ([]SweepData, error) {
	resp := struct {
		Sweeps []SweepData `json:"sweeps"`
	}{}
	path := v3Path + futuresPath + "/" + sweepsPath
	return resp.Sweeps, e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, path, nil, nil, true, &resp)
}

// ScheduleFuturesSweep schedules a sweep of funds from a CFTC-regulated futures account to a Coinbase USD Spot wallet. Request submitted before 5 pm ET are processed the following business day, requests submitted after are processed in 2 business days. Only one sweep request can be pending at a time. Funds transferred depend on the excess available in the futures account. An amount of 0 will sweep all available excess funds
func (e *Exchange) ScheduleFuturesSweep(ctx context.Context, amount float64) (bool, error) {
	path := v3Path + futuresPath + "/" + sweepsPath + "/" + schedulePath
	var resp SuccessResp
	return resp.Success, e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, path, nil, futuresSweepReqBase{USDAmount: amount}, true, &resp)
}

// SetIntradayMarginSetting sets the futures intraday margin setting
func (e *Exchange) SetIntradayMarginSetting(ctx context.Context, setting string) error {
	if setting == "" {
		return errSettingEmpty
	}
	path := v3Path + futuresPath + "/" + intradayPath + "/" + marginSettingPath
	return e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, path, nil, marginSettingReqBase{Setting: setting}, true, nil)
}

// CancelOrders cancels orders by orderID. Can only cancel 100 orders per request
func (e *Exchange) CancelOrders(ctx context.Context, orderIDs []string) ([]OrderCancelDetail, error) {
	if len(orderIDs) == 0 {
		return nil, order.ErrOrderIDNotSet
	}
	if len(orderIDs) > 100 {
		return nil, errCancelLimitExceeded
	}
	path := v3Path + ordersPath + "/" + batchCancelpath
	resp := struct {
		Results []OrderCancelDetail `json:"results"`
	}{}
	return resp.Results, e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, path, nil, cancelOrdersReqBase{OrderIDs: orderIDs}, true, &resp)
}

// ClosePosition closes a position by client order ID, product ID, and size
func (e *Exchange) ClosePosition(ctx context.Context, clientOrderID string, productID currency.Pair, size float64) (*SuccessFailureConfig, error) {
	if clientOrderID == "" {
		return nil, order.ErrClientOrderIDMustBeSet
	}
	if productID.IsEmpty() {
		return nil, errProductIDEmpty
	}
	if size <= 0 {
		return nil, order.ErrAmountIsInvalid
	}
	path := v3Path + ordersPath + "/" + closePositionPath
	req := closePositionReqBase{
		ClientOrderID: clientOrderID,
		ProductID:     productID,
		Size:          size,
	}
	var resp *SuccessFailureConfig
	return resp, e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, path, nil, req, true, &resp)
}

// PlaceOrder places either a limit, market, or stop order
func (e *Exchange) PlaceOrder(ctx context.Context, ord *PlaceOrderInfo) (*SuccessFailureConfig, error) {
	if ord.ClientOID == "" {
		return nil, order.ErrClientOrderIDMustBeSet
	}
	if ord.ProductID == "" {
		return nil, errProductIDEmpty
	}
	if ord.BaseAmount <= 0 {
		return nil, order.ErrAmountIsInvalid
	}
	orderConfig, err := createOrderConfig(&ord.OrderInfo)
	if err != nil {
		return nil, err
	}
	req := placeOrderReqbase{
		ClientOID:                  ord.ClientOID,
		ProductID:                  ord.ProductID,
		Side:                       ord.Side,
		OrderConfiguration:         &orderConfig,
		RetailPortfolioID:          ord.RetailPortfolioID,
		PreviewID:                  ord.PreviewID,
		AttachedOrderConfiguration: &ord.AttachedOrderConfiguration,
		MarginType:                 FormatMarginType(ord.MarginType),
	}
	if ord.Leverage != 1 {
		req.Leverage = ord.Leverage
	}
	var resp *SuccessFailureConfig
	return resp, e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, v3Path+ordersPath, nil, req, true, &resp)
}

// EditOrder edits an order to a new size or price. Only limit orders with a good-till-cancelled time in force can be edited
func (e *Exchange) EditOrder(ctx context.Context, orderID string, size, price float64) (bool, error) {
	if orderID == "" {
		return false, order.ErrOrderIDNotSet
	}
	if size <= 0 && price <= 0 {
		return false, errSizeAndPriceZero
	}
	path := v3Path + ordersPath + "/" + editPath
	req := editOrderReqBase{
		OrderID: orderID,
		Size:    size,
		Price:   price,
	}
	var resp SuccessResp
	return resp.Success, e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, path, nil, req, true, &resp)
}

// EditOrderPreview simulates an edit order request, to preview the result. Only limit orders with a good-till-cancelled time in force can be edited.
func (e *Exchange) EditOrderPreview(ctx context.Context, orderID string, size, price float64) (*EditOrderPreviewResp, error) {
	if orderID == "" {
		return nil, order.ErrOrderIDNotSet
	}
	if size <= 0 && price <= 0 {
		return nil, errSizeAndPriceZero
	}
	path := v3Path + ordersPath + "/" + editPreviewPath
	req := editOrderReqBase{
		OrderID: orderID,
		Size:    size,
		Price:   price,
	}
	var resp *EditOrderPreviewResp
	return resp, e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, path, nil, req, true, &resp)
}

// GetOrderByID returns a single order by order id.
func (e *Exchange) GetOrderByID(ctx context.Context, orderID, clientOID string, userNativeCurrency currency.Code) (*GetOrderResponse, error) {
	if orderID == "" {
		return nil, order.ErrOrderIDNotSet
	}
	vals := url.Values{}
	if clientOID != "" {
		vals.Set("client_order_id", clientOID)
	}
	if !userNativeCurrency.IsEmpty() {
		vals.Set("user_native_currency", userNativeCurrency.String())
	}
	path := v3Path + ordersPath + "/" + historicalPath + "/" + orderID
	resp := struct {
		Order GetOrderResponse `json:"order"`
	}{}
	return &resp.Order, e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, path, vals, nil, true, &resp)
}

// ListFills returns information on recent order fills
func (e *Exchange) ListFills(ctx context.Context, orderIDs, tradeIDs []string, productIDs currency.Pairs, cursor int64, sortBy string, startDate, endDate time.Time, limit uint16) (*FillResponse, error) {
	vals, err := urlValsFromDateRange(startDate, endDate, "start_sequence_timestamp", "end_sequence_timestamp")
	if err != nil {
		return nil, err
	}
	for i := range orderIDs {
		vals.Add("order_ids", orderIDs[i])
	}
	for i := range tradeIDs {
		vals.Add("trade_ids", tradeIDs[i])
	}
	for i := range productIDs {
		vals.Add("product_ids", productIDs[i].String())
	}
	vals.Set("limit", strconv.FormatInt(int64(limit), 10))
	vals.Set("cursor", strconv.FormatInt(cursor, 10))
	if sortBy != "" {
		vals.Set("sort_by", sortBy)
	}
	path := v3Path + ordersPath + "/" + historicalPath + "/" + fillsPath
	var resp *FillResponse
	return resp, e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, path, vals, nil, true, &resp)
}

// ListOrders lists orders, filtered by their status
func (e *Exchange) ListOrders(ctx context.Context, req *ListOrdersReq) (*ListOrdersResp, error) {
	vals, err := urlValsFromDateRange(req.StartDate, req.EndDate, startDateString, endDateString)
	if err != nil {
		return nil, err
	}
	for x := range req.OrderStatus {
		vals.Add("order_status", req.OrderStatus[x])
	}
	for x := range req.OrderIDs {
		vals.Add("order_ids", req.OrderIDs[x])
	}
	for x := range req.TimeInForces {
		vals.Add("time_in_forces", req.TimeInForces[x])
	}
	for x := range req.OrderTypes {
		vals.Add("order_types", req.OrderTypes[x])
	}
	for x := range req.AssetFilters {
		vals.Add("asset_filters", req.AssetFilters[x])
	}
	for x := range req.ProductIDs {
		vals.Add("product_ids", req.ProductIDs[x].String())
	}
	if req.ProductType != "" {
		vals.Set("product_type", req.ProductType)
	}
	if req.OrderSide != "" {
		vals.Set("order_side", req.OrderSide)
	}
	if req.OrderPlacementSource != "" {
		vals.Set("order_placement_source", req.OrderPlacementSource)
	}
	if req.ContractExpiryType != "" {
		vals.Set("contract_expiry_type", req.ContractExpiryType)
	}
	if req.SortBy != "" {
		vals.Set("sort_by", req.SortBy)
	}
	vals.Set("cursor", strconv.FormatInt(req.Cursor, 10))
	vals.Set("limit", strconv.FormatInt(int64(req.Limit), 10))
	vals.Set("user_native_currency", req.UserNativeCurrency.String())
	vals.Set("retail_portfolio_id", req.RetailPortfolioID) // deprecated and only works for legacy API keys
	path := v3Path + ordersPath + "/" + historicalPath + "/" + batchPath
	var resp *ListOrdersResp
	return resp, e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, path, vals, nil, true, &resp)
}

// PreviewOrder simulates the results of an order request
func (e *Exchange) PreviewOrder(ctx context.Context, inf *PreviewOrderInfo) (*PreviewOrderResp, error) {
	if inf.BaseAmount <= 0 && inf.QuoteAmount <= 0 {
		return nil, order.ErrAmountIsInvalid
	}
	orderConfig, err := createOrderConfig(&inf.OrderInfo)
	if err != nil {
		return nil, err
	}
	req := previewOrderReqBase{
		ProductID:                  inf.ProductID,
		Side:                       inf.Side,
		OrderConfiguration:         &orderConfig,
		RetailPortfolioID:          inf.RetailPortfolioID,
		Leverage:                   inf.Leverage,
		AttachedOrderConfiguration: &inf.AttachedOrderConfiguration,
		MarginType:                 FormatMarginType(inf.MarginType),
	}
	var resp *PreviewOrderResp
	path := v3Path + ordersPath + "/" + previewPath
	return resp, e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, path, nil, req, true, &resp)
}

// GetPaymentMethodByID returns information on a single payment method associated with the user's account
func (e *Exchange) GetPaymentMethodByID(ctx context.Context, paymentMethodID string) (*PaymentMethodData, error) {
	if paymentMethodID == "" {
		return nil, errPaymentMethodEmpty
	}
	path := v3Path + paymentMethodsPath + "/" + paymentMethodID
	resp := struct {
		PaymentMethod PaymentMethodData `json:"payment_method"`
	}{}
	return &resp.PaymentMethod, e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, path, nil, nil, true, &resp)
}

// ListPaymentMethods returns a list of all payment methods associated with the user's account
func (e *Exchange) ListPaymentMethods(ctx context.Context) ([]PaymentMethodData, error) {
	resp := struct {
		PaymentMethods []PaymentMethodData `json:"payment_methods"`
	}{}
	path := v3Path + paymentMethodsPath
	return resp.PaymentMethods, e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, path, nil, paymentMethodReqBase{Currency: currency.BTC}, true, &resp)
}

// AllocatePortfolio allocates funds to a position in your perpetuals portfolio
func (e *Exchange) AllocatePortfolio(ctx context.Context, portfolioID, productID, cur string, amount float64) error {
	if portfolioID == "" {
		return errPortfolioIDEmpty
	}
	if productID == "" {
		return errProductIDEmpty
	}
	if cur == "" {
		return currency.ErrCurrencyCodeEmpty
	}
	if amount <= 0 {
		return order.ErrAmountIsInvalid
	}
	req := allocatePortfolioReqBase{
		PortfolioUUID: portfolioID,
		Symbol:        productID,
		Currency:      cur,
		Amount:        amount,
	}
	path := v3Path + intxPath + "/" + allocatePath
	return e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, path, nil, req, true, nil)
}

// GetPerpetualsPortfolioSummary returns a summary of your perpetuals portfolio
func (e *Exchange) GetPerpetualsPortfolioSummary(ctx context.Context, portfolioID string) (*PerpetualPortfolioResponse, error) {
	if portfolioID == "" {
		return nil, errPortfolioIDEmpty
	}
	path := v3Path + intxPath + "/" + portfolioPath + "/" + portfolioID
	resp := struct {
		Summary PerpetualPortfolioResponse `json:"summary"`
	}{}
	return &resp.Summary, e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, path, nil, nil, true, &resp)
}

// GetPerpetualsPositionByID returns information on a single open position in your perpetuals portfolio
func (e *Exchange) GetPerpetualsPositionByID(ctx context.Context, portfolioID string, productID currency.Pair) (*PerpPositionDetail, error) {
	if portfolioID == "" {
		return nil, errPortfolioIDEmpty
	}
	if productID.IsEmpty() {
		return nil, errProductIDEmpty
	}
	path := v3Path + intxPath + "/" + positionsPath + "/" + portfolioID + "/" + productID.String()
	resp := struct {
		Position PerpPositionDetail `json:"position"`
	}{}
	return &resp.Position, e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, path, nil, nil, true, &resp)
}

// GetPortfolioBalances returns the current balances for all assets in your portfolio
func (e *Exchange) GetPortfolioBalances(ctx context.Context, portfolioID string) ([]PortfolioBalancesResponse, error) {
	if portfolioID == "" {
		return nil, errPortfolioIDEmpty
	}
	path := v3Path + intxPath + "/" + balancesPath + "/" + portfolioID
	resp := struct {
		PortfolioBalances []PortfolioBalancesResponse `json:"portfolio_balances"`
	}{}
	return resp.PortfolioBalances, e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, path, nil, nil, true, &resp)
}

// GetAllPerpetualsPositions returns a list of all open positions in your perpetuals portfolio
func (e *Exchange) GetAllPerpetualsPositions(ctx context.Context, portfolioID string) (*AllPerpPosResponse, error) {
	if portfolioID == "" {
		return nil, errPortfolioIDEmpty
	}
	path := v3Path + intxPath + "/" + positionsPath + "/" + portfolioID
	var resp *AllPerpPosResponse
	return resp, e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, path, nil, nil, true, &resp)
}

// MultiAssetCollateralToggle allows for the toggling of multi-asset collateral on a portfolio
func (e *Exchange) MultiAssetCollateralToggle(ctx context.Context, portfolioID string, enabled bool) (bool, error) {
	if portfolioID == "" {
		return false, errPortfolioIDEmpty
	}
	path := v3Path + intxPath + "/" + multiAssetCollateralPath
	var resp struct {
		Enabled bool `json:"multi_asset_collateral_enabled"`
	}
	return resp.Enabled, e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, path, nil, assetCollateralToggleReqBase{PortfolioUUID: portfolioID, Enabled: enabled}, true, &resp)
}

// CreatePortfolio creates a new portfolio
func (e *Exchange) CreatePortfolio(ctx context.Context, name string) (*SimplePortfolioData, error) {
	if name == "" {
		return nil, errNameEmpty
	}
	resp := struct {
		Portfolio SimplePortfolioData `json:"portfolio"`
	}{}
	return &resp.Portfolio, e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, v3Path+portfoliosPath, nil, nameReqBase{Name: name}, true, &resp)
}

// DeletePortfolio deletes a portfolio
func (e *Exchange) DeletePortfolio(ctx context.Context, portfolioID string) error {
	if portfolioID == "" {
		return errPortfolioIDEmpty
	}
	path := v3Path + portfoliosPath + "/" + portfolioID
	return e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodDelete, path, nil, nil, true, nil)
}

// EditPortfolio edits the name of a portfolio
func (e *Exchange) EditPortfolio(ctx context.Context, portfolioID, name string) (*SimplePortfolioData, error) {
	if portfolioID == "" {
		return nil, errPortfolioIDEmpty
	}
	if name == "" {
		return nil, errNameEmpty
	}
	path := v3Path + portfoliosPath + "/" + portfolioID
	resp := struct {
		Portfolio SimplePortfolioData `json:"portfolio"`
	}{}
	return &resp.Portfolio, e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodPut, path, nil, nameReqBase{Name: name}, true, &resp)
}

// GetPortfolioByID provides detailed information on a single portfolio
func (e *Exchange) GetPortfolioByID(ctx context.Context, portfolioID string) (*DetailedPortfolioResponse, error) {
	if portfolioID == "" {
		return nil, errPortfolioIDEmpty
	}
	path := v3Path + portfoliosPath + "/" + portfolioID
	resp := struct {
		Breakdown DetailedPortfolioResponse `json:"breakdown"`
	}{}
	return &resp.Breakdown, e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, path, nil, nil, true, &resp)
}

// GetAllPortfolios returns a list of portfolios associated with the user
func (e *Exchange) GetAllPortfolios(ctx context.Context, portfolioType string) ([]SimplePortfolioData, error) {
	resp := struct {
		Portfolios []SimplePortfolioData `json:"portfolios"`
	}{}
	vals := url.Values{}
	if portfolioType != "" {
		vals.Set("portfolio_type", portfolioType)
	}
	return resp.Portfolios, e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, v3Path+portfoliosPath, vals, nil, true, &resp)
}

// MovePortfolioFunds transfers funds between portfolios
func (e *Exchange) MovePortfolioFunds(ctx context.Context, cur currency.Code, from, to string, amount float64) (*MovePortfolioFundsResponse, error) {
	if from == "" || to == "" {
		return nil, errPortfolioIDEmpty
	}
	if cur.IsEmpty() {
		return nil, currency.ErrCurrencyCodeEmpty
	}
	if amount <= 0 {
		return nil, order.ErrAmountIsInvalid
	}
	req := movePortfolioFundsReqBase{
		SourcePortfolioUUID: from,
		TargetPortfolioUUID: to,
		Funds: fundsData{
			Value:    amount,
			Currency: cur,
		},
	}
	path := v3Path + portfoliosPath + "/" + moveFundsPath
	var resp *MovePortfolioFundsResponse
	return resp, e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, path, nil, req, true, &resp)
}

// GetBestBidAsk returns the best bid/ask for all products. Can be filtered to certain products by passing through additional strings
func (e *Exchange) GetBestBidAsk(ctx context.Context, products []string) ([]ProductBook, error) {
	vals := url.Values{}
	for x := range products {
		vals.Add("product_ids", products[x])
	}
	resp := struct {
		Pricebooks []ProductBook `json:"pricebooks"`
	}{}
	return resp.Pricebooks, e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, v3Path+bestBidAskPath, vals, nil, true, &resp)
}

// GetTicker returns snapshot information about the last trades (ticks) and best bid/ask. Contrary to documentation, this does not tell you the 24h volume
func (e *Exchange) GetTicker(ctx context.Context, productID currency.Pair, limit uint16, startDate, endDate time.Time, authenticated bool) (*Ticker, error) {
	if productID.IsEmpty() {
		return nil, errProductIDEmpty
	}
	vals := url.Values{}
	vals.Set("limit", strconv.FormatInt(int64(limit), 10))
	if !startDate.IsZero() && !startDate.Equal(time.Time{}) {
		vals.Set("start", strconv.FormatInt(startDate.Unix(), 10))
	}
	if !endDate.IsZero() && !endDate.Equal(time.Time{}) {
		vals.Set("end", strconv.FormatInt(endDate.Unix(), 10))
	}
	var resp *Ticker
	if authenticated {
		path := v3Path + productsPath + "/" + productID.String() + "/" + tickerPath
		return resp, e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, path, vals, nil, true, &resp)
	}
	path := v3Path + marketPath + "/" + productsPath + "/" + productID.String() + "/" + tickerPath
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, path, vals, &resp)
}

// GetProductByID returns information on a single specified currency pair
func (e *Exchange) GetProductByID(ctx context.Context, productID currency.Pair, authenticated bool) (*Product, error) {
	if productID.IsEmpty() {
		return nil, errProductIDEmpty
	}
	var resp *Product
	if authenticated {
		path := v3Path + productsPath + "/" + productID.String()
		return resp, e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, path, nil, nil, true, &resp)
	}
	path := v3Path + marketPath + "/" + productsPath + "/" + productID.String()
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, path, nil, &resp)
}

// GetProductBookV3 returns a list of bids/asks for a single product
func (e *Exchange) GetProductBookV3(ctx context.Context, productID currency.Pair, limit uint16, aggregationIncrement float64, authenticated bool) (*ProductBookResp, error) {
	if productID.IsEmpty() {
		return nil, errProductIDEmpty
	}
	vals := url.Values{}
	vals.Set("product_id", productID.String())
	if limit != 0 {
		vals.Set("limit", strconv.FormatInt(int64(limit), 10))
	}
	if aggregationIncrement != 0 {
		vals.Set("aggregation_price_increment", strconv.FormatFloat(aggregationIncrement, 'f', -1, 64))
	}
	var resp *ProductBookResp
	if authenticated {
		return resp, e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, v3Path+productBookPath, vals, nil, true, &resp)
	}
	path := v3Path + marketPath + "/" + productBookPath
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, path, vals, &resp)
}

// GetHistoricKlines returns historic candles for a product. Candles are returned in grouped buckets based on requested granularity. Requests that return more than 300 data points are rejected
func (e *Exchange) GetHistoricKlines(ctx context.Context, productID string, granularity kline.Interval, startDate, endDate time.Time, authenticated bool) ([]kline.Candle, error) {
	if productID == "" {
		return nil, errProductIDEmpty
	}
	gran, ok := allowedGranularities[granularity]
	if !ok {
		return nil, fmt.Errorf("%w %v, allowed granularities are: %+v", kline.ErrUnsupportedInterval, granularity, allowedGranularities)
	}
	vals := url.Values{}
	vals.Set("start", strconv.FormatInt(startDate.Unix(), 10))
	vals.Set("end", strconv.FormatInt(endDate.Unix(), 10))
	vals.Set("granularity", gran)
	resp := struct {
		Candles []Klines `json:"candles"`
	}{}
	var err error
	if authenticated {
		path := v3Path + productsPath + "/" + productID + "/" + candlesPath
		err = e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, path, vals, nil, true, &resp)
	} else {
		path := v3Path + marketPath + "/" + productsPath + "/" + productID + "/" + candlesPath
		err = e.SendHTTPRequest(ctx, exchange.RestSpot, path, vals, &resp)
	}
	if err != nil {
		return nil, err
	}
	timeSeries := make([]kline.Candle, len(resp.Candles))
	for x := range resp.Candles {
		timeSeries[x] = kline.Candle{
			Time:   resp.Candles[x].Start.Time(),
			Low:    resp.Candles[x].Low.Float64(),
			High:   resp.Candles[x].High.Float64(),
			Open:   resp.Candles[x].Open.Float64(),
			Close:  resp.Candles[x].Close.Float64(),
			Volume: resp.Candles[x].Volume.Float64(),
		}
	}
	return timeSeries, nil
}

// GetAllProducts returns information on all currency pairs that are available for trading
// The getTradabilityStatus parameter is only used for authenticated requests, and will return the tradability status of SPOT products in their view_only field
// The getAllProducts parameter overrides the set productType; with it set to true, it will return both SPOT and Futures products
func (e *Exchange) GetAllProducts(ctx context.Context, limit, offset int32, productType, contractExpiryType, expiringContractStatus, productsSortOrder string, productIDs []string, getTradabilityStatus, getAllProducts, authenticated bool) (*AllProducts, error) {
	vals := url.Values{}
	vals.Set("limit", strconv.FormatInt(int64(limit), 10))
	if offset != 0 {
		vals.Set("offset", strconv.FormatInt(int64(offset), 10))
	}
	if productType != "" {
		vals.Set("product_type", productType)
	}
	if contractExpiryType != "" {
		vals.Set("contract_expiry_type", contractExpiryType)
	}
	if expiringContractStatus != "" {
		vals.Set("expiring_contract_status", expiringContractStatus)
	}
	if productsSortOrder != "" {
		vals.Set("products_sort_order", productsSortOrder)
	}
	for x := range productIDs {
		vals.Add("product_ids", productIDs[x])
	}
	vals.Set("get_tradability_status", strconv.FormatBool(getTradabilityStatus))
	vals.Set("get_all_products", strconv.FormatBool(getAllProducts))
	var resp *AllProducts
	if authenticated {
		return resp, e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, v3Path+productsPath, vals, nil, true, &resp)
	}
	path := v3Path + marketPath + "/" + productsPath
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, path, vals, &resp)
}

// GetV3Time returns the current server time, calling V3 of the API
func (e *Exchange) GetV3Time(ctx context.Context) (*ServerTimeV3, error) {
	var resp *ServerTimeV3
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, v3Path+timePath, nil, &resp)
}

// SendMoney can send funds to an email or cryptocurrency address (if "traType" is set to "send"), or to another one of the user's wallets or vaults (if "traType" is set to "transfer"). Coinbase may delay or cancel the transaction at their discretion. The "idem" parameter is an optional string for idempotency; a token with a max length of 100 characters, if a previous transaction included the same token as a parameter, the new transaction won't be processed, and information on the previous transaction will be returned instead
func (e *Exchange) SendMoney(ctx context.Context, traType, walletID, to, description, idem, destinationTag, network string, cur currency.Code, amount float64, skipNotifications bool, travelRuleData *TravelRule) (*TransactionData, error) {
	if traType == "" {
		return nil, errTransactionTypeEmpty
	}
	if walletID == "" {
		return nil, errWalletIDEmpty
	}
	if to == "" {
		return nil, errToEmpty
	}
	if amount <= 0 {
		return nil, order.ErrAmountIsInvalid
	}
	if cur.IsEmpty() {
		return nil, currency.ErrCurrencyCodeEmpty
	}
	path := v2Path + accountsPath + "/" + walletID + "/" + transactionsPath
	req := sendMoneyReqBase{
		Type:              traType,
		To:                to,
		Amount:            amount,
		Currency:          cur,
		Description:       description,
		SkipNotifications: skipNotifications,
		Idem:              idem,
		DestinationTag:    destinationTag,
		Network:           network,
		TravelRuleData:    travelRuleData,
	}
	resp := struct {
		Data TransactionData `json:"data"`
	}{}
	return &resp.Data, e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, path, nil, req, false, &resp)
}

// CreateAddress generates a crypto address for depositing to the specified wallet
func (e *Exchange) CreateAddress(ctx context.Context, walletID, name string) (*AddressData, error) {
	if walletID == "" {
		return nil, errWalletIDEmpty
	}
	path := v2Path + accountsPath + "/" + walletID + "/" + addressesPath
	resp := struct {
		Data AddressData `json:"data"`
	}{}
	return &resp.Data, e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, path, nil, nameReqBase{Name: name}, false, &resp)
}

// GetAllAddresses returns information on all addresses associated with a wallet
func (e *Exchange) GetAllAddresses(ctx context.Context, walletID string, pag PaginationInp) (*GetAllAddrResponse, error) {
	if walletID == "" {
		return nil, errWalletIDEmpty
	}
	path := v2Path + accountsPath + "/" + walletID + "/" + addressesPath
	vals := urlValsFromPagination(pag)
	var resp *GetAllAddrResponse
	return resp, e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, path, vals, nil, false, &resp)
}

// GetAddressByID returns information on a single address associated with the specified wallet
func (e *Exchange) GetAddressByID(ctx context.Context, walletID, addressID string) (*AddressData, error) {
	if walletID == "" {
		return nil, errWalletIDEmpty
	}
	if addressID == "" {
		return nil, errAddressIDEmpty
	}
	path := v2Path + accountsPath + "/" + walletID + "/" + addressesPath + "/" + addressID
	resp := struct {
		Data AddressData `json:"data"`
	}{}
	return &resp.Data, e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, path, nil, nil, false, &resp)
}

// GetAddressTransactions returns a list of transactions associated with the specified address
func (e *Exchange) GetAddressTransactions(ctx context.Context, walletID, addressID string, pag PaginationInp) (*ManyTransactionsResp, error) {
	if walletID == "" {
		return nil, errWalletIDEmpty
	}
	if addressID == "" {
		return nil, errAddressIDEmpty
	}
	path := v2Path + accountsPath + "/" + walletID + "/" + addressesPath + "/" + addressID + "/" + transactionsPath
	vals := urlValsFromPagination(pag)
	var resp *ManyTransactionsResp
	return resp, e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, path, vals, nil, false, &resp)
}

// FiatTransfer prepares and optionally processes a transfer of funds between the exchange and a fiat payment method. "Deposit" signifies funds going from exchange to bank, "withdraw" signifies funds going from bank to exchange
func (e *Exchange) FiatTransfer(ctx context.Context, walletID, cur, paymentMethod string, amount float64, commit bool, transferType FiatTransferType) (*DeposWithdrData, error) {
	if walletID == "" {
		return nil, errWalletIDEmpty
	}
	if amount <= 0 {
		return nil, order.ErrAmountIsInvalid
	}
	if cur == "" {
		return nil, currency.ErrCurrencyCodeEmpty
	}
	if paymentMethod == "" {
		return nil, errPaymentMethodEmpty
	}
	var path string
	switch transferType {
	case FiatDeposit:
		path = v2Path + accountsPath + "/" + walletID + "/" + depositsPath
	case FiatWithdrawal:
		path = v2Path + accountsPath + "/" + walletID + "/" + withdrawalsPath
	}
	req := fiatTransferReqBase{
		Currency:      cur,
		PaymentMethod: paymentMethod,
		Amount:        amount,
		Commit:        commit,
	}
	resp := struct {
		Data DeposWithdrData `json:"transfer"`
	}{}
	return &resp.Data, e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, path, nil, req, false, &resp)
}

// CommitTransfer processes a deposit/withdrawal that was created with the "commit" parameter set to false
func (e *Exchange) CommitTransfer(ctx context.Context, walletID, depositID string, transferType FiatTransferType) (*DeposWithdrData, error) {
	if walletID == "" {
		return nil, errWalletIDEmpty
	}
	if depositID == "" {
		return nil, errDepositIDEmpty
	}
	var path string
	switch transferType {
	case FiatDeposit:
		path = v2Path + accountsPath + "/" + walletID + "/" + depositsPath + "/" + depositID + "/" + commitPath
	case FiatWithdrawal:
		path = v2Path + accountsPath + "/" + walletID + "/" + withdrawalsPath + "/" + depositID + "/" + commitPath
	}
	resp := struct {
		Data DeposWithdrData `json:"data"`
	}{}
	return &resp.Data, e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, path, nil, nil, false, &resp)
}

// GetAllFiatTransfers returns a list of transfers either to or from fiat payment methods and the specified wallet
func (e *Exchange) GetAllFiatTransfers(ctx context.Context, walletID string, pag PaginationInp, transferType FiatTransferType) (*ManyDeposWithdrResp, error) {
	if walletID == "" {
		return nil, errWalletIDEmpty
	}
	var path string
	switch transferType {
	case FiatDeposit:
		path = v2Path + accountsPath + "/" + walletID + "/" + depositsPath
	case FiatWithdrawal:
		path = v2Path + accountsPath + "/" + walletID + "/" + withdrawalsPath
	}
	vals := urlValsFromPagination(pag)
	var resp *ManyDeposWithdrResp
	return resp, e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, path, vals, nil, false, &resp)
}

// GetFiatTransferByID returns information on a single deposit/withdrawal associated with the specified wallet
func (e *Exchange) GetFiatTransferByID(ctx context.Context, walletID, depositID string, transferType FiatTransferType) (*DeposWithdrData, error) {
	if walletID == "" {
		return nil, errWalletIDEmpty
	}
	if depositID == "" {
		return nil, errDepositIDEmpty
	}
	var path string
	switch transferType {
	case FiatDeposit:
		path = v2Path + accountsPath + "/" + walletID + "/" + depositsPath + "/" + depositID
	case FiatWithdrawal:
		path = v2Path + accountsPath + "/" + walletID + "/" + withdrawalsPath + "/" + depositID
	}
	resp := struct {
		Data DeposWithdrData `json:"data"`
	}{}
	return &resp.Data, e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, path, nil, nil, false, &resp)
}

// GetAllWallets lists all accounts associated with the API key
func (e *Exchange) GetAllWallets(ctx context.Context, pag PaginationInp) (*GetAllWalletsResponse, error) {
	vals := urlValsFromPagination(pag)
	var resp *GetAllWalletsResponse
	return resp, e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, v2Path+accountsPath, vals, nil, false, &resp)
}

// GetWalletByID returns information about a single wallet. In lieu of a wallet ID, a currency can be provided to get the primary account for that currency
func (e *Exchange) GetWalletByID(ctx context.Context, walletID string, cur currency.Code) (*WalletData, error) {
	if (walletID == "" && cur.IsEmpty()) || (walletID != "" && !cur.IsEmpty()) {
		return nil, errCurrWalletConflict
	}
	var path string
	if walletID != "" {
		path = v2Path + accountsPath + "/" + walletID
	}
	if !cur.IsEmpty() {
		path = v2Path + accountsPath + "/" + cur.String()
	}
	resp := struct {
		Data WalletData `json:"data"`
	}{}
	return &resp.Data, e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, path, nil, nil, false, &resp)
}

// GetAllTransactions returns a list of transactions associated with the specified wallet
func (e *Exchange) GetAllTransactions(ctx context.Context, walletID string, pag PaginationInp) (*ManyTransactionsResp, error) {
	if walletID == "" {
		return nil, errWalletIDEmpty
	}
	vals := urlValsFromPagination(pag)
	path := v2Path + accountsPath + "/" + walletID + "/" + transactionsPath
	var resp *ManyTransactionsResp
	return resp, e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, path, vals, nil, false, &resp)
}

// GetTransactionByID returns information on a single transaction associated with the specified wallet
func (e *Exchange) GetTransactionByID(ctx context.Context, walletID, transactionID string) (*TransactionData, error) {
	if walletID == "" {
		return nil, errWalletIDEmpty
	}
	if transactionID == "" {
		return nil, errTransactionIDEmpty
	}
	path := v2Path + accountsPath + "/" + walletID + "/" + transactionsPath + "/" + transactionID
	resp := struct {
		Data TransactionData `json:"data"`
	}{}
	return &resp.Data, e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, path, nil, nil, false, &resp)
}

// GetFiatCurrencies lists currencies that Coinbase knows about
func (e *Exchange) GetFiatCurrencies(ctx context.Context) ([]FiatData, error) {
	resp := struct {
		Data []FiatData `json:"data"`
	}{}
	return resp.Data, e.SendHTTPRequest(ctx, exchange.RestSpot, v2Path+currenciesPath, nil, &resp)
}

// GetCryptocurrencies lists cryptocurrencies that Coinbase knows about
func (e *Exchange) GetCryptocurrencies(ctx context.Context) ([]CryptoData, error) {
	resp := struct {
		Data []CryptoData `json:"data"`
	}{}
	path := v2Path + currenciesPath + "/" + cryptoPath
	return resp.Data, e.SendHTTPRequest(ctx, exchange.RestSpot, path, nil, &resp)
}

// GetExchangeRates returns exchange rates for the specified currency. If none is specified, it defaults to USD
func (e *Exchange) GetExchangeRates(ctx context.Context, cur string) (*GetExchangeRatesResp, error) {
	resp := struct {
		Data GetExchangeRatesResp `json:"data"`
	}{}
	vals := url.Values{}
	vals.Set("currency", cur)
	return &resp.Data, e.SendHTTPRequest(ctx, exchange.RestSpot, v2Path+exchangeRatesPath, vals, &resp)
}

// GetPrice returns the price the spot/buy/sell price for the specified currency pair, including the standard Coinbase fee of 1%, but excluding any other fees
func (e *Exchange) GetPrice(ctx context.Context, currencyPair, priceType string) (*GetPriceResp, error) {
	var path string
	switch priceType {
	case "spot", "buy", "sell":
		path = v2Path + pricesPath + "/" + currencyPair + "/" + priceType
	default:
		return nil, errInvalidPriceType
	}
	resp := struct {
		Data GetPriceResp `json:"data"`
	}{}
	return &resp.Data, e.SendHTTPRequest(ctx, exchange.RestSpot, path, nil, &resp)
}

// GetV2Time returns the current server time, calling V2 of the API
func (e *Exchange) GetV2Time(ctx context.Context) (*ServerTimeV2, error) {
	resp := struct {
		Data ServerTimeV2 `json:"data"`
	}{}
	return &resp.Data, e.SendHTTPRequest(ctx, exchange.RestSpot, v2Path+timePath, nil, &resp)
}

// GetCurrentUser returns information about the user associated with the API key
func (e *Exchange) GetCurrentUser(ctx context.Context) (*UserResponse, error) {
	resp := struct {
		Data UserResponse `json:"data"`
	}{}
	return &resp.Data, e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, v2Path+userPath, nil, nil, false, &resp)
}

// GetAllCurrencies returns a list of all currencies that Coinbase knows about. These aren't necessarily tradable
func (e *Exchange) GetAllCurrencies(ctx context.Context) ([]CurrencyData, error) {
	var resp []CurrencyData
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpotSupplementary, currenciesPath, nil, &resp)
}

// GetACurrency returns information on a single currency specified by the user
func (e *Exchange) GetACurrency(ctx context.Context, cur string) (*CurrencyData, error) {
	if cur == "" {
		return nil, currency.ErrCurrencyCodeEmpty
	}
	var resp *CurrencyData
	path := currenciesPath + "/" + cur
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpotSupplementary, path, nil, &resp)
}

// GetAllTradingPairs returns a list of currency pairs which are available for trading
func (e *Exchange) GetAllTradingPairs(ctx context.Context, pairType string) ([]PairData, error) {
	var resp []PairData
	vals := url.Values{}
	if pairType != "" {
		vals.Set("type", pairType)
	}
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpotSupplementary, productsPath, vals, &resp)
}

// GetAllPairVolumes returns a list of currency pairs and their associated volumes
func (e *Exchange) GetAllPairVolumes(ctx context.Context) ([]PairVolumeData, error) {
	var resp []PairVolumeData
	path := productsPath + "/" + volumeSummaryPath
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpotSupplementary, path, nil, &resp)
}

// GetPairDetails returns information on a single currency pair
func (e *Exchange) GetPairDetails(ctx context.Context, pair string) (*PairData, error) {
	if pair == "" {
		return nil, currency.ErrCurrencyPairEmpty
	}
	var resp *PairData
	path := productsPath + "/" + pair
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpotSupplementary, path, nil, &resp)
}

// GetProductBookV1 returns the order book for the specified currency pair. Level 1 only returns the best bids and asks, Level 2 returns the full order book with orders at the same price aggregated, Level 3 returns the full non-aggregated order book.
func (e *Exchange) GetProductBookV1(ctx context.Context, pair string, level uint8) (*OrderBookResp, error) {
	if pair == "" {
		return nil, currency.ErrCurrencyPairEmpty
	}
	var resp *OrderBookResp
	vals := url.Values{}
	vals.Set("level", strconv.FormatUint(uint64(level), 10))
	path := productsPath + "/" + pair + "/" + bookPath
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpotSupplementary, path, vals, &resp)
}

// GetProductCandles returns historical market data for the specified currency pair.
func (e *Exchange) GetProductCandles(ctx context.Context, pair string, granularity uint32, startTime, endTime time.Time) ([]Candle, error) {
	if pair == "" {
		return nil, currency.ErrCurrencyPairEmpty
	}
	vals, err := urlValsFromDateRange(startTime, endTime, "start", "end")
	if err != nil {
		return nil, err
	}
	if granularity != 0 {
		vals.Set("granularity", strconv.FormatUint(uint64(granularity), 10))
	}
	path := productsPath + "/" + pair + "/" + candlesPath
	var resp []Candle
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpotSupplementary, path, vals, &resp)
}

// GetProductStats returns information on a specific pair's price and volume
func (e *Exchange) GetProductStats(ctx context.Context, pair string) (*ProductStats, error) {
	if pair == "" {
		return nil, currency.ErrCurrencyPairEmpty
	}
	path := productsPath + "/" + pair + "/" + statsPath
	var resp *ProductStats
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpotSupplementary, path, nil, &resp)
}

// GetProductTicker returns the ticker for the specified currency pair
func (e *Exchange) GetProductTicker(ctx context.Context, pair string) (*ProductTicker, error) {
	if pair == "" {
		return nil, currency.ErrCurrencyPairEmpty
	}
	path := productsPath + "/" + pair + "/" + tickerPath
	var resp *ProductTicker
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpotSupplementary, path, nil, &resp)
}

// GetProductTrades returns a list of the latest traides for a pair
func (e *Exchange) GetProductTrades(ctx context.Context, pair, step, direction string, limit int64) ([]ProductTrades, error) {
	if pair == "" {
		return nil, currency.ErrCurrencyPairEmpty
	}
	vals := url.Values{}
	if step != "" {
		vals.Set(direction, step)
	}
	vals.Set("limit", strconv.FormatInt(limit, 10))
	path := productsPath + "/" + pair + "/" + tradesPath
	var resp []ProductTrades
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpotSupplementary, path, vals, &resp)
}

// GetAllWrappedAssets returns a list of supported wrapped assets
func (e *Exchange) GetAllWrappedAssets(ctx context.Context) (*AllWrappedAssets, error) {
	var resp *AllWrappedAssets
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpotSupplementary, wrappedAssetsPath, nil, &resp)
}

// GetWrappedAssetDetails returns information on a single wrapped asset
func (e *Exchange) GetWrappedAssetDetails(ctx context.Context, wrappedAsset string) (*WrappedAsset, error) {
	if wrappedAsset == "" {
		return nil, errWrappedAssetEmpty
	}
	var resp *WrappedAsset
	path := wrappedAssetsPath + "/" + wrappedAsset
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpotSupplementary, path, nil, &resp)
}

// GetWrappedAssetConversionRate returns the conversion rate for the specified wrapped asset
func (e *Exchange) GetWrappedAssetConversionRate(ctx context.Context, wrappedAsset string) (*WrappedAssetConversionRate, error) {
	if wrappedAsset == "" {
		return nil, errWrappedAssetEmpty
	}
	var resp *WrappedAssetConversionRate
	path := wrappedAssetsPath + "/" + wrappedAsset + "/" + conversionRatePath
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpotSupplementary, path, nil, &resp)
}

// SendHTTPRequest sends an unauthenticated HTTP request
func (e *Exchange) SendHTTPRequest(ctx context.Context, ep exchange.URL, path string, vals url.Values, result any) error {
	endpoint, err := e.API.Endpoints.GetURL(ep)
	if err != nil {
		return err
	}
	rLim := PubRate
	if strings.Contains(path, v2Path) {
		rLim = V2Rate
	}
	path = common.EncodeURLValues(path, vals)
	item := &request.Item{
		Method:                 http.MethodGet,
		Path:                   endpoint + path,
		Result:                 result,
		Verbose:                e.Verbose,
		HTTPDebugging:          e.HTTPDebugging,
		HTTPRecording:          e.HTTPRecording,
		HTTPMockDataSliceLimit: e.HTTPMockDataSliceLimit,
	}
	return e.SendPayload(ctx, rLim, func() (*request.Item, error) {
		return item, nil
	}, request.UnauthenticatedRequest)
}

// SendAuthenticatedHTTPRequest sends an authenticated HTTP request
func (e *Exchange) SendAuthenticatedHTTPRequest(ctx context.Context, ep exchange.URL, method, path string, queryParams url.Values, payload any, isVersion3 bool, result any) (err error) {
	endpoint, err := e.API.Endpoints.GetURL(ep)
	if err != nil {
		return err
	}
	if len(endpoint) < 8 {
		return errEndpointPathInvalid
	}
	interim := json.RawMessage{}
	newRequest := func() (*request.Item, error) {
		payloadBytes := []byte("")
		if payload != nil {
			if payloadBytes, err = json.Marshal(payload); err != nil {
				return nil, err
			}
		}
		var jwt string
		if jwt, _, err = e.GetJWT(ctx, method+" "+endpoint[8:]+path); err != nil {
			return nil, err
		}
		headers := make(map[string]string)
		headers["Content-Type"] = "application/json"
		headers["CB-VERSION"] = "2025-03-26"
		headers["Authorization"] = "Bearer " + jwt
		return &request.Item{
			Method:                 method,
			Path:                   endpoint + common.EncodeURLValues(path, queryParams),
			Headers:                headers,
			Body:                   bytes.NewBuffer(payloadBytes),
			Result:                 &interim,
			Verbose:                e.Verbose,
			HTTPDebugging:          e.HTTPDebugging,
			HTTPRecording:          e.HTTPRecording,
			HTTPMockDataSliceLimit: e.HTTPMockDataSliceLimit,
		}, nil
	}
	rateLim := V2Rate
	if isVersion3 {
		rateLim = V3Rate
	}
	if err := e.SendPayload(ctx, rateLim, newRequest, request.AuthenticatedRequest); err != nil {
		return err
	}
	// Doing this error handling because the docs indicate that errors can be returned even with a 200 status code, and that these errors can be buried in the JSON returned
	singleErrCap := struct {
		ErrorResponse ErrorResponse `json:"error_response"`
	}{}
	if err := json.Unmarshal(interim, &singleErrCap); err == nil {
		if singleErrCap.ErrorResponse.ErrorType != "" {
			return fmt.Errorf("message: %s, error type: %s, error details: %s, edit failure reason: %s, preview failure reason: %s, new order failure reason: %s", singleErrCap.ErrorResponse.Message, singleErrCap.ErrorResponse.ErrorType, singleErrCap.ErrorResponse.ErrorDetails, singleErrCap.ErrorResponse.EditFailureReason, singleErrCap.ErrorResponse.PreviewFailureReason, singleErrCap.ErrorResponse.NewOrderFailureReason)
		}
	}
	manyErrCap := struct {
		Results []ManyErrors `json:"results"`
		Errors  []ManyErrors `json:"errors"`
	}{}
	if err := json.Unmarshal(interim, &manyErrCap); err == nil {
		errMessage := ""
		for i := range manyErrCap.Errors {
			if !manyErrCap.Errors[i].Success && (manyErrCap.Errors[i].EditFailureReason != "" || manyErrCap.Errors[i].PreviewFailureReason != "") {
				errMessage += fmt.Sprintf("order id: %s, failure reason: %s, edit failure reason: %s, preview failure reason: %s", manyErrCap.Errors[i].OrderID, manyErrCap.Errors[i].FailureReason, manyErrCap.Errors[i].EditFailureReason, manyErrCap.Errors[i].PreviewFailureReason)
			}
		}
		for i := range manyErrCap.Results {
			if !manyErrCap.Results[i].Success && (manyErrCap.Results[i].EditFailureReason != "" || manyErrCap.Results[i].PreviewFailureReason != "") {
				errMessage += fmt.Sprintf("order id: %s, failure reason: %s, edit failure reason: %s, preview failure reason: %s", manyErrCap.Results[i].OrderID, manyErrCap.Results[i].FailureReason, manyErrCap.Results[i].EditFailureReason, manyErrCap.Results[i].PreviewFailureReason)
			}
		}
		if errMessage != "" {
			return errors.New(errMessage)
		}
	}
	if result == nil {
		return nil
	}
	return json.Unmarshal(interim, result)
}

// GetJWT generates a new JWT
func (e *Exchange) GetJWT(ctx context.Context, uri string) (string, time.Time, error) {
	creds, err := e.GetCredentials(ctx)
	if err != nil {
		return "", time.Time{}, err
	}
	block, _ := pem.Decode([]byte(creds.Secret))
	if block == nil {
		return "", time.Time{}, errDecodingPrivateKey
	}
	privateKey, err := x509.ParseECPrivateKey(block.Bytes)
	if err != nil {
		return "", time.Time{}, err
	}
	nonce, err := common.GenerateRandomString(16, "1234567890ABCDEF")
	if err != nil {
		return "", time.Time{}, err
	}
	head := map[string]any{
		"kid":   creds.Key,
		"typ":   "JWT",
		"alg":   "ES256",
		"nonce": nonce,
	}
	headJSON, err := json.Marshal(head)
	if err != nil {
		return "", time.Time{}, err
	}
	headEnc := base64.RawURLEncoding.EncodeToString(headJSON)
	regTime := time.Now()
	body := map[string]any{
		"iss": "cdp",
		"nbf": regTime.Unix(),
		// As per documentation, the JWT expires after two minutes, with the exchange expecting this expiry time to be set accordingly
		"exp": regTime.Add(2 * time.Minute).Unix(),
		"sub": creds.Key,
	}
	if uri != "" {
		body["uri"] = uri
	}
	bodyJSON, err := json.Marshal(body)
	if err != nil {
		return "", time.Time{}, err
	}
	bodyEnc := base64.RawURLEncoding.EncodeToString(bodyJSON)
	signingInput := headEnc + "." + bodyEnc
	hash := sha256.Sum256([]byte(signingInput))
	r, s, err := ecdsa.Sign(rand.Reader, privateKey, hash[:])
	if err != nil {
		return "", time.Time{}, err
	}
	n := privateKey.Params().N
	halfN := new(big.Int).Rsh(n, 1)
	if s.Cmp(halfN) == 1 {
		s.Sub(n, s)
	}
	rb := r.Bytes()
	sb := s.Bytes()
	sig := make([]byte, 64)
	copy(sig[32-len(rb):32], rb)
	copy(sig[64-len(sb):], sb)
	sigEnc := base64.RawURLEncoding.EncodeToString(sig)
	return signingInput + "." + sigEnc, regTime.Add(2 * time.Minute), nil
}

// GetFee returns an estimate of fee based on type of transaction
func (e *Exchange) GetFee(ctx context.Context, feeBuilder *exchange.FeeBuilder) (float64, error) {
	if feeBuilder == nil {
		return 0, fmt.Errorf("%T %w", feeBuilder, common.ErrNilPointer)
	}
	var fee float64
	switch {
	case !isStablePair(feeBuilder.Pair) && feeBuilder.FeeType == exchange.CryptocurrencyTradeFee:
		fees, err := e.GetTransactionSummary(ctx, time.Now().Add(-time.Hour*24*30), time.Now(), "", "", "")
		if err != nil {
			return 0, err
		}
		if feeBuilder.IsMaker {
			fee = fees.FeeTier.MakerFeeRate.Float64()
		} else {
			fee = fees.FeeTier.TakerFeeRate.Float64()
		}
	case feeBuilder.IsMaker && isStablePair(feeBuilder.Pair) && (feeBuilder.FeeType == exchange.CryptocurrencyTradeFee || feeBuilder.FeeType == exchange.OfflineTradeFee):
		fee = StablePairMakerFee
	case !feeBuilder.IsMaker && isStablePair(feeBuilder.Pair) && (feeBuilder.FeeType == exchange.CryptocurrencyTradeFee || feeBuilder.FeeType == exchange.OfflineTradeFee):
		fee = WorstCaseStablePairTakerFee
	case feeBuilder.IsMaker && !isStablePair(feeBuilder.Pair) && feeBuilder.FeeType == exchange.OfflineTradeFee:
		fee = WorstCaseMakerFee
	case !feeBuilder.IsMaker && !isStablePair(feeBuilder.Pair) && feeBuilder.FeeType == exchange.OfflineTradeFee:
		fee = WorstCaseTakerFee
	default:
		return 0, errFeeTypeNotSupported
	}
	return fee * feeBuilder.Amount * feeBuilder.PurchasePrice, nil
}

var stableMap = map[key.PairAsset]bool{
	{Base: currency.USDT.Item, Quote: currency.USD.Item}:  true,
	{Base: currency.USDT.Item, Quote: currency.EUR.Item}:  true,
	{Base: currency.USDC.Item, Quote: currency.EUR.Item}:  true,
	{Base: currency.USDC.Item, Quote: currency.GBP.Item}:  true,
	{Base: currency.USDT.Item, Quote: currency.GBP.Item}:  true,
	{Base: currency.USDT.Item, Quote: currency.USDC.Item}: true,
	{Base: currency.DAI.Item, Quote: currency.USD.Item}:   true,
	{Base: currency.CBETH.Item, Quote: currency.ETH.Item}: true,
	{Base: currency.PYUSD.Item, Quote: currency.USD.Item}: true,
	{Base: currency.EUROC.Item, Quote: currency.USD.Item}: true,
	{Base: currency.GUSD.Item, Quote: currency.USD.Item}:  true,
	{Base: currency.EUROC.Item, Quote: currency.EUR.Item}: true,
	{Base: currency.WBTC.Item, Quote: currency.BTC.Item}:  true,
	{Base: currency.LSETH.Item, Quote: currency.ETH.Item}: true,
	{Base: currency.GYEN.Item, Quote: currency.USD.Item}:  true,
	{Base: currency.PAX.Item, Quote: currency.USD.Item}:   true,
}

// IsStablePair returns true if the currency pair is considered a "stable pair" by Coinbase
func isStablePair(pair currency.Pair) bool {
	return stableMap[key.PairAsset{Base: pair.Base.Item, Quote: pair.Quote.Item}]
}

// urlValsFromDateRange encodes a set of parameters indicating start and end dates
func urlValsFromDateRange(startDate, endDate time.Time, labelStart, labelEnd string) (url.Values, error) {
	values := url.Values{}
	if err := common.StartEndTimeCheck(startDate, endDate); err != nil {
		if errors.Is(err, common.ErrDateUnset) {
			return values, nil
		}
		return nil, err
	}
	if labelStart == "" || labelEnd == "" {
		return nil, errDateLabelEmpty
	}
	values.Set(labelStart, startDate.Format(time.RFC3339))
	values.Set(labelEnd, endDate.Format(time.RFC3339))
	return values, nil
}

// urlValsFromPagination formats pagination information in the way the exchange expects
func urlValsFromPagination(pag PaginationInp) url.Values {
	values := url.Values{}
	if pag.Limit != 0 {
		values.Set("limit", strconv.FormatInt(int64(pag.Limit), 10))
	}
	if pag.OrderAscend {
		values.Set("order", "asc")
	}
	if pag.StartingAfter != "" {
		values.Set("starting_after", pag.StartingAfter)
	}
	if pag.EndingBefore != "" {
		values.Set("ending_before", pag.EndingBefore)
	}
	return values
}

// createOrderConfig populates the OrderConfiguration struct
func createOrderConfig(sharedParams *OrderInfo) (OrderConfiguration, error) {
	if sharedParams == nil {
		return OrderConfiguration{}, fmt.Errorf("%T %w", sharedParams, common.ErrNilPointer)
	}
	var orderConfig OrderConfiguration
	switch sharedParams.OrderType {
	case order.Market:
		if sharedParams.BaseAmount != 0 {
			orderConfig.MarketMarketIOC = &MarketMarketIOC{BaseSize: types.Number(sharedParams.BaseAmount), RFQDisabled: sharedParams.RFQDisabled}
		}
		if sharedParams.QuoteAmount != 0 {
			orderConfig.MarketMarketIOC = &MarketMarketIOC{QuoteSize: types.Number(sharedParams.QuoteAmount), RFQDisabled: sharedParams.RFQDisabled}
		}
	case order.Limit:
		switch {
		case sharedParams.TimeInForce == order.StopOrReduce:
			orderConfig.SORLimitIOC = &QuoteBaseLimit{BaseSize: types.Number(sharedParams.BaseAmount), QuoteSize: types.Number(sharedParams.QuoteAmount), LimitPrice: types.Number(sharedParams.LimitPrice), RFQDisabled: sharedParams.RFQDisabled}
		case sharedParams.TimeInForce == order.FillOrKill:
			orderConfig.LimitLimitFOK = &QuoteBaseLimit{BaseSize: types.Number(sharedParams.BaseAmount), QuoteSize: types.Number(sharedParams.QuoteAmount), LimitPrice: types.Number(sharedParams.LimitPrice), RFQDisabled: sharedParams.RFQDisabled}
		case sharedParams.EndTime.IsZero():
			orderConfig.LimitLimitGTC = &LimitLimitGTC{LimitPrice: types.Number(sharedParams.LimitPrice), PostOnly: sharedParams.PostOnly, RFQDisabled: sharedParams.RFQDisabled}
			if sharedParams.BaseAmount != 0 {
				orderConfig.LimitLimitGTC.BaseSize = types.Number(sharedParams.BaseAmount)
			}
			if sharedParams.QuoteAmount != 0 {
				orderConfig.LimitLimitGTC.QuoteSize = types.Number(sharedParams.QuoteAmount)
			}
		default:
			if sharedParams.EndTime.Before(time.Now()) {
				return orderConfig, errEndTimeInPast
			}
			orderConfig.LimitLimitGTD = &LimitLimitGTD{LimitPrice: types.Number(sharedParams.LimitPrice), PostOnly: sharedParams.PostOnly, EndTime: sharedParams.EndTime, RFQDisabled: sharedParams.RFQDisabled}
			if sharedParams.BaseAmount != 0 {
				orderConfig.LimitLimitGTD.BaseSize = types.Number(sharedParams.BaseAmount)
			}
			if sharedParams.QuoteAmount != 0 {
				orderConfig.LimitLimitGTD.QuoteSize = types.Number(sharedParams.QuoteAmount)
			}
		}
	case order.TWAP:
		if sharedParams.EndTime.Before(time.Now()) {
			return orderConfig, errEndTimeInPast
		}
		orderConfig.TWAPLimitGTD = &TWAPLimitGTD{StartTime: time.Now(), EndTime: sharedParams.EndTime, LimitPrice: types.Number(sharedParams.LimitPrice), NumberBuckets: sharedParams.BucketNumber, BucketSize: types.Number(sharedParams.BucketSize), BucketDuration: strconv.FormatFloat(sharedParams.BucketDuration.Seconds(), 'f', -1, 64) + "s"}
	case order.StopLimit:
		if sharedParams.EndTime.IsZero() {
			orderConfig.StopLimitStopLimitGTC = &StopLimitStopLimitGTC{LimitPrice: types.Number(sharedParams.LimitPrice), StopPrice: types.Number(sharedParams.StopPrice), StopDirection: sharedParams.StopDirection}
			if sharedParams.BaseAmount != 0 {
				orderConfig.StopLimitStopLimitGTC.BaseSize = types.Number(sharedParams.BaseAmount)
			}
			if sharedParams.QuoteAmount != 0 {
				orderConfig.StopLimitStopLimitGTC.QuoteSize = types.Number(sharedParams.QuoteAmount)
			}
		} else {
			if sharedParams.EndTime.Before(time.Now()) {
				return orderConfig, errEndTimeInPast
			}
			orderConfig.StopLimitStopLimitGTD = &StopLimitStopLimitGTD{LimitPrice: types.Number(sharedParams.LimitPrice), StopPrice: types.Number(sharedParams.StopPrice), StopDirection: sharedParams.StopDirection, EndTime: sharedParams.EndTime}
			if sharedParams.BaseAmount != 0 {
				orderConfig.StopLimitStopLimitGTD.BaseSize = types.Number(sharedParams.BaseAmount)
			}
			if sharedParams.QuoteAmount != 0 {
				orderConfig.StopLimitStopLimitGTD.QuoteSize = types.Number(sharedParams.QuoteAmount)
			}
		}
	case order.Bracket:
		if sharedParams.EndTime.IsZero() {
			orderConfig.TriggerBracketGTC = &TriggerBracketGTC{BaseSize: types.Number(sharedParams.BaseAmount), LimitPrice: types.Number(sharedParams.LimitPrice), StopTriggerPrice: types.Number(sharedParams.StopPrice)}
		} else {
			if sharedParams.EndTime.Before(time.Now()) {
				return orderConfig, errEndTimeInPast
			}
			orderConfig.TriggerBracketGTD = &TriggerBracketGTD{BaseSize: types.Number(sharedParams.BaseAmount), LimitPrice: types.Number(sharedParams.LimitPrice), StopTriggerPrice: types.Number(sharedParams.StopPrice), EndTime: sharedParams.EndTime}
		}
	default:
		return orderConfig, errInvalidOrderType
	}
	return orderConfig, nil
}

// FormatMarginType properly formats the margin type for the request
func FormatMarginType(marginType string) string {
	switch marginType {
	case "ISOLATED", "CROSS":
		return marginType
	case "MULTI":
		return "CROSS"
	}
	return ""
}

// String implements the stringer interface
func (f FiatTransferType) String() string {
	if f {
		return "withdrawal"
	}
	return "deposit"
}

// UnmarshalJSON unmarshals the JSON data
func (o *Orders) UnmarshalJSON(data []byte) error {
	var alias any
	err := json.Unmarshal(data, &[3]any{&o.Price, &o.Size, &alias})
	if err != nil {
		return err
	}
	switch a := alias.(type) {
	case string:
		if o.OrderID, err = uuid.FromString(a); err != nil {
			return err
		}
		o.OrderCount = 1
	case float64:
		o.OrderCount = uint64(a)
	default:
		return common.GetTypeAssertError("string | float64", alias, "Orders[3]")
	}
	return nil
}

// UnmarshalJSON unmarshals the JSON data
func (c *Candle) UnmarshalJSON(data []byte) error {
	return json.Unmarshal(data, &[6]any{&c.Time, &c.Low, &c.High, &c.Open, &c.Close, &c.Volume})
}

// UnmarshalJSON unmarshals the JSON data
func (i *Integer) UnmarshalJSON(data []byte) error {
	var temp string
	if err := json.Unmarshal(data, &temp); err != nil {
		return err
	}
	if temp == "" {
		*i = 0
		return nil
	}
	value, err := strconv.ParseInt(temp, 10, 64)
	if err != nil {
		return err
	}
	*i = Integer(value)
	return nil
}
