status-go/services/wallet/history/exchange.go

177 lines
4.9 KiB
Go

package history
import (
"errors"
"sync"
"time"
"github.com/status-im/status-go/services/wallet/thirdparty"
)
type tokenType = string
type currencyType = string
type yearType = int
type allTimeEntry struct {
value float32
startTimestamp int64
endTimestamp int64
}
// Exchange caches conversion rates in memory on a daily basis
type Exchange struct {
// 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
cache map[tokenType]map[currencyType]map[yearType][]float32
// special case for all time information
allTimeCache map[tokenType]map[currencyType][]allTimeEntry
fetchMutex sync.Mutex
cryptoCompare *thirdparty.CryptoCompare
}
func NewExchange(cryptoCompare *thirdparty.CryptoCompare) *Exchange {
return &Exchange{
cache: make(map[tokenType]map[currencyType]map[yearType][]float32),
cryptoCompare: cryptoCompare,
}
}
// GetExchangeRate returns the exchange rate from token to currency in the day of the given date
// if none exists returns "missing <element>" error
func (e *Exchange) GetExchangeRateForDay(token tokenType, currency currencyType, date time.Time) (float32, error) {
e.fetchMutex.Lock()
defer e.fetchMutex.Unlock()
currencyMap, found := e.cache[token]
if !found {
return 0, errors.New("missing token")
}
yearsMap, found := currencyMap[currency]
if !found {
return 0, errors.New("missing currency")
}
year := date.Year()
valueForDays, found := yearsMap[year]
if !found {
// Search closest in all time
allCurrencyMap, found := e.allTimeCache[token]
if !found {
return 0, errors.New("missing token in all time data")
}
allYearsMap, found := allCurrencyMap[currency]
if !found {
return 0, errors.New("missing currency in all time data")
}
for _, entry := range allYearsMap {
if entry.startTimestamp <= date.Unix() && entry.endTimestamp > date.Unix() {
return entry.value, nil
}
}
return 0, errors.New("missing entry")
}
day := date.YearDay()
if day >= len(valueForDays) {
return 0, errors.New("missing day")
}
return valueForDays[day], nil
}
// fetchAndCacheRates fetches and in memory cache exchange rates for this and last year
func (e *Exchange) FetchAndCacheMissingRates(token tokenType, currency currencyType) error {
// Protect REST calls also to prevent fetching the same token/currency twice
e.fetchMutex.Lock()
defer e.fetchMutex.Unlock()
// Allocate missing values
currencyMap, found := e.cache[token]
if !found {
currencyMap = make(map[currencyType]map[yearType][]float32)
e.cache[token] = currencyMap
}
yearsMap, found := currencyMap[currency]
if !found {
yearsMap = make(map[yearType][]float32)
currencyMap[currency] = yearsMap
}
currentTime := time.Now().UTC()
endOfPrevYearTime := time.Date(currentTime.Year()-1, 12, 31, 23, 0, 0, 0, time.UTC)
daysToFetch := extendDaysSliceForYear(yearsMap, endOfPrevYearTime)
curYearTime := time.Date(currentTime.Year(), currentTime.Month(), currentTime.Day(), 0, 0, 0, 0, time.UTC)
daysToFetch += extendDaysSliceForYear(yearsMap, curYearTime)
if daysToFetch == 0 {
return nil
}
res, err := e.cryptoCompare.FetchDailyMarketValues(token, currency, daysToFetch, false, 1)
if err != nil {
return err
}
for i := 0; i < len(res); i++ {
t := time.Unix(res[i].Timestamp, 0).UTC()
yearDayIndex := t.YearDay() - 1
yearValues, found := yearsMap[t.Year()]
if found && yearDayIndex < len(yearValues) {
yearValues[yearDayIndex] = float32(res[i].Value)
}
}
// Fetch all time
allTime, err := e.cryptoCompare.FetchDailyMarketValues(token, currency, 1, true, 30)
if err != nil {
return err
}
if e.allTimeCache == nil {
e.allTimeCache = make(map[tokenType]map[currencyType][]allTimeEntry)
}
_, found = e.allTimeCache[token]
if !found {
e.allTimeCache[token] = make(map[currencyType][]allTimeEntry)
}
// No benefit to fetch intermendiate values, overwrite historical
e.allTimeCache[token][currency] = make([]allTimeEntry, 0)
for i := 0; i < len(allTime) && allTime[i].Timestamp < res[0].Timestamp; i++ {
if allTime[i].Value > 0 {
var endTimestamp int64
if i+1 < len(allTime) {
endTimestamp = allTime[i+1].Timestamp
} else {
endTimestamp = res[0].Timestamp
}
e.allTimeCache[token][currency] = append(e.allTimeCache[token][currency],
allTimeEntry{
value: float32(allTime[i].Value),
startTimestamp: allTime[i].Timestamp,
endTimestamp: endTimestamp,
})
}
}
return nil
}
func extendDaysSliceForYear(yearsMap map[yearType][]float32, untilTime time.Time) (daysToFetch int) {
year := untilTime.Year()
_, found := yearsMap[year]
if !found {
yearsMap[year] = make([]float32, untilTime.YearDay())
return untilTime.YearDay()
}
// Just extend the slice if needed
missingDays := untilTime.YearDay() - len(yearsMap[year])
yearsMap[year] = append(yearsMap[year], make([]float32, missingDays)...)
return missingDays
}