Files

327 lines
9.4 KiB
Go

// Copyright (C) 2022, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package subnetcmd
import (
"context"
"errors"
"fmt"
"os"
"strconv"
"strings"
"time"
"github.com/luxfi/cli/v2/pkg/constants"
"github.com/luxfi/cli/v2/pkg/models"
"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/utils/rpc"
"github.com/luxfi/node/v2/v2/vms/platformvm"
"github.com/luxfi/node/v2/v2/vms/platformvm/api"
"github.com/luxfi/node/v2/v2/vms/platformvm/signer"
"github.com/olekukonko/tablewriter"
"github.com/spf13/cobra"
)
// lux subnet stats
func newStatsCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "stats [subnetName]",
Short: "Show validator statistics for the given subnet",
Long: `The subnet stats command prints validator statistics for the given Subnet.`,
Args: cobra.ExactArgs(1),
RunE: stats,
SilenceUsage: true,
}
cmd.Flags().BoolVar(&deployTestnet, "testnet", false, "print stats on testnet")
cmd.Flags().BoolVar(&deployMainnet, "mainnet", false, "print stats on `mainnet`")
return cmd
}
func stats(_ *cobra.Command, args []string) error {
var network models.Network
switch {
case deployTestnet:
network = models.Testnet
case deployMainnet:
network = models.Mainnet
}
if network == models.Undefined {
networkStr, err := app.Prompt.CaptureList(
"Choose a network from which you want to get the statistics (this command only supports public networks)",
[]string{models.Testnet.String(), models.Mainnet.String()},
)
if err != nil {
return err
}
// flag provided
networkStr = strings.Title(networkStr)
// as we are allowing a flag, we need to check if a supported network has been provided
if !(networkStr == models.Testnet.String() || networkStr == models.Mainnet.String()) {
return errors.New("unsupported network")
}
network = models.NetworkFromString(networkStr)
}
chains, err := validateSubnetNameAndGetChains(args)
if err != nil {
return err
}
subnetName := chains[0]
sc, err := app.LoadSidecar(subnetName)
if err != nil {
return err
}
subnetID := sc.Networks[network.String()].SubnetID
if subnetID == ids.Empty {
return errors.New("no subnetID found for the provided subnet name; has this subnet 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()
table = tablewriter.NewWriter(os.Stdout)
rows, err = buildPendingValidatorStats(pClient, infoClient, table, subnetID)
if err != nil {
return err
}
if len(rows) == 0 {
return nil
}
for _, row := range rows {
table.Append(row)
}
table.Render()
return nil
}
// Removed duplicate PlatformClient interface - using the one from platformvm package directly
// InfoClient interface for info client operations
type InfoClient interface {
GetNodeID(ctx context.Context, options ...rpc.Option) (ids.NodeID, *signer.ProofOfPossession, error)
GetNodeVersion(ctx context.Context, options ...rpc.Option) (*info.GetNodeVersionReply, error)
}
func buildPendingValidatorStats(pClient platformvm.Client, infoClient InfoClient, table *tablewriter.Table, subnetID ids.ID) ([][]string, error) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
// TODO: GetPendingValidators is not available in the current platformvm client
// pendingValidatorsIface, pendingDelegatorsIface, err := pClient.GetPendingValidators(ctx, subnetID, []ids.NodeID{})
// if err != nil {
// return nil, fmt.Errorf("failed to query the API endpoint for the pending validators: %w", err)
// }
var pendingValidatorsIface []interface{}
var pendingDelegatorsIface []interface{}
pendingValidators := make([]api.PermissionlessValidator, len(pendingValidatorsIface))
var ok bool
for i, v := range pendingValidatorsIface {
pendingValidators[i], ok = v.(api.PermissionlessValidator)
if !ok {
return nil, fmt.Errorf("expected type api.PermissionlessValidator, but got %T", v)
}
}
pendingDelegators := make([]api.Staker, len(pendingDelegatorsIface))
for i, v := range pendingDelegatorsIface {
pendingDelegators[i], ok = v.(api.Staker)
if !ok {
return nil, fmt.Errorf("expected type api.Staker, but got %T", v)
}
}
rows := [][]string{}
if len(pendingValidators) == 0 {
ux.Logger.PrintToUser("No pending validators found.")
return rows, nil
}
ux.Logger.PrintToUser("Pending validators (not yet validating the subnet)")
ux.Logger.PrintToUser("==================================================")
header := []string{"nodeID", "weight", "start-time", "end-time", "vmversion"}
table.SetHeader(header)
table.SetAutoMergeCellsByColumnIndex([]int{0})
table.SetAutoMergeCells(true)
table.SetRowLine(true)
var (
startTime, endTime time.Time
localNodeID ids.NodeID
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 pendingValidators {
startTime = time.Unix(int64(v.StartTime), 0)
endTime = time.Unix(int64(v.EndTime), 0)
uint64Weight := v.Weight
for _, d := range pendingDelegators {
uint64Weight += d.Weight
}
weight = strconv.FormatUint(uint64(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(),
weight,
startTime.Local().String(),
endTime.Local().String(),
versionStr,
})
}
return rows, nil
}
func buildCurrentValidatorStats(pClient platformvm.Client, infoClient InfoClient, 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) {
// 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
}
}
var url string
// try public APIs
switch network {
case models.Testnet:
url = constants.TestnetAPIEndpoint
case models.Mainnet:
url = constants.MainnetAPIEndpoint
}
// unsupported network
if url == "" {
return nil, nil
}
// create client to public API
c = platformvm.NewClient(url)
// 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
}
return nil, nil
}