Skip to content

Conversation

@chatton
Copy link
Collaborator

@chatton chatton commented Nov 10, 2025

Overview

Adds a new balance monitor which monitors specified addresses and provides their balance as metrics.

closes #15

@chatton chatton requested a review from auricom November 10, 2025 15:28
@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @chatton, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request enhances the monitoring tool by integrating a balance checker for Celestia addresses. It allows users to track the native token holdings of specified accounts and provides visibility into the health and reliability of the consensus RPC endpoints used for these queries, all exposed via Prometheus metrics. This adds a critical layer of operational insight for blockchain infrastructure.

Highlights

  • New Balance Monitoring Feature: Introduced a new capability to monitor native token balances for specified Celestia addresses using consensus RPC.
  • Prometheus Metrics: Added new Prometheus metrics to track account balances (ev_metrics_account_balance), consensus RPC endpoint availability (ev_metrics_consensus_rpc_endpoint_availability), and errors (ev_metrics_consensus_rpc_endpoint_errors_total).
  • Automatic RPC Failover: Implemented logic for automatic failover across multiple consensus RPC endpoints when querying account balances, enhancing reliability.
  • New CLI Flags: Added new command-line flags (--balance.addresses, --balance.consensus-rpc-urls, --balance.scrape-interval) to configure and enable the balance monitoring.
  • Documentation Updates: The README.md has been updated to reflect the new balance monitoring feature, including usage examples and descriptions of the new metrics.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a balance monitoring feature, which is a great addition. The implementation includes a new Cosmos client, a balance exporter, and corresponding Prometheus metrics. My review focuses on improving security, efficiency, and consistency. I've identified a critical security issue with insecure gRPC connections, a high-severity performance issue due to inefficient connection handling, and a minor documentation inconsistency. The proposed changes will make the new feature more robust and secure.

Comment on lines +33 to +35
grpcEndpoint := stripScheme(endpoint)

conn, err := grpc.NewClient(grpcEndpoint, grpc.WithTransportCredentials(insecure.NewCredentials()))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

