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

@@ -654,8 +654,7 @@ func TestCompareJobsToData(t *testing.T) {
}
}
func TestRunJob(t *testing.T) {
t.Parallel()
func TestRunJob(t *testing.T) { // nolint // TO-DO: Fix race t.Parallel() usage
testCases := []*DataHistoryJob{
{
Nickname: "TestRunJobDataHistoryCandleDataType",

View File

@@ -7,55 +7,86 @@ import (
"sync"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/exchanges/deposit"
)
// vars related to the deposit address helpers
var (
ErrDepositAddressStoreIsNil = errors.New("deposit address store is nil")
ErrDepositAddressNotFound = errors.New("deposit address does not exist")
ErrDepositAddressStoreIsNil = errors.New("deposit address store is nil")
ErrDepositAddressNotFound = errors.New("deposit address does not exist")
errDepositAddressChainNotFound = errors.New("deposit address for specified chain not found")
errNoDepositAddressesRetrieved = errors.New("no deposit addresses retrieved")
)
// DepositAddressManager manages the exchange deposit address store
type DepositAddressManager struct {
m sync.Mutex
store map[string]map[string]string
m sync.RWMutex
store map[string]map[string][]deposit.Address
}
// IsSynced returns whether or not the deposit address store has synced its data
func (m *DepositAddressManager) IsSynced() bool {
if m.store == nil {
return false
}
m.m.RLock()
defer m.m.RUnlock()
return len(m.store) > 0
}
// SetupDepositAddressManager returns a DepositAddressManager
func SetupDepositAddressManager() *DepositAddressManager {
return &DepositAddressManager{
store: make(map[string]map[string]string),
store: make(map[string]map[string][]deposit.Address),
}
}
// GetDepositAddressByExchangeAndCurrency returns a deposit address for the specified exchange and cryptocurrency
// if it exists
func (m *DepositAddressManager) GetDepositAddressByExchangeAndCurrency(exchName string, currencyItem currency.Code) (string, error) {
m.m.Lock()
defer m.m.Unlock()
func (m *DepositAddressManager) GetDepositAddressByExchangeAndCurrency(exchName, chain string, currencyItem currency.Code) (deposit.Address, error) {
m.m.RLock()
defer m.m.RUnlock()
if len(m.store) == 0 {
return "", ErrDepositAddressStoreIsNil
return deposit.Address{}, ErrDepositAddressStoreIsNil
}
r, ok := m.store[strings.ToUpper(exchName)]
if !ok {
return "", ErrExchangeNotFound
return deposit.Address{}, ErrExchangeNotFound
}
addr, ok := r[strings.ToUpper(currencyItem.String())]
if !ok {
return "", ErrDepositAddressNotFound
return deposit.Address{}, ErrDepositAddressNotFound
}
return addr, nil
if len(addr) == 0 {
return deposit.Address{}, errNoDepositAddressesRetrieved
}
if chain != "" {
for x := range addr {
if strings.EqualFold(addr[x].Chain, chain) {
return addr[x], nil
}
}
return deposit.Address{}, errDepositAddressChainNotFound
}
for x := range addr {
if strings.EqualFold(addr[x].Chain, currencyItem.String()) {
return addr[x], nil
}
}
return addr[0], nil
}
// GetDepositAddressesByExchange returns a list of cryptocurrency addresses for the specified
// exchange if they exist
func (m *DepositAddressManager) GetDepositAddressesByExchange(exchName string) (map[string]string, error) {
m.m.Lock()
defer m.m.Unlock()
func (m *DepositAddressManager) GetDepositAddressesByExchange(exchName string) (map[string][]deposit.Address, error) {
m.m.RLock()
defer m.m.RUnlock()
if len(m.store) == 0 {
return nil, ErrDepositAddressStoreIsNil
@@ -66,11 +97,15 @@ func (m *DepositAddressManager) GetDepositAddressesByExchange(exchName string) (
return nil, ErrDepositAddressNotFound
}
return r, nil
cpy := make(map[string][]deposit.Address, len(r))
for k, v := range r {
cpy[k] = v
}
return cpy, nil
}
// Sync synchronises all deposit addresses
func (m *DepositAddressManager) Sync(addresses map[string]map[string]string) error {
func (m *DepositAddressManager) Sync(addresses map[string]map[string][]deposit.Address) error {
if m == nil {
return fmt.Errorf("deposit address manager %w", ErrNilSubsystem)
}
@@ -81,7 +116,7 @@ func (m *DepositAddressManager) Sync(addresses map[string]map[string]string) err
}
for k, v := range addresses {
r := make(map[string]string)
r := make(map[string][]deposit.Address)
for w, x := range v {
r[strings.ToUpper(w)] = x
}

View File

@@ -5,6 +5,7 @@ import (
"testing"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/exchanges/deposit"
)
const (
@@ -13,7 +14,32 @@ const (
btc = "BTC"
)
func TestIsSynced(t *testing.T) {
t.Parallel()
var d DepositAddressManager
if d.IsSynced() {
t.Error("should be false")
}
m := SetupDepositAddressManager()
err := m.Sync(map[string]map[string][]deposit.Address{
bitStamp: {
btc: []deposit.Address{
{
Address: address,
},
},
},
})
if err != nil {
t.Error(err)
}
if !m.IsSynced() {
t.Error("should be synced")
}
}
func TestSetupDepositAddressManager(t *testing.T) {
t.Parallel()
m := SetupDepositAddressManager()
if m.store == nil {
t.Fatal("expected store")
@@ -21,27 +47,36 @@ func TestSetupDepositAddressManager(t *testing.T) {
}
func TestSync(t *testing.T) {
t.Parallel()
m := SetupDepositAddressManager()
err := m.Sync(map[string]map[string]string{
err := m.Sync(map[string]map[string][]deposit.Address{
bitStamp: {
btc: address,
btc: []deposit.Address{
{
Address: address,
},
},
},
})
if err != nil {
t.Error(err)
}
r, err := m.GetDepositAddressByExchangeAndCurrency(bitStamp, currency.BTC)
r, err := m.GetDepositAddressByExchangeAndCurrency(bitStamp, "", currency.BTC)
if err != nil {
t.Error("unexpected result")
}
if r != address {
if r.Address != address {
t.Error("unexpected result")
}
m.store = nil
err = m.Sync(map[string]map[string]string{
err = m.Sync(map[string]map[string][]deposit.Address{
bitStamp: {
btc: address,
btc: []deposit.Address{
{
Address: address,
},
},
},
})
if !errors.Is(err, ErrDepositAddressStoreIsNil) {
@@ -49,9 +84,13 @@ func TestSync(t *testing.T) {
}
m = nil
err = m.Sync(map[string]map[string]string{
err = m.Sync(map[string]map[string][]deposit.Address{
bitStamp: {
btc: address,
btc: []deposit.Address{
{
Address: address,
},
},
},
})
if !errors.Is(err, ErrNilSubsystem) {
@@ -60,35 +99,91 @@ func TestSync(t *testing.T) {
}
func TestGetDepositAddressByExchangeAndCurrency(t *testing.T) {
t.Parallel()
m := SetupDepositAddressManager()
_, err := m.GetDepositAddressByExchangeAndCurrency("", currency.BTC)
_, err := m.GetDepositAddressByExchangeAndCurrency("", "", currency.BTC)
if !errors.Is(err, ErrDepositAddressStoreIsNil) {
t.Errorf("received %v, expected %v", err, ErrDepositAddressStoreIsNil)
}
m.store = map[string]map[string]string{
m.store = map[string]map[string][]deposit.Address{
bitStamp: {
btc: address,
btc: []deposit.Address{
{
Address: address,
},
},
"USDT": []deposit.Address{
{
Address: "ABsdZ",
Chain: "SOL",
},
{
Address: "0x1b",
Chain: "ERC20",
},
{
Address: "1asdad",
Chain: "USDT",
},
},
"BNB": nil,
},
}
_, err = m.GetDepositAddressByExchangeAndCurrency(bitStamp, currency.BTC)
_, err = m.GetDepositAddressByExchangeAndCurrency("asdf", "", currency.BTC)
if !errors.Is(err, ErrExchangeNotFound) {
t.Errorf("received %v, expected %v", err, ErrExchangeNotFound)
}
_, err = m.GetDepositAddressByExchangeAndCurrency(bitStamp, "", currency.LTC)
if !errors.Is(err, ErrDepositAddressNotFound) {
t.Errorf("received %v, expected %v", err, ErrDepositAddressNotFound)
}
_, err = m.GetDepositAddressByExchangeAndCurrency(bitStamp, "", currency.BNB)
if !errors.Is(err, errNoDepositAddressesRetrieved) {
t.Errorf("received %v, expected %v", err, errNoDepositAddressesRetrieved)
}
_, err = m.GetDepositAddressByExchangeAndCurrency(bitStamp, "NON-EXISTENT-CHAIN", currency.USDT)
if !errors.Is(err, errDepositAddressChainNotFound) {
t.Errorf("received %v, expected %v", err, errDepositAddressChainNotFound)
}
if r, _ := m.GetDepositAddressByExchangeAndCurrency(bitStamp, "ErC20", currency.USDT); r.Address != "0x1b" && r.Chain != "ERC20" {
t.Error("unexpected values")
}
if r, _ := m.GetDepositAddressByExchangeAndCurrency(bitStamp, "sOl", currency.USDT); r.Address != "ABsdZ" && r.Chain != "SOL" {
t.Error("unexpected values")
}
if r, _ := m.GetDepositAddressByExchangeAndCurrency(bitStamp, "", currency.USDT); r.Address != "1asdad" && r.Chain != "USDT" {
t.Error("unexpected values")
}
_, err = m.GetDepositAddressByExchangeAndCurrency(bitStamp, "", currency.BTC)
if !errors.Is(err, nil) {
t.Errorf("received %v, expected %v", err, nil)
}
}
func TestGetDepositAddressesByExchange(t *testing.T) {
t.Parallel()
m := SetupDepositAddressManager()
_, err := m.GetDepositAddressesByExchange("")
if !errors.Is(err, ErrDepositAddressStoreIsNil) {
t.Errorf("received %v, expected %v", err, ErrDepositAddressStoreIsNil)
}
m.store = map[string]map[string]string{
m.store = map[string]map[string][]deposit.Address{
bitStamp: {
btc: address,
btc: []deposit.Address{
{
Address: address,
},
},
},
}
_, err = m.GetDepositAddressesByExchange("non-existent")
if !errors.Is(err, ErrDepositAddressNotFound) {
t.Errorf("received %v, expected %v", err, ErrDepositAddressNotFound)
}
_, err = m.GetDepositAddressesByExchange(bitStamp)
if !errors.Is(err, nil) {
t.Errorf("received %v, expected %v", err, nil)

View File

@@ -494,7 +494,7 @@ func (bot *Engine) Start() error {
if bot.Settings.EnableDepositAddressManager {
bot.DepositAddressManager = SetupDepositAddressManager()
go func() {
err = bot.DepositAddressManager.Sync(bot.GetExchangeCryptocurrencyDepositAddresses())
err = bot.DepositAddressManager.Sync(bot.GetAllExchangeCryptocurrencyDepositAddresses())
if err != nil {
gctlog.Errorf(gctlog.Global, "Deposit address manager unable to setup: %s", err)
}

View File

@@ -16,6 +16,7 @@ import (
"os"
"path/filepath"
"strings"
"sync"
"time"
"github.com/pquerna/otp/totp"
@@ -27,6 +28,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/orderbook"
"github.com/thrasher-corp/gocryptotrader/exchanges/stats"
"github.com/thrasher-corp/gocryptotrader/exchanges/ticker"
@@ -668,12 +670,15 @@ func (bot *Engine) GetCryptocurrenciesByExchange(exchangeName string, enabledExc
}
// GetCryptocurrencyDepositAddressesByExchange returns the cryptocurrency deposit addresses for a particular exchange
func (bot *Engine) GetCryptocurrencyDepositAddressesByExchange(exchName string) (map[string]string, error) {
func (bot *Engine) GetCryptocurrencyDepositAddressesByExchange(exchName string) (map[string][]deposit.Address, error) {
if bot.DepositAddressManager != nil {
return bot.DepositAddressManager.GetDepositAddressesByExchange(exchName)
if bot.DepositAddressManager.IsSynced() {
return bot.DepositAddressManager.GetDepositAddressesByExchange(exchName)
}
return nil, errors.New("deposit address manager has not yet synced all exchange deposit addresses")
}
result := bot.GetExchangeCryptocurrencyDepositAddresses()
result := bot.GetAllExchangeCryptocurrencyDepositAddresses()
r, ok := result[exchName]
if !ok {
return nil, ErrExchangeNotFound
@@ -683,50 +688,112 @@ func (bot *Engine) GetCryptocurrencyDepositAddressesByExchange(exchName string)
// GetExchangeCryptocurrencyDepositAddress returns the cryptocurrency deposit address for a particular
// exchange
func (bot *Engine) GetExchangeCryptocurrencyDepositAddress(ctx context.Context, exchName, accountID string, item currency.Code) (string, error) {
if bot.DepositAddressManager != nil {
return bot.DepositAddressManager.GetDepositAddressByExchangeAndCurrency(exchName, item)
func (bot *Engine) GetExchangeCryptocurrencyDepositAddress(ctx context.Context, exchName, accountID, chain string, item currency.Code, bypassCache bool) (*deposit.Address, error) {
if bot.DepositAddressManager != nil && bot.DepositAddressManager.IsSynced() && !bypassCache {
resp, err := bot.DepositAddressManager.GetDepositAddressByExchangeAndCurrency(exchName, chain, item)
if err != nil {
return nil, err
}
return &resp, nil
}
exch, err := bot.GetExchangeByName(exchName)
if err != nil {
return "", err
return nil, err
}
return exch.GetDepositAddress(ctx, item, accountID)
return exch.GetDepositAddress(ctx, item, accountID, chain)
}
// GetExchangeCryptocurrencyDepositAddresses obtains an exchanges deposit cryptocurrency list
func (bot *Engine) GetExchangeCryptocurrencyDepositAddresses() map[string]map[string]string {
result := make(map[string]map[string]string)
// GetAllExchangeCryptocurrencyDepositAddresses obtains an exchanges deposit cryptocurrency list
func (bot *Engine) GetAllExchangeCryptocurrencyDepositAddresses() map[string]map[string][]deposit.Address {
result := make(map[string]map[string][]deposit.Address)
exchanges := bot.GetExchanges()
var depositSyncer sync.WaitGroup
depositSyncer.Add(len(exchanges))
var m sync.Mutex
for x := range exchanges {
exchName := exchanges[x].GetName()
if !exchanges[x].GetAuthenticatedAPISupport(exchange.RestAuthentication) {
if bot.Settings.Verbose {
log.Debugf(log.ExchangeSys, "GetExchangeCryptocurrencyDepositAddresses: Skippping %s due to disabled authenticated API support.\n", exchName)
go func(x int) {
defer depositSyncer.Done()
exchName := exchanges[x].GetName()
if !exchanges[x].GetAuthenticatedAPISupport(exchange.RestAuthentication) {
if bot.Settings.Verbose {
log.Debugf(log.ExchangeSys, "GetAllExchangeCryptocurrencyDepositAddresses: Skippping %s due to disabled authenticated API support.\n", exchName)
}
return
}
continue
}
cryptoCurrencies, err := bot.GetCryptocurrenciesByExchange(exchName, true, true, asset.Spot)
if err != nil {
log.Debugf(log.ExchangeSys, "%s failed to get cryptocurrency deposit addresses. Err: %s\n", exchName, err)
continue
}
cryptoAddr := make(map[string]string)
for y := range cryptoCurrencies {
cryptocurrency := cryptoCurrencies[y]
depositAddr, err := exchanges[x].GetDepositAddress(context.TODO(),
currency.NewCode(cryptocurrency),
"")
cryptoCurrencies, err := bot.GetCryptocurrenciesByExchange(exchName, true, true, asset.Spot)
if err != nil {
log.Errorf(log.Global, "%s failed to get cryptocurrency deposit addresses. Err: %s\n", exchName, err)
continue
log.Errorf(log.ExchangeSys, "%s failed to get cryptocurrency deposit addresses. Err: %s\n", exchName, err)
return
}
cryptoAddr[cryptocurrency] = depositAddr
}
result[exchName] = cryptoAddr
supportsMultiChain := exchanges[x].GetBase().Features.Supports.RESTCapabilities.MultiChainDeposits
requiresChainSet := exchanges[x].GetBase().Features.Supports.RESTCapabilities.MultiChainDepositRequiresChainSet
cryptoAddr := make(map[string][]deposit.Address)
for y := range cryptoCurrencies {
cryptocurrency := cryptoCurrencies[y]
isSingular := false
var depositAddrs []deposit.Address
if supportsMultiChain {
availChains, err := exchanges[x].GetAvailableTransferChains(context.TODO(), currency.NewCode(cryptocurrency))
if err != nil {
log.Errorf(log.Global, "%s failed to get cryptocurrency available transfer chains. Err: %s\n", exchName, err)
continue
}
if len(availChains) > 0 {
// store the default non-chain specified address for a specified crypto
chainContainsItself := common.StringDataCompareInsensitive(availChains, cryptocurrency)
if !chainContainsItself && !requiresChainSet {
depositAddr, err := exchanges[x].GetDepositAddress(context.TODO(), currency.NewCode(cryptocurrency), "", "")
if err != nil {
log.Errorf(log.Global, "%s failed to get cryptocurrency deposit address for %s. Err: %s\n",
exchName,
cryptocurrency,
err)
continue
}
depositAddr.Chain = cryptocurrency
depositAddrs = append(depositAddrs, *depositAddr)
}
for z := range availChains {
depositAddr, err := exchanges[x].GetDepositAddress(context.TODO(), currency.NewCode(cryptocurrency), "", availChains[z])
if err != nil {
log.Errorf(log.Global, "%s failed to get cryptocurrency deposit address for %s [chain %s]. Err: %s\n",
exchName,
cryptocurrency,
availChains[z],
err)
continue
}
depositAddr.Chain = availChains[z]
depositAddrs = append(depositAddrs, *depositAddr)
}
} else {
// cryptocurrency doesn't support multichain transfers
isSingular = true
}
}
if !supportsMultiChain || isSingular {
depositAddr, err := exchanges[x].GetDepositAddress(context.TODO(), currency.NewCode(cryptocurrency), "", "")
if err != nil {
log.Errorf(log.Global, "%s failed to get cryptocurrency deposit address for %s. Err: %s\n",
exchName,
cryptocurrency,
err)
continue
}
depositAddrs = append(depositAddrs, *depositAddr)
}
cryptoAddr[cryptocurrency] = depositAddrs
}
m.Lock()
result[exchName] = cryptoAddr
m.Unlock()
}(x)
}
depositSyncer.Wait()
if len(result) > 0 {
log.Infoln(log.Global, "Deposit addresses synced")
}
return result
}

View File

@@ -24,9 +24,12 @@ import (
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/database"
"github.com/thrasher-corp/gocryptotrader/dispatch"
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/orderbook"
"github.com/thrasher-corp/gocryptotrader/exchanges/protocol"
"github.com/thrasher-corp/gocryptotrader/exchanges/stats"
"github.com/thrasher-corp/gocryptotrader/exchanges/ticker"
"github.com/thrasher-corp/gocryptotrader/gctscript/vm"
@@ -115,8 +118,7 @@ func TestGetRPCEndpoints(t *testing.T) {
}
}
func TestSetSubsystem(t *testing.T) {
t.Parallel()
func TestSetSubsystem(t *testing.T) { // nolint // TO-DO: Fix race t.Parallel() usage
testCases := []struct {
Subsystem string
Engine *Engine
@@ -983,13 +985,191 @@ func TestGetExchangeLowestPriceByCurrencyPair(t *testing.T) {
func TestGetCryptocurrenciesByExchange(t *testing.T) {
t.Parallel()
e := CreateTestBot(t)
_, err := e.GetCryptocurrenciesByExchange("Bitfinex", false, false, asset.Spot)
if err != nil {
t.Fatalf("Err %s", err)
}
}
type fakeDepositExchangeOpts struct {
SupportsAuth bool
SupportsMultiChain bool
RequiresChainSet bool
ReturnMultipleChains bool
ThrowPairError bool
ThrowTransferChainError bool
ThrowDepositAddressError bool
}
type fakeDepositExchange struct {
exchange.IBotExchange
*fakeDepositExchangeOpts
}
func (f fakeDepositExchange) GetName() string {
return "fake"
}
func (f fakeDepositExchange) GetAuthenticatedAPISupport(endpoint uint8) bool {
return f.SupportsAuth
}
func (f fakeDepositExchange) GetBase() *exchange.Base {
return &exchange.Base{
Features: exchange.Features{Supports: exchange.FeaturesSupported{
RESTCapabilities: protocol.Features{
MultiChainDeposits: f.SupportsMultiChain,
MultiChainDepositRequiresChainSet: f.RequiresChainSet,
},
}},
}
}
func (f fakeDepositExchange) GetAvailableTransferChains(_ context.Context, c currency.Code) ([]string, error) {
if f.ThrowTransferChainError {
return nil, errors.New("unable to get available transfer chains")
}
if c.Match(currency.XRP) {
return nil, nil
}
if c.Match(currency.USDT) {
return []string{"sol", "btc", "usdt"}, nil
}
return []string{"BITCOIN"}, nil
}
func (f fakeDepositExchange) GetDepositAddress(_ context.Context, c currency.Code, chain, accountID string) (*deposit.Address, error) {
if f.ThrowDepositAddressError {
return nil, errors.New("unable to get deposit address")
}
return &deposit.Address{Address: "fakeaddr"}, nil
}
func createDepositEngine(opts *fakeDepositExchangeOpts) *Engine {
ps := currency.PairStore{
AssetEnabled: convert.BoolPtr(true),
Enabled: currency.Pairs{
currency.NewPair(currency.BTC, currency.USDT),
currency.NewPair(currency.XRP, currency.USDT),
},
Available: currency.Pairs{
currency.NewPair(currency.BTC, currency.USDT),
currency.NewPair(currency.XRP, currency.USDT),
},
}
if opts.ThrowPairError {
ps.Available = nil
}
return &Engine{
Settings: Settings{Verbose: true},
Config: &config.Config{
Exchanges: []config.ExchangeConfig{
{
Name: "fake",
Enabled: true,
CurrencyPairs: &currency.PairsManager{
UseGlobalFormat: true,
ConfigFormat: &currency.PairFormat{},
Pairs: map[asset.Item]*currency.PairStore{
asset.Spot: &ps,
},
},
},
},
},
ExchangeManager: &ExchangeManager{
exchanges: map[string]exchange.IBotExchange{
"fake": fakeDepositExchange{
fakeDepositExchangeOpts: opts,
},
},
},
}
}
func TestGetCryptocurrencyDepositAddressesByExchange(t *testing.T) {
t.Parallel()
const exchName = "fake"
e := createDepositEngine(&fakeDepositExchangeOpts{SupportsAuth: true, SupportsMultiChain: true})
_, err := e.GetCryptocurrencyDepositAddressesByExchange(exchName)
if err != nil {
t.Error(err)
}
if _, err = e.GetCryptocurrencyDepositAddressesByExchange("non-existent"); !errors.Is(err, ErrExchangeNotFound) {
t.Errorf("received %s, expected: %s", err, ErrExchangeNotFound)
}
e.DepositAddressManager = SetupDepositAddressManager()
_, err = e.GetCryptocurrencyDepositAddressesByExchange(exchName)
if err == nil {
t.Error("expected error")
}
if err = e.DepositAddressManager.Sync(e.GetAllExchangeCryptocurrencyDepositAddresses()); err != nil {
t.Fatal(err)
}
_, err = e.GetCryptocurrencyDepositAddressesByExchange(exchName)
if err != nil {
t.Error(err)
}
}
func TestGetExchangeCryptocurrencyDepositAddress(t *testing.T) {
t.Parallel()
e := createDepositEngine(&fakeDepositExchangeOpts{SupportsAuth: true, SupportsMultiChain: true})
const exchName = "fake"
if _, err := e.GetExchangeCryptocurrencyDepositAddress(context.Background(), "non-existent", "", "", currency.BTC, false); !errors.Is(err, ErrExchangeNotFound) {
t.Errorf("received %s, expected: %s", err, ErrExchangeNotFound)
}
r, err := e.GetExchangeCryptocurrencyDepositAddress(context.Background(), exchName, "", "", currency.BTC, false)
if err != nil {
t.Error(err)
}
if r.Address != "fakeaddr" {
t.Error("unexpected address")
}
e.DepositAddressManager = SetupDepositAddressManager()
if err := e.DepositAddressManager.Sync(e.GetAllExchangeCryptocurrencyDepositAddresses()); err != nil {
t.Fatal(err)
}
if _, err := e.GetExchangeCryptocurrencyDepositAddress(context.Background(), "meow", "", "", currency.BTC, false); !errors.Is(err, ErrExchangeNotFound) {
t.Errorf("received %s, expected: %s", err, ErrExchangeNotFound)
}
if _, err := e.GetExchangeCryptocurrencyDepositAddress(context.Background(), exchName, "", "", currency.BTC, false); err != nil {
t.Error(err)
}
}
func TestGetAllExchangeCryptocurrencyDepositAddresses(t *testing.T) {
t.Parallel()
e := createDepositEngine(&fakeDepositExchangeOpts{})
if r := e.GetAllExchangeCryptocurrencyDepositAddresses(); len(r) > 0 {
t.Error("should have no addresses returned for an unauthenticated exchange")
}
e = createDepositEngine(&fakeDepositExchangeOpts{SupportsAuth: true, ThrowPairError: true})
if r := e.GetAllExchangeCryptocurrencyDepositAddresses(); len(r) > 0 {
t.Error("should have no cryptos returned for no enabled pairs")
}
e = createDepositEngine(&fakeDepositExchangeOpts{SupportsAuth: true, SupportsMultiChain: true, ThrowTransferChainError: true})
if r := e.GetAllExchangeCryptocurrencyDepositAddresses(); len(r["fake"]) != 0 {
t.Error("should have returned no deposit addresses for a fake exchange with transfer error")
}
e = createDepositEngine(&fakeDepositExchangeOpts{SupportsAuth: true, SupportsMultiChain: true, ThrowDepositAddressError: true})
if r := e.GetAllExchangeCryptocurrencyDepositAddresses(); len(r["fake"]["btc"]) != 0 {
t.Error("should have returned no deposit addresses for fake exchange with deposit error, with multichain support enabled")
}
e = createDepositEngine(&fakeDepositExchangeOpts{SupportsAuth: true, SupportsMultiChain: true, RequiresChainSet: true})
if r := e.GetAllExchangeCryptocurrencyDepositAddresses(); len(r["fake"]["btc"]) == 0 {
t.Error("should of returned a BTC address")
}
e = createDepositEngine(&fakeDepositExchangeOpts{SupportsAuth: true, SupportsMultiChain: true})
if r := e.GetAllExchangeCryptocurrencyDepositAddresses(); len(r["fake"]["btc"]) == 0 {
t.Error("should of returned a BTC address")
}
e = createDepositEngine(&fakeDepositExchangeOpts{SupportsAuth: true})
if r := e.GetAllExchangeCryptocurrencyDepositAddresses(); len(r["fake"]["xrp"]) == 0 {
t.Error("should have returned a XRP address")
}
}
func TestGetExchangeNames(t *testing.T) {
t.Parallel()
bot := CreateTestBot(t)

View File

@@ -17,6 +17,7 @@ import (
"github.com/gofrs/uuid"
grpcauth "github.com/grpc-ecosystem/go-grpc-middleware/auth"
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
"github.com/pquerna/otp/totp"
"github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/common/crypto"
"github.com/thrasher-corp/gocryptotrader/common/file"
@@ -62,6 +63,7 @@ var (
errAssetTypeUnset = errors.New("asset type unset")
errDispatchSystem = errors.New("dispatch system offline")
errCurrencyNotEnabled = errors.New("currency not enabled")
errCurrencyNotSpecified = errors.New("a currency must be specified")
errCurrencyPairInvalid = errors.New("currency provided is not found in the available pairs list")
errNoTrades = errors.New("no trades returned from supplied params")
errNilRequestData = errors.New("nil request data received, cannot continue")
@@ -1439,6 +1441,7 @@ func (s *RPCServer) CancelAllOrders(ctx context.Context, r *gctrpc.CancelAllOrde
}, nil
}
// ModifyOrder modifies an existing order if it exists
func (s *RPCServer) ModifyOrder(ctx context.Context, r *gctrpc.ModifyOrderRequest) (*gctrpc.ModifyOrderResponse, error) {
assetType, err := asset.New(r.Asset)
if err != nil {
@@ -1531,28 +1534,90 @@ func (s *RPCServer) RemoveEvent(ctx context.Context, r *gctrpc.RemoveEventReques
// GetCryptocurrencyDepositAddresses returns a list of cryptocurrency deposit
// addresses specified by an exchange
func (s *RPCServer) GetCryptocurrencyDepositAddresses(ctx context.Context, r *gctrpc.GetCryptocurrencyDepositAddressesRequest) (*gctrpc.GetCryptocurrencyDepositAddressesResponse, error) {
_, err := s.GetExchangeByName(r.Exchange)
exch, err := s.GetExchangeByName(r.Exchange)
if err != nil {
return nil, err
}
if !exch.GetAuthenticatedAPISupport(exchange.RestAuthentication) {
return nil, exchange.ErrAuthenticatedRequestWithoutCredentialsSet
}
result, err := s.GetCryptocurrencyDepositAddressesByExchange(r.Exchange)
return &gctrpc.GetCryptocurrencyDepositAddressesResponse{Addresses: result}, err
if err != nil {
return nil, err
}
var resp gctrpc.GetCryptocurrencyDepositAddressesResponse
resp.Addresses = make(map[string]*gctrpc.DepositAddresses)
for k, v := range result {
var depositAddrs []*gctrpc.DepositAddress
for a := range v {
depositAddrs = append(depositAddrs, &gctrpc.DepositAddress{
Address: v[a].Address,
Tag: v[a].Tag,
Chain: v[a].Chain,
})
}
resp.Addresses[k] = &gctrpc.DepositAddresses{Addresses: depositAddrs}
}
return &resp, nil
}
// GetCryptocurrencyDepositAddress returns a cryptocurrency deposit address
// specified by exchange and cryptocurrency
func (s *RPCServer) GetCryptocurrencyDepositAddress(ctx context.Context, r *gctrpc.GetCryptocurrencyDepositAddressRequest) (*gctrpc.GetCryptocurrencyDepositAddressResponse, error) {
_, err := s.GetExchangeByName(r.Exchange)
exch, err := s.GetExchangeByName(r.Exchange)
if err != nil {
return nil, err
}
if !exch.GetAuthenticatedAPISupport(exchange.RestAuthentication) {
return nil, exchange.ErrAuthenticatedRequestWithoutCredentialsSet
}
addr, err := s.GetExchangeCryptocurrencyDepositAddress(ctx,
r.Exchange,
"",
currency.NewCode(r.Cryptocurrency))
return &gctrpc.GetCryptocurrencyDepositAddressResponse{Address: addr}, err
r.Chain,
currency.NewCode(r.Cryptocurrency),
r.Bypass,
)
if err != nil {
return nil, err
}
return &gctrpc.GetCryptocurrencyDepositAddressResponse{
Address: addr.Address,
Tag: addr.Tag,
}, nil
}
// GetAvailableTransferChains returns the supported transfer chains specified by
// exchange and cryptocurrency
func (s *RPCServer) GetAvailableTransferChains(ctx context.Context, r *gctrpc.GetAvailableTransferChainsRequest) (*gctrpc.GetAvailableTransferChainsResponse, error) {
exch, err := s.GetExchangeByName(r.Exchange)
if err != nil {
return nil, err
}
curr := currency.NewCode(r.Cryptocurrency)
if curr.IsEmpty() {
return nil, errCurrencyNotSpecified
}
resp, err := exch.GetAvailableTransferChains(ctx, curr)
if err != nil {
return nil, err
}
if len(resp) == 0 {
return nil, errors.New("no available transfer chains found")
}
return &gctrpc.GetAvailableTransferChainsResponse{
Chains: resp,
}, nil
}
// WithdrawCryptocurrencyFunds withdraws cryptocurrency funds specified by
@@ -1573,9 +1638,38 @@ func (s *RPCServer) WithdrawCryptocurrencyFunds(ctx context.Context, r *gctrpc.W
Address: r.Address,
AddressTag: r.AddressTag,
FeeAmount: r.Fee,
Chain: r.Chain,
},
}
exchCfg, err := s.Config.GetExchangeConfig(r.Exchange)
if err != nil {
return nil, err
}
if exchCfg.API.Credentials.OTPSecret != "" {
code, errOTP := totp.GenerateCode(exchCfg.API.Credentials.OTPSecret, time.Now())
if errOTP != nil {
return nil, errOTP
}
codeNum, errOTP := strconv.ParseInt(code, 10, 64)
if errOTP != nil {
return nil, errOTP
}
request.OneTimePassword = codeNum
}
if exchCfg.API.Credentials.PIN != "" {
pinCode, errPin := strconv.ParseInt(exchCfg.API.Credentials.PIN, 10, 64)
if err != nil {
return nil, errPin
}
request.PIN = pinCode
}
request.TradePassword = exchCfg.API.Credentials.TradePassword
resp, err := s.Engine.WithdrawManager.SubmitWithdrawal(ctx, request)
if err != nil {
return nil, err
@@ -1618,6 +1712,34 @@ func (s *RPCServer) WithdrawFiatFunds(ctx context.Context, r *gctrpc.WithdrawFia
},
}
exchCfg, err := s.Config.GetExchangeConfig(r.Exchange)
if err != nil {
return nil, err
}
if exchCfg.API.Credentials.OTPSecret != "" {
code, errOTP := totp.GenerateCode(exchCfg.API.Credentials.OTPSecret, time.Now())
if err != nil {
return nil, errOTP
}
codeNum, errOTP := strconv.ParseInt(code, 10, 64)
if err != nil {
return nil, errOTP
}
request.OneTimePassword = codeNum
}
if exchCfg.API.Credentials.PIN != "" {
pinCode, errPIN := strconv.ParseInt(exchCfg.API.Credentials.PIN, 10, 64)
if err != nil {
return nil, errPIN
}
request.PIN = pinCode
}
request.TradePassword = exchCfg.API.Credentials.TradePassword
resp, err := s.Engine.WithdrawManager.SubmitWithdrawal(ctx, request)
if err != nil {
return nil, err

View File

@@ -86,10 +86,10 @@ func (m *WithdrawManager) SubmitWithdrawal(ctx context.Context, req *withdraw.Re
}
}
}
dbwithdraw.Event(resp)
if err == nil {
withdraw.Cache.Add(resp.ID, resp)
}
dbwithdraw.Event(resp)
return resp, err
}