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

@@ -69,7 +69,7 @@ func (w *CurrencyDetails) loadPairs(data map[string]Ticker) error {
}
// loadCodes loads currency codes from currency map
func (w *CurrencyDetails) loadCodes(data map[string]Currencies) error {
func (w *CurrencyDetails) loadCodes(data map[string]*Currencies) error {
if data == nil {
return errCannotLoadNoData
}

View File

@@ -108,25 +108,25 @@ func TestWsCurrencyMap(t *testing.T) {
t.Fatal("expecting USDT_BTC pair")
}
maid, err := m.GetCode(127)
eth, err := m.GetCode(267)
if !errors.Is(err, nil) {
t.Fatalf("expected: %v but received: %v", nil, err)
}
if maid.String() != "MAID" {
if eth.String() != "ETH" {
t.Fatal("unexpected value")
}
txFee, err := m.GetWithdrawalTXFee(maid)
txFee, err := m.GetWithdrawalTXFee(eth)
if err != nil {
t.Fatal(err)
}
if txFee != 80 {
if txFee == 0 {
t.Fatal("unexpected value")
}
_, err = m.GetDepositAddress(maid)
_, err = m.GetDepositAddress(eth)
if !errors.Is(err, errNoDepositAddress) {
t.Fatalf("expected: %v but received: %v", errNoDepositAddress, err)
}
@@ -140,7 +140,7 @@ func TestWsCurrencyMap(t *testing.T) {
t.Fatal("unexpected deposit address")
}
wdEnabled, err := m.IsWithdrawAndDepositsEnabled(maid)
wdEnabled, err := m.IsWithdrawAndDepositsEnabled(eth)
if !errors.Is(err, nil) {
t.Fatalf("expected: %v but received: %v", nil, err)
}
@@ -149,7 +149,7 @@ func TestWsCurrencyMap(t *testing.T) {
t.Fatal("unexpected results")
}
tEnabled, err := m.IsTradingEnabledForCurrency(maid)
tEnabled, err := m.IsTradingEnabledForCurrency(eth)
if !errors.Is(err, nil) {
t.Fatalf("expected: %v but received: %v", nil, err)
}

View File

@@ -9,6 +9,7 @@ import (
"net/http"
"net/url"
"strconv"
"strings"
"time"
"github.com/thrasher-corp/gocryptotrader/common/crypto"
@@ -220,12 +221,16 @@ func (p *Poloniex) GetChartData(ctx context.Context, currencyPair string, start,
}
// GetCurrencies returns information about currencies
func (p *Poloniex) GetCurrencies(ctx context.Context) (map[string]Currencies, error) {
func (p *Poloniex) GetCurrencies(ctx context.Context) (map[string]*Currencies, error) {
type Response struct {
Data map[string]Currencies
Data map[string]*Currencies
}
resp := Response{}
return resp.Data, p.SendHTTPRequest(ctx, exchange.RestSpot, "/public?command=returnCurrencies", &resp.Data)
return resp.Data, p.SendHTTPRequest(ctx,
exchange.RestSpot,
"/public?command=returnCurrencies&includeMultiChainCurrencies=true",
&resp.Data,
)
}
// GetLoanOrders returns the list of loan offers and demands for a given
@@ -577,12 +582,15 @@ func (p *Poloniex) MoveOrder(ctx context.Context, orderID int64, rate, amount fl
return result, nil
}
// Withdraw withdraws a currency to a specific delegated address
// Withdraw withdraws a currency to a specific delegated address.
// For currencies where there are multiple networks to choose from (like USDT or BTC),
// you can specify the chain by setting the "currency" parameter to be a multiChain currency
// name, like USDTTRON, USDTETH, or BTCTRON
func (p *Poloniex) Withdraw(ctx context.Context, currency, address string, amount float64) (*Withdraw, error) {
result := &Withdraw{}
values := url.Values{}
values.Set("currency", currency)
values.Set("currency", strings.ToUpper(currency))
values.Set("amount", strconv.FormatFloat(amount, 'f', -1, 64))
values.Set("address", address)
@@ -869,7 +877,7 @@ func (p *Poloniex) SendAuthenticatedHTTPRequest(ctx context.Context, ep exchange
headers := make(map[string]string)
headers["Content-Type"] = "application/x-www-form-urlencoded"
headers["Key"] = p.API.Credentials.Key
values.Set("nonce", strconv.FormatInt(time.Now().UnixNano(), 10))
values.Set("nonce", p.Requester.GetNonce(true).String())
values.Set("command", endpoint)
hmac, err := crypto.GetHMAC(crypto.HashSHA512,
@@ -888,6 +896,7 @@ func (p *Poloniex) SendAuthenticatedHTTPRequest(ctx context.Context, ep exchange
Body: bytes.NewBufferString(values.Encode()),
Result: result,
AuthRequest: true,
NonceEnabled: true,
Verbose: p.Verbose,
HTTPDebugging: p.HTTPDebugging,
HTTPRecording: p.HTTPRecording,

View File

@@ -1,4 +1,5 @@
//+build mock_test_off
//go:build mock_test_off
// +build mock_test_off
// This will build if build tag mock_test_off is parsed and will do live testing
// using all tests in (exchange)_test.go

View File

@@ -459,9 +459,9 @@ func TestWithdraw(t *testing.T) {
Exchange: p.Name,
Crypto: withdraw.CryptoRequest{
Address: core.BitcoinDonationAddress,
FeeAmount: 1,
FeeAmount: 0,
},
Amount: 0.00001337,
Amount: -1,
Currency: currency.LTC,
Description: "WITHDRAW IT ALL",
TradePassword: "Password",
@@ -477,8 +477,8 @@ func TestWithdraw(t *testing.T) {
t.Errorf("Withdraw failed to be placed: %v", err)
case !areTestAPIKeysSet() && !mockTests && err == nil:
t.Error("Expecting an error when no keys are set")
case mockTests && err != nil:
t.Error(err)
case mockTests && err == nil:
t.Error("should error due to invalid amount")
}
}
@@ -513,7 +513,7 @@ func TestWithdrawInternationalBank(t *testing.T) {
func TestGetDepositAddress(t *testing.T) {
t.Parallel()
_, err := p.GetDepositAddress(context.Background(), currency.DASH, "")
_, err := p.GetDepositAddress(context.Background(), currency.USDT, "", "USDTETH")
switch {
case areTestAPIKeysSet() && err != nil:
t.Error("GetDepositAddress()", err)
@@ -524,6 +524,17 @@ func TestGetDepositAddress(t *testing.T) {
}
}
func TestGenerateNewAddress(t *testing.T) {
t.Parallel()
if !areTestAPIKeysSet() {
t.Skip("api keys not set, skipping test")
}
_, err := p.GenerateNewAddress(context.Background(), currency.XRP.String())
if err != nil {
t.Fatal(err)
}
}
// TestWsAuth dials websocket, sends login request.
// Will receive a message only on failure
func TestWsAuth(t *testing.T) {
@@ -1060,3 +1071,11 @@ func TestUpdateTickers(t *testing.T) {
t.Error(err)
}
}
func TestGetAvailableTransferChains(t *testing.T) {
t.Parallel()
_, err := p.GetAvailableTransferChains(context.Background(), currency.USDT)
if err != nil {
t.Fatal(err)
}
}

View File

@@ -115,14 +115,23 @@ type ChartData struct {
// Currencies contains currency information
type Currencies struct {
ID float64 `json:"id"`
Name string `json:"name"`
TxFee float64 `json:"txFee,string"`
MinConfirmations int64 `json:"minConf"`
DepositAddress string `json:"depositAddress"`
WithdrawalDepositDisabled uint8 `json:"disabled"`
Delisted uint8 `json:"delisted"`
Frozen uint8 `json:"frozen"`
ID float64 `json:"id"`
Name string `json:"name"`
HumanType string `json:"humanType"`
CurrencyType string `json:"currencyType"`
TxFee float64 `json:"txFee,string"`
MinConfirmations int64 `json:"minConf"`
DepositAddress string `json:"depositAddress"`
WithdrawalDepositDisabled uint8 `json:"disabled"`
Frozen uint8 `json:"frozen"`
HexColour string `json:"hexColor"`
Blockchain string `json:"blockchain"`
Delisted uint8 `json:"delisted"`
ParentChain string `json:"parentChain"`
IsMultiChain uint8 `json:"isMultiChain"`
IsChildChain uint8 `json:"isChildChain"`
ChildChains []string `json:"childChains"`
IsGeofenced uint8 `json:"isGeofenced"`
}
// LoanOrder holds loan order information

View File

@@ -2,6 +2,7 @@ package poloniex
import (
"context"
"errors"
"fmt"
"sort"
"strconv"
@@ -15,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"
@@ -78,25 +80,27 @@ func (p *Poloniex) 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,
CancelOrder: true,
CancelOrders: true,
SubmitOrder: true,
DepositHistory: true,
WithdrawalHistory: 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,
CancelOrder: true,
CancelOrders: true,
SubmitOrder: true,
DepositHistory: true,
WithdrawalHistory: true,
UserTradeHistory: true,
CryptoDeposit: true,
CryptoWithdrawal: true,
TradeFee: true,
CryptoWithdrawalFee: true,
MultiChainDeposits: true,
MultiChainWithdrawals: true,
},
WebsocketCapabilities: protocol.Features{
TickerFetching: true,
@@ -691,19 +695,72 @@ func (p *Poloniex) GetOrderInfo(ctx context.Context, orderID string, pair curren
}
// GetDepositAddress returns a deposit address for a specified currency
func (p *Poloniex) GetDepositAddress(ctx context.Context, cryptocurrency currency.Code, _ string) (string, error) {
a, err := p.GetDepositAddresses(ctx)
func (p *Poloniex) GetDepositAddress(ctx context.Context, cryptocurrency currency.Code, _, chain string) (*deposit.Address, error) {
depositAddrs, err := p.GetDepositAddresses(ctx)
if err != nil {
return "", err
return nil, err
}
address, ok := a.Addresses[cryptocurrency.Upper().String()]
// Some coins use a main address, so we must use this in conjunction with the returned
// deposit address to produce the full deposit address and tag
currencies, err := p.GetCurrencies(ctx)
if err != nil {
return nil, err
}
coinParams, ok := currencies[cryptocurrency.Upper().String()]
if !ok {
return "", fmt.Errorf("cannot find deposit address for %s",
cryptocurrency)
return nil, fmt.Errorf("unable to find currency %s in map", cryptocurrency)
}
return address, nil
// Handle coins with payment ID's like XRP
var address, tag string
if coinParams.CurrencyType == "address-payment-id" && coinParams.DepositAddress != "" {
address = coinParams.DepositAddress
tag, ok = depositAddrs.Addresses[cryptocurrency.Upper().String()]
if !ok {
newAddr, err := p.GenerateNewAddress(ctx, cryptocurrency.Upper().String())
if err != nil {
return nil, err
}
tag = newAddr
}
return &deposit.Address{
Address: address,
Tag: tag,
}, nil
}
// Handle coins like BTC or multichain coins
targetCurrency := cryptocurrency.String()
if chain != "" && !strings.EqualFold(chain, cryptocurrency.String()) {
targetCurrency = chain
}
address, ok = depositAddrs.Addresses[strings.ToUpper(targetCurrency)]
if !ok {
if len(coinParams.ChildChains) > 1 && chain != "" && !common.StringDataCompare(coinParams.ChildChains, targetCurrency) {
// rather than assume, return an error
return nil, fmt.Errorf("currency %s has %v chains available, one of these must be specified",
cryptocurrency,
coinParams.ChildChains)
}
coinParams, ok = currencies[strings.ToUpper(targetCurrency)]
if !ok {
return nil, fmt.Errorf("unable to find currency %s in map", cryptocurrency)
}
if coinParams.WithdrawalDepositDisabled == 1 {
return nil, fmt.Errorf("deposits and withdrawals for %v are currently disabled", targetCurrency)
}
newAddr, err := p.GenerateNewAddress(ctx, targetCurrency)
if err != nil {
return nil, err
}
address = newAddr
}
return &deposit.Address{Address: address}, nil
}
// WithdrawCryptocurrencyFunds returns a withdrawal ID when a withdrawal is
@@ -712,7 +769,12 @@ func (p *Poloniex) WithdrawCryptocurrencyFunds(ctx context.Context, withdrawRequ
if err := withdrawRequest.Validate(); err != nil {
return nil, err
}
v, err := p.Withdraw(ctx, withdrawRequest.Currency.String(), withdrawRequest.Crypto.Address, withdrawRequest.Amount)
targetCurrency := withdrawRequest.Currency.String()
if withdrawRequest.Crypto.Chain != "" {
targetCurrency = withdrawRequest.Crypto.Chain
}
v, err := p.Withdraw(ctx, targetCurrency, withdrawRequest.Crypto.Address, withdrawRequest.Amount)
if err != nil {
return nil, err
}
@@ -910,3 +972,19 @@ func (p *Poloniex) GetHistoricCandles(ctx context.Context, pair currency.Pair, a
func (p *Poloniex) GetHistoricCandlesExtended(ctx context.Context, pair currency.Pair, a asset.Item, start, end time.Time, interval kline.Interval) (kline.Item, error) {
return p.GetHistoricCandles(ctx, pair, a, start, end, interval)
}
// GetAvailableTransferChains returns the available transfer blockchains for the specific
// cryptocurrency
func (p *Poloniex) GetAvailableTransferChains(ctx context.Context, cryptocurrency currency.Code) ([]string, error) {
currencies, err := p.GetCurrencies(ctx)
if err != nil {
return nil, err
}
curr, ok := currencies[cryptocurrency.Upper().String()]
if !ok {
return nil, errors.New("unable to locate currency in map")
}
return curr.ChildChains, nil
}