cloud_functions: use daily token prices to compute cumulative TVL

Use daily token prices to compute TVL instead of the prices fetched
at token transfer time, if available.

Added inactive tokenlist to exclude tokens that have essentially been
deactivated by CoinGecko (market data no longer available).
This commit is contained in:
Kevin Peters 2022-07-15 19:45:26 +00:00 committed by Evan Gray
parent 7402259fc7
commit c5a7d3e517
5 changed files with 188 additions and 18 deletions

View File

@ -343,11 +343,80 @@ func fetchTokenPrices(ctx context.Context, coinIds []string) map[string]float64
allPrices[coinId] = price allPrices[coinId] = price
} }
coinGeckoRateLimitSleep()
}
return allPrices
}
func coinGeckoRateLimitSleep() {
// CoinGecko rate limit is low (5/second), be very cautious about bursty requests // CoinGecko rate limit is low (5/second), be very cautious about bursty requests
time.Sleep(time.Millisecond * 200) time.Sleep(time.Millisecond * 200)
} }
return allPrices // fetchTokenPriceHistories returns the daily prices for coinIds from start to end
func fetchTokenPriceHistories(ctx context.Context, coinIds []string, start time.Time, end time.Time) map[string]map[string]float64 {
log.Printf("Fetching price history for %d tokens\n", len(coinIds))
priceHistories := map[string]map[string]float64{}
baseUrl := cgBaseUrl
cgApiKey := os.Getenv("COINGECKO_API_KEY")
if cgApiKey != "" {
baseUrl = cgProBaseUrl
}
startTimestamp := start.Unix()
endTimestamp := end.Unix()
for _, coinId := range coinIds {
defer coinGeckoRateLimitSleep()
url := fmt.Sprintf("%vcoins/%v/market_chart/range?vs_currency=usd&from=%v&to=%v", baseUrl, coinId, startTimestamp, endTimestamp)
req, reqErr := http.NewRequest("GET", url, nil)
if reqErr != nil {
log.Fatalf("failed coins request, err: %v\n", reqErr)
}
if cgApiKey != "" {
req.Header.Set("X-Cg-Pro-Api-Key", cgApiKey)
}
res, resErr := http.DefaultClient.Do(req)
if resErr != nil {
log.Fatalf("failed get coins response, err: %v\n", resErr)
}
defer res.Body.Close()
if res.StatusCode >= 400 {
errorMsg := fmt.Sprintf("failed to get CoinGecko price history for %s, Status: %s", coinId, res.Status)
if res.StatusCode == 404 {
log.Println(errorMsg)
continue
} else {
log.Fatalln(errorMsg)
}
}
body, bodyErr := ioutil.ReadAll(res.Body)
if bodyErr != nil {
log.Fatalf("failed decoding coins body, err: %v\n", bodyErr)
}
var parsed CoinGeckoMarketRes
parseErr := json.Unmarshal(body, &parsed)
if parseErr != nil {
log.Printf("fetchTokenPriceHistories failed parsing body. err %v\n", parseErr)
var errRes CoinGeckoErrorRes
if err := json.Unmarshal(body, &errRes); err == nil {
log.Println("Failed calling CoinGecko, got err", errRes.Error)
}
} else {
for _, market := range parsed.Prices {
seconds := int64(market[0]) / 1e3
date := time.Unix(seconds, 0).Format("2006-01-02")
price := market[1]
if _, ok := priceHistories[date]; !ok {
priceHistories[date] = map[string]float64{}
}
priceHistories[date][coinId] = price
}
}
}
return priceHistories
} }
const solanaTokenListURL = "https://raw.githubusercontent.com/solana-labs/token-list/main/src/tokens/solana.tokenlist.json" const solanaTokenListURL = "https://raw.githubusercontent.com/solana-labs/token-list/main/src/tokens/solana.tokenlist.json"

View File

