mirror of
https://github.com/d0zingcat/gocryptotrader.git
synced 2026-05-13 15:09:42 +00:00
portfolio: Fix CryptoID balance issue and assertify tests (#1861)
* portfolio: Fix CryptoID balance issue and assertify tests * portfolio: Expand context usage, enhance tests and a few other minor improvements * portfolio: Further improvements and enhance common.IsValidCryptoAddress * config, portfolio: Use v6.DefaultConfig, switch to context.WithCancel
This commit is contained in:
@@ -77,8 +77,7 @@ func main() {
|
||||
log.Println("Loaded config file.")
|
||||
|
||||
displayCurrency = cfg.Currency.FiatDisplayCurrency
|
||||
port := portfolio.Base{}
|
||||
port.Seed(cfg.Portfolio)
|
||||
port := cfg.Portfolio
|
||||
result := port.GetPortfolioSummary()
|
||||
|
||||
log.Println("Fetched portfolio data.")
|
||||
|
||||
@@ -54,23 +54,24 @@ var (
|
||||
|
||||
// Public common Errors
|
||||
var (
|
||||
ErrNotYetImplemented = errors.New("not yet implemented")
|
||||
ErrFunctionNotSupported = errors.New("unsupported wrapper function")
|
||||
errInvalidCryptoCurrency = errors.New("invalid crypto currency")
|
||||
ErrDateUnset = errors.New("date unset")
|
||||
ErrStartAfterEnd = errors.New("start date after end date")
|
||||
ErrStartEqualsEnd = errors.New("start date equals end date")
|
||||
ErrStartAfterTimeNow = errors.New("start date is after current time")
|
||||
ErrNilPointer = errors.New("nil pointer")
|
||||
ErrEmptyParams = errors.New("empty parameters")
|
||||
ErrCannotCalculateOffline = errors.New("cannot calculate offline, unsupported")
|
||||
ErrNoResponse = errors.New("no response")
|
||||
ErrTypeAssertFailure = errors.New("type assert failure")
|
||||
ErrNoResults = errors.New("no results found")
|
||||
ErrUnknownError = errors.New("unknown error")
|
||||
ErrGettingField = errors.New("error getting field")
|
||||
ErrSettingField = errors.New("error setting field")
|
||||
ErrParsingWSField = errors.New("error parsing websocket field")
|
||||
ErrNotYetImplemented = errors.New("not yet implemented")
|
||||
ErrFunctionNotSupported = errors.New("unsupported wrapper function")
|
||||
ErrAddressIsEmptyOrInvalid = errors.New("address is empty or invalid")
|
||||
ErrUnsupportedCryptocurrency = errors.New("unsupported cryptocurrency") // TODO: Remove me, used because of an import cycle if we use the currency package
|
||||
ErrDateUnset = errors.New("date unset")
|
||||
ErrStartAfterEnd = errors.New("start date after end date")
|
||||
ErrStartEqualsEnd = errors.New("start date equals end date")
|
||||
ErrStartAfterTimeNow = errors.New("start date is after current time")
|
||||
ErrNilPointer = errors.New("nil pointer")
|
||||
ErrEmptyParams = errors.New("empty parameters")
|
||||
ErrCannotCalculateOffline = errors.New("cannot calculate offline, unsupported")
|
||||
ErrNoResponse = errors.New("no response")
|
||||
ErrTypeAssertFailure = errors.New("type assert failure")
|
||||
ErrNoResults = errors.New("no results found")
|
||||
ErrUnknownError = errors.New("unknown error")
|
||||
ErrGettingField = errors.New("error getting field")
|
||||
ErrSettingField = errors.New("error setting field")
|
||||
ErrParsingWSField = errors.New("error parsing websocket field")
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -190,17 +191,30 @@ func IsEnabled(isEnabled bool) string {
|
||||
// IsValidCryptoAddress validates your cryptocurrency address string using the
|
||||
// regexp package // Validation issues occurring because "3" is contained in
|
||||
// litecoin and Bitcoin addresses - non-fatal
|
||||
func IsValidCryptoAddress(address, crypto string) (bool, error) {
|
||||
func IsValidCryptoAddress(address, crypto string) error {
|
||||
var matched bool
|
||||
var err error
|
||||
|
||||
switch strings.ToLower(crypto) {
|
||||
case "btc":
|
||||
return regexp.MatchString("^(bc1|[13])[a-zA-HJ-NP-Z0-9]{25,90}$", address)
|
||||
matched, err = regexp.MatchString("^(bc1|[13])[a-zA-HJ-NP-Z0-9]{25,90}$", address)
|
||||
case "ltc":
|
||||
return regexp.MatchString("^[L3M][a-km-zA-HJ-NP-Z1-9]{25,34}$", address)
|
||||
matched, err = regexp.MatchString("^[L3M][a-km-zA-HJ-NP-Z1-9]{25,34}$", address)
|
||||
case "eth":
|
||||
return regexp.MatchString("^0x[a-km-z0-9]{40}$", address)
|
||||
matched, err = regexp.MatchString("^0x[a-km-z0-9]{40}$", address)
|
||||
default:
|
||||
return false, fmt.Errorf("%w %s", errInvalidCryptoCurrency, crypto)
|
||||
return fmt.Errorf("%w: %q", ErrUnsupportedCryptocurrency, crypto)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !matched {
|
||||
return fmt.Errorf("%w: %q", ErrAddressIsEmptyOrInvalid, address)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// YesOrNo returns a boolean variable to check if input is "y" or "yes"
|
||||
|
||||
@@ -137,101 +137,30 @@ func TestIsEnabled(t *testing.T) {
|
||||
|
||||
func TestIsValidCryptoAddress(t *testing.T) {
|
||||
t.Parallel()
|
||||
b, err := IsValidCryptoAddress("1Mz7153HMuxXTuR2R1t78mGSdzaAtNbBWX", "bTC")
|
||||
if !errors.Is(err, nil) {
|
||||
t.Errorf("received '%v' expected '%v'", err, nil)
|
||||
}
|
||||
if !b {
|
||||
t.Errorf("expected address '%s' to be valid", "1Mz7153HMuxXTuR2R1t78mGSdzaAtNbBWX")
|
||||
|
||||
tests := []struct {
|
||||
name, addr, code string
|
||||
err error
|
||||
}{
|
||||
{"Valid BTC legacy", "1Mz7153HMuxXTuR2R1t78mGSdzaAtNbBWX", "bTC", nil},
|
||||
{"Valid BTC bech32", "bc1qw508d6qejxtdg4y5r3zarvaly0c5xw7kv8f3t4", "bTC", nil},
|
||||
{"Invalid BTC (too long)", "an84characterslonghumanreadablepartthatcontainsthenumber1andtheexcludedcharactersbio1569pvx", "bTC", ErrAddressIsEmptyOrInvalid},
|
||||
{"Valid BTC bech32 (longer)", "bc1qc7slrfxkknqcq2jevvvkdgvrt8080852dfjewde450xdlk4ugp7szw5tk9", "bTC", nil},
|
||||
{"Invalid BTC (starts with 0)", "0Mz7153HMuxXTuR2R1t78mGSdzaAtNbBWX", "bTC", ErrAddressIsEmptyOrInvalid},
|
||||
{"Invalid LTC (BTC address)", "1Mz7153HMuxXTuR2R1t78mGSdzaAtNbBWX", "lTc", ErrAddressIsEmptyOrInvalid},
|
||||
{"Valid LTC", "3CDJNfdWX8m2NwuGUV3nhXHXEeLygMXoAj", "lTc", nil},
|
||||
{"Invalid LTC (starts with N)", "NCDJNfdWX8m2NwuGUV3nhXHXEeLygMXoAj", "lTc", ErrAddressIsEmptyOrInvalid},
|
||||
{"Valid ETH", "0xb794f5ea0ba39494ce839613fffba74279579268", "eth", nil},
|
||||
{"Invalid ETH (starts with xx)", "xxb794f5ea0ba39494ce839613fffba74279579268", "eth", ErrAddressIsEmptyOrInvalid},
|
||||
{"Unsupported crypto", "xxb794f5ea0ba39494ce839613fffba74279579268", "wif", ErrUnsupportedCryptocurrency},
|
||||
{"Empty address", "", "btc", ErrAddressIsEmptyOrInvalid},
|
||||
}
|
||||
|
||||
b, err = IsValidCryptoAddress("bc1qw508d6qejxtdg4y5r3zarvaly0c5xw7kv8f3t4", "bTC")
|
||||
if !errors.Is(err, nil) {
|
||||
t.Errorf("received '%v' expected '%v'", err, nil)
|
||||
}
|
||||
if !b {
|
||||
t.Errorf("expected address '%s' to be valid", "bc1qw508d6qejxtdg4y5r3zarvaly0c5xw7kv8f3t4")
|
||||
}
|
||||
|
||||
b, err = IsValidCryptoAddress("an84characterslonghumanreadablepartthatcontainsthenumber1andtheexcludedcharactersbio1569pvx", "bTC")
|
||||
if !errors.Is(err, nil) {
|
||||
t.Errorf("received '%v' expected '%v'", err, nil)
|
||||
}
|
||||
if b {
|
||||
t.Errorf("expected address '%s' to be invalid", "an84characterslonghumanreadablepartthatcontainsthenumber1andtheexcludedcharactersbio1569pvx")
|
||||
}
|
||||
|
||||
b, err = IsValidCryptoAddress("bc1qc7slrfxkknqcq2jevvvkdgvrt8080852dfjewde450xdlk4ugp7szw5tk9", "bTC")
|
||||
if !errors.Is(err, nil) {
|
||||
t.Errorf("received '%v' expected '%v'", err, nil)
|
||||
}
|
||||
if !b {
|
||||
t.Errorf("expected address '%s' to be valid", "bc1qc7slrfxkknqcq2jevvvkdgvrt8080852dfjewde450xdlk4ugp7szw5tk9")
|
||||
}
|
||||
|
||||
b, err = IsValidCryptoAddress("0Mz7153HMuxXTuR2R1t78mGSdzaAtNbBWX", "btc")
|
||||
if !errors.Is(err, nil) {
|
||||
t.Errorf("received '%v' expected '%v'", err, nil)
|
||||
}
|
||||
if b {
|
||||
t.Errorf("expected address '%s' to be invalid", "0Mz7153HMuxXTuR2R1t78mGSdzaAtNbBWX")
|
||||
}
|
||||
|
||||
b, err = IsValidCryptoAddress("1Mz7153HMuxXTuR2R1t78mGSdzaAtNbBWX", "lTc")
|
||||
if !errors.Is(err, nil) {
|
||||
t.Errorf("received '%v' expected '%v'", err, nil)
|
||||
}
|
||||
if b {
|
||||
t.Errorf("expected address '%s' to be invalid", "1Mz7153HMuxXTuR2R1t78mGSdzaAtNbBWX")
|
||||
}
|
||||
|
||||
b, err = IsValidCryptoAddress("3CDJNfdWX8m2NwuGUV3nhXHXEeLygMXoAj", "ltc")
|
||||
if !errors.Is(err, nil) {
|
||||
t.Errorf("received '%v' expected '%v'", err, nil)
|
||||
}
|
||||
if !b {
|
||||
t.Errorf("expected address '%s' to be valid", "3CDJNfdWX8m2NwuGUV3nhXHXEeLygMXoAj")
|
||||
}
|
||||
|
||||
b, err = IsValidCryptoAddress("NCDJNfdWX8m2NwuGUV3nhXHXEeLygMXoAj", "lTc")
|
||||
if !errors.Is(err, nil) {
|
||||
t.Errorf("received '%v' expected '%v'", err, nil)
|
||||
}
|
||||
if b {
|
||||
t.Errorf("expected address '%s' to be invalid", "NCDJNfdWX8m2NwuGUV3nhXHXEeLygMXoAj")
|
||||
}
|
||||
|
||||
b, err = IsValidCryptoAddress(
|
||||
"0xb794f5ea0ba39494ce839613fffba74279579268",
|
||||
"eth",
|
||||
)
|
||||
if !errors.Is(err, nil) {
|
||||
t.Errorf("received '%v' expected '%v'", err, nil)
|
||||
}
|
||||
if !b {
|
||||
t.Errorf("expected address '%s' to be valid", "0xb794f5ea0ba39494ce839613fffba74279579268")
|
||||
}
|
||||
|
||||
b, err = IsValidCryptoAddress(
|
||||
"xxb794f5ea0ba39494ce839613fffba74279579268",
|
||||
"eTh",
|
||||
)
|
||||
if !errors.Is(err, nil) {
|
||||
t.Errorf("received '%v' expected '%v'", err, nil)
|
||||
}
|
||||
if b {
|
||||
t.Errorf("expected address '%s' to be invalid", "xxb794f5ea0ba39494ce839613fffba74279579268")
|
||||
}
|
||||
|
||||
b, err = IsValidCryptoAddress(
|
||||
"xxb794f5ea0ba39494ce839613fffba74279579268",
|
||||
"ding",
|
||||
)
|
||||
if !errors.Is(err, errInvalidCryptoCurrency) {
|
||||
t.Errorf("received '%v' expected '%v'", err, errInvalidCryptoCurrency)
|
||||
}
|
||||
if b {
|
||||
t.Errorf("expected address '%s' to be invalid", "xxb794f5ea0ba39494ce839613fffba74279579268")
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
require.ErrorIs(t, IsValidCryptoAddress(tc.addr, tc.code), tc.err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -107,7 +107,7 @@ type Config struct {
|
||||
Currency currency.Config `json:"currencyConfig"`
|
||||
Communications base.CommunicationsConfig `json:"communications"`
|
||||
RemoteControl RemoteControlConfig `json:"remoteControl"`
|
||||
Portfolio portfolio.Base `json:"portfolioAddresses"`
|
||||
Portfolio *portfolio.Base `json:"portfolioAddresses"`
|
||||
Exchanges []Exchange `json:"exchanges"`
|
||||
BankAccounts []banking.Account `json:"bankAccounts"`
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ func init() {
|
||||
// Note: Do not be tempted to use a constant for Duration. Whilst defaults are still written to config, we need to manage default upgrades discretely.
|
||||
var defaultFuturesTrackingSeekDuration = strconv.FormatInt(int64(time.Hour)*24*365, 10)
|
||||
|
||||
var defaultConfig = []byte(`{
|
||||
var defaultConfigV5 = []byte(`{
|
||||
"enabled": true,
|
||||
"verbose": false,
|
||||
"activelyTrackFuturesPositions": true,
|
||||
@@ -37,7 +37,7 @@ func (v *Version5) UpgradeConfig(_ context.Context, e []byte) ([]byte, error) {
|
||||
_, valueType, _, err := jsonparser.Get(e, "orderManager", "enabled")
|
||||
switch {
|
||||
case errors.Is(err, jsonparser.KeyPathNotFoundError), valueType == jsonparser.Null:
|
||||
return jsonparser.Set(e, defaultConfig, "orderManager")
|
||||
return jsonparser.Set(e, defaultConfigV5, "orderManager")
|
||||
case err != nil:
|
||||
return e, err
|
||||
}
|
||||
|
||||
34
config/versions/v6.go
Normal file
34
config/versions/v6.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package versions
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/buger/jsonparser"
|
||||
v6 "github.com/thrasher-corp/gocryptotrader/config/versions/v6"
|
||||
)
|
||||
|
||||
// Version6 implements ConfigVersion
|
||||
type Version6 struct{}
|
||||
|
||||
func init() {
|
||||
Manager.registerVersion(6, &Version6{})
|
||||
}
|
||||
|
||||
// UpgradeConfig checks and upgrades the portfolioAddresses.providers field
|
||||
func (v *Version6) UpgradeConfig(_ context.Context, e []byte) ([]byte, error) {
|
||||
_, valueType, _, err := jsonparser.Get(e, "portfolioAddresses", "providers")
|
||||
switch {
|
||||
case errors.Is(err, jsonparser.KeyPathNotFoundError), valueType == jsonparser.Null:
|
||||
return jsonparser.Set(e, v6.DefaultConfig, "portfolioAddresses", "providers")
|
||||
case err != nil:
|
||||
return e, err
|
||||
}
|
||||
return e, nil
|
||||
}
|
||||
|
||||
// DowngradeConfig removes the portfolioAddresses.providers field
|
||||
func (v *Version6) DowngradeConfig(_ context.Context, e []byte) ([]byte, error) {
|
||||
e = jsonparser.Delete(e, "portfolioAddresses", "providers")
|
||||
return e, nil
|
||||
}
|
||||
18
config/versions/v6/types.go
Normal file
18
config/versions/v6/types.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package v6
|
||||
|
||||
// DefaultConfig is the default config used for the version 6 portfolio providers upgrade
|
||||
var DefaultConfig = []byte(`[
|
||||
{
|
||||
"name": "Ethplorer",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"name": "XRPScan",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"name": "CryptoID",
|
||||
"enabled": false,
|
||||
"apiKey": "Key"
|
||||
}
|
||||
]`)
|
||||
52
config/versions/v6_test.go
Normal file
52
config/versions/v6_test.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package versions
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/buger/jsonparser"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
v6 "github.com/thrasher-corp/gocryptotrader/config/versions/v6"
|
||||
)
|
||||
|
||||
var testData = []byte(`{
|
||||
"portfolioAddresses": {
|
||||
"addresses": [
|
||||
{
|
||||
"Address": "1JCe8z4jJVNXSjohjM4i9Hh813dLCNx2Sy",
|
||||
"CoinType": "BTC",
|
||||
"Balance": 0.00108832,
|
||||
"Description": "",
|
||||
"WhiteListed": false,
|
||||
"ColdStorage": false,
|
||||
"SupportedExchanges": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
}`)
|
||||
|
||||
func TestUpgrade(t *testing.T) {
|
||||
t.Parallel()
|
||||
r, err := new(Version6).UpgradeConfig(context.Background(), testData)
|
||||
require.NoError(t, err, "UpgradeConfig must not error")
|
||||
require.True(t, bytes.Contains(r, v6.DefaultConfig))
|
||||
|
||||
r2, err := new(Version6).UpgradeConfig(context.Background(), r)
|
||||
require.NoError(t, err, "UpgradeConfig must not error")
|
||||
assert.Equal(t, r, r2, "UpgradeConfig should not affect an already upgraded config")
|
||||
}
|
||||
|
||||
func TestDowngrade(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
r, err := new(Version6).UpgradeConfig(context.Background(), testData)
|
||||
require.NoError(t, err, "UpgradeConfig must not error")
|
||||
require.True(t, bytes.Contains(r, v6.DefaultConfig))
|
||||
|
||||
r, err = new(Version6).DowngradeConfig(context.Background(), r)
|
||||
require.NoError(t, err, "DowngradeConfig must not error")
|
||||
_, _, _, err = jsonparser.Get(r, "portfolioAddresses", "providers") //nolint:dogsled // Return values not needed
|
||||
assert.ErrorIs(t, err, jsonparser.KeyPathNotFoundError, "providers should be removed")
|
||||
}
|
||||
@@ -258,6 +258,21 @@
|
||||
"ColdStorage": false,
|
||||
"SupportedExchanges": ""
|
||||
}
|
||||
],
|
||||
"providers": [
|
||||
{
|
||||
"name": "Ethplorer",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"name": "XRPScan",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"name": "CryptoID",
|
||||
"enabled": false,
|
||||
"apiKey": "Key"
|
||||
}
|
||||
]
|
||||
},
|
||||
"exchanges": [
|
||||
|
||||
@@ -409,7 +409,7 @@ func (bot *Engine) Start() error {
|
||||
|
||||
if bot.Settings.EnablePortfolioManager {
|
||||
if bot.portfolioManager == nil {
|
||||
if p, err := setupPortfolioManager(bot.ExchangeManager, bot.Settings.PortfolioManagerDelay, &bot.Config.Portfolio); err != nil {
|
||||
if p, err := setupPortfolioManager(bot.ExchangeManager, bot.Settings.PortfolioManagerDelay, bot.Config.Portfolio); err != nil {
|
||||
gctlog.Errorf(gctlog.Global, "portfolio manager unable to setup: %s", err)
|
||||
} else {
|
||||
bot.portfolioManager = p
|
||||
@@ -586,7 +586,7 @@ func (bot *Engine) Stop() {
|
||||
gctlog.Debugln(gctlog.Global, "Engine shutting down..")
|
||||
|
||||
if len(bot.portfolioManager.GetAddresses()) != 0 {
|
||||
bot.Config.Portfolio = *bot.portfolioManager.GetPortfolio()
|
||||
bot.Config.Portfolio = bot.portfolioManager.GetPortfolio()
|
||||
}
|
||||
|
||||
if bot.gctScriptManager.IsRunning() {
|
||||
|
||||
@@ -166,7 +166,7 @@ func (bot *Engine) SetSubsystem(subSystemName string, enable bool) error {
|
||||
case PortfolioManagerName:
|
||||
if enable {
|
||||
if bot.portfolioManager == nil {
|
||||
bot.portfolioManager, err = setupPortfolioManager(bot.ExchangeManager, bot.Settings.PortfolioManagerDelay, &bot.Config.Portfolio)
|
||||
bot.portfolioManager, err = setupPortfolioManager(bot.ExchangeManager, bot.Settings.PortfolioManagerDelay, bot.Config.Portfolio)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -41,6 +41,7 @@ func setupPortfolioManager(e *ExchangeManager, portfolioManagerDelay time.Durati
|
||||
if portfolioManagerDelay <= 0 {
|
||||
portfolioManagerDelay = PortfolioSleepDelay
|
||||
}
|
||||
|
||||
if cfg == nil {
|
||||
cfg = &portfolio.Base{Addresses: []portfolio.Address{}}
|
||||
}
|
||||
@@ -130,21 +131,14 @@ func (m *portfolioManager) processPortfolio() {
|
||||
allExchangesHoldings := m.getExchangeAccountInfo(exchanges)
|
||||
m.seedExchangeAccountInfo(allExchangesHoldings)
|
||||
|
||||
data := m.base.GetPortfolioGroupedCoin()
|
||||
data := m.base.GetPortfolioAddressesGroupedByCoin()
|
||||
for key, value := range data {
|
||||
err := m.base.UpdatePortfolio(value, key)
|
||||
if err != nil {
|
||||
log.Errorf(log.PortfolioMgr,
|
||||
"PortfolioWatcher error %s for currency %s\n",
|
||||
err,
|
||||
key)
|
||||
if err := m.base.UpdatePortfolio(context.TODO(), value, key); err != nil {
|
||||
log.Errorf(log.PortfolioMgr, "Portfolio manager: UpdatePortfolio error: %s for currency %s\n", err, key)
|
||||
continue
|
||||
}
|
||||
|
||||
log.Debugf(log.PortfolioMgr,
|
||||
"Portfolio manager: Successfully updated address balance for %s address(es) %s\n",
|
||||
key,
|
||||
value)
|
||||
log.Debugf(log.PortfolioMgr, "Portfolio manager: Successfully updated address balance for %s address(es) %s\n", key, value)
|
||||
}
|
||||
atomic.CompareAndSwapInt32(&m.processing, 1, 0)
|
||||
}
|
||||
@@ -182,7 +176,7 @@ func (m *portfolioManager) seedExchangeAccountInfo(accounts []account.Holdings)
|
||||
}
|
||||
|
||||
for j := range currencies {
|
||||
if !m.base.ExchangeAddressExists(accounts[x].Exchange, currencies[j].Currency) {
|
||||
if !m.base.ExchangeAddressCoinExists(accounts[x].Exchange, currencies[j].Currency) {
|
||||
if currencies[j].Total <= 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/thrasher-corp/gocryptotrader/currency"
|
||||
"github.com/thrasher-corp/gocryptotrader/encoding/json"
|
||||
"github.com/thrasher-corp/gocryptotrader/log"
|
||||
"golang.org/x/time/rate"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -27,55 +28,71 @@ const (
|
||||
ExchangeAddress = "Exchange"
|
||||
// PersonalAddress is a label for a personal/offline address
|
||||
PersonalAddress = "Personal"
|
||||
|
||||
defaultAPIKey = "Key"
|
||||
defaultInterval = 10 * time.Second
|
||||
)
|
||||
|
||||
var errNotEthAddress = errors.New("not an Ethereum address")
|
||||
var (
|
||||
errProviderNotFound = errors.New("provider not found")
|
||||
errProviderNotEnabled = errors.New("provider not enabled")
|
||||
errProviderAPIKeyNotSet = errors.New("provider API key not set")
|
||||
errPortfolioItemNotFound = errors.New("portfolio item not found")
|
||||
errNoPortfolioItemsToWatch = errors.New("no portfolio items to watch")
|
||||
)
|
||||
|
||||
// GetEthereumBalance single or multiple address information as
|
||||
// EtherchainBalanceResponse
|
||||
func (b *Base) GetEthereumBalance(address string) (EthplorerResponse, error) {
|
||||
valid, _ := common.IsValidCryptoAddress(address, "eth")
|
||||
if !valid {
|
||||
return EthplorerResponse{}, errNotEthAddress
|
||||
// GetEthereumAddressBalance fetches Ethereum address balance for a given address
|
||||
func (b *Base) GetEthereumAddressBalance(ctx context.Context, address string) (float64, error) {
|
||||
if err := common.IsValidCryptoAddress(address, "eth"); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
urlPath := fmt.Sprintf(
|
||||
"%s/%s/%s?apiKey=freekey", ethplorerAPIURL, ethplorerAddressInfo, address,
|
||||
)
|
||||
apiKey := "freekey"
|
||||
if p, ok := b.Providers.GetProvider("ethplorer"); ok && p.APIKey != "" {
|
||||
apiKey = p.APIKey
|
||||
}
|
||||
|
||||
result := EthplorerResponse{}
|
||||
contents, err := common.SendHTTPRequest(context.TODO(),
|
||||
http.MethodGet,
|
||||
urlPath,
|
||||
nil,
|
||||
nil,
|
||||
b.Verbose)
|
||||
urlPath := ethplorerAPIURL + "/" + ethplorerAddressInfo + "/" + address + "?apiKey=" + apiKey
|
||||
|
||||
contents, err := common.SendHTTPRequest(ctx, http.MethodGet, urlPath, nil, nil, b.Verbose)
|
||||
if err != nil {
|
||||
return result, err
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return result, json.Unmarshal(contents, &result)
|
||||
var result EthplorerResponse
|
||||
if err := json.Unmarshal(contents, &result); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return result.ETH.Balance, nil
|
||||
}
|
||||
|
||||
// GetCryptoIDAddress queries CryptoID for an address balance for a
|
||||
// specified cryptocurrency
|
||||
func (b *Base) GetCryptoIDAddress(address string, coinType currency.Code) (float64, error) {
|
||||
ok, err := common.IsValidCryptoAddress(address, coinType.String())
|
||||
if !ok || err != nil {
|
||||
return 0, errors.New("invalid address")
|
||||
// GetCryptoIDAddressBalance fetches the address balance for a specified cryptocurrency
|
||||
func (b *Base) GetCryptoIDAddressBalance(ctx context.Context, address string, coinType currency.Code) (float64, error) {
|
||||
if err := common.IsValidCryptoAddress(address, coinType.String()); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%s/%s/api.dws?q=getbalance&a=%s",
|
||||
cryptoIDAPIURL,
|
||||
coinType.Lower(),
|
||||
address)
|
||||
p, ok := b.Providers.GetProvider("cryptoid")
|
||||
if !ok {
|
||||
return 0, fmt.Errorf("cryptoid: %w", errProviderNotFound)
|
||||
}
|
||||
|
||||
contents, err := common.SendHTTPRequest(context.TODO(),
|
||||
http.MethodGet,
|
||||
url,
|
||||
nil,
|
||||
nil,
|
||||
b.Verbose)
|
||||
if p.APIKey == "" || p.APIKey == defaultAPIKey {
|
||||
return 0, fmt.Errorf("cryptoid: %w", errProviderAPIKeyNotSet)
|
||||
}
|
||||
|
||||
b.cryptoIDLimiterOnce.Do(func() {
|
||||
b.cryptoIDLimiter = rate.NewLimiter(rate.Every(10*time.Second), 1)
|
||||
})
|
||||
|
||||
if err := b.cryptoIDLimiter.Wait(ctx); err != nil {
|
||||
return 0, fmt.Errorf("rate limiter wait error: %w", err)
|
||||
}
|
||||
|
||||
urlPath := cryptoIDAPIURL + "/" + coinType.Lower().String() + "/api.dws?q=getbalance&a=" + address + "&key=" + p.APIKey
|
||||
|
||||
contents, err := common.SendHTTPRequest(ctx, http.MethodGet, urlPath, nil, nil, b.Verbose)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
@@ -84,83 +101,78 @@ func (b *Base) GetCryptoIDAddress(address string, coinType currency.Code) (float
|
||||
return result, json.Unmarshal(contents, &result)
|
||||
}
|
||||
|
||||
// GetRippleBalance returns the value for a ripple address
|
||||
func (b *Base) GetRippleBalance(address string) (float64, error) {
|
||||
// GetRippleAddressBalance returns the value for a ripple address
|
||||
func (b *Base) GetRippleAddressBalance(ctx context.Context, address string) (float64, error) {
|
||||
contents, err := common.SendHTTPRequest(ctx, http.MethodGet, xrpScanAPIURL+address, nil, nil, b.Verbose)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
var result XRPScanAccount
|
||||
contents, err := common.SendHTTPRequest(context.TODO(),
|
||||
http.MethodGet,
|
||||
xrpScanAPIURL+address,
|
||||
nil,
|
||||
nil,
|
||||
b.Verbose)
|
||||
if err != nil {
|
||||
if err := json.Unmarshal(contents, &result); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
err = json.Unmarshal(contents, &result)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if (result == XRPScanAccount{}) {
|
||||
return 0, errors.New("no balance info returned")
|
||||
}
|
||||
|
||||
return result.XRPBalance, nil
|
||||
}
|
||||
|
||||
// GetAddressBalance accesses the portfolio base and returns the balance by passed
|
||||
// in address, coin type and description
|
||||
func (b *Base) GetAddressBalance(address, description string, coinType currency.Code) (float64, bool) {
|
||||
for x := range b.Addresses {
|
||||
if b.Addresses[x].Address == address &&
|
||||
b.Addresses[x].Description == description &&
|
||||
b.Addresses[x].CoinType.Equal(coinType) {
|
||||
return b.Addresses[x].Balance, true
|
||||
}
|
||||
b.mtx.RLock()
|
||||
defer b.mtx.RUnlock()
|
||||
|
||||
idx := slices.IndexFunc(b.Addresses, func(a Address) bool {
|
||||
return a.Address == address && a.Description == description && a.CoinType.Equal(coinType)
|
||||
})
|
||||
if idx == -1 {
|
||||
return 0, false
|
||||
}
|
||||
return 0, false
|
||||
return b.Addresses[idx].Balance, true
|
||||
}
|
||||
|
||||
// ExchangeExists checks to see if an exchange exists in the portfolio base
|
||||
func (b *Base) ExchangeExists(exchangeName string) bool {
|
||||
for x := range b.Addresses {
|
||||
if b.Addresses[x].Address == exchangeName {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
b.mtx.RLock()
|
||||
defer b.mtx.RUnlock()
|
||||
|
||||
return slices.ContainsFunc(b.Addresses, func(a Address) bool {
|
||||
return a.Address == exchangeName && a.Description == ExchangeAddress
|
||||
})
|
||||
}
|
||||
|
||||
// AddressExists checks to see if there is an address associated with the
|
||||
// portfolio base
|
||||
func (b *Base) AddressExists(address string) bool {
|
||||
for x := range b.Addresses {
|
||||
if b.Addresses[x].Address == address {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
b.mtx.RLock()
|
||||
defer b.mtx.RUnlock()
|
||||
|
||||
return slices.ContainsFunc(b.Addresses, func(a Address) bool {
|
||||
return a.Address == address
|
||||
})
|
||||
}
|
||||
|
||||
// ExchangeAddressExists checks to see if there is an exchange address
|
||||
// ExchangeAddressCoinExists checks to see if there is an exchange address
|
||||
// associated with the portfolio base
|
||||
func (b *Base) ExchangeAddressExists(exchangeName string, coinType currency.Code) bool {
|
||||
for x := range b.Addresses {
|
||||
if b.Addresses[x].Address == exchangeName && b.Addresses[x].CoinType.Equal(coinType) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
func (b *Base) ExchangeAddressCoinExists(exchangeName string, coinType currency.Code) bool {
|
||||
b.mtx.RLock()
|
||||
defer b.mtx.RUnlock()
|
||||
|
||||
return slices.ContainsFunc(b.Addresses, func(a Address) bool {
|
||||
return a.Address == exchangeName && a.CoinType.Equal(coinType) && a.Description == ExchangeAddress
|
||||
})
|
||||
}
|
||||
|
||||
// AddExchangeAddress adds an exchange address to the portfolio base
|
||||
func (b *Base) AddExchangeAddress(exchangeName string, coinType currency.Code, balance float64) {
|
||||
if b.ExchangeAddressExists(exchangeName, coinType) {
|
||||
if b.ExchangeAddressCoinExists(exchangeName, coinType) {
|
||||
b.UpdateExchangeAddressBalance(exchangeName, coinType, balance)
|
||||
return
|
||||
}
|
||||
|
||||
b.mtx.Lock()
|
||||
defer b.mtx.Unlock()
|
||||
|
||||
b.Addresses = append(b.Addresses, Address{
|
||||
Address: exchangeName,
|
||||
CoinType: coinType,
|
||||
@@ -171,6 +183,9 @@ func (b *Base) AddExchangeAddress(exchangeName string, coinType currency.Code, b
|
||||
|
||||
// UpdateAddressBalance updates the portfolio base balance
|
||||
func (b *Base) UpdateAddressBalance(address string, amount float64) {
|
||||
b.mtx.Lock()
|
||||
defer b.mtx.Unlock()
|
||||
|
||||
for x := range b.Addresses {
|
||||
if b.Addresses[x].Address == address {
|
||||
b.Addresses[x].Balance = amount
|
||||
@@ -180,6 +195,9 @@ func (b *Base) UpdateAddressBalance(address string, amount float64) {
|
||||
|
||||
// RemoveExchangeAddress removes an exchange address from the portfolio.
|
||||
func (b *Base) RemoveExchangeAddress(exchangeName string, coinType currency.Code) {
|
||||
b.mtx.Lock()
|
||||
defer b.mtx.Unlock()
|
||||
|
||||
b.Addresses = slices.Clip(slices.DeleteFunc(b.Addresses, func(a Address) bool {
|
||||
return a.Address == exchangeName && a.CoinType.Equal(coinType)
|
||||
}))
|
||||
@@ -188,6 +206,9 @@ func (b *Base) RemoveExchangeAddress(exchangeName string, coinType currency.Code
|
||||
// UpdateExchangeAddressBalance updates the portfolio balance when checked
|
||||
// against correct exchangeName and coinType.
|
||||
func (b *Base) UpdateExchangeAddressBalance(exchangeName string, coinType currency.Code, balance float64) {
|
||||
b.mtx.Lock()
|
||||
defer b.mtx.Unlock()
|
||||
|
||||
for x := range b.Addresses {
|
||||
if b.Addresses[x].Address == exchangeName && b.Addresses[x].CoinType.Equal(coinType) {
|
||||
b.Addresses[x].Balance = balance
|
||||
@@ -195,14 +216,14 @@ func (b *Base) UpdateExchangeAddressBalance(exchangeName string, coinType curren
|
||||
}
|
||||
}
|
||||
|
||||
// AddAddress adds an address to the portfolio base
|
||||
// AddAddress adds an address to the portfolio base or updates its balance if it already exists
|
||||
func (b *Base) AddAddress(address, description string, coinType currency.Code, balance float64) error {
|
||||
if address == "" {
|
||||
return errors.New("address is empty")
|
||||
return common.ErrAddressIsEmptyOrInvalid
|
||||
}
|
||||
|
||||
if coinType.String() == "" {
|
||||
return errors.New("coin type is empty")
|
||||
if coinType.IsEmpty() {
|
||||
return currency.ErrCurrencyCodeEmpty
|
||||
}
|
||||
|
||||
if description == ExchangeAddress {
|
||||
@@ -211,6 +232,9 @@ func (b *Base) AddAddress(address, description string, coinType currency.Code, b
|
||||
}
|
||||
|
||||
if !b.AddressExists(address) {
|
||||
b.mtx.Lock()
|
||||
defer b.mtx.Unlock()
|
||||
|
||||
b.Addresses = append(b.Addresses, Address{
|
||||
Address: address,
|
||||
CoinType: coinType,
|
||||
@@ -220,11 +244,6 @@ func (b *Base) AddAddress(address, description string, coinType currency.Code, b
|
||||
return nil
|
||||
}
|
||||
|
||||
if balance <= 0 {
|
||||
if err := b.RemoveAddress(address, description, coinType); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
b.UpdateAddressBalance(address, balance)
|
||||
return nil
|
||||
}
|
||||
@@ -233,84 +252,86 @@ func (b *Base) AddAddress(address, description string, coinType currency.Code, b
|
||||
// coinType
|
||||
func (b *Base) RemoveAddress(address, description string, coinType currency.Code) error {
|
||||
if address == "" {
|
||||
return errors.New("address is empty")
|
||||
return common.ErrAddressIsEmptyOrInvalid
|
||||
}
|
||||
|
||||
if coinType.String() == "" {
|
||||
return errors.New("coin type is empty")
|
||||
if coinType.IsEmpty() {
|
||||
return currency.ErrCurrencyCodeEmpty
|
||||
}
|
||||
|
||||
b.mtx.Lock()
|
||||
defer b.mtx.Unlock()
|
||||
|
||||
idx := slices.IndexFunc(b.Addresses, func(a Address) bool {
|
||||
return a.Address == address && a.CoinType.Equal(coinType) && a.Description == description
|
||||
})
|
||||
if idx == -1 {
|
||||
return errors.New("portfolio item does not exist")
|
||||
return errPortfolioItemNotFound
|
||||
}
|
||||
b.Addresses = slices.Clip(slices.Delete(b.Addresses, idx, idx+1))
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdatePortfolio adds to the portfolio addresses by coin type
|
||||
func (b *Base) UpdatePortfolio(addresses []string, coinType currency.Code) error {
|
||||
if strings.Contains(strings.Join(addresses, ","), ExchangeAddress) ||
|
||||
strings.Contains(strings.Join(addresses, ","), PersonalAddress) {
|
||||
func (b *Base) UpdatePortfolio(ctx context.Context, addresses []string, coinType currency.Code) error {
|
||||
if slices.ContainsFunc(addresses, func(a string) bool {
|
||||
return a == PersonalAddress || a == ExchangeAddress
|
||||
}) {
|
||||
return nil
|
||||
}
|
||||
|
||||
var providerName string
|
||||
var getBalance func(ctx context.Context, address string) (float64, error)
|
||||
|
||||
switch coinType {
|
||||
case currency.ETH:
|
||||
for x := range addresses {
|
||||
result, err := b.GetEthereumBalance(addresses[x])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if result.Error.Message != "" {
|
||||
return errors.New(result.Error.Message)
|
||||
}
|
||||
|
||||
err = b.AddAddress(addresses[x],
|
||||
PersonalAddress,
|
||||
coinType,
|
||||
result.ETH.Balance)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
providerName = "Ethplorer"
|
||||
getBalance = b.GetEthereumAddressBalance
|
||||
case currency.XRP:
|
||||
for x := range addresses {
|
||||
result, err := b.GetRippleBalance(addresses[x])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = b.AddAddress(addresses[x],
|
||||
PersonalAddress,
|
||||
coinType,
|
||||
result)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
providerName = "XRPScan"
|
||||
getBalance = b.GetRippleAddressBalance
|
||||
case currency.BTC, currency.LTC:
|
||||
providerName = "CryptoID"
|
||||
getBalance = func(ctx context.Context, address string) (float64, error) {
|
||||
return b.GetCryptoIDAddressBalance(ctx, address, coinType)
|
||||
}
|
||||
default:
|
||||
for x := range addresses {
|
||||
result, err := b.GetCryptoIDAddress(addresses[x], coinType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = b.AddAddress(addresses[x],
|
||||
PersonalAddress,
|
||||
coinType,
|
||||
result)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return fmt.Errorf("%w: %s", currency.ErrCurrencyNotSupported, coinType)
|
||||
}
|
||||
|
||||
p, ok := b.Providers.GetProvider(providerName)
|
||||
if !ok {
|
||||
return fmt.Errorf("%w: %s", errProviderNotFound, providerName)
|
||||
}
|
||||
|
||||
if !p.Enabled {
|
||||
return fmt.Errorf("%w: %s", errProviderNotEnabled, providerName)
|
||||
}
|
||||
|
||||
if p.Name == "CryptoID" && (p.APIKey == "" || p.APIKey == defaultAPIKey) {
|
||||
return fmt.Errorf("%w: %s", errProviderAPIKeyNotSet, providerName)
|
||||
}
|
||||
|
||||
var errs error
|
||||
for x := range addresses {
|
||||
balance, err := getBalance(ctx, addresses[x])
|
||||
if err != nil {
|
||||
errs = common.AppendError(errs, fmt.Errorf("error getting balance for %s: %w", addresses[x], err))
|
||||
continue
|
||||
}
|
||||
|
||||
if err := b.AddAddress(addresses[x], PersonalAddress, coinType, balance); err != nil {
|
||||
errs = common.AppendError(errs, fmt.Errorf("error adding address %s: %w", addresses[x], err))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
return errs
|
||||
}
|
||||
|
||||
// GetPortfolioByExchange returns currency portfolio amount by exchange
|
||||
func (b *Base) GetPortfolioByExchange(exchangeName string) map[currency.Code]float64 {
|
||||
b.mtx.RLock()
|
||||
defer b.mtx.RUnlock()
|
||||
|
||||
result := make(map[currency.Code]float64)
|
||||
for x := range b.Addresses {
|
||||
if strings.Contains(b.Addresses[x].Address, exchangeName) {
|
||||
@@ -322,6 +343,9 @@ func (b *Base) GetPortfolioByExchange(exchangeName string) map[currency.Code]flo
|
||||
|
||||
// GetExchangePortfolio returns current portfolio base information
|
||||
func (b *Base) GetExchangePortfolio() map[currency.Code]float64 {
|
||||
b.mtx.RLock()
|
||||
defer b.mtx.RUnlock()
|
||||
|
||||
result := make(map[currency.Code]float64)
|
||||
for i := range b.Addresses {
|
||||
if b.Addresses[i].Description != ExchangeAddress {
|
||||
@@ -339,6 +363,9 @@ func (b *Base) GetExchangePortfolio() map[currency.Code]float64 {
|
||||
|
||||
// GetPersonalPortfolio returns current portfolio base information
|
||||
func (b *Base) GetPersonalPortfolio() map[currency.Code]float64 {
|
||||
b.mtx.RLock()
|
||||
defer b.mtx.RUnlock()
|
||||
|
||||
result := make(map[currency.Code]float64)
|
||||
for i := range b.Addresses {
|
||||
if strings.EqualFold(b.Addresses[i].Description, ExchangeAddress) {
|
||||
@@ -460,8 +487,11 @@ func (b *Base) GetPortfolioSummary() Summary {
|
||||
return portfolioOutput
|
||||
}
|
||||
|
||||
// GetPortfolioGroupedCoin returns portfolio base information grouped by coin
|
||||
func (b *Base) GetPortfolioGroupedCoin() map[currency.Code][]string {
|
||||
// GetPortfolioAddressesGroupedByCoin returns portfolio addresses grouped by coin
|
||||
func (b *Base) GetPortfolioAddressesGroupedByCoin() map[currency.Code][]string {
|
||||
b.mtx.RLock()
|
||||
defer b.mtx.RUnlock()
|
||||
|
||||
result := make(map[currency.Code][]string)
|
||||
for i := range b.Addresses {
|
||||
if strings.EqualFold(b.Addresses[i].Description, ExchangeAddress) {
|
||||
@@ -472,70 +502,84 @@ func (b *Base) GetPortfolioGroupedCoin() map[currency.Code][]string {
|
||||
return result
|
||||
}
|
||||
|
||||
// Seed appends a portfolio base object with another base portfolio
|
||||
// addresses
|
||||
func (b *Base) Seed(port Base) {
|
||||
b.Addresses = port.Addresses
|
||||
}
|
||||
|
||||
// StartPortfolioWatcher observes the portfolio object
|
||||
func (b *Base) StartPortfolioWatcher() {
|
||||
addrCount := len(b.Addresses)
|
||||
log.Debugf(log.PortfolioMgr,
|
||||
"PortfolioWatcher started: Have %d entries in portfolio.\n", addrCount,
|
||||
)
|
||||
for {
|
||||
data := b.GetPortfolioGroupedCoin()
|
||||
for key, value := range data {
|
||||
err := b.UpdatePortfolio(value, key)
|
||||
if err != nil {
|
||||
log.Errorf(log.PortfolioMgr,
|
||||
"PortfolioWatcher error %s for currency %s, val %v\n",
|
||||
err,
|
||||
key,
|
||||
value)
|
||||
func (b *Base) StartPortfolioWatcher(ctx context.Context, interval time.Duration) error {
|
||||
if len(b.Addresses) == 0 {
|
||||
return errNoPortfolioItemsToWatch
|
||||
}
|
||||
|
||||
if interval <= 0 {
|
||||
interval = defaultInterval
|
||||
}
|
||||
|
||||
log.Infof(log.PortfolioMgr, "PortfolioWatcher started: Have %d entries in portfolio.\n", len(b.Addresses))
|
||||
|
||||
updatePortfolio := func() {
|
||||
for key, value := range b.GetPortfolioAddressesGroupedByCoin() {
|
||||
if err := b.UpdatePortfolio(ctx, value, key); err != nil {
|
||||
log.Errorf(log.PortfolioMgr, "PortfolioWatcher: UpdatePortfolio error: %s for currency %s", err, key)
|
||||
continue
|
||||
}
|
||||
|
||||
log.Debugf(log.PortfolioMgr,
|
||||
"PortfolioWatcher: Successfully updated address balance for %s address(es) %s\n",
|
||||
key,
|
||||
value)
|
||||
log.Debugf(log.PortfolioMgr, "PortfolioWatcher: Successfully updated address balance for %s", key)
|
||||
}
|
||||
}
|
||||
|
||||
updatePortfolio()
|
||||
|
||||
ticker := time.NewTicker(interval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
log.Debugf(log.PortfolioMgr, "PortfolioWatcher stopped: context cancelled")
|
||||
return ctx.Err()
|
||||
case <-ticker.C:
|
||||
updatePortfolio()
|
||||
}
|
||||
time.Sleep(time.Minute * 10)
|
||||
}
|
||||
}
|
||||
|
||||
// IsExchangeSupported checks if exchange is supported by portfolio address
|
||||
func (b *Base) IsExchangeSupported(exchange, address string) (ret bool) {
|
||||
for x := range b.Addresses {
|
||||
if b.Addresses[x].Address != address {
|
||||
continue
|
||||
func (b *Base) IsExchangeSupported(exchange, address string) bool {
|
||||
b.mtx.RLock()
|
||||
defer b.mtx.RUnlock()
|
||||
|
||||
return slices.ContainsFunc(b.Addresses, func(a Address) bool {
|
||||
if a.Address != address {
|
||||
return false
|
||||
}
|
||||
exchangeList := strings.Split(b.Addresses[x].SupportedExchanges, ",")
|
||||
exchangeList := strings.Split(a.SupportedExchanges, ",")
|
||||
return common.StringSliceContainsInsensitive(exchangeList, exchange)
|
||||
}
|
||||
return
|
||||
})
|
||||
}
|
||||
|
||||
// IsColdStorage checks if address is a cold storage wallet
|
||||
func (b *Base) IsColdStorage(address string) bool {
|
||||
for x := range b.Addresses {
|
||||
if b.Addresses[x].Address != address {
|
||||
continue
|
||||
}
|
||||
return b.Addresses[x].ColdStorage
|
||||
}
|
||||
return false
|
||||
b.mtx.RLock()
|
||||
defer b.mtx.RUnlock()
|
||||
|
||||
return slices.ContainsFunc(b.Addresses, func(a Address) bool {
|
||||
return a.Address == address && a.ColdStorage
|
||||
})
|
||||
}
|
||||
|
||||
// IsWhiteListed checks if address is whitelisted for withdraw transfers
|
||||
func (b *Base) IsWhiteListed(address string) bool {
|
||||
for x := range b.Addresses {
|
||||
if b.Addresses[x].Address != address {
|
||||
continue
|
||||
}
|
||||
return b.Addresses[x].WhiteListed
|
||||
}
|
||||
return false
|
||||
b.mtx.RLock()
|
||||
defer b.mtx.RUnlock()
|
||||
|
||||
return slices.ContainsFunc(b.Addresses, func(a Address) bool {
|
||||
return a.Address == address && a.WhiteListed
|
||||
})
|
||||
}
|
||||
|
||||
// GetProvider returns a provider by name
|
||||
func (p providers) GetProvider(name string) (provider, bool) {
|
||||
for _, provider := range p {
|
||||
if strings.EqualFold(provider.Name, name) {
|
||||
return provider, true
|
||||
}
|
||||
}
|
||||
return provider{}, false
|
||||
}
|
||||
|
||||
@@ -1,496 +1,274 @@
|
||||
package portfolio
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/thrasher-corp/gocryptotrader/common"
|
||||
"github.com/thrasher-corp/gocryptotrader/core"
|
||||
"github.com/thrasher-corp/gocryptotrader/currency"
|
||||
)
|
||||
|
||||
const (
|
||||
testBTCAddress = "0x1D01TH0R53"
|
||||
testInvalidBTCAddress = "0x1D01TH0R53"
|
||||
testLTCAddress = "LX2LMYXtuv5tiYEMztSSoEZcafFPYJFRK1"
|
||||
testBTCAddress = "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa"
|
||||
testETHAddress = "0xb794f5ea0ba39494ce839613fffba74279579268"
|
||||
testXRPAddress = "rs8ZPbYqgecRcDzQpJYAMhSxSi5htsjnza"
|
||||
cryptoIDAPIKey = ""
|
||||
)
|
||||
|
||||
func TestGetEthereumBalance(t *testing.T) {
|
||||
func TestGetEthereumAddressBalance(t *testing.T) {
|
||||
t.Parallel()
|
||||
b := Base{}
|
||||
address := "0xb794f5ea0ba39494ce839613fffba74279579268"
|
||||
nonsenseAddress := "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
|
||||
|
||||
response, err := b.GetEthereumBalance(address)
|
||||
if err != nil {
|
||||
t.Errorf("Portfolio GetEthereumBalance() Error: %s", err)
|
||||
}
|
||||
_, err := b.GetEthereumAddressBalance(t.Context(), testBTCAddress)
|
||||
assert.ErrorIs(t, err, common.ErrAddressIsEmptyOrInvalid)
|
||||
|
||||
if response.Address != "0xb794f5ea0ba39494ce839613fffba74279579268" {
|
||||
t.Error("Portfolio GetEthereumBalance() address invalid")
|
||||
}
|
||||
|
||||
_, err = b.GetEthereumBalance(nonsenseAddress)
|
||||
if !errors.Is(err, errNotEthAddress) {
|
||||
t.Errorf("received '%v', expected '%v'", err, errNotEthAddress)
|
||||
}
|
||||
_, err = b.GetEthereumAddressBalance(t.Context(), testETHAddress)
|
||||
assert.NoError(t, err, "GetEthereumAddressBalance should not error")
|
||||
}
|
||||
|
||||
func TestGetCryptoIDBalance(t *testing.T) {
|
||||
func TestGetCryptoIDAddressBalance(t *testing.T) {
|
||||
t.Parallel()
|
||||
b := Base{}
|
||||
ltcAddress := "LX2LMYXtuv5tiYEMztSSoEZcafFPYJFRK1"
|
||||
_, err := b.GetCryptoIDAddress(ltcAddress, currency.LTC)
|
||||
if err != nil {
|
||||
t.Fatalf("TestGetCryptoIDBalance error: %s", err)
|
||||
|
||||
_, err := b.GetCryptoIDAddressBalance(t.Context(), testInvalidBTCAddress, currency.BTC)
|
||||
assert.ErrorIs(t, err, common.ErrAddressIsEmptyOrInvalid)
|
||||
|
||||
_, err = b.GetCryptoIDAddressBalance(t.Context(), testLTCAddress, currency.LTC)
|
||||
assert.ErrorIs(t, err, errProviderNotFound)
|
||||
|
||||
b.Providers = providers{{Name: "CryptoID"}}
|
||||
_, err = b.GetCryptoIDAddressBalance(t.Context(), testLTCAddress, currency.LTC)
|
||||
assert.ErrorIs(t, err, errProviderAPIKeyNotSet)
|
||||
|
||||
b.Providers[0].APIKey = "bob"
|
||||
ctx, cancel := context.WithDeadline(t.Context(), time.Now().Add(time.Nanosecond))
|
||||
defer cancel()
|
||||
_, err = b.GetCryptoIDAddressBalance(ctx, testLTCAddress, currency.LTC)
|
||||
assert.True(t, errors.Is(err, context.DeadlineExceeded) || strings.Contains(err.Error(), "rate limiter wait error"),
|
||||
"GetCryptoIDAddressBalance should return DeadlineExceeded or rate limiter wait error")
|
||||
|
||||
if cryptoIDAPIKey == "" {
|
||||
t.Skip("Skipping test as CryptoID API key is not set")
|
||||
}
|
||||
|
||||
b.Providers[0].APIKey = cryptoIDAPIKey
|
||||
_, err = b.GetCryptoIDAddressBalance(t.Context(), testLTCAddress, currency.LTC)
|
||||
assert.NoError(t, err, "GetCryptoIDAddressBalance should not error")
|
||||
}
|
||||
|
||||
func TestGetRippleAddressBalance(t *testing.T) {
|
||||
t.Parallel()
|
||||
b := Base{}
|
||||
|
||||
_, err := b.GetRippleAddressBalance(t.Context(), testXRPAddress)
|
||||
assert.NoError(t, err, "GetRippleAddressBalance should not error")
|
||||
}
|
||||
|
||||
func TestGetAddressBalance(t *testing.T) {
|
||||
t.Parallel()
|
||||
ltcAddress := "LdP8Qox1VAhCzLJNqrr74YovaWYyNBUWvL"
|
||||
ltc := currency.LTC
|
||||
description := "Description of Wallet"
|
||||
balance := float64(1000)
|
||||
|
||||
const (
|
||||
description = "Description of Wallet"
|
||||
balance = 1000.0
|
||||
)
|
||||
|
||||
b := Base{}
|
||||
err := b.AddAddress(ltcAddress, description, ltc, balance)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
assert.NoError(t, b.AddAddress(testLTCAddress, description, currency.LTC, balance))
|
||||
|
||||
addBalance, _ := b.GetAddressBalance("LdP8Qox1VAhCzLJNqrr74YovaWYyNBUWvL",
|
||||
description,
|
||||
ltc)
|
||||
r, ok := b.GetAddressBalance("meow", description, currency.LTC)
|
||||
assert.False(t, ok, "GetAddressBalance should return false for non-existent address")
|
||||
assert.Zero(t, r, "GetAddressBalance should return 0 for non-existent address")
|
||||
|
||||
if addBalance != balance {
|
||||
t.Error("Incorrect value")
|
||||
}
|
||||
|
||||
addBalance, found := b.GetAddressBalance("WigWham",
|
||||
description,
|
||||
ltc)
|
||||
|
||||
if addBalance != 0 {
|
||||
t.Error("Incorrect value")
|
||||
}
|
||||
if found {
|
||||
t.Error("Incorrect value")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetRippleBalance(t *testing.T) {
|
||||
t.Parallel()
|
||||
b := Base{}
|
||||
nonsenseAddress := "Wigwham"
|
||||
_, err := b.GetRippleBalance(nonsenseAddress)
|
||||
if err == nil {
|
||||
t.Error("error cannot be nil on a bad address")
|
||||
}
|
||||
|
||||
rippleAddress := "r962iS5subzbVeXZN8MTzyEuuaQKo5qksh"
|
||||
_, err = b.GetRippleBalance(rippleAddress)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
r, ok = b.GetAddressBalance(testLTCAddress, description, currency.LTC)
|
||||
assert.True(t, ok, "GetAddressBalance should return true for existing address")
|
||||
assert.Equal(t, balance, r, "GetAddressBalance should return the correct balance")
|
||||
}
|
||||
|
||||
func TestExchangeExists(t *testing.T) {
|
||||
t.Parallel()
|
||||
newBase := Base{}
|
||||
err := newBase.AddAddress("someaddress",
|
||||
currency.LTC.String(),
|
||||
currency.NewCode("LTCWALLETTEST"),
|
||||
0.02)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if !newBase.ExchangeExists("someaddress") {
|
||||
t.Error("expected exchange to exist")
|
||||
}
|
||||
if newBase.ExchangeExists("bla") {
|
||||
t.Error("expected exchange to not exist")
|
||||
}
|
||||
b := Base{}
|
||||
assert.False(t, b.ExchangeExists("someaddress"))
|
||||
b.AddExchangeAddress("someaddress", currency.LTC, 0.02)
|
||||
assert.True(t, b.ExchangeExists("someaddress"))
|
||||
}
|
||||
|
||||
func TestAddressExists(t *testing.T) {
|
||||
t.Parallel()
|
||||
newBase := Base{}
|
||||
err := newBase.AddAddress("someaddress",
|
||||
currency.LTC.String(),
|
||||
currency.NewCode("LTCWALLETTEST"),
|
||||
0.02)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if !newBase.AddressExists("someaddress") {
|
||||
t.Error("expected address to exist")
|
||||
}
|
||||
if newBase.AddressExists("bla") {
|
||||
t.Error("expected address to not exist")
|
||||
}
|
||||
b := Base{}
|
||||
assert.False(t, b.AddressExists("meow"))
|
||||
assert.NoError(t, b.AddAddress("someaddress", "desc", currency.NewCode("LTCWALLETTEST"), 0.02))
|
||||
assert.True(t, b.AddressExists("someaddress"))
|
||||
}
|
||||
|
||||
func TestExchangeAddressExists(t *testing.T) {
|
||||
func TestExchangeAddressCoinExists(t *testing.T) {
|
||||
t.Parallel()
|
||||
newBase := Base{}
|
||||
err := newBase.AddAddress("someaddress",
|
||||
currency.LTC.String(),
|
||||
currency.LTC,
|
||||
0.02)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if !newBase.ExchangeAddressExists("someaddress", currency.LTC) {
|
||||
t.Error("expected exchange address to exist")
|
||||
}
|
||||
if newBase.ExchangeAddressExists("TEST", currency.LTC) {
|
||||
t.Error("expected exchange address to not exist")
|
||||
}
|
||||
b := Base{}
|
||||
assert.False(t, b.ExchangeAddressCoinExists("someaddress", currency.LTC))
|
||||
b.AddExchangeAddress("someaddress", currency.LTC, 0.02)
|
||||
assert.True(t, b.ExchangeAddressCoinExists("someaddress", currency.LTC))
|
||||
assert.False(t, b.ExchangeAddressCoinExists("someaddress", currency.BTC))
|
||||
}
|
||||
|
||||
func TestAddExchangeAddress(t *testing.T) {
|
||||
t.Parallel()
|
||||
newBase := Base{}
|
||||
newBase.AddExchangeAddress("Okx", currency.BTC, 100)
|
||||
newBase.AddExchangeAddress("Okx", currency.BTC, 200)
|
||||
|
||||
if !newBase.ExchangeAddressExists("Okx", currency.BTC) {
|
||||
t.Error("address doesn't exist")
|
||||
}
|
||||
b := Base{}
|
||||
b.AddExchangeAddress("someaddress", currency.LTC, 69)
|
||||
bal, ok := b.GetAddressBalance("someaddress", ExchangeAddress, currency.LTC)
|
||||
assert.True(t, ok, "GetAddressBalance should return true for existing address")
|
||||
assert.Equal(t, 69.0, bal, "GetAddressBalance should return the correct balance")
|
||||
b.AddExchangeAddress("someaddress", currency.LTC, 420)
|
||||
bal, ok = b.GetAddressBalance("someaddress", ExchangeAddress, currency.LTC)
|
||||
assert.True(t, ok, "GetAddressBalance should return true for existing address")
|
||||
assert.Equal(t, 420.0, bal, "GetAddressBalance should return the correct balance")
|
||||
}
|
||||
|
||||
func TestUpdateAddressBalance(t *testing.T) {
|
||||
t.Parallel()
|
||||
newBase := Base{}
|
||||
err := newBase.AddAddress("someaddress",
|
||||
currency.LTC.String(),
|
||||
currency.NewCode("LTCWALLETTEST"),
|
||||
0.02)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
newBase.UpdateAddressBalance("someaddress", 0.03)
|
||||
|
||||
value := newBase.GetPortfolioSummary()
|
||||
if !value.Totals[0].Coin.Equal(currency.LTC) &&
|
||||
value.Totals[0].Balance != 0.03 {
|
||||
t.Error("UpdateUpdateAddressBalance error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveAddress(t *testing.T) {
|
||||
t.Parallel()
|
||||
var newBase Base
|
||||
if err := newBase.RemoveAddress("", "MEOW", currency.LTC); err == nil {
|
||||
t.Error("invalid address should throw an error")
|
||||
}
|
||||
|
||||
if err := newBase.RemoveAddress("Gibson", "", currency.NewCode("")); err == nil {
|
||||
t.Error("invalid coin type should throw an error")
|
||||
}
|
||||
|
||||
if err := newBase.RemoveAddress("HIDDENERINO", "MEOW", currency.LTC); err == nil {
|
||||
t.Error("non-existent address should throw an error")
|
||||
}
|
||||
|
||||
err := newBase.AddAddress("someaddr",
|
||||
currency.LTC.String(),
|
||||
currency.NewCode("LTCWALLETTEST"),
|
||||
420)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if !newBase.AddressExists("someaddr") {
|
||||
t.Error("address does not exist")
|
||||
}
|
||||
|
||||
err = newBase.RemoveAddress("someaddr",
|
||||
currency.LTC.String(),
|
||||
currency.NewCode("LTCWALLETTEST"))
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if newBase.AddressExists("someaddr") {
|
||||
t.Error("address should not exist")
|
||||
}
|
||||
b := Base{}
|
||||
assert.NoError(t, b.AddAddress("someaddress", "desc", currency.LTC, 0.02))
|
||||
b.UpdateAddressBalance("someaddress", 0.03)
|
||||
bal, ok := b.GetAddressBalance("someaddress", "desc", currency.LTC)
|
||||
assert.True(t, ok, "GetAddressBalance should return true for existing address")
|
||||
assert.Equal(t, 0.03, bal, "GetAddressBalance should return the correct balance")
|
||||
}
|
||||
|
||||
func TestRemoveExchangeAddress(t *testing.T) {
|
||||
t.Parallel()
|
||||
newBase := Base{}
|
||||
exchangeName := "BallerExchange"
|
||||
coinType := currency.LTC
|
||||
|
||||
newBase.AddExchangeAddress(exchangeName, coinType, 420)
|
||||
|
||||
if !newBase.ExchangeAddressExists(exchangeName, coinType) {
|
||||
t.Error("address does not exist")
|
||||
}
|
||||
|
||||
newBase.RemoveExchangeAddress(exchangeName, coinType)
|
||||
if newBase.ExchangeAddressExists(exchangeName, coinType) {
|
||||
t.Error("address should not exist")
|
||||
}
|
||||
b := Base{}
|
||||
b.AddExchangeAddress("BallerExchange", currency.LTC, 420)
|
||||
bal, ok := b.GetAddressBalance("BallerExchange", ExchangeAddress, currency.LTC)
|
||||
assert.True(t, ok, "GetAddressBalance should return true for existing address")
|
||||
assert.Equal(t, 420.0, bal, "GetAddressBalance should return the correct balance")
|
||||
b.RemoveExchangeAddress("BallerExchange", currency.LTC)
|
||||
bal, ok = b.GetAddressBalance("BallerExchange", ExchangeAddress, currency.LTC)
|
||||
assert.False(t, ok, "GetAddressBalance should return false for non-existent address")
|
||||
assert.Zero(t, bal, "GetAddressBalance should return 0 for non-existent address")
|
||||
}
|
||||
|
||||
func TestUpdateExchangeAddressBalance(t *testing.T) {
|
||||
t.Parallel()
|
||||
newBase := Base{}
|
||||
newBase.AddExchangeAddress("someaddress", currency.LTC, 0.02)
|
||||
b := Base{}
|
||||
b.Seed(newBase)
|
||||
b.AddExchangeAddress("someaddress", currency.LTC, 0.02)
|
||||
b.UpdateExchangeAddressBalance("someaddress", currency.LTC, 0.04)
|
||||
|
||||
value := b.GetPortfolioSummary()
|
||||
if !value.Totals[0].Coin.Equal(currency.LTC) && value.Totals[0].Balance != 0.04 {
|
||||
t.Error("incorrect portfolio balance")
|
||||
}
|
||||
bal, ok := b.GetAddressBalance("someaddress", ExchangeAddress, currency.LTC)
|
||||
assert.True(t, ok, "GetAddressBalance should return true for existing address")
|
||||
assert.Equal(t, 0.04, bal, "GetAddressBalance should return the correct balance")
|
||||
}
|
||||
|
||||
func TestAddAddress(t *testing.T) {
|
||||
t.Parallel()
|
||||
var newBase Base
|
||||
if err := newBase.AddAddress("", "MEOW", currency.LTC, 1); err == nil {
|
||||
t.Error("invalid address should throw an error")
|
||||
}
|
||||
b := Base{}
|
||||
assert.ErrorIs(t, b.AddAddress("", "desc", currency.LTC, 0.02), common.ErrAddressIsEmptyOrInvalid)
|
||||
assert.ErrorIs(t, b.AddAddress("someaddress", "", currency.EMPTYCODE, 0.02), currency.ErrCurrencyCodeEmpty)
|
||||
assert.NoError(t, b.AddAddress("okx", ExchangeAddress, currency.LTC, 0.02))
|
||||
assert.True(t, b.ExchangeAddressCoinExists("okx", currency.LTC), "ExchangeAddressCoinExists should return true for an existing address and coin")
|
||||
assert.NoError(t, b.AddAddress("someaddress", PersonalAddress, currency.LTC, 0.03))
|
||||
assert.True(t, b.AddressExists("someaddress"), "AddressExists should return true for an existing address")
|
||||
assert.NoError(t, b.AddAddress("someaddress", PersonalAddress, currency.LTC, 69))
|
||||
bal, ok := b.GetAddressBalance("someaddress", PersonalAddress, currency.LTC)
|
||||
assert.True(t, ok, "GetAddressBalance should return true for existing address")
|
||||
assert.Equal(t, 69.0, bal, "GetAddressBalance should return the correct balance")
|
||||
}
|
||||
|
||||
if err := newBase.AddAddress("Gibson", "", currency.NewCode(""), 1); err == nil {
|
||||
t.Error("invalid coin type should throw an error")
|
||||
}
|
||||
|
||||
// test adding an exchange address
|
||||
err := newBase.AddAddress("COINUT", ExchangeAddress, currency.LTC, 0)
|
||||
if err != nil {
|
||||
t.Errorf("failed to add address: %v", err)
|
||||
}
|
||||
|
||||
// add a test portfolio address and amount
|
||||
err = newBase.AddAddress("Gibson",
|
||||
currency.LTC.String(),
|
||||
currency.NewCode("LTCWALLETTEST"),
|
||||
0.02)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
// test updating the balance and make sure it's reflected
|
||||
err = newBase.AddAddress("Gibson", currency.LTC.String(),
|
||||
currency.NewCode("LTCWALLETTEST"), 0.05)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
b, _ := newBase.GetAddressBalance("Gibson", "LTC",
|
||||
currency.NewCode("LTCWALLETTEST"))
|
||||
if b != 0.05 {
|
||||
t.Error("invalid portfolio amount")
|
||||
}
|
||||
|
||||
nb := Base{}
|
||||
nb.Seed(newBase)
|
||||
if !nb.AddressExists("Gibson") {
|
||||
t.Error("AddAddress error")
|
||||
}
|
||||
|
||||
// Test updating balance to <= 0, expected result is to remove the address.
|
||||
// Fail if address still exists.
|
||||
err = newBase.AddAddress("Gibson",
|
||||
currency.LTC.String(),
|
||||
currency.NewCode("LTCWALLETTEST"),
|
||||
-1)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if newBase.AddressExists("Gibson") {
|
||||
t.Error("AddAddress error")
|
||||
}
|
||||
func TestRemoveAddress(t *testing.T) {
|
||||
t.Parallel()
|
||||
b := Base{}
|
||||
assert.ErrorIs(t, b.RemoveAddress("", "desc", currency.LTC), common.ErrAddressIsEmptyOrInvalid)
|
||||
assert.ErrorIs(t, b.RemoveAddress("someaddress", "", currency.EMPTYCODE), currency.ErrCurrencyCodeEmpty)
|
||||
assert.ErrorIs(t, b.RemoveAddress("someaddress", "desc", currency.LTC), errPortfolioItemNotFound)
|
||||
assert.NoError(t, b.AddAddress("someaddress", "desc", currency.LTC, 0.02))
|
||||
assert.NoError(t, b.RemoveAddress("someaddress", "desc", currency.LTC))
|
||||
assert.False(t, b.AddressExists("someaddress"), "AddressExists should return false for non-existent address")
|
||||
}
|
||||
|
||||
func TestUpdatePortfolio(t *testing.T) {
|
||||
t.Parallel()
|
||||
newBase := Base{}
|
||||
err := newBase.UpdatePortfolio([]string{"Testy"}, currency.LTC)
|
||||
if err == nil {
|
||||
t.Error("UpdatePortfolio error cannot be nil")
|
||||
}
|
||||
err = newBase.UpdatePortfolio([]string{
|
||||
"LdP8Qox1VAhCzLJNqrr74YovaWYyNBUWvL",
|
||||
"LVa8wZ983PvWtdwXZ8viK6SocMENLCXkEy",
|
||||
},
|
||||
currency.LTC,
|
||||
)
|
||||
if err != nil {
|
||||
t.Error("UpdatePortfolio error", err)
|
||||
}
|
||||
err = newBase.UpdatePortfolio(
|
||||
[]string{"Testy"}, currency.LTC,
|
||||
)
|
||||
if err == nil {
|
||||
t.Error("UpdatePortfolio error cannot be nil")
|
||||
b := Base{
|
||||
Providers: providers{
|
||||
{
|
||||
Name: "XRPScan",
|
||||
Enabled: true,
|
||||
},
|
||||
{
|
||||
Name: "Ethplorer",
|
||||
Enabled: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
err = newBase.UpdatePortfolio([]string{
|
||||
"0xb794f5ea0ba39494ce839613fffba74279579268",
|
||||
},
|
||||
currency.ETH)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
err = newBase.UpdatePortfolio([]string{
|
||||
"TESTY",
|
||||
},
|
||||
currency.ETH)
|
||||
if err == nil {
|
||||
t.Error("UpdatePortfolio error cannot be nil")
|
||||
}
|
||||
assert.NoError(t, b.UpdatePortfolio(t.Context(), []string{PersonalAddress, ExchangeAddress}, currency.LTC))
|
||||
assert.NoError(t, b.UpdatePortfolio(t.Context(), []string{testETHAddress}, currency.ETH))
|
||||
assert.NoError(t, b.UpdatePortfolio(t.Context(), []string{testXRPAddress}, currency.XRP))
|
||||
assert.ErrorIs(t, b.UpdatePortfolio(t.Context(), []string{testETHAddress}, currency.ADA), currency.ErrCurrencyNotSupported)
|
||||
assert.ErrorIs(t, b.UpdatePortfolio(t.Context(), []string{testBTCAddress}, currency.BTC), errProviderNotFound)
|
||||
|
||||
err = newBase.UpdatePortfolio([]string{
|
||||
ExchangeAddress,
|
||||
PersonalAddress,
|
||||
},
|
||||
currency.LTC)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
b.Providers = append(b.Providers, provider{
|
||||
Name: "CryptoID",
|
||||
})
|
||||
|
||||
err = newBase.UpdatePortfolio([]string{
|
||||
"r962iS5subzbVeXZN8MTzyEuuaQKo5qksh",
|
||||
},
|
||||
currency.XRP)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
assert.ErrorIs(t, b.UpdatePortfolio(t.Context(), []string{testLTCAddress}, currency.LTC), errProviderNotEnabled)
|
||||
b.Providers[2].Enabled = true
|
||||
assert.ErrorIs(t, b.UpdatePortfolio(t.Context(), []string{testLTCAddress}, currency.LTC), errProviderAPIKeyNotSet)
|
||||
|
||||
err = newBase.UpdatePortfolio([]string{
|
||||
"TESTY",
|
||||
},
|
||||
currency.XRP)
|
||||
if err == nil {
|
||||
t.Error("error cannot be nil")
|
||||
if cryptoIDAPIKey == "" {
|
||||
t.Skip("Skipping test as CryptoID API key is not set")
|
||||
}
|
||||
b.Providers[2].APIKey = cryptoIDAPIKey
|
||||
assert.NoError(t, b.UpdatePortfolio(t.Context(), []string{testLTCAddress}, currency.LTC))
|
||||
assert.NoError(t, b.UpdatePortfolio(t.Context(), []string{testBTCAddress}, currency.BTC))
|
||||
}
|
||||
|
||||
func TestGetPortfolioByExchange(t *testing.T) {
|
||||
t.Parallel()
|
||||
newBase := Base{}
|
||||
newBase.AddExchangeAddress("Okx", currency.LTC, 0.07)
|
||||
newBase.AddExchangeAddress("Bitfinex", currency.LTC, 0.05)
|
||||
err := newBase.AddAddress("someaddress", "LTC", currency.NewCode(PersonalAddress), 0.03)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
value := newBase.GetPortfolioByExchange("Okx")
|
||||
result, ok := value[currency.LTC]
|
||||
if !ok {
|
||||
t.Error("missing portfolio entry")
|
||||
}
|
||||
|
||||
if result != 0.07 {
|
||||
t.Error("incorrect result")
|
||||
}
|
||||
|
||||
value = newBase.GetPortfolioByExchange("Bitfinex")
|
||||
result, ok = value[currency.LTC]
|
||||
if !ok {
|
||||
t.Error("missing portfolio entry")
|
||||
}
|
||||
|
||||
if result != 0.05 {
|
||||
t.Error("incorrect result")
|
||||
}
|
||||
b := Base{}
|
||||
b.AddExchangeAddress("Okx", currency.LTC, 0.07)
|
||||
b.AddExchangeAddress("Bitfinex", currency.LTC, 0.05)
|
||||
assert.NoError(t, b.AddAddress("someaddress", "LTC", currency.NewCode(PersonalAddress), 0.03))
|
||||
assert.Equal(t, 0.07, b.GetPortfolioByExchange("Okx")[currency.LTC], "GetPortfolioByExchange should return the correct balance")
|
||||
assert.Equal(t, 0.05, b.GetPortfolioByExchange("Bitfinex")[currency.LTC], "GetPortfolioByExchange should return the correct balance")
|
||||
}
|
||||
|
||||
func TestGetExchangePortfolio(t *testing.T) {
|
||||
t.Parallel()
|
||||
newBase := Base{}
|
||||
err := newBase.AddAddress("Okx", ExchangeAddress, currency.LTC, 0.03)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = newBase.AddAddress("Bitfinex", ExchangeAddress, currency.LTC, 0.05)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = newBase.AddAddress("someaddress", PersonalAddress, currency.LTC, 0.03)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
value := newBase.GetExchangePortfolio()
|
||||
|
||||
result, ok := value[currency.LTC]
|
||||
if !ok {
|
||||
t.Error("missing portfolio entry")
|
||||
}
|
||||
|
||||
if result != 0.08 {
|
||||
t.Error("result != 0.08")
|
||||
}
|
||||
b := Base{}
|
||||
assert.NoError(t, b.AddAddress("Okx", ExchangeAddress, currency.LTC, 0.03))
|
||||
assert.NoError(t, b.AddAddress("Bitfinex", ExchangeAddress, currency.LTC, 0.05))
|
||||
assert.NoError(t, b.AddAddress("someaddress", PersonalAddress, currency.LTC, 0.03))
|
||||
assert.Equal(t, 0.08, b.GetExchangePortfolio()[currency.LTC], "GetExchangePortfolio should return the correct balance")
|
||||
}
|
||||
|
||||
func TestGetPersonalPortfolio(t *testing.T) {
|
||||
t.Parallel()
|
||||
newBase := Base{}
|
||||
err := newBase.AddAddress("someaddress", PersonalAddress, currency.N2O, 0.02)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = newBase.AddAddress("anotheraddress", PersonalAddress, currency.N2O, 0.03)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = newBase.AddAddress("Exchange", ExchangeAddress, currency.N2O, 0.01)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
value := newBase.GetPersonalPortfolio()
|
||||
result, ok := value[currency.N2O]
|
||||
if !ok {
|
||||
t.Error("GetPersonalPortfolio error")
|
||||
}
|
||||
|
||||
if result != 0.05 {
|
||||
t.Error("GetPersonalPortfolio result != 0.05")
|
||||
}
|
||||
b := Base{}
|
||||
assert.NoError(t, b.AddAddress("someaddress", PersonalAddress, currency.WIF, 0.02))
|
||||
assert.NoError(t, b.AddAddress("anotheraddress", PersonalAddress, currency.WIF, 0.03))
|
||||
assert.NoError(t, b.AddAddress("Exchange", ExchangeAddress, currency.WIF, 0.01))
|
||||
assert.Equal(t, 0.05, b.GetPersonalPortfolio()[currency.WIF], "GetPersonalPortfolio should return the correct balance")
|
||||
}
|
||||
|
||||
func TestGetPortfolioSummary(t *testing.T) {
|
||||
t.Parallel()
|
||||
newBase := Base{}
|
||||
b := Base{}
|
||||
// Personal holdings
|
||||
err := newBase.AddAddress("someaddress", PersonalAddress, currency.LTC, 1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = newBase.AddAddress("someaddress2", PersonalAddress, currency.LTC, 2)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = newBase.AddAddress("someaddress3", PersonalAddress, currency.BTC, 100)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = newBase.AddAddress("0xde0b295669a9fd93d5f28d9ec85e40f4cb697bae",
|
||||
PersonalAddress, currency.ETH, 865346880000000000)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = newBase.AddAddress("0x9edc81c813b26165f607a8d1b8db87a02f34307f",
|
||||
PersonalAddress, currency.ETH, 165346880000000000)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.NoError(t, b.AddAddress("someaddress", PersonalAddress, currency.LTC, 1))
|
||||
assert.NoError(t, b.AddAddress("someaddress2", PersonalAddress, currency.LTC, 2))
|
||||
assert.NoError(t, b.AddAddress("someaddress3", PersonalAddress, currency.BTC, 100))
|
||||
assert.NoError(t, b.AddAddress("0xde0b295669a9fd93d5f28d9ec85e40f4cb697bae", PersonalAddress, currency.ETH, 69))
|
||||
assert.NoError(t, b.AddAddress("0x9edc81c813b26165f607a8d1b8db87a02f34307f", PersonalAddress, currency.ETH, 420))
|
||||
|
||||
// Exchange holdings
|
||||
newBase.AddExchangeAddress("Bitfinex", currency.LTC, 20)
|
||||
newBase.AddExchangeAddress("Bitfinex", currency.BTC, 100)
|
||||
newBase.AddExchangeAddress("Okx", currency.ETH, 42)
|
||||
b.AddExchangeAddress("Bitfinex", currency.LTC, 20)
|
||||
b.AddExchangeAddress("Bitfinex", currency.BTC, 100)
|
||||
b.AddExchangeAddress("Okx", currency.ETH, 42)
|
||||
|
||||
value := newBase.GetPortfolioSummary()
|
||||
value := b.GetPortfolioSummary()
|
||||
|
||||
getTotalsVal := func(c currency.Code) Coin {
|
||||
for x := range value.Totals {
|
||||
@@ -501,147 +279,118 @@ func TestGetPortfolioSummary(t *testing.T) {
|
||||
return Coin{}
|
||||
}
|
||||
|
||||
if !getTotalsVal(currency.LTC).Coin.Equal(currency.LTC) {
|
||||
t.Error("mismatched currency")
|
||||
}
|
||||
|
||||
if getTotalsVal(currency.ETH).Coin.Equal(currency.LTC) {
|
||||
t.Error("mismatched currency")
|
||||
}
|
||||
|
||||
if getTotalsVal(currency.LTC).Balance != 23 {
|
||||
t.Error("incorrect balance")
|
||||
}
|
||||
|
||||
if getTotalsVal(currency.BTC).Balance != 200 {
|
||||
t.Error("incorrect balance")
|
||||
}
|
||||
assert.Equal(t, currency.LTC, getTotalsVal(currency.LTC).Coin, "Coin should be LTC")
|
||||
assert.Equal(t, 23.0, getTotalsVal(currency.LTC).Balance, "LTC balance should be correct")
|
||||
assert.Equal(t, 200.0, getTotalsVal(currency.BTC).Balance, "BTC balance should be correct")
|
||||
assert.Equal(t, 69.0+420.0+42, getTotalsVal(currency.ETH).Balance, "ETH balance should be correct")
|
||||
}
|
||||
|
||||
func TestGetPortfolioGroupedCoin(t *testing.T) {
|
||||
func TestGetPortfolioAddressesGroupedByCoin(t *testing.T) {
|
||||
t.Parallel()
|
||||
newBase := Base{}
|
||||
err := newBase.AddAddress("someaddress", currency.LTC.String(), currency.LTC, 0.02)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = newBase.AddAddress("Exchange", ExchangeAddress, currency.LTC, 0.05)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
value := newBase.GetPortfolioGroupedCoin()
|
||||
if value[currency.LTC][0] != "someaddress" && len(value[currency.LTC][0]) != 1 {
|
||||
t.Error("incorrect balance")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSeed(t *testing.T) {
|
||||
t.Parallel()
|
||||
newBase := Base{}
|
||||
err := newBase.AddAddress("someaddress", currency.LTC.String(), currency.LTC, 0.02)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !newBase.AddressExists("someaddress") {
|
||||
t.Error("Seed error")
|
||||
}
|
||||
b := Base{}
|
||||
assert.NoError(t, b.AddAddress(testLTCAddress, PersonalAddress, currency.LTC, 0.02))
|
||||
assert.NoError(t, b.AddAddress("Exchange", ExchangeAddress, currency.LTC, 0.03))
|
||||
assert.Len(t, b.GetPortfolioAddressesGroupedByCoin(), 1, "GetPortfolioAddressesGroupedByCoin should return the correct number of addresses")
|
||||
assert.Equal(t, testLTCAddress, b.GetPortfolioAddressesGroupedByCoin()[currency.LTC][0], "GetPortfolioAddressesGroupedByCoin should return the correct address")
|
||||
}
|
||||
|
||||
func TestIsExchangeSupported(t *testing.T) {
|
||||
t.Parallel()
|
||||
newBase := seedPortFolioForTest(t)
|
||||
ret := newBase.IsExchangeSupported("BTC Markets", core.BitcoinDonationAddress)
|
||||
if !ret {
|
||||
t.Fatal("expected IsExchangeSupported() to return true")
|
||||
}
|
||||
ret = newBase.IsExchangeSupported("Kraken", core.BitcoinDonationAddress)
|
||||
if ret {
|
||||
t.Fatal("expected IsExchangeSupported() to return false")
|
||||
b := Base{
|
||||
Addresses: []Address{
|
||||
{
|
||||
Address: core.BitcoinDonationAddress,
|
||||
SupportedExchanges: "Binance, BTC Markets",
|
||||
},
|
||||
},
|
||||
}
|
||||
assert.True(t, b.IsExchangeSupported("Binance", core.BitcoinDonationAddress), "IsExchangeSupported should return true for supported exchange")
|
||||
assert.False(t, b.IsExchangeSupported("Coinbase", core.BitcoinDonationAddress), "IsExchangeSupported should return false for unsupported exchange")
|
||||
assert.False(t, b.IsExchangeSupported("Binance", testBTCAddress), "IsExchangeSupported should return false for non-existent address")
|
||||
}
|
||||
|
||||
func TestIsColdStorage(t *testing.T) {
|
||||
t.Parallel()
|
||||
newBase := seedPortFolioForTest(t)
|
||||
ret := newBase.IsColdStorage(core.BitcoinDonationAddress)
|
||||
if !ret {
|
||||
t.Fatal("expected IsColdStorage() to return true")
|
||||
}
|
||||
ret = newBase.IsColdStorage(testBTCAddress)
|
||||
if ret {
|
||||
t.Fatal("expected IsColdStorage() to return false")
|
||||
}
|
||||
ret = newBase.IsColdStorage("hello")
|
||||
if ret {
|
||||
t.Fatal("expected IsColdStorage() to return false")
|
||||
b := Base{
|
||||
Addresses: []Address{
|
||||
{
|
||||
Address: core.BitcoinDonationAddress,
|
||||
ColdStorage: true,
|
||||
},
|
||||
{
|
||||
Address: testBTCAddress,
|
||||
},
|
||||
},
|
||||
}
|
||||
assert.True(t, b.IsColdStorage(core.BitcoinDonationAddress), "IsColdStorage should return true for cold storage address")
|
||||
assert.False(t, b.IsColdStorage(testBTCAddress), "IsColdStorage should return false for non-cold storage address")
|
||||
}
|
||||
|
||||
func TestIsWhiteListed(t *testing.T) {
|
||||
t.Parallel()
|
||||
b := seedPortFolioForTest(t)
|
||||
ret := b.IsWhiteListed(core.BitcoinDonationAddress)
|
||||
if !ret {
|
||||
t.Fatal("expected IsWhiteListed() to return true")
|
||||
}
|
||||
ret = b.IsWhiteListed(testBTCAddress)
|
||||
if ret {
|
||||
t.Fatal("expected IsWhiteListed() to return false")
|
||||
}
|
||||
ret = b.IsWhiteListed("hello")
|
||||
if ret {
|
||||
t.Fatal("expected IsWhiteListed() to return false")
|
||||
b := Base{
|
||||
Addresses: []Address{
|
||||
{
|
||||
Address: core.BitcoinDonationAddress,
|
||||
WhiteListed: true,
|
||||
},
|
||||
{
|
||||
Address: testBTCAddress,
|
||||
},
|
||||
},
|
||||
}
|
||||
assert.True(t, b.IsWhiteListed(core.BitcoinDonationAddress), "IsWhiteListed should return true for whitelisted address")
|
||||
assert.False(t, b.IsWhiteListed(testBTCAddress), "IsWhiteListed should return false for non-whitelisted address")
|
||||
}
|
||||
|
||||
func TestStartPortfolioWatcher(t *testing.T) {
|
||||
t.Parallel()
|
||||
newBase := Base{}
|
||||
err := newBase.AddAddress("LX2LMYXtuv5tiYEMztSSoEZcafFPYJFRK1",
|
||||
currency.LTC.String(),
|
||||
currency.NewCode(PersonalAddress),
|
||||
0.02)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
err = newBase.AddAddress("Testy",
|
||||
currency.LTC.String(),
|
||||
currency.NewCode(PersonalAddress),
|
||||
0.02)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if !newBase.AddressExists("LX2LMYXtuv5tiYEMztSSoEZcafFPYJFRK1") {
|
||||
t.Error("address does not exist")
|
||||
}
|
||||
|
||||
go newBase.StartPortfolioWatcher()
|
||||
}
|
||||
|
||||
func seedPortFolioForTest(t *testing.T) *Base {
|
||||
t.Helper()
|
||||
newBase := Base{}
|
||||
|
||||
err := newBase.AddAddress(core.BitcoinDonationAddress, "test", currency.BTC, 1500)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to add portfolio address with reason: %v, unable to continue tests", err)
|
||||
}
|
||||
newBase.Addresses[0].WhiteListed = true
|
||||
newBase.Addresses[0].ColdStorage = true
|
||||
newBase.Addresses[0].SupportedExchanges = "BTC Markets,Binance"
|
||||
|
||||
err = newBase.AddAddress(testBTCAddress, "test", currency.BTC, 1500)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to add portfolio address with reason: %v, unable to continue tests", err)
|
||||
}
|
||||
newBase.Addresses[1].SupportedExchanges = "BTC Markets,Binance"
|
||||
b := Base{}
|
||||
b.Seed(newBase)
|
||||
if len(b.Addresses) == 0 {
|
||||
t.Error("failed to seed")
|
||||
}
|
||||
return &b
|
||||
assert.ErrorIs(t, b.StartPortfolioWatcher(t.Context(), time.Second), errNoPortfolioItemsToWatch)
|
||||
|
||||
assert.NoError(t, b.AddAddress(testXRPAddress, PersonalAddress, currency.XRP, 0.02))
|
||||
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
cancel()
|
||||
assert.ErrorIs(t, b.StartPortfolioWatcher(ctx, 0), context.Canceled, "StartPortfolioWatcher should return context.Canceled")
|
||||
|
||||
b.Providers = append(b.Providers, provider{
|
||||
Name: "XRPScan",
|
||||
Enabled: true,
|
||||
})
|
||||
|
||||
ctx2, cancel2 := context.WithCancel(t.Context())
|
||||
|
||||
doneCh := make(chan error)
|
||||
go func() {
|
||||
doneCh <- b.StartPortfolioWatcher(ctx2, time.Second)
|
||||
}()
|
||||
|
||||
assert.Eventually(t, func() bool {
|
||||
portfolio := b.GetPersonalPortfolio()
|
||||
xrpBalance, ok := portfolio[currency.XRP]
|
||||
return ok && xrpBalance > 0.02
|
||||
}, 10*time.Second, time.Second, "GetPersonalPortfolio should return a balance greater than 0.02")
|
||||
|
||||
cancel2()
|
||||
assert.ErrorIs(t, <-doneCh, context.Canceled, "StartPortfolioWatcher should return a context canceled error")
|
||||
}
|
||||
|
||||
func TestGetProvider(t *testing.T) {
|
||||
t.Parallel()
|
||||
b := Base{
|
||||
Providers: providers{
|
||||
{
|
||||
Name: "XRPScan",
|
||||
Enabled: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
p, ok := b.Providers.GetProvider("XrPSCaN")
|
||||
assert.True(t, ok, "GetProvider should return true for existing provider")
|
||||
assert.Equal(t, "XRPScan", p.Name, "GetProvider should return the correct provider name")
|
||||
assert.True(t, p.Enabled, "GetProvider should return the correct provider enabled status")
|
||||
p, ok = b.Providers.GetProvider("NonExistent")
|
||||
assert.False(t, ok, "GetProvider should return false for non-existent provider")
|
||||
assert.Equal(t, provider{}, p, "GetProvider should return an empty provider for non-existent provider")
|
||||
}
|
||||
|
||||
@@ -1,15 +1,22 @@
|
||||
package portfolio
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/thrasher-corp/gocryptotrader/currency"
|
||||
"golang.org/x/time/rate"
|
||||
)
|
||||
|
||||
// Base holds the portfolio base addresses
|
||||
type Base struct {
|
||||
Addresses []Address `json:"addresses"`
|
||||
Verbose bool
|
||||
Providers providers `json:"providers,omitzero"`
|
||||
Verbose bool `json:"verbose"`
|
||||
|
||||
mtx sync.RWMutex
|
||||
cryptoIDLimiter *rate.Limiter
|
||||
cryptoIDLimiterOnce sync.Once
|
||||
}
|
||||
|
||||
// Address sub type holding address information for portfolio
|
||||
@@ -159,3 +166,11 @@ type AccountInfo struct {
|
||||
Twitter string `json:"twitter"`
|
||||
Verified bool `json:"verified"`
|
||||
}
|
||||
|
||||
type provider struct {
|
||||
Name string `json:"name"`
|
||||
Enabled bool `json:"enabled"`
|
||||
APIKey string `json:"apiKey,omitempty"`
|
||||
}
|
||||
|
||||
type providers []provider
|
||||
|
||||
15
testdata/configtest.json
vendored
15
testdata/configtest.json
vendored
@@ -238,6 +238,21 @@
|
||||
"ColdStorage": false,
|
||||
"SupportedExchanges": ""
|
||||
}
|
||||
],
|
||||
"providers": [
|
||||
{
|
||||
"name": "Ethplorer",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"name": "XRPScan",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"name": "CryptoID",
|
||||
"enabled": false,
|
||||
"apiKey": "Key"
|
||||
}
|
||||
]
|
||||
},
|
||||
"exchanges": [
|
||||
|
||||
Reference in New Issue
Block a user