diff --git a/exchanges/bitget/bitget.go b/exchanges/bitget/bitget.go index 45b3fadf1ef..48eae16452f 100644 --- a/exchanges/bitget/bitget.go +++ b/exchanges/bitget/bitget.go @@ -25,73 +25,104 @@ const ( bitgetAPIURL = "https://api.bitget.com/api/v2/" // Public endpoints - bitgetPublic = "public/" - bitgetAnnouncements = "annoucements" // sic - bitgetTime = "time" - bitgetCoins = "coins" - bitgetSymbols = "symbols" - bitgetMarket = "market/" - bitgetVIPFeeRate = "vip-fee-rate" - bitgetTickers = "tickers" - bitgetMergeDepth = "merge-depth" - bitgetOrderbook = "orderbook" - bitgetCandles = "candles" - bitgetHistoryCandles = "history-candles" - bitgetFills = "fills" - bitgetFillsHistory = "fills-history" + bitgetPublic = "public/" + bitgetAnnouncements = "annoucements" // sic + bitgetTime = "time" + bitgetCoins = "coins" + bitgetSymbols = "symbols" + bitgetMarket = "market/" + bitgetVIPFeeRate = "vip-fee-rate" + bitgetTickers = "tickers" + bitgetMergeDepth = "merge-depth" + bitgetOrderbook = "orderbook" + bitgetCandles = "candles" + bitgetHistoryCandles = "history-candles" + bitgetFills = "fills" + bitgetFillsHistory = "fills-history" + bitgetTicker = "ticker" + bitgetHistoryIndexCandles = "history-index-candles" + bitgetHistoryMarkCandles = "history-mark-candles" + bitgetOpenInterest = "open-interest" + bitgetFundingTime = "funding-time" + bitgetSymbolPrice = "symbol-price" + bitgetHistoryFundRate = "history-fund-rate" + bitgetCurrentFundRate = "current-fund-rate" + bitgetContracts = "contracts" // Mixed endpoints bitgetSpot = "spot/" + bitgetMix = "mix/" // Authenticated endpoints - bitgetCommon = "common/" - bitgetTradeRate = "trade-rate" - bitgetTax = "tax/" - bitgetSpotRecord = "spot-record" - bitgetFutureRecord = "future-record" - bitgetMarginRecord = "margin-record" - bitgetP2PRecord = "p2p-record" - bitgetP2P = "p2p/" - bitgetMerchantList = "merchantList" - bitgetMerchantInfo = "merchantInfo" - bitgetOrderList = "orderList" - bitgetAdvList = "advList" - bitgetUser = "user/" - bitgetCreate = "create-" - bitgetVirtualSubaccount = "virtual-subaccount" - bitgetModify = "modify-" - bitgetBatchCreateSubAccApi = "batch-create-subaccount-and-apikey" - bitgetList = "list" - bitgetAPIKey = "apikey" - bitgetConvert = "convert/" - bitgetCurrencies = "currencies" - bitgetQuotedPrice = "quoted-price" - bitgetTrade = "trade" - bitgetConvertRecord = "convert-record" - bitgetBGBConvert = "bgb-convert" - bitgetConvertCoinList = "bgb-convert-coin-list" - bitgetBGBConvertRecords = "bgb-convert-records" - bitgetPlaceOrder = "/place-order" - bitgetCancelOrder = "/cancel-order" - bitgetBatchOrders = "/batch-orders" - bitgetBatchCancel = "/batch-cancel-order" - bitgetCancelSymbolOrder = "/cancel-symbol-order" - bitgetOrderInfo = "/orderInfo" - bitgetUnfilledOrders = "/unfilled-orders" - bitgetHistoryOrders = "/history-orders" - bitgetPlacePlanOrder = "/place-plan-order" - bitgetModifyPlanOrder = "/modify-plan-order" - bitgetCancelPlanOrder = "/cancel-plan-order" - bitgetCurrentPlanOrder = "/current-plan-order" - bitgetPlanSubOrder = "/plan-sub-order" - bitgetPlanOrderHistory = "/history-plan-order" - bitgetBatchCancelPlanOrder = "/batch-cancel-plan-order" - bitgetAccount = "account/" - bitgetInfo = "info" - bitgetAssets = "assets" - bitgetSubAccountAssets = "subaccount-assets" - bitgetWallet = "wallet/" - bitgetModifyDepositAccount = "modify-deposit-account" + bitgetCommon = "common/" + bitgetTradeRate = "trade-rate" + bitgetTax = "tax/" + bitgetSpotRecord = "spot-record" + bitgetFutureRecord = "future-record" + bitgetMarginRecord = "margin-record" + bitgetP2PRecord = "p2p-record" + bitgetP2P = "p2p/" + bitgetMerchantList = "merchantList" + bitgetMerchantInfo = "merchantInfo" + bitgetOrderList = "orderList" + bitgetAdvList = "advList" + bitgetUser = "user/" + bitgetCreate = "create-" + bitgetVirtualSubaccount = "virtual-subaccount" + bitgetModify = "modify-" + bitgetBatchCreateSubAccApi = "batch-create-subaccount-and-apikey" + bitgetList = "list" + bitgetAPIKey = "apikey" + bitgetConvert = "convert/" + bitgetCurrencies = "currencies" + bitgetQuotedPrice = "quoted-price" + bitgetTrade = "trade" + bitgetConvertRecord = "convert-record" + bitgetBGBConvert = "bgb-convert" + bitgetConvertCoinList = "bgb-convert-coin-list" + bitgetBGBConvertRecords = "bgb-convert-records" + bitgetPlaceOrder = "/place-order" + bitgetCancelOrder = "/cancel-order" + bitgetBatchOrders = "/batch-orders" + bitgetBatchCancel = "/batch-cancel-order" + bitgetCancelSymbolOrder = "/cancel-symbol-order" + bitgetOrderInfo = "/orderInfo" + bitgetUnfilledOrders = "/unfilled-orders" + bitgetHistoryOrders = "/history-orders" + bitgetPlacePlanOrder = "/place-plan-order" + bitgetModifyPlanOrder = "/modify-plan-order" + bitgetCancelPlanOrder = "/cancel-plan-order" + bitgetCurrentPlanOrder = "/current-plan-order" + bitgetPlanSubOrder = "/plan-sub-order" + bitgetPlanOrderHistory = "/history-plan-order" + bitgetBatchCancelPlanOrder = "/batch-cancel-plan-order" + bitgetAccount = "account" + bitgetInfo = "/info" + bitgetAssets = "/assets" + bitgetSubaccountAssets = "/subaccount-assets" + bitgetWallet = "wallet/" + bitgetModifyDepositAccount = "modify-deposit-account" + bitgetBills = "/bills" + bitgetTransfer = "transfer" + bitgetTransferCoinInfo = "transfer-coin-info" + bitgetSubaccountTransfer = "subaccount-transfer" + bitgetWithdrawal = "withdrawal" + bitgetSubaccountTransferRecord = "/sub-main-trans-record" + bitgetTransferRecord = "/transferRecords" + bitgetDepositAddress = "deposit-address" + bitgetSubaccountDepositAddress = "subaccount-deposit-address" + bitgetCancelWithdrawal = "cancel-withdrawal" + bitgetSubaccountDepositRecord = "subaccount-deposit-records" + bitgetWithdrawalRecord = "withdrawal-records" + bitgetDepositRecord = "deposit-records" + bitgetAccounts = "/accounts" + bitgetSubaccountAssets2 = "/sub-account-assets" + bitgetOpenCount = "/open-count" + bitgetSetLeverage = "/set-leverage" + bitgetSetMargin = "/set-margin" + bitgetSetMarginMode = "/set-margin-mode" + bitgetSetPositionMode = "/set-position-mode" + bitgetBill = "/bill" // Errors errUnknownEndpointLimit = "unknown endpoint limit %v" @@ -102,7 +133,7 @@ var ( errPairEmpty = errors.New("currency pair cannot be empty") errCurrencyEmpty = errors.New("currency cannot be empty") errProductTypeEmpty = errors.New("productType cannot be empty") - errSubAccountEmpty = errors.New("subaccounts cannot be empty") + errSubaccountEmpty = errors.New("subaccounts cannot be empty") errNewStatusEmpty = errors.New("newStatus cannot be empty") errNewPermsEmpty = errors.New("newPerms cannot be empty") errPassphraseEmpty = errors.New("passphrase cannot be empty") @@ -133,6 +164,20 @@ var ( errTriggerTypeEmpty = errors.New("triggerType cannot be empty") errNonsenseRequest = errors.New("nonsense request expected error") errAccountTypeEmpty = errors.New("accountType cannot be empty") + errFromTypeEmpty = errors.New("fromType cannot be empty") + errToTypeEmpty = errors.New("toType cannot be empty") + errCurrencyAndPairEmpty = errors.New("currency and pair cannot both be empty") + errFromIDEmpty = errors.New("fromID cannot be empty") + errToIDEmpty = errors.New("toID cannot be empty") + errTransferTypeEmpty = errors.New("transferType cannot be empty") + errAddressEmpty = errors.New("address cannot be empty") + errNoCandleData = errors.New("no candle data") + errMarginCoinEmpty = errors.New("marginCoin cannot be empty") + errOpenAmountEmpty = errors.New("openAmount cannot be empty") + errOpenPriceEmpty = errors.New("openPrice cannot be empty") + errLeverageEmpty = errors.New("leverage cannot be empty") + errMarginModeEmpty = errors.New("marginMode cannot be empty") + errPositionModeEmpty = errors.New("positionMode cannot be empty") ) // QueryAnnouncement returns announcements from the exchange, filtered by type and time @@ -338,7 +383,7 @@ func (bi *Bitget) GetMerchantAdvertisementList(ctx context.Context, startTime, e // no spaces, no numbers, and be exactly 8 characters long. func (bi *Bitget) CreateVirtualSubaccounts(ctx context.Context, subaccounts []string) (*CrVirSubResp, error) { if len(subaccounts) == 0 { - return nil, errSubAccountEmpty + return nil, errSubaccountEmpty } path := bitgetUser + bitgetCreate + bitgetVirtualSubaccount req := map[string]interface{}{ @@ -352,7 +397,7 @@ func (bi *Bitget) CreateVirtualSubaccounts(ctx context.Context, subaccounts []st // ModifyVirtualSubaccount changes the permissions and/or status of a virtual subaccount func (bi *Bitget) ModifyVirtualSubaccount(ctx context.Context, subaccountID, newStatus string, newPerms []string) (*SuccessBoolResp, error) { if subaccountID == "" { - return nil, errSubAccountEmpty + return nil, errSubaccountEmpty } if newStatus == "" { return nil, errNewStatusEmpty @@ -377,7 +422,7 @@ func (bi *Bitget) ModifyVirtualSubaccount(ctx context.Context, subaccountID, new // A maximum of 30 IPs can be a part of the whitelist. func (bi *Bitget) CreateSubaccountAndAPIKey(ctx context.Context, subaccountName, passphrase, label string, whiteList, permList []string) (*CrSubAccAPIKeyResp, error) { if subaccountName == "" { - return nil, errSubAccountEmpty + return nil, errSubaccountEmpty } req := map[string]interface{}{ "subAccountName": subaccountName, @@ -410,7 +455,7 @@ func (bi *Bitget) GetVirtualSubaccounts(ctx context.Context, limit, pagination i // CreateAPIKey creates an API key for the selected virtual sub-account func (bi *Bitget) CreateAPIKey(ctx context.Context, subaccountID, passphrase, label string, whiteList, permList []string) (*AlterAPIKeyResp, error) { if subaccountID == "" { - return nil, errSubAccountEmpty + return nil, errSubaccountEmpty } if passphrase == "" { return nil, errPassphraseEmpty @@ -444,7 +489,7 @@ func (bi *Bitget) ModifyAPIKey(ctx context.Context, subaccountID, passphrase, la return nil, errLabelEmpty } if subaccountID == "" { - return nil, errSubAccountEmpty + return nil, errSubaccountEmpty } path := bitgetUser + bitgetModify + bitgetVirtualSubaccount + "-" + bitgetAPIKey req := make(map[string]interface{}) @@ -462,7 +507,7 @@ func (bi *Bitget) ModifyAPIKey(ctx context.Context, subaccountID, passphrase, la // GetAPIKeys lists the API keys associated with the selected virtual sub-account func (bi *Bitget) GetAPIKeys(ctx context.Context, subaccountID string) (*GetAPIKeyResp, error) { if subaccountID == "" { - return nil, errSubAccountEmpty + return nil, errSubaccountEmpty } vals := url.Values{} vals.Set("subAccountUid", subaccountID) @@ -607,15 +652,16 @@ func (bi *Bitget) GetSymbolInfo(ctx context.Context, pair string) (*SymbolInfoRe return resp, bi.SendHTTPRequest(ctx, exchange.RestSpot, Rate20, path, vals, &resp) } -// GetVIPFeeRate returns the different levels of VIP fee rates -func (bi *Bitget) GetVIPFeeRate(ctx context.Context) (*VIPFeeRateResp, error) { +// GetSpotVIPFeeRate returns the different levels of VIP fee rates for spot trading +func (bi *Bitget) GetSpotVIPFeeRate(ctx context.Context) (*VIPFeeRateResp, error) { path := bitgetSpot + bitgetMarket + bitgetVIPFeeRate var resp *VIPFeeRateResp return resp, bi.SendHTTPRequest(ctx, exchange.RestSpot, Rate10, path, nil, &resp) } -// GetTickerInformation returns the ticker information for all trading pairs, or a single pair of the user's choice -func (bi *Bitget) GetTickerInformation(ctx context.Context, pair string) (*TickerResp, error) { +// GetSpotTickerInformation returns the ticker information for all trading pairs, or a single pair of the user's +// choice +func (bi *Bitget) GetSpotTickerInformation(ctx context.Context, pair string) (*TickerResp, error) { vals := url.Values{} vals.Set("symbol", pair) path := bitgetSpot + bitgetMarket + bitgetTickers @@ -623,10 +669,10 @@ func (bi *Bitget) GetTickerInformation(ctx context.Context, pair string) (*Ticke return resp, bi.SendHTTPRequest(ctx, exchange.RestSpot, Rate20, path, vals, &resp) } -// GetMergeDepth returns part of the orderbook, with options to merge orders of similar price levels together, +// GetSpotMergeDepth returns part of the orderbook, with options to merge orders of similar price levels together, // and to change how many results are returned. Limit's a string instead of the typical int64 because the API // will accept a value of "max" -func (bi *Bitget) GetMergeDepth(ctx context.Context, pair, precision, limit string) (*DepthResp, error) { +func (bi *Bitget) GetSpotMergeDepth(ctx context.Context, pair, precision, limit string) (*DepthResp, error) { if pair == "" { return nil, errPairEmpty } @@ -651,8 +697,8 @@ func (bi *Bitget) GetOrderbookDepth(ctx context.Context, pair, step string, limi return resp, bi.SendHTTPRequest(ctx, exchange.RestSpot, Rate20, path, vals, &resp) } -// GetCandlestickData returns candlestick data for a given trading pair -func (bi *Bitget) GetCandlestickData(ctx context.Context, pair, granularity string, startTime, endTime time.Time, limit uint16, historic bool) (*CandleData, error) { +// GetSpotCandlestickData returns candlestick data for a given trading pair +func (bi *Bitget) GetSpotCandlestickData(ctx context.Context, pair, granularity string, startTime, endTime time.Time, limit uint16, historic bool) (*CandleData, error) { if pair == "" { return nil, errPairEmpty } @@ -675,91 +721,11 @@ func (bi *Bitget) GetCandlestickData(ctx context.Context, pair, granularity stri return nil, err } } - params.Values.Set("symbol", pair) - params.Values.Set("granularity", granularity) - if limit != 0 { - params.Values.Set("limit", strconv.FormatUint(uint64(limit), 10)) - } - var resp *CandleResponse - err := bi.SendHTTPRequest(ctx, exchange.RestSpot, Rate20, path, params.Values, &resp) - if err != nil { - return nil, err - } - var data CandleData - data.Candles = make([]OneCandle, len(resp.Data)) - for i := range resp.Data { - timeTemp, ok := resp.Data[i][0].(string) - if !ok { - return nil, errTypeAssertTimestamp - } - timeTemp = (timeTemp)[1 : len(timeTemp)-1] - timeTemp2, err := strconv.ParseInt(timeTemp, 10, 64) - if err != nil { - return nil, err - } - data.Candles[i].Timestamp = time.Time(UnixTimestamp(time.UnixMilli(timeTemp2).UTC())) - openTemp, ok := resp.Data[i][1].(string) - if !ok { - return nil, errTypeAssertOpenPrice - } - data.Candles[i].Open, err = strconv.ParseFloat(openTemp, 64) - if err != nil { - return nil, err - } - highTemp, ok := resp.Data[i][2].(string) - if !ok { - return nil, errTypeAssertHighPrice - } - data.Candles[i].High, err = strconv.ParseFloat(highTemp, 64) - if err != nil { - return nil, err - } - lowTemp, ok := resp.Data[i][3].(string) - if !ok { - return nil, errTypeAssertLowPrice - } - data.Candles[i].Low, err = strconv.ParseFloat(lowTemp, 64) - if err != nil { - return nil, err - } - closeTemp, ok := resp.Data[i][4].(string) - if !ok { - return nil, errTypeAssertClosePrice - } - data.Candles[i].Close, err = strconv.ParseFloat(closeTemp, 64) - if err != nil { - return nil, err - } - baseVolumeTemp := resp.Data[i][5].(string) - if !ok { - return nil, errTypeAssertBaseVolume - } - data.Candles[i].BaseVolume, err = strconv.ParseFloat(baseVolumeTemp, 64) - if err != nil { - return nil, err - } - quoteVolumeTemp := resp.Data[i][6].(string) - if !ok { - return nil, errTypeAssertQuoteVolume - } - data.Candles[i].QuoteVolume, err = strconv.ParseFloat(quoteVolumeTemp, 64) - if err != nil { - return nil, err - } - usdtVolumeTemp := resp.Data[i][7].(string) - if !ok { - return nil, errTypeAssertUSDTVolume - } - data.Candles[i].USDTVolume, err = strconv.ParseFloat(usdtVolumeTemp, 64) - if err != nil { - return nil, err - } - } - return &data, nil + return bi.candlestickHelper(ctx, pair, granularity, path, limit, params) } -// GetRecentFills returns the most recent trades for a given pair -func (bi *Bitget) GetRecentFills(ctx context.Context, pair string, limit uint16) (*MarketFillsResp, error) { +// GetRecentSpotFills returns the most recent trades for a given pair +func (bi *Bitget) GetRecentSpotFills(ctx context.Context, pair string, limit uint16) (*MarketFillsResp, error) { if pair == "" { return nil, errPairEmpty } @@ -771,8 +737,8 @@ func (bi *Bitget) GetRecentFills(ctx context.Context, pair string, limit uint16) return resp, bi.SendHTTPRequest(ctx, exchange.RestSpot, Rate10, path, vals, &resp) } -// GetMarketTrades returns trades for a given pair within a particular time range, and/or before a certain ID -func (bi *Bitget) GetMarketTrades(ctx context.Context, pair string, startTime, endTime time.Time, limit, pagination int64) (*MarketFillsResp, error) { +// GetSpotMarketTrades returns trades for a given pair within a particular time range, and/or before a certain ID +func (bi *Bitget) GetSpotMarketTrades(ctx context.Context, pair string, startTime, endTime time.Time, limit, pagination int64) (*MarketFillsResp, error) { if pair == "" { return nil, errPairEmpty } @@ -1230,10 +1196,10 @@ func (bi *Bitget) GetAccountAssets(ctx context.Context, currency, assetType stri return resp, bi.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, Rate10, http.MethodGet, path, vals, nil, &resp) } -// GetSubAccountAssets returns information on assets in the user's sub-accounts -func (bi *Bitget) GetSubAccountAssets(ctx context.Context) (*SubAccountAssetsResp, error) { - path := bitgetSpot + bitgetAccount + bitgetSubAccountAssets - var resp *SubAccountAssetsResp +// GetSpotSubaccountAssets returns information on assets in the user's sub-accounts +func (bi *Bitget) GetSpotSubaccountAssets(ctx context.Context) (*SubaccountAssetsResp, error) { + path := bitgetSpot + bitgetAccount + bitgetSubaccountAssets + var resp *SubaccountAssetsResp return resp, bi.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, Rate10, http.MethodGet, path, nil, nil, &resp) } @@ -1255,165 +1221,1046 @@ func (bi *Bitget) ModifyDepositAccount(ctx context.Context, accountType, currenc &resp) } -// SendAuthenticatedHTTPRequest sends an authenticated HTTP request -func (bi *Bitget) SendAuthenticatedHTTPRequest(ctx context.Context, ep exchange.URL, rateLim request.EndpointLimit, method, path string, queryParams url.Values, bodyParams map[string]interface{}, result interface{}) error { - creds, err := bi.GetCredentials(ctx) +// GetSpotAccountBills returns a section of the user's billing history +func (bi *Bitget) GetSpotAccountBills(ctx context.Context, currency, groupType, businessType string, startTime, endTime time.Time, limit, pagination int64) (*SpotAccBillResp, error) { + var params Params + params.Values = make(url.Values) + err := params.prepareDateString(startTime, endTime, true) if err != nil { - return err + return nil, err } - endpoint, err := bi.API.Endpoints.GetURL(ep) - if err != nil { - return err + if currency != "" { + params.Values.Set("coin", currency) } - path = common.EncodeURLValues(path, queryParams) - newRequest := func() (*request.Item, error) { - payload := []byte("") - if bodyParams != nil { - payload, err = json.Marshal(bodyParams) - if err != nil { - return nil, err - } - } - t := strconv.FormatInt(time.Now().UnixMilli(), 10) - message := t + method + "/api/v2/" + path + string(payload) - // The exchange also supports user-generated RSA keys, but we haven't implemented that yet - var hmac []byte - hmac, err = crypto.GetHMAC(crypto.HashSHA256, []byte(message), []byte(creds.Secret)) - if err != nil { - return nil, err - } - headers := make(map[string]string) - headers["ACCESS-KEY"] = creds.Key - headers["ACCESS-SIGN"] = crypto.Base64Encode(hmac) - headers["ACCESS-TIMESTAMP"] = t - headers["ACCESS-PASSPHRASE"] = creds.ClientID - headers["Content-Type"] = "application/json" - headers["locale"] = "en-US" - return &request.Item{ - Method: method, - Path: endpoint + path, - Headers: headers, - Body: bytes.NewBuffer(payload), - Result: &result, - Verbose: bi.Verbose, - HTTPDebugging: bi.HTTPDebugging, - HTTPRecording: bi.HTTPRecording, - }, nil + params.Values.Set("groupType", groupType) + params.Values.Set("businessType", businessType) + if limit != 0 { + params.Values.Set("limit", strconv.FormatInt(limit, 10)) } - return bi.SendPayload(ctx, rateLim, newRequest, request.AuthenticatedRequest) + if pagination != 0 { + params.Values.Set("idLessThan", strconv.FormatInt(pagination, 10)) + } + path := bitgetSpot + bitgetAccount + bitgetBills + var resp *SpotAccBillResp + return resp, bi.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, Rate10, http.MethodGet, path, params.Values, + nil, &resp) } -// SendHTTPRequest sends an unauthenticated HTTP request, with a few assumptions about the request; -// namely that it is a GET request with no body -func (bi *Bitget) SendHTTPRequest(ctx context.Context, ep exchange.URL, rateLim request.EndpointLimit, path string, queryParams url.Values, result interface{}) error { - endpoint, err := bi.API.Endpoints.GetURL(ep) - if err != nil { - return err +// TransferAsset transfers a certain amount of a currency or pair between different productType accounts +func (bi *Bitget) TransferAsset(ctx context.Context, fromType, toType, currency, pair, clientOrderID string, amount float64) (*TransferResp, error) { + if fromType == "" { + return nil, errFromTypeEmpty } - newRequest := func() (*request.Item, error) { - path = common.EncodeURLValues(path, queryParams) - return &request.Item{ - Method: "GET", - Path: endpoint + path, - Result: &result, - Verbose: bi.Verbose, - HTTPDebugging: bi.HTTPDebugging, - HTTPRecording: bi.HTTPRecording, - }, nil + if toType == "" { + return nil, errToTypeEmpty } - return bi.SendPayload(ctx, rateLim, newRequest, request.UnauthenticatedRequest) -} - -// PrepareDateString encodes a set of parameters indicating start & end dates -func (p *Params) prepareDateString(startDate, endDate time.Time, ignoreUnset bool) error { - err := common.StartEndTimeCheck(startDate, endDate) - if err != nil { - if errors.Is(err, common.ErrDateUnset) && ignoreUnset { - return nil - } - return err + if currency == "" && pair == "" { + return nil, errCurrencyAndPairEmpty } - p.Values.Set("startTime", strconv.FormatInt(startDate.UnixMilli(), 10)) - p.Values.Set("endTime", strconv.FormatInt(endDate.UnixMilli(), 10)) - return nil -} - -// UnmarshalJSON unmarshals the JSON input into a UnixTimestamp type -func (t *UnixTimestamp) UnmarshalJSON(b []byte) error { - var timestampStr string - err := json.Unmarshal(b, ×tampStr) - if err != nil { - return err + if amount == 0 { + return nil, errAmountEmpty } - if timestampStr == "" { - *t = UnixTimestamp(time.Time{}) - return nil + req := map[string]interface{}{ + "fromType": fromType, + "toType": toType, + "amount": strconv.FormatFloat(amount, 'f', -1, 64), + "coin": currency, + "symbol": pair, } - timestamp, err := strconv.ParseInt(timestampStr, 10, 64) - if err != nil { - return err + if clientOrderID != "" { + req["clientOid"] = clientOrderID } - *t = UnixTimestamp(time.UnixMilli(timestamp).UTC()) - return nil -} - -// String implements the stringer interface -func (t *UnixTimestamp) String() string { - return t.Time().String() + path := bitgetSpot + bitgetWallet + bitgetTransfer + var resp *TransferResp + return resp, bi.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, Rate10, http.MethodPost, path, nil, req, + &resp) } -// Time returns the time.Time representation of the UnixTimestamp -func (t *UnixTimestamp) Time() time.Time { - return time.Time(*t) +// GetTransferableCoinList returns a list of coins that can be transferred between the provided accounts +func (bi *Bitget) GetTransferableCoinList(ctx context.Context, fromType, toType string) (*TransferableCoinsResp, error) { + if fromType == "" { + return nil, errFromTypeEmpty + } + if toType == "" { + return nil, errToTypeEmpty + } + vals := url.Values{} + vals.Set("fromType", fromType) + vals.Set("toType", toType) + path := bitgetSpot + bitgetWallet + bitgetTransferCoinInfo + var resp *TransferableCoinsResp + return resp, bi.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, Rate10, http.MethodGet, path, vals, nil, + &resp) } -// UnmarshalJSON unmarshals the JSON input into a YesNoBool type -func (y *YesNoBool) UnmarshalJSON(b []byte) error { - var yn string - err := json.Unmarshal(b, &yn) - if err != nil { - return err +// SubaccountTransfer transfers assets between sub-accounts +func (bi *Bitget) SubaccountTransfer(ctx context.Context, fromType, toType, currency, pair, clientOrderID, fromID, toID string, amount float64) (*TransferResp, error) { + if fromType == "" { + return nil, errFromTypeEmpty } - switch yn { - case "yes": - *y = true - case "no": - *y = false + if toType == "" { + return nil, errToTypeEmpty } - return nil + if currency == "" && pair == "" { + return nil, errCurrencyAndPairEmpty + } + if fromID == "" { + return nil, errFromIDEmpty + } + if toID == "" { + return nil, errToIDEmpty + } + if amount == 0 { + return nil, errAmountEmpty + } + req := map[string]interface{}{ + "fromType": fromType, + "toType": toType, + "amount": strconv.FormatFloat(amount, 'f', -1, 64), + "coin": currency, + "symbol": pair, + "fromId": fromID, + "toId": toID, + } + if clientOrderID != "" { + req["clientOid"] = clientOrderID + } + path := bitgetSpot + bitgetWallet + bitgetSubaccountTransfer + var resp *TransferResp + return resp, bi.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, Rate10, http.MethodPost, path, nil, req, + &resp) } -// UnmarshalJSON unmarshals the JSON input into a SuccessBool type -func (s *SuccessBool) UnmarshalJSON(b []byte) error { - var success string - err := json.Unmarshal(b, &success) - if err != nil { - return err +// WithdrawFunds withdraws funds from the user's account +func (bi *Bitget) WithdrawFunds(ctx context.Context, currency, transferType, address, chain, innerAddressType, areaCode, tag, note, clientOrderID string, amount float64) (*OrderResp, error) { + if currency == "" { + return nil, errCurrencyEmpty } - switch success { - case "success": - *s = true - case "failure": - *s = false + if transferType == "" { + return nil, errTransferTypeEmpty } - return nil + if address == "" { + return nil, errAddressEmpty + } + if amount == 0 { + return nil, errAmountEmpty + } + req := map[string]interface{}{ + "coin": currency, + "transferType": transferType, + "address": address, + "chain": chain, + "innerToType": innerAddressType, + "areaCode": areaCode, + "tag": tag, + "size": strconv.FormatFloat(amount, 'f', -1, 64), + "remark": note, + "clientOid": clientOrderID, + } + path := bitgetSpot + bitgetWallet + bitgetWithdrawal + var resp *OrderResp + return resp, bi.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, Rate10, http.MethodPost, path, nil, req, + &resp) } -// UnmarshalJSON unmarshals the JSON input into an EmptyInt type -func (e *EmptyInt) UnmarshalJSON(b []byte) error { - var num string - err := json.Unmarshal(b, &num) +// GetSubaccountTransferRecord returns the user's sub-account transfer history +func (bi *Bitget) GetSubaccountTransferRecord(ctx context.Context, currency, subaccountID, clientOrderID string, startTime, endTime time.Time, limit, pagination int64) (*SubaccTfrRecResp, error) { + var params Params + params.Values = make(url.Values) + err := params.prepareDateString(startTime, endTime, true) if err != nil { - return err + return nil, err } - if num == "" { - *e = 0 - return nil + params.Values.Set("coin", currency) + params.Values.Set("subUid", subaccountID) + if clientOrderID != "" { + params.Values.Set("clientOid", clientOrderID) } - i, err := strconv.ParseInt(num, 10, 64) - if err != nil { - return err + if limit != 0 { + params.Values.Set("limit", strconv.FormatInt(limit, 10)) + } + if pagination != 0 { + params.Values.Set("idLessThan", strconv.FormatInt(pagination, 10)) + } + path := bitgetSpot + bitgetAccount + bitgetSubaccountTransferRecord + var resp *SubaccTfrRecResp + return resp, bi.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, Rate20, http.MethodGet, path, params.Values, + nil, &resp) +} + +// GetTransferRecord returns the user's transfer history +func (bi *Bitget) GetTransferRecord(ctx context.Context, currency, fromType, clientOrderID string, startTime, endTime time.Time, limit, pagination int64) (*TransferRecResp, error) { + if currency == "" { + return nil, errCurrencyEmpty + } + if fromType == "" { + return nil, errFromTypeEmpty + } + var params Params + params.Values = make(url.Values) + err := params.prepareDateString(startTime, endTime, true) + if err != nil { + return nil, err + } + params.Values.Set("coin", currency) + params.Values.Set("fromType", fromType) + if clientOrderID != "" { + params.Values.Set("clientOid", clientOrderID) + } + if limit != 0 { + params.Values.Set("limit", strconv.FormatInt(limit, 10)) + } + if pagination != 0 { + params.Values.Set("idLessThan", strconv.FormatInt(pagination, 10)) + } + path := bitgetSpot + bitgetAccount + bitgetTransferRecord + var resp *TransferRecResp + return resp, bi.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, Rate20, http.MethodGet, path, params.Values, + nil, &resp) +} + +// GetDepositAddressForCurrency returns the user's deposit address for a particular currency +func (bi *Bitget) GetDepositAddressForCurrency(ctx context.Context, currency, chain string) (*DepositAddressResp, error) { + if currency == "" { + return nil, errCurrencyEmpty + } + vals := url.Values{} + vals.Set("coin", currency) + vals.Set("chain", chain) + path := bitgetSpot + bitgetWallet + bitgetDepositAddress + var resp *DepositAddressResp + return resp, bi.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, Rate10, http.MethodGet, path, vals, nil, + &resp) +} + +// GetSubaccountDepositAddress returns the deposit address for a particular currency and sub-account +func (bi *Bitget) GetSubaccountDepositAddress(ctx context.Context, subaccountID, currency, chain string) (*DepositAddressResp, error) { + if subaccountID == "" { + return nil, errSubaccountEmpty + } + if currency == "" { + return nil, errCurrencyEmpty + } + vals := url.Values{} + vals.Set("subUid", subaccountID) + vals.Set("coin", currency) + vals.Set("chain", chain) + path := bitgetSpot + bitgetWallet + bitgetSubaccountDepositAddress + var resp *DepositAddressResp + return resp, bi.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, Rate10, http.MethodGet, path, vals, nil, + &resp) +} + +// CancelWithdrawal cancels a large withdrawal request that was placed in the last minute +func (bi *Bitget) CancelWithdrawal(ctx context.Context, orderID string) (*SuccessBool, error) { + if orderID == "" { + return nil, errOrderIDEmpty + } + req := map[string]interface{}{ + "orderId": orderID, + } + path := bitgetSpot + bitgetWallet + bitgetCancelWithdrawal + var resp *SuccessBool + return resp, bi.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, Rate10, http.MethodPost, path, nil, req, + &resp) +} + +// GetSubaccountDepositRecords returns the deposit history for a sub-account +func (bi *Bitget) GetSubaccountDepositRecords(ctx context.Context, subaccountID, currency string, orderID, pagination, limit int64, startTime, endTime time.Time) (*SubaccDepRecResp, error) { + if subaccountID == "" { + return nil, errSubaccountEmpty + } + var params Params + params.Values = make(url.Values) + err := params.prepareDateString(startTime, endTime, true) + if err != nil { + return nil, err + } + params.Values.Set("subUid", subaccountID) + params.Values.Set("coin", currency) + params.Values.Set("orderId", strconv.FormatInt(orderID, 10)) + if pagination != 0 { + params.Values.Set("idLessThan", strconv.FormatInt(pagination, 10)) + } + if limit != 0 { + params.Values.Set("limit", strconv.FormatInt(limit, 10)) + } + path := bitgetSpot + bitgetWallet + bitgetSubaccountDepositRecord + var resp *SubaccDepRecResp + return resp, bi.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, Rate10, http.MethodGet, path, params.Values, + nil, &resp) +} + +// GetWithdrawalRecords returns the user's withdrawal history +func (bi *Bitget) GetWithdrawalRecords(ctx context.Context, currency, clientOrderID string, startTime, endTime time.Time, pagination, orderID, limit int64) (*WithdrawRecordsResp, error) { + if currency == "" { + return nil, errCurrencyEmpty + } + var params Params + params.Values = make(url.Values) + err := params.prepareDateString(startTime, endTime, true) + if err != nil { + return nil, err + } + params.Values.Set("coin", currency) + params.Values.Set("clientOid", clientOrderID) + if pagination != 0 { + params.Values.Set("idLessThan", strconv.FormatInt(pagination, 10)) + } + params.Values.Set("orderId", strconv.FormatInt(orderID, 10)) + if limit != 0 { + params.Values.Set("limit", strconv.FormatInt(limit, 10)) + } + path := bitgetSpot + bitgetWallet + bitgetWithdrawalRecord + var resp *WithdrawRecordsResp + return resp, bi.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, Rate10, http.MethodGet, path, params.Values, + nil, &resp) +} + +// GetDepositRecords returns the user's cryptocurrency deposit history +func (bi *Bitget) GetDepositRecords(ctx context.Context, crypto string, orderID, pagination, limit int64, startTime, endTime time.Time) (*CryptoDepRecResp, error) { + var params Params + params.Values = make(url.Values) + err := params.prepareDateString(startTime, endTime, false) + if err != nil { + return nil, err + } + params.Values.Set("coin", crypto) + if pagination != 0 { + params.Values.Set("idLessThan", strconv.FormatInt(pagination, 10)) + } + params.Values.Set("orderId", strconv.FormatInt(orderID, 10)) + if limit != 0 { + params.Values.Set("limit", strconv.FormatInt(limit, 10)) + } + path := bitgetSpot + bitgetWallet + bitgetDepositRecord + var resp *CryptoDepRecResp + return resp, bi.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, Rate10, http.MethodGet, path, params.Values, + nil, &resp) +} + +// GetFuturesVIPFeeRate returns the different levels of VIP fee rates for futures trading +func (bi *Bitget) GetFuturesVIPFeeRate(ctx context.Context) (*VIPFeeRateResp, error) { + path := bitgetMix + bitgetMarket + bitgetVIPFeeRate + var resp *VIPFeeRateResp + return resp, bi.SendHTTPRequest(ctx, exchange.RestSpot, Rate10, path, nil, &resp) +} + +// GetFuturesMergeDepth returns part of the orderbook, with options to merge orders of similar price levels together, +// and to change how many results are returned. Limit's a string instead of the typical int64 because the API +// will accept a value of "max" +func (bi *Bitget) GetFuturesMergeDepth(ctx context.Context, pair, productType, precision, limit string) (*DepthResp, error) { + if pair == "" { + return nil, errPairEmpty + } + if productType == "" { + return nil, errProductTypeEmpty + } + vals := url.Values{} + vals.Set("symbol", pair) + vals.Set("productType", productType) + vals.Set("precision", precision) + vals.Set("limit", limit) + path := bitgetMix + bitgetMarket + bitgetMergeDepth + var resp *DepthResp + return resp, bi.SendHTTPRequest(ctx, exchange.RestSpot, Rate20, path, vals, &resp) +} + +// GetFuturesTicker returns the ticker information for a pair of the user's choice +func (bi *Bitget) GetFuturesTicker(ctx context.Context, pair, productType string) (*FutureTickerResp, error) { + if pair == "" { + return nil, errPairEmpty + } + if productType == "" { + return nil, errProductTypeEmpty + } + vals := url.Values{} + vals.Set("symbol", pair) + vals.Set("productType", productType) + path := bitgetMix + bitgetMarket + bitgetTicker + var resp *FutureTickerResp + return resp, bi.SendHTTPRequest(ctx, exchange.RestSpot, Rate20, path, vals, &resp) +} + +// GetAllFuturesTickers returns the ticker information for all pairs +func (bi *Bitget) GetAllFuturesTickers(ctx context.Context, productType string) (*FutureTickerResp, error) { + if productType == "" { + return nil, errProductTypeEmpty + } + vals := url.Values{} + vals.Set("productType", productType) + path := bitgetMix + bitgetMarket + bitgetTickers + var resp *FutureTickerResp + return resp, bi.SendHTTPRequest(ctx, exchange.RestSpot, Rate20, path, vals, &resp) +} + +// GetRecentFuturesFills returns the most recent trades for a given pair +func (bi *Bitget) GetRecentFuturesFills(ctx context.Context, pair, productType string, limit int64) (*MarketFillsResp, error) { + if pair == "" { + return nil, errPairEmpty + } + if productType == "" { + return nil, errProductTypeEmpty + } + vals := url.Values{} + vals.Set("symbol", pair) + vals.Set("productType", productType) + if limit != 0 { + vals.Set("limit", strconv.FormatInt(limit, 10)) + } + path := bitgetMix + bitgetMarket + bitgetFills + var resp *MarketFillsResp + return resp, bi.SendHTTPRequest(ctx, exchange.RestSpot, Rate20, path, vals, &resp) +} + +// GetFuturesMarketTrades returns trades for a given pair within a particular time range, and/or before a certain ID +func (bi *Bitget) GetFuturesMarketTrades(ctx context.Context, pair, productType string, limit, pagination int64, startTime, endTime time.Time) (*MarketFillsResp, error) { + if pair == "" { + return nil, errPairEmpty + } + if productType == "" { + return nil, errProductTypeEmpty + } + var params Params + params.Values = make(url.Values) + err := params.prepareDateString(startTime, endTime, true) + if err != nil { + return nil, err + } + params.Values.Set("symbol", pair) + params.Values.Set("productType", productType) + if limit != 0 { + params.Values.Set("limit", strconv.FormatInt(limit, 10)) + } + if pagination != 0 { + params.Values.Set("idLessThan", strconv.FormatInt(pagination, 10)) + } + path := bitgetMix + bitgetMarket + bitgetFillsHistory + var resp *MarketFillsResp + return resp, bi.SendHTTPRequest(ctx, exchange.RestSpot, Rate10, path, params.Values, &resp) +} + +// GetFuturesCandlestickData returns candlestick data for a given pair within a particular time range +func (bi *Bitget) GetFuturesCandlestickData(ctx context.Context, pair, productType, granularity string, startTime, endTime time.Time, limit uint16, mode CallMode) (*CandleData, error) { + if pair == "" { + return nil, errPairEmpty + } + if productType == "" { + return nil, errProductTypeEmpty + } + if granularity == "" { + return nil, errGranEmpty + } + var params Params + params.Values = make(url.Values) + err := params.prepareDateString(startTime, endTime, true) + if err != nil { + return nil, err + } + params.Values.Set("productType", productType) + path := bitgetMix + bitgetMarket + switch mode { + case CallModeNormal: + path += bitgetCandles + case CallModeHistory: + path += bitgetHistoryCandles + case CallModeIndex: + path += bitgetHistoryIndexCandles + case CallModeMark: + path += bitgetHistoryMarkCandles + } + return bi.candlestickHelper(ctx, pair, granularity, path, limit, params) +} + +// GetOpenPositions returns the total positions of a particular pair +func (bi *Bitget) GetOpenPositions(ctx context.Context, pair, productType string) (*OpenPositionsResp, error) { + if pair == "" { + return nil, errPairEmpty + } + if productType == "" { + return nil, errProductTypeEmpty + } + vals := url.Values{} + vals.Set("symbol", pair) + vals.Set("productType", productType) + path := bitgetMix + bitgetMarket + bitgetOpenInterest + var resp *OpenPositionsResp + return resp, bi.SendHTTPRequest(ctx, exchange.RestSpot, Rate20, path, vals, &resp) +} + +// GetNextFundingTime returns the settlement time and period of a particular contract +func (bi *Bitget) GetNextFundingTime(ctx context.Context, pair, productType string) (*FundingTimeResp, error) { + if pair == "" { + return nil, errPairEmpty + } + if productType == "" { + return nil, errProductTypeEmpty + } + vals := url.Values{} + vals.Set("symbol", pair) + vals.Set("productType", productType) + path := bitgetMix + bitgetMarket + bitgetFundingTime + var resp *FundingTimeResp + return resp, bi.SendHTTPRequest(ctx, exchange.RestSpot, Rate20, path, vals, &resp) +} + +// GetFuturesPrices returns the current market, index, and mark prices for a given pair +func (bi *Bitget) GetFuturesPrices(ctx context.Context, pair, productType string) (*FuturesPriceResp, error) { + if pair == "" { + return nil, errPairEmpty + } + if productType == "" { + return nil, errProductTypeEmpty + } + vals := url.Values{} + vals.Set("symbol", pair) + vals.Set("productType", productType) + path := bitgetMix + bitgetMarket + bitgetSymbolPrice + var resp *FuturesPriceResp + return resp, bi.SendHTTPRequest(ctx, exchange.RestSpot, Rate20, path, vals, &resp) +} + +// GetFundingHistorical returns the historical funding rates for a given pair +func (bi *Bitget) GetFundingHistorical(ctx context.Context, pair, productType string, limit, pagination int64) (*FundingHistoryResp, error) { + if pair == "" { + return nil, errPairEmpty + } + if productType == "" { + return nil, errProductTypeEmpty + } + vals := url.Values{} + vals.Set("symbol", pair) + vals.Set("productType", productType) + if limit != 0 { + vals.Set("pageSize", strconv.FormatInt(limit, 10)) + } + if pagination != 0 { + vals.Set("pageNo", strconv.FormatInt(pagination, 10)) + } + path := bitgetMix + bitgetMarket + bitgetHistoryFundRate + var resp *FundingHistoryResp + return resp, bi.SendHTTPRequest(ctx, exchange.RestSpot, Rate20, path, vals, &resp) +} + +// GetFundingCurrent returns the current funding rate for a given pair +func (bi *Bitget) GetFundingCurrent(ctx context.Context, pair, productType string) (*FundingCurrentResp, error) { + if pair == "" { + return nil, errPairEmpty + } + if productType == "" { + return nil, errProductTypeEmpty + } + vals := url.Values{} + vals.Set("symbol", pair) + vals.Set("productType", productType) + path := bitgetMix + bitgetMarket + bitgetCurrentFundRate + var resp *FundingCurrentResp + return resp, bi.SendHTTPRequest(ctx, exchange.RestSpot, Rate20, path, vals, &resp) +} + +// GetContractConfig returns details for a given contract +func (bi *Bitget) GetContractConfig(ctx context.Context, pair, productType string) (*ContractConfigResp, error) { + if pair == "" { + return nil, errPairEmpty + } + if productType == "" { + return nil, errProductTypeEmpty + } + vals := url.Values{} + vals.Set("symbol", pair) + vals.Set("productType", productType) + path := bitgetMix + bitgetMarket + bitgetContracts + var resp *ContractConfigResp + return resp, bi.SendHTTPRequest(ctx, exchange.RestSpot, Rate20, path, vals, &resp) +} + +// GetOneFuturesAccount returns details for the account associated with a given pair, margin coin, and product type +func (bi *Bitget) GetOneFuturesAccount(ctx context.Context, pair, productType, marginCoin string) (*OneAccResp, error) { + if pair == "" { + return nil, errPairEmpty + } + if productType == "" { + return nil, errProductTypeEmpty + } + if marginCoin == "" { + return nil, errMarginCoinEmpty + } + vals := url.Values{} + vals.Set("symbol", pair) + vals.Set("productType", productType) + vals.Set("marginCoin", marginCoin) + path := bitgetMix + bitgetAccount + "/" + bitgetAccount + var resp *OneAccResp + return resp, bi.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, Rate10, http.MethodGet, path, vals, nil, + &resp) +} + +// GetAllFuturesAccounts returns details for all accounts +func (bi *Bitget) GetAllFuturesAccounts(ctx context.Context, productType string) (*AllAccResp, error) { + if productType == "" { + return nil, errProductTypeEmpty + } + vals := url.Values{} + vals.Set("productType", productType) + path := bitgetMix + bitgetAccount + bitgetAccounts + var resp *AllAccResp + return resp, bi.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, Rate10, http.MethodGet, path, vals, nil, + &resp) +} + +// GetFuturesSubaccountAssets returns details on the assets of all sub-accounts +func (bi *Bitget) GetFuturesSubaccountAssets(ctx context.Context, productType string) (*SubaccountFuturesResp, error) { + if productType == "" { + return nil, errProductTypeEmpty + } + vals := url.Values{} + vals.Set("productType", productType) + path := bitgetMix + bitgetAccount + bitgetSubaccountAssets2 + var resp *SubaccountFuturesResp + return resp, bi.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, Rate10, http.MethodGet, path, vals, nil, + &resp) +} + +// GetEstimatedOpenCount returns the estimated size of open orders for a given pair +func (bi *Bitget) GetEstimatedOpenCount(ctx context.Context, pair, productType, marginCoin string, openAmount, openPrice, leverage float64) (*EstOpenCountResp, error) { + if pair == "" { + return nil, errPairEmpty + } + if productType == "" { + return nil, errProductTypeEmpty + } + if marginCoin == "" { + return nil, errMarginCoinEmpty + } + if openAmount == 0 { + return nil, errOpenAmountEmpty + } + if openPrice == 0 { + return nil, errOpenPriceEmpty + } + vals := url.Values{} + vals.Set("symbol", pair) + vals.Set("productType", productType) + vals.Set("marginCoin", marginCoin) + vals.Set("openAmount", strconv.FormatFloat(openAmount, 'f', -1, 64)) + vals.Set("openPrice", strconv.FormatFloat(openPrice, 'f', -1, 64)) + if leverage != 0 { + vals.Set("leverage", strconv.FormatFloat(leverage, 'f', -1, 64)) + } + path := bitgetMix + bitgetAccount + bitgetOpenCount + var resp *EstOpenCountResp + return resp, bi.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, Rate10, http.MethodGet, path, vals, nil, + &resp) +} + +// ChangeLeverage changes the leverage for the given pair and product type +func (bi *Bitget) ChangeLeverage(ctx context.Context, pair, productType, marginCoin, holdSide string, leverage float64) (*LeverageResp, error) { + if pair == "" { + return nil, errPairEmpty + } + if productType == "" { + return nil, errProductTypeEmpty + } + if marginCoin == "" { + return nil, errMarginCoinEmpty + } + if leverage == 0 { + return nil, errLeverageEmpty + } + req := map[string]interface{}{ + "symbol": pair, + "productType": productType, + "marginCoin": marginCoin, + "holdSide": holdSide, + "leverage": strconv.FormatFloat(leverage, 'f', -1, 64), + } + path := bitgetMix + bitgetAccount + bitgetSetLeverage + var resp *LeverageResp + return resp, bi.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, Rate5, http.MethodPost, path, nil, req, + &resp) +} + +// AdjustMargin adds or subtracts margin from a position +func (bi *Bitget) AdjustMargin(ctx context.Context, pair, productType, marginCoin, holdSide string, amount float64) error { + if pair == "" { + return errPairEmpty + } + if productType == "" { + return errProductTypeEmpty + } + if marginCoin == "" { + return errMarginCoinEmpty + } + if amount == 0 { + return errAmountEmpty + } + req := map[string]interface{}{ + "symbol": pair, + "productType": productType, + "marginCoin": marginCoin, + "amount": strconv.FormatFloat(amount, 'f', -1, 64), + } + if holdSide != "" { + req["holdSide"] = holdSide + } + path := bitgetMix + bitgetAccount + bitgetSetMargin + return bi.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, Rate5, http.MethodPost, path, nil, req, nil) +} + +// ChangeMarginMode changes the margin mode for a given pair. Can only be done when there the user has no open +// positions or orders +func (bi *Bitget) ChangeMarginMode(ctx context.Context, pair, productType, marginCoin, marginMode string) (*LeverageResp, error) { + if pair == "" { + return nil, errPairEmpty + } + if productType == "" { + return nil, errProductTypeEmpty + } + if marginCoin == "" { + return nil, errMarginCoinEmpty + } + if marginMode == "" { + return nil, errMarginModeEmpty + } + req := map[string]interface{}{ + "symbol": pair, + "productType": productType, + "marginCoin": marginCoin, + "marginMode": marginMode, + } + path := bitgetMix + bitgetAccount + bitgetSetMarginMode + var resp *LeverageResp + return resp, bi.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, Rate5, http.MethodPost, path, nil, req, + &resp) +} + +// ChangePositionMode changes the position mode for any pair. Having any positions or orders on any side of any pair +// may cause this to fail. +func (bi *Bitget) ChangePositionMode(ctx context.Context, productType, positionMode string) (*PosModeResp, error) { + if productType == "" { + return nil, errProductTypeEmpty + } + if positionMode == "" { + return nil, errPositionModeEmpty + } + req := map[string]interface{}{ + "productType": productType, + "posMode": positionMode, + } + path := bitgetMix + bitgetAccount + bitgetSetPositionMode + var resp *PosModeResp + return resp, bi.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, Rate5, http.MethodPost, path, nil, req, + &resp) +} + +// GetFuturesAccountBills returns a section of the user's billing history +func (bi *Bitget) GetFuturesAccountBills(ctx context.Context, productType, pair, currency, businessType string, pagination, limit int64, startTime, endTime time.Time) (*FutureAccBillResp, error) { + if productType == "" { + return nil, errProductTypeEmpty + } + var params Params + params.Values = make(url.Values) + err := params.prepareDateString(startTime, endTime, true) + if err != nil { + return nil, err + } + params.Values.Set("productType", productType) + params.Values.Set("symbol", pair) + params.Values.Set("coin", currency) + params.Values.Set("businessType", businessType) + if pagination != 0 { + params.Values.Set("idLessThan", strconv.FormatInt(pagination, 10)) + } + if limit != 0 { + params.Values.Set("limit", strconv.FormatInt(limit, 10)) + } + path := bitgetMix + bitgetAccount + bitgetBill + var resp *FutureAccBillResp + return resp, bi.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, Rate10, http.MethodGet, path, params.Values, + nil, &resp) +} + +// SendAuthenticatedHTTPRequest sends an authenticated HTTP request +func (bi *Bitget) SendAuthenticatedHTTPRequest(ctx context.Context, ep exchange.URL, rateLim request.EndpointLimit, method, path string, queryParams url.Values, bodyParams map[string]interface{}, result interface{}) error { + creds, err := bi.GetCredentials(ctx) + if err != nil { + return err + } + endpoint, err := bi.API.Endpoints.GetURL(ep) + if err != nil { + return err + } + path = common.EncodeURLValues(path, queryParams) + newRequest := func() (*request.Item, error) { + payload := []byte("") + if bodyParams != nil { + payload, err = json.Marshal(bodyParams) + if err != nil { + return nil, err + } + } + t := strconv.FormatInt(time.Now().UnixMilli(), 10) + message := t + method + "/api/v2/" + path + string(payload) + // The exchange also supports user-generated RSA keys, but we haven't implemented that yet + var hmac []byte + hmac, err = crypto.GetHMAC(crypto.HashSHA256, []byte(message), []byte(creds.Secret)) + if err != nil { + return nil, err + } + headers := make(map[string]string) + headers["ACCESS-KEY"] = creds.Key + headers["ACCESS-SIGN"] = crypto.Base64Encode(hmac) + headers["ACCESS-TIMESTAMP"] = t + headers["ACCESS-PASSPHRASE"] = creds.ClientID + headers["Content-Type"] = "application/json" + headers["locale"] = "en-US" + return &request.Item{ + Method: method, + Path: endpoint + path, + Headers: headers, + Body: bytes.NewBuffer(payload), + Result: &result, + Verbose: bi.Verbose, + HTTPDebugging: bi.HTTPDebugging, + HTTPRecording: bi.HTTPRecording, + }, nil + } + return bi.SendPayload(ctx, rateLim, newRequest, request.AuthenticatedRequest) +} + +// SendHTTPRequest sends an unauthenticated HTTP request, with a few assumptions about the request; +// namely that it is a GET request with no body +func (bi *Bitget) SendHTTPRequest(ctx context.Context, ep exchange.URL, rateLim request.EndpointLimit, path string, queryParams url.Values, result interface{}) error { + endpoint, err := bi.API.Endpoints.GetURL(ep) + if err != nil { + return err + } + newRequest := func() (*request.Item, error) { + path = common.EncodeURLValues(path, queryParams) + return &request.Item{ + Method: "GET", + Path: endpoint + path, + Result: &result, + Verbose: bi.Verbose, + HTTPDebugging: bi.HTTPDebugging, + HTTPRecording: bi.HTTPRecording, + }, nil + } + return bi.SendPayload(ctx, rateLim, newRequest, request.UnauthenticatedRequest) +} + +// PrepareDateString encodes a set of parameters indicating start & end dates +func (p *Params) prepareDateString(startDate, endDate time.Time, ignoreUnset bool) error { + err := common.StartEndTimeCheck(startDate, endDate) + if err != nil { + if errors.Is(err, common.ErrDateUnset) && ignoreUnset { + return nil + } + return err + } + p.Values.Set("startTime", strconv.FormatInt(startDate.UnixMilli(), 10)) + p.Values.Set("endTime", strconv.FormatInt(endDate.UnixMilli(), 10)) + return nil +} + +// UnmarshalJSON unmarshals the JSON input into a UnixTimestamp type +func (t *UnixTimestamp) UnmarshalJSON(b []byte) error { + var timestampStr string + err := json.Unmarshal(b, ×tampStr) + if err != nil { + return err + } + if timestampStr == "" { + *t = UnixTimestamp(time.Time{}) + return nil + } + timestamp, err := strconv.ParseInt(timestampStr, 10, 64) + if err != nil { + return err + } + *t = UnixTimestamp(time.UnixMilli(timestamp).UTC()) + return nil +} + +// String implements the stringer interface +func (t *UnixTimestamp) String() string { + return t.Time().String() +} + +// Time returns the time.Time representation of the UnixTimestamp +func (t *UnixTimestamp) Time() time.Time { + return time.Time(*t) +} + +// UnmarshalJSON unmarshals the JSON input into a YesNoBool type +func (y *YesNoBool) UnmarshalJSON(b []byte) error { + var yn string + err := json.Unmarshal(b, &yn) + if err != nil { + return err + } + switch yn { + case "yes": + *y = true + case "no": + *y = false + } + return nil +} + +// UnmarshalJSON unmarshals the JSON input into a SuccessBool type +func (s *SuccessBool) UnmarshalJSON(b []byte) error { + var success string + err := json.Unmarshal(b, &success) + if err != nil { + return err + } + switch success { + case "success": + *s = true + case "failure": + *s = false + } + return nil +} + +// UnmarshalJSON unmarshals the JSON input into an EmptyInt type +func (e *EmptyInt) UnmarshalJSON(b []byte) error { + var num string + err := json.Unmarshal(b, &num) + if err != nil { + return err + } + if num == "" { + *e = 0 + return nil + } + i, err := strconv.ParseInt(num, 10, 64) + if err != nil { + return err } *e = EmptyInt(i) return nil } + +// CandlestickHelper pulls out common candlestick functionality to avoid repetition +func (bi *Bitget) candlestickHelper(ctx context.Context, pair, granularity, path string, limit uint16, params Params) (*CandleData, error) { + params.Values.Set("symbol", pair) + params.Values.Set("granularity", granularity) + if limit != 0 { + params.Values.Set("limit", strconv.FormatUint(uint64(limit), 10)) + } + var resp *CandleResponse + err := bi.SendHTTPRequest(ctx, exchange.RestSpot, Rate20, path, params.Values, &resp) + if err != nil { + return nil, err + } + if len(resp.Data) == 0 { + return nil, errNoCandleData + } + var spot bool + var data CandleData + if resp.Data[0][7] == nil { + data.FuturesCandles = make([]OneFuturesCandle, len(resp.Data)) + } else { + spot = true + data.SpotCandles = make([]OneSpotCandle, len(resp.Data)) + } + for i := range resp.Data { + timeTemp, ok := resp.Data[i][0].(string) + if !ok { + return nil, errTypeAssertTimestamp + } + timeTemp = (timeTemp)[1 : len(timeTemp)-1] + timeTemp2, err := strconv.ParseInt(timeTemp, 10, 64) + if err != nil { + return nil, err + } + openTemp, ok := resp.Data[i][1].(string) + if !ok { + return nil, errTypeAssertOpenPrice + } + highTemp, ok := resp.Data[i][2].(string) + if !ok { + return nil, errTypeAssertHighPrice + } + lowTemp, ok := resp.Data[i][3].(string) + if !ok { + return nil, errTypeAssertLowPrice + } + closeTemp, ok := resp.Data[i][4].(string) + if !ok { + return nil, errTypeAssertClosePrice + } + baseVolumeTemp := resp.Data[i][5].(string) + if !ok { + return nil, errTypeAssertBaseVolume + } + quoteVolumeTemp := resp.Data[i][6].(string) + if !ok { + return nil, errTypeAssertQuoteVolume + } + if spot { + usdtVolumeTemp := resp.Data[i][7].(string) + if !ok { + return nil, errTypeAssertUSDTVolume + } + data.SpotCandles[i].Timestamp = time.Time(UnixTimestamp(time.UnixMilli(timeTemp2).UTC())) + data.SpotCandles[i].Open, err = strconv.ParseFloat(openTemp, 64) + if err != nil { + return nil, err + } + data.SpotCandles[i].High, err = strconv.ParseFloat(highTemp, 64) + if err != nil { + return nil, err + } + data.SpotCandles[i].Low, err = strconv.ParseFloat(lowTemp, 64) + if err != nil { + return nil, err + } + data.SpotCandles[i].Close, err = strconv.ParseFloat(closeTemp, 64) + if err != nil { + return nil, err + } + data.SpotCandles[i].BaseVolume, err = strconv.ParseFloat(baseVolumeTemp, 64) + if err != nil { + return nil, err + } + data.SpotCandles[i].QuoteVolume, err = strconv.ParseFloat(quoteVolumeTemp, 64) + if err != nil { + return nil, err + } + data.SpotCandles[i].USDTVolume, err = strconv.ParseFloat(usdtVolumeTemp, 64) + if err != nil { + return nil, err + } + } else { + data.FuturesCandles[i].Timestamp = time.Time(UnixTimestamp(time.UnixMilli(timeTemp2).UTC())) + data.FuturesCandles[i].Entry, err = strconv.ParseFloat(openTemp, 64) + if err != nil { + return nil, err + } + data.FuturesCandles[i].High, err = strconv.ParseFloat(highTemp, 64) + if err != nil { + return nil, err + } + data.FuturesCandles[i].Low, err = strconv.ParseFloat(lowTemp, 64) + if err != nil { + return nil, err + } + data.FuturesCandles[i].Exit, err = strconv.ParseFloat(closeTemp, 64) + if err != nil { + return nil, err + } + data.FuturesCandles[i].BaseVolume, err = strconv.ParseFloat(baseVolumeTemp, 64) + if err != nil { + return nil, err + } + data.FuturesCandles[i].QuoteVolume, err = strconv.ParseFloat(quoteVolumeTemp, 64) + if err != nil { + return nil, err + } + } + } + return &data, nil +} diff --git a/exchanges/bitget/bitget_test.go b/exchanges/bitget/bitget_test.go index 973d500cd01..14ca52bf4b6 100644 --- a/exchanges/bitget/bitget_test.go +++ b/exchanges/bitget/bitget_test.go @@ -28,7 +28,12 @@ const ( canManipulateRealOrders = false testingInSandbox = false - testSubAccountName = "GCTTESTA" + testSubaccountName = "GCTTESTA" + testIP = "14.203.57.50" + testAmount = 0.00001 + testPrice = 1e9 + // Donation address by default + testAddress = "bc1qk0jareu4jytc0cfrhr5wgshsq8282awpavfahc" ) // User-defined variables to aid testing @@ -37,19 +42,15 @@ var ( testCrypto2 = currency.DOGE // Used for endpoints which consume all available funds testFiat = currency.USDT testPair = currency.NewPair(testCrypto, testFiat) - testIP = "14.203.57.50" - testAmount = 0.00001 - testPrice = 1e9 ) // Developer-defined constants to aid testing const ( - skipTestSubAccNotFound = "test sub-account not found, skipping" + skipTestSubAccNotFound = "appropriate sub-account (equals %v, not equals %v) not found, skipping" skipInsufficientAPIKeysFound = "insufficient API keys found, skipping" skipInsufficientBalance = "insufficient balance to place order, skipping" - errorAPIKeyLimitPartial = `Bitget unsuccessful HTTP status code: 400 raw response: {"code":"40063","msg":"API exceeds the maximum limit added","requestTime":` - errorInsufficientBalancePartial = `Bitget unsuccessful HTTP status code: 400 raw response: {"code":"43012","msg":"Insufficient balance","requestTime":` + errorAPIKeyLimitPartial = `Bitget unsuccessful HTTP status code: 400 raw response: {"code":"40063","msg":"API exceeds the maximum limit added","requestTime":` ) var bi = &Bitget{} @@ -185,9 +186,9 @@ func TestGetMerchantAdvertisementList(t *testing.T) { func TestCreateVirtualSubaccounts(t *testing.T) { _, err := bi.CreateVirtualSubaccounts(context.Background(), []string{}) - assert.ErrorIs(t, err, errSubAccountEmpty) + assert.ErrorIs(t, err, errSubaccountEmpty) sharedtestvalues.SkipTestIfCredentialsUnset(t, bi) - resp, err := bi.CreateVirtualSubaccounts(context.Background(), []string{testSubAccountName}) + resp, err := bi.CreateVirtualSubaccounts(context.Background(), []string{testSubaccountName}) assert.NoError(t, err) assert.NotEmpty(t, resp) } @@ -195,13 +196,13 @@ func TestCreateVirtualSubaccounts(t *testing.T) { func TestModifyVirtualSubaccount(t *testing.T) { perms := []string{} _, err := bi.ModifyVirtualSubaccount(context.Background(), "", "", perms) - assert.ErrorIs(t, err, errSubAccountEmpty) + assert.ErrorIs(t, err, errSubaccountEmpty) _, err = bi.ModifyVirtualSubaccount(context.Background(), "meow", "", perms) assert.ErrorIs(t, err, errNewStatusEmpty) _, err = bi.ModifyVirtualSubaccount(context.Background(), "meow", "woof", perms) assert.ErrorIs(t, err, errNewPermsEmpty) sharedtestvalues.SkipTestIfCredentialsUnset(t, bi, canManipulateRealOrders) - tarID := subAccTestHelper(t) + tarID := subAccTestHelper(t, strings.ToLower(string(testSubaccountName[:3]))+"****@virtual-bitget.com", "") perms = append(perms, "read") resp, err := bi.ModifyVirtualSubaccount(context.Background(), tarID, "normal", perms) assert.NoError(t, err) @@ -211,7 +212,7 @@ func TestModifyVirtualSubaccount(t *testing.T) { func TestCreateSubaccountAndAPIKey(t *testing.T) { ipL := []string{} _, err := bi.CreateSubaccountAndAPIKey(context.Background(), "", "", "", ipL, ipL) - assert.ErrorIs(t, err, errSubAccountEmpty) + assert.ErrorIs(t, err, errSubaccountEmpty) sharedtestvalues.SkipTestIfCredentialsUnset(t, bi, canManipulateRealOrders) ipL = append(ipL, testIP) pL := []string{"read"} @@ -231,18 +232,22 @@ func TestGetVirtualSubaccounts(t *testing.T) { func TestCreateAPIKey(t *testing.T) { ipL := []string{} _, err := bi.CreateAPIKey(context.Background(), "", "", "", ipL, ipL) - assert.ErrorIs(t, err, errSubAccountEmpty) + assert.ErrorIs(t, err, errSubaccountEmpty) _, err = bi.CreateAPIKey(context.Background(), "woof", "", "", ipL, ipL) assert.ErrorIs(t, err, errPassphraseEmpty) _, err = bi.CreateAPIKey(context.Background(), "woof", "meow", "", ipL, ipL) assert.ErrorIs(t, err, errLabelEmpty) sharedtestvalues.SkipTestIfCredentialsUnset(t, bi, canManipulateRealOrders) - tarID := subAccTestHelper(t) + tarID := subAccTestHelper(t, strings.ToLower(string(testSubaccountName[:3]))+"****@virtual-bitget.com", "") ipL = append(ipL, testIP) pL := []string{"read"} _, err = bi.CreateAPIKey(context.Background(), tarID, clientID, "neigh whinny", ipL, pL) - if err != nil && !strings.Contains(err.Error(), errorAPIKeyLimitPartial) { - t.Error(err) + if err != nil { + if !strings.Contains(err.Error(), errorAPIKeyLimitPartial) { + t.Log(err) + } else { + t.Error(err) + } } } @@ -255,15 +260,15 @@ func TestModifyAPIKey(t *testing.T) { _, err = bi.ModifyAPIKey(context.Background(), "", "meow", "", "woof", ipL, ipL) assert.ErrorIs(t, err, errLabelEmpty) _, err = bi.ModifyAPIKey(context.Background(), "", "meow", "quack", "woof", ipL, ipL) - assert.ErrorIs(t, err, errSubAccountEmpty) + assert.ErrorIs(t, err, errSubaccountEmpty) sharedtestvalues.SkipTestIfCredentialsUnset(t, bi, canManipulateRealOrders) - tarID := subAccTestHelper(t) + tarID := subAccTestHelper(t, strings.ToLower(string(testSubaccountName[:3]))+"****@virtual-bitget.com", "") resp, err := bi.GetAPIKeys(context.Background(), tarID) assert.NoError(t, err) if len(resp.Data) == 0 { t.Skip(skipInsufficientAPIKeysFound) } - resp2, err := bi.ModifyAPIKey(context.Background(), tarID, clientID, "oink", resp.Data[0].SubAccountApiKey, + resp2, err := bi.ModifyAPIKey(context.Background(), tarID, clientID, "oink", resp.Data[0].SubaccountApiKey, ipL, ipL) assert.NoError(t, err) assert.NotEmpty(t, resp2) @@ -271,9 +276,9 @@ func TestModifyAPIKey(t *testing.T) { func TestGetAPIKeys(t *testing.T) { _, err := bi.GetAPIKeys(context.Background(), "") - assert.ErrorIs(t, err, errSubAccountEmpty) + assert.ErrorIs(t, err, errSubaccountEmpty) sharedtestvalues.SkipTestIfCredentialsUnset(t, bi) - tarID := subAccTestHelper(t) + tarID := subAccTestHelper(t, strings.ToLower(string(testSubaccountName[:3]))+"****@virtual-bitget.com", "") resp, err := bi.GetAPIKeys(context.Background(), tarID) assert.NoError(t, err) assert.NotEmpty(t, resp) @@ -312,9 +317,7 @@ func TestCommitConversion(t *testing.T) { assert.NoError(t, err) _, err = bi.CommitConversion(context.Background(), testCrypto.String(), testFiat.String(), resp.Data.TraceID, resp.Data.FromCoinSize, resp.Data.ToCoinSize, resp.Data.ConvertPrice) - if err != nil && !strings.Contains(err.Error(), errorInsufficientBalancePartial) { - t.Error(err) - } + assert.NoError(t, err) } func TestGetConvertHistory(t *testing.T) { @@ -362,20 +365,20 @@ func TestGetSymbolInfo(t *testing.T) { assert.NotEmpty(t, resp) } -func TestGetVIPFeeRate(t *testing.T) { - testGetNoArgs(t, bi.GetVIPFeeRate) +func TestGetSpotVIPFeeRate(t *testing.T) { + testGetNoArgs(t, bi.GetSpotVIPFeeRate) } -func TestGetTickerInformation(t *testing.T) { - resp, err := bi.GetTickerInformation(context.Background(), testPair.String()) +func TestGetSpotTickerInformation(t *testing.T) { + resp, err := bi.GetSpotTickerInformation(context.Background(), testPair.String()) assert.NoError(t, err) assert.NotEmpty(t, resp) } -func TestGetMergeDepth(t *testing.T) { - _, err := bi.GetMergeDepth(context.Background(), "", "", "") +func TestGetSpotMergeDepth(t *testing.T) { + _, err := bi.GetSpotMergeDepth(context.Background(), "", "", "") assert.ErrorIs(t, err, errPairEmpty) - resp, err := bi.GetMergeDepth(context.Background(), testPair.String(), "scale3", "5") + resp, err := bi.GetSpotMergeDepth(context.Background(), testPair.String(), "scale3", "5") assert.NoError(t, err) assert.NotEmpty(t, resp) } @@ -386,41 +389,40 @@ func TestGetOrderbookDepth(t *testing.T) { assert.NotEmpty(t, resp) } -func TestGetCandlestickData(t *testing.T) { - _, err := bi.GetCandlestickData(context.Background(), "", "", time.Time{}, time.Time{}, 0, false) +func TestGetSpotCandlestickData(t *testing.T) { + _, err := bi.GetSpotCandlestickData(context.Background(), "", "", time.Time{}, time.Time{}, 0, false) assert.ErrorIs(t, err, errPairEmpty) - _, err = bi.GetCandlestickData(context.Background(), "meow", "", time.Time{}, time.Time{}, 0, false) + _, err = bi.GetSpotCandlestickData(context.Background(), "meow", "", time.Time{}, time.Time{}, 0, false) assert.ErrorIs(t, err, errGranEmpty) - _, err = bi.GetCandlestickData(context.Background(), "meow", "woof", time.Time{}, time.Time{}, 5, - true) + _, err = bi.GetSpotCandlestickData(context.Background(), "meow", "woof", time.Time{}, time.Time{}, 5, true) assert.ErrorIs(t, err, errEndTimeEmpty) - _, err = bi.GetCandlestickData(context.Background(), "meow", "woof", time.Now(), time.Now().Add(-time.Second), 0, - false) + _, err = bi.GetSpotCandlestickData(context.Background(), "meow", "woof", time.Now(), time.Now().Add(-time.Second), + 0, false) assert.ErrorIs(t, err, common.ErrStartAfterEnd) - resp, err := bi.GetCandlestickData(context.Background(), testPair.String(), "1min", time.Time{}, time.Time{}, 5, - false) + resp, err := bi.GetSpotCandlestickData(context.Background(), testPair.String(), "1min", time.Time{}, time.Time{}, + 5, false) assert.NoError(t, err) assert.NotEmpty(t, resp) - resp, err = bi.GetCandlestickData(context.Background(), testPair.String(), "1min", time.Time{}, time.Now(), 5, + resp, err = bi.GetSpotCandlestickData(context.Background(), testPair.String(), "1min", time.Time{}, time.Now(), 5, true) assert.NoError(t, err) assert.NotEmpty(t, resp) } -func TestGetRecentFills(t *testing.T) { - _, err := bi.GetRecentFills(context.Background(), "", 0) +func TestGetRecentSpotFills(t *testing.T) { + _, err := bi.GetRecentSpotFills(context.Background(), "", 0) assert.ErrorIs(t, err, errPairEmpty) - resp, err := bi.GetRecentFills(context.Background(), testPair.String(), 5) + resp, err := bi.GetRecentSpotFills(context.Background(), testPair.String(), 5) assert.NoError(t, err) assert.NotEmpty(t, resp) } -func TestGetMarketTrades(t *testing.T) { - _, err := bi.GetMarketTrades(context.Background(), "", time.Time{}, time.Time{}, 0, 0) +func TestGetSpotMarketTrades(t *testing.T) { + _, err := bi.GetSpotMarketTrades(context.Background(), "", time.Time{}, time.Time{}, 0, 0) assert.ErrorIs(t, err, errPairEmpty) - _, err = bi.GetMarketTrades(context.Background(), "meow", time.Now(), time.Now().Add(-time.Second), 0, 0) + _, err = bi.GetSpotMarketTrades(context.Background(), "meow", time.Now(), time.Now().Add(-time.Second), 0, 0) assert.ErrorIs(t, err, common.ErrStartAfterEnd) - resp, err := bi.GetMarketTrades(context.Background(), testPair.String(), time.Time{}, time.Time{}, 5, + resp, err := bi.GetSpotMarketTrades(context.Background(), testPair.String(), time.Time{}, time.Time{}, 5, 1<<63-1) assert.NoError(t, err) assert.NotEmpty(t, resp) @@ -441,9 +443,7 @@ func TestPlaceOrder(t *testing.T) { assert.ErrorIs(t, err, errAmountEmpty) sharedtestvalues.SkipTestIfCredentialsUnset(t, bi, canManipulateRealOrders) _, err = bi.PlaceOrder(context.Background(), testPair.String(), "sell", "limit", "IOC", "", testPrice, testAmount) - if err != nil && !strings.Contains(err.Error(), errorInsufficientBalancePartial) { - t.Error(err) - } + assert.NoError(t, err) } func TestCancelOrderByID(t *testing.T) { @@ -456,13 +456,15 @@ func TestCancelOrderByID(t *testing.T) { if err == nil { t.Error(errNonsenseRequest) } - resp, err := bi.PlaceOrder(context.Background(), testPair.String(), "sell", "limit", "GTC", "", testPrice, - testAmount) - if strings.Contains(err.Error(), errorInsufficientBalancePartial) { - t.Skip(skipInsufficientBalance) + cID := testSubaccountName + strconv.FormatInt(time.Now().Unix(), 10) + if len(cID) > 50 { + cID = cID[:50] } - assert.NoError(t, err) - _, err = bi.CancelOrderByID(context.Background(), testPair.String(), "", int64(resp.Data.OrderID)) + resp, err := bi.PlaceOrder(context.Background(), testPair.String(), "sell", "limit", "GTC", cID, testPrice, + testAmount) + require.NoError(t, err) + _, err = bi.CancelOrderByID(context.Background(), testPair.String(), resp.Data.ClientOrderID, + int64(resp.Data.OrderID)) assert.NoError(t, err) } @@ -497,13 +499,10 @@ func TestBatchCancelOrders(t *testing.T) { t.Error(errNonsenseRequest) } resp, err := bi.PlaceOrder(context.Background(), testPair.String(), "sell", "limit", "IOC", "", testPrice, testAmount) - if strings.Contains(err.Error(), errorInsufficientBalancePartial) { - t.Skip(skipInsufficientBalance) - } - assert.NoError(t, err) + require.NoError(t, err) req = append(req, OrderIDStruct{ - OrderID: int64(resp.Data.OrderID), - ClientOID: resp.Data.ClientOrderID, + OrderID: int64(resp.Data.OrderID), + ClientOrderID: resp.Data.ClientOrderID, }) resp2, err := bi.BatchCancelOrders(context.Background(), testPair.String(), req) assert.NoError(t, err) @@ -524,7 +523,7 @@ func TestGetOrderDetails(t *testing.T) { assert.ErrorIs(t, err, errOrderClientEmpty) sharedtestvalues.SkipTestIfCredentialsUnset(t, bi) ordIDs := getPlanOrdIDHelper(t) - _, err = bi.GetOrderDetails(context.Background(), ordIDs.OrderID, ordIDs.ClientOID) + _, err = bi.GetOrderDetails(context.Background(), ordIDs.OrderID, ordIDs.ClientOrderID) assert.NoError(t, err) } @@ -572,7 +571,7 @@ func TestPlacePlanOrder(t *testing.T) { _, err = bi.PlacePlanOrder(context.Background(), "meow", "woof", "neigh", "", "", "", "", 1, 0, 1) assert.ErrorIs(t, err, errTriggerTypeEmpty) sharedtestvalues.SkipTestIfCredentialsUnset(t, bi, canManipulateRealOrders) - cID := testSubAccountName + strconv.FormatInt(time.Now().Unix(), 10) + cID := testSubaccountName + strconv.FormatInt(time.Now().Unix(), 10) if len(cID) > 50 { cID = cID[:50] } @@ -598,7 +597,7 @@ func TestModifyPlanOrder(t *testing.T) { "ioc", testPrice, testPrice, testAmount) assert.NoError(t, err) require.NotEmpty(t, ordID) - resp, err := bi.ModifyPlanOrder(context.Background(), ordID.Data.OrderID, ordID.Data.ClientOID, "limit", + resp, err := bi.ModifyPlanOrder(context.Background(), ordID.Data.OrderID, ordID.Data.ClientOrderID, "limit", testPrice, testPrice, testAmount) assert.NoError(t, err) assert.NotEmpty(t, resp) @@ -612,7 +611,7 @@ func TestCancelPlanOrder(t *testing.T) { "ioc", testPrice, testPrice, testAmount) assert.NoError(t, err) require.NotEmpty(t, ordID) - resp, err := bi.CancelPlanOrder(context.Background(), ordID.Data.OrderID, ordID.Data.ClientOID) + resp, err := bi.CancelPlanOrder(context.Background(), ordID.Data.OrderID, ordID.Data.ClientOrderID) assert.NoError(t, err) assert.NotEmpty(t, resp) } @@ -670,9 +669,9 @@ func TestGetAccountAssets(t *testing.T) { assert.NotEmpty(t, resp) } -func TestGetSubAccountAssets(t *testing.T) { +func TestGetSpotSubaccountAssets(t *testing.T) { sharedtestvalues.SkipTestIfCredentialsUnset(t, bi) - resp, err := bi.GetSubAccountAssets(context.Background()) + resp, err := bi.GetSpotSubaccountAssets(context.Background()) assert.NoError(t, err) assert.NotEmpty(t, resp) } @@ -688,6 +687,360 @@ func TestModifyDepositAccount(t *testing.T) { assert.NotEmpty(t, resp) } +func TestGetAccountBills(t *testing.T) { + _, err := bi.GetSpotAccountBills(context.Background(), "", "", "", time.Now(), time.Now().Add(-time.Minute), 0, 0) + assert.ErrorIs(t, err, common.ErrStartAfterEnd) + sharedtestvalues.SkipTestIfCredentialsUnset(t, bi) + _, err = bi.GetSpotAccountBills(context.Background(), testCrypto.String(), "", "", time.Time{}, time.Time{}, 3, 1<<62) + assert.NoError(t, err) +} + +func TestTransferAsset(t *testing.T) { + _, err := bi.TransferAsset(context.Background(), "", "", "", "", "", 0) + assert.ErrorIs(t, err, errFromTypeEmpty) + _, err = bi.TransferAsset(context.Background(), "meow", "", "", "", "", 0) + assert.ErrorIs(t, err, errToTypeEmpty) + _, err = bi.TransferAsset(context.Background(), "meow", "woof", "", "", "", 0) + assert.ErrorIs(t, err, errCurrencyAndPairEmpty) + _, err = bi.TransferAsset(context.Background(), "meow", "woof", "neigh", "", "", 0) + assert.ErrorIs(t, err, errAmountEmpty) + sharedtestvalues.SkipTestIfCredentialsUnset(t, bi, canManipulateRealOrders) + cID := testSubaccountName + strconv.FormatInt(time.Now().Unix(), 10) + if len(cID) > 50 { + cID = cID[:50] + } + _, err = bi.TransferAsset(context.Background(), "spot", "p2p", testCrypto.String(), testPair.String(), cID, + testAmount) + assert.NoError(t, err) +} + +func TestGetTransferableCoinList(t *testing.T) { + _, err := bi.GetTransferableCoinList(context.Background(), "", "") + assert.ErrorIs(t, err, errFromTypeEmpty) + _, err = bi.GetTransferableCoinList(context.Background(), "meow", "") + assert.ErrorIs(t, err, errToTypeEmpty) + sharedtestvalues.SkipTestIfCredentialsUnset(t, bi) + resp, err := bi.GetTransferableCoinList(context.Background(), "spot", "p2p") + assert.NoError(t, err) + assert.NotEmpty(t, resp) +} + +func TestSubaccountTransfer(t *testing.T) { + _, err := bi.SubaccountTransfer(context.Background(), "", "", "", "", "", "", "", 0) + assert.ErrorIs(t, err, errFromTypeEmpty) + _, err = bi.SubaccountTransfer(context.Background(), "meow", "", "", "", "", "", "", 0) + assert.ErrorIs(t, err, errToTypeEmpty) + _, err = bi.SubaccountTransfer(context.Background(), "meow", "woof", "", "", "", "", "", 0) + assert.ErrorIs(t, err, errCurrencyAndPairEmpty) + _, err = bi.SubaccountTransfer(context.Background(), "meow", "woof", "neigh", "", "", "", "", 0) + assert.ErrorIs(t, err, errFromIDEmpty) + _, err = bi.SubaccountTransfer(context.Background(), "meow", "woof", "neigh", "", "", "oink", "", 0) + assert.ErrorIs(t, err, errToIDEmpty) + _, err = bi.SubaccountTransfer(context.Background(), "meow", "woof", "neigh", "", "", "oink", "quack", 0) + assert.ErrorIs(t, err, errAmountEmpty) + sharedtestvalues.SkipTestIfCredentialsUnset(t, bi, canManipulateRealOrders) + fromID := subAccTestHelper(t, "", strings.ToLower(string(testSubaccountName[:3]))+"****@virtual-bitget.com") + toID := subAccTestHelper(t, strings.ToLower(string(testSubaccountName[:3]))+"****@virtual-bitget.com", "") + cID := testSubaccountName + strconv.FormatInt(time.Now().Unix(), 10) + if len(cID) > 50 { + cID = cID[:50] + } + _, err = bi.SubaccountTransfer(context.Background(), "spot", "p2p", testCrypto.String(), testPair.String(), cID, + fromID, toID, testAmount) + assert.NoError(t, err) +} + +func TestWithdrawFunds(t *testing.T) { + _, err := bi.WithdrawFunds(context.Background(), "", "", "", "", "", "", "", "", "", 0) + assert.ErrorIs(t, err, errCurrencyEmpty) + _, err = bi.WithdrawFunds(context.Background(), "meow", "", "", "", "", "", "", "", "", 0) + assert.ErrorIs(t, err, errTransferTypeEmpty) + _, err = bi.WithdrawFunds(context.Background(), "meow", "woof", "", "", "", "", "", "", "", 0) + assert.ErrorIs(t, err, errAddressEmpty) + _, err = bi.WithdrawFunds(context.Background(), "meow", "woof", "neigh", "", "", "", "", "", "", 0) + assert.ErrorIs(t, err, errAmountEmpty) + sharedtestvalues.SkipTestIfCredentialsUnset(t, bi, canManipulateRealOrders) + _, err = bi.WithdrawFunds(context.Background(), testCrypto.String(), "on_chain", testAddress, "", "", "", "", "", + "", testAmount) + assert.NoError(t, err) +} + +func TestGetSubaccountTransferRecord(t *testing.T) { + _, err := bi.GetSubaccountTransferRecord(context.Background(), "", "", "", time.Now(), time.Now().Add(-time.Minute), + 0, 0) + assert.ErrorIs(t, err, common.ErrStartAfterEnd) + sharedtestvalues.SkipTestIfCredentialsUnset(t, bi) + _, err = bi.GetSubaccountTransferRecord(context.Background(), "", "", "meow", time.Time{}, time.Time{}, 3, 1<<62) + assert.NoError(t, err) +} + +func TestGetTransferRecord(t *testing.T) { + _, err := bi.GetTransferRecord(context.Background(), "", "", "", time.Time{}, time.Time{}, 0, 0) + assert.ErrorIs(t, err, errCurrencyEmpty) + _, err = bi.GetTransferRecord(context.Background(), "meow", "", "", time.Time{}, time.Time{}, 0, 0) + assert.ErrorIs(t, err, errFromTypeEmpty) + _, err = bi.GetTransferRecord(context.Background(), "meow", "woof", "", time.Now(), time.Now().Add(-time.Minute), 0, 0) + assert.ErrorIs(t, err, common.ErrStartAfterEnd) + sharedtestvalues.SkipTestIfCredentialsUnset(t, bi) + _, err = bi.GetTransferRecord(context.Background(), testCrypto.String(), "spot", "meow", time.Time{}, time.Time{}, + 3, 1<<62) + assert.NoError(t, err) +} + +func TestGetDepositAddressForCurrency(t *testing.T) { + _, err := bi.GetDepositAddressForCurrency(context.Background(), "", "") + assert.ErrorIs(t, err, errCurrencyEmpty) + sharedtestvalues.SkipTestIfCredentialsUnset(t, bi) + resp, err := bi.GetDepositAddressForCurrency(context.Background(), testCrypto.String(), "") + assert.NoError(t, err) + assert.NotEmpty(t, resp) +} + +func TestGetSubaccountDepositAddress(t *testing.T) { + _, err := bi.GetSubaccountDepositAddress(context.Background(), "", "", "") + assert.ErrorIs(t, err, errSubaccountEmpty) + _, err = bi.GetSubaccountDepositAddress(context.Background(), "meow", "", "") + assert.ErrorIs(t, err, errCurrencyEmpty) + sharedtestvalues.SkipTestIfCredentialsUnset(t, bi) + tarID := subAccTestHelper(t, "", "") + resp, err := bi.GetSubaccountDepositAddress(context.Background(), tarID, testCrypto.String(), "") + assert.NoError(t, err) + assert.NotEmpty(t, resp) +} + +func TestGetSubaccountDepositRecords(t *testing.T) { + _, err := bi.GetSubaccountDepositRecords(context.Background(), "", "", 0, 0, 0, time.Time{}, time.Time{}) + assert.ErrorIs(t, err, errSubaccountEmpty) + _, err = bi.GetSubaccountDepositRecords(context.Background(), "meow", "", 0, 0, 0, time.Now(), + time.Now().Add(-time.Minute)) + assert.ErrorIs(t, err, common.ErrStartAfterEnd) + sharedtestvalues.SkipTestIfCredentialsUnset(t, bi) + tarID := subAccTestHelper(t, "", "") + _, err = bi.GetSubaccountDepositRecords(context.Background(), tarID, "", 0, 1<<62, 2, time.Time{}, time.Time{}) + assert.NoError(t, err) +} + +func TestGetDepositRecords(t *testing.T) { + _, err := bi.GetDepositRecords(context.Background(), "", 0, 0, 0, time.Time{}, time.Time{}) + assert.ErrorIs(t, err, common.ErrDateUnset) + sharedtestvalues.SkipTestIfCredentialsUnset(t, bi) + _, err = bi.GetDepositRecords(context.Background(), testCrypto.String(), 0, 1<<62, 2, + time.Now().Add(-time.Hour*24*90), time.Now()) + assert.NoError(t, err) +} + +func TestGetFuturesMergeDepth(t *testing.T) { + _, err := bi.GetFuturesMergeDepth(context.Background(), "", "", "", "") + assert.ErrorIs(t, err, errPairEmpty) + _, err = bi.GetFuturesMergeDepth(context.Background(), "meow", "", "", "") + assert.ErrorIs(t, err, errProductTypeEmpty) + resp, err := bi.GetFuturesMergeDepth(context.Background(), testPair.String(), "USDT-FUTURES", "scale3", "5") + assert.NoError(t, err) + assert.NotEmpty(t, resp) +} + +func TestGetFuturesVIPFeeRate(t *testing.T) { + testGetNoArgs(t, bi.GetFuturesVIPFeeRate) +} + +func TestGetFuturesTicker(t *testing.T) { + testGetTwoArgs(t, bi.GetFuturesTicker) +} + +func TestGetAllFuturesTickers(t *testing.T) { + _, err := bi.GetAllFuturesTickers(context.Background(), "") + assert.ErrorIs(t, err, errProductTypeEmpty) + resp, err := bi.GetAllFuturesTickers(context.Background(), "COIN-FUTURES") + assert.NoError(t, err) + assert.NotEmpty(t, resp) +} + +func TestGetRecentFuturesFills(t *testing.T) { + _, err := bi.GetRecentFuturesFills(context.Background(), "", "", 0) + assert.ErrorIs(t, err, errPairEmpty) + _, err = bi.GetRecentFuturesFills(context.Background(), "meow", "", 0) + assert.ErrorIs(t, err, errProductTypeEmpty) + resp, err := bi.GetRecentFuturesFills(context.Background(), testPair.String(), "USDT-FUTURES", 5) + assert.NoError(t, err) + assert.NotEmpty(t, resp) +} + +func TestGetFuturesCandlestickData(t *testing.T) { + _, err := bi.GetFuturesCandlestickData(context.Background(), "", "", "", time.Time{}, time.Time{}, 0, 0) + assert.ErrorIs(t, err, errPairEmpty) + _, err = bi.GetFuturesCandlestickData(context.Background(), "meow", "", "", time.Time{}, time.Time{}, 0, 0) + assert.ErrorIs(t, err, errProductTypeEmpty) + _, err = bi.GetFuturesCandlestickData(context.Background(), "meow", "woof", "", time.Time{}, time.Time{}, 0, 0) + assert.ErrorIs(t, err, errGranEmpty) + _, err = bi.GetFuturesCandlestickData(context.Background(), "meow", "woof", "neigh", time.Now(), + time.Now().Add(-time.Minute), 0, 0) + assert.ErrorIs(t, err, common.ErrStartAfterEnd) + resp, err := bi.GetFuturesCandlestickData(context.Background(), testPair.String(), "USDT-FUTURES", "1m", + time.Time{}, time.Time{}, 5, CallModeNormal) + assert.NoError(t, err) + assert.NotEmpty(t, resp) + resp, err = bi.GetFuturesCandlestickData(context.Background(), testPair.String(), "COIN-FUTURES", "1m", + time.Time{}, time.Time{}, 5, CallModeHistory) + assert.NoError(t, err) + assert.NotEmpty(t, resp) + resp, err = bi.GetFuturesCandlestickData(context.Background(), testPair.String(), "USDC-FUTURES", "1m", + time.Time{}, time.Now(), 5, CallModeIndex) + assert.NoError(t, err) + assert.NotEmpty(t, resp) + resp, err = bi.GetFuturesCandlestickData(context.Background(), testPair.String(), "USDT-FUTURES", "1m", + time.Time{}, time.Now(), 5, CallModeMark) + assert.NoError(t, err) + assert.NotEmpty(t, resp) +} + +func TestGetOpenPositions(t *testing.T) { + testGetTwoArgs(t, bi.GetOpenPositions) +} + +func TestGetNextFundingTime(t *testing.T) { + testGetTwoArgs(t, bi.GetNextFundingTime) +} + +func TestGetFuturesPrices(t *testing.T) { + testGetTwoArgs(t, bi.GetFuturesPrices) +} + +func TestGetFundingHistorical(t *testing.T) { + _, err := bi.GetFundingHistorical(context.Background(), "", "", 0, 0) + assert.ErrorIs(t, err, errPairEmpty) + _, err = bi.GetFundingHistorical(context.Background(), "meow", "", 0, 0) + assert.ErrorIs(t, err, errProductTypeEmpty) + resp, err := bi.GetFundingHistorical(context.Background(), testPair.String(), "USDT-FUTURES", 5, 1) + assert.NoError(t, err) + assert.NotEmpty(t, resp) +} + +func TestGetFundingCurrent(t *testing.T) { + testGetTwoArgs(t, bi.GetFundingCurrent) +} + +func TestGetContractConfig(t *testing.T) { + testGetTwoArgs(t, bi.GetContractConfig) +} + +func TestGetOneFuturesAccount(t *testing.T) { + _, err := bi.GetOneFuturesAccount(context.Background(), "", "", "") + assert.ErrorIs(t, err, errPairEmpty) + _, err = bi.GetOneFuturesAccount(context.Background(), "meow", "", "") + assert.ErrorIs(t, err, errProductTypeEmpty) + _, err = bi.GetOneFuturesAccount(context.Background(), "meow", "woof", "") + assert.ErrorIs(t, err, errMarginCoinEmpty) + sharedtestvalues.SkipTestIfCredentialsUnset(t, bi) + resp, err := bi.GetOneFuturesAccount(context.Background(), testPair.String(), "USDT-FUTURES", "USDT") + assert.NoError(t, err) + assert.NotEmpty(t, resp) +} + +func TestGetAllFuturesAccounts(t *testing.T) { + _, err := bi.GetAllFuturesAccounts(context.Background(), "") + assert.ErrorIs(t, err, errProductTypeEmpty) + sharedtestvalues.SkipTestIfCredentialsUnset(t, bi) + resp, err := bi.GetAllFuturesAccounts(context.Background(), "COIN-FUTURES") + assert.NoError(t, err) + assert.NotEmpty(t, resp) +} + +func TestGetFuturesSubaccountAssets(t *testing.T) { + _, err := bi.GetFuturesSubaccountAssets(context.Background(), "") + assert.ErrorIs(t, err, errProductTypeEmpty) + sharedtestvalues.SkipTestIfCredentialsUnset(t, bi) + resp, err := bi.GetFuturesSubaccountAssets(context.Background(), "USDT-FUTURES") + assert.NoError(t, err) + assert.NotEmpty(t, resp) +} + +func TestGetEstimatedOpenCount(t *testing.T) { + _, err := bi.GetEstimatedOpenCount(context.Background(), "", "", "", 0, 0, 0) + assert.ErrorIs(t, err, errPairEmpty) + _, err = bi.GetEstimatedOpenCount(context.Background(), "meow", "", "", 0, 0, 0) + assert.ErrorIs(t, err, errProductTypeEmpty) + _, err = bi.GetEstimatedOpenCount(context.Background(), "meow", "woof", "", 0, 0, 0) + assert.ErrorIs(t, err, errMarginCoinEmpty) + _, err = bi.GetEstimatedOpenCount(context.Background(), "meow", "woof", "neigh", 0, 0, 0) + assert.ErrorIs(t, err, errOpenAmountEmpty) + _, err = bi.GetEstimatedOpenCount(context.Background(), "meow", "woof", "neigh", 1, 0, 0) + assert.ErrorIs(t, err, errOpenPriceEmpty) + sharedtestvalues.SkipTestIfCredentialsUnset(t, bi) + resp, err := bi.GetEstimatedOpenCount(context.Background(), testPair.String(), "USDT-FUTURES", "USDT", testPrice, + testAmount, 20) + assert.NoError(t, err) + assert.NotEmpty(t, resp) +} + +func TestChangeLeverage(t *testing.T) { + _, err := bi.ChangeLeverage(context.Background(), "", "", "", "", 0) + assert.ErrorIs(t, err, errPairEmpty) + _, err = bi.ChangeLeverage(context.Background(), "meow", "", "", "", 0) + assert.ErrorIs(t, err, errProductTypeEmpty) + _, err = bi.ChangeLeverage(context.Background(), "meow", "woof", "", "", 0) + assert.ErrorIs(t, err, errMarginCoinEmpty) + _, err = bi.ChangeLeverage(context.Background(), "meow", "woof", "neigh", "", 0) + assert.ErrorIs(t, err, errLeverageEmpty) + sharedtestvalues.SkipTestIfCredentialsUnset(t, bi, canManipulateRealOrders) + resp, err := bi.ChangeLeverage(context.Background(), testPair.String(), "USDT-FUTURES", "USDT", "", 20) + assert.NoError(t, err) + assert.NotEmpty(t, resp) +} + +func TestAdjustMargin(t *testing.T) { + err := bi.AdjustMargin(context.Background(), "", "", "", "", 0) + assert.ErrorIs(t, err, errPairEmpty) + err = bi.AdjustMargin(context.Background(), "meow", "", "", "", 0) + assert.ErrorIs(t, err, errProductTypeEmpty) + err = bi.AdjustMargin(context.Background(), "meow", "woof", "", "", 0) + assert.ErrorIs(t, err, errMarginCoinEmpty) + err = bi.AdjustMargin(context.Background(), "meow", "woof", "neigh", "", 0) + assert.ErrorIs(t, err, errAmountEmpty) + sharedtestvalues.SkipTestIfCredentialsUnset(t, bi, canManipulateRealOrders) + // This is getting the error "verification exception margin mode == FIXED", and I can't find a way to + // skirt around that + err = bi.AdjustMargin(context.Background(), testPair.String(), "USDT-FUTURES", "USDT", "long", -testAmount) + assert.NoError(t, err) +} + +func TestChangeMarginMode(t *testing.T) { + _, err := bi.ChangeMarginMode(context.Background(), "", "", "", "") + assert.ErrorIs(t, err, errPairEmpty) + _, err = bi.ChangeMarginMode(context.Background(), "meow", "", "", "") + assert.ErrorIs(t, err, errProductTypeEmpty) + _, err = bi.ChangeMarginMode(context.Background(), "meow", "woof", "", "") + assert.ErrorIs(t, err, errMarginCoinEmpty) + _, err = bi.ChangeMarginMode(context.Background(), "meow", "woof", "neigh", "") + assert.ErrorIs(t, err, errMarginModeEmpty) + sharedtestvalues.SkipTestIfCredentialsUnset(t, bi, canManipulateRealOrders) + resp, err := bi.ChangeMarginMode(context.Background(), testPair.String(), "USDT-FUTURES", "USDT", "crossed") + assert.NoError(t, err) + assert.NotEmpty(t, resp) +} + +func TestChangePositionMode(t *testing.T) { + _, err := bi.ChangePositionMode(context.Background(), "", "") + assert.ErrorIs(t, err, errProductTypeEmpty) + _, err = bi.ChangePositionMode(context.Background(), "meow", "") + assert.ErrorIs(t, err, errPositionModeEmpty) + sharedtestvalues.SkipTestIfCredentialsUnset(t, bi, canManipulateRealOrders) + resp, err := bi.ChangePositionMode(context.Background(), "USDT-FUTURES", "hedge_mode") + assert.NoError(t, err) + assert.NotEmpty(t, resp) +} + +func TestGetFuturesAccountBills(t *testing.T) { + _, err := bi.GetFuturesAccountBills(context.Background(), "", "", "", "", 0, 0, time.Time{}, time.Time{}) + assert.ErrorIs(t, err, errProductTypeEmpty) + _, err = bi.GetFuturesAccountBills(context.Background(), "meow", "", "", "", 0, 0, time.Now(), + time.Now().Add(-time.Minute)) + assert.ErrorIs(t, err, common.ErrStartAfterEnd) + sharedtestvalues.SkipTestIfCredentialsUnset(t, bi) + _, err = bi.GetFuturesAccountBills(context.Background(), "USDT-FUTURES", "", "", "", 0, 0, time.Time{}, + time.Time{}) + assert.NoError(t, err) +} + type getNoArgsResp interface { *TimeResp | *P2PMerInfoResp | *ConvertCoinsResp | *BGBConvertCoinsResp | *VIPFeeRateResp } @@ -701,20 +1054,42 @@ func testGetNoArgs[G getNoArgsResp](t *testing.T, f getNoArgsAssertNotEmpty[G]) assert.NotEmpty(t, resp) } -func subAccTestHelper(t *testing.T) string { +type getTwoArgsResp interface { + *FutureTickerResp | *OpenPositionsResp | *FundingTimeResp | *FuturesPriceResp | *FundingCurrentResp | + *ContractConfigResp +} + +type getTwoArgsPairProduct[G getTwoArgsResp] func(context.Context, string, string) (G, error) + +func testGetTwoArgs[G getTwoArgsResp](t *testing.T, f getTwoArgsPairProduct[G]) { + t.Helper() + _, err := f(context.Background(), "", "") + assert.ErrorIs(t, err, errPairEmpty) + _, err = f(context.Background(), "meow", "") + assert.ErrorIs(t, err, errProductTypeEmpty) + resp, err := f(context.Background(), testPair.String(), "USDT-FUTURES") + assert.NoError(t, err) + assert.NotEmpty(t, resp) +} + +func subAccTestHelper(t *testing.T, compString, ignoreString string) string { t.Helper() resp, err := bi.GetVirtualSubaccounts(context.Background(), 25, 0, "") assert.NoError(t, err) tarID := "" - compString := strings.ToLower(string(testSubAccountName[:3])) + "****@virtual-bitget.com" - for i := range resp.Data.SubAccountList { - if resp.Data.SubAccountList[i].SubAccountName == compString { - tarID = resp.Data.SubAccountList[i].SubAccountUID + for i := range resp.Data.SubaccountList { + if resp.Data.SubaccountList[i].SubaccountName == compString && + resp.Data.SubaccountList[i].SubaccountName != ignoreString { + tarID = resp.Data.SubaccountList[i].SubaccountUID + break + } + if compString == "" && resp.Data.SubaccountList[i].SubaccountName != ignoreString { + tarID = resp.Data.SubaccountList[i].SubaccountUID break } } if tarID == "" { - t.Skip(skipTestSubAccNotFound) + t.Skipf(skipTestSubAccNotFound, compString, ignoreString) } return tarID } @@ -732,10 +1107,10 @@ func getPlanOrdIDHelper(t *testing.T) OrderIDStruct { "", "ioc", testPrice, testPrice, testAmount) require.NoError(t, err) require.NotEmpty(t, resp2) - ordIDs.ClientOID = resp2.Data.ClientOID + ordIDs.ClientOrderID = resp2.Data.ClientOrderID ordIDs.OrderID = resp2.Data.OrderID } else { - ordIDs.ClientOID = resp.Data.OrderList[0].ClientOrderID + ordIDs.ClientOrderID = resp.Data.OrderList[0].ClientOrderID ordIDs.OrderID = resp.Data.OrderList[0].OrderID } return ordIDs @@ -761,3 +1136,18 @@ func BenchmarkGen(b *testing.B) { aBenchmarkHelper(g, 5e7) } } + +func TestSilly(t *testing.T) { + i := 51 + var f float64 + f = 1 << 511 + log.Print(f) + f = float64(i << i) + log.Print(f) + i = 100 + f = float64(i << i) + log.Print(f) + i = 512 + f = float64(i << i) + log.Print(f) +} diff --git a/exchanges/bitget/bitget_types.go b/exchanges/bitget/bitget_types.go index c7a87a59005..4115681063a 100644 --- a/exchanges/bitget/bitget_types.go +++ b/exchanges/bitget/bitget_types.go @@ -235,11 +235,11 @@ type P2PAdListResp struct { type CrVirSubResp struct { Data struct { FailureList []struct { - SubAccountName string `json:"subaAccountName"` + SubaccountName string `json:"subaAccountName"` } `json:"failureList"` SuccessList []struct { - SubAccountUID string `json:"subAccountUid"` - SubAccountName string `json:"subaAccountName"` + SubaccountUID string `json:"subAccountUid"` + SubaccountName string `json:"subaAccountName"` Status string `json:"status"` PermList []string `json:"permList"` Label string `json:"label"` @@ -263,10 +263,10 @@ type SuccessBoolResp struct { // an API key type CrSubAccAPIKeyResp struct { Data []struct { - SubAccountUID string `json:"subAccountUid"` - SubAccountName string `json:"subAccountName"` + SubaccountUID string `json:"subAccountUid"` + SubaccountName string `json:"subAccountName"` Label string `json:"label"` - SubAccountAPIKey string `json:"subAccountApiKey"` + SubaccountAPIKey string `json:"subAccountApiKey"` SecretKey string `json:"secretKey"` PermList []string `json:"permList"` IPList []string `json:"ipList"` @@ -276,9 +276,9 @@ type CrSubAccAPIKeyResp struct { // GetVirSubResp contains information on the user's virtual sub-accounts type GetVirSubResp struct { Data struct { - SubAccountList []struct { - SubAccountUID string `json:"subAccountUid"` - SubAccountName string `json:"subAccountName"` + SubaccountList []struct { + SubaccountUID string `json:"subAccountUid"` + SubaccountName string `json:"subAccountName"` Label string `json:"label"` Status string `json:"status"` PermList []string `json:"permList"` @@ -292,8 +292,8 @@ type GetVirSubResp struct { // AlterAPIKeyResp contains information returned when creating or modifying an API key type AlterAPIKeyResp struct { Data struct { - SubAccountUID string `json:"subAccountUid"` - SubAccountApiKey string `json:"subAccountApiKey"` + SubaccountUID string `json:"subAccountUid"` + SubaccountApiKey string `json:"subAccountApiKey"` SecretKey string `json:"secretKey"` PermList []string `json:"permList"` Label string `json:"label"` @@ -304,8 +304,8 @@ type AlterAPIKeyResp struct { // GetAPIKeyResp contains information on the user's API keys type GetAPIKeyResp struct { Data []struct { - SubAccountUID string `json:"subAccountUid"` - SubAccountApiKey string `json:"subAccountApiKey"` + SubaccountUID string `json:"subAccountUid"` + SubaccountApiKey string `json:"subAccountApiKey"` IPList []string `json:"ipList"` PermList []string `json:"permList"` Label string `json:"label"` @@ -515,8 +515,8 @@ type CandleResponse struct { Data [][8]interface{} `json:"data"` } -// OneCandle contains a single candle -type OneCandle struct { +// OneSpotCandle contains a single candle +type OneSpotCandle struct { Timestamp time.Time Open float64 High float64 @@ -527,9 +527,21 @@ type OneCandle struct { USDTVolume float64 } +// OneFuturesCandle contains a single candle +type OneFuturesCandle struct { + Timestamp time.Time + Entry float64 + High float64 + Low float64 + Exit float64 + BaseVolume float64 + QuoteVolume float64 +} + // CandleData contains sorted candle data type CandleData struct { - Candles []OneCandle + SpotCandles []OneSpotCandle + FuturesCandles []OneFuturesCandle } // MarketFillsResp contains information on a batch of trades @@ -583,8 +595,8 @@ type BatchOrderResp struct { // OrderIDStruct contains order IDs type OrderIDStruct struct { - OrderID int64 `json:"orderId,string,omitempty"` - ClientOID string `json:"clientOid,omitempty"` + OrderID int64 `json:"orderId,string,omitempty"` + ClientOrderID string `json:"clientOid,omitempty"` } // SymbolResp holds a single symbol @@ -773,8 +785,8 @@ type AccountAssetsResp struct { Data []AssetData `json:"data"` } -// SubAccountAssetsResp contains information on assets in a user's sub-accounts -type SubAccountAssetsResp struct { +// SubaccountAssetsResp contains information on assets in a user's sub-accounts +type SubaccountAssetsResp struct { Data []struct { UserID int64 `json:"userId,string"` AssetsList []AssetData `json:"assetsList"` @@ -785,3 +797,357 @@ type SubAccountAssetsResp struct { type SuccessBoolResp2 struct { Success SuccessBool `json:"data"` } + +// SpotAccBillResp contains information on the user's billing history +type SpotAccBillResp struct { + Data []struct { + CreationTime UnixTimestamp `json:"cTime"` + Coin string `json:"coin"` + GroupType string `json:"groupType"` + BusinessType string `json:"businessType"` + Size float64 `json:"size,string"` + Balance float64 `json:"balance,string"` + Fees float64 `json:"fees,string"` + BillID int64 `json:"billId,string"` + } `json:"data"` +} + +// TransferResp contains information on an asset transfer +type TransferResp struct { + Data struct { + TransferID int64 `json:"transferId,string"` + ClientOrderID string `json:"clientOid"` + } `json:"data"` +} + +// TransferableCoinsResp contains a list of coins that can be transferred between the provided accounts +type TransferableCoinsResp struct { + Data []string `json:"data"` +} + +// SubaccTfrRecResp contains detailed information on asset transfers between sub-accounts +type SubaccTfrRecResp struct { + Data []struct { + Coin string `json:"coin"` + Status string `json:"status"` + ToType string `json:"toType"` + FromType string `json:"fromType"` + Size float64 `json:"size,string"` + Timestamp UnixTimestamp `json:"ts"` + ClientOrderID string `json:"clientOid"` + TransferID int64 `json:"transferId,string"` + FromUserID int64 `json:"fromUserId,string"` + ToUserID int64 `json:"toUserId,string"` + } `json:"data"` +} + +// TransferRecResp contains detailed information on asset transfers +type TransferRecResp struct { + Data []struct { + Coin string `json:"coin"` + Status string `json:"status"` + ToType string `json:"toType"` + ToSymbol string `json:"toSymbol"` + FromType string `json:"fromType"` + FromSymbol string `json:"fromSymbol"` + Size float64 `json:"size,string"` + Timestamp UnixTimestamp `json:"ts"` + ClientOrderID string `json:"clientOid"` + TransferID int64 `json:"transferId,string"` + } `json:"data"` +} + +// DepositAddressResp contains information on a deposit address +type DepositAddressResp struct { + Data struct { + Address string `json:"address"` + Chain string `json:"chain"` + Coin string `json:"coin"` + Tag string `json:"tag"` + URL string `json:"url"` + } `json:"data"` +} + +// SubaccDepRecResp contains detailed information on deposits to sub-accounts +type SubaccDepRecResp struct { + Data []struct { + OrderID int64 `json:"orderId,string"` + TradeID int64 `json:"tradeId,string"` + Coin string `json:"coin"` + Size float64 `json:"size,string"` + Status string `json:"status"` + FromAddress string `json:"fromAddress"` + ToAddress string `json:"toAddress"` + Chain string `json:"chain"` + Destination string `json:"dest"` + CreationTime UnixTimestamp `json:"cTime"` + UpdateTime UnixTimestamp `json:"uTime"` + } `json:"data"` +} + +// WithdrawRecordsResp contains detailed information on withdrawals +type WithdrawRecordsResp struct { + Data []struct { + OrderID int64 `json:"orderId,string"` + TradeID int64 `json:"tradeId,string"` + Coin string `json:"coin"` + ClientOrderID string `json:"clientOid"` + OrderType string `json:"type"` + Destination string `json:"dest"` + Size float64 `json:"size,string"` + Fee float64 `json:"fee,string"` + Status string `json:"status"` + FromAddress string `json:"fromAddress"` + ToAddress string `json:"toAddress"` + Chain string `json:"chain"` + Confirm uint32 `json:"confirm,string"` + Tag string `json:"tag"` + CreationTime UnixTimestamp `json:"cTime"` + UpdateTime UnixTimestamp `json:"uTime"` + } `json:"data"` +} + +// CryptoDepRecResp contains detailed information on cryptocurrency deposits +type CryptoDepRecResp struct { + Data []struct { + OrderID int64 `json:"orderId,string"` + TradeID int64 `json:"tradeId,string"` + Coin string `json:"coin"` + OrderType string `json:"type"` + Size float64 `json:"size,string"` + Status string `json:"status"` + FromAddress string `json:"fromAddress"` + ToAddress string `json:"toAddress"` + Chain string `json:"chain"` + Destination string `json:"dest"` + CreationTime UnixTimestamp `json:"cTime"` + UpdateTime UnixTimestamp `json:"uTime"` + } `json:"data"` +} + +// FutureTickerResp contains information on a futures ticker +type FutureTickerResp struct { + Data []struct { + Symbol string `json:"symbol"` + LastPrice float64 `json:"lastPr,string"` + AskPrice float64 `json:"askPr,string"` + BidPrice float64 `json:"bidPr,string"` + BidSize float64 `json:"bidSz,string"` + AskSize float64 `json:"askSz,string"` + High24H float64 `json:"high24h,string"` + Low24H float64 `json:"low24h,string"` + Timestamp UnixTimestamp `json:"ts"` + Change24H float64 `json:"change24h,string"` + BaseVolume float64 `json:"baseVolume,string"` + QuoteVolume float64 `json:"quoteVolume,string"` + USDTVolume float64 `json:"usdtVolume,string"` + OpenUTC float64 `json:"openUtc,string"` + ChangeUTC24H float64 `json:"changeUtc24h,string"` + IndexPrice float64 `json:"indexPrice,string"` + FundingRate float64 `json:"fundingRate,string"` + HoldingAmount float64 `json:"holdingAmount,string"` + DeliveryStartTime UnixTimestamp `json:"deliveryStartTime"` + DeliveryTime UnixTimestamp `json:"deliveryTime"` + DeliveryStatus string `json:"deliveryStatus"` + Open24H float64 `json:"open24h,string"` + } `json:"data"` +} + +// CallMode represents the call mode for the futures candlestick endpoints +type CallMode uint8 + +const ( + // CallModeNormal represents the normal call mode + CallModeNormal CallMode = iota + // CallModeHistory represents the history call mode + CallModeHistory + // CallModeIndex represents the historical index call mode + CallModeIndex + // CallModeMark represents the historical mark call mode + CallModeMark +) + +// OpenInterestResp contains information on open positions +type OpenPositionsResp struct { + Data struct { + OpenInterestList []struct { + Symbol string `json:"symbol"` + Size float64 `json:"size,string"` + } `json:"openInterestList"` + Timestamp UnixTimestamp `json:"ts"` + } `json:"data"` +} + +// FundingTimeResp contains information on funding times +type FundingTimeResp struct { + Data []struct { + Symbol string `json:"symbol"` + NextFundingTime UnixTimestamp `json:"nextFundingTime"` + RatePeriod uint16 `json:"ratePeriod,string"` + } `json:"data"` +} + +// FuturesPriceResp contains information on futures prices +type FuturesPriceResp struct { + Data []struct { + Symbol string `json:"symbol"` + Price float64 `json:"price,string"` + IndexPrice float64 `json:"indexPrice,string"` + MarkPrice float64 `json:"markPrice,string"` + Timestamp UnixTimestamp `json:"ts"` + } `json:"data"` +} + +// FundingHistoryResp contains information on funding history +type FundingHistoryResp struct { + Data []struct { + Symbol string `json:"symbol"` + FundingRate float64 `json:"fundingRate,string"` + FundingTime UnixTimestamp `json:"fundingTime"` + } `json:"data"` +} + +// FundingCurrentResp contains information on current funding rates +type FundingCurrentResp struct { + Data []struct { + Symbol string `json:"symbol"` + FundingRate float64 `json:"fundingRate,string"` + } `json:"data"` +} + +// ContractConfigResp contains information on contract details +type ContractConfigResp struct { + Data []struct { + Symbol string `json:"symbol"` + BaseCoin string `json:"baseCoin"` + QuoteCoin string `json:"quoteCoin"` + BuyLimitPriceRatio float64 `json:"buyLimitPriceRatio,string"` + SellLimitPriceRatio float64 `json:"sellLimitPriceRatio,string"` + FeeRateUpRatio float64 `json:"feeRateUpRatio,string"` + MakerFeeRate float64 `json:"makerFeeRate,string"` + TakerFeeRate float64 `json:"takerFeeRate,string"` + OpenCostUpRatio float64 `json:"openCostUpRatio,string"` + SupportMarginCoins []string `json:"supportMarginCoins"` + MinTradeNum float64 `json:"minTradeNum,string"` + PriceEndStep float64 `json:"priceEndStep,string"` + VolumePlace float64 `json:"volumePlace,string"` + PricePlace float64 `json:"pricePlace,string"` + SizeMultiplier float64 `json:"sizeMultiplier,string"` + SymbolType string `json:"symbolType"` + MinTradeUSDT float64 `json:"minTradeUSDT,string"` + MaxSymbolOrderNum int64 `json:"maxSymbolOrderNum,string"` + MaxSymbolOpenOrderNum int64 `json:"maxSymbolOpenOrderNum,string"` + MaxPositionNum int64 `json:"maxPositionNum,string"` + SymbolStatus string `json:"symbolStatus"` + OffTime int64 `json:"offTime,string"` + LimitOpenTime int64 `json:"limitOpenTime,string"` + DeliveryTime EmptyInt `json:"deliveryTime"` + DeliveryStartTime EmptyInt `json:"deliveryStartTime"` + DeliveryPeriod EmptyInt `json:"deliveryPeriod"` + LaunchTime EmptyInt `json:"launchTime"` + FundInterval uint16 `json:"fundInterval,string"` + MinLever float64 `json:"minLever,string"` + MaxLever float64 `json:"maxLever,string"` + PosLimit float64 `json:"posLimit,string"` + MaintainTime UnixTimestamp `json:"maintainTime"` + } `json:"data"` +} + +// OneAccResp contains information on a single account +type OneAccResp struct { + Data struct { + MarginCoin string `json:"marginCoin"` + Locked float64 `json:"locked,string"` + Available float64 `json:"available,string"` + CrossedMaxAvailable float64 `json:"crossedMaxAvailable,string"` + IsolatedMaxAvailable float64 `json:"isolatedMaxAvailable,string"` + MaxTransferOut float64 `json:"maxTransferOut,string"` + AccountEquity float64 `json:"accountEquity,string"` + USDTEquity float64 `json:"usdtEquity,string"` + BTCEquity float64 `json:"btcEquity,string"` + CrossedRiskRate float64 `json:"crossedRiskRate,string"` + CrossedMarginleverage float64 `json:"crossedMarginleverage"` + IsolatedLongLever float64 `json:"isolatedLongLever"` + IsolatedShortLever float64 `json:"isolatedShortLever"` + MarginMode string `json:"marginMode"` + PosMode string `json:"posMode"` + UnrealizedPL types.Number `json:"unrealizedPL"` + Coupon types.Number `json:"coupon,string"` + CrossedUnrealizedPL types.Number `json:"crossedUnrealizedPL"` + IsolatedUnrealizedPL types.Number `json:"isolatedUnrealizedPL"` + } `json:"data"` +} + +// FutureAccDetails contains information on a user's futures account +type FutureAccDetails struct { + MarginCoin string `json:"marginCoin"` + Locked float64 `json:"locked,string"` + Available float64 `json:"available,string"` + CrossedMaxAvailable float64 `json:"crossedMaxAvailable,string"` + IsolatedMaxAvailable float64 `json:"isolatedMaxAvailable,string"` + MaxTransferOut float64 `json:"maxTransferOut,string"` + AccountEquity float64 `json:"accountEquity,string"` + USDTEquity float64 `json:"usdtEquity,string"` + BTCEquity float64 `json:"btcEquity,string"` + CrossedRiskRate float64 `json:"crossedRiskRate,string"` + UnrealizedPL types.Number `json:"unrealizedPL"` + Coupon types.Number `json:"coupon"` + CrossedUnrealizedPL types.Number `json:"crossedUnrealizedPL"` + IsolatedUnrealizedPL types.Number `json:"isolatedUnrealizedPL"` +} + +// AllAccResp contains information on all accounts +type AllAccResp struct { + Data []FutureAccDetails `json:"data"` +} + +// SubaccountFuturesResp contains information on futures details of a user's sub-accounts +type SubaccountFuturesResp struct { + Data []struct { + UserID int64 `json:"userId"` + AssetList []FutureAccDetails `json:"assetList"` + } `json:"data"` +} + +// EstOpenCountResp contains information on the estimated size of open orders +type EstOpenCountResp struct { + Data struct { + Size float64 `json:"size,string"` + } `json:"data"` +} + +// LeverageResp contains information on the leverage of a position +type LeverageResp struct { + Data struct { + Symbol string `json:"symbol"` + MarginCoin string `json:"marginCoin"` + LongLeverage float64 `json:"longLeverage,string"` + ShortLeverage float64 `json:"shortLeverage,string"` + CrossMarginLeverage types.Number `json:"crossMarginLeverage"` + MarginMode string `json:"marginMode"` + } `json:"data"` +} + +// PosModeResp contains information on the position mode +type PosModeResp struct { + Data struct { + PositionMode string `json:"posMode"` + } `json:"data"` +} + +// FutureAccBillResp contains information on futures billing history +type FutureAccBillResp struct { + Data struct { + Bills []struct { + OrderID int64 `json:"orderId,string"` + Symbol string `json:"symbol"` + Amount float64 `json:"amount,string"` + Fee float64 `json:"fee,string"` + FeeByCoupon types.Number `json:"feeByCoupon"` + FeeCoin string `json:"feeCoin"` + BusinessType string `json:"businessType"` + Coin string `json:"coin"` + CreateTime UnixTimestamp `json:"cTime"` + } `json:"bills"` + } `json:"data"` +} diff --git a/types/number.go b/types/number.go index 452760fe760..b81eb42ae09 100644 --- a/types/number.go +++ b/types/number.go @@ -16,8 +16,11 @@ type Number float64 // UnmarshalJSON implements json.Unmarshaler func (f *Number) UnmarshalJSON(data []byte) error { switch c := data[0]; c { // From json.decode literalInterface - case 'n', 't', 'f': // null, true, false + case 't', 'f': // true, false return fmt.Errorf("%w: %s", errInvalidNumberValue, data) + case 'n': // null + *f = Number(0) // Set to 0 value + return nil case '"': // string if len(data) < 2 || data[len(data)-1] != '"' { return fmt.Errorf("%w: %s", errInvalidNumberValue, data) diff --git a/types/number_test.go b/types/number_test.go index aba61bf0e76..9e6c891d77d 100644 --- a/types/number_test.go +++ b/types/number_test.go @@ -22,12 +22,16 @@ func TestNumberUnmarshalJSON(t *testing.T) { assert.NoError(t, err, "Unmarshal should not error") assert.Zero(t, n.Float64(), "UnmarshalJSON should parse empty as 0") + err = n.UnmarshalJSON([]byte(`null`)) + assert.NoError(t, err, "Unmarshal should not error on null") + assert.Zero(t, n.Float64(), "UnmarshalJSON should parse null as 0") + err = n.UnmarshalJSON([]byte(`1337.37`)) assert.NoError(t, err, "Unmarshal should not error on number types") assert.Equal(t, 1337.37, n.Float64(), "UnmarshalJSON should handle raw numerics") // Invalid value checking - for _, i := range []string{`"MEOW"`, `null`, `false`, `true`, `"1337.37`} { + for _, i := range []string{`"MEOW"`, `false`, `true`, `"1337.37`} { err = n.UnmarshalJSON([]byte(i)) assert.ErrorIsf(t, err, errInvalidNumberValue, "UnmarshalJSON should error with invalid Value for `%s`", i) }