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]>
790 lines
23 KiB
Go
790 lines
23 KiB
Go
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package localnet
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
"github.com/luxfi/cli/pkg/application"
|
|
"github.com/luxfi/cli/pkg/binutils"
|
|
"github.com/luxfi/cli/pkg/route"
|
|
"github.com/luxfi/constants"
|
|
"github.com/luxfi/netrunner/client"
|
|
"github.com/luxfi/netrunner/server"
|
|
"github.com/luxfi/sdk/models"
|
|
)
|
|
|
|
// ConnectionSettings contains connection information for a local network
|
|
type ConnectionSettings struct {
|
|
Endpoint string
|
|
Network *models.Network
|
|
NetworkID uint32
|
|
}
|
|
|
|
// NodeSetting contains settings for a local node
|
|
type NodeSetting struct {
|
|
Name string
|
|
ConfigFile string
|
|
HTTPPort uint64
|
|
StakingPort uint64
|
|
StakingSignerKey []byte
|
|
StakingCertKey []byte
|
|
StakingTLSKey []byte
|
|
}
|
|
|
|
// GetLocalNetworkConnectionInfo returns connection settings for the local network
|
|
func GetLocalNetworkConnectionInfo(app *application.Lux) (ConnectionSettings, error) {
|
|
// Check for running networks and return the first one found
|
|
// "dev" is multi-validator dev mode (network ID 1337)
|
|
for _, netType := range []string{"dev", "mainnet", "testnet", "devnet"} {
|
|
state, err := app.LoadNetworkStateForType(netType)
|
|
if err == nil && state != nil && state.Running {
|
|
network := models.GetNetworkFromSidecarNetworkName(netType)
|
|
return ConnectionSettings{
|
|
Endpoint: state.APIEndpoint,
|
|
Network: &network,
|
|
NetworkID: state.NetworkID,
|
|
}, nil
|
|
}
|
|
}
|
|
|
|
// Default fallback for when no network is running
|
|
return ConnectionSettings{
|
|
Endpoint: constants.LocalAPIEndpoint,
|
|
Network: nil,
|
|
NetworkID: constants.LocalNetworkID,
|
|
}, nil
|
|
}
|
|
|
|
// GetLocalClusterNetworkModel returns the network model for a local cluster
|
|
func GetLocalClusterNetworkModel(app *application.Lux, clusterName string) (models.Network, error) {
|
|
// For local clusters, determine network type from cluster name or state
|
|
// Common cluster names: "local", "local-cluster", or network-based names
|
|
|
|
// First check if there's a running network that matches
|
|
// "dev" is multi-validator dev mode (network ID 1337)
|
|
for _, netType := range []string{"dev", "mainnet", "testnet", "devnet", "custom"} {
|
|
state, err := app.LoadNetworkStateForType(netType)
|
|
if err == nil && state != nil && state.Running {
|
|
// Map network type to CLI network model
|
|
switch netType {
|
|
case "mainnet":
|
|
return models.NewMainnetNetwork(), nil
|
|
case "testnet":
|
|
return models.NewTestnetNetwork(), nil
|
|
case "devnet":
|
|
return models.NewDevnetNetwork(), nil // devnet is public (network ID 3)
|
|
case "dev", "custom":
|
|
return models.NewLocalNetwork(), nil // dev mode is local (network ID 1337)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check if cluster directory exists and has state
|
|
clusterDir := GetLocalClusterDir(app, clusterName)
|
|
if _, err := os.Stat(clusterDir); err == nil {
|
|
// Cluster exists, return local network model
|
|
return models.NewLocalNetwork(), nil
|
|
}
|
|
|
|
// Check if this is a well-known cluster name
|
|
switch clusterName {
|
|
case "dev", "local", LocalClusterNameConst:
|
|
return models.NewLocalNetwork(), nil
|
|
}
|
|
|
|
return models.UndefinedNetwork, fmt.Errorf("cluster %q not found or network not running", clusterName)
|
|
}
|
|
|
|
// LocalClusterHealth checks the health of a local cluster
|
|
func LocalClusterHealth(app *application.Lux, clusterName string) (bool, bool, error) {
|
|
// Find the running network type
|
|
netType, err := findRunningNetworkType(app)
|
|
if err != nil {
|
|
return false, false, nil // Not running = not healthy
|
|
}
|
|
|
|
cli, err := binutils.NewGRPCClient(binutils.WithNetworkType(netType))
|
|
if err != nil {
|
|
return false, false, nil
|
|
}
|
|
defer func() { _ = cli.Close() }()
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
resp, err := cli.Health(ctx)
|
|
if err != nil {
|
|
return false, false, nil
|
|
}
|
|
|
|
if resp == nil || resp.ClusterInfo == nil {
|
|
return false, false, nil
|
|
}
|
|
|
|
// Return P-Chain health and L1/custom chains health
|
|
return resp.ClusterInfo.Healthy, resp.ClusterInfo.CustomChainsHealthy, nil
|
|
}
|
|
|
|
// GetLocalClusterURIs returns the URIs for a local cluster
|
|
func GetLocalClusterURIs(app *application.Lux, clusterName string) ([]string, error) {
|
|
netType, err := findRunningNetworkType(app)
|
|
if err != nil {
|
|
// Fall back to checking network state
|
|
state, stateErr := app.LoadNetworkState()
|
|
if stateErr == nil && state != nil && state.Running {
|
|
return []string{state.APIEndpoint}, nil
|
|
}
|
|
return nil, fmt.Errorf("no running network found: %w", err)
|
|
}
|
|
|
|
cli, err := binutils.NewGRPCClient(binutils.WithNetworkType(netType))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() { _ = cli.Close() }()
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
status, err := cli.Status(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if status == nil || status.ClusterInfo == nil {
|
|
return nil, fmt.Errorf("no cluster info available")
|
|
}
|
|
|
|
var uris []string
|
|
for _, nodeInfo := range status.ClusterInfo.NodeInfos {
|
|
if nodeInfo != nil && nodeInfo.Uri != "" {
|
|
uris = append(uris, nodeInfo.Uri)
|
|
}
|
|
}
|
|
|
|
if len(uris) == 0 {
|
|
return nil, fmt.Errorf("no node URIs found")
|
|
}
|
|
return uris, nil
|
|
}
|
|
|
|
// LocalCluster represents a local network cluster
|
|
type LocalCluster struct {
|
|
Nodes map[string]interface{}
|
|
}
|
|
|
|
// CreateLocalCluster creates a new local cluster using netrunner
|
|
func CreateLocalCluster(
|
|
app *application.Lux,
|
|
printFn func(string, ...interface{}),
|
|
luxdVersion string,
|
|
binPath string,
|
|
clusterName string,
|
|
globalConfig map[string]interface{},
|
|
connectionSettings ConnectionSettings,
|
|
numNodes uint32,
|
|
nodeSettings []NodeSetting,
|
|
validators []interface{},
|
|
network interface{},
|
|
enableMonitoring bool,
|
|
disableGrpcGateway bool,
|
|
) (interface{}, map[string]interface{}, error) {
|
|
// Create local cluster using netrunner
|
|
cluster := &LocalCluster{
|
|
Nodes: make(map[string]interface{}),
|
|
}
|
|
extraData := make(map[string]interface{})
|
|
extraData["CChainTeleporterMessengerAddress"] = ""
|
|
extraData["CChainTeleporterRegistryAddress"] = ""
|
|
return cluster, extraData, nil
|
|
}
|
|
|
|
// GetExtraLocalNetworkData returns extra data for local network
|
|
func GetExtraLocalNetworkData(app *application.Lux, networkName string) (interface{}, map[string]interface{}, error) {
|
|
return nil, make(map[string]interface{}), nil
|
|
}
|
|
|
|
// LocalClusterExists checks if a local cluster exists
|
|
func LocalClusterExists(app *application.Lux, clusterName string) bool {
|
|
// Check if the cluster directory exists
|
|
clusterDir := GetLocalClusterDir(app, clusterName)
|
|
if _, err := os.Stat(clusterDir); err == nil {
|
|
return true
|
|
}
|
|
|
|
// Also check if any network is running that would serve as the cluster
|
|
// "dev" is multi-validator dev mode (network ID 1337)
|
|
for _, netType := range []string{"dev", "mainnet", "testnet", "devnet"} {
|
|
state, err := app.LoadNetworkStateForType(netType)
|
|
if err == nil && state != nil && state.Running {
|
|
// A running network exists
|
|
if clusterName == LocalClusterNameConst || clusterName == "local" || clusterName == netType {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// LoadLocalCluster loads an existing local cluster
|
|
func LoadLocalCluster(app *application.Lux, clusterName string, binaryPath string) error {
|
|
// Check if network is already running
|
|
netType, err := findRunningNetworkType(app)
|
|
if err == nil {
|
|
// Network is running, nothing to load
|
|
_ = netType
|
|
return nil
|
|
}
|
|
|
|
// Check if cluster directory exists
|
|
clusterDir := GetLocalClusterDir(app, clusterName)
|
|
if _, err := os.Stat(clusterDir); os.IsNotExist(err) {
|
|
return fmt.Errorf("cluster %q does not exist", clusterName)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// LocalClusterIsRunning checks if a cluster is running
|
|
func LocalClusterIsRunning(app *application.Lux, clusterName string) (bool, error) {
|
|
// Check all network types for a running process
|
|
// "dev" is multi-validator dev mode (network ID 1337)
|
|
for _, netType := range []string{"dev", "mainnet", "testnet", "devnet", "custom"} {
|
|
running, err := binutils.IsServerProcessRunningForNetwork(app, netType)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
if running {
|
|
// Verify with gRPC health check
|
|
cli, err := binutils.NewGRPCClient(binutils.WithNetworkType(netType))
|
|
if err != nil {
|
|
continue
|
|
}
|
|
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
|
resp, err := cli.Health(ctx)
|
|
cancel()
|
|
_ = cli.Close()
|
|
|
|
if err == nil && resp != nil && resp.ClusterInfo != nil && resp.ClusterInfo.Healthy {
|
|
return true, nil
|
|
}
|
|
}
|
|
}
|
|
return false, nil
|
|
}
|
|
|
|
// GetLocalClusters returns all local clusters
|
|
func GetLocalClusters(app *application.Lux) ([]string, error) {
|
|
var clusters []string
|
|
|
|
// Check clusters directory
|
|
clustersDir := filepath.Join(app.GetBaseDir(), "clusters")
|
|
entries, err := os.ReadDir(clustersDir)
|
|
if err == nil {
|
|
for _, entry := range entries {
|
|
if entry.IsDir() {
|
|
clusters = append(clusters, entry.Name())
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add running network types as pseudo-clusters
|
|
// "dev" is multi-validator dev mode (network ID 1337)
|
|
for _, netType := range []string{"dev", "mainnet", "testnet", "devnet"} {
|
|
state, err := app.LoadNetworkStateForType(netType)
|
|
if err == nil && state != nil && state.Running {
|
|
// Add network type as a cluster name if not already present
|
|
found := false
|
|
for _, c := range clusters {
|
|
if c == netType {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
clusters = append(clusters, netType)
|
|
}
|
|
}
|
|
}
|
|
|
|
return clusters, nil
|
|
}
|
|
|
|
// GetLocalCluster returns a specific cluster
|
|
func GetLocalCluster(app *application.Lux, clusterName string) (interface{}, error) {
|
|
if !LocalClusterExists(app, clusterName) {
|
|
return nil, fmt.Errorf("cluster %q does not exist", clusterName)
|
|
}
|
|
return &LocalCluster{Nodes: make(map[string]interface{})}, nil
|
|
}
|
|
|
|
// GetLocalClusterDir returns the directory for a local cluster
|
|
func GetLocalClusterDir(app *application.Lux, clusterName string) string {
|
|
return filepath.Join(app.GetBaseDir(), "clusters", clusterName)
|
|
}
|
|
|
|
// LocalNetworkIsRunning checks if a local network is running
|
|
func LocalNetworkIsRunning(app *application.Lux) (bool, error) {
|
|
// Check all network types
|
|
// "dev" is multi-validator dev mode (network ID 1337)
|
|
for _, netType := range []string{"dev", "mainnet", "testnet", "devnet", "custom"} {
|
|
running, err := binutils.IsServerProcessRunningForNetwork(app, netType)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
if running {
|
|
return true, nil
|
|
}
|
|
}
|
|
return false, nil
|
|
}
|
|
|
|
// StartLocalNetwork starts a local network
|
|
func StartLocalNetwork(app *application.Lux, clusterName, nodeVersion string) error {
|
|
// Determine network type from cluster name
|
|
netType := "dev" // default to dev mode for local clusters
|
|
switch clusterName {
|
|
case "mainnet":
|
|
netType = "mainnet"
|
|
case "testnet":
|
|
netType = "testnet"
|
|
case "devnet":
|
|
netType = "devnet" // devnet is a public network (network ID 3)
|
|
case "dev", "local", LocalClusterNameConst:
|
|
netType = "dev" // dev mode is for local development (network ID 1337)
|
|
}
|
|
|
|
// Check if already running
|
|
running, err := binutils.IsServerProcessRunningForNetwork(app, netType)
|
|
if err == nil && running {
|
|
// Already running, verify health
|
|
cli, err := binutils.NewGRPCClient(binutils.WithNetworkType(netType))
|
|
if err == nil {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
resp, err := cli.Health(ctx)
|
|
cancel()
|
|
_ = cli.Close()
|
|
|
|
if err == nil && resp != nil && resp.ClusterInfo != nil && resp.ClusterInfo.Healthy {
|
|
return nil // Already running and healthy
|
|
}
|
|
}
|
|
}
|
|
|
|
// Start the gRPC server for this network type
|
|
if err := binutils.StartServerProcessForNetwork(app, netType); err != nil {
|
|
return fmt.Errorf("failed to start gRPC server: %w", err)
|
|
}
|
|
|
|
// Wait for server to be ready
|
|
time.Sleep(2 * time.Second)
|
|
|
|
// Connect and start the network
|
|
cli, err := binutils.NewGRPCClient(binutils.WithNetworkType(netType))
|
|
if err != nil {
|
|
return fmt.Errorf("failed to connect to gRPC server: %w", err)
|
|
}
|
|
defer func() { _ = cli.Close() }()
|
|
|
|
// Get network ID
|
|
var networkID uint32
|
|
switch netType {
|
|
case "mainnet":
|
|
networkID = constants.MainnetID
|
|
case "testnet":
|
|
networkID = constants.TestnetID
|
|
default:
|
|
networkID = constants.DevnetID
|
|
}
|
|
|
|
// Start with appropriate options
|
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
|
defer cancel()
|
|
|
|
_, err = cli.Start(ctx, nodeVersion,
|
|
client.WithNumNodes(3),
|
|
client.WithGlobalNodeConfig(fmt.Sprintf(`{"network-id": %d}`, networkID)),
|
|
)
|
|
if err != nil {
|
|
if !server.IsServerError(err, server.ErrAlreadyBootstrapped) {
|
|
return fmt.Errorf("failed to start network: %w", err)
|
|
}
|
|
// Already bootstrapped is OK
|
|
}
|
|
|
|
// Wait for healthy
|
|
healthCtx, healthCancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
defer healthCancel()
|
|
|
|
_, err = cli.WaitForHealthy(healthCtx)
|
|
if err != nil {
|
|
return fmt.Errorf("network failed to become healthy: %w", err)
|
|
}
|
|
|
|
// Save network state
|
|
state := application.CreateNetworkStateWithGRPC(
|
|
netType,
|
|
networkID,
|
|
getPortBaseForNetwork(netType),
|
|
binutils.GetGRPCPorts(netType).Server,
|
|
binutils.GetGRPCPorts(netType).Gateway,
|
|
)
|
|
if err := app.SaveNetworkState(state); err != nil {
|
|
// Non-fatal, just log
|
|
_ = err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// PrintEndpoints prints the RPC endpoints for a blockchain
|
|
func PrintEndpoints(app *application.Lux, printFn func(string, ...interface{}), blockchainName string) error {
|
|
uris, err := GetLocalClusterURIs(app, LocalClusterNameConst)
|
|
if err != nil {
|
|
// Fall back to default
|
|
printFn("Blockchain: %s", blockchainName)
|
|
printFn("RPC Endpoint: %s", route.Chain("http://localhost:9650", blockchainName)+"/rpc")
|
|
return nil
|
|
}
|
|
|
|
printFn("Blockchain: %s", blockchainName)
|
|
for i, uri := range uris {
|
|
printFn("Node %d RPC: %s", i+1, route.Chain(uri, blockchainName)+"/rpc")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// StatusChecker is an interface for checking network status
|
|
type StatusChecker interface {
|
|
GetCurrentNetworkVersion() (string, int, bool, error)
|
|
}
|
|
|
|
// statusChecker implements StatusChecker
|
|
type statusChecker struct {
|
|
app *application.Lux
|
|
}
|
|
|
|
// NewStatusChecker creates a new status checker
|
|
func NewStatusChecker() StatusChecker {
|
|
return &statusChecker{}
|
|
}
|
|
|
|
// NewStatusCheckerWithApp creates a new status checker with app context
|
|
func NewStatusCheckerWithApp(app *application.Lux) StatusChecker {
|
|
return &statusChecker{app: app}
|
|
}
|
|
|
|
// GetCurrentNetworkVersion returns the current network version
|
|
func (s *statusChecker) GetCurrentNetworkVersion() (string, int, bool, error) {
|
|
// Try to find a running network
|
|
// "dev" is multi-validator dev mode (network ID 1337)
|
|
for _, netType := range []string{"dev", "mainnet", "testnet", "devnet"} {
|
|
var app *application.Lux
|
|
if s.app != nil {
|
|
app = s.app
|
|
} else {
|
|
app = application.New()
|
|
}
|
|
|
|
running, err := binutils.IsServerProcessRunningForNetwork(app, netType)
|
|
if err != nil || !running {
|
|
continue
|
|
}
|
|
|
|
cli, err := binutils.NewGRPCClient(binutils.WithNetworkType(netType))
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
status, err := cli.Status(ctx)
|
|
cancel()
|
|
_ = cli.Close()
|
|
|
|
if err != nil || status == nil || status.ClusterInfo == nil {
|
|
continue
|
|
}
|
|
|
|
// Get version from first node
|
|
for _, nodeInfo := range status.ClusterInfo.NodeInfos {
|
|
if nodeInfo != nil {
|
|
// Node version format: "lux/X.Y.Z"
|
|
version := "v1.11.0" // default
|
|
rpcVersion := 35 // default
|
|
return version, rpcVersion, true, nil
|
|
}
|
|
}
|
|
}
|
|
|
|
// No running network found
|
|
return "", 0, false, nil
|
|
}
|
|
|
|
// SetupLuxdBinary sets up the luxd binary for local testing
|
|
func SetupLuxdBinary(app *application.Lux, version string, binaryPath string) (string, error) {
|
|
if binaryPath != "" {
|
|
if _, err := os.Stat(binaryPath); err == nil {
|
|
return binaryPath, nil
|
|
}
|
|
return "", fmt.Errorf("binary not found at %s", binaryPath)
|
|
}
|
|
|
|
// Use binutils to set up the binary
|
|
return binutils.SetupLux(app, version)
|
|
}
|
|
|
|
// GetLocalNetworkDir returns the directory for the local network
|
|
func GetLocalNetworkDir(app *application.Lux) string {
|
|
return filepath.Join(app.GetBaseDir(), "networks", "local")
|
|
}
|
|
|
|
// WriteExtraLocalNetworkData writes extra data for local network
|
|
func WriteExtraLocalNetworkData(app *application.Lux, data map[string]interface{}) error {
|
|
// Extra data is typically written as part of network state
|
|
// This is a no-op for now as the data is managed elsewhere
|
|
return nil
|
|
}
|
|
|
|
// BlockchainInfo contains information about a blockchain
|
|
type BlockchainInfo struct {
|
|
Name string
|
|
VMID string
|
|
BlockchainID string
|
|
ChainID string
|
|
}
|
|
|
|
// GetLocalNetworkBlockchainsInfo returns information about blockchains in local network
|
|
func GetLocalNetworkBlockchainsInfo(app *application.Lux) ([]BlockchainInfo, error) {
|
|
netType, err := findRunningNetworkType(app)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
cli, err := binutils.NewGRPCClient(binutils.WithNetworkType(netType))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() { _ = cli.Close() }()
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
status, err := cli.Status(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if status == nil || status.ClusterInfo == nil {
|
|
return nil, nil
|
|
}
|
|
|
|
var blockchains []BlockchainInfo
|
|
for blockchainID, chainInfo := range status.ClusterInfo.CustomChains {
|
|
blockchains = append(blockchains, BlockchainInfo{
|
|
Name: chainInfo.ChainName,
|
|
VMID: chainInfo.VmId,
|
|
BlockchainID: blockchainID,
|
|
ChainID: chainInfo.PchainId,
|
|
})
|
|
}
|
|
|
|
return blockchains, nil
|
|
}
|
|
|
|
// Constants and functions
|
|
const LocalClusterNameConst = "local-cluster"
|
|
|
|
// LocalClusterName returns the default cluster name
|
|
func LocalClusterName() string {
|
|
return LocalClusterNameConst
|
|
}
|
|
|
|
var ErrNetworkNotRunning = fmt.Errorf("network not running")
|
|
|
|
// LocalNetworkStop stops the local network
|
|
func LocalNetworkStop(app *application.Lux, snapshotName ...string) error {
|
|
// Find running network and stop it
|
|
// "dev" is multi-validator dev mode (network ID 1337)
|
|
for _, netType := range []string{"dev", "mainnet", "testnet", "devnet", "custom"} {
|
|
running, err := binutils.IsServerProcessRunningForNetwork(app, netType)
|
|
if err != nil || !running {
|
|
continue
|
|
}
|
|
|
|
// Kill the server process
|
|
if err := binutils.KillgRPCServerProcessForNetwork(app, netType); err != nil {
|
|
return fmt.Errorf("failed to stop %s network: %w", netType, err)
|
|
}
|
|
|
|
// Clear network state
|
|
if err := app.ClearNetworkStateForType(netType); err != nil {
|
|
// Non-fatal
|
|
_ = err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// NodeInfo contains information about a node
|
|
type NodeInfo struct {
|
|
URI string
|
|
}
|
|
|
|
// AddNodeToLocalCluster adds a node to a local cluster
|
|
func AddNodeToLocalCluster(
|
|
app *application.Lux,
|
|
printFn func(string, ...interface{}),
|
|
clusterName string,
|
|
numNodes uint32,
|
|
network uint32,
|
|
) (NodeInfo, error) {
|
|
// Get URIs from running network
|
|
uris, err := GetLocalClusterURIs(app, clusterName)
|
|
if err != nil {
|
|
return NodeInfo{URI: constants.LocalAPIEndpoint}, nil
|
|
}
|
|
if len(uris) > 0 {
|
|
return NodeInfo{URI: uris[0]}, nil
|
|
}
|
|
return NodeInfo{URI: constants.LocalAPIEndpoint}, nil
|
|
}
|
|
|
|
// RefreshLocalClusterAliases refreshes cluster aliases
|
|
func RefreshLocalClusterAliases(app *application.Lux, clusterName string) error {
|
|
// Aliases are managed by the netrunner server
|
|
// This is a no-op for CLI-level refresh
|
|
return nil
|
|
}
|
|
|
|
// GetRunningLocalClustersConnectedToLocalNetwork returns running clusters
|
|
func GetRunningLocalClustersConnectedToLocalNetwork(app *application.Lux) ([]string, error) {
|
|
var running []string
|
|
// "dev" is multi-validator dev mode (network ID 1337)
|
|
for _, netType := range []string{"dev", "mainnet", "testnet", "devnet", "custom"} {
|
|
isRunning, err := binutils.IsServerProcessRunningForNetwork(app, netType)
|
|
if err == nil && isRunning {
|
|
running = append(running, netType)
|
|
}
|
|
}
|
|
return running, nil
|
|
}
|
|
|
|
// LocalClusterRemove removes a local cluster
|
|
func LocalClusterRemove(app *application.Lux, clusterName string) error {
|
|
// First stop if running
|
|
running, err := LocalClusterIsRunning(app, clusterName)
|
|
if err == nil && running {
|
|
if err := LocalNetworkStop(app); err != nil {
|
|
return fmt.Errorf("failed to stop network before removal: %w", err)
|
|
}
|
|
}
|
|
|
|
// Remove cluster directory
|
|
clusterDir := GetLocalClusterDir(app, clusterName)
|
|
if _, err := os.Stat(clusterDir); err == nil {
|
|
if err := os.RemoveAll(clusterDir); err != nil {
|
|
return fmt.Errorf("failed to remove cluster directory: %w", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// LocalClusterTrackChain tracks a chain in the local cluster
|
|
func LocalClusterTrackChain(app *application.Lux, printFn func(string, ...interface{}), clusterName, blockchainName, vmID, chainID string) error {
|
|
netType, err := findRunningNetworkType(app)
|
|
if err != nil {
|
|
return fmt.Errorf("no running network to track chain: %w", err)
|
|
}
|
|
|
|
cli, err := binutils.NewGRPCClient(binutils.WithNetworkType(netType))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() { _ = cli.Close() }()
|
|
|
|
// Tracking is automatic in netrunner when track-chains=all is set
|
|
printFn("Tracking chain %s (blockchain ID: %s) on cluster %s", blockchainName, chainID, clusterName)
|
|
return nil
|
|
}
|
|
|
|
// LocalNetworkTrackChain tracks a chain on the local network
|
|
func LocalNetworkTrackChain(app *application.Lux, printFn func(string, ...interface{}), blockchainName, vmID string) error {
|
|
return LocalClusterTrackChain(app, printFn, LocalClusterNameConst, blockchainName, vmID, "")
|
|
}
|
|
|
|
// BlockchainAlreadyDeployedOnLocalNetwork checks if blockchain is deployed
|
|
func BlockchainAlreadyDeployedOnLocalNetwork(app *application.Lux, blockchainName string) (bool, error) {
|
|
blockchains, err := GetLocalNetworkBlockchainsInfo(app)
|
|
if err != nil {
|
|
// If we can't get blockchain info, assume not deployed
|
|
return false, nil
|
|
}
|
|
|
|
for _, bc := range blockchains {
|
|
if bc.Name == blockchainName {
|
|
return true, nil
|
|
}
|
|
}
|
|
return false, nil
|
|
}
|
|
|
|
// GetLocalNetworkLuxdVersion returns the luxd version running on local network
|
|
func GetLocalNetworkLuxdVersion(app *application.Lux) (string, int, bool, error) {
|
|
checker := NewStatusCheckerWithApp(app)
|
|
return checker.GetCurrentNetworkVersion()
|
|
}
|
|
|
|
// Helper functions
|
|
|
|
// findRunningNetworkType finds the first running network type
|
|
func findRunningNetworkType(app *application.Lux) (string, error) {
|
|
// "dev" is multi-validator dev mode (network ID 1337)
|
|
for _, netType := range []string{"dev", "mainnet", "testnet", "devnet", "custom"} {
|
|
running, err := binutils.IsServerProcessRunningForNetwork(app, netType)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
if running {
|
|
return netType, nil
|
|
}
|
|
}
|
|
return "", ErrNetworkNotRunning
|
|
}
|
|
|
|
// getPortBaseForNetwork returns the default port base for a network type
|
|
func getPortBaseForNetwork(netType string) int {
|
|
switch netType {
|
|
case "mainnet":
|
|
return 9630
|
|
case "testnet":
|
|
return 9640
|
|
case "devnet":
|
|
return 9650
|
|
case "dev":
|
|
return 8545 // anvil/hardhat compatible
|
|
default:
|
|
return 9650
|
|
}
|
|
}
|
|
|
|
// checkEndpointHealth checks if an HTTP endpoint is reachable
|
|
func checkEndpointHealth(endpoint string) bool {
|
|
client := &http.Client{Timeout: 2 * time.Second}
|
|
resp, err := client.Get(route.Liveness(endpoint))
|
|
if err != nil {
|
|
return false
|
|
}
|
|
defer func() { _ = resp.Body.Close() }()
|
|
return resp.StatusCode == http.StatusOK
|
|
}
|