github.com/status-im/status-go@v1.1.0/services/wallet/market/market.go (about)

     1  package market
     2  
     3  import (
     4  	"context"
     5  	"sync"
     6  	"time"
     7  
     8  	"github.com/ethereum/go-ethereum/common"
     9  	"github.com/ethereum/go-ethereum/event"
    10  	"github.com/ethereum/go-ethereum/log"
    11  
    12  	"github.com/status-im/status-go/circuitbreaker"
    13  	"github.com/status-im/status-go/services/wallet/thirdparty"
    14  	"github.com/status-im/status-go/services/wallet/walletevent"
    15  )
    16  
    17  const (
    18  	EventMarketStatusChanged walletevent.EventType = "wallet-market-status-changed"
    19  )
    20  
    21  type DataPoint struct {
    22  	Price     float64
    23  	UpdatedAt int64
    24  }
    25  
    26  type DataPerTokenAndCurrency = map[string]map[string]DataPoint
    27  
    28  type Manager struct {
    29  	feed            *event.Feed
    30  	priceCache      DataPerTokenAndCurrency
    31  	priceCacheLock  sync.RWMutex
    32  	IsConnected     bool
    33  	LastCheckedAt   int64
    34  	IsConnectedLock sync.RWMutex
    35  	circuitbreaker  *circuitbreaker.CircuitBreaker
    36  	providers       []thirdparty.MarketDataProvider
    37  }
    38  
    39  func NewManager(providers []thirdparty.MarketDataProvider, feed *event.Feed) *Manager {
    40  	cb := circuitbreaker.NewCircuitBreaker(circuitbreaker.Config{
    41  		Timeout:               10000,
    42  		MaxConcurrentRequests: 100,
    43  		SleepWindow:           300000,
    44  		ErrorPercentThreshold: 25,
    45  	})
    46  
    47  	return &Manager{
    48  		feed:           feed,
    49  		priceCache:     make(DataPerTokenAndCurrency),
    50  		IsConnected:    true,
    51  		LastCheckedAt:  time.Now().Unix(),
    52  		circuitbreaker: cb,
    53  		providers:      providers,
    54  	}
    55  }
    56  
    57  func (pm *Manager) setIsConnected(value bool) {
    58  	pm.IsConnectedLock.Lock()
    59  	defer pm.IsConnectedLock.Unlock()
    60  	pm.LastCheckedAt = time.Now().Unix()
    61  	if value != pm.IsConnected {
    62  		message := "down"
    63  		if value {
    64  			message = "up"
    65  		}
    66  		pm.feed.Send(walletevent.Event{
    67  			Type:     EventMarketStatusChanged,
    68  			Accounts: []common.Address{},
    69  			Message:  message,
    70  			At:       time.Now().Unix(),
    71  		})
    72  	}
    73  	pm.IsConnected = value
    74  }
    75  
    76  func (pm *Manager) makeCall(providers []thirdparty.MarketDataProvider, f func(provider thirdparty.MarketDataProvider) (interface{}, error)) (interface{}, error) {
    77  	cmd := circuitbreaker.NewCommand(context.Background(), nil)
    78  	for _, provider := range providers {
    79  		provider := provider
    80  		cmd.Add(circuitbreaker.NewFunctor(func() ([]interface{}, error) {
    81  			result, err := f(provider)
    82  			return []interface{}{result}, err
    83  		}, provider.ID()))
    84  	}
    85  
    86  	result := pm.circuitbreaker.Execute(cmd)
    87  	pm.setIsConnected(result.Error() == nil)
    88  
    89  	if result.Error() != nil {
    90  		log.Error("Error fetching prices", "error", result.Error())
    91  		return nil, result.Error()
    92  	}
    93  
    94  	return result.Result()[0], nil
    95  }
    96  
    97  func (pm *Manager) FetchHistoricalDailyPrices(symbol string, currency string, limit int, allData bool, aggregate int) ([]thirdparty.HistoricalPrice, error) {
    98  	result, err := pm.makeCall(pm.providers, func(provider thirdparty.MarketDataProvider) (interface{}, error) {
    99  		return provider.FetchHistoricalDailyPrices(symbol, currency, limit, allData, aggregate)
   100  	})
   101  
   102  	if err != nil {
   103  		log.Error("Error fetching prices", "error", err)
   104  		return nil, err
   105  	}
   106  
   107  	prices := result.([]thirdparty.HistoricalPrice)
   108  	return prices, nil
   109  }
   110  
   111  func (pm *Manager) FetchHistoricalHourlyPrices(symbol string, currency string, limit int, aggregate int) ([]thirdparty.HistoricalPrice, error) {
   112  	result, err := pm.makeCall(pm.providers, func(provider thirdparty.MarketDataProvider) (interface{}, error) {
   113  		return provider.FetchHistoricalHourlyPrices(symbol, currency, limit, aggregate)
   114  	})
   115  
   116  	if err != nil {
   117  		log.Error("Error fetching prices", "error", err)
   118  		return nil, err
   119  	}
   120  
   121  	prices := result.([]thirdparty.HistoricalPrice)
   122  	return prices, nil
   123  }
   124  
   125  func (pm *Manager) FetchTokenMarketValues(symbols []string, currency string) (map[string]thirdparty.TokenMarketValues, error) {
   126  	result, err := pm.makeCall(pm.providers, func(provider thirdparty.MarketDataProvider) (interface{}, error) {
   127  		return provider.FetchTokenMarketValues(symbols, currency)
   128  	})
   129  
   130  	if err != nil {
   131  		log.Error("Error fetching prices", "error", err)
   132  		return nil, err
   133  	}
   134  
   135  	marketValues := result.(map[string]thirdparty.TokenMarketValues)
   136  	return marketValues, nil
   137  }
   138  
   139  func (pm *Manager) FetchTokenDetails(symbols []string) (map[string]thirdparty.TokenDetails, error) {
   140  	result, err := pm.makeCall(pm.providers, func(provider thirdparty.MarketDataProvider) (interface{}, error) {
   141  		return provider.FetchTokenDetails(symbols)
   142  	})
   143  
   144  	if err != nil {
   145  		log.Error("Error fetching prices", "error", err)
   146  		return nil, err
   147  	}
   148  
   149  	tokenDetails := result.(map[string]thirdparty.TokenDetails)
   150  	return tokenDetails, nil
   151  }
   152  
   153  func (pm *Manager) FetchPrice(symbol string, currency string) (float64, error) {
   154  	symbols := [1]string{symbol}
   155  	currencies := [1]string{currency}
   156  
   157  	prices, err := pm.FetchPrices(symbols[:], currencies[:])
   158  
   159  	if err != nil {
   160  		return 0, err
   161  	}
   162  
   163  	return prices[symbol][currency], nil
   164  }
   165  
   166  func (pm *Manager) FetchPrices(symbols []string, currencies []string) (map[string]map[string]float64, error) {
   167  	response, err := pm.makeCall(pm.providers, func(provider thirdparty.MarketDataProvider) (interface{}, error) {
   168  		return provider.FetchPrices(symbols, currencies)
   169  	})
   170  
   171  	if err != nil {
   172  		log.Error("Error fetching prices", "error", err)
   173  		return nil, err
   174  	}
   175  
   176  	prices := response.(map[string]map[string]float64)
   177  	pm.updatePriceCache(prices)
   178  	return prices, nil
   179  }
   180  
   181  func (pm *Manager) getCachedPricesFor(symbols []string, currencies []string) DataPerTokenAndCurrency {
   182  	prices := make(DataPerTokenAndCurrency)
   183  
   184  	for _, symbol := range symbols {
   185  		prices[symbol] = make(map[string]DataPoint)
   186  		for _, currency := range currencies {
   187  			prices[symbol][currency] = pm.priceCache[symbol][currency]
   188  		}
   189  	}
   190  
   191  	return prices
   192  }
   193  
   194  func (pm *Manager) updatePriceCache(prices map[string]map[string]float64) {
   195  	pm.priceCacheLock.Lock()
   196  	defer pm.priceCacheLock.Unlock()
   197  
   198  	for token, pricesPerCurrency := range prices {
   199  		_, present := pm.priceCache[token]
   200  		if !present {
   201  			pm.priceCache[token] = make(map[string]DataPoint)
   202  		}
   203  		for currency, price := range pricesPerCurrency {
   204  			pm.priceCache[token][currency] = DataPoint{
   205  				Price:     price,
   206  				UpdatedAt: time.Now().Unix(),
   207  			}
   208  		}
   209  	}
   210  }
   211  
   212  func (pm *Manager) GetCachedPrices() DataPerTokenAndCurrency {
   213  	pm.priceCacheLock.RLock()
   214  	defer pm.priceCacheLock.RUnlock()
   215  
   216  	return pm.priceCache
   217  }
   218  
   219  // Return cached price if present in cache and age is less than maxAgeInSeconds. Fetch otherwise.
   220  func (pm *Manager) GetOrFetchPrices(symbols []string, currencies []string, maxAgeInSeconds int64) (DataPerTokenAndCurrency, error) {
   221  	symbolsToFetchMap := make(map[string]bool)
   222  	symbolsToFetch := make([]string, 0, len(symbols))
   223  
   224  	now := time.Now().Unix()
   225  
   226  	for _, symbol := range symbols {
   227  		tokenPriceCache, ok := pm.GetCachedPrices()[symbol]
   228  		if !ok {
   229  			if !symbolsToFetchMap[symbol] {
   230  				symbolsToFetchMap[symbol] = true
   231  				symbolsToFetch = append(symbolsToFetch, symbol)
   232  			}
   233  			continue
   234  		}
   235  		for _, currency := range currencies {
   236  			if now-tokenPriceCache[currency].UpdatedAt > maxAgeInSeconds {
   237  				if !symbolsToFetchMap[symbol] {
   238  					symbolsToFetchMap[symbol] = true
   239  					symbolsToFetch = append(symbolsToFetch, symbol)
   240  				}
   241  				break
   242  			}
   243  		}
   244  	}
   245  
   246  	if len(symbolsToFetch) > 0 {
   247  		_, err := pm.FetchPrices(symbolsToFetch, currencies)
   248  		if err != nil {
   249  			return nil, err
   250  		}
   251  	}
   252  
   253  	prices := pm.getCachedPricesFor(symbols, currencies)
   254  
   255  	return prices, nil
   256  }