@ -42,6 +42,7 @@ type TransferData struct {
Notional float64 Notional float64
TokenPrice float64 TokenPrice float64
TokenDecimals int TokenDecimals int
TransferTimestamp string
} }
// finds all the TokenTransfer rows within the specified period // finds all the TokenTransfer rows within the specified period
@ -83,6 +84,8 @@ func fetchTransferRowsInInterval(tbl *bigtable.Table, ctx context.Context, prefi
t.CoinGeckoCoinId = string(item.Value) t.CoinGeckoCoinId = string(item.Value)
case "TokenTransferDetails:Decimals": case "TokenTransferDetails:Decimals":
t.TokenDecimals, _ = strconv.Atoi(string(item.Value)) t.TokenDecimals, _ = strconv.Atoi(string(item.Value))
case "TokenTransferDetails:TransferTimestamp":
t.TransferTimestamp = string(item.Value)
} }
} }
@ -100,7 +103,8 @@ func fetchTransferRowsInInterval(tbl *bigtable.Table, ctx context.Context, prefi
keyParts := strings.Split(row.Key(), ":") keyParts := strings.Split(row.Key(), ":")
t.LeavingChain = keyParts[0] t.LeavingChain = keyParts[0]
if isTokenAllowed(t.OriginChain, t.TokenAddress) { transferDateStr := t.TransferTimestamp[0:10]
if isTokenAllowed(t.OriginChain, t.TokenAddress) && isTokenActive(t.OriginChain, t.TokenAddress, transferDateStr) {
rows = append(rows, *t) rows = append(rows, *t)
} }
} }
@ -116,8 +120,8 @@ func fetchTransferRowsInInterval(tbl *bigtable.Table, ctx context.Context, prefi
bigtable.StripValueFilter(), // no columns/values, just the row.Key() bigtable.StripValueFilter(), // no columns/values, just the row.Key()
), ),
bigtable.ChainFilters( bigtable.ChainFilters(
bigtable.FamilyFilter(fmt.Sprintf("%v|%v", columnFamilies[2], columnFamilies[5])), bigtable.FamilyFilter(fmt.Sprintf("%v|%v", transferPayloadFam, transferDetailsFam)),
bigtable.ColumnFilter("Amount|NotionalUSD|OriginSymbol|OriginName|OriginChain|TargetChain|CoinGeckoCoinId|OriginTokenAddress|TokenPriceUSD|Decimals"), bigtable.ColumnFilter("Amount|NotionalUSD|OriginSymbol|OriginName|OriginChain|TargetChain|CoinGeckoCoinId|OriginTokenAddress|TokenPriceUSD|Decimals|TransferTimestamp"),
bigtable.LatestNFilter(1), bigtable.LatestNFilter(1),
), ),
bigtable.BlockAllFilter(), bigtable.BlockAllFilter(),

View File

