From 1e5739dffa4f560fd6ca03c5721877b2cb15a79c Mon Sep 17 00:00:00 2001 From: Adrian Gallagher Date: Tue, 27 May 2025 19:03:09 +1000 Subject: [PATCH] LBank: Fix TestGetWithdrawConfig, assertify tests and other minor changes (#1920) * LBank: Fix TestGetWithdrawConfig, assertify tests and other minor changes * refactor: Update pair var and other tweaks * refactor: Increase test coverage for crypto funcs and minor adjustments * refactor: Replace assert with require for error checks in TestCreateOrder * Update exchanges/lbank/lbank.go Co-authored-by: Ryan O'Hara-Reid * refactor: Correct spelling of HighestPrice in KlineResponse and increase test coverage * refactor: Update error handling in CreateOrder and improve GetTrades comment --------- Co-authored-by: Ryan O'Hara-Reid --- exchanges/lbank/lbank.go | 57 ++-- exchanges/lbank/lbank_test.go | 530 ++++++++++++------------------- exchanges/lbank/lbank_types.go | 28 +- exchanges/lbank/lbank_wrapper.go | 104 +++--- testdata/configtest.json | 2 +- 5 files changed, 293 insertions(+), 428 deletions(-) diff --git a/exchanges/lbank/lbank.go b/exchanges/lbank/lbank.go index a2b7a7f4..28b6f4cf 100644 --- a/exchanges/lbank/lbank.go +++ b/exchanges/lbank/lbank.go @@ -16,7 +16,9 @@ import ( "strings" "time" + "github.com/thrasher-corp/gocryptotrader/common" gctcrypto "github.com/thrasher-corp/gocryptotrader/common/crypto" + "github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/encoding/json" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" "github.com/thrasher-corp/gocryptotrader/exchanges/order" @@ -61,10 +63,16 @@ const ( lbankTimestamp = "timestamp.do" ) +var ( + errPEMBlockIsNil = errors.New("pem block is nil") + errUnableToParsePrivateKey = errors.New("unable to parse private key") + errPrivateKeyNotLoaded = errors.New("private key not loaded") +) + // GetTicker returns a ticker for the specified symbol // symbol: eth_btc -func (l *Lbank) GetTicker(ctx context.Context, symbol string) (TickerResponse, error) { - var t TickerResponse +func (l *Lbank) GetTicker(ctx context.Context, symbol string) (*TickerResponse, error) { + var t *TickerResponse params := url.Values{} params.Set("symbol", symbol) path := fmt.Sprintf("/v%s/%s?%s", lbankAPIVersion1, lbankTicker, params.Encode()) @@ -79,7 +87,7 @@ func (l *Lbank) GetTimestamp(ctx context.Context) (time.Time, error) { if err != nil { return time.Time{}, err } - return time.UnixMilli(resp.Timestamp), nil + return resp.Timestamp.Time(), nil } // GetTickers returns all tickers @@ -100,39 +108,39 @@ func (l *Lbank) GetCurrencyPairs(ctx context.Context) ([]string, error) { } // GetMarketDepths returns arrays of asks, bids and timestamp -func (l *Lbank) GetMarketDepths(ctx context.Context, symbol, size, merge string) (*MarketDepthResponse, error) { +func (l *Lbank) GetMarketDepths(ctx context.Context, symbol string, size uint64) (*MarketDepthResponse, error) { var m MarketDepthResponse params := url.Values{} params.Set("symbol", symbol) - params.Set("size", size) - params.Set("merge", merge) + params.Set("size", strconv.FormatUint(size, 10)) path := fmt.Sprintf("/v%s/%s?%s", lbankAPIVersion2, lbankMarketDepths, params.Encode()) return &m, l.SendHTTPRequest(ctx, exchange.RestSpot, path, &m) } // GetTrades returns an array of available trades regarding a particular exchange -func (l *Lbank) GetTrades(ctx context.Context, symbol string, limit, time int64) ([]TradeResponse, error) { +// The time parameter is optional, if provided it will return trades after the given time +func (l *Lbank) GetTrades(ctx context.Context, symbol string, limit uint64, tm time.Time) ([]TradeResponse, error) { var g []TradeResponse params := url.Values{} params.Set("symbol", symbol) if limit > 0 { - params.Set("size", strconv.FormatInt(limit, 10)) + params.Set("size", strconv.FormatUint(limit, 10)) } - if time > 0 { - params.Set("time", strconv.FormatInt(time, 10)) + if !tm.IsZero() { + params.Set("time", strconv.FormatInt(tm.Unix(), 10)) } path := fmt.Sprintf("/v%s/%s?%s", lbankAPIVersion1, lbankTrades, params.Encode()) return g, l.SendHTTPRequest(ctx, exchange.RestSpot, path, &g) } // GetKlines returns kline data -func (l *Lbank) GetKlines(ctx context.Context, symbol, size, klineType, tm string) ([]KlineResponse, error) { +func (l *Lbank) GetKlines(ctx context.Context, symbol, size, klineType string, tm time.Time) ([]KlineResponse, error) { var klineTemp [][]float64 params := url.Values{} params.Set("symbol", symbol) params.Set("size", size) params.Set("type", klineType) - params.Set("time", tm) + params.Set("time", strconv.FormatInt(tm.Unix(), 10)) path := fmt.Sprintf("/v%s/%s?%s", lbankAPIVersion1, lbankKlines, params.Encode()) err := l.SendHTTPRequest(ctx, exchange.RestSpot, path, &klineTemp) if err != nil { @@ -147,7 +155,7 @@ func (l *Lbank) GetKlines(ctx context.Context, symbol, size, klineType, tm strin k[x] = KlineResponse{ TimeStamp: time.Unix(int64(klineTemp[x][0]), 0).UTC(), OpenPrice: klineTemp[x][1], - HigestPrice: klineTemp[x][2], + HighestPrice: klineTemp[x][2], LowestPrice: klineTemp[x][3], ClosePrice: klineTemp[x][4], TradingVolume: klineTemp[x][5], @@ -175,18 +183,17 @@ func (l *Lbank) GetUserInfo(ctx context.Context) (InfoFinalResponse, error) { // CreateOrder creates an order func (l *Lbank) CreateOrder(ctx context.Context, pair, side string, amount, price float64) (CreateOrderResponse, error) { var resp CreateOrderResponse - if !strings.EqualFold(side, order.Buy.String()) && - !strings.EqualFold(side, order.Sell.String()) { - return resp, errors.New("side type invalid can only be 'buy' or 'sell'") + if !strings.EqualFold(side, order.Buy.String()) && !strings.EqualFold(side, order.Sell.String()) { + return resp, order.ErrSideIsInvalid } if amount <= 0 { - return resp, errors.New("amount can't be smaller than or equal to 0") + return resp, order.ErrAmountBelowMin } if price <= 0 { - return resp, errors.New("price can't be smaller than or equal to 0") + return resp, order.ErrPriceBelowMin } - params := url.Values{} + params := url.Values{} params.Set("symbol", pair) params.Set("type", strings.ToLower(side)) params.Set("price", strconv.FormatFloat(price, 'f', -1, 64)) @@ -384,10 +391,10 @@ func (l *Lbank) USD2RMBRate(ctx context.Context) (ExchangeRateResponse, error) { } // GetWithdrawConfig gets information about withdrawals -func (l *Lbank) GetWithdrawConfig(ctx context.Context, assetCode string) ([]WithdrawConfigResponse, error) { +func (l *Lbank) GetWithdrawConfig(ctx context.Context, c currency.Code) ([]WithdrawConfigResponse, error) { var resp []WithdrawConfigResponse params := url.Values{} - params.Set("assetCode", assetCode) + params.Set("assetCode", c.Lower().String()) path := fmt.Sprintf("/v%s/%s?%s", lbankAPIVersion1, lbankWithdrawConfig, params.Encode()) return resp, l.SendHTTPRequest(ctx, exchange.RestSpot, path, &resp) } @@ -506,25 +513,25 @@ func (l *Lbank) loadPrivKey(ctx context.Context) error { block, _ := pem.Decode([]byte(key)) if block == nil { - return errors.New("pem block is nil") + return errPEMBlockIsNil } p, err := x509.ParsePKCS8PrivateKey(block.Bytes) if err != nil { - return fmt.Errorf("unable to decode priv key: %s", err) + return fmt.Errorf("%w: %w", errUnableToParsePrivateKey, err) } var ok bool l.privateKey, ok = p.(*rsa.PrivateKey) if !ok { - return errors.New("unable to parse RSA private key") + return common.GetTypeAssertError("*rsa.PrivateKey", p) } return nil } func (l *Lbank) sign(data string) (string, error) { if l.privateKey == nil { - return "", errors.New("private key not loaded") + return "", errPrivateKeyNotLoaded } md5hash, err := gctcrypto.GetMD5([]byte(data)) if err != nil { diff --git a/exchanges/lbank/lbank_test.go b/exchanges/lbank/lbank_test.go index acbac33f..25108eb4 100644 --- a/exchanges/lbank/lbank_test.go +++ b/exchanges/lbank/lbank_test.go @@ -1,16 +1,24 @@ package lbank import ( - "context" + "crypto" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/base64" + "encoding/hex" "log" "os" - "strconv" + "strings" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/thrasher-corp/gocryptotrader/config" + "github.com/thrasher-corp/gocryptotrader/common" + gctcrypto "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/account" @@ -23,120 +31,84 @@ import ( // Please supply your own keys here for due diligence testing const ( - testAPIKey = "" - testAPISecret = "" + apiKey = "" + apiSecret = "" canManipulateRealOrders = false - testCurrencyPair = "btc_usdt" ) -var l = &Lbank{} +var ( + l = &Lbank{} + testPair = currency.NewBTCUSDT().Format(currency.PairFormat{Delimiter: "_"}) +) func TestMain(m *testing.M) { - l.SetDefaults() - cfg := config.GetConfig() - err := cfg.LoadConfig("../../testdata/configtest.json", true) - if err != nil { + l = new(Lbank) + if err := testexch.Setup(l); err != nil { log.Fatal(err) } - lbankConfig, err := cfg.GetExchangeConfig("Lbank") - if err != nil { - log.Fatal(err) - } - lbankConfig.API.AuthenticatedSupport = true - lbankConfig.API.Credentials.Key = testAPIKey - lbankConfig.API.Credentials.Secret = testAPISecret - err = l.Setup(lbankConfig) - if err != nil { - log.Fatal(err) - } - err = l.UpdateTradablePairs(context.Background(), true) - if err != nil { - log.Fatal(err) - } - cp, err := currency.NewPairFromString(testCurrencyPair) - if err != nil { - log.Fatal(err) - } - err = l.CurrencyPairs.EnablePair(asset.Spot, cp) - if err != nil { - log.Fatal(err) + if apiKey != "" && apiSecret != "" { + l.API.AuthenticatedSupport = true + l.SetCredentials(apiKey, apiSecret, "", "", "", "") } os.Exit(m.Run()) } func TestGetTicker(t *testing.T) { t.Parallel() - _, err := l.GetTicker(t.Context(), testCurrencyPair) - if err != nil { - t.Error(err) - } + _, err := l.GetTicker(t.Context(), testPair.String()) + assert.NoError(t, err, "GetTicker should not error") +} + +func TestGetTimestamp(t *testing.T) { + t.Parallel() + ts, err := l.GetTimestamp(t.Context()) + require.NoError(t, err, "GetTimestamp must not error") + assert.NotZero(t, ts, "GetTimestamp should return a non-zero time") } func TestGetTickers(t *testing.T) { t.Parallel() tickers, err := l.GetTickers(t.Context()) - if err != nil { - t.Fatal(err) - } - if len(tickers) <= 1 { - t.Errorf("expected multiple tickers, received %v", len(tickers)) - } + require.NoError(t, err, "GetTickers must not error") + assert.Greater(t, len(tickers), 1, "GetTickers should return more than 1 ticker") } func TestGetCurrencyPairs(t *testing.T) { t.Parallel() _, err := l.GetCurrencyPairs(t.Context()) - if err != nil { - t.Error(err) - } + assert.NoError(t, err, "GetCurrencyPairs should not error") } func TestGetMarketDepths(t *testing.T) { t.Parallel() - _, err := l.GetMarketDepths(t.Context(), testCurrencyPair, "600", "1") - if err != nil { - t.Fatal(err) - } - a, _ := l.GetMarketDepths(t.Context(), testCurrencyPair, "4", "0") - if len(a.Data.Asks) != 4 { - t.Errorf("asks length requested doesn't match the output") - } + d, err := l.GetMarketDepths(t.Context(), testPair.String(), 4) + require.NoError(t, err, "GetMarketDepths must not error") + require.NotEmpty(t, d, "GetMarketDepths must return a non-empty response") + assert.Len(t, d.Data.Asks, 4, "GetMarketDepths should return 4 asks") } func TestGetTrades(t *testing.T) { t.Parallel() - _, err := l.GetTrades(t.Context(), testCurrencyPair, 600, time.Now().Unix()) - if err != nil { - t.Error(err) - } - a, err := l.GetTrades(t.Context(), testCurrencyPair, 600, 0) - if len(a) != 600 && err != nil { - t.Error(err) - } + r, err := l.GetTrades(t.Context(), testPair.String(), 420, time.Now()) + require.NoError(t, err, "GetTrades must not error") + require.NotEmpty(t, r, "GetTrades must return a non-empty response") + assert.Len(t, r, 420, "GetTrades should return 420 trades") } func TestGetKlines(t *testing.T) { t.Parallel() - _, err := l.GetKlines(t.Context(), - testCurrencyPair, "600", "minute1", - strconv.FormatInt(time.Now().Unix(), 10)) - if err != nil { - t.Error(err) - } + _, err := l.GetKlines(t.Context(), testPair.String(), "600", "minute1", time.Now()) + assert.NoError(t, err, "GetKlines should not error") } func TestUpdateOrderbook(t *testing.T) { t.Parallel() - p := currency.Pair{ - Delimiter: "_", - Base: currency.ETH, - Quote: currency.BTC, - } - - _, err := l.UpdateOrderbook(t.Context(), p.Lower(), asset.Spot) - if err != nil { - t.Error(err) - } + _, err := l.UpdateOrderbook(t.Context(), currency.EMPTYPAIR, asset.Spot) + assert.ErrorIs(t, err, currency.ErrCurrencyPairEmpty) + _, err = l.UpdateOrderbook(t.Context(), testPair, asset.Options) + assert.ErrorIs(t, err, asset.ErrNotSupported) + _, err = l.UpdateOrderbook(t.Context(), testPair, asset.Spot) + assert.NoError(t, err, "UpdateOrderbook should not error") } func TestGetUserInfo(t *testing.T) { @@ -144,125 +116,90 @@ func TestGetUserInfo(t *testing.T) { sharedtestvalues.SkipTestIfCredentialsUnset(t, l) _, err := l.GetUserInfo(t.Context()) - if err != nil { - t.Error(err) - } + require.NoError(t, err, "GetUserInfo must not error") } func TestCreateOrder(t *testing.T) { t.Parallel() + + _, err := l.CreateOrder(t.Context(), testPair.String(), "what", 1231, 12314) + require.ErrorIs(t, err, order.ErrSideIsInvalid) + _, err = l.CreateOrder(t.Context(), testPair.String(), order.Buy.String(), 0, 0) + require.ErrorIs(t, err, order.ErrAmountBelowMin) + _, err = l.CreateOrder(t.Context(), testPair.String(), order.Sell.String(), 1231, 0) + require.ErrorIs(t, err, order.ErrPriceBelowMin) + sharedtestvalues.SkipTestIfCredentialsUnset(t, l, canManipulateRealOrders) - cp := currency.NewPairWithDelimiter(currency.BTC.String(), currency.USDT.String(), "_") - _, err := l.CreateOrder(t.Context(), cp.Lower().String(), "what", 1231, 12314) - if err == nil { - t.Error("CreateOrder error cannot be nil") - } - _, err = l.CreateOrder(t.Context(), cp.Lower().String(), order.Buy.Lower(), 0, 0) - if err == nil { - t.Error("CreateOrder error cannot be nil") - } - _, err = l.CreateOrder(t.Context(), cp.Lower().String(), order.Sell.Lower(), 1231, 0) - if err == nil { - t.Error("CreateOrder error cannot be nil") - } - _, err = l.CreateOrder(t.Context(), cp.Lower().String(), order.Buy.Lower(), 58, 681) - if err != nil { - t.Errorf("Unexpected error: %v", err) - } + _, err = l.CreateOrder(t.Context(), testPair.String(), order.Buy.String(), 58, 681) + assert.NoError(t, err, "CreateOrder should not error") } func TestRemoveOrder(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, l, canManipulateRealOrders) - cp := currency.NewPairWithDelimiter(currency.ETH.String(), currency.BTC.String(), "_") - _, err := l.RemoveOrder(t.Context(), - cp.Lower().String(), "24f7ce27-af1d-4dca-a8c1-ef1cbeec1b23") - if err != nil { - t.Error(err) - } + _, err := l.RemoveOrder(t.Context(), testPair.String(), "24f7ce27-af1d-4dca-a8c1-ef1cbeec1b23") + assert.NoError(t, err, "RemoveOrder should not error") } func TestQueryOrder(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, l) - cp := currency.NewPairWithDelimiter(currency.BTC.String(), currency.USDT.String(), "_") - _, err := l.QueryOrder(t.Context(), cp.Lower().String(), "1") - if err != nil { - t.Error(err) - } + _, err := l.QueryOrder(t.Context(), testPair.String(), "1") + assert.NoError(t, err, "QueryOrder should not error") } func TestQueryOrderHistory(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, l) - cp := currency.NewPairWithDelimiter(currency.BTC.String(), currency.USDT.String(), "_") - _, err := l.QueryOrderHistory(t.Context(), - cp.Lower().String(), "1", "100") - if err != nil { - t.Error(err) - } + _, err := l.QueryOrderHistory(t.Context(), testPair.String(), "1", "100") + assert.NoError(t, err, "QueryOrderHistory should not error") } func TestGetPairInfo(t *testing.T) { t.Parallel() _, err := l.GetPairInfo(t.Context()) - if err != nil { - t.Error(err) - } + assert.NoError(t, err, "GetPairInfo should not error") } func TestOrderTransactionDetails(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, l) - _, err := l.OrderTransactionDetails(t.Context(), - testCurrencyPair, "24f7ce27-af1d-4dca-a8c1-ef1cbeec1b23") - if err != nil { - t.Error(err) - } + _, err := l.OrderTransactionDetails(t.Context(), testPair.String(), "24f7ce27-af1d-4dca-a8c1-ef1cbeec1b23") + assert.NoError(t, err, "OrderTransactionDetails should not error") } func TestTransactionHistory(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, l) - _, err := l.TransactionHistory(t.Context(), - testCurrencyPair, "", "", "", "", "", "") - if err != nil { - t.Error(err) - } + _, err := l.TransactionHistory(t.Context(), testPair.String(), "", "", "", "", "", "") + assert.NoError(t, err, "TransactionHistory should not error") } func TestGetOpenOrders(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, l) - cp := currency.NewPairWithDelimiter(currency.BTC.String(), currency.USDT.String(), "_") - _, err := l.GetOpenOrders(t.Context(), cp.Lower().String(), "1", "50") - if err != nil { - t.Error(err) - } + _, err := l.GetOpenOrders(t.Context(), testPair.String(), "1", "50") + assert.NoError(t, err, "GetOpenOrders should not error") } func TestUSD2RMBRate(t *testing.T) { t.Parallel() _, err := l.USD2RMBRate(t.Context()) - if err != nil { - t.Error(err) - } + assert.NoError(t, err, "USD2RMBRate should not error") } func TestGetWithdrawConfig(t *testing.T) { t.Parallel() - _, err := l.GetWithdrawConfig(t.Context(), - currency.ETH.Lower().String()) - if err != nil { - t.Error(err) - } + c, err := l.GetWithdrawConfig(t.Context(), currency.ETH) + require.NoError(t, err, "GetWithdrawConfig must not error") + assert.NotEmpty(t, c) } func TestWithdraw(t *testing.T) { @@ -270,75 +207,106 @@ func TestWithdraw(t *testing.T) { sharedtestvalues.SkipTestIfCredentialsUnset(t, l, canManipulateRealOrders) _, err := l.Withdraw(t.Context(), "", "", "", "", "", "") - if err != nil { - t.Error(err) - } + require.NoError(t, err, "Withdraw must not error") } func TestGetWithdrawRecords(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, l) - _, err := l.GetWithdrawalRecords(t.Context(), currency.ETH.Lower().String(), 0, 1, 20) - if err != nil { - t.Error(err) - } + _, err := l.GetWithdrawalRecords(t.Context(), currency.ETH.String(), 0, 1, 20) + assert.NoError(t, err, "GetWithdrawRecords should not error") } func TestLoadPrivKey(t *testing.T) { t.Parallel() + + l2 := new(Lbank) + l2.SetDefaults() + require.ErrorIs(t, l2.loadPrivKey(t.Context()), exchange.ErrCredentialsAreEmpty) + + ctx := account.DeployCredentialsToContext(t.Context(), &account.Credentials{Key: "test", Secret: "errortest"}) + assert.ErrorIs(t, l2.loadPrivKey(ctx), errPEMBlockIsNil) + + key, err := rsa.GenerateKey(rand.Reader, 2048) + require.NoError(t, err) + der := x509.MarshalPKCS1PrivateKey(key) + ctx = account.DeployCredentialsToContext(t.Context(), &account.Credentials{Key: "test", Secret: base64.StdEncoding.EncodeToString(der)}) + require.ErrorIs(t, l2.loadPrivKey(ctx), errUnableToParsePrivateKey) + + ecdsaKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + der, err = x509.MarshalPKCS8PrivateKey(ecdsaKey) + require.NoError(t, err) + ctx = account.DeployCredentialsToContext(t.Context(), &account.Credentials{Key: "test", Secret: base64.StdEncoding.EncodeToString(der)}) + require.ErrorIs(t, l2.loadPrivKey(ctx), common.ErrTypeAssertFailure) + + key, err = rsa.GenerateKey(rand.Reader, 2048) + require.NoError(t, err) + der, err = x509.MarshalPKCS8PrivateKey(key) + require.NoError(t, err) + ctx = account.DeployCredentialsToContext(t.Context(), &account.Credentials{Key: "test", Secret: base64.StdEncoding.EncodeToString(der)}) + assert.NoError(t, l2.loadPrivKey(ctx), "loadPrivKey should not error") + sharedtestvalues.SkipTestIfCredentialsUnset(t, l) - err := l.loadPrivKey(t.Context()) - if err != nil { - t.Error(err) - } - - ctx := account.DeployCredentialsToContext(t.Context(), - &account.Credentials{Secret: "errortest"}) - err = l.loadPrivKey(ctx) - if err == nil { - t.Errorf("Expected error due to pemblock nil") - } + assert.NoError(t, l.loadPrivKey(t.Context()), "loadPrivKey should not error") } func TestSign(t *testing.T) { t.Parallel() + + l2 := new(Lbank) + l2.SetDefaults() + _, err := l2.sign("hello123") + require.ErrorIs(t, err, errPrivateKeyNotLoaded) + + key, err := rsa.GenerateKey(rand.Reader, 2048) + require.NoError(t, err, "GenerateKey must not error") + l2.privateKey = key + + targetMessage := "hello123" + msg, err := l2.sign(targetMessage) + require.NoError(t, err, "sign must not error") + + md5hash, err := gctcrypto.GetMD5([]byte(targetMessage)) + require.NoError(t, err) + m := strings.ToUpper(hex.EncodeToString(md5hash)) + shaHash, err := gctcrypto.GetSHA256([]byte(m)) + require.NoError(t, err) + + sigBytes, err := base64.StdEncoding.DecodeString(msg) + require.NoError(t, err) + err = rsa.VerifyPKCS1v15(&l2.privateKey.PublicKey, crypto.SHA256, shaHash, sigBytes) + require.NoError(t, err) + sharedtestvalues.SkipTestIfCredentialsUnset(t, l) - err := l.loadPrivKey(t.Context()) - if err != nil { - t.Fatal(err) - } + require.NoError(t, l.loadPrivKey(t.Context()), "loadPrivKey must not error") + _, err = l.sign("hello123") - if err != nil { - t.Error(err) - } + assert.NoError(t, err, "sign should not error") } func TestSubmitOrder(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCannotManipulateOrders(t, l, canManipulateRealOrders) - orderSubmission := &order.Submit{ - Exchange: l.Name, - Pair: currency.Pair{ - Base: currency.BTC, - Quote: currency.USDT, - Delimiter: "_", - }, + r, err := l.SubmitOrder(t.Context(), &order.Submit{ + Exchange: l.Name, + Pair: testPair, Side: order.Buy, Type: order.Limit, Price: 1, Amount: 1, ClientID: "meowOrder", AssetType: asset.Spot, - } - response, err := l.SubmitOrder(t.Context(), orderSubmission) - if sharedtestvalues.AreAPICredentialsSet(l) && (err != nil || response.Status != order.New) { - t.Errorf("Order failed to be placed: %v", err) - } else if !sharedtestvalues.AreAPICredentialsSet(l) && err == nil { - t.Error("Expecting an error when no keys are set") + }) + if sharedtestvalues.AreAPICredentialsSet(l) { + require.NoError(t, err, "SubmitOrder must not error") + assert.Equal(t, order.New, r.Status, "SubmitOrder should return order status New") + } else { + assert.Error(t, err, "SubmitOrder should error when credentials are not set") } } @@ -346,26 +314,20 @@ func TestCancelOrder(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, l, canManipulateRealOrders) - cp := currency.NewPairWithDelimiter(currency.ETH.String(), currency.BTC.String(), "_") - var a order.Cancel - a.Pair = cp - a.AssetType = asset.Spot - a.OrderID = "24f7ce27-af1d-4dca-a8c1-ef1cbeec1b23" - err := l.CancelOrder(t.Context(), &a) - if err != nil { - t.Error(err) - } + err := l.CancelOrder(t.Context(), &order.Cancel{ + Pair: testPair, + AssetType: asset.Spot, + OrderID: "24f7ce27-af1d-4dca-a8c1-ef1cbeec1b23", + }) + assert.NoError(t, err, "CancelOrder should not error") } func TestGetOrderInfo(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, l) - _, err := l.GetOrderInfo(t.Context(), - "9ead39f5-701a-400b-b635-d7349eb0f6b", currency.EMPTYPAIR, asset.Spot) - if err != nil { - t.Error(err) - } + _, err := l.GetOrderInfo(t.Context(), "9ead39f5-701a-400b-b635-d7349eb0f6b", currency.EMPTYPAIR, asset.Spot) + assert.NoError(t, err, "GetOrderInfo should not error") } func TestGetAllOpenOrderID(t *testing.T) { @@ -373,22 +335,17 @@ func TestGetAllOpenOrderID(t *testing.T) { sharedtestvalues.SkipTestIfCredentialsUnset(t, l) _, err := l.getAllOpenOrderID(t.Context()) - if err != nil { - t.Error(err) - } + assert.NoError(t, err, "getAllOpenOrderID should not error") } func TestGetFeeByType(t *testing.T) { t.Parallel() - cp := currency.NewPairWithDelimiter(currency.BTC.String(), currency.USDT.String(), "_") - var input exchange.FeeBuilder - input.Amount = 2 - input.FeeType = exchange.CryptocurrencyWithdrawalFee - input.Pair = cp - _, err := l.GetFeeByType(t.Context(), &input) - if err != nil { - t.Error(err) - } + _, err := l.GetFeeByType(t.Context(), &exchange.FeeBuilder{ + Amount: 2, + FeeType: exchange.CryptocurrencyWithdrawalFee, + Pair: testPair, + }) + assert.NoError(t, err, "GetFeeByType should not error") } func TestGetAccountInfo(t *testing.T) { @@ -396,75 +353,50 @@ func TestGetAccountInfo(t *testing.T) { sharedtestvalues.SkipTestIfCredentialsUnset(t, l) _, err := l.UpdateAccountInfo(t.Context(), asset.Spot) - if err != nil { - t.Error(err) - } + assert.NoError(t, err, "UpdateAccountInfo should not error") } func TestGetActiveOrders(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, l) - var input order.MultiOrderRequest - input.Side = order.Buy - input.AssetType = asset.Spot - input.Type = order.AnyType - input.Side = order.AnySide - _, err := l.GetActiveOrders(t.Context(), &input) - if err != nil { - t.Error(err) - } + _, err := l.GetActiveOrders(t.Context(), &order.MultiOrderRequest{ + Side: order.AnySide, + AssetType: asset.Spot, + Type: order.AnyType, + }) + assert.NoError(t, err, "GetActiveOrders should not error") } func TestGetOrderHistory(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, l) - var input order.MultiOrderRequest - input.Side = order.Buy - input.AssetType = asset.Spot - input.Type = order.AnyType - input.Side = order.AnySide - _, err := l.GetOrderHistory(t.Context(), &input) - if err != nil { - t.Error(err) - } + _, err := l.GetOrderHistory(t.Context(), &order.MultiOrderRequest{ + Side: order.AnySide, + AssetType: asset.Spot, + Type: order.AnyType, + }) + assert.NoError(t, err, "GetOrderHistory should not error") } func TestGetHistoricCandles(t *testing.T) { t.Parallel() - cp, err := currency.NewPairFromString(testCurrencyPair) - if err != nil { - t.Fatal(err) - } - _, err = l.GetHistoricCandles(t.Context(), cp, asset.Spot, kline.OneMin, time.Now().Add(-24*time.Hour), time.Now()) - if err != nil { - t.Fatal(err) - } - - _, err = l.GetHistoricCandles(t.Context(), cp, asset.Spot, kline.OneHour, time.Now().Add(-24*time.Hour), time.Now()) - if err != nil { - t.Fatal(err) - } + _, err := l.GetHistoricCandles(t.Context(), currency.EMPTYPAIR, asset.Spot, kline.OneMin, time.Time{}, time.Time{}) + assert.ErrorIs(t, err, currency.ErrCurrencyPairEmpty) + _, err = l.GetHistoricCandles(t.Context(), testPair, asset.Spot, kline.OneMin, time.Now().Add(-24*time.Hour), time.Now()) + assert.NoError(t, err, "GetHistoricCandles should not error") } func TestGetHistoricCandlesExtended(t *testing.T) { t.Parallel() - startTime := time.Now().Add(-time.Minute * 2) - end := time.Now() - cp, err := currency.NewPairFromString(testCurrencyPair) - if err != nil { - t.Fatal(err) - } - _, err = l.GetHistoricCandlesExtended(t.Context(), cp, asset.Spot, kline.OneMin, startTime, end) - if err != nil { - t.Fatal(err) - } + _, err := l.GetHistoricCandlesExtended(t.Context(), testPair, asset.Spot, kline.OneMin, time.Now().Add(-time.Minute*2), time.Now()) + assert.NoError(t, err, "GetHistoricCandlesExtended should not error") } -func Test_FormatExchangeKlineInterval(t *testing.T) { +func TestFormatExchangeKlineInterval(t *testing.T) { t.Parallel() - testCases := []struct { + for _, tc := range []struct { name string interval kline.Interval output string @@ -494,71 +426,41 @@ func Test_FormatExchangeKlineInterval(t *testing.T) { kline.FifteenDay, "", }, - } - - for x := range testCases { - test := testCases[x] - - t.Run(test.name, func(t *testing.T) { + } { + t.Run(tc.name, func(t *testing.T) { t.Parallel() - ret := l.FormatExchangeKlineInterval(test.interval) - - if ret != test.output { - t.Fatalf("unexpected result return expected: %v received: %v", test.output, ret) - } + ret := l.FormatExchangeKlineInterval(tc.interval) + assert.Equalf(t, tc.output, ret, "FormatExchangeKlineInterval(%s) should return %q", tc.interval, tc.output) }) } } func TestGetRecentTrades(t *testing.T) { t.Parallel() - currencyPair, err := currency.NewPairFromString(testCurrencyPair) - if err != nil { - t.Fatal(err) - } - _, err = l.GetRecentTrades(t.Context(), currencyPair, asset.Spot) - if err != nil { - t.Error(err) - } + _, err := l.GetRecentTrades(t.Context(), testPair, asset.Spot) + assert.NoError(t, err, "GetRecentTrades should not error") } func TestGetHistoricTrades(t *testing.T) { t.Parallel() - currencyPair, err := currency.NewPairFromString(testCurrencyPair) - if err != nil { - t.Fatal(err) - } - _, err = l.GetHistoricTrades(t.Context(), - currencyPair, asset.Spot, time.Now().Add(-time.Minute*15), time.Now()) - if err != nil { - t.Error(err) - } - // longer term - _, err = l.GetHistoricTrades(t.Context(), - currencyPair, asset.Spot, time.Now().Add(-time.Minute*60*200), time.Now().Add(-time.Minute*60*199)) - if err != nil { - t.Error(err) - } + _, err := l.GetHistoricTrades(t.Context(), testPair, asset.Spot, time.Now().AddDate(69, 0, 0), time.Now()) + assert.ErrorIs(t, err, common.ErrStartAfterEnd) + _, err = l.GetHistoricTrades(t.Context(), currency.EMPTYPAIR, asset.Spot, time.Now().Add(-time.Minute*15), time.Now()) + assert.ErrorIs(t, err, currency.ErrCurrencyPairEmpty) + _, err = l.GetHistoricTrades(t.Context(), testPair, asset.Spot, time.Now().Add(-time.Minute*15), time.Now()) + assert.NoError(t, err, "GetHistoricTrades should not error") } func TestUpdateTicker(t *testing.T) { t.Parallel() - cp, err := currency.NewPairFromString(testCurrencyPair) - if err != nil { - t.Fatal(err) - } - _, err = l.UpdateTicker(t.Context(), cp, asset.Spot) - if err != nil { - t.Error(err) - } + _, err := l.UpdateTicker(t.Context(), testPair, asset.Spot) + assert.NoError(t, err, "UpdateTicker should not error") } func TestUpdateTickers(t *testing.T) { t.Parallel() err := l.UpdateTickers(t.Context(), asset.Spot) - if err != nil { - t.Error(err) - } + assert.NoError(t, err, "UpdateTickers should not error") } func TestGetStatus(t *testing.T) { @@ -574,36 +476,18 @@ func TestGetStatus(t *testing.T) { {status: 4, resp: order.Cancelling}, {status: 5, resp: order.UnknownStatus}, } { - t.Run("", func(t *testing.T) { + t.Run(tt.resp.String(), func(t *testing.T) { t.Parallel() - resp := l.GetStatus(tt.status) - if resp != tt.resp { - t.Fatalf("received: '%v' but expected: '%v'", resp, tt.resp) - } + assert.Equalf(t, tt.resp.String(), l.GetStatus(tt.status).String(), "GetStatus(%d) should return %s", tt.status, tt.resp) }) } } -func TestGetTimestamp(t *testing.T) { - t.Parallel() - tt, err := l.GetTimestamp(t.Context()) - if err != nil { - t.Error(err) - } - if tt.IsZero() { - t.Error("expected time") - } -} - func TestGetServerTime(t *testing.T) { t.Parallel() - tt, err := l.GetServerTime(t.Context(), asset.Spot) - if err != nil { - t.Error(err) - } - if tt.IsZero() { - t.Error("expected time") - } + ts, err := l.GetServerTime(t.Context(), asset.Spot) + require.NoError(t, err, "GetServerTime must not error") + assert.NotZero(t, ts, "GetServerTime should return a non-zero time") } func TestGetWithdrawalsHistory(t *testing.T) { @@ -611,9 +495,7 @@ func TestGetWithdrawalsHistory(t *testing.T) { sharedtestvalues.SkipTestIfCredentialsUnset(t, l) _, err := l.GetWithdrawalsHistory(t.Context(), currency.BTC, asset.Spot) - if err != nil { - t.Error(err) - } + assert.NoError(t, err, "GetWithdrawalsHistory should not error") } func TestGetCurrencyTradeURL(t *testing.T) { @@ -621,8 +503,8 @@ func TestGetCurrencyTradeURL(t *testing.T) { testexch.UpdatePairsOnce(t, l) for _, a := range l.GetAssetTypes(false) { pairs, err := l.CurrencyPairs.GetPairs(a, false) - require.NoError(t, err, "cannot get pairs for %s", a) - require.NotEmpty(t, pairs, "no pairs for %s", a) + require.NoErrorf(t, err, "GetPairs must not error for asset %s", a) + require.NotEmptyf(t, pairs, "GetPairs for asset %s must return pairs", a) resp, err := l.GetCurrencyTradeURL(t.Context(), a, pairs[0]) require.NoError(t, err) assert.NotEmpty(t, resp) diff --git a/exchanges/lbank/lbank_types.go b/exchanges/lbank/lbank_types.go index b13309b1..0d31e3dc 100644 --- a/exchanges/lbank/lbank_types.go +++ b/exchanges/lbank/lbank_types.go @@ -5,6 +5,7 @@ import ( "github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/encoding/json" + "github.com/thrasher-corp/gocryptotrader/types" ) // Ticker stores the ticker price data for a currency pair @@ -20,7 +21,7 @@ type Ticker struct { // TickerResponse stores the ticker price data and timestamp for a currency pair type TickerResponse struct { Symbol currency.Pair `json:"symbol"` - Timestamp int64 `json:"timestamp"` + Timestamp types.Time `json:"timestamp"` Ticker Ticker `json:"ticker"` } @@ -28,10 +29,10 @@ type TickerResponse struct { type MarketDepthResponse struct { ErrCapture Data struct { - Asks [][2]string `json:"asks"` - Bids [][2]string `json:"bids"` - Timestamp int64 `json:"timestamp"` - } + Asks [][2]types.Number `json:"asks"` + Bids [][2]types.Number `json:"bids"` + Timestamp types.Time `json:"timestamp"` + } `json:"data"` } // TradeResponse stores date_ms, amount, price, type, tid for a currency pair @@ -47,7 +48,7 @@ type TradeResponse struct { type KlineResponse struct { TimeStamp time.Time `json:"timestamp"` OpenPrice float64 `json:"openprice"` - HigestPrice float64 `json:"highestprice"` + HighestPrice float64 `json:"highestprice"` LowestPrice float64 `json:"lowestprice"` ClosePrice float64 `json:"closeprice"` TradingVolume float64 `json:"tradingvolume"` @@ -183,10 +184,15 @@ type ExchangeRateResponse struct { // WithdrawConfigResponse stores info about withdrawal configurations type WithdrawConfigResponse struct { - AssetCode string `json:"assetCode"` - Minimum string `json:"min"` - CanWithDraw bool `json:"canWithDraw"` - Fee string `json:"fee"` + AmountScale int64 `json:"amountScale,string"` + Chain string `json:"chain"` + AssetCode currency.Code `json:"assetCode"` + Minimum float64 `json:"min,string"` + TransferAmountScale int64 `json:"transferAmtScale,string"` + CanWithdraw bool `json:"canWithDraw"` + Fee float64 `json:"fee"` + MinimumTransfer float64 `json:"minTransfer,string"` + Type int64 `json:"type,string"` } // WithdrawResponse stores info about the withdrawal @@ -238,7 +244,7 @@ type GetAllOpenIDResp struct { // TimestampResponse holds timestamp data type TimestampResponse struct { - Timestamp int64 `json:"data"` + Timestamp types.Time `json:"data"` } var errorCodes = map[int64]string{ diff --git a/exchanges/lbank/lbank_wrapper.go b/exchanges/lbank/lbank_wrapper.go index 94893ef5..c79ec78b 100644 --- a/exchanges/lbank/lbank_wrapper.go +++ b/exchanges/lbank/lbank_wrapper.go @@ -170,17 +170,16 @@ func (l *Lbank) UpdateTickers(ctx context.Context, a asset.Item) error { continue } - err = ticker.ProcessTicker(&ticker.Price{ + if err := ticker.ProcessTicker(&ticker.Price{ Last: tickerInfo[j].Ticker.Latest, High: tickerInfo[j].Ticker.High, Low: tickerInfo[j].Ticker.Low, Volume: tickerInfo[j].Ticker.Volume, Pair: tickerInfo[j].Symbol, - LastUpdated: time.Unix(0, tickerInfo[j].Timestamp), + LastUpdated: tickerInfo[j].Timestamp.Time(), ExchangeName: l.Name, AssetType: a, - }) - if err != nil { + }); err != nil { return err } } @@ -198,62 +197,42 @@ func (l *Lbank) UpdateTicker(ctx context.Context, p currency.Pair, a asset.Item) // UpdateOrderbook updates and returns the orderbook for a currency pair func (l *Lbank) UpdateOrderbook(ctx context.Context, p currency.Pair, assetType asset.Item) (*orderbook.Base, error) { - if p.IsEmpty() { - return nil, currency.ErrCurrencyPairEmpty + if !l.SupportsAsset(assetType) { + return nil, fmt.Errorf("%w: %q", asset.ErrNotSupported, assetType) } - if err := l.CurrencyPairs.IsAssetEnabled(assetType); err != nil { + + fPair, err := l.FormatExchangeCurrency(p, assetType) + if err != nil { return nil, err } + + d, err := l.GetMarketDepths(ctx, fPair.String(), 60) + if err != nil { + return nil, err + } + book := &orderbook.Base{ Exchange: l.Name, Pair: p, Asset: assetType, VerifyOrderbook: l.CanVerifyOrderbook, - } - fPair, err := l.FormatExchangeCurrency(p, assetType) - if err != nil { - return book, err + Asks: make(orderbook.Tranches, len(d.Data.Asks)), + Bids: make(orderbook.Tranches, len(d.Data.Bids)), } - a, err := l.GetMarketDepths(ctx, fPair.String(), "60", "1") - if err != nil { - return book, err + for i := range d.Data.Asks { + book.Asks[i].Price = d.Data.Asks[i][0].Float64() + book.Asks[i].Amount = d.Data.Asks[i][1].Float64() + } + for i := range d.Data.Bids { + book.Bids[i].Price = d.Data.Bids[i][0].Float64() + book.Bids[i].Amount = d.Data.Bids[i][1].Float64() } - book.Asks = make(orderbook.Tranches, len(a.Data.Asks)) - for i := range a.Data.Asks { - price, convErr := strconv.ParseFloat(a.Data.Asks[i][0], 64) - if convErr != nil { - return book, convErr - } - amount, convErr := strconv.ParseFloat(a.Data.Asks[i][1], 64) - if convErr != nil { - return book, convErr - } - book.Asks[i] = orderbook.Tranche{ - Price: price, - Amount: amount, - } - } - book.Bids = make(orderbook.Tranches, len(a.Data.Bids)) - for i := range a.Data.Bids { - price, convErr := strconv.ParseFloat(a.Data.Bids[i][0], 64) - if convErr != nil { - return book, convErr - } - amount, convErr := strconv.ParseFloat(a.Data.Bids[i][1], 64) - if convErr != nil { - return book, convErr - } - book.Bids[i] = orderbook.Tranche{ - Price: price, - Amount: amount, - } - } - err = book.Process() - if err != nil { - return book, err + if err := book.Process(); err != nil { + return nil, err } + return orderbook.Get(l.Name, p, assetType) } @@ -352,14 +331,11 @@ func (l *Lbank) GetHistoricTrades(ctx context.Context, p currency.Pair, assetTyp } var resp []trade.Data ts := timestampStart - limit := 600 + const limit uint64 = 600 allTrades: for { var tradeData []TradeResponse - tradeData, err = l.GetTrades(ctx, - p.String(), - int64(limit), - ts.UnixMilli()) + tradeData, err = l.GetTrades(ctx, p.String(), limit, ts) if err != nil { return nil, err } @@ -390,7 +366,7 @@ allTrades: ts = tradeTime } } - if len(tradeData) != limit { + if len(tradeData) != int(limit) { break allTrades } } @@ -756,22 +732,16 @@ func (l *Lbank) GetFeeByType(ctx context.Context, feeBuilder *exchange.FeeBuilde return feeBuilder.Amount * feeBuilder.PurchasePrice * 0.002, nil } if feeBuilder.FeeType == exchange.CryptocurrencyWithdrawalFee { - withdrawalFee, err := l.GetWithdrawConfig(ctx, - feeBuilder.Pair.Base.Lower().String()) + withdrawalFee, err := l.GetWithdrawConfig(ctx, feeBuilder.Pair.Base) if err != nil { return resp, err } for i := range withdrawalFee { - if !strings.EqualFold(withdrawalFee[i].AssetCode, feeBuilder.Pair.Base.String()) { + if !withdrawalFee[i].AssetCode.Equal(feeBuilder.Pair.Base) { continue } - if withdrawalFee[i].Fee == "" { - return 0, nil - } - resp, err = strconv.ParseFloat(withdrawalFee[i].Fee, 64) - if err != nil { - return resp, err - } + resp = withdrawalFee[i].Fee + break } } return resp, nil @@ -856,7 +826,7 @@ func (l *Lbank) GetHistoricCandles(ctx context.Context, pair currency.Pair, a as req.RequestFormatted.String(), strconv.FormatUint(req.RequestLimit, 10), l.FormatExchangeKlineInterval(req.ExchangeInterval), - strconv.FormatInt(req.Start.Unix(), 10)) + req.Start) if err != nil { return nil, err } @@ -866,7 +836,7 @@ func (l *Lbank) GetHistoricCandles(ctx context.Context, pair currency.Pair, a as timeSeries[x] = kline.Candle{ Time: data[x].TimeStamp, Open: data[x].OpenPrice, - High: data[x].HigestPrice, + High: data[x].HighestPrice, Low: data[x].LowestPrice, Close: data[x].ClosePrice, Volume: data[x].TradingVolume, @@ -889,7 +859,7 @@ func (l *Lbank) GetHistoricCandlesExtended(ctx context.Context, pair currency.Pa req.RequestFormatted.String(), strconv.FormatUint(req.RequestLimit, 10), l.FormatExchangeKlineInterval(req.ExchangeInterval), - strconv.FormatInt(req.RangeHolder.Ranges[x].Start.Ticks, 10)) + req.RangeHolder.Ranges[x].Start.Time) if err != nil { return nil, err } @@ -901,7 +871,7 @@ func (l *Lbank) GetHistoricCandlesExtended(ctx context.Context, pair currency.Pa timeSeries = append(timeSeries, kline.Candle{ Time: data[i].TimeStamp, Open: data[i].OpenPrice, - High: data[i].HigestPrice, + High: data[i].HighestPrice, Low: data[i].LowestPrice, Close: data[i].ClosePrice, Volume: data[i].TradingVolume, diff --git a/testdata/configtest.json b/testdata/configtest.json index ad1973f7..07d652dd 100644 --- a/testdata/configtest.json +++ b/testdata/configtest.json @@ -2167,7 +2167,7 @@ ], "pairs": { "spot": { - "enabled": "eth_btc", + "enabled": "BTC_USDT", "available": "FBC_USDT,GALT_USDT,IOEX_USDT,OATH_USDT,BLOC_USDT,BTC_USDT,ETH_USDT,ETH_BTC,ABBC_BTC,KISC_ETH,BXA_USDT,ATP_USDT,MAT_USDT,SKY_BTC,RNT_USDT,VENA_USDT,GRIN_USDT,IDA_USDT,PNT_USDT,OPX_USDT,VTHO_BTC,AMO_ETH,UBEX_BTC,EOS_BTC,UBEX_USDT,TNS_BTC,SAIT_ETH,DAX_BTC,DAX_ETH,DALI_USDT,VET_USDT,BCH_BTC,BCH_USDT,NEO_USDT,QTUM_USDT,ZEC_USDT,VET_BTC,PAI_BTC,PNT_BTC,NEO_BTC,DASH_BTC,LTC_BTC,ETC_BTC,QTUM_BTC,ZEC_BTC,SC_BTC,BTS_BTC,CPX_BTC,XWC_BTC,FIL6_BTC,FIL12_BTC,FIL36_BTC,EOS_USDT,UT_ETH,ELA_ETH,VET_ETH,VTHO_ETH,PAI_ETH,HER_ETH,PTT_ETH,TAC_ETH,IDHUB_ETH,SSC_ETH,SKM_ETH,PLY_ETH,EXT_ETH,EOS_ETH,YOYOW_ETH,TRX_ETH,QTUM_ETH,ZEC_ETH,BTS_ETH,BTM_ETH,MITH_ETH,NAS_ETH,MAN_ETH,DBC_ETH,BTO_ETH,DDD_ETH,CPX_ETH,CS_ETH,IHT_ETH,OCN_ETH,EKO_ETH,XWC_ETH,PUT_ETH,PNT_ETH,AAC_ETH,FIL6_ETH,FIL12_ETH,FIL36_ETH,SEER_ETH,BSB_ETH,CDC_ETH,GRAMS_ETH,DDMX_ETH,EAI_ETH,BNB_USDT,HT_USDT,KBC_BTC,KBC_USDT,MAI_USDT,PHV_USDT,GT_USDT,VOKEN_USDT,CYE_USDT,BRC_USDT,BTC_AUSD,DDMX_USDT,SEAL_USDT,SEOS_BTC,BTY_USDT,FO_USDT,DLX_USDT,BFC_USDT,LBK_USDT,SERO_USDT,MTV_USDT,CKB_USDT,ARPA_USDT,ZIP_USDT,AT_USDT,DOT_USDT,DILI_USDT,DUO_USDT,TEP_USDT,BIKI_USDT,MX_USDT,DNS_USDT,OKB_USDT,FLDT_USDT,CCTC_USDT,WIN_USDT,BTT_USDT,TRX_USDT,GRS_BTC,GST_USDT,GST_ETH,ABBC_USDT,UTK_USDT,GKI_USDT,BPX_USDT,SUTER_USDT,LT_USDT,LM_USDT,HTDF_USDT" } }