github.com/status-im/status-go@v1.1.0/services/wallet/history/exchange.go (about) 1 package history 2 3 import ( 4 "errors" 5 "sync" 6 "time" 7 8 "github.com/status-im/status-go/services/wallet/market" 9 ) 10 11 type tokenType = string 12 type currencyType = string 13 type yearType = int 14 15 type allTimeEntry struct { 16 value float32 17 startTimestamp int64 18 endTimestamp int64 19 } 20 21 // Exchange caches conversion rates in memory on a daily basis 22 type Exchange struct { 23 // year map keeps a list of values with days as index in the slice for the corresponding year (key) starting from the first to the last available 24 cache map[tokenType]map[currencyType]map[yearType][]float32 25 // special case for all time information 26 allTimeCache map[tokenType]map[currencyType][]allTimeEntry 27 fetchMutex sync.Mutex 28 29 marketManager *market.Manager 30 } 31 32 func NewExchange(marketManager *market.Manager) *Exchange { 33 return &Exchange{ 34 cache: make(map[tokenType]map[currencyType]map[yearType][]float32), 35 marketManager: marketManager, 36 } 37 } 38 39 // GetExchangeRate returns the exchange rate from token to currency in the day of the given date 40 // if none exists returns "missing <element>" error 41 func (e *Exchange) GetExchangeRateForDay(token tokenType, currency currencyType, date time.Time) (float32, error) { 42 e.fetchMutex.Lock() 43 defer e.fetchMutex.Unlock() 44 45 currencyMap, found := e.cache[token] 46 if !found { 47 return 0, errors.New("missing token") 48 } 49 50 yearsMap, found := currencyMap[currency] 51 if !found { 52 return 0, errors.New("missing currency") 53 } 54 55 year := date.Year() 56 valueForDays, found := yearsMap[year] 57 if !found { 58 // Search closest in all time 59 allCurrencyMap, found := e.allTimeCache[token] 60 if !found { 61 return 0, errors.New("missing token in all time data") 62 } 63 64 allYearsMap, found := allCurrencyMap[currency] 65 if !found { 66 return 0, errors.New("missing currency in all time data") 67 } 68 for _, entry := range allYearsMap { 69 if entry.startTimestamp <= date.Unix() && entry.endTimestamp > date.Unix() { 70 return entry.value, nil 71 } 72 } 73 return 0, errors.New("missing entry") 74 } 75 76 day := date.YearDay() 77 if day >= len(valueForDays) { 78 return 0, errors.New("missing day") 79 } 80 return valueForDays[day], nil 81 } 82 83 // fetchAndCacheRates fetches and in memory cache exchange rates for this and last year 84 func (e *Exchange) FetchAndCacheMissingRates(token tokenType, currency currencyType) error { 85 // Protect REST calls also to prevent fetching the same token/currency twice 86 e.fetchMutex.Lock() 87 defer e.fetchMutex.Unlock() 88 89 // Allocate missing values 90 currencyMap, found := e.cache[token] 91 if !found { 92 currencyMap = make(map[currencyType]map[yearType][]float32) 93 e.cache[token] = currencyMap 94 } 95 96 yearsMap, found := currencyMap[currency] 97 if !found { 98 yearsMap = make(map[yearType][]float32) 99 currencyMap[currency] = yearsMap 100 } 101 102 currentTime := time.Now().UTC() 103 endOfPrevYearTime := time.Date(currentTime.Year()-1, 12, 31, 23, 0, 0, 0, time.UTC) 104 105 daysToFetch := extendDaysSliceForYear(yearsMap, endOfPrevYearTime) 106 107 curYearTime := time.Date(currentTime.Year(), currentTime.Month(), currentTime.Day(), 0, 0, 0, 0, time.UTC) 108 daysToFetch += extendDaysSliceForYear(yearsMap, curYearTime) 109 if daysToFetch == 0 { 110 return nil 111 } 112 113 res, err := e.marketManager.FetchHistoricalDailyPrices(token, currency, daysToFetch, false, 1) 114 if err != nil { 115 return err 116 } 117 118 for i := 0; i < len(res); i++ { 119 t := time.Unix(res[i].Timestamp, 0).UTC() 120 yearDayIndex := t.YearDay() - 1 121 yearValues, found := yearsMap[t.Year()] 122 if found && yearDayIndex < len(yearValues) { 123 yearValues[yearDayIndex] = float32(res[i].Value) 124 } 125 } 126 127 // Fetch all time 128 allTime, err := e.marketManager.FetchHistoricalDailyPrices(token, currency, 1, true, 30) 129 if err != nil { 130 return err 131 } 132 133 if e.allTimeCache == nil { 134 e.allTimeCache = make(map[tokenType]map[currencyType][]allTimeEntry) 135 } 136 _, found = e.allTimeCache[token] 137 if !found { 138 e.allTimeCache[token] = make(map[currencyType][]allTimeEntry) 139 } 140 141 // No benefit to fetch intermendiate values, overwrite historical 142 e.allTimeCache[token][currency] = make([]allTimeEntry, 0) 143 144 for i := 0; i < len(allTime) && allTime[i].Timestamp < res[0].Timestamp; i++ { 145 if allTime[i].Value > 0 { 146 var endTimestamp int64 147 if i+1 < len(allTime) { 148 endTimestamp = allTime[i+1].Timestamp 149 } else { 150 endTimestamp = res[0].Timestamp 151 } 152 e.allTimeCache[token][currency] = append(e.allTimeCache[token][currency], 153 allTimeEntry{ 154 value: float32(allTime[i].Value), 155 startTimestamp: allTime[i].Timestamp, 156 endTimestamp: endTimestamp, 157 }) 158 } 159 } 160 161 return nil 162 } 163 164 func extendDaysSliceForYear(yearsMap map[yearType][]float32, untilTime time.Time) (daysToFetch int) { 165 year := untilTime.Year() 166 _, found := yearsMap[year] 167 if !found { 168 yearsMap[year] = make([]float32, untilTime.YearDay()) 169 return untilTime.YearDay() 170 } 171 172 // Just extend the slice if needed 173 missingDays := untilTime.YearDay() - len(yearsMap[year]) 174 yearsMap[year] = append(yearsMap[year], make([]float32, missingDays)...) 175 return missingDays 176 }