Using insecure.NewCredentials() establishes a non-TLS gRPC connection, which is a significant security risk when connecting to public RPC endpoints as shown in the README examples (https://...). This exposes traffic to potential eavesdropping and man-in-the-middle attacks. The client should dynamically use TLS when the endpoint URL has an https:// prefix.

Note: This change requires adding the google.golang.org/grpc/credentials import.

Suggested change
grpcEndpoint := stripScheme(endpoint)
conn, err := grpc.NewClient(grpcEndpoint, grpc.WithTransportCredentials(insecure.NewCredentials()))
useTLS := strings.HasPrefix(endpoint, "https://")
grpcEndpoint := stripScheme(endpoint)
var transportCreds credentials.TransportCredentials
if useTLS {
transportCreds = credentials.NewTLS(nil)
} else {
transportCreds = insecure.NewCredentials()
}
conn, err := grpc.NewClient(grpcEndpoint, grpc.WithTransportCredentials(transportCreds))

Comment on lines +59 to +61
for _, address := range e.addresses {
e.checkBalance(ctx, m, address)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

This loop calls checkBalance for each address, and checkBalance in turn loops through endpoints, creating a new gRPC connection each time. This is inefficient as it can result in many connections per scrape interval. It's better to fetch all balances using a single connection per available endpoint. I'm suggesting changing this to a single function call. Please see my other comment on the checkBalance function for the implementation of checkBalances.

				e.checkBalances(ctx, m)

Comment on lines +66 to +135
// checkBalance queries balance with fallback across endpoints
func (e *exporter) checkBalance(ctx context.Context, m *metrics.Metrics, address string) {
// try each endpoint until one succeeds
for _, endpoint := range e.endpoints {
// create client
client, err := cosmos.NewClient(endpoint, e.logger)
if err != nil {
e.logger.Warn().
Err(err).
Str("endpoint", endpoint).
Str("address", address).
Msg("failed to create client, trying next endpoint")
m.RecordConsensusRpcEndpointAvailability(e.chainID, endpoint, false)
m.RecordConsensusRpcEndpointError(e.chainID, endpoint, utils.CategorizeError(err))
continue
}

// query balance
balances, err := client.GetBalance(ctx, address)

// close client
_ = client.Close()

if err != nil {
e.logger.Warn().
Err(err).
Str("endpoint", endpoint).
Str("address", address).
Msg("failed to query balance, trying next endpoint")
m.RecordConsensusRpcEndpointAvailability(e.chainID, endpoint, false)
m.RecordConsensusRpcEndpointError(e.chainID, endpoint, utils.CategorizeError(err))
continue
}

// success - record availability
m.RecordConsensusRpcEndpointAvailability(e.chainID, endpoint, true)

// record balances for each denom
for _, coin := range balances {
// convert cosmos math.Int to string then to big.Float
amountStr := coin.Amount.String()
amount, ok := new(big.Float).SetString(amountStr)
if !ok {
e.logger.Warn().
Str("amount", amountStr).
Str("denom", coin.Denom).
Msg("failed to parse balance amount")
continue
}

balanceFloat, _ := amount.Float64()
m.RecordAccountBalance(e.chainID, address, coin.Denom, balanceFloat)

e.logger.Info().
Str("address", address).
Str("denom", coin.Denom).
Float64("balance", balanceFloat).
Str("endpoint", endpoint).
Msg("recorded account balance")
}

// successfully queried from this endpoint
return
}

// all endpoints failed
e.logger.Error().
Str("address", address).
Msg("failed to query balance from all endpoints")
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

To improve efficiency, this function should be replaced with a new function checkBalances that handles all addresses in one go, avoiding the creation of multiple gRPC connections. This new function will iterate through endpoints, and for each successful connection, it will query balances for all addresses. This also includes logging the error from client.Close() instead of ignoring it. This change is related to my other comment in ExportMetrics.

// checkBalances queries balance for all addresses with fallback across endpoints
func (e *exporter) checkBalances(ctx context.Context, m *metrics.Metrics) {
	// try each endpoint until one succeeds for all addresses
	for _, endpoint := range e.endpoints {
		client, err := cosmos.NewClient(endpoint, e.logger)
		if err != nil {
			e.logger.Warn().
				Err(err).
				Str("endpoint", endpoint).
				Msg("failed to create client, trying next endpoint")
			m.RecordConsensusRpcEndpointAvailability(e.chainID, endpoint, false)
			m.RecordConsensusRpcEndpointError(e.chainID, endpoint, utils.CategorizeError(err))
			continue
		}

		allSucceeded := true
		for _, address := range e.addresses {
			balances, err := client.GetBalance(ctx, address)
			if err != nil {
				e.logger.Warn().
					Err(err).
					Str("endpoint", endpoint).
					Str("address", address).
					Msg("failed to query balance, trying next endpoint")
				m.RecordConsensusRpcEndpointAvailability(e.chainID, endpoint, false)
				m.RecordConsensusRpcEndpointError(e.chainID, endpoint, utils.CategorizeError(err))
				allSucceeded = false
				break // This endpoint is faulty, try the next one
			}

			// success for this address - record availability for endpoint
			m.RecordConsensusRpcEndpointAvailability(e.chainID, endpoint, true)

			// record balances for each denom
			for _, coin := range balances {
				amountStr := coin.Amount.String()
				amount, ok := new(big.Float).SetString(amountStr)
				if !ok {
					e.logger.Warn().
						Str("amount", amountStr).
						Str("denom", coin.Denom).
						Msg("failed to parse balance amount")
					continue
				}

				balanceFloat, _ := amount.Float64()
				m.RecordAccountBalance(e.chainID, address, coin.Denom, balanceFloat)

				e.logger.Info().
					Str("address", address).
					Str("denom", coin.Denom).
					Float64("balance", balanceFloat).
					Str("endpoint", endpoint).
					Msg("recorded account balance")
			}
		}

		if err := client.Close(); err != nil {
			e.logger.Warn().Err(err).Str("endpoint", endpoint).Msg("failed to close client connection")
		}

		if allSucceeded {
			return // All done for this scrape interval
		}
	}

	// all endpoints failed for at least one address
		e.logger.Error().
			Msg("failed to query balances from all endpoints")
}

Comment on lines +113 to +114
--header-namespace collect_testnet_header \
--data-namespace collect_testnet_data \
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The namespaces in this example (collect_testnet_header, collect_testnet_data) are inconsistent with the changes made elsewhere in this README, where they were updated to testnet_header and testnet_data. For consistency, this example should also use the updated namespace names.

Suggested change
--header-namespace collect_testnet_header \
--data-namespace collect_testnet_data \
--header-namespace testnet_header \
--data-namespace testnet_data \

Comment on lines +227 to +228
addressList := strings.Split(flags.balanceAddresses, ",")
rpcUrls := strings.Split(flags.balanceRpcUrls, ",")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

While splitting comma-separated strings is a common pattern, it can be brittle if users accidentally add extra spaces (e.g., "addr1, addr2"). Using strings.FieldsFunc with a custom splitter can make parsing more robust by trimming whitespace automatically.

Suggested change
addressList := strings.Split(flags.balanceAddresses, ",")
rpcUrls := strings.Split(flags.balanceRpcUrls, ",")
addressList := strings.FieldsFunc(flags.balanceAddresses, func(c rune) bool { return c == ',' })
rpcUrls := strings.FieldsFunc(flags.balanceRpcUrls, func(c rune) bool { return c == ',' })

Copy link
Collaborator

@auricom auricom left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

working great

@chatton chatton merged commit 11f9068 into main Nov 10, 2025
3 checks passed
@chatton chatton deleted the cian/add-balance-checker branch November 10, 2025 16:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature Request]: Add balance-checker subcommand for multiple accounts via Celestia consensus RPC

3 participants