414 lines
13 KiB
Go
414 lines
13 KiB
Go
// Package p contains an HTTP Cloud Function.
|
|
package p
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"log"
|
|
"net/http"
|
|
|
|
"sort"
|
|
"strconv"
|
|
"sync"
|
|
"time"
|
|
|
|
"cloud.google.com/go/bigtable"
|
|
)
|
|
|
|
type tvlCumulativeResult struct {
|
|
DailyLocked map[string]map[string]map[string]LockedAsset
|
|
}
|
|
|
|
// an in-memory cache of previously calculated results
|
|
var warmTvlCumulativeCache = map[string]map[string]map[string]LockedAsset{}
|
|
var muWarmTvlCumulativeCache sync.RWMutex
|
|
var warmTvlCumulativeCacheFilePath = "tvl-cumulative-cache.json"
|
|
|
|
var notionalTvlCumulativeResultPath = "notional-tvl-cumulative.json"
|
|
|
|
// days to be excluded from the TVL result
|
|
var skipDays = map[string]bool{
|
|
// for example:
|
|
// "2022-02-19": true,
|
|
}
|
|
|
|
// calculates a running total of notional value transferred, by symbol, since the start time specified.
|
|
func createTvlCumulativeOfInterval(tbl *bigtable.Table, ctx context.Context, start time.Time) map[string]map[string]map[string]LockedAsset {
|
|
if len(warmTvlCumulativeCache) == 0 {
|
|
loadJsonToInterface(ctx, warmTvlCumulativeCacheFilePath, &muWarmTvlCumulativeCache, &warmTvlCumulativeCache)
|
|
}
|
|
|
|
now := time.Now().UTC()
|
|
today := now.Format("2006-01-02")
|
|
|
|
cacheNeedsUpdate := false
|
|
muWarmTvlCumulativeCache.Lock()
|
|
if len(warmTvlCumulativeCache) == 0 {
|
|
warmTvlCumulativeCache = map[string]map[string]map[string]LockedAsset{}
|
|
}
|
|
muWarmTvlCumulativeCache.Unlock()
|
|
|
|
results := map[string]map[string]map[string]LockedAsset{}
|
|
|
|
// fetch the amounts of transfers by symbol, for each day since launch (releaseDay)
|
|
dailyAmounts := tvlInInterval(tbl, ctx, releaseDay)
|
|
|
|
// create a slice of dates, order oldest first
|
|
dateKeys := make([]string, 0, len(dailyAmounts))
|
|
for k := range dailyAmounts {
|
|
dateKeys = append(dateKeys, k)
|
|
}
|
|
sort.Strings(dateKeys)
|
|
|
|
// iterate through the dates in the result set, and accumulate the amounts
|
|
// of each token transfer by symbol, based on the destination of the transfer.
|
|
for i, date := range dateKeys {
|
|
results[date] = map[string]map[string]LockedAsset{"*": {"*": LockedAsset{}}}
|
|
muWarmTvlCumulativeCache.RLock()
|
|
if dateCache, ok := warmTvlCumulativeCache[date]; ok && useCache(date) && dateCache != nil {
|
|
// have a cached value for this day, use it.
|
|
// iterate through cache and copy values to the result
|
|
for chain, tokens := range dateCache {
|
|
results[date][chain] = map[string]LockedAsset{}
|
|
for token, lockedAsset := range tokens {
|
|
results[date][chain][token] = LockedAsset{
|
|
Symbol: lockedAsset.Symbol,
|
|
Name: lockedAsset.Name,
|
|
Address: lockedAsset.Address,
|
|
CoinGeckoId: lockedAsset.CoinGeckoId,
|
|
TokenPrice: lockedAsset.TokenPrice,
|
|
Amount: lockedAsset.Amount,
|
|
Notional: lockedAsset.Notional,
|
|
}
|
|
}
|
|
}
|
|
muWarmTvlCumulativeCache.RUnlock()
|
|
} else {
|
|
// no cached value for this day, must calculate it
|
|
muWarmTvlCumulativeCache.RUnlock()
|
|
if i == 0 {
|
|
// special case for first day, no need to sum.
|
|
for chain, tokens := range dailyAmounts[date] {
|
|
results[date][chain] = map[string]LockedAsset{}
|
|
for token, lockedAsset := range tokens {
|
|
results[date][chain][token] = LockedAsset{
|
|
Symbol: lockedAsset.Symbol,
|
|
Name: lockedAsset.Name,
|
|
Address: lockedAsset.Address,
|
|
CoinGeckoId: lockedAsset.CoinGeckoId,
|
|
TokenPrice: lockedAsset.TokenPrice,
|
|
Amount: lockedAsset.Amount,
|
|
Notional: lockedAsset.Notional,
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
// find the string of the previous day
|
|
prevDate := dateKeys[i-1]
|
|
prevDayAmounts := results[prevDate]
|
|
thisDayAmounts := dailyAmounts[date]
|
|
// iterate through all the transfers and add the previous day's amount, if it exists
|
|
for chain, thisDaySymbols := range thisDayAmounts {
|
|
// create a union of the symbols from this day, and previous days
|
|
symbolsUnion := map[string]string{}
|
|
for symbol := range prevDayAmounts[chain] {
|
|
symbolsUnion[symbol] = symbol
|
|
}
|
|
for symbol := range thisDaySymbols {
|
|
symbolsUnion[symbol] = symbol
|
|
}
|
|
// initalize the chain/symbol map for this date
|
|
if _, ok := results[date][chain]; !ok {
|
|
results[date][chain] = map[string]LockedAsset{}
|
|
}
|
|
// iterate through the union of symbols, creating an amount for each one,
|
|
// and adding it the the results.
|
|
for symbol := range symbolsUnion {
|
|
|
|
asset := LockedAsset{}
|
|
|
|
prevDayAmount := float64(0)
|
|
if lockedAsset, ok := results[prevDate][chain][symbol]; ok {
|
|
prevDayAmount = lockedAsset.Amount
|
|
asset = lockedAsset
|
|
}
|
|
|
|
thisDayAmount := float64(0)
|
|
if lockedAsset, ok := thisDaySymbols[symbol]; ok {
|
|
thisDayAmount = lockedAsset.Amount
|
|
// use today's locked asset, rather than prevDay's, for freshest price.
|
|
asset = lockedAsset
|
|
}
|
|
cumulativeAmount := prevDayAmount + thisDayAmount
|
|
|
|
results[date][chain][symbol] = LockedAsset{
|
|
Symbol: asset.Symbol,
|
|
Name: asset.Name,
|
|
Address: asset.Address,
|
|
CoinGeckoId: asset.CoinGeckoId,
|
|
TokenPrice: asset.TokenPrice,
|
|
Amount: cumulativeAmount,
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// don't cache today
|
|
if date != today {
|
|
// set the result in the cache
|
|
muWarmTvlCumulativeCache.Lock()
|
|
if _, ok := warmTvlCumulativeCache[date]; !ok || !useCache(date) {
|
|
// cache does not have this date, persist it for other instances.
|
|
warmTvlCumulativeCache[date] = map[string]map[string]LockedAsset{}
|
|
for chain, tokens := range results[date] {
|
|
warmTvlCumulativeCache[date][chain] = map[string]LockedAsset{}
|
|
for token, asset := range tokens {
|
|
warmTvlCumulativeCache[date][chain][token] = LockedAsset{
|
|
Symbol: asset.Symbol,
|
|
Name: asset.Name,
|
|
Address: asset.Address,
|
|
CoinGeckoId: asset.CoinGeckoId,
|
|
TokenPrice: asset.TokenPrice,
|
|
Amount: asset.Amount,
|
|
}
|
|
}
|
|
}
|
|
cacheNeedsUpdate = true
|
|
}
|
|
muWarmTvlCumulativeCache.Unlock()
|
|
|
|
}
|
|
}
|
|
}
|
|
|
|
if cacheNeedsUpdate {
|
|
persistInterfaceToJson(ctx, warmTvlCumulativeCacheFilePath, &muWarmTvlCumulativeCache, warmTvlCumulativeCache)
|
|
}
|
|
|
|
// take the most recent n days, rather than returning all days since launch
|
|
selectDays := map[string]map[string]map[string]LockedAsset{}
|
|
days := getDaysInRange(start, now)
|
|
for _, day := range days {
|
|
selectDays[day] = map[string]map[string]LockedAsset{}
|
|
for chain, tokens := range results[day] {
|
|
selectDays[day][chain] = map[string]LockedAsset{}
|
|
for symbol, asset := range tokens {
|
|
selectDays[day][chain][symbol] = LockedAsset{
|
|
Symbol: asset.Symbol,
|
|
Name: asset.Name,
|
|
Address: asset.Address,
|
|
CoinGeckoId: asset.CoinGeckoId,
|
|
TokenPrice: asset.TokenPrice,
|
|
Amount: asset.Amount,
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return selectDays
|
|
|
|
}
|
|
|
|
// calculates the cumulative value transferred each day since launch.
|
|
func ComputeTvlCumulative(w http.ResponseWriter, r *http.Request) {
|
|
// Set CORS headers for the preflight request
|
|
if r.Method == http.MethodOptions {
|
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
|
w.Header().Set("Access-Control-Allow-Methods", "POST")
|
|
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
|
|
w.Header().Set("Access-Control-Max-Age", "3600")
|
|
w.WriteHeader(http.StatusNoContent)
|
|
return
|
|
}
|
|
// Set CORS headers for the main request.
|
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
|
|
|
// days since launch day
|
|
queryDays := int(time.Now().UTC().Sub(releaseDay).Hours() / 24)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
|
defer cancel()
|
|
|
|
dailyTvl := map[string]map[string]map[string]LockedAsset{}
|
|
|
|
hours := (24 * queryDays)
|
|
periodInterval := -time.Duration(hours) * time.Hour
|
|
now := time.Now().UTC()
|
|
prev := now.Add(periodInterval)
|
|
start := time.Date(prev.Year(), prev.Month(), prev.Day(), 0, 0, 0, 0, prev.Location())
|
|
|
|
transfers := createTvlCumulativeOfInterval(tbl, ctx, start)
|
|
|
|
// calculate the notional tvl based on the price of the tokens each day
|
|
for date, chains := range transfers {
|
|
if _, ok := skipDays[date]; ok {
|
|
log.Println("skipping ", date)
|
|
continue
|
|
}
|
|
dailyTvl[date] = map[string]map[string]LockedAsset{}
|
|
dailyTvl[date]["*"] = map[string]LockedAsset{}
|
|
dailyTvl[date]["*"]["*"] = LockedAsset{
|
|
Symbol: "*",
|
|
Notional: 0,
|
|
}
|
|
for chain, tokens := range chains {
|
|
if chain == "*" {
|
|
continue
|
|
}
|
|
dailyTvl[date][chain] = map[string]LockedAsset{}
|
|
dailyTvl[date][chain]["*"] = LockedAsset{
|
|
Symbol: "*",
|
|
Notional: 0,
|
|
}
|
|
|
|
for symbol, asset := range tokens {
|
|
if symbol == "*" {
|
|
continue
|
|
}
|
|
if _, ok := tokensToSkip[symbol]; ok {
|
|
log.Printf("going to skip %v, on chain %v, date %v", asset.Symbol, chain, date)
|
|
continue
|
|
}
|
|
|
|
notional := asset.Amount * asset.TokenPrice
|
|
if notional <= 0 {
|
|
continue
|
|
}
|
|
|
|
asset.Notional = roundToTwoDecimalPlaces(notional)
|
|
|
|
// Note: disable individual symbols to reduce response size for now
|
|
//// create a new LockAsset in order to exclude TokenPrice and Amount
|
|
//dailyTvl[date][chain][symbol] = LockedAsset{
|
|
// Symbol: asset.Symbol,
|
|
// Address: asset.Address,
|
|
// CoinGeckoId: asset.CoinGeckoId,
|
|
// Notional: asset.Notional,
|
|
//}
|
|
|
|
// add this asset's notional to the date/chain/*
|
|
if allAssets, ok := dailyTvl[date][chain]["*"]; ok {
|
|
allAssets.Notional += notional
|
|
dailyTvl[date][chain]["*"] = allAssets
|
|
}
|
|
} // end symbols iteration
|
|
|
|
// add chain total to the daily total
|
|
if allAssets, ok := dailyTvl[date]["*"]["*"]; ok {
|
|
allAssets.Notional += dailyTvl[date][chain]["*"].Notional
|
|
dailyTvl[date]["*"]["*"] = allAssets
|
|
}
|
|
|
|
// round the day's chain total
|
|
if allAssets, ok := dailyTvl[date][chain]["*"]; ok {
|
|
allAssets.Notional = roundToTwoDecimalPlaces(allAssets.Notional)
|
|
dailyTvl[date][chain]["*"] = allAssets
|
|
}
|
|
} // end chains iteration
|
|
|
|
// round the daily total
|
|
if allAssets, ok := dailyTvl[date]["*"]["*"]; ok {
|
|
allAssets.Notional = roundToTwoDecimalPlaces(allAssets.Notional)
|
|
dailyTvl[date]["*"]["*"] = allAssets
|
|
}
|
|
}
|
|
|
|
result := &tvlCumulativeResult{
|
|
DailyLocked: dailyTvl,
|
|
}
|
|
|
|
persistInterfaceToJson(ctx, notionalTvlCumulativeResultPath, &muWarmTvlCumulativeCache, result)
|
|
|
|
// Note: GCP HTTP Load Balancer returns 502 error if response data is large,
|
|
// just return success if there wasn't an error
|
|
// jsonBytes, err := json.Marshal(result)
|
|
|
|
//if err != nil {
|
|
// w.WriteHeader(http.StatusInternalServerError)
|
|
// w.Write([]byte(err.Error()))
|
|
// log.Println(err.Error())
|
|
// return
|
|
//}
|
|
w.WriteHeader(http.StatusOK)
|
|
//w.Write(jsonBytes)
|
|
}
|
|
|
|
func TvlCumulative(w http.ResponseWriter, r *http.Request) {
|
|
// Set CORS headers for the preflight request
|
|
if r.Method == http.MethodOptions {
|
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
|
w.Header().Set("Access-Control-Allow-Methods", "POST")
|
|
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
|
|
w.Header().Set("Access-Control-Max-Age", "3600")
|
|
w.WriteHeader(http.StatusNoContent)
|
|
return
|
|
}
|
|
// Set CORS headers for the main request.
|
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
|
|
|
var numDays string
|
|
var totalsOnly string
|
|
switch r.Method {
|
|
case http.MethodGet:
|
|
queryParams := r.URL.Query()
|
|
numDays = queryParams.Get("numDays")
|
|
totalsOnly = queryParams.Get("totalsOnly")
|
|
}
|
|
|
|
var queryDays int
|
|
if numDays == "" {
|
|
// days since launch day
|
|
queryDays = int(time.Now().UTC().Sub(releaseDay).Hours() / 24)
|
|
} else {
|
|
var convErr error
|
|
queryDays, convErr = strconv.Atoi(numDays)
|
|
if convErr != nil {
|
|
fmt.Fprint(w, "numDays must be an integer")
|
|
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
|
return
|
|
}
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
|
defer cancel()
|
|
|
|
hours := (24 * queryDays)
|
|
periodInterval := -time.Duration(hours) * time.Hour
|
|
now := time.Now().UTC()
|
|
prev := now.Add(periodInterval)
|
|
start := time.Date(prev.Year(), prev.Month(), prev.Day(), 0, 0, 0, 0, prev.Location())
|
|
startStr := start.Format("2006-01-02")
|
|
|
|
var cachedResult tvlCumulativeResult
|
|
loadJsonToInterface(ctx, notionalTvlCumulativeResultPath, &muWarmTvlCumulativeCache, &cachedResult)
|
|
|
|
dailyLocked := map[string]map[string]map[string]LockedAsset{}
|
|
for date, chains := range cachedResult.DailyLocked {
|
|
if date >= startStr {
|
|
if totalsOnly == "" {
|
|
dailyLocked[date] = chains
|
|
} else {
|
|
dailyLocked[date] = map[string]map[string]LockedAsset{}
|
|
for chain, addresses := range chains {
|
|
dailyLocked[date][chain] = map[string]LockedAsset{}
|
|
dailyLocked[date][chain]["*"] = addresses["*"]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
result := &tvlCumulativeResult{
|
|
DailyLocked: dailyLocked,
|
|
}
|
|
|
|
jsonBytes, err := json.Marshal(result)
|
|
|
|
if err != nil {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
w.Write([]byte(err.Error()))
|
|
log.Println(err.Error())
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write(jsonBytes)
|
|
}
|