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 <oharareid.ryan@gmail.com>

* 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 <oharareid.ryan@gmail.com>
This commit is contained in:
Adrian Gallagher
2025-05-27 19:03:09 +10:00
committed by GitHub
parent a1d06686d3
commit 1e5739dffa
5 changed files with 293 additions and 428 deletions

View File

@@ -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 {

View File

@@ -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)

View File

@@ -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{

View File

@@ -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,