Files
gocryptotrader/currency/storage.go
Ryan O'Hara-Reid 11da520dc8 Currency: Add additional functionality, refactor and improvements (#881)
* currency: Add method to derive pair

* currency: Add method to lower entire charset but used the slice copy and returned that. This will change the original, just gotta see if this is an issue, but the slice usually goes out of scope anyway.

* currency/pairs: add filter method

* currency: add function to derive select currencies from currency pairs

* currency/engine: slight adjustments

* currency: fix linter issue also shift burden of proof to caller instead of repair, more performant.

* currency: more linter

* pairs: optimize; reduce allocs/op and B/op

* currency: Add in function 'NewPairsFromString' for testing purposes

* currency: don't suppress error

* currency: stop panic on empty currency code

* currency: Add helper method to match currencies between exchanges

* currency: fixed my bad spelling

* currency: Implement stable coin checks, refactored base code methods, optimized upper and lower case strings for currency code/pairs

* currency: add pairs method to derive stable coins from internal list.

* Currency: Cleanup, fix tests.

* engine/exchanges/currency: fix whoops

* Currency: force govet no copy on Item datatype

* Currency: fix naughty linter issues

* exchange: revert change

* currency/config: fix config upgrade mistake

* currency: re-implement currency sub-systems

* *RetrieveConfigCurrencyPairs removed
*CheckCurrencyConfigValues to only provide warnings, add additional support when, disable when support is lost or not available and set default values.
*Drop Cryptocurrencies from configuration as this is not needed.
*Drop REST Poll delay field as this was unused.
*Update default values for currencyFileUpdateDuration & foreignExchangeUpdateDuration.
*Allow Role to be marshalled for file type.
*Refactor RunUpdater to verify and check config values and set default running foreign exchange provider.

* currency: cleanup

* currency: change match -> equal for comparison which is more of a standard and little easier to find

* currency: address nits

* currency: fix whoops

* currency: Add some more pairs methods

* currency: linter issues

* currency: RM unused field

* currency: rm verbose

* currency: fix word

* currency: gocritic

* currency: fix another whoopsie

* example_config: default to show log system name

* Currency: Force all support packages to use Equal method for comparison as there is a small comparison bug when checking upper and lower casing, this has a more of a pronounced impact between exchanges and client instances of currency generation

* currency: fix log name

* ordermanager: fix potential panic

* currency: small optim.

* engine: display correct bool and force shutdown

* currency: add function and fix regression
* Change ConvertCurrency -> ConvertFiat to be more precise
* ADD GetForeignExchangeRate to get specific exchange rate for fiat pair
* Fix currency display and formatting regression and tied in with config.Currency fields

* engine: fix tests

* currency: return the amount when no conversion needs to take place

* currency: reduce method name

* currency: Address nits glorious nits

* currency: fix linter

* currency: addr nits

* currency: check underlying role in test

* gct: change to EMPTYCODE and EMPTYPAIR across codebase

* currency: fix nits

* currency: this fixes test race but this issue has not been resolved. Please see: https://trello.com/c/54eizOIo/143-currency-package-upgrades

* currency: Add temp dir for testing

* Update engine/engine.go

Co-authored-by: Adrian Gallagher <adrian.gallagher@thrasher.io>

* documentation: update and regen

* currency: Address niterinos

* currency: Add test case for config upgrade when falling over to exchange rate host as default from exchangeRates provider

* currency: addr nits

* currency: fix whoops

Co-authored-by: Ryan O'Hara-Reid <ryan.oharareid@thrasher.io>
Co-authored-by: Adrian Gallagher <adrian.gallagher@thrasher.io>
2022-02-17 16:24:57 +11:00

763 lines
22 KiB
Go