@ -27,12 +27,58 @@ var warmTvlCumulativeCacheFilePath = "tvl-cumulative-cache.json"
var notionalTvlCumulativeResultPath = "notional-tvl-cumulative.json" var notionalTvlCumulativeResultPath = "notional-tvl-cumulative.json"
var coinGeckoPriceCacheFilePath = "coingecko-price-cache.json"
var coinGeckoPriceCache = map[string]map[string]float64{}
var loadedCoinGeckoPriceCache bool
// days to be excluded from the TVL result // days to be excluded from the TVL result
var skipDays = map[string]bool{ var skipDays = map[string]bool{
// for example: // for example:
// "2022-02-19": true, // "2022-02-19": true,
} }
func loadAndUpdateCoinGeckoPriceCache(ctx context.Context, coinIds []string, now time.Time) {
// at cold-start, load the price cache into memory, and fetch any missing token price histories and add them to the cache
if !loadedCoinGeckoPriceCache {
// load the price cache
loadJsonToInterface(ctx, coinGeckoPriceCacheFilePath, &muWarmTvlCumulativeCache, &coinGeckoPriceCache)
loadedCoinGeckoPriceCache = true
// find tokens missing price history
missing := []string{}
for _, coinId := range coinIds {
found := false
for _, prices := range coinGeckoPriceCache {
if _, ok := prices[coinId]; ok {
found = true
break
}
}
if !found {
missing = append(missing, coinId)
}
}
// fetch missing price histories and add them to the cache
priceHistories := fetchTokenPriceHistories(ctx, missing, releaseDay, now)
for date, prices := range priceHistories {
for coinId, price := range prices {
if _, ok := coinGeckoPriceCache[date]; !ok {
coinGeckoPriceCache[date] = map[string]float64{}
}
coinGeckoPriceCache[date][coinId] = price
}
}
}
// fetch today's latest prices
today := now.Format("2006-01-02")
coinGeckoPriceCache[today] = fetchTokenPrices(ctx, coinIds)
// write to the cache file
persistInterfaceToJson(ctx, coinGeckoPriceCacheFilePath, &muWarmCumulativeAddressesCache, coinGeckoPriceCache)
}
// calculates a running total of notional value transferred, by symbol, since the start time specified. // 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 { func createTvlCumulativeOfInterval(tbl *bigtable.Table, ctx context.Context, start time.Time) map[string]map[string]map[string]LockedAsset {
if len(warmTvlCumulativeCache) == 0 { if len(warmTvlCumulativeCache) == 0 {
@ -238,6 +284,22 @@ func ComputeTvlCumulative(w http.ResponseWriter, r *http.Request) {
transfers := createTvlCumulativeOfInterval(tbl, ctx, start) transfers := createTvlCumulativeOfInterval(tbl, ctx, start)
coinIdSet := map[string]bool{}
for _, chains := range transfers {
for _, assets := range chains {
for _, asset := range assets {
if asset.CoinGeckoId != "*" {
coinIdSet[asset.CoinGeckoId] = true
}
}
}
}
coinIds := []string{}
for coinId := range coinIdSet {
coinIds = append(coinIds, coinId)
}
loadAndUpdateCoinGeckoPriceCache(ctx, coinIds, now)
// calculate the notional tvl based on the price of the tokens each day // calculate the notional tvl based on the price of the tokens each day
for date, chains := range transfers { for date, chains := range transfers {
if _, ok := skipDays[date]; ok { if _, ok := skipDays[date]; ok {
@ -265,7 +327,16 @@ func ComputeTvlCumulative(w http.ResponseWriter, r *http.Request) {
continue continue
} }
notional := asset.Amount * asset.TokenPrice // asset.TokenPrice is the price that was fetched when this token was last transferred, possibly before this date
// prefer to use the cached price for this date if it's available, because it might be newer
tokenPrice := asset.TokenPrice
if prices, ok := coinGeckoPriceCache[date]; ok {
if price, ok := prices[asset.CoinGeckoId]; ok {
// use the cached price
tokenPrice = price
}
}
notional := asset.Amount * tokenPrice
if notional <= 0 { if notional <= 0 {
continue continue
} }

View File

@ -306,6 +306,9 @@ func ComputeTVL(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel() defer cancel()
now := time.Now().UTC()
todaysDateStr := now.Format("2006-01-02")
getNotionalAmounts := func(ctx context.Context, tokensLocked map[string]map[string]LockedAsset) map[string]map[string]LockedAsset { getNotionalAmounts := func(ctx context.Context, tokensLocked map[string]map[string]LockedAsset) map[string]map[string]LockedAsset {
// create a map of all the coinIds // create a map of all the coinIds
seenCoinIds := map[string]bool{} seenCoinIds := map[string]bool{}
@ -340,6 +343,9 @@ func ComputeTVL(w http.ResponseWriter, r *http.Request) {
Address: "*", Address: "*",
} }
for address, lockedAsset := range tokens { for address, lockedAsset := range tokens {
if !isTokenActive(chain, address, todaysDateStr) {
continue
}
coinId := lockedAsset.CoinGeckoId coinId := lockedAsset.CoinGeckoId
amount := lockedAsset.Amount amount := lockedAsset.Amount
@ -391,7 +397,6 @@ func ComputeTVL(w http.ResponseWriter, r *http.Request) {
wg.Add(1) wg.Add(1)
go func() { go func() {
last24HourInterval := -time.Duration(24) * time.Hour last24HourInterval := -time.Duration(24) * time.Hour
now := time.Now().UTC()
start := now.Add(last24HourInterval) start := now.Add(last24HourInterval)
defer wg.Done() defer wg.Done()
transfers := tvlForInterval(tbl, ctx, start, now) transfers := tvlForInterval(tbl, ctx, start, now)

View File

@ -9,6 +9,7 @@ import (
"log" "log"
"math" "math"
"os" "os"
"strconv"
"strings" "strings"
"sync" "sync"
"time" "time"
@ -264,6 +265,10 @@ func chainIdStringToType(chainId string) vaa.ChainID {
return vaa.ChainIDUnset return vaa.ChainIDUnset
} }
func chainIDToNumberString(c vaa.ChainID) string {
return strconv.FormatUint(uint64(c), 10)
}
func makeSummary(row bigtable.Row) *Summary { func makeSummary(row bigtable.Row) *Summary {
summary := &Summary{} summary := &Summary{}
if _, ok := row[messagePubFam]; ok { if _, ok := row[messagePubFam]; ok {
@ -460,3 +465,19 @@ func isTokenAllowed(chainId string, tokenAddress string) bool {
} }
return false return false
} }
// tokens with no trading activity recorded by exchanges integrated on CoinGecko since the specified date
var inactiveTokens = map[string]map[string]string{
chainIDToNumberString(vaa.ChainIDEthereum): {
"0x707f9118e33a9b8998bea41dd0d46f38bb963fc8": "2022-06-15", // Anchor bETH token
},
}
func isTokenActive(chainId string, tokenAddress string, date string) bool {
if deactivatedDates, ok := inactiveTokens[chainId]; ok {
if deactivatedDate, ok := deactivatedDates[tokenAddress]; ok {
return date < deactivatedDate
}
}
return true
}