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  }