exchanges/engine: Add multichain deposit/withdrawal support (#794)

* Add exchange multichain support

* Start tidying up

* Add multichain transfer support for Bitfinex and fix poloniex bug

* Add Coinbene multichain support

* Start adjusting the deposit address manager

* Fix deposit tests and further enhancements

* Cleanup

* Add bypass flag, expand tests plus error coverage for Huobi

Adjust helpers

* Address nitterinos

* BFX wd changes

* Address nitterinos

* Minor fixes rebasing on master

* Fix BFX acceptableMethods test

* Add some TO-DOs for 2 tests WRT races

* Fix acceptableMethods test round 2

* Address nitterinos
This commit is contained in:
Adrian Gallagher
2021-10-15 15:55:38 +11:00
committed by GitHub
parent b093a7df19
commit 0c00b7e1df
145 changed files with 46329 additions and 5507 deletions

View File

@@ -6,6 +6,7 @@ import (
"errors"
"fmt"
"net/http"
"net/url"
"strconv"
"strings"
@@ -521,53 +522,64 @@ func getCryptocurrencyWithdrawalFee(c currency.Code) float64 {
}
// WithdrawCrypto withdraws cryptocurrency to your selected wallet
func (g *Gateio) WithdrawCrypto(ctx context.Context, currency, address string, amount float64) (*withdraw.ExchangeResponse, error) {
type response struct {
func (g *Gateio) WithdrawCrypto(ctx context.Context, curr, address, memo, chain string, amount float64) (*withdraw.ExchangeResponse, error) {
if curr == "" || address == "" || amount <= 0 {
return nil, errors.New("currency, address and amount must be set")
}
resp := struct {
Result bool `json:"result"`
Message string `json:"message"`
Code int `json:"code"`
}{}
vals := url.Values{}
vals.Set("currency", strings.ToUpper(curr))
vals.Set("amount", strconv.FormatFloat(amount, 'f', -1, 64))
// Transaction MEMO has to be entered after the address separated by a space
if memo != "" {
address += " " + memo
}
vals.Set("address", address)
if chain != "" {
vals.Set("chain", strings.ToUpper(chain))
}
var result response
params := fmt.Sprintf("currency=%v&amount=%v&address=%v",
currency,
address,
amount,
)
err := g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, gateioWithdraw, params, &result)
err := g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, gateioWithdraw, vals.Encode(), &resp)
if err != nil {
return nil, err
}
if !result.Result {
return nil, fmt.Errorf("code:%d message:%s", result.Code, result.Message)
if !resp.Result {
return nil, fmt.Errorf("code:%d message:%s", resp.Code, resp.Message)
}
return &withdraw.ExchangeResponse{
Status: result.Message,
Status: resp.Message,
}, nil
}
// GetCryptoDepositAddress returns a deposit address for a cryptocurrency
func (g *Gateio) GetCryptoDepositAddress(ctx context.Context, currency string) (string, error) {
type response struct {
Result bool `json:"result,string"`
Code int `json:"code"`
Message string `json:"message"`
Address string `json:"addr"`
}
var result response
func (g *Gateio) GetCryptoDepositAddress(ctx context.Context, curr string) (*DepositAddr, error) {
var result DepositAddr
params := fmt.Sprintf("currency=%s",
currency)
curr)
err := g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, gateioDepositAddress, params, &result)
if err != nil {
return "", err
return nil, err
}
if !result.Result {
return "", fmt.Errorf("code:%d message:%s", result.Code, result.Message)
return nil, fmt.Errorf("code:%d message:%s", result.Code, result.Message)
}
return result.Address, nil
// For memo/payment ID currencies
if strings.Contains(result.Address, " ") {
split := strings.Split(result.Address, " ")
result.Address = split[0]
result.Tag = split[1]
}
return &result, nil
}

View File

@@ -423,6 +423,7 @@ func TestModifyOrder(t *testing.T) {
func TestWithdraw(t *testing.T) {
withdrawCryptoRequest := withdraw.Request{
Exchange: g.Name,
Amount: -1,
Currency: currency.BTC,
Description: "WITHDRAW IT ALL",
@@ -472,12 +473,12 @@ func TestWithdrawInternationalBank(t *testing.T) {
func TestGetDepositAddress(t *testing.T) {
if areTestAPIKeysSet() {
_, err := g.GetDepositAddress(context.Background(), currency.ETC, "")
_, err := g.GetDepositAddress(context.Background(), currency.USDT, "", "TRX")
if err != nil {
t.Error("Test Fail - GetDepositAddress error", err)
}
} else {
_, err := g.GetDepositAddress(context.Background(), currency.ETC, "")
_, err := g.GetDepositAddress(context.Background(), currency.ETC, "", "")
if err == nil {
t.Error("Test Fail - GetDepositAddress error cannot be nil")
}
@@ -867,3 +868,25 @@ func TestUpdateTickers(t *testing.T) {
t.Error(err)
}
}
func TestGetCryptoDepositAddress(t *testing.T) {
t.Parallel()
if !areTestAPIKeysSet() {
t.Skip("api keys not set")
}
_, err := g.GetCryptoDepositAddress(context.Background(), currency.USDT.String())
if err != nil {
t.Error(err)
}
}
func TestGetAvailableTransferTrains(t *testing.T) {
t.Parallel()
if !areTestAPIKeysSet() {
t.Skip("api keys not set")
}
_, err := g.GetAvailableTransferChains(context.Background(), currency.USDT)
if err != nil {
t.Error(err)
}
}

View File

@@ -532,3 +532,19 @@ type wsOrderbook struct {
Bids [][]string `json:"bids"`
ID int64 `json:"id"`
}
// DepositAddr stores the deposit address info
type DepositAddr struct {
Result bool `json:"result,string"`
Code int `json:"code"`
Message string `json:"message"`
Address string `json:"addr"`
Tag string
MultichainAddresses []struct {
Chain string `json:"chain"`
Address string `json:"address"`
PaymentID string `json:"payment_id"`
PaymentName string `json:"payment_name"`
ObtainFailed uint8 `json:"obtain_failed"`
} `json:"multichain_addresses"`
}

View File

@@ -17,6 +17,7 @@ import (
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/deposit"
"github.com/thrasher-corp/gocryptotrader/exchanges/kline"
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
"github.com/thrasher-corp/gocryptotrader/exchanges/orderbook"
@@ -72,23 +73,25 @@ func (g *Gateio) SetDefaults() {
REST: true,
Websocket: true,
RESTCapabilities: protocol.Features{
TickerBatching: true,
TickerFetching: true,
KlineFetching: true,
TradeFetching: true,
OrderbookFetching: true,
AutoPairUpdates: true,
AccountInfo: true,
GetOrder: true,
GetOrders: true,
CancelOrders: true,
CancelOrder: true,
SubmitOrder: true,
UserTradeHistory: true,
CryptoDeposit: true,
CryptoWithdrawal: true,
TradeFee: true,
CryptoWithdrawalFee: true,
TickerBatching: true,
TickerFetching: true,
KlineFetching: true,
TradeFetching: true,
OrderbookFetching: true,
AutoPairUpdates: true,
AccountInfo: true,
GetOrder: true,
GetOrders: true,
CancelOrders: true,
CancelOrder: true,
SubmitOrder: true,
UserTradeHistory: true,
CryptoDeposit: true,
CryptoWithdrawal: true,
TradeFee: true,
CryptoWithdrawalFee: true,
MultiChainDeposits: true,
MultiChainWithdrawals: true,
},
WebsocketCapabilities: protocol.Features{
TickerFetching: true,
@@ -626,17 +629,32 @@ func (g *Gateio) GetOrderInfo(ctx context.Context, orderID string, pair currency
}
// GetDepositAddress returns a deposit address for a specified currency
func (g *Gateio) GetDepositAddress(ctx context.Context, cryptocurrency currency.Code, _ string) (string, error) {
func (g *Gateio) GetDepositAddress(ctx context.Context, cryptocurrency currency.Code, _, chain string) (*deposit.Address, error) {
addr, err := g.GetCryptoDepositAddress(ctx, cryptocurrency.String())
if err != nil {
return "", err
return nil, err
}
if addr == gateioGenerateAddress {
return "",
if addr.Address == gateioGenerateAddress {
return nil,
errors.New("new deposit address is being generated, please retry again shortly")
}
return addr, nil
if chain != "" {
for x := range addr.MultichainAddresses {
if strings.EqualFold(addr.MultichainAddresses[x].Chain, chain) {
return &deposit.Address{
Address: addr.MultichainAddresses[x].Address,
Tag: addr.MultichainAddresses[x].PaymentName,
}, nil
}
}
return nil, fmt.Errorf("network %s not found", chain)
}
return &deposit.Address{
Address: addr.Address,
Tag: addr.Tag,
}, nil
}
// WithdrawCryptocurrencyFunds returns a withdrawal ID when a withdrawal is
@@ -648,7 +666,10 @@ func (g *Gateio) WithdrawCryptocurrencyFunds(ctx context.Context, withdrawReques
return g.WithdrawCrypto(ctx,
withdrawRequest.Currency.String(),
withdrawRequest.Crypto.Address,
withdrawRequest.Amount)
withdrawRequest.Crypto.AddressTag,
withdrawRequest.Crypto.Chain,
withdrawRequest.Amount,
)
}
// WithdrawFiatFunds returns a withdrawal ID when a
@@ -865,3 +886,18 @@ func (g *Gateio) GetHistoricCandles(ctx context.Context, pair currency.Pair, a a
func (g *Gateio) GetHistoricCandlesExtended(ctx context.Context, pair currency.Pair, a asset.Item, start, end time.Time, interval kline.Interval) (kline.Item, error) {
return g.GetHistoricCandles(ctx, pair, a, start, end, interval)
}
// GetAvailableTransferChains returns the available transfer blockchains for the specific
// cryptocurrency
func (g *Gateio) GetAvailableTransferChains(ctx context.Context, cryptocurrency currency.Code) ([]string, error) {
chains, err := g.GetCryptoDepositAddress(ctx, cryptocurrency.String())
if err != nil {
return nil, err
}
var availableChains []string
for x := range chains.MultichainAddresses {
availableChains = append(availableChains, chains.MultichainAddresses[x].Chain)
}
return availableChains, nil
}