diff --git a/node/pkg/governor/governor.go b/node/pkg/governor/governor.go index 01941f521..01c2a0e70 100644 --- a/node/pkg/governor/governor.go +++ b/node/pkg/governor/governor.go @@ -105,7 +105,7 @@ type ChainGovernor struct { logger *zap.Logger mutex sync.Mutex tokens map[tokenKey]*tokenEntry - tokensByCoinGeckoId map[string]*tokenEntry + tokensByCoinGeckoId map[string][]*tokenEntry chains map[vaa.ChainID]*chainEntry msgsToPublish []*common.MessagePublication dayLengthInMinutes int @@ -122,7 +122,7 @@ func NewChainGovernor( db: db, logger: logger, tokens: make(map[tokenKey]*tokenEntry), - tokensByCoinGeckoId: make(map[string]*tokenEntry), + tokensByCoinGeckoId: make(map[string][]*tokenEntry), chains: make(map[vaa.ChainID]*chainEntry), env: env, } @@ -185,6 +185,17 @@ func (gov *ChainGovernor) initConfig() error { te := &tokenEntry{cfgPrice: cfgPrice, price: initialPrice, decimals: decimals, symbol: ct.symbol, coinGeckoId: ct.coinGeckoId, token: key} te.updatePrice() + gov.tokens[key] = te + + // Multiple tokens can share a CoinGecko price, so we keep an array of tokens per CoinGecko ID. + cge, cgExists := gov.tokensByCoinGeckoId[te.coinGeckoId] + if !cgExists { + gov.tokensByCoinGeckoId[te.coinGeckoId] = []*tokenEntry{te} + } else { + cge = append(cge, te) + gov.tokensByCoinGeckoId[te.coinGeckoId] = cge + } + gov.logger.Info("cgov: will monitor token:", zap.Stringer("chain", key.chain), zap.Stringer("addr", key.addr), zap.String("symbol", te.symbol), @@ -193,9 +204,6 @@ func (gov *ChainGovernor) initConfig() error { zap.Int64("decimals", dec), zap.Int64("origDecimals", ct.decimals), ) - - gov.tokens[key] = te - gov.tokensByCoinGeckoId[te.coinGeckoId] = te } if len(gov.tokens) == 0 { diff --git a/node/pkg/governor/governor_prices.go b/node/pkg/governor/governor_prices.go index 93a621cf3..e40970f06 100644 --- a/node/pkg/governor/governor_prices.go +++ b/node/pkg/governor/governor_prices.go @@ -109,13 +109,13 @@ func (gov *ChainGovernor) queryCoinGecko() { gov.mutex.Lock() defer gov.mutex.Unlock() - localTokenMap := make(map[string]*tokenEntry) - for coinGeckoId, te := range gov.tokensByCoinGeckoId { - localTokenMap[coinGeckoId] = te + localTokenMap := make(map[string][]*tokenEntry) + for coinGeckoId, cge := range gov.tokensByCoinGeckoId { + localTokenMap[coinGeckoId] = cge } for coinGeckoId, data := range result { - te, exists := gov.tokensByCoinGeckoId[coinGeckoId] + cge, exists := gov.tokensByCoinGeckoId[coinGeckoId] if exists { price, ok := data.(map[string]interface{})["usd"].(float64) if !ok { @@ -123,18 +123,21 @@ func (gov *ChainGovernor) queryCoinGecko() { // By continuing, we leave this one in the local map so the price will get reverted below. continue } - te.coinGeckoPrice = big.NewFloat(price) - te.updatePrice() - te.priceTime = now - gov.logger.Info("cgov: updated price", - zap.String("symbol", te.symbol), - zap.String("coinGeckoId", - te.coinGeckoId), - zap.Stringer("price", te.price), - zap.Stringer("cfgPrice", te.cfgPrice), - zap.Stringer("coinGeckoPrice", te.coinGeckoPrice), - ) + for _, te := range cge { + te.coinGeckoPrice = big.NewFloat(price) + te.updatePrice() + te.priceTime = now + + gov.logger.Info("cgov: updated price", + zap.String("symbol", te.symbol), + zap.String("coinGeckoId", + te.coinGeckoId), + zap.Stringer("price", te.price), + zap.Stringer("cfgPrice", te.cfgPrice), + zap.Stringer("coinGeckoPrice", te.coinGeckoPrice), + ) + } delete(localTokenMap, coinGeckoId) } else { @@ -143,8 +146,29 @@ func (gov *ChainGovernor) queryCoinGecko() { } if len(localTokenMap) != 0 { - for _, te := range localTokenMap { - gov.logger.Error("cgov: did not receive a CoinGecko response for symbol, reverting to configured price", + for _, lcge := range localTokenMap { + for _, te := range lcge { + gov.logger.Error("cgov: did not receive a CoinGecko response for symbol, reverting to configured price", + zap.String("symbol", te.symbol), + zap.String("coinGeckoId", + te.coinGeckoId), + zap.Stringer("cfgPrice", te.cfgPrice), + ) + + te.price = te.cfgPrice + // Don't update the timestamp so we'll know when we last received an update from CoinGecko. + } + } + } +} + +func (gov *ChainGovernor) revertAllPrices() { + gov.mutex.Lock() + defer gov.mutex.Unlock() + + for _, cge := range gov.tokensByCoinGeckoId { + for _, te := range cge { + gov.logger.Error("cgov: reverting to configured price", zap.String("symbol", te.symbol), zap.String("coinGeckoId", te.coinGeckoId), @@ -157,23 +181,6 @@ func (gov *ChainGovernor) queryCoinGecko() { } } -func (gov *ChainGovernor) revertAllPrices() { - gov.mutex.Lock() - defer gov.mutex.Unlock() - - for _, te := range gov.tokensByCoinGeckoId { - gov.logger.Error("cgov: reverting to configured price", - zap.String("symbol", te.symbol), - zap.String("coinGeckoId", - te.coinGeckoId), - zap.Stringer("cfgPrice", te.cfgPrice), - ) - - te.price = te.cfgPrice - // Don't update the timestamp so we'll know when we last received an update from CoinGecko. - } -} - // We should use the max(coinGeckoPrice, configuredPrice) as our price for computing notional value. func (te tokenEntry) updatePrice() { if (te.coinGeckoPrice == nil) || (te.coinGeckoPrice.Cmp(te.cfgPrice) < 0) { diff --git a/node/pkg/governor/governor_test.go b/node/pkg/governor/governor_test.go index 012897e58..fe3c4f9b3 100644 --- a/node/pkg/governor/governor_test.go +++ b/node/pkg/governor/governor_test.go @@ -78,7 +78,13 @@ func (gov *ChainGovernor) setTokenForTesting(tokenChainID vaa.ChainID, tokenAddr key := tokenKey{chain: vaa.ChainID(tokenChainID), addr: tokenAddr} te := &tokenEntry{cfgPrice: bigPrice, price: bigPrice, decimals: decimals, symbol: symbol, coinGeckoId: symbol, token: key} gov.tokens[key] = te - gov.tokensByCoinGeckoId[symbol] = te + cge, cgExists := gov.tokensByCoinGeckoId[te.coinGeckoId] + if !cgExists { + gov.tokensByCoinGeckoId[te.coinGeckoId] = []*tokenEntry{te} + } else { + cge = append(cge, te) + gov.tokensByCoinGeckoId[te.coinGeckoId] = cge + } return nil }