Files
gocryptotrader/cmd/exchange_wrapper_standards/exchange_wrapper_standards_test.go
Gareth Kirwan b0fa9c9881 cmd/exchange_wrapper_standards: Fix race on t.Context in exchange wrapper tests
* Tests: Fix race on t.Context in exchange wrapper tests

* fixup! Tests: Fix race on t.Context in exchange wrapper tests
2025-08-14 12:58:14 +10:00

783 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"
"github.com/thrasher-corp/gocryptotrader/exchange/websocket"
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 := t.Context()
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(ctx, 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)
require.Truef(t, errors.Is(err, context.DeadlineExceeded) || err == nil, "Exchange %s UpdateTradablePairs must not error: %s", name, err)
b := exch.GetBase()
assets := b.CurrencyPairs.GetAssetTypes(false)
require.NotEmptyf(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 asset %s: %s", name, a, err)
}
// 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)
require.Truef(t, errors.Is(err, currency.ErrPairAlreadyEnabled) || err == nil, "Exchange %s EnablePair must not error for %s", name, p)
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
}
var validWrapperParams = []reflect.Type{
assetParam,
orderSubmitParam,
orderModifyParam,
orderCancelParam,
orderCancelsParam,
pairKeySliceParam,
getOrdersRequestParam,
latestRateRequest,
}
type testCtxKey string
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)
if slices.ContainsFunc(validWrapperParams, func(t reflect.Type) bool {
return input.AssignableTo(t)
}) {
assetLen = len(assetParams)
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 := range assetLen {
ap := assetParams[y]
t.Run(methodName+"-"+ap.Asset.String()+"-"+ap.Pair.String(), func(t *testing.T) {
t.Parallel()
// Create a new context for each test run to avoid race conditions
ctx := context.WithValue(ctx, testCtxKey("test"), t.Name()) //nolint:govet // Intentional shadow
inputs := make([]reflect.Value, method.Type().NumIn())
argGenerator := &MethodArgumentGenerator{
Exchange: exch,
AssetParams: ap,
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
}
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([]any, len(methodValues))
for j := range methodValues {
switch {
case methodValues[j].Type().Implements(contextParam):
// Errorf will use reflection on ctx and cause a race, so we need to replace it
literalInputs[j] = "<context>"
case methodValues[j].Kind() == reflect.Ptr:
// dereference pointers just to add a bit more clarity
literalInputs[j] = methodValues[j].Elem().Interface()
default:
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",
TimeInForce: order.ImmediateOrCancel,
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",
TimeInForce: order.ImmediateOrCancel,
})
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",
"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,
websocket.ErrNotConnected,
}
// 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.NewBTCUSDT().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()
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,
}
return slices.Contains(fiats, c)
}
// 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"
}