mirror of
https://github.com/d0zingcat/gocryptotrader.git
synced 2026-05-14 07:26:47 +00:00
FTX: Add REST subaccount support (#653)
* FTX: Add REST API subaccount support * Add API key check to GetSubaccounts * Fix missing comment and expand to rest of the codebase * Address glorious nits * Address various nits * Fix ZB typo https://www.zb.com/api#hsptccieyyqomlp
This commit is contained in:
@@ -39,7 +39,8 @@ var (
|
||||
// ErrNilArguments is a common error response to highlight that nils were passed in
|
||||
// when they should not have been
|
||||
ErrNilArguments = errors.New("received nil argument(s)")
|
||||
ErrNilEvent = errors.New("nil event received")
|
||||
// ErrNilEvent is a common error for whenever a nil event occurs when it shouldn't have
|
||||
ErrNilEvent = errors.New("nil event received")
|
||||
)
|
||||
|
||||
// EventHandler interface implements required GetTime() & Pair() return
|
||||
|
||||
@@ -2,6 +2,7 @@ package base
|
||||
|
||||
import "errors"
|
||||
|
||||
// Error vars related to strategies and invalid config settings
|
||||
var (
|
||||
ErrCustomSettingsUnsupported = errors.New("custom settings not supported")
|
||||
ErrSimultaneousProcessingNotSupported = errors.New("does not support simultaneous processing and could not be loaded")
|
||||
|
||||
@@ -46,7 +46,7 @@ type Storage struct {
|
||||
// FiatExchangeMarkets defines an interface to access FX data for fiat
|
||||
// currency rates
|
||||
fiatExchangeMarkets *forexprovider.ForexProviders
|
||||
// CurrencyAnalysis defines a full market analysis suite to receieve and
|
||||
// CurrencyAnalysis defines a full market analysis suite to receive and
|
||||
// define different fiat currencies, cryptocurrencies and markets
|
||||
currencyAnalysis *coinmarketcap.Coinmarketcap
|
||||
// Path defines the main folder to dump and find currency JSON
|
||||
|
||||
@@ -73,21 +73,21 @@ func TestHoldings(t *testing.T) {
|
||||
}
|
||||
|
||||
if u.Accounts[0].ID != "1337" {
|
||||
t.Errorf("expecting 1337 but receieved %s", u.Accounts[0].ID)
|
||||
t.Errorf("expecting 1337 but received %s", u.Accounts[0].ID)
|
||||
}
|
||||
|
||||
if u.Accounts[0].Currencies[0].CurrencyName != currency.BTC {
|
||||
t.Errorf("expecting BTC but receieved %s",
|
||||
t.Errorf("expecting BTC but received %s",
|
||||
u.Accounts[0].Currencies[0].CurrencyName)
|
||||
}
|
||||
|
||||
if u.Accounts[0].Currencies[0].TotalValue != 100 {
|
||||
t.Errorf("expecting 100 but receieved %f",
|
||||
t.Errorf("expecting 100 but received %f",
|
||||
u.Accounts[0].Currencies[0].TotalValue)
|
||||
}
|
||||
|
||||
if u.Accounts[0].Currencies[0].Hold != 20 {
|
||||
t.Errorf("expecting 20 but receieved %f",
|
||||
t.Errorf("expecting 20 but received %f",
|
||||
u.Accounts[0].Currencies[0].Hold)
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrNotSupported is an error for an unsupported asset type
|
||||
ErrNotSupported = errors.New("received unsupported asset type")
|
||||
)
|
||||
|
||||
|
||||
@@ -2459,12 +2459,12 @@ func TestSetExchangeOrderExecutionLimits(t *testing.T) {
|
||||
|
||||
err = limit.Conforms(0.000001, 0.1, order.Limit)
|
||||
if !errors.Is(err, order.ErrAmountBelowMin) {
|
||||
t.Fatalf("expected %v, but receieved %v", order.ErrAmountBelowMin, err)
|
||||
t.Fatalf("expected %v, but received %v", order.ErrAmountBelowMin, err)
|
||||
}
|
||||
|
||||
err = limit.Conforms(0.01, 1, order.Limit)
|
||||
if !errors.Is(err, order.ErrPriceBelowMin) {
|
||||
t.Fatalf("expected %v, but receieved %v", order.ErrPriceBelowMin, err)
|
||||
t.Fatalf("expected %v, but received %v", order.ErrPriceBelowMin, err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
|
||||
"github.com/thrasher-corp/gocryptotrader/common"
|
||||
"github.com/thrasher-corp/gocryptotrader/common/crypto"
|
||||
"github.com/thrasher-corp/gocryptotrader/currency"
|
||||
exchange "github.com/thrasher-corp/gocryptotrader/exchanges"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/request"
|
||||
@@ -90,6 +91,10 @@ const (
|
||||
requestOTCQuote = "/otc/quotes"
|
||||
getOTCQuoteStatus = "/otc/quotes/"
|
||||
acceptOTCQuote = "/otc/quotes/%s/accept"
|
||||
subaccounts = "/subaccounts"
|
||||
subaccountsUpdateName = "/subaccounts/update_name"
|
||||
subaccountsBalance = "/subaccounts/%s/balances"
|
||||
subaccountsTransfer = "/subaccounts/transfer"
|
||||
|
||||
// Margin Endpoints
|
||||
marginBorrowRates = "/spot_margin/borrow_rates"
|
||||
@@ -114,7 +119,12 @@ const (
|
||||
)
|
||||
|
||||
var (
|
||||
errStartTimeCannotBeAfterEndTime = errors.New("start timestamp cannot be after end timestamp")
|
||||
errStartTimeCannotBeAfterEndTime = errors.New("start timestamp cannot be after end timestamp")
|
||||
errSubaccountNameMustBeSpecified = errors.New("a subaccount name must be specified")
|
||||
errSubaccountUpdateNameInvalid = errors.New("invalid subaccount old/new name")
|
||||
errCoinMustBeSpecified = errors.New("a coin must be specified")
|
||||
errSubaccountTransferSizeGreaterThanZero = errors.New("transfer size must be greater than 0")
|
||||
errSubaccountTransferSourceDestinationMustNotBeEqual = errors.New("subaccount transfer source and destination must not be the same value")
|
||||
)
|
||||
|
||||
// GetMarkets gets market data
|
||||
@@ -403,17 +413,17 @@ func (f *FTX) GetCoins() ([]WalletCoinsData, error) {
|
||||
}
|
||||
|
||||
// GetBalances gets balances of the account
|
||||
func (f *FTX) GetBalances() ([]BalancesData, error) {
|
||||
func (f *FTX) GetBalances() ([]WalletBalance, error) {
|
||||
resp := struct {
|
||||
Data []BalancesData `json:"result"`
|
||||
Data []WalletBalance `json:"result"`
|
||||
}{}
|
||||
return resp.Data, f.SendAuthHTTPRequest(exchange.RestSpot, http.MethodGet, getBalances, nil, &resp)
|
||||
}
|
||||
|
||||
// GetAllWalletBalances gets all wallets' balances
|
||||
func (f *FTX) GetAllWalletBalances() (AllWalletAccountData, error) {
|
||||
func (f *FTX) GetAllWalletBalances() (AllWalletBalances, error) {
|
||||
resp := struct {
|
||||
Data AllWalletAccountData `json:"result"`
|
||||
Data AllWalletBalances `json:"result"`
|
||||
}{}
|
||||
return resp.Data, f.SendAuthHTTPRequest(exchange.RestSpot, http.MethodGet, getAllWalletBalances, nil, &resp)
|
||||
}
|
||||
@@ -1105,3 +1115,104 @@ func (f *FTX) GetOTCQuoteStatus(marketName, quoteID string) ([]QuoteStatusData,
|
||||
func (f *FTX) AcceptOTCQuote(quoteID string) error {
|
||||
return f.SendAuthHTTPRequest(exchange.RestSpot, http.MethodPost, fmt.Sprintf(acceptOTCQuote, quoteID), nil, nil)
|
||||
}
|
||||
|
||||
// GetSubaccounts returns the users subaccounts
|
||||
func (f *FTX) GetSubaccounts() ([]Subaccount, error) {
|
||||
resp := struct {
|
||||
Data []Subaccount `json:"result"`
|
||||
}{}
|
||||
return resp.Data, f.SendAuthHTTPRequest(exchange.RestSpot, http.MethodGet, subaccounts, nil, &resp)
|
||||
}
|
||||
|
||||
// CreateSubaccount creates a new subaccount
|
||||
func (f *FTX) CreateSubaccount(name string) (*Subaccount, error) {
|
||||
if name == "" {
|
||||
return nil, errSubaccountNameMustBeSpecified
|
||||
}
|
||||
d := make(map[string]string)
|
||||
d["nickname"] = name
|
||||
|
||||
resp := struct {
|
||||
Data Subaccount `json:"result"`
|
||||
}{}
|
||||
if err := f.SendAuthHTTPRequest(exchange.RestSpot, http.MethodPost, subaccounts, d, &resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &resp.Data, nil
|
||||
}
|
||||
|
||||
// UpdateSubaccountName updates an existing subaccount name
|
||||
func (f *FTX) UpdateSubaccountName(oldName, newName string) (*Subaccount, error) {
|
||||
if oldName == "" || newName == "" || oldName == newName {
|
||||
return nil, errSubaccountUpdateNameInvalid
|
||||
}
|
||||
d := make(map[string]string)
|
||||
d["nickname"] = oldName
|
||||
d["newNickname"] = newName
|
||||
|
||||
resp := struct {
|
||||
Data Subaccount `json:"result"`
|
||||
}{}
|
||||
if err := f.SendAuthHTTPRequest(exchange.RestSpot, http.MethodPost, subaccountsUpdateName, d, &resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &resp.Data, nil
|
||||
}
|
||||
|
||||
// DeleteSubaccountName deletes the specified subaccount name
|
||||
func (f *FTX) DeleteSubaccount(name string) error {
|
||||
if name == "" {
|
||||
return errSubaccountNameMustBeSpecified
|
||||
}
|
||||
d := make(map[string]string)
|
||||
d["nickname"] = name
|
||||
resp := struct {
|
||||
Data Subaccount `json:"result"`
|
||||
}{}
|
||||
return f.SendAuthHTTPRequest(exchange.RestSpot, http.MethodDelete, subaccounts, d, &resp)
|
||||
}
|
||||
|
||||
// SubaccountBalances returns the user's subaccount balances
|
||||
func (f *FTX) SubaccountBalances(name string) ([]SubaccountBalance, error) {
|
||||
if name == "" {
|
||||
return nil, errSubaccountNameMustBeSpecified
|
||||
}
|
||||
resp := struct {
|
||||
Data []SubaccountBalance `json:"result"`
|
||||
}{}
|
||||
if err := f.SendAuthHTTPRequest(exchange.RestSpot, http.MethodGet, fmt.Sprintf(subaccountsBalance, name), nil, &resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp.Data, nil
|
||||
}
|
||||
|
||||
// SubaccountTransfer transfers a desired coin to the specified subaccount
|
||||
func (f *FTX) SubaccountTransfer(coin currency.Code, source, destination string, size float64) (*SubaccountTransferStatus, error) {
|
||||
if coin.IsEmpty() {
|
||||
return nil, errCoinMustBeSpecified
|
||||
}
|
||||
if size <= 0 {
|
||||
return nil, errSubaccountTransferSizeGreaterThanZero
|
||||
}
|
||||
if source == destination {
|
||||
return nil, errSubaccountTransferSourceDestinationMustNotBeEqual
|
||||
}
|
||||
d := make(map[string]interface{})
|
||||
d["coin"] = coin.Upper().String()
|
||||
d["size"] = size
|
||||
if source == "" {
|
||||
source = "main"
|
||||
}
|
||||
d["source"] = source
|
||||
if destination == "" {
|
||||
destination = "main"
|
||||
}
|
||||
d["destination"] = destination
|
||||
resp := struct {
|
||||
Data SubaccountTransferStatus `json:"result"`
|
||||
}{}
|
||||
if err := f.SendAuthHTTPRequest(exchange.RestSpot, http.MethodPost, subaccountsTransfer, d, &resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &resp.Data, nil
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package ftx
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log"
|
||||
"os"
|
||||
"reflect"
|
||||
@@ -1450,3 +1451,126 @@ func TestParsingWSOBData2(t *testing.T) {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetSubaccounts(t *testing.T) {
|
||||
t.Parallel()
|
||||
if !areTestAPIKeysSet() {
|
||||
t.Skip("skipping test, api keys not set")
|
||||
}
|
||||
_, err := f.GetSubaccounts()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateSubaccount(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, err := f.CreateSubaccount("")
|
||||
if !errors.Is(err, errSubaccountNameMustBeSpecified) {
|
||||
t.Errorf("expected %v, but received: %s", errSubaccountNameMustBeSpecified, err)
|
||||
}
|
||||
|
||||
if !areTestAPIKeysSet() || !canManipulateRealOrders {
|
||||
t.Skip("skipping test, either api keys or canManipulateRealOrders isn't set")
|
||||
}
|
||||
_, err = f.CreateSubaccount("subzero")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err = f.DeleteSubaccount("subzero"); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateSubaccountName(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, err := f.UpdateSubaccountName("", "")
|
||||
if !errors.Is(err, errSubaccountUpdateNameInvalid) {
|
||||
t.Errorf("expected %v, but received: %s", errSubaccountUpdateNameInvalid, err)
|
||||
}
|
||||
|
||||
if !areTestAPIKeysSet() || !canManipulateRealOrders {
|
||||
t.Skip("skipping test, either api keys or canManipulateRealOrders isn't set")
|
||||
}
|
||||
_, err = f.CreateSubaccount("subzero")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = f.UpdateSubaccountName("subzero", "bizzlebot")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := f.DeleteSubaccount("bizzlebot"); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteSubaccountName(t *testing.T) {
|
||||
t.Parallel()
|
||||
if err := f.DeleteSubaccount(""); !errors.Is(err, errSubaccountNameMustBeSpecified) {
|
||||
t.Errorf("expected %v, but received: %s", errSubaccountNameMustBeSpecified, err)
|
||||
}
|
||||
if !areTestAPIKeysSet() || !canManipulateRealOrders {
|
||||
t.Skip("skipping test, either api keys or canManipulateRealOrders isn't set")
|
||||
}
|
||||
_, err := f.CreateSubaccount("subzero")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := f.DeleteSubaccount("subzero"); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubaccountBalances(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, err := f.SubaccountBalances("")
|
||||
if !errors.Is(err, errSubaccountNameMustBeSpecified) {
|
||||
t.Errorf("expected %s, but received: %s", errSubaccountNameMustBeSpecified, err)
|
||||
}
|
||||
if !areTestAPIKeysSet() {
|
||||
t.Skip("skipping test, api keys not set")
|
||||
}
|
||||
_, err = f.SubaccountBalances("non-existent")
|
||||
if err == nil {
|
||||
t.Error("expecting non-existent subaccount to return an error")
|
||||
}
|
||||
_, err = f.CreateSubaccount("subzero")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = f.SubaccountBalances("subzero")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if err := f.DeleteSubaccount("subzero"); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubaccountTransfer(t *testing.T) {
|
||||
tt := []struct {
|
||||
Coin currency.Code
|
||||
Source string
|
||||
Destination string
|
||||
Size float64
|
||||
ErrExpected error
|
||||
}{
|
||||
{ErrExpected: errCoinMustBeSpecified},
|
||||
{Coin: currency.BTC, ErrExpected: errSubaccountTransferSizeGreaterThanZero},
|
||||
{Coin: currency.BTC, Size: 420, ErrExpected: errSubaccountTransferSourceDestinationMustNotBeEqual},
|
||||
}
|
||||
for x := range tt {
|
||||
_, err := f.SubaccountTransfer(tt[x].Coin, tt[x].Source, tt[x].Destination, tt[x].Size)
|
||||
if !errors.Is(err, tt[x].ErrExpected) {
|
||||
t.Errorf("expected %s, but received: %s", tt[x].ErrExpected, err)
|
||||
}
|
||||
}
|
||||
if !areTestAPIKeysSet() || !canManipulateRealOrders {
|
||||
t.Skip("skipping test, either api keys or canManipulateRealOrders isn't set")
|
||||
}
|
||||
_, err := f.SubaccountTransfer(currency.BTC, "", "test", 0.1)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -234,18 +234,18 @@ type WalletCoinsData struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// BalancesData stores balances data
|
||||
type BalancesData struct {
|
||||
Coin string `json:"coin"`
|
||||
Free float64 `json:"free"`
|
||||
Total float64 `json:"total"`
|
||||
// WalletBalance stores balances data
|
||||
type WalletBalance struct {
|
||||
Coin string `json:"coin"`
|
||||
Free float64 `json:"free"`
|
||||
Total float64 `json:"total"`
|
||||
AvailableWithoutBorrow float64 `json:"availableWithoutBorrow"`
|
||||
USDValue float64 `json:"usdValue"`
|
||||
SpotBorrow float64 `json:"spotBorrow"`
|
||||
}
|
||||
|
||||
// AllWalletAccountData stores account data on all WalletCoins
|
||||
type AllWalletAccountData struct {
|
||||
Main []BalancesData `json:"main"`
|
||||
BattleRoyale []BalancesData `json:"Battle Royale"`
|
||||
}
|
||||
// AllWalletBalances stores all the user's account balances
|
||||
type AllWalletBalances map[string][]WalletBalance
|
||||
|
||||
// DepositData stores deposit address data
|
||||
type DepositData struct {
|
||||
@@ -771,3 +771,31 @@ type QuoteStatusData struct {
|
||||
type AcceptQuote struct {
|
||||
Success bool `json:"success"`
|
||||
}
|
||||
|
||||
// Subaccount stores subaccount data
|
||||
type Subaccount struct {
|
||||
Nickname string `json:"nickname"`
|
||||
Special bool `json:"special"`
|
||||
Deletable bool `json:"deletable"`
|
||||
Editable bool `json:"editable"`
|
||||
Competition bool `json:"competition"`
|
||||
}
|
||||
|
||||
// SubaccountBalance stores the user's subaccount balance
|
||||
type SubaccountBalance struct {
|
||||
Coin string `json:"coin"`
|
||||
Free float64 `json:"free"`
|
||||
Total float64 `json:"total"`
|
||||
SpotBorrow float64 `json:"spotBorrow"`
|
||||
AvailableWithoutBorrow float64 `json:"availableWithoutBorrow"`
|
||||
}
|
||||
|
||||
// SubaccountTransferStatus stores the subaccount transfer details
|
||||
type SubaccountTransferStatus struct {
|
||||
ID int64 `json:"id"`
|
||||
Coin string `json:"coin"`
|
||||
Size float64 `json:"size"`
|
||||
Time time.Time `json:"time"`
|
||||
Notes string `json:"notes"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
@@ -444,7 +444,7 @@ func (h *IntervalRangeHolder) VerifyResultsHaveData(c []Candle) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Set is a simple helper function to set the time twice
|
||||
// CreateIntervalTime is a simple helper function to set the time twice
|
||||
func CreateIntervalTime(tt time.Time) IntervalTime {
|
||||
return IntervalTime{
|
||||
Time: tt,
|
||||
|
||||
@@ -39,6 +39,7 @@ const (
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrMissingCandleData is an error for missing candle data
|
||||
ErrMissingCandleData = errors.New("missing candle data")
|
||||
// SupportedIntervals is a list of all supported intervals
|
||||
SupportedIntervals = []Interval{
|
||||
|
||||
@@ -534,7 +534,7 @@ type BatchOrderData struct {
|
||||
Status string `json:"status"`
|
||||
OrderTag string `json:"order_tag"`
|
||||
OrderID string `json:"order_id"`
|
||||
DateTimeReceived string `json:"dateTimeReceieved"`
|
||||
DateTimeReceived string `json:"dateTimeReceived"`
|
||||
OrderEvents []struct {
|
||||
OrderPlaced FuturesOrderData `json:"orderPlaced"`
|
||||
ReduceOnly bool `json:"reduceOnly"`
|
||||
|
||||
@@ -859,7 +859,7 @@ func TestOrderBookPartialChecksumCalculator(t *testing.T) {
|
||||
|
||||
calculatedChecksum := o.CalculatePartialOrderbookChecksum(&dataResponse)
|
||||
if calculatedChecksum != dataResponse.Checksum {
|
||||
t.Errorf("Expected %v, Receieved %v", dataResponse.Checksum, calculatedChecksum)
|
||||
t.Errorf("Expected %v, received %v", dataResponse.Checksum, calculatedChecksum)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1672,7 +1672,7 @@ func TestOrderBookPartialChecksumCalculator(t *testing.T) {
|
||||
|
||||
calculatedChecksum := o.CalculatePartialOrderbookChecksum(&dataResponse)
|
||||
if calculatedChecksum != dataResponse.Checksum {
|
||||
t.Errorf("Expected %v, Receieved %v", dataResponse.Checksum, calculatedChecksum)
|
||||
t.Errorf("Expected %v, received %v", dataResponse.Checksum, calculatedChecksum)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -436,7 +436,7 @@ func (z *ZB) Withdraw(currency, address, safepassword string, amount, fees float
|
||||
vals.Set("fees", strconv.FormatFloat(fees, 'f', -1, 64))
|
||||
vals.Set("itransfer", strconv.FormatBool(itransfer))
|
||||
vals.Set("method", "withdraw")
|
||||
vals.Set("recieveAddr", address)
|
||||
vals.Set("receiveAddr", address)
|
||||
vals.Set("safePwd", safepassword)
|
||||
|
||||
var resp response
|
||||
|
||||
@@ -254,7 +254,7 @@ func TestValidateFiat(t *testing.T) {
|
||||
err := test.request.Validate(test.validate)
|
||||
if err != nil {
|
||||
if test.output.(error).Error() != err.Error() {
|
||||
t.Fatalf("Test Name %s expecting error [%s] but receieved [%s]", test.name, test.output.(error).Error(), err)
|
||||
t.Fatalf("Test Name %s expecting error [%s] but received [%s]", test.name, test.output.(error).Error(), err)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user