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

@@ -21,25 +21,27 @@ const (
exmoAPIURL = "https://api.exmo.com"
exmoAPIVersion = "1"
exmoTrades = "trades"
exmoOrderbook = "order_book"
exmoTicker = "ticker"
exmoPairSettings = "pair_settings"
exmoCurrency = "currency"
exmoUserInfo = "user_info"
exmoOrderCreate = "order_create"
exmoOrderCancel = "order_cancel"
exmoOpenOrders = "user_open_orders"
exmoUserTrades = "user_trades"
exmoCancelledOrders = "user_cancelled_orders"
exmoOrderTrades = "order_trades"
exmoRequiredAmount = "required_amount"
exmoDepositAddress = "deposit_address"
exmoWithdrawCrypt = "withdraw_crypt"
exmoGetWithdrawTXID = "withdraw_get_txid"
exmoExcodeCreate = "excode_create"
exmoExcodeLoad = "excode_load"
exmoWalletHistory = "wallet_history"
exmoTrades = "trades"
exmoOrderbook = "order_book"
exmoTicker = "ticker"
exmoPairSettings = "pair_settings"
exmoCurrency = "currency"
exmoUserInfo = "user_info"
exmoOrderCreate = "order_create"
exmoOrderCancel = "order_cancel"
exmoOpenOrders = "user_open_orders"
exmoUserTrades = "user_trades"
exmoCancelledOrders = "user_cancelled_orders"
exmoOrderTrades = "order_trades"
exmoRequiredAmount = "required_amount"
exmoDepositAddress = "deposit_address"
exmoWithdrawCrypt = "withdraw_crypt"
exmoGetWithdrawTXID = "withdraw_get_txid"
exmoExcodeCreate = "excode_create"
exmoExcodeLoad = "excode_load"
exmoWalletHistory = "wallet_history"
exmoCryptoPaymentProviderList = "payments/providers/crypto/list"
// Rate limit: 180 per/minute
exmoRateInterval = time.Minute
@@ -220,7 +222,6 @@ func (e *EXMO) GetCryptoDepositAddress(ctx context.Context) (map[string]string,
mapString[key] = v
}
return mapString, nil
default:
return nil, errors.New("no addresses found, generate required addresses via site")
}
@@ -228,7 +229,7 @@ func (e *EXMO) GetCryptoDepositAddress(ctx context.Context) (map[string]string,
// WithdrawCryptocurrency withdraws a cryptocurrency from the exchange to the desired address
// NOTE: This API function is available only after request to their tech support team
func (e *EXMO) WithdrawCryptocurrency(ctx context.Context, currency, address, invoice string, amount float64) (int64, error) {
func (e *EXMO) WithdrawCryptocurrency(ctx context.Context, currency, address, invoice, transport string, amount float64) (int64, error) {
type response struct {
TaskID int64 `json:"task_id,string"`
Result bool `json:"result"`
@@ -240,8 +241,12 @@ func (e *EXMO) WithdrawCryptocurrency(ctx context.Context, currency, address, in
v.Set("currency", currency)
v.Set("address", address)
if strings.EqualFold(currency, "XRP") {
v.Set(invoice, invoice)
if invoice != "" {
v.Set("invoice", invoice)
}
if transport != "" {
v.Set("transport", strings.ToUpper(transport))
}
v.Set("amount", strconv.FormatFloat(amount, 'f', -1, 64))
@@ -526,3 +531,10 @@ func getInternationalBankDepositFee(c currency.Code, amount float64, bankTransac
return fee
}
// GetCryptoPaymentProvidersList returns a map of all the supported cryptocurrency transfer settings
func (e *EXMO) GetCryptoPaymentProvidersList(ctx context.Context) (map[string][]CryptoPaymentProvider, error) {
var result map[string][]CryptoPaymentProvider
path := "/v" + exmoAPIVersion + "/" + exmoCryptoPaymentProviderList
return result, e.SendHTTPRequest(ctx, exchange.RestSpot, path, &result)
}

View File

@@ -383,6 +383,7 @@ func TestWithdraw(t *testing.T) {
}
withdrawCryptoRequest := withdraw.Request{
Exchange: e.Name,
Amount: -1,
Currency: currency.BTC,
Description: "WITHDRAW IT ALL",
@@ -428,12 +429,12 @@ func TestWithdrawInternationalBank(t *testing.T) {
func TestGetDepositAddress(t *testing.T) {
if areTestAPIKeysSet() {
_, err := e.GetDepositAddress(context.Background(), currency.LTC, "")
_, err := e.GetDepositAddress(context.Background(), currency.USDT, "", "ERC20")
if err != nil {
t.Error("GetDepositAddress() error", err)
}
} else {
_, err := e.GetDepositAddress(context.Background(), currency.LTC, "")
_, err := e.GetDepositAddress(context.Background(), currency.LTC, "", "")
if err == nil {
t.Error("GetDepositAddress() error cannot be nil")
}
@@ -483,3 +484,18 @@ func TestUpdateTickers(t *testing.T) {
t.Error(err)
}
}
func TestGetCryptoPaymentProvidersList(t *testing.T) {
t.Parallel()
_, err := e.GetCryptoPaymentProvidersList(context.Background())
if err != nil {
t.Fatal(err)
}
}
func TestGetAvailableTransferChains(t *testing.T) {
_, err := e.GetAvailableTransferChains(context.Background(), currency.USDT)
if err != nil {
t.Error(err)
}
}

View File

@@ -175,3 +175,16 @@ var WithdrawalFees = map[currency.Code]float64{
currency.ZRX: 1,
currency.GNT: 1,
}
// CryptoPaymentProvider stores the cryptocurrency transfer settings
type CryptoPaymentProvider struct {
Type string `json:"type"`
Name string `json:"name"`
CurrencyName string `json:"currency_name"`
Min float64 `json:"min,string"`
Max float64 `json:"max,string"`
Enabled bool `json:"enabled"`
Comment string `json:"comment"`
CommissionDescription string `json:"commission_desc"`
CurrencyConfirmations uint16 `json:"currency_confirmations"`
}

View File

@@ -16,6 +16,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"
@@ -77,26 +78,29 @@ func (e *EXMO) SetDefaults() {
REST: true,
Websocket: false,
RESTCapabilities: protocol.Features{
TickerBatching: true,
TickerFetching: true,
TradeFetching: true,
OrderbookFetching: true,
AutoPairUpdates: true,
AccountInfo: true,
GetOrder: true,
GetOrders: true,
CancelOrder: true,
SubmitOrder: true,
DepositHistory: true,
WithdrawalHistory: true,
UserTradeHistory: true,
CryptoDeposit: true,
CryptoWithdrawal: true,
TradeFee: true,
FiatDepositFee: true,
FiatWithdrawalFee: true,
CryptoDepositFee: true,
CryptoWithdrawalFee: true,
TickerBatching: true,
TickerFetching: true,
TradeFetching: true,
OrderbookFetching: true,
AutoPairUpdates: true,
AccountInfo: true,
GetOrder: true,
GetOrders: true,
CancelOrder: true,
SubmitOrder: true,
DepositHistory: true,
WithdrawalHistory: true,
UserTradeHistory: true,
CryptoDeposit: true,
CryptoWithdrawal: true,
TradeFee: true,
FiatDepositFee: true,
FiatWithdrawalFee: true,
CryptoDepositFee: true,
CryptoWithdrawalFee: true,
MultiChainDeposits: true,
MultiChainWithdrawals: true,
MultiChainDepositRequiresChainSet: true,
},
WithdrawPermissions: exchange.AutoWithdrawCryptoWithSetup |
exchange.NoFiatWithdrawals,
@@ -524,19 +528,42 @@ func (e *EXMO) GetOrderInfo(ctx context.Context, orderID string, pair currency.P
}
// GetDepositAddress returns a deposit address for a specified currency
func (e *EXMO) GetDepositAddress(ctx context.Context, cryptocurrency currency.Code, _ string) (string, error) {
func (e *EXMO) GetDepositAddress(ctx context.Context, cryptocurrency currency.Code, _, chain string) (*deposit.Address, error) {
fullAddr, err := e.GetCryptoDepositAddress(ctx)
if err != nil {
return "", err
return nil, err
}
// TODO: Protect map with mutex
addr, ok := fullAddr[cryptocurrency.String()]
curr := cryptocurrency.Upper().String()
if chain != "" && !strings.EqualFold(chain, curr) {
curr += strings.ToUpper(chain)
}
addr, ok := fullAddr[curr]
if !ok {
return "", fmt.Errorf("currency %s could not be found, please generate via the exmo website", cryptocurrency.String())
chains, err := e.GetAvailableTransferChains(ctx, cryptocurrency)
if err != nil {
return nil, err
}
if len(chains) > 1 {
// rather than assume, return an error
return nil, fmt.Errorf("currency %s has %v chains available, one must be specified", cryptocurrency, chains)
}
return nil, fmt.Errorf("deposit address for %s could not be found, please generate via the exmo website", cryptocurrency.String())
}
return addr, nil
var tag string
if strings.Contains(addr, ",") {
split := strings.Split(addr, ",")
addr, tag = split[0], split[1]
}
return &deposit.Address{
Address: addr,
Tag: tag,
Chain: chain,
}, nil
}
// WithdrawCryptocurrencyFunds returns a withdrawal ID when a withdrawal is
@@ -549,6 +576,7 @@ func (e *EXMO) WithdrawCryptocurrencyFunds(ctx context.Context, withdrawRequest
withdrawRequest.Currency.String(),
withdrawRequest.Crypto.Address,
withdrawRequest.Crypto.AddressTag,
withdrawRequest.Crypto.Chain,
withdrawRequest.Amount)
return &withdraw.ExchangeResponse{
@@ -680,3 +708,30 @@ func (e *EXMO) GetHistoricCandles(ctx context.Context, pair currency.Pair, a ass
func (e *EXMO) GetHistoricCandlesExtended(ctx context.Context, pair currency.Pair, a asset.Item, start, end time.Time, interval kline.Interval) (kline.Item, error) {
return kline.Item{}, common.ErrFunctionNotSupported
}
// GetAvailableTransferChains returns the available transfer blockchains for the specific
// cryptocurrency
func (e *EXMO) GetAvailableTransferChains(ctx context.Context, cryptocurrency currency.Code) ([]string, error) {
chains, err := e.GetCryptoPaymentProvidersList(ctx)
if err != nil {
return nil, err
}
methods, ok := chains[cryptocurrency.Upper().String()]
if !ok {
return nil, errors.New("no available chains")
}
var availChains []string
for x := range methods {
if methods[x].Type == "deposit" && methods[x].Enabled {
chain := methods[x].Name
if strings.Contains(chain, "(") && strings.Contains(chain, ")") {
chain = chain[strings.Index(chain, "(")+1 : strings.Index(chain, ")")]
}
availChains = append(availChains, chain)
}
}
return availChains, nil
}