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 }