Files
gocryptotrader/cmd/exchange_wrapper_standards/exchange_wrapper_standards_test.go
Gareth Kirwan 16d2d9f35a Config: AssetEnabled upgrade (#1735)
* Config: Move assetEnabled upgrade to Version management

* Assets: Do not error on asset not enabled, or disabled

This became more messy with Disabling something that's defaulted to
disabled.
Taking an idealogical stance against erroring that what you want to have
done is already done.

* CurrencyManager: Set AssetEnabled when StorePairs(enabled)

* RPCServer: Fix tests expecting StoreAssetPairFormat to enable the asset

Also assertifies

* Bitfinex: Fix tests for MarginFunding subs

* GCTWrapper: Improve TestMain clarity

* BTSE: Add futures to testconfig

* Exchanges: Rename StoreAssetPairStore

Previously we were calling it "Format", but accepting everything from
the PairStore.
We were also defaulting to turning the Asset on.

Now callers need to get their AssetEnabled set as they want it, so
there's no magic

This change also moves responsibility for error wrapping outside to the
caller.

* Config: AssetEnabled upgrade should respect assetTypes

Previously we ignored the field and just turned on everything.
I think that was because we couldn't get at the old value.
In either case, we have the option to do better, and respect the
assetEnabled value

* Config: Improve exchange config version upgrade error messages
2025-03-17 21:47:37 +11:00

778 lines
30 KiB
Go

package exchangewrapperstandards
import (
"context"
"errors"
"os"
"reflect"
"slices"
"strings"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/common/key"
"github.com/thrasher-corp/gocryptotrader/config"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/engine"
exchange "github.com/thrasher-corp/gocryptotrader/exchanges"
"github.com/thrasher-corp/gocryptotrader/exchanges/account"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/collateral"
"github.com/thrasher-corp/gocryptotrader/exchanges/deposit"
"github.com/thrasher-corp/gocryptotrader/exchanges/fundingrate"
"github.com/thrasher-corp/gocryptotrader/exchanges/futures"
"github.com/thrasher-corp/gocryptotrader/exchanges/kline"
"github.com/thrasher-corp/gocryptotrader/exchanges/margin"
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
"github.com/thrasher-corp/gocryptotrader/exchanges/orderbook"
"github.com/thrasher-corp/gocryptotrader/exchanges/request"
"github.com/thrasher-corp/gocryptotrader/exchanges/ticker"
"github.com/thrasher-corp/gocryptotrader/portfolio/banking"
"github.com/thrasher-corp/gocryptotrader/portfolio/withdraw"
)
func TestMain(m *testing.M) {
// only run testing suite for one CI/CD environment
if skipAdditionalWrapperCITests() {
return
}
os.Exit(m.Run())
}
// singleExchangeOverride enter an exchange name to only test that exchange
var singleExchangeOverride = ""
func TestAllExchangeWrappers(t *testing.T) {
t.Parallel()
cfg := config.GetConfig()
err := cfg.LoadConfig("../../testdata/configtest.json", true)
if err != nil {
t.Fatal("load config error", err)
}
for i := range cfg.Exchanges {
name := strings.ToLower(cfg.Exchanges[i].Name)
t.Run(name+" wrapper tests", func(t *testing.T) {
t.Parallel()
if slices.Contains(unsupportedExchangeNames, name) {
t.Skipf("skipping unsupported exchange %v", name)
}
if singleExchangeOverride != "" && name != singleExchangeOverride {
t.Skip("skipping ", name, " due to override")
}
ctx := context.Background()
if isCITest() && slices.Contains(blockedCIExchanges, name) {
// rather than skipping tests where execution is blocked, provide an expired
// context, so no executions can take place
var cancelFn context.CancelFunc
ctx, cancelFn = context.WithTimeout(context.Background(), 0)
cancelFn()
}
exch, assetPairs := setupExchange(ctx, t, name, cfg)
executeExchangeWrapperTests(ctx, t, exch, assetPairs)
})
}
}
func setupExchange(ctx context.Context, t *testing.T, name string, cfg *config.Config) (exchange.IBotExchange, []assetPair) {
t.Helper()
em := engine.NewExchangeManager()
exch, err := em.NewExchangeByName(name)
if err != nil {
t.Fatalf("Cannot setup %v NewExchangeByName %v", name, err)
}
var exchCfg *config.Exchange
exchCfg, err = cfg.GetExchangeConfig(name)
if err != nil {
t.Fatalf("Cannot setup %v GetExchangeConfig %v", name, err)
}
exch.SetDefaults()
exchCfg.API.AuthenticatedSupport = true
exchCfg.API.Credentials = getExchangeCredentials(name)
err = exch.Setup(exchCfg)
if err != nil {
t.Fatalf("Cannot setup %v exchange Setup %v", name, err)
}
err = exch.UpdateTradablePairs(ctx, true)
if err != nil && !errors.Is(err, context.DeadlineExceeded) {
t.Fatalf("Cannot setup %v UpdateTradablePairs %v", name, err)
}
b := exch.GetBase()
assets := b.CurrencyPairs.GetAssetTypes(false)
require.NotEmpty(t, assets, "exchange %s must have assets", name)
for _, a := range assets {
require.NoErrorf(t, b.CurrencyPairs.SetAssetEnabled(a, true), "exchange %s SetAssetEnabled must not error for %s", name, a)
}
// Add +1 to len to verify that exchanges can handle requests with unset pairs and assets
assetPairs := make([]assetPair, 0, len(assets)+1)
assets:
for j := range assets {
var pairs currency.Pairs
pairs, err = b.CurrencyPairs.GetPairs(assets[j], false)
if err != nil {
t.Fatalf("Cannot setup %v asset %v GetPairs %v", name, assets[j], err)
}
var p currency.Pair
p, err = getPairFromPairs(t, pairs)
if err != nil {
if errors.Is(err, currency.ErrCurrencyPairsEmpty) {
continue
}
t.Fatalf("Cannot setup %v asset %v getPairFromPairs %v", name, assets[j], err)
}
err = b.CurrencyPairs.EnablePair(assets[j], p)
if err != nil && !errors.Is(err, currency.ErrPairAlreadyEnabled) {
t.Fatalf("Cannot setup %v asset %v EnablePair %v", name, assets[j], err)
}
p, err = b.FormatExchangeCurrency(p, assets[j])
if err != nil {
t.Fatalf("Cannot setup %v asset %v FormatExchangeCurrency %v", name, assets[j], err)
}
for x := range unsupportedAssets {
if assets[j] == unsupportedAssets[x] {
// this asset cannot handle disrupt formatting
continue assets
}
}
p, err = disruptFormatting(t, p)
if err != nil {
t.Fatalf("Cannot setup %v asset %v disruptFormatting %v", name, assets[j], err)
}
assetPairs = append(assetPairs, assetPair{
Pair: p,
Asset: assets[j],
})
}
assetPairs = append(assetPairs, assetPair{})
return exch, assetPairs
}
// isUnacceptableError sentences errs to 10 years dungeon if unacceptable
func isUnacceptableError(t *testing.T, err error) error {
t.Helper()
for i := range acceptableErrors {
if errors.Is(err, acceptableErrors[i]) {
return nil
}
}
for i := range warningErrors {
if errors.Is(err, warningErrors[i]) {
t.Log(err)
return nil
}
}
return err
}
func executeExchangeWrapperTests(ctx context.Context, t *testing.T, exch exchange.IBotExchange, assetParams []assetPair) {
t.Helper()
iExchange := reflect.TypeOf(&exch).Elem()
actualExchange := reflect.ValueOf(exch)
for x := range iExchange.NumMethod() {
methodName := iExchange.Method(x).Name
if _, ok := excludedMethodNames[methodName]; ok {
continue
}
method := actualExchange.MethodByName(methodName)
var assetLen int
for y := range method.Type().NumIn() {
input := method.Type().In(y)
for _, t := range []reflect.Type{
assetParam, orderSubmitParam, orderModifyParam, orderCancelParam, orderCancelsParam, pairKeySliceParam, getOrdersRequestParam, latestRateRequest,
} {
if input.AssignableTo(t) {
// this allows wrapper functions that support assets types
// to be tested with all supported assets
assetLen = len(assetParams) - 1
break
}
}
}
tt := time.Now()
e := time.Date(tt.Year(), tt.Month(), tt.Day()-1, 0, 0, 0, 0, time.UTC)
s := e.Add(-time.Hour * 24 * 2)
if methodName == "GetHistoricTrades" {
// limit trade history
e = time.Now()
s = e.Add(-time.Minute * 3)
}
for y := 0; y <= assetLen; y++ {
inputs := make([]reflect.Value, method.Type().NumIn())
argGenerator := &MethodArgumentGenerator{
Exchange: exch,
AssetParams: assetParams[y],
MethodName: methodName,
Start: s,
End: e,
}
for z := range method.Type().NumIn() {
argGenerator.MethodInputType = method.Type().In(z)
generatedArg := generateMethodArg(ctx, t, argGenerator)
inputs[z] = *generatedArg
}
assetY := assetParams[y].Asset.String()
pairY := assetParams[y].Pair.String()
t.Run(methodName+"-"+assetY+"-"+pairY, func(t *testing.T) {
t.Parallel()
CallExchangeMethod(t, method, inputs, methodName, exch)
})
}
}
}
// CallExchangeMethod will call an exchange's method using generated arguments
// and determine if the error is friendly
func CallExchangeMethod(t *testing.T, methodToCall reflect.Value, methodValues []reflect.Value, methodName string, exch exchange.IBotExchange) {
t.Helper()
outputs := methodToCall.Call(methodValues)
for i := range outputs {
outputInterface := outputs[i].Interface()
err, ok := outputInterface.(error)
if !ok {
continue
}
if isUnacceptableError(t, err) != nil {
literalInputs := make([]interface{}, len(methodValues))
for j := range methodValues {
if methodValues[j].Kind() == reflect.Ptr {
// dereference pointers just to add a bit more clarity
literalInputs[j] = methodValues[j].Elem().Interface()
continue
}
literalInputs[j] = methodValues[j].Interface()
}
t.Errorf("%v Func '%v' Error: '%v'. Inputs: %v.", exch.GetName(), methodName, err, literalInputs)
}
break
}
}
// MethodArgumentGenerator is used to create arguments for
// an IBotExchange method
type MethodArgumentGenerator struct {
Exchange exchange.IBotExchange
AssetParams assetPair
MethodInputType reflect.Type
MethodName string
Start time.Time
End time.Time
StartTimeSet bool
argNum int64
}
var (
currencyPairParam = reflect.TypeOf((*currency.Pair)(nil)).Elem()
klineParam = reflect.TypeOf((*kline.Interval)(nil)).Elem()
contextParam = reflect.TypeOf((*context.Context)(nil)).Elem()
timeParam = reflect.TypeOf((*time.Time)(nil)).Elem()
codeParam = reflect.TypeOf((*currency.Code)(nil)).Elem()
currencyPairsParam = reflect.TypeOf((*currency.Pairs)(nil)).Elem()
withdrawRequestParam = reflect.TypeOf((**withdraw.Request)(nil)).Elem()
stringParam = reflect.TypeOf((*string)(nil)).Elem()
feeBuilderParam = reflect.TypeOf((**exchange.FeeBuilder)(nil)).Elem()
credentialsParam = reflect.TypeOf((**account.Credentials)(nil)).Elem()
orderSideParam = reflect.TypeOf((*order.Side)(nil)).Elem()
collateralModeParam = reflect.TypeOf((*collateral.Mode)(nil)).Elem()
marginTypeParam = reflect.TypeOf((*margin.Type)(nil)).Elem()
int64Param = reflect.TypeOf((*int64)(nil)).Elem()
float64Param = reflect.TypeOf((*float64)(nil)).Elem()
// types with asset in params
assetParam = reflect.TypeOf((*asset.Item)(nil)).Elem()
orderSubmitParam = reflect.TypeOf((**order.Submit)(nil)).Elem()
orderModifyParam = reflect.TypeOf((**order.Modify)(nil)).Elem()
orderCancelParam = reflect.TypeOf((**order.Cancel)(nil)).Elem()
orderCancelsParam = reflect.TypeOf((*[]order.Cancel)(nil)).Elem()
getOrdersRequestParam = reflect.TypeOf((**order.MultiOrderRequest)(nil)).Elem()
positionChangeRequestParam = reflect.TypeOf((**margin.PositionChangeRequest)(nil)).Elem()
positionSummaryRequestParam = reflect.TypeOf((**futures.PositionSummaryRequest)(nil)).Elem()
positionsRequestParam = reflect.TypeOf((**futures.PositionsRequest)(nil)).Elem()
latestRateRequest = reflect.TypeOf((**fundingrate.LatestRateRequest)(nil)).Elem()
pairKeySliceParam = reflect.TypeOf((*[]key.PairAsset)(nil)).Elem()
)
// generateMethodArg determines the argument type and returns a pre-made
// response, else an empty version of the type
func generateMethodArg(ctx context.Context, t *testing.T, argGenerator *MethodArgumentGenerator) *reflect.Value {
t.Helper()
exchName := strings.ToLower(argGenerator.Exchange.GetName())
var input reflect.Value
switch {
case argGenerator.MethodInputType.AssignableTo(stringParam):
switch argGenerator.MethodName {
case "GetDepositAddress":
if argGenerator.argNum == 2 {
// account type
input = reflect.ValueOf("trading")
} else {
// Crypto Chain
input = reflect.ValueOf(cryptoChainPerExchange[exchName])
}
case "MatchSymbolWithAvailablePairs", "MatchSymbolCheckEnabled":
input = reflect.ValueOf(argGenerator.AssetParams.Pair.Base.Lower().String() + argGenerator.AssetParams.Pair.Quote.Lower().String())
default:
// OrderID
input = reflect.ValueOf("1337")
}
case argGenerator.MethodInputType.AssignableTo(pairKeySliceParam):
input = reflect.ValueOf(key.PairAsset{
Base: argGenerator.AssetParams.Pair.Base.Item,
Quote: argGenerator.AssetParams.Pair.Quote.Item,
Asset: argGenerator.AssetParams.Asset,
})
case argGenerator.MethodInputType.AssignableTo(credentialsParam):
input = reflect.ValueOf(&account.Credentials{
Key: "test",
Secret: "test",
ClientID: "test",
PEMKey: "test",
SubAccount: "test",
OneTimePassword: "test",
})
case argGenerator.MethodInputType.Implements(contextParam):
// Need to deploy a context.Context value as nil value is not
// checked throughout codebase.
input = reflect.ValueOf(ctx)
case argGenerator.MethodInputType.AssignableTo(feeBuilderParam):
input = reflect.ValueOf(&exchange.FeeBuilder{
FeeType: exchange.OfflineTradeFee,
Amount: 150,
PurchasePrice: 150,
Pair: argGenerator.AssetParams.Pair,
})
case argGenerator.MethodInputType.AssignableTo(currencyPairParam):
input = reflect.ValueOf(argGenerator.AssetParams.Pair)
case argGenerator.MethodInputType.AssignableTo(assetParam):
input = reflect.ValueOf(argGenerator.AssetParams.Asset)
case argGenerator.MethodInputType.AssignableTo(klineParam):
input = reflect.ValueOf(kline.OneDay)
case argGenerator.MethodInputType.AssignableTo(codeParam):
if argGenerator.MethodName == "GetAvailableTransferChains" {
input = reflect.ValueOf(currency.ETH)
} else {
input = reflect.ValueOf(argGenerator.AssetParams.Pair.Base)
}
case argGenerator.MethodInputType.AssignableTo(timeParam):
if !argGenerator.StartTimeSet {
input = reflect.ValueOf(argGenerator.Start)
argGenerator.StartTimeSet = true
} else {
input = reflect.ValueOf(argGenerator.End)
}
case argGenerator.MethodInputType.AssignableTo(currencyPairsParam):
b := argGenerator.Exchange.GetBase()
if argGenerator.AssetParams.Asset != asset.Empty {
input = reflect.ValueOf(b.CurrencyPairs.Pairs[argGenerator.AssetParams.Asset].Available)
} else {
input = reflect.ValueOf(currency.Pairs{
argGenerator.AssetParams.Pair,
})
}
case argGenerator.MethodInputType.AssignableTo(withdrawRequestParam):
req := &withdraw.Request{
Exchange: exchName,
Description: "1337",
Amount: 1,
ClientOrderID: "1337",
}
if argGenerator.MethodName == "WithdrawCryptocurrencyFunds" {
req.Type = withdraw.Crypto
switch {
case !isFiat(t, argGenerator.AssetParams.Pair.Base.Item.Lower):
req.Currency = argGenerator.AssetParams.Pair.Base
case !isFiat(t, argGenerator.AssetParams.Pair.Quote.Item.Lower):
req.Currency = argGenerator.AssetParams.Pair.Quote
default:
req.Currency = currency.ETH
}
req.Crypto = withdraw.CryptoRequest{
Address: "1337",
AddressTag: "1337",
Chain: cryptoChainPerExchange[exchName],
}
} else {
req.Type = withdraw.Fiat
b := argGenerator.Exchange.GetBase()
if len(b.Config.BaseCurrencies) > 0 {
req.Currency = b.Config.BaseCurrencies[0]
} else {
req.Currency = currency.USD
}
req.Fiat = withdraw.FiatRequest{
Bank: banking.Account{
Enabled: true,
ID: "1337",
BankName: "1337",
BankAddress: "1337",
BankPostalCode: "1337",
BankPostalCity: "1337",
BankCountry: "1337",
AccountName: "1337",
AccountNumber: "1337",
SWIFTCode: "1337",
IBAN: "1337",
BSBNumber: "1337",
BankCode: 1337,
SupportedCurrencies: req.Currency.String(),
SupportedExchanges: exchName,
},
IsExpressWire: false,
RequiresIntermediaryBank: false,
IntermediaryBankAccountNumber: 1338,
IntermediaryBankName: "1338",
IntermediaryBankAddress: "1338",
IntermediaryBankCity: "1338",
IntermediaryBankCountry: "1338",
IntermediaryBankPostalCode: "1338",
IntermediarySwiftCode: "1338",
IntermediaryBankCode: 1338,
IntermediaryIBAN: "1338",
WireCurrency: "1338",
}
}
input = reflect.ValueOf(req)
case argGenerator.MethodInputType.AssignableTo(orderSubmitParam):
input = reflect.ValueOf(&order.Submit{
Exchange: exchName,
Type: order.Limit,
Side: order.Buy,
Pair: argGenerator.AssetParams.Pair,
AssetType: argGenerator.AssetParams.Asset,
Price: 150,
Amount: 1,
ClientID: "1337",
ClientOrderID: "13371337",
ImmediateOrCancel: true,
Leverage: 1,
})
case argGenerator.MethodInputType.AssignableTo(orderModifyParam):
input = reflect.ValueOf(&order.Modify{
Exchange: exchName,
Type: order.Limit,
Side: order.Buy,
Pair: argGenerator.AssetParams.Pair,
AssetType: argGenerator.AssetParams.Asset,
Price: 150,
Amount: 1,
ClientOrderID: "13371337",
OrderID: "1337",
ImmediateOrCancel: true,
})
case argGenerator.MethodInputType.AssignableTo(orderCancelParam):
input = reflect.ValueOf(&order.Cancel{
Exchange: exchName,
Type: order.Limit,
Side: order.Buy,
Pair: argGenerator.AssetParams.Pair,
AssetType: argGenerator.AssetParams.Asset,
OrderID: "1337",
})
case argGenerator.MethodInputType.AssignableTo(orderCancelsParam):
input = reflect.ValueOf([]order.Cancel{
{
Exchange: exchName,
Type: order.Market,
Side: order.Buy,
Pair: argGenerator.AssetParams.Pair,
AssetType: argGenerator.AssetParams.Asset,
OrderID: "1337",
},
})
case argGenerator.MethodInputType.AssignableTo(getOrdersRequestParam):
input = reflect.ValueOf(&order.MultiOrderRequest{
Type: order.AnyType,
Side: order.AnySide,
FromOrderID: "1337",
AssetType: argGenerator.AssetParams.Asset,
Pairs: currency.Pairs{argGenerator.AssetParams.Pair},
})
case argGenerator.MethodInputType.AssignableTo(marginTypeParam):
input = reflect.ValueOf(margin.Isolated)
case argGenerator.MethodInputType.AssignableTo(collateralModeParam):
input = reflect.ValueOf(collateral.SingleMode)
case argGenerator.MethodInputType.AssignableTo(positionChangeRequestParam):
input = reflect.ValueOf(&margin.PositionChangeRequest{
Exchange: argGenerator.Exchange.GetName(),
Pair: argGenerator.AssetParams.Pair,
Asset: argGenerator.AssetParams.Asset,
MarginType: margin.Isolated,
OriginalAllocatedMargin: 150,
NewAllocatedMargin: 151,
})
case argGenerator.MethodInputType.AssignableTo(positionSummaryRequestParam):
input = reflect.ValueOf(&futures.PositionSummaryRequest{
Asset: argGenerator.AssetParams.Asset,
Pair: argGenerator.AssetParams.Pair,
Direction: order.Buy,
})
case argGenerator.MethodInputType.AssignableTo(positionsRequestParam):
input = reflect.ValueOf(&futures.PositionsRequest{
Asset: argGenerator.AssetParams.Asset,
Pairs: currency.Pairs{argGenerator.AssetParams.Pair},
StartDate: argGenerator.Start,
EndDate: argGenerator.End,
RespectOrderHistoryLimits: true,
})
case argGenerator.MethodInputType.AssignableTo(orderSideParam):
input = reflect.ValueOf(order.Long)
case argGenerator.MethodInputType.AssignableTo(int64Param):
input = reflect.ValueOf(150)
case argGenerator.MethodInputType.AssignableTo(float64Param):
input = reflect.ValueOf(150.0)
case argGenerator.MethodInputType.AssignableTo(latestRateRequest):
input = reflect.ValueOf(&fundingrate.LatestRateRequest{
Asset: argGenerator.AssetParams.Asset,
Pair: argGenerator.AssetParams.Pair,
IncludePredictedRate: true,
})
default:
input = reflect.Zero(argGenerator.MethodInputType)
}
argGenerator.argNum++
return &input
}
// assetPair holds a currency pair associated with an asset
type assetPair struct {
Pair currency.Pair
Asset asset.Item
}
// excludedMethodNames represent the functions that are not
// currently tested under this suite due to irrelevance
// or not worth checking yet
var excludedMethodNames = map[string]struct{}{
"Setup": {}, // Is run via test setup
"Start": {}, // Is run via test setup
"SetDefaults": {}, // Is run via test setup
"UpdateTradablePairs": {}, // Is run via test setup
"GetDefaultConfig": {}, // Is run via test setup
"FetchTradablePairs": {}, // Is run via test setup
"AuthenticateWebsocket": {}, // Unnecessary websocket test
"FlushWebsocketChannels": {}, // Unnecessary websocket test
"UnsubscribeToWebsocketChannels": {}, // Unnecessary websocket test
"SubscribeToWebsocketChannels": {}, // Unnecessary websocket test
"UpdateCurrencyStates": {}, // Not widely supported/implemented feature
"CanTradePair": {}, // Not widely supported/implemented feature
"CanTrade": {}, // Not widely supported/implemented feature
"CanWithdraw": {}, // Not widely supported/implemented feature
"CanDeposit": {}, // Not widely supported/implemented feature
"GetCurrencyStateSnapshot": {}, // Not widely supported/implemented feature
"SetHTTPClientUserAgent": {}, // standard base implementation
"SetClientProxyAddress": {}, // standard base implementation
// Not widely supported/implemented futures endpoints
"GetCollateralCurrencyForContract": {},
"GetCurrencyForRealisedPNL": {},
"GetFuturesPositions": {},
"GetHistoricalFundingRates": {},
"IsPerpetualFutureCurrency": {},
"GetMarginRatesHistory": {},
"CalculatePNL": {},
"CalculateTotalCollateral": {},
"ScaleCollateral": {},
"GetPositionSummary": {},
"GetFuturesPositionSummary": {},
"GetFuturesPositionOrders": {},
"SetCollateralMode": {},
"GetCollateralMode": {},
"SetLeverage": {},
"GetLeverage": {},
"SetMarginType": {},
"ChangePositionMargin": {},
}
// blockedCIExchanges are exchanges that are not able to be tested on CI
var blockedCIExchanges = []string{
"binance", // binance API is banned from executing within the US where github Actions is ran
"bybit", // bybit API is banned from executing within the US where github Actions is ran
}
// unsupportedAssets contains assets that cannot handle
// normal processing for testing. This is to be used very sparingly
var unsupportedAssets = []asset.Item{
asset.Index,
}
var unsupportedExchangeNames = []string{
"testexch",
"alphapoint",
"bitflyer", // Bitflyer has many "ErrNotYetImplemented, which is true, but not what we care to test for here
"btse", // TODO rm once timeout issues resolved
"poloniex", // outdated API // TODO rm once updated
"coinbasepro", // outdated API // TODO rm once updated
}
// cryptoChainPerExchange holds the deposit address chain per exchange
var cryptoChainPerExchange = map[string]string{
"binanceus": "ERC20",
"bybit": "ERC20",
"gateio": "ERC20",
}
// acceptable errors do not throw test errors, see below for why
var acceptableErrors = []error{
common.ErrFunctionNotSupported, // Shows API cannot perform function and developer has recognised this
common.ErrNotYetImplemented, // Shows API can perform function but developer has not implemented it yet
asset.ErrNotSupported, // Shows that valid and invalid asset types are handled
request.ErrAuthRequestFailed, // We must set authenticated requests properly in order to understand and better handle auth failures
order.ErrUnsupportedOrderType, // Should be returned if an ordertype like ANY is requested and the implementation knows to throw this specific error
currency.ErrCurrencyPairEmpty, // Demonstrates handling of EMPTYPAIR scenario and returns the correct error
currency.ErrCurrencyNotSupported, // Ensures a standard error is used for when a particular currency/pair is not supported by an exchange
currency.ErrCurrencyNotFound, // Semi-randomly selected currency pairs may not be found at an endpoint, so long as this is returned it is okay
asset.ErrNotEnabled, // Allows distinction when checking for supported versus enabled
request.ErrRateLimiterAlreadyEnabled, // If the rate limiter is already enabled, it is not an error
context.DeadlineExceeded, // If the context deadline is exceeded, it is not an error as only blockedCIExchanges use expired contexts by design
order.ErrPairIsEmpty, // Is thrown when the empty pair and asset scenario for an order submission is sent in the Validate() function
deposit.ErrAddressNotFound, // Is thrown when an address is not found due to the exchange requiring valid API keys
futures.ErrNotFuturesAsset, // Is thrown when a futures function receives a non-futures asset
currency.ErrSymbolStringEmpty, // Is thrown when a symbol string is empty for blank MatchSymbol func checks
futures.ErrNotPerpetualFuture, // Is thrown when a futures function receives a non-perpetual future
order.ErrExchangeLimitNotLoaded, // Is thrown when the limits aren't loaded for a particular exchange, asset, pair
order.ErrCannotValidateAsset, // Is thrown when attempting to get order limits from an asset that is not yet loaded
order.ErrCannotValidateBaseCurrency, // Is thrown when attempting to get order limits from an base currency that is not yet loaded
order.ErrCannotValidateQuoteCurrency, // Is thrown when attempting to get order limits from an quote currency that is not yet loaded
account.ErrExchangeHoldingsNotFound,
ticker.ErrTickerNotFound,
orderbook.ErrOrderbookNotFound,
}
// warningErrors will t.Log(err) when thrown to diagnose things, but not necessarily suggest
// that the implementation is in error
var warningErrors = []error{
kline.ErrNoTimeSeriesDataToConvert, // No data returned for a candle isn't worth failing the test suite over necessarily
}
// getPairFromPairs prioritises more normal pairs for an increased
// likelihood of returning data from API endpoints
func getPairFromPairs(t *testing.T, p currency.Pairs) (currency.Pair, error) {
t.Helper()
pFmt, err := p.GetFormatting()
if err != nil {
return currency.Pair{}, err
}
goodEth := currency.NewPair(currency.ETH, currency.USDT).Format(pFmt)
if p.Contains(goodEth, true) {
return goodEth, nil
}
for i := range p {
if p[i].Base.Equal(currency.ETH) {
return p[i], nil
}
}
goodBtc := currency.NewPair(currency.BTC, currency.USDT).Format(pFmt)
if p.Contains(goodBtc, true) {
return goodBtc, nil
}
for i := range p {
if p[i].Base.Equal(currency.BTC) {
return p[i], nil
}
}
return p.GetRandomPair()
}
// isFiat helps determine fiat currency without using currency.storage
func isFiat(t *testing.T, c string) bool {
t.Helper()
var fiats = []string{
currency.USD.Item.Lower,
currency.AUD.Item.Lower,
currency.EUR.Item.Lower,
currency.CAD.Item.Lower,
currency.TRY.Item.Lower,
currency.UAH.Item.Lower,
currency.RUB.Item.Lower,
currency.RUR.Item.Lower,
currency.JPY.Item.Lower,
currency.HKD.Item.Lower,
currency.SGD.Item.Lower,
currency.ZUSD.Item.Lower,
currency.ZEUR.Item.Lower,
currency.ZCAD.Item.Lower,
currency.ZJPY.Item.Lower,
}
for i := range fiats {
if fiats[i] == c {
return true
}
}
return false
}
// disruptFormatting adds in an unused delimiter and strange casing features to
// ensure format currency pair is used throughout the code base.
func disruptFormatting(t *testing.T, p currency.Pair) (currency.Pair, error) {
t.Helper()
if p.Base.IsEmpty() {
return currency.EMPTYPAIR, errors.New("cannot disrupt formatting as base is not populated")
}
// NOTE: Quote can be empty for margin funding
return currency.Pair{
Base: p.Base.Upper(),
Quote: p.Quote.Lower(),
Delimiter: "-TEST-DELIM-",
}, nil
}
func getExchangeCredentials(exchangeName string) config.APICredentialsConfig {
var resp config.APICredentialsConfig
switch exchangeName {
case "lbank":
// these are just random keys, they are not usable
resp.Key = `MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3R2vuz3cpQUbCX0TgYZL
TiLSxUXdrvVEIyoqQyxNf+9fmHLEBrsO1s1msIKvWg24gdbLWXQ6NBCygO8OvZpm
+lfXD4MRv/0PxxIAkaD6Iplhv+qbae8nJkYQOpDJF3bPC9LCKfchCnRpZoGqkHgS
GqOBU13UDZ8BM1SaOLVBzcmE/iJCLPQPORNSzfLSb8TC+woe0AcaDmF9KjIzXPd0
Slacp1ZgZ+yIi1B5/akwxu6sGzHov6weXj/v9K8nUhL9+oPMd8FNzZ+z3viHY0fm
yWiHBywwlh4LgzrjGTUdUk9msjSr2rwjTdCp268A8ECC1fChvhdJfO3lYVj8ltDb
OQIDAQAB`
resp.Secret = `MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDdHa+7PdylBRsJ
fROBhktOItLFRd2u9UQjKipDLE1/71+YcsQGuw7WzWawgq9aDbiB1stZdDo0ELKA
7w69mmb6V9cPgxG//Q/HEgCRoPoimWG/6ptp7ycmRhA6kMkXds8L0sIp9yEKdGlm
gaqQeBIao4FTXdQNnwEzVJo4tUHNyYT+IkIs9A85E1LN8tJvxML7Ch7QBxoOYX0q
MjNc93RKVpynVmBn7IiLUHn9qTDG7qwbMei/rB5eP+/0rydSEv36g8x3wU3Nn7Pe
+IdjR+bJaIcHLDCWHguDOuMZNR1ST2ayNKvavCNN0KnbrwDwQILV8KG+F0l87eVh
WPyW0Ns5AgMBAAECggEAdBs7hJmWO7yzlsbrsC7BajUU8eue3VkCv2hLqtwfkdcz
HkzdLB+bSiWvD25//0yHHv6X5tAGJALEiLl+xwbFnhzz27xaXLLYTxLf45hg4Dwk
PO9HTlf6+bj+mpIeVcjYLYAs3nZbDi9UjTP3SUcTUpOavBjf2YstyTNai/55oEF/
x+ulzP/OISVhKrk5iiSKgjB4KyFpQnBWyluTmnlNS17/T/k6FkECQFgNpzbUmHTH
Yq+s0I9fGXMMvsNnnoJjX6ALe9fkMjY6ijeA45plDeBZp+5J8uGOKV+/iTCNzm5o
wrQKPz335+tTZgsDdKLUFA9Rwmkcpn4PShOtnR6aZQKBgQD9tzFlomqt/mSWbHAV
Gfjog9snlvgEWBIUjfP5Ow79rbz0cGcL3GAexwKK1dwNmMHDx+fu4uVAIf0dM5aT
xfdp/I4OTkxOFcIupu+L4gmz1vY32pFLPQYbp+9oOAMy4thUFb5o/Dsq2g65e2BC
+gNALEWxPuhNYbI7c0cu5Y7AJwKBgQDfG1ovhNlETJO+oli25csayRwgm/qll4fH
sOnYospQiJ3ka0WjPT6NY8m2anWDp7+/guIwq+xXVF6wQNxNZc+6/MgNJo2R3XG5
FKPH5FYgI52Zv6VN1AUhdfInDpKQXQ8vWO6HV+/uJmHeZK2+D6nycN4dL2h7ElK/
sCthmNtFnwKBgQDCGdaGpLzspAScOBV/b0FH0Shmn07bM+2RIBCYiaAsXzCB6URM
hKpcoW/Ge1pAZK9IcrVzws4URGx6XK9EGl3wDbE4LJqf2nGWc0wsPh+iIEB59pLV
drgnjFDR8Jgx4+4QVho4A0/Ytr4xFLxOQSsfez9OHIxoNue+J7E7pY+SXQKBgBTT
0tl4x2eO1oQHV8zLKui3OX750K5AtRY5N7tXhxd5iXPXZ8rTXtGILT5wNcQylr3k
FAWDJy8H20cM5wP6qyfDjVFc9f5V89XZTWjNshSR/pZpw56+WjRDdHWc8KW1akN7
Q9kypl1PC/fc4jNJ9w2A59tFn7VNgpgOdB5KTL31AoGBAN3BIjKXzoOJnVGL3bja
SYC2m+JcRn/mVO7I5Hop8GDoWXPFAnPNx1YKSpRLM/EV+ukUJsOV/LTPb7BsXMsJ
IY9SZceJS6glsxt+blFxGEpypyv13xW+jeCrPjlxQX2TNbL0KwHqvm1zMnM9bss/
Rsd80LrBCVI8ctzrvYRFSugC`
default:
resp.Key = "realKey"
resp.Secret = "YXBpU2VjcmV0" // base64 encoded "apiSecret"
resp.ClientID = "realClientID"
}
return resp
}
func isCITest() bool {
return os.Getenv("CI") == "true"
}
func skipAdditionalWrapperCITests() bool {
return os.Getenv("SKIP_WRAPPER_CI_TESTS") == "true"
}