mirror of
https://github.com/luxfi/cli.git
synced 2026-10-30 17:01:20 +00:00
`lux dev start` could not bring up a node built from the current tree, for
three reasons that share a shape with the chain-segment rename: the CLI
spelled out an address the node had already left behind, and nothing failed
to compile.
The health service moved onto typed ops. /v1/health is gone, so every probe
written against it read a healthy node as unhealthy and every printed
endpoint sent a reader to a 404. route.Health, route.Liveness and
route.Readiness compose the three addresses the node answers on, next to
route.Chain and off the same base, and the ten call sites that had it
written out now ask for the one they mean: a probe polls readiness, a
report reads the full checks, a printed endpoint offers the report.
Two luxd flags no longer exist, and passing an unknown flag is fatal, so
the node refused the config before it read any of it:
--api-keystore-enabled the keystore API is gone
--automine-interval block cadence is the C-Chain's own
enable-automining, read by the block builder from
the chain config dir, not a node flag
The --automine duration flag went with the second: it named a knob that
does not exist.
`lux dev stop` looked only in the default data-dir, and otherwise pkill'd
anything whose command line carried a quorum size of one โ a pattern that
fits every dev node on the machine, not just the one this command started.
It takes the same --data-dir as start now, resolved by the same function,
and reads the PID from there; no PID file is an answer rather than a reason
to guess.
Health gets composition but not the guard that pins chain addresses:
pkg/ssh still asks for health as a JSON-RPC method, and the whole
JSON-RPC-over-SSH family it belongs to is owed the same move onto typed
ops. A guard added before that migration would need an exemption for it,
and an exemption is how a guard rots.
Measured against luxd 1.36.181 (constants v1.6.4): start reaches ready,
prints /v1/chain/C/rpc and /v1/health/ops/health, both answer 200, and
stop interrupts the node it started and reports plainly when there is
none.
Co-authored-by: Hanzo Dev <[email protected]>
325 lines
11 KiB
Go
325 lines
11 KiB
Go
// Copyright (C) 2022-2025, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package ux
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"golang.org/x/text/language"
|
|
"golang.org/x/text/message"
|
|
|
|
"github.com/luxfi/cli/pkg/route"
|
|
luxlog "github.com/luxfi/log"
|
|
"github.com/luxfi/netrunner/rpcpb"
|
|
"github.com/olekukonko/tablewriter"
|
|
)
|
|
|
|
var Logger *UserLog
|
|
|
|
type UserLog struct {
|
|
log luxlog.Logger
|
|
writer io.Writer
|
|
}
|
|
|
|
func NewUserLog(log luxlog.Logger, userwriter io.Writer) {
|
|
if Logger == nil {
|
|
Logger = &UserLog{
|
|
log: log,
|
|
writer: userwriter,
|
|
}
|
|
}
|
|
}
|
|
|
|
// PrintToUser prints msg directly to stdout (command output)
|
|
// Does NOT log to avoid duplication - logs should go to stderr separately
|
|
func (ul *UserLog) PrintToUser(msg string, args ...interface{}) {
|
|
formattedMsg := fmt.Sprintf(msg, args...)
|
|
_, _ = fmt.Fprintln(ul.writer, formattedMsg)
|
|
}
|
|
|
|
// Info logs an info message
|
|
func (ul *UserLog) Info(msg string, args ...interface{}) {
|
|
formattedMsg := fmt.Sprintf(msg, args...)
|
|
ul.log.Info(formattedMsg)
|
|
}
|
|
|
|
// PrintLineSeparator prints a line separator
|
|
func (ul *UserLog) PrintLineSeparator(msg ...string) {
|
|
separator := "=========================================="
|
|
if len(msg) > 0 && msg[0] != "" {
|
|
separator = msg[0]
|
|
}
|
|
_, _ = fmt.Fprintln(ul.writer, separator)
|
|
ul.log.Info(separator)
|
|
}
|
|
|
|
// Error logs an error message
|
|
func (ul *UserLog) Error(msg string, args ...interface{}) {
|
|
formattedMsg := fmt.Sprintf(msg, args...)
|
|
ul.log.Error(formattedMsg)
|
|
}
|
|
|
|
// RedXToUser prints a red X error message to the user
|
|
func (ul *UserLog) RedXToUser(msg string, args ...interface{}) {
|
|
formattedMsg := fmt.Sprintf("โ %s", fmt.Sprintf(msg, args...))
|
|
_, _ = fmt.Fprintln(ul.writer, formattedMsg)
|
|
ul.log.Error(formattedMsg)
|
|
}
|
|
|
|
// GreenCheckmarkToUser prints a green checkmark success message to the user
|
|
func (ul *UserLog) GreenCheckmarkToUser(msg string, args ...interface{}) {
|
|
formattedMsg := fmt.Sprintf("โ %s", fmt.Sprintf(msg, args...))
|
|
_, _ = fmt.Fprintln(ul.writer, formattedMsg)
|
|
ul.log.Info(formattedMsg)
|
|
}
|
|
|
|
// PrintError prints a visible error message with ERROR prefix to the user
|
|
func (ul *UserLog) PrintError(msg string, args ...interface{}) {
|
|
formattedMsg := fmt.Sprintf(msg, args...)
|
|
errorMsg := fmt.Sprintf("\nERROR: %s\n", formattedMsg)
|
|
_, _ = fmt.Fprintln(ul.writer, errorMsg)
|
|
ul.log.Error(formattedMsg)
|
|
}
|
|
|
|
// PrintWait does some dot printing to entertain the user
|
|
func PrintWait(cancel chan struct{}) {
|
|
for {
|
|
select {
|
|
case <-time.After(1 * time.Second):
|
|
fmt.Print(".")
|
|
case <-cancel:
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// StepTracker tracks progress of multi-step operations with elapsed time
|
|
type StepTracker struct {
|
|
stepStart time.Time
|
|
warnAfter time.Duration
|
|
warningShown bool
|
|
stepName string
|
|
ul *UserLog
|
|
}
|
|
|
|
// NewStepTracker creates a tracker that warns if a step takes longer than warnAfter
|
|
func NewStepTracker(ul *UserLog, warnAfter time.Duration) *StepTracker {
|
|
return &StepTracker{
|
|
ul: ul,
|
|
warnAfter: warnAfter,
|
|
}
|
|
}
|
|
|
|
// Start begins tracking a new step
|
|
func (st *StepTracker) Start(stepName string) {
|
|
st.stepStart = time.Now()
|
|
st.stepName = stepName
|
|
st.warningShown = false
|
|
st.ul.PrintToUser("%s...", stepName)
|
|
}
|
|
|
|
// Elapsed returns the elapsed time for the current step
|
|
func (st *StepTracker) Elapsed() time.Duration {
|
|
return time.Since(st.stepStart)
|
|
}
|
|
|
|
// CheckWarn prints a warning if the step has taken longer than the threshold
|
|
// Returns true if warning was printed
|
|
func (st *StepTracker) CheckWarn() bool {
|
|
if st.warningShown {
|
|
return false
|
|
}
|
|
elapsed := st.Elapsed()
|
|
if elapsed > st.warnAfter {
|
|
st.ul.PrintToUser("Warning: %s taking longer than expected (%.1fs)...", st.stepName, elapsed.Seconds())
|
|
st.warningShown = true
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Complete marks the step as done with success
|
|
func (st *StepTracker) Complete(suffix string) {
|
|
elapsed := st.Elapsed()
|
|
if suffix != "" {
|
|
st.ul.GreenCheckmarkToUser("%s (%.1fs) - %s", st.stepName, elapsed.Seconds(), suffix)
|
|
} else {
|
|
st.ul.GreenCheckmarkToUser("%s (%.1fs)", st.stepName, elapsed.Seconds())
|
|
}
|
|
}
|
|
|
|
// CompleteSuccess is shorthand for Complete with "Success" suffix
|
|
func (st *StepTracker) CompleteSuccess() {
|
|
st.Complete("Success")
|
|
}
|
|
|
|
// Failed marks the step as failed with an error
|
|
func (st *StepTracker) Failed(reason string) {
|
|
elapsed := st.Elapsed()
|
|
st.ul.RedXToUser("%s (%.1fs) - FAILED: %s", st.stepName, elapsed.Seconds(), reason)
|
|
}
|
|
|
|
// PrintTableEndpoints prints the endpoints coming from the healthy call
|
|
func PrintTableEndpoints(clusterInfo *rpcpb.ClusterInfo) {
|
|
table := tablewriter.NewWriter(os.Stdout)
|
|
// Note: SetHeader is not available in v1.0.9, use Append for header row
|
|
|
|
nodeInfos := map[string]*rpcpb.NodeInfo{}
|
|
for _, nodeInfo := range clusterInfo.NodeInfos {
|
|
nodeInfos[nodeInfo.Name] = nodeInfo
|
|
}
|
|
for _, nodeName := range clusterInfo.NodeNames {
|
|
nodeInfo := nodeInfos[nodeName]
|
|
for blockchainID, chainInfo := range clusterInfo.CustomChains {
|
|
_ = table.Append([]string{
|
|
nodeInfo.Name,
|
|
chainInfo.GetChainName(),
|
|
route.Chain(nodeInfo.GetUri(), blockchainID) + "/rpc",
|
|
route.Chain(nodeInfo.GetUri(), chainInfo.GetChainName()) + "/rpc",
|
|
})
|
|
}
|
|
}
|
|
_ = table.Render()
|
|
}
|
|
|
|
// DefaultTable creates a default table with the given title and headers
|
|
func DefaultTable(title string, headers []string) *tablewriter.Table {
|
|
table := tablewriter.NewWriter(os.Stdout)
|
|
// Note: v1.0.9 API doesn't have SetCaption, SetBorder, SetAutoWrapText, SetAlignment
|
|
// These would need to be set via Options during creation or not at all
|
|
return table
|
|
}
|
|
|
|
func ConvertToStringWithThousandSeparator(input uint64) string {
|
|
p := message.NewPrinter(language.English)
|
|
s := p.Sprintf("%d", input)
|
|
return strings.ReplaceAll(s, ",", "_")
|
|
}
|
|
|
|
// NativeChainInfo holds info for pretty-printing a native chain
|
|
type NativeChainInfo struct {
|
|
Letter string // P, C, X, Q, A, B, T, Z, G, K, D
|
|
Name string // Platform, EVM, Exchange, etc.
|
|
Type string // RPC or WS
|
|
Under string // what the chain answers under: nothing at its root, "/rpc", "/ws"
|
|
}
|
|
|
|
// Address is where this chain answers on the node at uri.
|
|
func (c NativeChainInfo) Address(uri string) string {
|
|
return route.Chain(uri, c.Letter) + c.Under
|
|
}
|
|
|
|
// GetNativeChains returns all native chain definitions for RPC display. P and
|
|
// X answer at the chain's root; the EVM chains answer under /rpc.
|
|
func GetNativeChains() []NativeChainInfo {
|
|
return []NativeChainInfo{
|
|
{Letter: "P", Name: "Platform", Type: "RPC"},
|
|
{Letter: "C", Name: "EVM", Type: "RPC", Under: "/rpc"},
|
|
{Letter: "C", Name: "EVM", Type: "WS", Under: "/ws"},
|
|
{Letter: "X", Name: "Exchange", Type: "RPC"},
|
|
{Letter: "Q", Name: "Quantum", Type: "RPC", Under: "/rpc"},
|
|
{Letter: "A", Name: "AI", Type: "RPC", Under: "/rpc"},
|
|
{Letter: "B", Name: "Bridge", Type: "RPC", Under: "/rpc"},
|
|
{Letter: "T", Name: "Threshold", Type: "RPC", Under: "/rpc"},
|
|
{Letter: "Z", Name: "ZK", Type: "RPC", Under: "/rpc"},
|
|
{Letter: "G", Name: "Graph", Type: "RPC", Under: "/rpc"},
|
|
{Letter: "K", Name: "KMS", Type: "RPC", Under: "/rpc"},
|
|
{Letter: "D", Name: "DEX", Type: "RPC", Under: "/rpc"},
|
|
}
|
|
}
|
|
|
|
// PrintCompactChainEndpoints prints chain endpoints in a compact format.
|
|
//
|
|
// Labels and addresses are measured rather than hand-padded, so the box stays
|
|
// square whatever the route segment is. It used to be padded by hand against a
|
|
// segment two characters long, and would have come out ragged the moment that
|
|
// changed.
|
|
func PrintCompactChainEndpoints(portBase int) {
|
|
chains := GetNativeChains()
|
|
|
|
// A letter with more than one row needs its type to tell them apart.
|
|
rows := map[string]int{}
|
|
for _, c := range chains {
|
|
rows[c.Letter]++
|
|
}
|
|
|
|
labels := make([]string, len(chains))
|
|
addresses := make([]string, len(chains))
|
|
labelWidth, addressWidth := 0, 0
|
|
for i, c := range chains {
|
|
label := fmt.Sprintf("%s-Chain (%s)", c.Letter, c.Name)
|
|
if rows[c.Letter] > 1 {
|
|
label += " " + c.Type
|
|
}
|
|
scheme := "http"
|
|
if c.Type == "WS" {
|
|
scheme = "ws"
|
|
}
|
|
labels[i] = label + ":"
|
|
addresses[i] = c.Address(fmt.Sprintf("%s://localhost:%d", scheme, portBase))
|
|
labelWidth = max(labelWidth, len(labels[i]))
|
|
addressWidth = max(addressWidth, len(addresses[i]))
|
|
}
|
|
|
|
Logger.PrintToUser("\n๐ก Native Chain RPC Endpoints:")
|
|
Logger.PrintToUser(" โ%sโ", strings.Repeat("โ", labelWidth+addressWidth+3))
|
|
for i := range chains {
|
|
Logger.PrintToUser(" โ %-*s %-*s โ", labelWidth, labels[i], addressWidth, addresses[i])
|
|
}
|
|
Logger.PrintToUser(" โ%sโ", strings.Repeat("โ", labelWidth+addressWidth+3))
|
|
Logger.PrintToUser("\n๐ง Utility Endpoints:")
|
|
Logger.PrintToUser(" Health: %s", route.Health(fmt.Sprintf("http://localhost:%d", portBase)))
|
|
Logger.PrintToUser(" Info: http://localhost:%d/v1/info", portBase)
|
|
Logger.PrintToUser(" Admin: http://localhost:%d/v1/admin", portBase)
|
|
}
|
|
|
|
// ValidatorKeyInfo holds derived key info for a validator
|
|
type ValidatorKeyInfo struct {
|
|
Index int
|
|
NodeID string
|
|
PChainAddr string
|
|
XChainAddr string
|
|
CChainAddr string // Ethereum-style 0x address
|
|
BLSPubKey string // Hex-encoded BLS public key
|
|
}
|
|
|
|
// PrintValidatorKeys prints validator key information in a formatted table
|
|
func PrintValidatorKeys(validators []ValidatorKeyInfo, networkHRP string) {
|
|
if len(validators) == 0 {
|
|
return
|
|
}
|
|
|
|
Logger.PrintToUser("\n๐ Validator Keys (derived from MNEMONIC):")
|
|
Logger.PrintToUser(" โโโโโโโโโคโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ")
|
|
Logger.PrintToUser(" โ # โ Validator Details โ")
|
|
Logger.PrintToUser(" โ โโโโโโโโชโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฃ")
|
|
|
|
for _, v := range validators {
|
|
Logger.PrintToUser(" โ %d โ NodeID: %s", v.Index, v.NodeID)
|
|
Logger.PrintToUser(" โ โ P-Chain: %s", v.PChainAddr)
|
|
Logger.PrintToUser(" โ โ X-Chain: %s", v.XChainAddr)
|
|
Logger.PrintToUser(" โ โ C-Chain: %s", v.CChainAddr)
|
|
if v.Index < len(validators)-1 {
|
|
Logger.PrintToUser(" โโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโข")
|
|
}
|
|
}
|
|
Logger.PrintToUser(" โโโโโโโโโงโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ")
|
|
}
|
|
|
|
// PrintValidatorKeysCompact prints validator keys in a compact single-line format
|
|
func PrintValidatorKeysCompact(validators []ValidatorKeyInfo) {
|
|
if len(validators) == 0 {
|
|
return
|
|
}
|
|
|
|
Logger.PrintToUser("\n๐ Validator Keys (from MNEMONIC):")
|
|
for _, v := range validators {
|
|
Logger.PrintToUser(" [%d] %s | C: %s", v.Index, v.NodeID, v.CChainAddr)
|
|
}
|
|
}
|