mirror of
https://github.com/d0zingcat/gocryptotrader.git
synced 2026-05-31 23:16:54 +00:00
FTX: Add REST subaccount support (#653)
* FTX: Add REST API subaccount support * Add API key check to GetSubaccounts * Fix missing comment and expand to rest of the codebase * Address glorious nits * Address various nits * Fix ZB typo https://www.zb.com/api#hsptccieyyqomlp
This commit is contained in:
@@ -15,6 +15,7 @@ import (
|
||||
|
||||
"github.com/thrasher-corp/gocryptotrader/common"
|
||||
"github.com/thrasher-corp/gocryptotrader/common/crypto"
|
||||
"github.com/thrasher-corp/gocryptotrader/currency"
|
||||
exchange "github.com/thrasher-corp/gocryptotrader/exchanges"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/request"
|
||||
@@ -90,6 +91,10 @@ const (
|
||||
requestOTCQuote = "/otc/quotes"
|
||||
getOTCQuoteStatus = "/otc/quotes/"
|
||||
acceptOTCQuote = "/otc/quotes/%s/accept"
|
||||
subaccounts = "/subaccounts"
|
||||
subaccountsUpdateName = "/subaccounts/update_name"
|
||||
subaccountsBalance = "/subaccounts/%s/balances"
|
||||
subaccountsTransfer = "/subaccounts/transfer"
|
||||
|
||||
// Margin Endpoints
|
||||
marginBorrowRates = "/spot_margin/borrow_rates"
|
||||
@@ -114,7 +119,12 @@ const (
|
||||
)
|
||||
|
||||
var (
|
||||
errStartTimeCannotBeAfterEndTime = errors.New("start timestamp cannot be after end timestamp")
|
||||
errStartTimeCannotBeAfterEndTime = errors.New("start timestamp cannot be after end timestamp")
|
||||
errSubaccountNameMustBeSpecified = errors.New("a subaccount name must be specified")
|
||||
errSubaccountUpdateNameInvalid = errors.New("invalid subaccount old/new name")
|
||||
errCoinMustBeSpecified = errors.New("a coin must be specified")
|
||||
errSubaccountTransferSizeGreaterThanZero = errors.New("transfer size must be greater than 0")
|
||||
errSubaccountTransferSourceDestinationMustNotBeEqual = errors.New("subaccount transfer source and destination must not be the same value")
|
||||
)
|
||||
|
||||
// GetMarkets gets market data
|
||||
@@ -403,17 +413,17 @@ func (f *FTX) GetCoins() ([]WalletCoinsData, error) {
|
||||
}
|
||||
|
||||
// GetBalances gets balances of the account
|
||||
func (f *FTX) GetBalances() ([]BalancesData, error) {
|
||||
func (f *FTX) GetBalances() ([]WalletBalance, error) {
|
||||
resp := struct {
|
||||
Data []BalancesData `json:"result"`
|
||||
Data []WalletBalance `json:"result"`
|
||||
}{}
|
||||
return resp.Data, f.SendAuthHTTPRequest(exchange.RestSpot, http.MethodGet, getBalances, nil, &resp)
|
||||
}
|
||||
|
||||
// GetAllWalletBalances gets all wallets' balances
|
||||
func (f *FTX) GetAllWalletBalances() (AllWalletAccountData, error) {
|
||||
func (f *FTX) GetAllWalletBalances() (AllWalletBalances, error) {
|
||||
resp := struct {
|
||||
Data AllWalletAccountData `json:"result"`
|
||||
Data AllWalletBalances `json:"result"`
|
||||
}{}
|
||||
return resp.Data, f.SendAuthHTTPRequest(exchange.RestSpot, http.MethodGet, getAllWalletBalances, nil, &resp)
|
||||
}
|
||||
@@ -1105,3 +1115,104 @@ func (f *FTX) GetOTCQuoteStatus(marketName, quoteID string) ([]QuoteStatusData,
|
||||
func (f *FTX) AcceptOTCQuote(quoteID string) error {
|
||||
return f.SendAuthHTTPRequest(exchange.RestSpot, http.MethodPost, fmt.Sprintf(acceptOTCQuote, quoteID), nil, nil)
|
||||
}
|
||||
|
||||
// GetSubaccounts returns the users subaccounts
|
||||
func (f *FTX) GetSubaccounts() ([]Subaccount, error) {
|
||||
resp := struct {
|
||||
Data []Subaccount `json:"result"`
|
||||
}{}
|
||||
return resp.Data, f.SendAuthHTTPRequest(exchange.RestSpot, http.MethodGet, subaccounts, nil, &resp)
|
||||
}
|
||||
|
||||
// CreateSubaccount creates a new subaccount
|
||||
func (f *FTX) CreateSubaccount(name string) (*Subaccount, error) {
|
||||
if name == "" {
|
||||
return nil, errSubaccountNameMustBeSpecified
|
||||
}
|
||||
d := make(map[string]string)
|
||||
d["nickname"] = name
|
||||
|
||||
resp := struct {
|
||||
Data Subaccount `json:"result"`
|
||||
}{}
|
||||
if err := f.SendAuthHTTPRequest(exchange.RestSpot, http.MethodPost, subaccounts, d, &resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &resp.Data, nil
|
||||
}
|
||||
|
||||
// UpdateSubaccountName updates an existing subaccount name
|
||||
func (f *FTX) UpdateSubaccountName(oldName, newName string) (*Subaccount, error) {
|
||||
if oldName == "" || newName == "" || oldName == newName {
|
||||
return nil, errSubaccountUpdateNameInvalid
|
||||
}
|
||||
d := make(map[string]string)
|
||||
d["nickname"] = oldName
|
||||
d["newNickname"] = newName
|
||||
|
||||
resp := struct {
|
||||
Data Subaccount `json:"result"`
|
||||
}{}
|
||||
if err := f.SendAuthHTTPRequest(exchange.RestSpot, http.MethodPost, subaccountsUpdateName, d, &resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &resp.Data, nil
|
||||
}
|
||||
|
||||
// DeleteSubaccountName deletes the specified subaccount name
|
||||
func (f *FTX) DeleteSubaccount(name string) error {
|
||||
if name == "" {
|
||||
return errSubaccountNameMustBeSpecified
|
||||
}
|
||||
d := make(map[string]string)
|
||||
d["nickname"] = name
|
||||
resp := struct {
|
||||
Data Subaccount `json:"result"`
|
||||
}{}
|
||||
return f.SendAuthHTTPRequest(exchange.RestSpot, http.MethodDelete, subaccounts, d, &resp)
|
||||
}
|
||||
|
||||
// SubaccountBalances returns the user's subaccount balances
|
||||
func (f *FTX) SubaccountBalances(name string) ([]SubaccountBalance, error) {
|
||||
if name == "" {
|
||||
return nil, errSubaccountNameMustBeSpecified
|
||||
}
|
||||
resp := struct {
|
||||
Data []SubaccountBalance `json:"result"`
|
||||
}{}
|
||||
if err := f.SendAuthHTTPRequest(exchange.RestSpot, http.MethodGet, fmt.Sprintf(subaccountsBalance, name), nil, &resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp.Data, nil
|
||||
}
|
||||
|
||||
// SubaccountTransfer transfers a desired coin to the specified subaccount
|
||||
func (f *FTX) SubaccountTransfer(coin currency.Code, source, destination string, size float64) (*SubaccountTransferStatus, error) {
|
||||
if coin.IsEmpty() {
|
||||
return nil, errCoinMustBeSpecified
|
||||
}
|
||||
if size <= 0 {
|
||||
return nil, errSubaccountTransferSizeGreaterThanZero
|
||||
}
|
||||
if source == destination {
|
||||
return nil, errSubaccountTransferSourceDestinationMustNotBeEqual
|
||||
}
|
||||
d := make(map[string]interface{})
|
||||
d["coin"] = coin.Upper().String()
|
||||
d["size"] = size
|
||||
if source == "" {
|
||||
source = "main"
|
||||
}
|
||||
d["source"] = source
|
||||
if destination == "" {
|
||||
destination = "main"
|
||||
}
|
||||
d["destination"] = destination
|
||||
resp := struct {
|
||||
Data SubaccountTransferStatus `json:"result"`
|
||||
}{}
|
||||
if err := f.SendAuthHTTPRequest(exchange.RestSpot, http.MethodPost, subaccountsTransfer, d, &resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &resp.Data, nil
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package ftx
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log"
|
||||
"os"
|
||||
"reflect"
|
||||
@@ -1450,3 +1451,126 @@ func TestParsingWSOBData2(t *testing.T) {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetSubaccounts(t *testing.T) {
|
||||
t.Parallel()
|
||||
if !areTestAPIKeysSet() {
|
||||
t.Skip("skipping test, api keys not set")
|
||||
}
|
||||
_, err := f.GetSubaccounts()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateSubaccount(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, err := f.CreateSubaccount("")
|
||||
if !errors.Is(err, errSubaccountNameMustBeSpecified) {
|
||||
t.Errorf("expected %v, but received: %s", errSubaccountNameMustBeSpecified, err)
|
||||
}
|
||||
|
||||
if !areTestAPIKeysSet() || !canManipulateRealOrders {
|
||||
t.Skip("skipping test, either api keys or canManipulateRealOrders isn't set")
|
||||
}
|
||||
_, err = f.CreateSubaccount("subzero")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err = f.DeleteSubaccount("subzero"); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateSubaccountName(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, err := f.UpdateSubaccountName("", "")
|
||||
if !errors.Is(err, errSubaccountUpdateNameInvalid) {
|
||||
t.Errorf("expected %v, but received: %s", errSubaccountUpdateNameInvalid, err)
|
||||
}
|
||||
|
||||
if !areTestAPIKeysSet() || !canManipulateRealOrders {
|
||||
t.Skip("skipping test, either api keys or canManipulateRealOrders isn't set")
|
||||
}
|
||||
_, err = f.CreateSubaccount("subzero")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = f.UpdateSubaccountName("subzero", "bizzlebot")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := f.DeleteSubaccount("bizzlebot"); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteSubaccountName(t *testing.T) {
|
||||
t.Parallel()
|
||||
if err := f.DeleteSubaccount(""); !errors.Is(err, errSubaccountNameMustBeSpecified) {
|
||||
t.Errorf("expected %v, but received: %s", errSubaccountNameMustBeSpecified, err)
|
||||
}
|
||||
if !areTestAPIKeysSet() || !canManipulateRealOrders {
|
||||
t.Skip("skipping test, either api keys or canManipulateRealOrders isn't set")
|
||||
}
|
||||
_, err := f.CreateSubaccount("subzero")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := f.DeleteSubaccount("subzero"); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubaccountBalances(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, err := f.SubaccountBalances("")
|
||||
if !errors.Is(err, errSubaccountNameMustBeSpecified) {
|
||||
t.Errorf("expected %s, but received: %s", errSubaccountNameMustBeSpecified, err)
|
||||
}
|
||||
if !areTestAPIKeysSet() {
|
||||
t.Skip("skipping test, api keys not set")
|
||||
}
|
||||
_, err = f.SubaccountBalances("non-existent")
|
||||
if err == nil {
|
||||
t.Error("expecting non-existent subaccount to return an error")
|
||||
}
|
||||
_, err = f.CreateSubaccount("subzero")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = f.SubaccountBalances("subzero")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if err := f.DeleteSubaccount("subzero"); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubaccountTransfer(t *testing.T) {
|
||||
tt := []struct {
|
||||
Coin currency.Code
|
||||
Source string
|
||||
Destination string
|
||||
Size float64
|
||||
ErrExpected error
|
||||
}{
|
||||
{ErrExpected: errCoinMustBeSpecified},
|
||||
{Coin: currency.BTC, ErrExpected: errSubaccountTransferSizeGreaterThanZero},
|
||||
{Coin: currency.BTC, Size: 420, ErrExpected: errSubaccountTransferSourceDestinationMustNotBeEqual},
|
||||
}
|
||||
for x := range tt {
|
||||
_, err := f.SubaccountTransfer(tt[x].Coin, tt[x].Source, tt[x].Destination, tt[x].Size)
|
||||
if !errors.Is(err, tt[x].ErrExpected) {
|
||||
t.Errorf("expected %s, but received: %s", tt[x].ErrExpected, err)
|
||||
}
|
||||
}
|
||||
if !areTestAPIKeysSet() || !canManipulateRealOrders {
|
||||
t.Skip("skipping test, either api keys or canManipulateRealOrders isn't set")
|
||||
}
|
||||
_, err := f.SubaccountTransfer(currency.BTC, "", "test", 0.1)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -234,18 +234,18 @@ type WalletCoinsData struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// BalancesData stores balances data
|
||||
type BalancesData struct {
|
||||
Coin string `json:"coin"`
|
||||
Free float64 `json:"free"`
|
||||
Total float64 `json:"total"`
|
||||
// WalletBalance stores balances data
|
||||
type WalletBalance struct {
|
||||
Coin string `json:"coin"`
|
||||
Free float64 `json:"free"`
|
||||
Total float64 `json:"total"`
|
||||
AvailableWithoutBorrow float64 `json:"availableWithoutBorrow"`
|
||||
USDValue float64 `json:"usdValue"`
|
||||
SpotBorrow float64 `json:"spotBorrow"`
|
||||
}
|
||||
|
||||
// AllWalletAccountData stores account data on all WalletCoins
|
||||
type AllWalletAccountData struct {
|
||||
Main []BalancesData `json:"main"`
|
||||
BattleRoyale []BalancesData `json:"Battle Royale"`
|
||||
}
|
||||
// AllWalletBalances stores all the user's account balances
|
||||
type AllWalletBalances map[string][]WalletBalance
|
||||
|
||||
// DepositData stores deposit address data
|
||||
type DepositData struct {
|
||||
@@ -771,3 +771,31 @@ type QuoteStatusData struct {
|
||||
type AcceptQuote struct {
|
||||
Success bool `json:"success"`
|
||||
}
|
||||
|
||||
// Subaccount stores subaccount data
|
||||
type Subaccount struct {
|
||||
Nickname string `json:"nickname"`
|
||||
Special bool `json:"special"`
|
||||
Deletable bool `json:"deletable"`
|
||||
Editable bool `json:"editable"`
|
||||
Competition bool `json:"competition"`
|
||||
}
|
||||
|
||||
// SubaccountBalance stores the user's subaccount balance
|
||||
type SubaccountBalance struct {
|
||||
Coin string `json:"coin"`
|
||||
Free float64 `json:"free"`
|
||||
Total float64 `json:"total"`
|
||||
SpotBorrow float64 `json:"spotBorrow"`
|
||||
AvailableWithoutBorrow float64 `json:"availableWithoutBorrow"`
|
||||
}
|
||||
|
||||
// SubaccountTransferStatus stores the subaccount transfer details
|
||||
type SubaccountTransferStatus struct {
|
||||
ID int64 `json:"id"`
|
||||
Coin string `json:"coin"`
|
||||
Size float64 `json:"size"`
|
||||
Time time.Time `json:"time"`
|
||||
Notes string `json:"notes"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user