refactored config
This commit is contained in:
parent
b2cde35375
commit
6f4fcad90d
|
@ -13,33 +13,45 @@ import (
|
||||||
"k8s.io/klog/v2"
|
"k8s.io/klog/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
SkipStatusLabel = "status"
|
||||||
|
StateLabel = "state"
|
||||||
|
NodekeyLabel = "nodekey"
|
||||||
|
VotekeyLabel = "votekey"
|
||||||
|
VersionLabel = "version"
|
||||||
|
AddressLabel = "address"
|
||||||
|
EpochLabel = "epoch"
|
||||||
|
|
||||||
|
StatusSkipped = "skipped"
|
||||||
|
StatusValid = "valid"
|
||||||
|
|
||||||
|
StateCurrent = "current"
|
||||||
|
StateDelinquent = "delinquent"
|
||||||
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
httpTimeout = 60 * time.Second
|
httpTimeout = 60 * time.Second
|
||||||
rpcAddr = flag.String("rpcURI", "", "Solana RPC URI (including protocol and path)")
|
rpcUrl = flag.String("rpc-url", "", "Solana RPC URI (including protocol and path)")
|
||||||
addr = flag.String("addr", ":8080", "Listen address")
|
listenAddress = flag.String("listen-address", ":8080", "Listen address")
|
||||||
votePubkey = flag.String("votepubkey", "", "Validator vote address (will only return results of this address)")
|
httpTimeoutSecs = flag.Int("http-timeout", 60, "HTTP timeout to use, in seconds.")
|
||||||
httpTimeoutSecs = flag.Int("http_timeout", 60, "HTTP timeout in seconds")
|
|
||||||
|
|
||||||
// addresses:
|
// addresses:
|
||||||
|
nodekeys = flag.String(
|
||||||
|
"nodekeys",
|
||||||
|
"",
|
||||||
|
"Comma-separated list of nodekeys (identity accounts) representing validators to monitor.",
|
||||||
|
)
|
||||||
|
comprehensiveSlotTracking = flag.Bool(
|
||||||
|
"comprehensive-slot-tracking",
|
||||||
|
false,
|
||||||
|
"Set this flag to track solana_leader_slots_by_epoch for ALL validators. "+
|
||||||
|
"Warning: this will lead to potentially thousands of new Prometheus metrics being created every epoch.",
|
||||||
|
)
|
||||||
balanceAddresses = flag.String(
|
balanceAddresses = flag.String(
|
||||||
"balance-addresses",
|
"balance-addresses",
|
||||||
"",
|
"",
|
||||||
"Comma-separated list of addresses to monitor SOL balances.",
|
"Comma-separated list of addresses to monitor SOL balances for, "+
|
||||||
)
|
"in addition to the identity and vote accounts of the provided nodekeys.",
|
||||||
leaderSlotAddresses = flag.String(
|
|
||||||
"leader-slot-addresses",
|
|
||||||
"",
|
|
||||||
"Comma-separated list of addresses to monitor leader slots by epoch for, leave nil to track by epoch for all validators (this creates a lot of Prometheus metrics with every new epoch).",
|
|
||||||
)
|
|
||||||
inflationRewardAddresses = flag.String(
|
|
||||||
"inflation-reward-addresses",
|
|
||||||
"",
|
|
||||||
"Comma-separated list of validator vote accounts to track inflationary rewards for",
|
|
||||||
)
|
|
||||||
feeRewardAddresses = flag.String(
|
|
||||||
"fee-reward-addresses",
|
|
||||||
"",
|
|
||||||
"Comma-separated list of validator identity accounts to track fee rewards for.",
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -47,15 +59,12 @@ func init() {
|
||||||
klog.InitFlags(nil)
|
klog.InitFlags(nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
type solanaCollector struct {
|
type SolanaCollector struct {
|
||||||
rpcClient rpc.Provider
|
rpcClient rpc.Provider
|
||||||
|
|
||||||
// config:
|
// config:
|
||||||
slotPace time.Duration
|
slotPace time.Duration
|
||||||
balanceAddresses []string
|
balanceAddresses []string
|
||||||
leaderSlotAddresses []string
|
|
||||||
inflationRewardAddresses []string
|
|
||||||
feeRewardAddresses []string
|
|
||||||
|
|
||||||
/// descriptors:
|
/// descriptors:
|
||||||
totalValidatorsDesc *prometheus.Desc
|
totalValidatorsDesc *prometheus.Desc
|
||||||
|
@ -67,84 +76,60 @@ type solanaCollector struct {
|
||||||
balances *prometheus.Desc
|
balances *prometheus.Desc
|
||||||
}
|
}
|
||||||
|
|
||||||
func createSolanaCollector(
|
func NewSolanaCollector(
|
||||||
provider rpc.Provider,
|
provider rpc.Provider, slotPace time.Duration, balanceAddresses []string, nodekeys []string, votekeys []string,
|
||||||
slotPace time.Duration,
|
) *SolanaCollector {
|
||||||
balanceAddresses []string,
|
collector := &SolanaCollector{
|
||||||
leaderSlotAddresses []string,
|
rpcClient: provider,
|
||||||
inflationRewardAddresses []string,
|
slotPace: slotPace,
|
||||||
feeRewardAddresses []string,
|
balanceAddresses: CombineUnique(balanceAddresses, nodekeys, votekeys),
|
||||||
) *solanaCollector {
|
|
||||||
return &solanaCollector{
|
|
||||||
rpcClient: provider,
|
|
||||||
slotPace: slotPace,
|
|
||||||
balanceAddresses: balanceAddresses,
|
|
||||||
leaderSlotAddresses: leaderSlotAddresses,
|
|
||||||
inflationRewardAddresses: inflationRewardAddresses,
|
|
||||||
feeRewardAddresses: feeRewardAddresses,
|
|
||||||
totalValidatorsDesc: prometheus.NewDesc(
|
totalValidatorsDesc: prometheus.NewDesc(
|
||||||
"solana_active_validators",
|
"solana_active_validators",
|
||||||
"Total number of active validators by state",
|
"Total number of active validators by state",
|
||||||
[]string{"state"},
|
[]string{StateLabel},
|
||||||
nil,
|
nil,
|
||||||
),
|
),
|
||||||
validatorActivatedStake: prometheus.NewDesc(
|
validatorActivatedStake: prometheus.NewDesc(
|
||||||
"solana_validator_activated_stake",
|
"solana_validator_activated_stake",
|
||||||
"Activated stake per validator",
|
"Activated stake per validator",
|
||||||
[]string{"pubkey", "nodekey"},
|
[]string{VotekeyLabel, NodekeyLabel},
|
||||||
nil,
|
nil,
|
||||||
),
|
),
|
||||||
validatorLastVote: prometheus.NewDesc(
|
validatorLastVote: prometheus.NewDesc(
|
||||||
"solana_validator_last_vote",
|
"solana_validator_last_vote",
|
||||||
"Last voted slot per validator",
|
"Last voted slot per validator",
|
||||||
[]string{"pubkey", "nodekey"},
|
[]string{VotekeyLabel, NodekeyLabel},
|
||||||
nil,
|
nil,
|
||||||
),
|
),
|
||||||
validatorRootSlot: prometheus.NewDesc(
|
validatorRootSlot: prometheus.NewDesc(
|
||||||
"solana_validator_root_slot",
|
"solana_validator_root_slot",
|
||||||
"Root slot per validator",
|
"Root slot per validator",
|
||||||
[]string{"pubkey", "nodekey"},
|
[]string{VotekeyLabel, NodekeyLabel},
|
||||||
nil,
|
nil,
|
||||||
),
|
),
|
||||||
validatorDelinquent: prometheus.NewDesc(
|
validatorDelinquent: prometheus.NewDesc(
|
||||||
"solana_validator_delinquent",
|
"solana_validator_delinquent",
|
||||||
"Whether a validator is delinquent",
|
"Whether a validator is delinquent",
|
||||||
[]string{"pubkey", "nodekey"},
|
[]string{VotekeyLabel, NodekeyLabel},
|
||||||
nil,
|
nil,
|
||||||
),
|
),
|
||||||
solanaVersion: prometheus.NewDesc(
|
solanaVersion: prometheus.NewDesc(
|
||||||
"solana_node_version",
|
"solana_node_version",
|
||||||
"Node version of solana",
|
"Node version of solana",
|
||||||
[]string{"version"},
|
[]string{VersionLabel},
|
||||||
nil,
|
nil,
|
||||||
),
|
),
|
||||||
balances: prometheus.NewDesc(
|
balances: prometheus.NewDesc(
|
||||||
"solana_account_balance",
|
"solana_account_balance",
|
||||||
"Solana account balances",
|
"Solana account balances",
|
||||||
[]string{"address"},
|
[]string{AddressLabel},
|
||||||
nil,
|
nil,
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
return collector
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSolanaCollector(
|
func (c *SolanaCollector) Describe(ch chan<- *prometheus.Desc) {
|
||||||
rpcAddr string,
|
|
||||||
balanceAddresses []string,
|
|
||||||
leaderSlotAddresses []string,
|
|
||||||
inflationRewardAddresses []string,
|
|
||||||
feeRewardAddresses []string,
|
|
||||||
) *solanaCollector {
|
|
||||||
return createSolanaCollector(
|
|
||||||
rpc.NewRPCClient(rpcAddr),
|
|
||||||
slotPacerSchedule,
|
|
||||||
balanceAddresses,
|
|
||||||
leaderSlotAddresses,
|
|
||||||
inflationRewardAddresses,
|
|
||||||
feeRewardAddresses,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *solanaCollector) Describe(ch chan<- *prometheus.Desc) {
|
|
||||||
ch <- c.totalValidatorsDesc
|
ch <- c.totalValidatorsDesc
|
||||||
ch <- c.solanaVersion
|
ch <- c.solanaVersion
|
||||||
ch <- c.validatorActivatedStake
|
ch <- c.validatorActivatedStake
|
||||||
|
@ -154,8 +139,8 @@ func (c *solanaCollector) Describe(ch chan<- *prometheus.Desc) {
|
||||||
ch <- c.balances
|
ch <- c.balances
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *solanaCollector) collectVoteAccounts(ctx context.Context, ch chan<- prometheus.Metric) {
|
func (c *SolanaCollector) collectVoteAccounts(ctx context.Context, ch chan<- prometheus.Metric) {
|
||||||
voteAccounts, err := c.rpcClient.GetVoteAccounts(ctx, rpc.CommitmentConfirmed, votePubkey)
|
voteAccounts, err := c.rpcClient.GetVoteAccounts(ctx, rpc.CommitmentConfirmed, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ch <- prometheus.NewInvalidMetric(c.totalValidatorsDesc, err)
|
ch <- prometheus.NewInvalidMetric(c.totalValidatorsDesc, err)
|
||||||
ch <- prometheus.NewInvalidMetric(c.validatorActivatedStake, err)
|
ch <- prometheus.NewInvalidMetric(c.validatorActivatedStake, err)
|
||||||
|
@ -166,10 +151,10 @@ func (c *solanaCollector) collectVoteAccounts(ctx context.Context, ch chan<- pro
|
||||||
}
|
}
|
||||||
|
|
||||||
ch <- prometheus.MustNewConstMetric(
|
ch <- prometheus.MustNewConstMetric(
|
||||||
c.totalValidatorsDesc, prometheus.GaugeValue, float64(len(voteAccounts.Delinquent)), "delinquent",
|
c.totalValidatorsDesc, prometheus.GaugeValue, float64(len(voteAccounts.Delinquent)), StateDelinquent,
|
||||||
)
|
)
|
||||||
ch <- prometheus.MustNewConstMetric(
|
ch <- prometheus.MustNewConstMetric(
|
||||||
c.totalValidatorsDesc, prometheus.GaugeValue, float64(len(voteAccounts.Current)), "current",
|
c.totalValidatorsDesc, prometheus.GaugeValue, float64(len(voteAccounts.Current)), StateCurrent,
|
||||||
)
|
)
|
||||||
|
|
||||||
for _, account := range append(voteAccounts.Current, voteAccounts.Delinquent...) {
|
for _, account := range append(voteAccounts.Current, voteAccounts.Delinquent...) {
|
||||||
|
@ -208,7 +193,7 @@ func (c *solanaCollector) collectVoteAccounts(ctx context.Context, ch chan<- pro
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *solanaCollector) collectVersion(ctx context.Context, ch chan<- prometheus.Metric) {
|
func (c *SolanaCollector) collectVersion(ctx context.Context, ch chan<- prometheus.Metric) {
|
||||||
version, err := c.rpcClient.GetVersion(ctx)
|
version, err := c.rpcClient.GetVersion(ctx)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -219,8 +204,8 @@ func (c *solanaCollector) collectVersion(ctx context.Context, ch chan<- promethe
|
||||||
ch <- prometheus.MustNewConstMetric(c.solanaVersion, prometheus.GaugeValue, 1, version)
|
ch <- prometheus.MustNewConstMetric(c.solanaVersion, prometheus.GaugeValue, 1, version)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *solanaCollector) collectBalances(ctx context.Context, ch chan<- prometheus.Metric) {
|
func (c *SolanaCollector) collectBalances(ctx context.Context, ch chan<- prometheus.Metric) {
|
||||||
balances, err := fetchBalances(ctx, c.rpcClient, c.balanceAddresses)
|
balances, err := FetchBalances(ctx, c.rpcClient, c.balanceAddresses)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ch <- prometheus.NewInvalidMetric(c.solanaVersion, err)
|
ch <- prometheus.NewInvalidMetric(c.solanaVersion, err)
|
||||||
return
|
return
|
||||||
|
@ -231,19 +216,7 @@ func (c *solanaCollector) collectBalances(ctx context.Context, ch chan<- prometh
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchBalances(ctx context.Context, client rpc.Provider, addresses []string) (map[string]float64, error) {
|
func (c *SolanaCollector) Collect(ch chan<- prometheus.Metric) {
|
||||||
balances := make(map[string]float64)
|
|
||||||
for _, address := range addresses {
|
|
||||||
balance, err := client.GetBalance(ctx, rpc.CommitmentConfirmed, address)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
balances[address] = balance
|
|
||||||
}
|
|
||||||
return balances, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *solanaCollector) Collect(ch chan<- prometheus.Metric) {
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), httpTimeout)
|
ctx, cancel := context.WithTimeout(context.Background(), httpTimeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
|
@ -253,15 +226,16 @@ func (c *solanaCollector) Collect(ch chan<- prometheus.Metric) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
ctx := context.Background()
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
if *rpcAddr == "" {
|
if *rpcUrl == "" {
|
||||||
klog.Fatal("Please specify -rpcURI")
|
klog.Fatal("Please specify -rpcURI")
|
||||||
}
|
}
|
||||||
|
|
||||||
if *leaderSlotAddresses == "" {
|
if *comprehensiveSlotTracking {
|
||||||
klog.Warning(
|
klog.Warning(
|
||||||
"Not specifying leader-slot-addresses will lead to potentially thousands of new " +
|
"Comprehensive slot tracking will lead to potentially thousands of new " +
|
||||||
"Prometheus metrics being created every epoch.",
|
"Prometheus metrics being created every epoch.",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -269,37 +243,31 @@ func main() {
|
||||||
httpTimeout = time.Duration(*httpTimeoutSecs) * time.Second
|
httpTimeout = time.Duration(*httpTimeoutSecs) * time.Second
|
||||||
|
|
||||||
var (
|
var (
|
||||||
balAddresses []string
|
balAddresses []string
|
||||||
lsAddresses []string
|
validatorNodekeys []string
|
||||||
irAddresses []string
|
|
||||||
frAddresses []string
|
|
||||||
)
|
)
|
||||||
if *balanceAddresses != "" {
|
if *balanceAddresses != "" {
|
||||||
balAddresses = strings.Split(*balanceAddresses, ",")
|
balAddresses = strings.Split(*balanceAddresses, ",")
|
||||||
klog.Infof("Monitoring balances for %v", balAddresses)
|
|
||||||
}
|
}
|
||||||
if *leaderSlotAddresses != "" {
|
if *nodekeys != "" {
|
||||||
lsAddresses = strings.Split(*leaderSlotAddresses, ",")
|
validatorNodekeys = strings.Split(*nodekeys, ",")
|
||||||
klog.Infof("Monitoring leader-slot by epoch for %v", lsAddresses)
|
klog.Infof("Monitoring the following validators: %v", validatorNodekeys)
|
||||||
|
|
||||||
}
|
|
||||||
if *inflationRewardAddresses != "" {
|
|
||||||
irAddresses = strings.Split(*inflationRewardAddresses, ",")
|
|
||||||
klog.Infof("Monitoring inflation reward by epoch for %v", irAddresses)
|
|
||||||
}
|
|
||||||
if *feeRewardAddresses != "" {
|
|
||||||
frAddresses = strings.Split(*feeRewardAddresses, ",")
|
|
||||||
klog.Infof("Monitoring fee reward by epoch for %v", frAddresses)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
collector := NewSolanaCollector(*rpcAddr, balAddresses, lsAddresses, irAddresses, frAddresses)
|
client := rpc.NewRPCClient(*rpcUrl)
|
||||||
|
ctx_, cancel := context.WithTimeout(ctx, httpTimeout)
|
||||||
slotWatcher := NewCollectorSlotWatcher(collector)
|
defer cancel()
|
||||||
go slotWatcher.WatchSlots(context.Background(), collector.slotPace)
|
votekeys, err := GetAssociatedVoteAccounts(ctx_, client, rpc.CommitmentFinalized, validatorNodekeys)
|
||||||
|
if err != nil {
|
||||||
|
klog.Fatalf("Failed to get associated vote accounts for %v: %v", nodekeys, err)
|
||||||
|
}
|
||||||
|
collector := NewSolanaCollector(client, slotPacerSchedule, balAddresses, validatorNodekeys, votekeys)
|
||||||
|
slotWatcher := NewSlotWatcher(client, validatorNodekeys, votekeys, *comprehensiveSlotTracking)
|
||||||
|
go slotWatcher.WatchSlots(ctx, collector.slotPace)
|
||||||
|
|
||||||
prometheus.MustRegister(collector)
|
prometheus.MustRegister(collector)
|
||||||
http.Handle("/metrics", promhttp.Handler())
|
http.Handle("/metrics", promhttp.Handler())
|
||||||
|
|
||||||
klog.Infof("listening on %s", *addr)
|
klog.Infof("listening on %s", *listenAddress)
|
||||||
klog.Fatal(http.ListenAndServe(*addr, nil))
|
klog.Fatal(http.ListenAndServe(*listenAddress, nil))
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,7 +43,7 @@ type (
|
||||||
var (
|
var (
|
||||||
identities = []string{"aaa", "bbb", "ccc"}
|
identities = []string{"aaa", "bbb", "ccc"}
|
||||||
votekeys = []string{"AAA", "BBB", "CCC"}
|
votekeys = []string{"AAA", "BBB", "CCC"}
|
||||||
balances = map[string]float64{"aaa": 1, "bbb": 2, "ccc": 3}
|
balances = map[string]float64{"aaa": 1, "bbb": 2, "ccc": 3, "AAA": 4, "BBB": 5, "CCC": 6}
|
||||||
identityVotes = map[string]string{"aaa": "AAA", "bbb": "BBB", "ccc": "CCC"}
|
identityVotes = map[string]string{"aaa": "AAA", "bbb": "BBB", "ccc": "CCC"}
|
||||||
nv = len(identities)
|
nv = len(identities)
|
||||||
staticEpochInfo = rpc.EpochInfo{
|
staticEpochInfo = rpc.EpochInfo{
|
||||||
|
@ -106,6 +106,16 @@ var (
|
||||||
staticLeaderSchedule = map[string][]int64{
|
staticLeaderSchedule = map[string][]int64{
|
||||||
"aaa": {0, 3, 6, 9, 12}, "bbb": {1, 4, 7, 10, 13}, "ccc": {2, 5, 8, 11, 14},
|
"aaa": {0, 3, 6, 9, 12}, "bbb": {1, 4, 7, 10, 13}, "ccc": {2, 5, 8, 11, 14},
|
||||||
}
|
}
|
||||||
|
balanceMetricResponse = `
|
||||||
|
# HELP solana_account_balance Solana account balances
|
||||||
|
# TYPE solana_account_balance gauge
|
||||||
|
solana_account_balance{address="AAA"} 4
|
||||||
|
solana_account_balance{address="BBB"} 5
|
||||||
|
solana_account_balance{address="CCC"} 6
|
||||||
|
solana_account_balance{address="aaa"} 1
|
||||||
|
solana_account_balance{address="bbb"} 2
|
||||||
|
solana_account_balance{address="ccc"} 3
|
||||||
|
`
|
||||||
)
|
)
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -394,9 +404,7 @@ func runCollectionTests(t *testing.T, collector prometheus.Collector, testCases
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSolanaCollector_Collect_Static(t *testing.T) {
|
func TestSolanaCollector_Collect_Static(t *testing.T) {
|
||||||
collector := createSolanaCollector(
|
collector := NewSolanaCollector(&staticRPCClient{}, slotPacerSchedule, nil, identities, votekeys)
|
||||||
&staticRPCClient{}, slotPacerSchedule, identities, []string{}, votekeys, identities,
|
|
||||||
)
|
|
||||||
prometheus.NewPedanticRegistry().MustRegister(collector)
|
prometheus.NewPedanticRegistry().MustRegister(collector)
|
||||||
|
|
||||||
testCases := []collectionTest{
|
testCases := []collectionTest{
|
||||||
|
@ -414,9 +422,9 @@ solana_active_validators{state="delinquent"} 1
|
||||||
ExpectedResponse: `
|
ExpectedResponse: `
|
||||||
# HELP solana_validator_activated_stake Activated stake per validator
|
# HELP solana_validator_activated_stake Activated stake per validator
|
||||||
# TYPE solana_validator_activated_stake gauge
|
# TYPE solana_validator_activated_stake gauge
|
||||||
solana_validator_activated_stake{nodekey="aaa",pubkey="AAA"} 49
|
solana_validator_activated_stake{nodekey="aaa",votekey="AAA"} 49
|
||||||
solana_validator_activated_stake{nodekey="bbb",pubkey="BBB"} 42
|
solana_validator_activated_stake{nodekey="bbb",votekey="BBB"} 42
|
||||||
solana_validator_activated_stake{nodekey="ccc",pubkey="CCC"} 43
|
solana_validator_activated_stake{nodekey="ccc",votekey="CCC"} 43
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -424,9 +432,9 @@ solana_validator_activated_stake{nodekey="ccc",pubkey="CCC"} 43
|
||||||
ExpectedResponse: `
|
ExpectedResponse: `
|
||||||
# HELP solana_validator_last_vote Last voted slot per validator
|
# HELP solana_validator_last_vote Last voted slot per validator
|
||||||
# TYPE solana_validator_last_vote gauge
|
# TYPE solana_validator_last_vote gauge
|
||||||
solana_validator_last_vote{nodekey="aaa",pubkey="AAA"} 92
|
solana_validator_last_vote{nodekey="aaa",votekey="AAA"} 92
|
||||||
solana_validator_last_vote{nodekey="bbb",pubkey="BBB"} 147
|
solana_validator_last_vote{nodekey="bbb",votekey="BBB"} 147
|
||||||
solana_validator_last_vote{nodekey="ccc",pubkey="CCC"} 148
|
solana_validator_last_vote{nodekey="ccc",votekey="CCC"} 148
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -434,9 +442,9 @@ solana_validator_last_vote{nodekey="ccc",pubkey="CCC"} 148
|
||||||
ExpectedResponse: `
|
ExpectedResponse: `
|
||||||
# HELP solana_validator_root_slot Root slot per validator
|
# HELP solana_validator_root_slot Root slot per validator
|
||||||
# TYPE solana_validator_root_slot gauge
|
# TYPE solana_validator_root_slot gauge
|
||||||
solana_validator_root_slot{nodekey="aaa",pubkey="AAA"} 3
|
solana_validator_root_slot{nodekey="aaa",votekey="AAA"} 3
|
||||||
solana_validator_root_slot{nodekey="bbb",pubkey="BBB"} 18
|
solana_validator_root_slot{nodekey="bbb",votekey="BBB"} 18
|
||||||
solana_validator_root_slot{nodekey="ccc",pubkey="CCC"} 19
|
solana_validator_root_slot{nodekey="ccc",votekey="CCC"} 19
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -444,9 +452,9 @@ solana_validator_root_slot{nodekey="ccc",pubkey="CCC"} 19
|
||||||
ExpectedResponse: `
|
ExpectedResponse: `
|
||||||
# HELP solana_validator_delinquent Whether a validator is delinquent
|
# HELP solana_validator_delinquent Whether a validator is delinquent
|
||||||
# TYPE solana_validator_delinquent gauge
|
# TYPE solana_validator_delinquent gauge
|
||||||
solana_validator_delinquent{nodekey="aaa",pubkey="AAA"} 1
|
solana_validator_delinquent{nodekey="aaa",votekey="AAA"} 1
|
||||||
solana_validator_delinquent{nodekey="bbb",pubkey="BBB"} 0
|
solana_validator_delinquent{nodekey="bbb",votekey="BBB"} 0
|
||||||
solana_validator_delinquent{nodekey="ccc",pubkey="CCC"} 0
|
solana_validator_delinquent{nodekey="ccc",votekey="CCC"} 0
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -458,14 +466,8 @@ solana_node_version{version="1.16.7"} 1
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "solana_account_balance",
|
Name: "solana_account_balance",
|
||||||
ExpectedResponse: `
|
ExpectedResponse: balanceMetricResponse,
|
||||||
# HELP solana_account_balance Solana account balances
|
|
||||||
# TYPE solana_account_balance gauge
|
|
||||||
solana_account_balance{address="aaa"} 1
|
|
||||||
solana_account_balance{address="bbb"} 2
|
|
||||||
solana_account_balance{address="ccc"} 3
|
|
||||||
`,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -474,7 +476,7 @@ solana_account_balance{address="ccc"} 3
|
||||||
|
|
||||||
func TestSolanaCollector_Collect_Dynamic(t *testing.T) {
|
func TestSolanaCollector_Collect_Dynamic(t *testing.T) {
|
||||||
client := newDynamicRPCClient()
|
client := newDynamicRPCClient()
|
||||||
collector := createSolanaCollector(client, slotPacerSchedule, identities, []string{}, votekeys, identities)
|
collector := NewSolanaCollector(client, slotPacerSchedule, nil, identities, votekeys)
|
||||||
prometheus.NewPedanticRegistry().MustRegister(collector)
|
prometheus.NewPedanticRegistry().MustRegister(collector)
|
||||||
|
|
||||||
// start off by testing initial state:
|
// start off by testing initial state:
|
||||||
|
@ -493,9 +495,9 @@ solana_active_validators{state="delinquent"} 0
|
||||||
ExpectedResponse: `
|
ExpectedResponse: `
|
||||||
# HELP solana_validator_activated_stake Activated stake per validator
|
# HELP solana_validator_activated_stake Activated stake per validator
|
||||||
# TYPE solana_validator_activated_stake gauge
|
# TYPE solana_validator_activated_stake gauge
|
||||||
solana_validator_activated_stake{nodekey="aaa",pubkey="AAA"} 1000000
|
solana_validator_activated_stake{nodekey="aaa",votekey="AAA"} 1000000
|
||||||
solana_validator_activated_stake{nodekey="bbb",pubkey="BBB"} 1000000
|
solana_validator_activated_stake{nodekey="bbb",votekey="BBB"} 1000000
|
||||||
solana_validator_activated_stake{nodekey="ccc",pubkey="CCC"} 1000000
|
solana_validator_activated_stake{nodekey="ccc",votekey="CCC"} 1000000
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -503,9 +505,9 @@ solana_validator_activated_stake{nodekey="ccc",pubkey="CCC"} 1000000
|
||||||
ExpectedResponse: `
|
ExpectedResponse: `
|
||||||
# HELP solana_validator_root_slot Root slot per validator
|
# HELP solana_validator_root_slot Root slot per validator
|
||||||
# TYPE solana_validator_root_slot gauge
|
# TYPE solana_validator_root_slot gauge
|
||||||
solana_validator_root_slot{nodekey="aaa",pubkey="AAA"} 0
|
solana_validator_root_slot{nodekey="aaa",votekey="AAA"} 0
|
||||||
solana_validator_root_slot{nodekey="bbb",pubkey="BBB"} 0
|
solana_validator_root_slot{nodekey="bbb",votekey="BBB"} 0
|
||||||
solana_validator_root_slot{nodekey="ccc",pubkey="CCC"} 0
|
solana_validator_root_slot{nodekey="ccc",votekey="CCC"} 0
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -513,9 +515,9 @@ solana_validator_root_slot{nodekey="ccc",pubkey="CCC"} 0
|
||||||
ExpectedResponse: `
|
ExpectedResponse: `
|
||||||
# HELP solana_validator_delinquent Whether a validator is delinquent
|
# HELP solana_validator_delinquent Whether a validator is delinquent
|
||||||
# TYPE solana_validator_delinquent gauge
|
# TYPE solana_validator_delinquent gauge
|
||||||
solana_validator_delinquent{nodekey="aaa",pubkey="AAA"} 0
|
solana_validator_delinquent{nodekey="aaa",votekey="AAA"} 0
|
||||||
solana_validator_delinquent{nodekey="bbb",pubkey="BBB"} 0
|
solana_validator_delinquent{nodekey="bbb",votekey="BBB"} 0
|
||||||
solana_validator_delinquent{nodekey="ccc",pubkey="CCC"} 0
|
solana_validator_delinquent{nodekey="ccc",votekey="CCC"} 0
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -527,14 +529,8 @@ solana_node_version{version="v1.0.0"} 1
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "solana_account_balance",
|
Name: "solana_account_balance",
|
||||||
ExpectedResponse: `
|
ExpectedResponse: balanceMetricResponse,
|
||||||
# HELP solana_account_balance Solana account balances
|
|
||||||
# TYPE solana_account_balance gauge
|
|
||||||
solana_account_balance{address="aaa"} 1
|
|
||||||
solana_account_balance{address="bbb"} 2
|
|
||||||
solana_account_balance{address="ccc"} 3
|
|
||||||
`,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -562,9 +558,9 @@ solana_active_validators{state="delinquent"} 1
|
||||||
ExpectedResponse: `
|
ExpectedResponse: `
|
||||||
# HELP solana_validator_activated_stake Activated stake per validator
|
# HELP solana_validator_activated_stake Activated stake per validator
|
||||||
# TYPE solana_validator_activated_stake gauge
|
# TYPE solana_validator_activated_stake gauge
|
||||||
solana_validator_activated_stake{nodekey="aaa",pubkey="AAA"} 2000000
|
solana_validator_activated_stake{nodekey="aaa",votekey="AAA"} 2000000
|
||||||
solana_validator_activated_stake{nodekey="bbb",pubkey="BBB"} 500000
|
solana_validator_activated_stake{nodekey="bbb",votekey="BBB"} 500000
|
||||||
solana_validator_activated_stake{nodekey="ccc",pubkey="CCC"} 1000000
|
solana_validator_activated_stake{nodekey="ccc",votekey="CCC"} 1000000
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -572,9 +568,9 @@ solana_validator_activated_stake{nodekey="ccc",pubkey="CCC"} 1000000
|
||||||
ExpectedResponse: `
|
ExpectedResponse: `
|
||||||
# HELP solana_validator_root_slot Root slot per validator
|
# HELP solana_validator_root_slot Root slot per validator
|
||||||
# TYPE solana_validator_root_slot gauge
|
# TYPE solana_validator_root_slot gauge
|
||||||
solana_validator_root_slot{nodekey="aaa",pubkey="AAA"} 0
|
solana_validator_root_slot{nodekey="aaa",votekey="AAA"} 0
|
||||||
solana_validator_root_slot{nodekey="bbb",pubkey="BBB"} 0
|
solana_validator_root_slot{nodekey="bbb",votekey="BBB"} 0
|
||||||
solana_validator_root_slot{nodekey="ccc",pubkey="CCC"} 0
|
solana_validator_root_slot{nodekey="ccc",votekey="CCC"} 0
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -582,9 +578,9 @@ solana_validator_root_slot{nodekey="ccc",pubkey="CCC"} 0
|
||||||
ExpectedResponse: `
|
ExpectedResponse: `
|
||||||
# HELP solana_validator_delinquent Whether a validator is delinquent
|
# HELP solana_validator_delinquent Whether a validator is delinquent
|
||||||
# TYPE solana_validator_delinquent gauge
|
# TYPE solana_validator_delinquent gauge
|
||||||
solana_validator_delinquent{nodekey="aaa",pubkey="AAA"} 0
|
solana_validator_delinquent{nodekey="aaa",votekey="AAA"} 0
|
||||||
solana_validator_delinquent{nodekey="bbb",pubkey="BBB"} 0
|
solana_validator_delinquent{nodekey="bbb",votekey="BBB"} 0
|
||||||
solana_validator_delinquent{nodekey="ccc",pubkey="CCC"} 1
|
solana_validator_delinquent{nodekey="ccc",votekey="CCC"} 1
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -596,14 +592,8 @@ solana_node_version{version="v1.2.3"} 1
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "solana_account_balance",
|
Name: "solana_account_balance",
|
||||||
ExpectedResponse: `
|
ExpectedResponse: balanceMetricResponse,
|
||||||
# HELP solana_account_balance Solana account balances
|
|
||||||
# TYPE solana_account_balance gauge
|
|
||||||
solana_account_balance{address="aaa"} 1
|
|
||||||
solana_account_balance{address="bbb"} 2
|
|
||||||
solana_account_balance{address="ccc"} 3
|
|
||||||
`,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -21,9 +21,9 @@ type SlotWatcher struct {
|
||||||
client rpc.Provider
|
client rpc.Provider
|
||||||
|
|
||||||
// config:
|
// config:
|
||||||
leaderSlotAddresses []string
|
nodekeys []string
|
||||||
inflationRewardAddresses []string
|
votekeys []string
|
||||||
feeRewardAddresses []string
|
comprehensiveSlotTracking bool
|
||||||
|
|
||||||
// currentEpoch is the current epoch we are watching
|
// currentEpoch is the current epoch we are watching
|
||||||
currentEpoch int64
|
currentEpoch int64
|
||||||
|
@ -68,7 +68,7 @@ var (
|
||||||
Name: "solana_leader_slots_total",
|
Name: "solana_leader_slots_total",
|
||||||
Help: "(DEPRECATED) Number of leader slots per leader, grouped by skip status",
|
Help: "(DEPRECATED) Number of leader slots per leader, grouped by skip status",
|
||||||
},
|
},
|
||||||
[]string{"status", "nodekey"},
|
[]string{SkipStatusLabel, NodekeyLabel},
|
||||||
)
|
)
|
||||||
|
|
||||||
leaderSlotsByEpoch = prometheus.NewCounterVec(
|
leaderSlotsByEpoch = prometheus.NewCounterVec(
|
||||||
|
@ -76,7 +76,7 @@ var (
|
||||||
Name: "solana_leader_slots_by_epoch",
|
Name: "solana_leader_slots_by_epoch",
|
||||||
Help: "Number of leader slots per leader, grouped by skip status and epoch",
|
Help: "Number of leader slots per leader, grouped by skip status and epoch",
|
||||||
},
|
},
|
||||||
[]string{"status", "nodekey", "epoch"},
|
[]string{SkipStatusLabel, NodekeyLabel, EpochLabel},
|
||||||
)
|
)
|
||||||
|
|
||||||
inflationRewards = prometheus.NewGaugeVec(
|
inflationRewards = prometheus.NewGaugeVec(
|
||||||
|
@ -84,7 +84,7 @@ var (
|
||||||
Name: "solana_inflation_rewards",
|
Name: "solana_inflation_rewards",
|
||||||
Help: "Inflation reward earned per validator vote account, per epoch",
|
Help: "Inflation reward earned per validator vote account, per epoch",
|
||||||
},
|
},
|
||||||
[]string{"votekey", "epoch"},
|
[]string{VotekeyLabel, EpochLabel},
|
||||||
)
|
)
|
||||||
|
|
||||||
feeRewards = prometheus.NewCounterVec(
|
feeRewards = prometheus.NewCounterVec(
|
||||||
|
@ -92,16 +92,18 @@ var (
|
||||||
Name: "solana_fee_rewards",
|
Name: "solana_fee_rewards",
|
||||||
Help: "Transaction fee rewards earned per validator identity account, per epoch",
|
Help: "Transaction fee rewards earned per validator identity account, per epoch",
|
||||||
},
|
},
|
||||||
[]string{"nodekey", "epoch"},
|
[]string{NodekeyLabel, EpochLabel},
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewCollectorSlotWatcher(collector *solanaCollector) *SlotWatcher {
|
func NewSlotWatcher(
|
||||||
|
client rpc.Provider, nodekeys []string, votekeys []string, comprehensiveSlotTracking bool,
|
||||||
|
) *SlotWatcher {
|
||||||
return &SlotWatcher{
|
return &SlotWatcher{
|
||||||
client: collector.rpcClient,
|
client: client,
|
||||||
leaderSlotAddresses: collector.leaderSlotAddresses,
|
nodekeys: nodekeys,
|
||||||
inflationRewardAddresses: collector.inflationRewardAddresses,
|
votekeys: votekeys,
|
||||||
feeRewardAddresses: collector.feeRewardAddresses,
|
comprehensiveSlotTracking: comprehensiveSlotTracking,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -157,8 +159,8 @@ func (c *SlotWatcher) WatchSlots(ctx context.Context, pace time.Duration) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if epochInfo.Epoch > c.currentEpoch {
|
if epochInfo.Epoch > c.currentEpoch {
|
||||||
// if we have configured inflation reward addresses, fetch em
|
// fetch inflation rewards for vote accounts:
|
||||||
if len(c.inflationRewardAddresses) > 0 {
|
if len(c.votekeys) > 0 {
|
||||||
err = c.fetchAndEmitInflationRewards(ctx, c.currentEpoch)
|
err = c.fetchAndEmitInflationRewards(ctx, c.currentEpoch)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
klog.Errorf("Failed to emit inflation rewards, bailing out: %v", err)
|
klog.Errorf("Failed to emit inflation rewards, bailing out: %v", err)
|
||||||
|
@ -222,9 +224,7 @@ func (c *SlotWatcher) trackEpoch(ctx context.Context, epoch *rpc.EpochInfo) {
|
||||||
ctx, cancel := context.WithTimeout(ctx, httpTimeout)
|
ctx, cancel := context.WithTimeout(ctx, httpTimeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
klog.Infof("Updating leader schedule for epoch %v ...", c.currentEpoch)
|
klog.Infof("Updating leader schedule for epoch %v ...", c.currentEpoch)
|
||||||
leaderSchedule, err := GetTrimmedLeaderSchedule(
|
leaderSchedule, err := GetTrimmedLeaderSchedule(ctx, c.client, c.nodekeys, epoch.AbsoluteSlot, c.firstSlot)
|
||||||
ctx, c.client, c.feeRewardAddresses, epoch.AbsoluteSlot, c.firstSlot,
|
|
||||||
)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
klog.Errorf("Failed to get trimmed leader schedule, bailing out: %v", err)
|
klog.Errorf("Failed to get trimmed leader schedule, bailing out: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -286,13 +286,13 @@ func (c *SlotWatcher) fetchAndEmitBlockProduction(ctx context.Context, endSlot i
|
||||||
valid := float64(production.BlocksProduced)
|
valid := float64(production.BlocksProduced)
|
||||||
skipped := float64(production.LeaderSlots - production.BlocksProduced)
|
skipped := float64(production.LeaderSlots - production.BlocksProduced)
|
||||||
|
|
||||||
leaderSlotsTotal.WithLabelValues("valid", address).Add(valid)
|
leaderSlotsTotal.WithLabelValues(StatusValid, address).Add(valid)
|
||||||
leaderSlotsTotal.WithLabelValues("skipped", address).Add(skipped)
|
leaderSlotsTotal.WithLabelValues(StatusSkipped, address).Add(skipped)
|
||||||
|
|
||||||
if len(c.leaderSlotAddresses) == 0 || slices.Contains(c.leaderSlotAddresses, address) {
|
if slices.Contains(c.nodekeys, address) || c.comprehensiveSlotTracking {
|
||||||
epochStr := toString(c.currentEpoch)
|
epochStr := toString(c.currentEpoch)
|
||||||
leaderSlotsByEpoch.WithLabelValues("valid", address, epochStr).Add(valid)
|
leaderSlotsByEpoch.WithLabelValues(StatusValid, address, epochStr).Add(valid)
|
||||||
leaderSlotsByEpoch.WithLabelValues("skipped", address, epochStr).Add(skipped)
|
leaderSlotsByEpoch.WithLabelValues(StatusSkipped, address, epochStr).Add(skipped)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -376,15 +376,13 @@ func (c *SlotWatcher) fetchAndEmitInflationRewards(ctx context.Context, epoch in
|
||||||
ctx, cancel := context.WithTimeout(ctx, httpTimeout)
|
ctx, cancel := context.WithTimeout(ctx, httpTimeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
rewardInfos, err := c.client.GetInflationReward(
|
rewardInfos, err := c.client.GetInflationReward(ctx, rpc.CommitmentConfirmed, c.votekeys, &epoch, nil)
|
||||||
ctx, rpc.CommitmentConfirmed, c.inflationRewardAddresses, &epoch, nil,
|
|
||||||
)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error fetching inflation rewards: %w", err)
|
return fmt.Errorf("error fetching inflation rewards: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, rewardInfo := range rewardInfos {
|
for i, rewardInfo := range rewardInfos {
|
||||||
address := c.inflationRewardAddresses[i]
|
address := c.votekeys[i]
|
||||||
reward := float64(rewardInfo.Amount) / float64(rpc.LamportsInSol)
|
reward := float64(rewardInfo.Amount) / float64(rpc.LamportsInSol)
|
||||||
inflationRewards.WithLabelValues(address, toString(epoch)).Set(reward)
|
inflationRewards.WithLabelValues(address, toString(epoch)).Set(reward)
|
||||||
}
|
}
|
||||||
|
|
|
@ -92,10 +92,9 @@ func TestSolanaCollector_WatchSlots_Static(t *testing.T) {
|
||||||
leaderSlotsTotal.Reset()
|
leaderSlotsTotal.Reset()
|
||||||
leaderSlotsByEpoch.Reset()
|
leaderSlotsByEpoch.Reset()
|
||||||
|
|
||||||
collector := createSolanaCollector(
|
client := staticRPCClient{}
|
||||||
&staticRPCClient{}, 100*time.Millisecond, identities, []string{}, votekeys, identities,
|
collector := NewSolanaCollector(&client, 100*time.Millisecond, nil, identities, votekeys)
|
||||||
)
|
watcher := NewSlotWatcher(&client, identities, votekeys, false)
|
||||||
watcher := NewCollectorSlotWatcher(collector)
|
|
||||||
prometheus.NewPedanticRegistry().MustRegister(collector)
|
prometheus.NewPedanticRegistry().MustRegister(collector)
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
@ -163,8 +162,8 @@ func TestSolanaCollector_WatchSlots_Dynamic(t *testing.T) {
|
||||||
|
|
||||||
// create clients:
|
// create clients:
|
||||||
client := newDynamicRPCClient()
|
client := newDynamicRPCClient()
|
||||||
collector := createSolanaCollector(client, 300*time.Millisecond, identities, []string{}, votekeys, identities)
|
collector := NewSolanaCollector(client, 300*time.Millisecond, nil, identities, votekeys)
|
||||||
watcher := NewCollectorSlotWatcher(collector)
|
watcher := NewSlotWatcher(client, identities, votekeys, false)
|
||||||
prometheus.NewPedanticRegistry().MustRegister(collector)
|
prometheus.NewPedanticRegistry().MustRegister(collector)
|
||||||
|
|
||||||
// start client/collector and wait a bit:
|
// start client/collector and wait a bit:
|
||||||
|
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/asymmetric-research/solana_exporter/pkg/rpc"
|
"github.com/asymmetric-research/solana_exporter/pkg/rpc"
|
||||||
"k8s.io/klog/v2"
|
"k8s.io/klog/v2"
|
||||||
|
"slices"
|
||||||
)
|
)
|
||||||
|
|
||||||
func assertf(condition bool, format string, args ...any) {
|
func assertf(condition bool, format string, args ...any) {
|
||||||
|
@ -60,3 +61,55 @@ func GetTrimmedLeaderSchedule(
|
||||||
|
|
||||||
return trimmedLeaderSchedule, nil
|
return trimmedLeaderSchedule, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetAssociatedVoteAccounts returns the votekeys associated with a given list of nodekeys
|
||||||
|
func GetAssociatedVoteAccounts(
|
||||||
|
ctx context.Context, client rpc.Provider, commitment rpc.Commitment, nodekeys []string,
|
||||||
|
) ([]string, error) {
|
||||||
|
voteAccounts, err := client.GetVoteAccounts(ctx, commitment, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// first map nodekey -> votekey:
|
||||||
|
voteAccountsMap := make(map[string]string)
|
||||||
|
for _, voteAccount := range append(voteAccounts.Current, voteAccounts.Delinquent...) {
|
||||||
|
voteAccountsMap[voteAccount.NodePubkey] = voteAccount.VotePubkey
|
||||||
|
}
|
||||||
|
|
||||||
|
votekeys := make([]string, len(nodekeys))
|
||||||
|
for i, nodeKey := range nodekeys {
|
||||||
|
votekey := voteAccountsMap[nodeKey]
|
||||||
|
if votekey == "" {
|
||||||
|
return nil, fmt.Errorf("failed to find vote key for node %v", nodeKey)
|
||||||
|
}
|
||||||
|
votekeys[i] = votekey
|
||||||
|
}
|
||||||
|
return votekeys, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FetchBalances fetches SOL balances for a list of addresses
|
||||||
|
func FetchBalances(ctx context.Context, client rpc.Provider, addresses []string) (map[string]float64, error) {
|
||||||
|
balances := make(map[string]float64)
|
||||||
|
for _, address := range addresses {
|
||||||
|
balance, err := client.GetBalance(ctx, rpc.CommitmentConfirmed, address)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
balances[address] = balance
|
||||||
|
}
|
||||||
|
return balances, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CombineUnique combines unique items from multiple arrays to a single array.
|
||||||
|
func CombineUnique[T comparable](args ...[]T) []T {
|
||||||
|
var uniqueItems []T
|
||||||
|
for _, arg := range args {
|
||||||
|
for _, item := range arg {
|
||||||
|
if !slices.Contains(uniqueItems, item) {
|
||||||
|
uniqueItems = append(uniqueItems, item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return uniqueItems
|
||||||
|
}
|
||||||
|
|
|
@ -22,3 +22,26 @@ func TestGetTrimmedLeaderSchedule(t *testing.T) {
|
||||||
|
|
||||||
assert.Equal(t, map[string][]int64{"aaa": {10, 13, 16, 19, 22}, "bbb": {11, 14, 17, 20, 23}}, schedule)
|
assert.Equal(t, map[string][]int64{"aaa": {10, 13, 16, 19, 22}, "bbb": {11, 14, 17, 20, 23}}, schedule)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCombineUnique(t *testing.T) {
|
||||||
|
var (
|
||||||
|
v1 = []string{"1", "2", "3"}
|
||||||
|
v2 = []string{"2", "3", "4"}
|
||||||
|
v3 = []string{"3", "4", "5"}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.Equal(t, []string{"1", "2", "3", "4", "5"}, CombineUnique(v1, v2, v3))
|
||||||
|
assert.Equal(t, []string{"2", "3", "4", "5"}, CombineUnique(nil, v2, v3))
|
||||||
|
assert.Equal(t, []string{"1", "2", "3", "4", "5"}, CombineUnique(v1, nil, v3))
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFetchBalances(t *testing.T) {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
client := staticRPCClient{}
|
||||||
|
fetchedBalances, err := FetchBalances(ctx, &client, CombineUnique(identities, votekeys))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, balances, fetchedBalances)
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue