Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add coin gecko pro API usage for the governor #4025

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
8 changes: 7 additions & 1 deletion node/cmd/guardiand/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,7 @@ var (

chainGovernorEnabled *bool
governorFlowCancelEnabled *bool
coinGeckoApiKey *string

ccqEnabled *bool
ccqAllowedRequesters *string
Expand Down Expand Up @@ -439,6 +440,7 @@ func init() {

chainGovernorEnabled = NodeCmd.Flags().Bool("chainGovernorEnabled", false, "Run the chain governor")
governorFlowCancelEnabled = NodeCmd.Flags().Bool("governorFlowCancelEnabled", false, "Enable flow cancel on the governor")
coinGeckoApiKey = NodeCmd.Flags().String("coinGeckoApiKey", "", "CoinGecko Pro API key. If no API key is provided, CoinGecko requests may be throttled or blocked.")

ccqEnabled = NodeCmd.Flags().Bool("ccqEnabled", false, "Enable cross chain query support")
ccqAllowedRequesters = NodeCmd.Flags().String("ccqAllowedRequesters", "", "Comma separated list of signers allowed to submit cross chain queries")
Expand Down Expand Up @@ -828,6 +830,10 @@ func runNode(cmd *cobra.Command, args []string) {
logger.Fatal("Either --gatewayContract, --gatewayWS and --gatewayLCD must all be set or all unset")
}

if !*chainGovernorEnabled && *coinGeckoApiKey != "" {
logger.Fatal("If coinGeckoApiKey is set, then chainGovernorEnabled must be set")
}

var publicRpcLogDetail common.GrpcLogDetail
switch *publicRpcLogDetailStr {
case "none":
Expand Down Expand Up @@ -1583,7 +1589,7 @@ func runNode(cmd *cobra.Command, args []string) {
node.GuardianOptionDatabase(db),
node.GuardianOptionWatchers(watcherConfigs, ibcWatcherConfig),
node.GuardianOptionAccountant(*accountantWS, *accountantContract, *accountantCheckEnabled, accountantWormchainConn, *accountantNttContract, accountantNttWormchainConn),
node.GuardianOptionGovernor(*chainGovernorEnabled, *governorFlowCancelEnabled),
node.GuardianOptionGovernor(*chainGovernorEnabled, *governorFlowCancelEnabled, *coinGeckoApiKey),
node.GuardianOptionGatewayRelayer(*gatewayRelayerContract, gatewayRelayerWormchainConn),
node.GuardianOptionQueryHandler(*ccqEnabled, *ccqAllowedRequesters),
node.GuardianOptionAdminService(*adminSocketPath, ethRPC, ethContract, rpcMap),
Expand Down
2 changes: 1 addition & 1 deletion node/pkg/adminrpc/adminserver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,7 @@ func Test_adminCommands(t *testing.T) {
}

func newNodePrivilegedServiceForGovernorTests() *nodePrivilegedService {
gov := governor.NewChainGovernor(zap.NewNop(), &db.MockGovernorDB{}, wh_common.GoTest, false)
gov := governor.NewChainGovernor(zap.NewNop(), &db.MockGovernorDB{}, wh_common.GoTest, false, "")

return &nodePrivilegedService{
db: nil,
Expand Down
3 changes: 3 additions & 0 deletions node/pkg/governor/governor.go
Original file line number Diff line number Diff line change
Expand Up @@ -201,13 +201,15 @@ type ChainGovernor struct {
statusPublishCounter int64
configPublishCounter int64
flowCancelEnabled bool
coinGeckoApiKey string
}

func NewChainGovernor(
logger *zap.Logger,
db db.GovernorDB,
env common.Environment,
flowCancelEnabled bool,
coinGeckoApiKey string,
) *ChainGovernor {
return &ChainGovernor{
db: db,
Expand All @@ -218,6 +220,7 @@ func NewChainGovernor(
msgsSeen: make(map[string]bool),
env: env,
flowCancelEnabled: flowCancelEnabled,
coinGeckoApiKey: coinGeckoApiKey,
}
}

Expand Down
2 changes: 1 addition & 1 deletion node/pkg/governor/governor_monitoring_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (

func TestIsVAAEnqueuedNilMessageID(t *testing.T) {
logger, _ := zap.NewProduction()
gov := NewChainGovernor(logger, nil, common.GoTest, true)
gov := NewChainGovernor(logger, nil, common.GoTest, true, "")
enqueued, err := gov.IsVAAEnqueued(nil)
require.EqualError(t, err, "no message ID specified")
assert.Equal(t, false, enqueued)
Expand Down
26 changes: 18 additions & 8 deletions node/pkg/governor/governor_prices.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ func (gov *ChainGovernor) initCoinGecko(ctx context.Context, run bool) error {
}

// Create the set of queries, breaking the IDs into the appropriate size chunks.
gov.coinGeckoQueries = createCoinGeckoQueries(ids, tokensPerCoinGeckoQuery)
gov.coinGeckoQueries = createCoinGeckoQueries(ids, tokensPerCoinGeckoQuery, gov.coinGeckoApiKey)
for queryIdx, query := range gov.coinGeckoQueries {
gov.logger.Info("coingecko query: ", zap.Int("queryIdx", queryIdx), zap.String("query", query))
}
Expand All @@ -64,15 +64,15 @@ func (gov *ChainGovernor) initCoinGecko(ctx context.Context, run bool) error {
}

// createCoinGeckoQueries creates the set of CoinGecko queries, breaking the set of IDs into the appropriate size chunks.
func createCoinGeckoQueries(idList []string, tokensPerQuery int) []string {
func createCoinGeckoQueries(idList []string, tokensPerQuery int, coinGeckoApiKey string) []string {
var queries []string
queryIdx := 0
tokenIdx := 0
ids := ""
first := true
for _, coinGeckoId := range idList {
if tokenIdx%tokensPerQuery == 0 && tokenIdx != 0 {
queries = append(queries, createCoinGeckoQuery(ids))
queries = append(queries, createCoinGeckoQuery(ids, coinGeckoApiKey))
ids = ""
first = true
queryIdx += 1
Expand All @@ -88,19 +88,29 @@ func createCoinGeckoQueries(idList []string, tokensPerQuery int) []string {
}

if ids != "" {
queries = append(queries, createCoinGeckoQuery(ids))
queries = append(queries, createCoinGeckoQuery(ids, coinGeckoApiKey))
}

return queries
}

// createCoinGeckoQuery creates a CoinGecko query for the specified set of IDs.
func createCoinGeckoQuery(ids string) string {
func createCoinGeckoQuery(ids string, coinGeckoApiKey string) string {
params := url.Values{}
params.Add("ids", ids)
params.Add("vs_currencies", "usd")

query := "https://api.coingecko.com/api/v3/simple/price?" + params.Encode()
// If modifying this code, ensure that the test 'TestCoinGeckoPriceChecks' passes when adding a pro API key to it.
// Since the code requires an API key (which we don't want to publish to git), this
// part of the test is normally skipped but mods to sensitive places should still be checked
query := ""
if coinGeckoApiKey == "" {
query = "https://api.coingecko.com/api/v3/simple/price?" + params.Encode()
} else { // Pro version API key path
params.Add("x_cg_pro_api_key", coinGeckoApiKey)
query = "https://pro-api.coingecko.com/api/v3/simple/price?" + params.Encode()
}

return query
}

Expand Down Expand Up @@ -160,7 +170,7 @@ func (gov *ChainGovernor) queryCoinGecko(ctx context.Context) error {
query := query + "&" + params.Encode()
thisResult, err := gov.queryCoinGeckoChunk(query)
if err != nil {
gov.logger.Error("CoinGecko query failed", zap.Int("queryIdx", queryIdx), zap.String("query", query), zap.Error(err))
gov.logger.Error("CoinGecko query failed", zap.Error(err), zap.Int("queryIdx", queryIdx), zap.String("query", query))
gov.revertAllPrices()
return err
}
Expand Down Expand Up @@ -309,7 +319,7 @@ func CheckQuery(logger *zap.Logger) error {
logger.Info("Instantiating governor.")
ctx := context.Background()
var db db.MockGovernorDB
gov := NewChainGovernor(logger, &db, common.MainNet, true)
gov := NewChainGovernor(logger, &db, common.MainNet, true, "")

if err := gov.initConfig(); err != nil {
return err
Expand Down
56 changes: 50 additions & 6 deletions node/pkg/governor/governor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,7 @@ func TestFlowCancelFeatureFlag(t *testing.T) {

ctx := context.Background()
var db db.MockGovernorDB
gov := NewChainGovernor(zap.NewNop(), &db, common.GoTest, true)
gov := NewChainGovernor(zap.NewNop(), &db, common.GoTest, true, "")

// Trigger the evaluation of the flow cancelling config
err := gov.Run(ctx)
Expand All @@ -322,7 +322,7 @@ func TestFlowCancelFeatureFlag(t *testing.T) {
assert.NotZero(t, numFlowCancelling)

// Disable flow cancelling
gov = NewChainGovernor(zap.NewNop(), &db, common.GoTest, false)
gov = NewChainGovernor(zap.NewNop(), &db, common.GoTest, false, "")

// Trigger the evaluation of the flow cancelling config
err = gov.Run(ctx)
Expand Down Expand Up @@ -666,7 +666,7 @@ func newChainGovernorForTestWithLogger(ctx context.Context, logger *zap.Logger)
}

var db db.MockGovernorDB
gov := NewChainGovernor(logger, &db, common.GoTest, true)
gov := NewChainGovernor(logger, &db, common.GoTest, true, "")

err := gov.Run(ctx)
if err != nil {
Expand Down Expand Up @@ -2183,7 +2183,7 @@ func TestSmallerPendingTransfersAfterBigOneShouldGetReleased(t *testing.T) {
func TestMainnetConfigIsValid(t *testing.T) {
logger := zap.NewNop()
var db db.MockGovernorDB
gov := NewChainGovernor(logger, &db, common.GoTest, true)
gov := NewChainGovernor(logger, &db, common.GoTest, true, "")

gov.env = common.TestNet
err := gov.initConfig()
Expand All @@ -2193,7 +2193,7 @@ func TestMainnetConfigIsValid(t *testing.T) {
func TestTestnetConfigIsValid(t *testing.T) {
logger := zap.NewNop()
var db db.MockGovernorDB
gov := NewChainGovernor(logger, &db, common.GoTest, true)
gov := NewChainGovernor(logger, &db, common.GoTest, true, "")

gov.env = common.TestNet
err := gov.initConfig()
Expand Down Expand Up @@ -3187,7 +3187,7 @@ func TestCoinGeckoQueries(t *testing.T) {
ids[idx] = fmt.Sprintf("id%d", idx)
}

queries := createCoinGeckoQueries(ids, tc.chunkSize)
queries := createCoinGeckoQueries(ids, tc.chunkSize, "")
require.Equal(t, tc.expectedQueries, len(queries))

results := make(map[string]string)
Expand Down Expand Up @@ -3216,6 +3216,50 @@ func TestCoinGeckoQueries(t *testing.T) {
}
}

// Test the URL of CoinGecko queries to be correct
func TestCoinGeckoQueryFormat(t *testing.T) {
id_amount := 10
ids := make([]string, id_amount)
for idx := 0; idx < id_amount; idx++ {
ids[idx] = fmt.Sprintf("id%d", idx)
}

// Create and parse the query
queries := createCoinGeckoQueries(ids, 100, "") // No API key
require.Equal(t, len(queries), 1)
query_url, err := url.Parse(queries[0])
require.Equal(t, err, nil)
params, err := url.ParseQuery(query_url.RawQuery)
require.Equal(t, err, nil)

// Test the portions of the URL for the non-pro version of the API
require.Equal(t, query_url.Scheme, "https")
require.Equal(t, query_url.Host, "api.coingecko.com")
require.Equal(t, query_url.Path, "/api/v3/simple/price")
require.Equal(t, params.Has("x_cg_pro_api_key"), false)
require.Equal(t, params.Has("vs_currencies"), true)
require.Equal(t, params["vs_currencies"][0], "usd")
require.Equal(t, params.Has("ids"), true)

// Create and parse the query with an API key
queries = createCoinGeckoQueries(ids, 100, "FAKE_KEY") // With API key
require.Equal(t, len(queries), 1)
query_url, err = url.Parse(queries[0])
require.Equal(t, err, nil)
params, err = url.ParseQuery(query_url.RawQuery)
require.Equal(t, err, nil)

// Test the portions of the URL actually provided
require.Equal(t, query_url.Scheme, "https")
require.Equal(t, query_url.Host, "pro-api.coingecko.com")
require.Equal(t, query_url.Path, "/api/v3/simple/price")
require.Equal(t, params.Has("x_cg_pro_api_key"), true)
require.Equal(t, params["x_cg_pro_api_key"][0], "FAKE_KEY")
require.Equal(t, params.Has("vs_currencies"), true)
require.Equal(t, params["vs_currencies"][0], "usd")
require.Equal(t, params.Has("ids"), true)
}

// setupLogsCapture is a helper function for making a zap logger/observer combination for testing that certain logs have been made
func setupLogsCapture(t testing.TB, options ...zap.Option) (*zap.Logger, *observer.ObservedLogs) {
t.Helper()
Expand Down
2 changes: 1 addition & 1 deletion node/pkg/node/adminServiceRunnable.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ func adminServiceRunnable(
contract := ethcommon.HexToAddress(*ethContract)
evmConnector, err = connectors.NewEthereumBaseConnector(ctx, "eth", *ethRpc, contract, logger)
if err != nil {
return nil, fmt.Errorf("failed to connecto to ethereum")
return nil, fmt.Errorf("failed to connect to ethereum")
}
}

Expand Down
2 changes: 1 addition & 1 deletion node/pkg/node/node_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ func mockGuardianRunnable(t testing.TB, gs []*mockGuardian, mockGuardianIndex ui
GuardianOptionDatabase(db),
GuardianOptionWatchers(watcherConfigs, nil),
GuardianOptionNoAccountant(), // disable accountant
GuardianOptionGovernor(true, false),
GuardianOptionGovernor(true, false, ""),
GuardianOptionGatewayRelayer("", nil), // disable gateway relayer
GuardianOptionP2P(gs[mockGuardianIndex].p2pKey, networkID, bootstrapPeers, nodeName, false, false, cfg.p2pPort, "", 0, "", "", func() string { return "" }),
GuardianOptionPublicRpcSocket(cfg.publicSocket, publicRpcLogDetail),
Expand Down
8 changes: 6 additions & 2 deletions node/pkg/node/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ func GuardianOptionAccountant(

// GuardianOptionGovernor enables or disables the governor.
// Dependencies: db
func GuardianOptionGovernor(governorEnabled bool, flowCancelEnabled bool) *GuardianOption {
func GuardianOptionGovernor(governorEnabled bool, flowCancelEnabled bool, coinGeckoApiKey string) *GuardianOption {
return &GuardianOption{
name: "governor",
dependencies: []string{"db"},
Expand All @@ -227,9 +227,13 @@ func GuardianOptionGovernor(governorEnabled bool, flowCancelEnabled bool) *Guard
if flowCancelEnabled {
logger.Info("chain governor is enabled with flow cancel enabled")
} else {

mdulin2 marked this conversation as resolved.
Show resolved Hide resolved
logger.Info("chain governor is enabled without flow cancel")
}
g.gov = governor.NewChainGovernor(logger, g.db, g.env, flowCancelEnabled)
if coinGeckoApiKey != "" {
logger.Info("coingecko pro API key in use")
}
g.gov = governor.NewChainGovernor(logger, g.db, g.env, flowCancelEnabled, coinGeckoApiKey)
} else {
logger.Info("chain governor is disabled")
}
Expand Down
2 changes: 1 addition & 1 deletion node/pkg/publicrpc/publicrpcserver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ func TestGetSignedVAABadAddress(t *testing.T) {
func TestGovernorIsVAAEnqueuedNoMessage(t *testing.T) {
ctx := context.Background()
logger, _ := zap.NewProduction()
gov := governor.NewChainGovernor(logger, nil, common.GoTest, false)
gov := governor.NewChainGovernor(logger, nil, common.GoTest, false, "")
server := &PublicrpcServer{logger: logger, gov: gov}

// A message without the messageId set should not panic but return an error instead.
Expand Down
Loading