solana_exporter/cmd/solana-exporter/utils.go

146 lines
4.4 KiB
Go

package main
import (
"context"
"encoding/json"
"fmt"
"github.com/asymmetric-research/solana-exporter/pkg/rpc"
"github.com/asymmetric-research/solana-exporter/pkg/slog"
"slices"
)
const VoteProgram = "Vote111111111111111111111111111111111111111"
func assertf(condition bool, format string, args ...any) {
logger := slog.Get()
if !condition {
logger.Fatalf(format, args...)
}
}
// toString is just a simple utility function for converting to strings
func toString(i any) string {
return fmt.Sprintf("%v", i)
}
// SelectFromSchedule takes a leader-schedule and returns a trimmed leader-schedule
// containing only the slots within the provided range
func SelectFromSchedule(schedule map[string][]int64, startSlot, endSlot int64) map[string][]int64 {
selected := make(map[string][]int64)
for key, values := range schedule {
var selectedValues []int64
for _, value := range values {
if value >= startSlot && value <= endSlot {
selectedValues = append(selectedValues, value)
}
}
selected[key] = selectedValues
}
return selected
}
// GetTrimmedLeaderSchedule fetches the leader schedule, but only for the validators we are interested in.
// Additionally, it adjusts the leader schedule to the current epoch offset.
func GetTrimmedLeaderSchedule(
ctx context.Context, client *rpc.Client, identities []string, slot, epochFirstSlot int64,
) (map[string][]int64, error) {
logger := slog.Get()
leaderSchedule, err := client.GetLeaderSchedule(ctx, rpc.CommitmentConfirmed, slot)
if err != nil {
return nil, fmt.Errorf("failed to get leader schedule: %w", err)
}
trimmedLeaderSchedule := make(map[string][]int64)
for _, id := range identities {
if leaderSlots, ok := leaderSchedule[id]; ok {
// when you fetch the leader schedule, it gives you slot indexes, we want absolute slots:
absoluteSlots := make([]int64, len(leaderSlots))
for i, slotIndex := range leaderSlots {
absoluteSlots[i] = slotIndex + epochFirstSlot
}
trimmedLeaderSchedule[id] = absoluteSlots
} else {
logger.Warnf("failed to find leader slots for %v", id)
}
}
return trimmedLeaderSchedule, nil
}
// GetAssociatedVoteAccounts returns the votekeys associated with a given list of nodekeys
func GetAssociatedVoteAccounts(
ctx context.Context, client *rpc.Client, commitment rpc.Commitment, nodekeys []string,
) ([]string, error) {
voteAccounts, err := client.GetVoteAccounts(ctx, commitment)
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.Client, 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
}
// GetEpochBounds returns the first slot and last slot within an [inclusive] Epoch
func GetEpochBounds(info *rpc.EpochInfo) (int64, int64) {
firstSlot := info.AbsoluteSlot - info.SlotIndex
return firstSlot, firstSlot + info.SlotsInEpoch - 1
}
func CountVoteTransactions(block *rpc.Block) (int, error) {
txData, err := json.Marshal(block.Transactions)
if err != nil {
return 0, fmt.Errorf("failed to marshal transactions: %w", err)
}
var transactions []rpc.FullTransaction
if err := json.Unmarshal(txData, &transactions); err != nil {
return 0, fmt.Errorf("failed to unmarshal transactions: %w", err)
}
voteCount := 0
for _, tx := range transactions {
if slices.Contains(tx.Transaction.Message.AccountKeys, VoteProgram) {
voteCount++
}
}
return voteCount, nil
}