Files
gocryptotrader/exchanges/coinbase/coinbase.go
cranktakular fd9aaf00a2 Coinbase: Update exchange implementation (#1480)
* Slight enhance of Coinbase tests

Continual enhance of Coinbase tests

The revamp continues

Oh jeez the Orderbook part's unfinished don't look

Coinbase revamp, Orderbook still unfinished

* Coinbase revamp; CreateReport is still WIP

* More coinbase improvements; onto sandbox testing

* Coinbase revamp continues

* Coinbase revamp continues

* Coinbasepro revamp is ceaseless

* Coinbase revamp, starting on advanced trade API

* Coinbase Advanced Trade Starts in Ernest

V3 done, onto V2

Coinbase revamp nears completion

Coinbase revamp nears completion

Test commit should fail

Coinbase revamp nears completion

* Coinbase revamp stage wrapper

* Coinbase wrapper coherence continues

* Coinbase wrapper continues writhing

* Coinbase wrapper & codebase cleanup

* Coinbase updates & wrap progress

* More Coinbase wrapper progress

* Wrapper is wrapped, kinda

* Test & type checking

* Coinbase REST revamp finished

* Post-merge fix

* WS revamp begins

* WS Main Revamp Done?

* CB websocket tidying up

* Coinbase WS wrapperupperer

* Coinbase revamp done??

* Linter progress

* Continued lint cleanup

* Further lint cleanup

* Increased lint coverage

* Does this fix all sloppy reassigns & shadowing?

* Undoing retry policy change

* Documentation regeneration

* Coinbase code improvements

* Providing warning about known issue

* Updating an error to new format

* Making gocritic happy

* Review adherence

* Endpoints moved to V3 & nil pointer fixes

* Removing seemingly superfluous constant

* Glorious improvements

* Removing unused error

* Partial public endpoint addition

* Slight improvements

* Wrapper improvements; still a few errors left in other packages

* A lil Coinbase progress

* Json cleaning

* Lint appeasement

* Config repair

* Config fix (real)

* Little fix

* New public endpoint incorporation

* Additional fixes

* Improvements & Appeasements

* LineSaver

* Additional fixes

* Another fix

* Fixing picked nits

* Quick fixies

* Lil fixes

* Subscriptions: Add List.Enabled

* CoinbasePro: Add subscription templating

* fixup! CoinbasePro: Add subscription templating

* fixup! CoinbasePro: Add subscription templating

* Comment fix

* Subsequent fixes

* Issues hopefully fixed

* Lint fix

* Glorious fixes

* Json formatting

* ShazNits

* (L/N)i(n/)t

* Adding a test

* Tiny test improvement

* Template patch testing

* Fixes

* Further shaznits

* Lint nit

* JWT move and other fixes

* Small nits

* Shaznit, singular

* Post-merge fix

* Post-merge fixes

* Typo fix

* Some glorious nits

* Required changes

* Stop going

* Alias attempt

* Alias fix & test cleanup

* Test fix

* GetDepositAddress logic improvement

* Status update: Fixed

* Lint fix

* Happy birthday to PR 1480

* Cleanups

* Necessary nit corrections

* Fixing sillybug

* As per request

* Programming progress

* Order fixes

* Further fixies

* Test fix

* Pre-merge fixes

* More shaznits

* Context

* Sonic error handling

* Import fix

* Better Sonic error handling

* Perfect Sonic error handling?

* F purge

* Coinbase improvements

* API Update Conformity

* Coinbase continuation

* Coinbase order improvements

* Coinbase order improvements

* CreateOrderConfig improvements

* Managing API updates

* Coinbase API update progression

* jwt rename

* Comment link fix

* Coinbase v2 cleanup

* Post-merge fixes

* Review fixes

* GK's suggestions

* Linter fix

* Minor gbjk fixes

* Nit fixes

* Merge fix

* Lint fixes

* Coinbase rename stage 1

* Coinbase rename stage 2

* Coinbase rename stage 3

* Coinbase rename stage 4

* Coinbase rename final fix

* Coinbase: PoC on converting to request structs

* Applying requested changes

* Many review fixes, handled

* Thrashed by nits

* More minor modifications

* The last nit!?

---------

Co-authored-by: Gareth Kirwan <gbjkirwan@gmail.com>
2025-09-16 13:37:00 +10:00

1736 lines
71 KiB
Go

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
}