Files

188 lines
5.0 KiB
Go

// Copyright (C) 2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package blockchaincmd
import (
"context"
"errors"
"fmt"
"os"
"strconv"
"time"
"github.com/luxfi/cli/v2/pkg/cobrautils"
"github.com/luxfi/cli/v2/pkg/constants"
"github.com/luxfi/cli/v2/pkg/models"
"github.com/luxfi/cli/v2/pkg/networkoptions"
"github.com/luxfi/cli/v2/pkg/ux"
"github.com/luxfi/node/v2/v2/api/info"
"github.com/luxfi/ids"
"github.com/luxfi/node/v2/v2/vms/platformvm"
"github.com/olekukonko/tablewriter"
"github.com/spf13/cobra"
)
// lux blockchain stats
func newStatsCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "stats [blockchainName]",
Short: "Show validator statistics for the given blockchain",
Long: `The blockchain stats command prints validator statistics for the given Blockchain.`,
Args: cobrautils.ExactArgs(1),
RunE: stats,
}
networkoptions.AddNetworkFlagsToCmd(cmd, &globalNetworkFlags, false, networkoptions.DefaultSupportedNetworkOptions)
return cmd
}
func stats(_ *cobra.Command, args []string) error {
network, err := networkoptions.GetNetworkFromCmdLineFlags(
app,
"",
globalNetworkFlags,
true,
false,
networkoptions.DefaultSupportedNetworkOptions,
"",
)
if err != nil {
return err
}
chains, err := ValidateSubnetNameAndGetChains(args)
if err != nil {
return err
}
blockchainName := chains[0]
sc, err := app.LoadSidecar(blockchainName)
if err != nil {
return err
}
subnetID := sc.Networks[network.Name()].SubnetID
if subnetID == ids.Empty {
return errors.New("no subnetID found for the provided blockchain name; has this blockchain actually been deployed to this network?")
}
pClient, infoClient := findAPIEndpoint(network)
if pClient == nil {
return errors.New("failed to create a client to an API endpoint")
}
table := tablewriter.NewWriter(os.Stdout)
rows, err := buildCurrentValidatorStats(pClient, infoClient, table, subnetID)
if err != nil {
return err
}
for _, row := range rows {
table.Append(row)
}
table.Render()
return nil
}
func buildCurrentValidatorStats(pClient platformvm.Client, infoClient info.Client, table *tablewriter.Table, subnetID ids.ID) ([][]string, error) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
currValidators, err := pClient.GetCurrentValidators(ctx, subnetID, []ids.NodeID{})
if err != nil {
return nil, fmt.Errorf("failed to query the API endpoint for the current validators: %w", err)
}
ux.Logger.PrintToUser("Current validators (already validating the subnet)")
ux.Logger.PrintToUser("==================================================")
header := []string{"nodeID", "connected", "weight", "remaining", "vmversion"}
table.SetHeader(header)
table.SetAutoMergeCellsByColumnIndex([]int{0})
table.SetAutoMergeCells(true)
table.SetRowLine(true)
rows := [][]string{}
var (
startTime, endTime time.Time
localNodeID ids.NodeID
remaining, connected, weight string
localVersionStr, versionStr string
)
// try querying the local node for its node version
reply, err := infoClient.GetNodeVersion(ctx)
if err == nil {
// we can ignore err here; if it worked, we have a non-zero node ID
localNodeID, _, _ = infoClient.GetNodeID(ctx)
for k, v := range reply.VMVersions {
localVersionStr = fmt.Sprintf("%s: %s\n", k, v)
}
}
for _, v := range currValidators {
startTime = time.Unix(int64(v.StartTime), 0)
endTime = time.Unix(int64(v.EndTime), 0)
remaining = ux.FormatDuration(endTime.Sub(startTime))
// some members of the returned object are pointers
// so we need to check the pointer is actually valid
if v.Connected != nil {
connected = strconv.FormatBool(*v.Connected)
} else {
connected = constants.NotAvailableLabel
}
uint64Weight := v.Weight
delegators := v.Delegators
for _, d := range delegators {
uint64Weight += d.Weight
}
weight = strconv.FormatUint(uint64Weight, 10)
// if retrieval of localNodeID failed, it will be empty,
// and this comparison fails
if v.NodeID == localNodeID {
versionStr = localVersionStr
}
// query peers for IP address of this NodeID...
rows = append(rows, []string{
v.NodeID.String(),
connected,
weight,
remaining,
versionStr,
})
}
return rows, nil
}
// findAPIEndpoint tries first to create a client to a local node
// if it doesn't find one, it tries public APIs
func findAPIEndpoint(network models.Network) (platformvm.Client, info.Client) {
var i info.Client
// first try local node
ctx := context.Background()
c := platformvm.NewClient(constants.LocalAPIEndpoint)
_, err := c.GetHeight(ctx)
if err == nil {
i = info.NewClient(constants.LocalAPIEndpoint)
// try calling it to make sure it actually worked
_, _, err := i.GetNodeID(ctx)
if err == nil {
return c, i
}
}
// create client to public API
c = platformvm.NewClient(network.Endpoint)
// try calling it to make sure it actually worked
_, err = c.GetHeight(ctx)
if err == nil {
// also try to get a local client
i = info.NewClient(constants.LocalAPIEndpoint)
}
return c, i
}