package currency
import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"path/filepath"
"time"
"github.com/thrasher-corp/gocryptotrader/common/file"
"github.com/thrasher-corp/gocryptotrader/currency/coinmarketcap"
"github.com/thrasher-corp/gocryptotrader/currency/forexprovider"
"github.com/thrasher-corp/gocryptotrader/currency/forexprovider/base"
"github.com/thrasher-corp/gocryptotrader/log"
)
// storage is an overarching type that keeps track of and updates currency,
// currency exchange rates and pairs
var storage Storage
func init() {
storage.SetDefaults()
}
// CurrencyFileUpdateDelay defines the rate at which the currency.json file is
// updated
const (
DefaultCurrencyFileDelay = 168 * time.Hour
DefaultForeignExchangeDelay = 1 * time.Minute
DefaultStorageFile = "currency.json"
DefaultForexProviderExchangeRatesAPI = "ExchangeRateHost"
)
var (
// ErrFiatDisplayCurrencyIsNotFiat defines an error for when the fiat
// display currency is not set as a fiat currency.
ErrFiatDisplayCurrencyIsNotFiat = errors.New("fiat display currency is not a fiat currency")
errUnexpectedRole = errors.New("unexpected currency role")
errFiatDisplayCurrencyUnset = errors.New("fiat display currency is unset")
errNoFilePathSet = errors.New("no file path set")
errInvalidCurrencyFileUpdateDuration = errors.New("invalid currency file update duration")
errInvalidForeignExchangeUpdateDuration = errors.New("invalid foreign exchange update duration")
errNoForeignExchangeProvidersEnabled = errors.New("no foreign exchange providers enabled")
errNotFiatCurrency = errors.New("not a fiat currency")
errInvalidAmount = errors.New("invalid amount")
)
// SetDefaults sets storage defaults for basic package functionality
func (s *Storage) SetDefaults() {
s.defaultBaseCurrency = USD
s.baseCurrency = s.defaultBaseCurrency
var fiatCurrencies []Code
for item := range symbols {
if item == USDT.Item {
continue
}
fiatCurrencies = append(fiatCurrencies, Code{Item: item, UpperCase: true})
}
err := s.SetDefaultFiatCurrencies(fiatCurrencies)
if err != nil {
log.Errorf(log.Currency, "Currency Storage: Setting default fiat currencies error: %s", err)
}
err = s.SetStableCoins(stables)
if err != nil {
log.Errorf(log.Currency, "Currency Storage: Setting default stable currencies error: %s", err)
}
err = s.SetDefaultCryptocurrencies(Currencies{BTC, LTC, ETH, DOGE, DASH, XRP, XMR, USDT, UST})
if err != nil {
log.Errorf(log.Currency, "Currency Storage: Setting default cryptocurrencies error: %s", err)
}
s.SetupConversionRates()
s.fiatExchangeMarkets = forexprovider.NewDefaultFXProvider()
}
// RunUpdater runs the foreign exchange updater service. This will set up a JSON
// dump file and keep foreign exchange rates updated as fast as possible without
// triggering rate limiters, it will also run a full cryptocurrency check
// through coin market cap and expose analytics for exchange services
func (s *Storage) RunUpdater(overrides BotOverrides, settings *Config, filePath string) error {
if settings.FiatDisplayCurrency.IsEmpty() {
return errFiatDisplayCurrencyUnset
}
if !settings.FiatDisplayCurrency.IsFiatCurrency() {
return fmt.Errorf("%s: %w", settings.FiatDisplayCurrency, ErrFiatDisplayCurrencyIsNotFiat)
}
if filePath == "" {
return errNoFilePathSet
}
if settings.CurrencyFileUpdateDuration <= 0 {
return errInvalidCurrencyFileUpdateDuration
}
if settings.ForeignExchangeUpdateDuration <= 0 {
return errInvalidForeignExchangeUpdateDuration
}
s.mtx.Lock()
s.shutdown = make(chan struct{})
s.baseCurrency = settings.FiatDisplayCurrency
s.path = filepath.Join(filePath, DefaultStorageFile)
s.currencyFileUpdateDelay = settings.CurrencyFileUpdateDuration
s.foreignExchangeUpdateDelay = settings.ForeignExchangeUpdateDuration
log.Debugf(log.Currency, "Fiat display currency: %s.\n", s.baseCurrency)
var err error
if overrides.Coinmarketcap {
if settings.CryptocurrencyProvider.APIKey != "" &&
settings.CryptocurrencyProvider.APIKey != "Key" {
log.Debugln(log.Currency, "Setting up currency analysis system with Coinmarketcap...")
s.currencyAnalysis, err = coinmarketcap.NewFromSettings(coinmarketcap.Settings(settings.CryptocurrencyProvider))
if err != nil {
log.Errorf(log.Currency, "Unable to setup CoinMarketCap analysis. Error: %s", err)
}
} else {
log.Warnf(log.Currency, "%s API key not set, disabling. Please set this in your config.json file\n",
settings.CryptocurrencyProvider.Name)
}
}
var fxSettings []base.Settings
var primaryProvider bool
for i := range settings.ForexProviders {
enabled := (settings.ForexProviders[i].Name == "CurrencyConverter" && overrides.CurrencyConverter) ||
(settings.ForexProviders[i].Name == "CurrencyLayer" && overrides.CurrencyLayer) ||
(settings.ForexProviders[i].Name == "Fixer" && overrides.Fixer) ||
(settings.ForexProviders[i].Name == "OpenExchangeRates" && overrides.OpenExchangeRates) ||
(settings.ForexProviders[i].Name == "ExchangeRates" && overrides.ExchangeRates) ||
(settings.ForexProviders[i].Name == "ExchangeRateHost" && overrides.ExchangeRateHost)
if !enabled {
continue
}
if settings.ForexProviders[i].Name != DefaultForexProviderExchangeRatesAPI {
if settings.ForexProviders[i].APIKey == "" || settings.ForexProviders[i].APIKey == "Key" {
log.Warnf(log.Currency, "%s forex provider API key not set, disabling. Please set this in your config.json file\n",
settings.ForexProviders[i].Name)
settings.ForexProviders[i].Enabled = false
settings.ForexProviders[i].PrimaryProvider = false
continue
}
if settings.ForexProviders[i].APIKeyLvl == -1 && settings.ForexProviders[i].Name != "ExchangeRates" {
log.Warnf(log.Currency, "%s APIKey level not set, functionality is limited. Please review this in your config.json file\n",
settings.ForexProviders[i].Name)
}
}
if settings.ForexProviders[i].PrimaryProvider {
if primaryProvider {
log.Warnf(log.Currency, "%s disabling primary provider, multiple primarys found. Please review providers in your config.json file\n",
settings.ForexProviders[i].Name)
settings.ForexProviders[i].PrimaryProvider = false
} else {
primaryProvider = true
}
}
fxSettings = append(fxSettings, base.Settings(settings.ForexProviders[i]))
}
if len(fxSettings) == 0 {
log.Warnln(log.Currency, "No foreign exchange providers enabled, setting default provider...")
for x := range settings.ForexProviders {
if settings.ForexProviders[x].Name != DefaultForexProviderExchangeRatesAPI {
continue
}
settings.ForexProviders[x].Enabled = true
settings.ForexProviders[x].PrimaryProvider = true
primaryProvider = true
log.Warnf(log.Currency, "No valid foreign exchange providers configured. Defaulting to %s.", DefaultForexProviderExchangeRatesAPI)
fxSettings = append(fxSettings, base.Settings(settings.ForexProviders[x]))
}
}
if len(fxSettings) == 0 {
s.mtx.Unlock()
return errNoForeignExchangeProvidersEnabled
}
if !primaryProvider {
for x := range settings.ForexProviders {
if settings.ForexProviders[x].Name == fxSettings[0].Name {
settings.ForexProviders[x].PrimaryProvider = true
fxSettings[0].PrimaryProvider = true
log.Warnf(log.Currency, "No primary foreign exchange provider set. Defaulting to %s.", fxSettings[0].Name)
break
}
}
}
s.fiatExchangeMarkets, err = forexprovider.StartFXService(fxSettings)
if err != nil {
s.mtx.Unlock()
return err
}
log.Debugf(log.Currency, "Using primary foreign exchange provider %s\n",
s.fiatExchangeMarkets.Primary.Provider.GetName())
for i := range s.fiatExchangeMarkets.Support {
log.Debugf(log.Currency, "Supporting foreign exchange provider %s\n",
s.fiatExchangeMarkets.Support[i].Provider.GetName())
}
// Mutex present in this go routine to lock down retrieving rate data
// until this system initially updates
s.wg.Add(1)
go s.ForeignExchangeUpdater()
return nil
}
// SetupConversionRates sets default conversion rate values
func (s *Storage) SetupConversionRates() {
s.fxRates = ConversionRates{
m: make(map[*Item]map[*Item]*float64),
}
}
// SetDefaultFiatCurrencies assigns the default fiat currency list and adds it
// to the running list
func (s *Storage) SetDefaultFiatCurrencies(c Currencies) error {
for i := range c {
err := s.currencyCodes.UpdateCurrency("", c[i].String(), "", 0, Fiat)
if err != nil {
return err
}
}
s.defaultFiatCurrencies = append(s.defaultFiatCurrencies, c...)
s.fiatCurrencies = append(s.fiatCurrencies, c...)
return nil
}
// SetStableCoins assigns the stable currency list and adds it to the running
// list
func (s *Storage) SetStableCoins(c Currencies) error {
for i := range c {
err := s.currencyCodes.UpdateCurrency("", c[i].String(), "", 0, Stable)
if err != nil {
return err
}
}
s.stableCurrencies = append(s.stableCurrencies, c...)
return nil
}
// SetDefaultCryptocurrencies assigns the default cryptocurrency list and adds
// it to the running list
func (s *Storage) SetDefaultCryptocurrencies(c Currencies) error {
for i := range c {
err := s.currencyCodes.UpdateCurrency("",
c[i].String(),
"",
0,
Cryptocurrency)
if err != nil {
return err
}
}
s.defaultCryptoCurrencies = append(s.defaultCryptoCurrencies, c...)
s.cryptocurrencies = append(s.cryptocurrencies, c...)
return nil
}
// SetupForexProviders sets up a new instance of the forex providers
func (s *Storage) SetupForexProviders(setting ...base.Settings) error {
addr, err := forexprovider.StartFXService(setting)
if err != nil {
return err
}
s.fiatExchangeMarkets = addr
return nil
}
// ForeignExchangeUpdater is a routine that seeds foreign exchange rate and keeps
// updated as fast as possible
func (s *Storage) ForeignExchangeUpdater() {
defer s.wg.Done()
log.Debugln(log.Currency, "Foreign exchange updater started, seeding FX rate list...")
err := s.SeedCurrencyAnalysisData()
if err != nil {
log.Errorln(log.Currency, err)
}
err = s.SeedForeignExchangeRates()
if err != nil {
log.Errorln(log.Currency, err)
}
// Unlock main rate retrieval mutex so all routines waiting can get access
// to data
s.mtx.Unlock()
// Set tickers to client defined rates or defaults
SeedForeignExchangeTick := time.NewTicker(s.foreignExchangeUpdateDelay)
SeedCurrencyAnalysisTick := time.NewTicker(s.currencyFileUpdateDelay)
defer SeedForeignExchangeTick.Stop()
defer SeedCurrencyAnalysisTick.Stop()
for {
select {
case <-s.shutdown:
return
case <-SeedForeignExchangeTick.C:
go func() {
err := s.SeedForeignExchangeRates()
if err != nil {
log.Errorln(log.Currency, err)
}
}()
case <-SeedCurrencyAnalysisTick.C:
go func() {
err := s.SeedCurrencyAnalysisData()
if err != nil {
log.Errorln(log.Currency, err)
}
}()
}
}
}
// SeedCurrencyAnalysisData sets a new instance of a coinmarketcap data.
func (s *Storage) SeedCurrencyAnalysisData() error {
if s.currencyCodes.LastMainUpdate.IsZero() {
b, err := ioutil.ReadFile(s.path)
if err != nil {
return s.FetchCurrencyAnalysisData()
}
var f *File
err = json.Unmarshal(b, &f)
if err != nil {
return err
}
err = s.LoadFileCurrencyData(f)
if err != nil {
return err
}
}
// Based on update delay update the file
if time.Now().After(s.currencyCodes.LastMainUpdate.Add(s.currencyFileUpdateDelay)) ||
s.currencyCodes.LastMainUpdate.IsZero() {
err := s.FetchCurrencyAnalysisData()
if err != nil {
return err
}
}
return nil
}
// FetchCurrencyAnalysisData fetches a new fresh batch of currency data and
// loads it into memory
func (s *Storage) FetchCurrencyAnalysisData() error {
if s.currencyAnalysis == nil {
log.Warnln(log.Currency,
"Currency analysis system offline, please set api keys for coinmarketcap if you wish to use this feature.")
return errors.New("currency analysis system offline")
}
return s.UpdateCurrencies()
}
// WriteCurrencyDataToFile writes the full currency data to a designated file
func (s *Storage) WriteCurrencyDataToFile(path string, mainUpdate bool) error {
data, err := s.currencyCodes.GetFullCurrencyData()
if err != nil {
return err
}
if mainUpdate {
t := time.Now()
data.LastMainUpdate = t.Unix()
s.currencyCodes.LastMainUpdate = t
}
var encoded []byte
encoded, err = json.MarshalIndent(data, "", " ")
if err != nil {
return err
}
return file.Write(path, encoded)
}
func (s *Storage) checkFileCurrencyData(item *Item, role Role) error {
if item.Role == Unset {
item.Role = role
}
if item.Role != role {
return fmt.Errorf("%w %s expecting: %s", errUnexpectedRole, item.Role, role)
}
return s.currencyCodes.LoadItem(item)
}
// LoadFileCurrencyData loads currencies into the currency codes
func (s *Storage) LoadFileCurrencyData(f *File) error {
for i := range f.Contracts {
err := s.checkFileCurrencyData(f.Contracts[i], Contract)
if err != nil {
return err
}
}
for i := range f.Cryptocurrency {
err := s.checkFileCurrencyData(f.Cryptocurrency[i], Cryptocurrency)
if err != nil {
return err
}
}
for i := range f.Token {
err := s.checkFileCurrencyData(f.Token[i], Token)
if err != nil {
return err
}
}
for i := range f.FiatCurrency {
err := s.checkFileCurrencyData(f.FiatCurrency[i], Fiat)
if err != nil {
return err
}
}
for i := range f.UnsetCurrency {
err := s.checkFileCurrencyData(f.UnsetCurrency[i], Unset)
if err != nil {
return err
}
}
for i := range f.Stable {
err := s.checkFileCurrencyData(f.Stable[i], Stable)
if err != nil {
return err
}
}
switch t := f.LastMainUpdate.(type) {
case string:
parseT, err := time.Parse(time.RFC3339Nano, t)
if err != nil {
return err
}
s.currencyCodes.LastMainUpdate = parseT
case float64:
s.currencyCodes.LastMainUpdate = time.Unix(int64(t), 0)
default:
return errors.New("unhandled type conversion for LastMainUpdate time")
}
return nil
}
// UpdateCurrencies updates currency role and information using coin market cap
func (s *Storage) UpdateCurrencies() error {
m, err := s.currencyAnalysis.GetCryptocurrencyIDMap()
if err != nil {
return err
}
for x := range m {
if m[x].IsActive != 1 {
continue
}
if m[x].Platform.Symbol != "" {
err = s.currencyCodes.UpdateCurrency(m[x].Name,
m[x].Symbol,
m[x].Platform.Symbol,
m[x].ID,
Token)
if err != nil {
return err
}
continue
}
err = s.currencyCodes.UpdateCurrency(m[x].Name,
m[x].Symbol,
"",
m[x].ID,
Cryptocurrency)
if err != nil {
return err
}
}
return nil
}
// SeedForeignExchangeRatesByCurrencies seeds the foreign exchange rates by
// currencies supplied
func (s *Storage) SeedForeignExchangeRatesByCurrencies(c Currencies) error {
s.fxRates.mtx.Lock()
defer s.fxRates.mtx.Unlock()
rates, err := s.fiatExchangeMarkets.GetCurrencyData(s.baseCurrency.String(),
c.Strings())
if err != nil {
return err
}
return s.updateExchangeRates(rates)
}
// SeedForeignExchangeRate returns a singular exchange rate
func (s *Storage) SeedForeignExchangeRate(from, to Code) (map[string]float64, error) {
return s.fiatExchangeMarkets.GetCurrencyData(from.String(),
[]string{to.String()})
}
// GetDefaultForeignExchangeRates returns foreign exchange rates based off
// default fiat currencies.
func (s *Storage) GetDefaultForeignExchangeRates() (Conversions, error) {
if !s.updaterRunning {
err := s.SeedDefaultForeignExchangeRates()
if err != nil {
return nil, err
}
}
return s.fxRates.GetFullRates(), nil
}
// SeedDefaultForeignExchangeRates seeds the default foreign exchange rates
func (s *Storage) SeedDefaultForeignExchangeRates() error {
s.fxRates.mtx.Lock()
defer s.fxRates.mtx.Unlock()
rates, err := s.fiatExchangeMarkets.GetCurrencyData(
s.defaultBaseCurrency.String(),
s.defaultFiatCurrencies.Strings())
if err != nil {
return err
}
return s.updateExchangeRates(rates)
}
// GetExchangeRates returns storage seeded exchange rates
func (s *Storage) GetExchangeRates() (Conversions, error) {
if !s.updaterRunning {
err := s.SeedForeignExchangeRates()
if err != nil {
return nil, err
}
}
return s.fxRates.GetFullRates(), nil
}
// SeedForeignExchangeRates seeds the foreign exchange rates from storage config
// currencies
func (s *Storage) SeedForeignExchangeRates() error {
s.fxRates.mtx.Lock()
defer s.fxRates.mtx.Unlock()
rates, err := s.fiatExchangeMarkets.GetCurrencyData(s.baseCurrency.String(),
s.fiatCurrencies.Strings())
if err != nil {
return err
}
return s.updateExchangeRates(rates)
}
// UpdateForeignExchangeRates sets exchange rates on the FX map
func (s *Storage) updateExchangeRates(m map[string]float64) error {
return s.fxRates.Update(m)
}
// GetTotalMarketCryptocurrencies returns the total seeded market
// cryptocurrencies
func (s *Storage) GetTotalMarketCryptocurrencies() (Currencies, error) {
if !s.currencyCodes.HasData() {
return nil, errors.New("market currency codes not populated")
}
return s.currencyCodes.GetCurrencies(), nil
}
// IsDefaultCurrency returns if a currency is a default currency
func (s *Storage) IsDefaultCurrency(c Code) bool {
for i := range s.defaultFiatCurrencies {
if s.defaultFiatCurrencies[i].Equal(c) ||
s.defaultFiatCurrencies[i].Equal(GetTranslation(c)) {
return true
}
}
return false
}
// IsDefaultCryptocurrency returns if a cryptocurrency is a default
// cryptocurrency
func (s *Storage) IsDefaultCryptocurrency(c Code) bool {
for i := range s.defaultCryptoCurrencies {
if s.defaultCryptoCurrencies[i].Equal(c) ||
s.defaultCryptoCurrencies[i].Equal(GetTranslation(c)) {
return true
}
}
return false
}
// ValidateCode validates string against currency list and returns a currency
// code
func (s *Storage) ValidateCode(newCode string) Code {
return s.currencyCodes.Register(newCode, Unset)
}
// ValidateFiatCode validates a fiat currency string and returns a currency
// code
func (s *Storage) ValidateFiatCode(newCode string) Code {
c := s.currencyCodes.Register(newCode, Fiat)
if !s.fiatCurrencies.Contains(c) {
s.fiatCurrencies = append(s.fiatCurrencies, c)
}
return c
}
// UpdateBaseCurrency changes base currency
func (s *Storage) UpdateBaseCurrency(c Code) error {
if c.IsFiatCurrency() {
s.baseCurrency = c
return nil
}
return fmt.Errorf("currency %s not fiat failed to set currency", c)
}
// GetCryptocurrencies returns the cryptocurrency list
func (s *Storage) GetCryptocurrencies() Currencies {
return s.cryptocurrencies
}
// GetDefaultCryptocurrencies returns a list of default cryptocurrencies
func (s *Storage) GetDefaultCryptocurrencies() Currencies {
return s.defaultCryptoCurrencies
}
// GetFiatCurrencies returns the fiat currencies list
func (s *Storage) GetFiatCurrencies() Currencies {
return s.fiatCurrencies
}
// GetDefaultFiatCurrencies returns the default fiat currencies list
func (s *Storage) GetDefaultFiatCurrencies() Currencies {
return s.defaultFiatCurrencies
}
// GetDefaultBaseCurrency returns the default base currency
func (s *Storage) GetDefaultBaseCurrency() Code {
return s.defaultBaseCurrency
}
// GetBaseCurrency returns the current storage base currency
func (s *Storage) GetBaseCurrency() Code {
return s.baseCurrency
}
// UpdateEnabledCryptoCurrencies appends new cryptocurrencies to the enabled
// currency list
func (s *Storage) UpdateEnabledCryptoCurrencies(c Currencies) {
for i := range c {
if !s.cryptocurrencies.Contains(c[i]) {
s.cryptocurrencies = append(s.cryptocurrencies, c[i])
}
}
}
// UpdateEnabledFiatCurrencies appends new fiat currencies to the enabled
// currency list
func (s *Storage) UpdateEnabledFiatCurrencies(c Currencies) {
for i := range c {
if !s.fiatCurrencies.Contains(c[i]) &&
!s.cryptocurrencies.Contains(c[i]) {
s.fiatCurrencies = append(s.fiatCurrencies, c[i])
}
}
}
// ConvertCurrency for example converts $1 USD to the equivalent Japanese Yen
// or vice versa.
func (s *Storage) ConvertCurrency(amount float64, from, to Code) (float64, error) {
if amount <= 0 {
return 0, fmt.Errorf("%f %w", amount, errInvalidAmount)
}
if !from.IsFiatCurrency() {
return 0, fmt.Errorf("%s %w", from, errNotFiatCurrency)
}
if !to.IsFiatCurrency() {
return 0, fmt.Errorf("%s %w", to, errNotFiatCurrency)
}
if from.Equal(to) { // No need to lock down storage for this rate.
return amount, nil
}
s.mtx.Lock()
defer s.mtx.Unlock()
if !s.fxRates.HasData() {
err := s.SeedDefaultForeignExchangeRates()
if err != nil {
return 0, err
}
}
r, err := s.fxRates.GetRate(from, to)
if err != nil {
return 0, err
}
return r * amount, nil
}
// GetStorageRate returns the rate of the conversion value
func (s *Storage) GetStorageRate(from, to Code) (float64, error) {
s.mtx.Lock()
defer s.mtx.Unlock()
if !s.fxRates.HasData() {
err := s.SeedDefaultForeignExchangeRates()
if err != nil {
return 0, err
}
}
return s.fxRates.GetRate(from, to)
}
// NewConversion returns a new conversion object that has a pointer to a related
// rate with its inversion.
func (s *Storage) NewConversion(from, to Code) (Conversion, error) {
s.mtx.Lock()
defer s.mtx.Unlock()
if !s.fxRates.HasData() {
err := storage.SeedDefaultForeignExchangeRates()
if err != nil {
return Conversion{}, err
}
}
return s.fxRates.Register(from, to)
}
// IsVerbose returns if the storage is in verbose mode
func (s *Storage) IsVerbose() bool {
return s.Verbose
}
// Shutdown shuts down the currency storage system and saves to currency.json
func (s *Storage) Shutdown() error {
s.mtx.Lock()
defer s.mtx.Unlock()
if s.shutdown == nil {
return nil
}
close(s.shutdown)
s.wg.Wait()
s.shutdown = nil
return s.WriteCurrencyDataToFile(s.path, true)
}