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:
parent
7402259fc7
commit
c5a7d3e517
|
@ -343,13 +343,82 @@ func fetchTokenPrices(ctx context.Context, coinIds []string) map[string]float64
|
||||||
allPrices[coinId] = price
|
allPrices[coinId] = price
|
||||||
}
|
}
|
||||||
|
|
||||||
// CoinGecko rate limit is low (5/second), be very cautious about bursty requests
|
coinGeckoRateLimitSleep()
|
||||||
time.Sleep(time.Millisecond * 200)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return allPrices
|
return allPrices
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func coinGeckoRateLimitSleep() {
|
||||||
|
// CoinGecko rate limit is low (5/second), be very cautious about bursty requests
|
||||||
|
time.Sleep(time.Millisecond * 200)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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"
|
||||||
|
|
||||||
type SolanaToken struct {
|
type SolanaToken struct {
|
||||||
|
|
|
@ -31,17 +31,18 @@ var muWarmTransfersToCache sync.RWMutex
|
||||||
var warmTransfersToCacheFilePath = "notional-transferred-to-cache.json"
|
var warmTransfersToCacheFilePath = "notional-transferred-to-cache.json"
|
||||||
|
|
||||||
type TransferData struct {
|
type TransferData struct {
|
||||||
TokenSymbol string
|
TokenSymbol string
|
||||||
TokenName string
|
TokenName string
|
||||||
TokenAddress string
|
TokenAddress string
|
||||||
TokenAmount float64
|
TokenAmount float64
|
||||||
CoinGeckoCoinId string
|
CoinGeckoCoinId string
|
||||||
OriginChain string
|
OriginChain string
|
||||||
LeavingChain string
|
LeavingChain string
|
||||||
DestinationChain string
|
DestinationChain string
|
||||||
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(),
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue