Skip to content

Commit 585ca4d

Browse files
committed
feat: remote network deploy support (devnet/testnet/mainnet via HTTPS API)
1 parent 1db57a4 commit 585ca4d

7 files changed

Lines changed: 262 additions & 16 deletions

File tree

cmd/chaincmd/deploy.go

Lines changed: 203 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,21 @@
44
package chaincmd
55

66
import (
7+
"context"
78
"encoding/json"
89
"errors"
910
"fmt"
11+
"net/http"
1012
"os"
1113
"path/filepath"
14+
"strings"
1215
"time"
1316

17+
"github.com/luxfi/cli/pkg/application"
1418
"github.com/luxfi/cli/pkg/binutils"
1519
"github.com/luxfi/cli/pkg/chain"
20+
"github.com/luxfi/cli/pkg/key"
21+
"github.com/luxfi/cli/pkg/keychain"
1622
"github.com/luxfi/cli/pkg/localnetworkinterface"
1723
"github.com/luxfi/cli/pkg/utils"
1824
"github.com/luxfi/cli/pkg/ux"
@@ -30,6 +36,8 @@ const (
3036
MaxConsecutiveHealthFailures = 10
3137
// LuxEVMName is the canonical name for the Lux EVM
3238
LuxEVMName = "Lux EVM"
39+
// RemoteProbeTimeout is the timeout for probing a remote network endpoint
40+
RemoteProbeTimeout = 10 * time.Second
3341
)
3442

3543
var (
@@ -39,6 +47,7 @@ var (
3947
deployDevnet bool
4048
nodeVersion string
4149
deployTimeout time.Duration
50+
deployKeyName string
4251
)
4352

4453
func newDeployCmd() *cobra.Command {
@@ -67,21 +76,30 @@ PREREQUISITES:
6776
1. Chain must be created:
6877
lux chain create mychain
6978
70-
2. Network must be running:
79+
2. For local networks, network must be running:
7180
lux network start --devnet
7281
73-
3. VM must be installed (for custom VMs):
82+
3. For remote networks (devnet, testnet, mainnet), a funded key is needed:
83+
Set LUX_MNEMONIC or LUX_PRIVATE_KEY env var, or use --key flag
84+
85+
4. VM must be installed (for custom VMs):
7486
lux vm link "Lux EVM" --path ~/work/lux/evm/build/evm
7587
7688
OPTIONS:
7789
7890
--node-version Specific luxd version to use (default: latest)
91+
--key Key name for remote network deployment (from ~/.lux/keys/)
7992
8093
EXAMPLES:
8194
82-
# Deploy to local devnet (most common)
95+
# Deploy to remote devnet (auto-detects remote endpoint)
96+
lux chain deploy mychain --devnet
97+
98+
# Deploy to remote devnet with specific key
99+
lux chain deploy mychain --devnet --key mykey
100+
101+
# Deploy to local devnet (if local network is running)
83102
lux chain deploy mychain --devnet
84-
lux chain deploy mychain -d
85103
86104
# Deploy to testnet
87105
lux chain deploy mychain --testnet
@@ -96,19 +114,26 @@ EXAMPLES:
96114
97115
DEPLOYMENT PROCESS:
98116
117+
Local network:
99118
1. Validates chain configuration exists
100-
2. Verifies network is running
119+
2. Verifies local gRPC network is running
101120
3. Checks VM plugin is installed
102-
4. Creates blockchain on the network
103-
5. Updates sidecar with deployment info (chain ID, blockchain ID)
104-
6. Returns endpoints for the deployed chain
121+
4. Creates blockchain via netrunner gRPC
122+
5. Updates sidecar with deployment info
123+
124+
Remote network:
125+
1. Validates chain configuration exists
126+
2. Probes remote endpoint (e.g., https://api.lux-dev.network)
127+
3. Creates subnet on P-chain via wallet transaction
128+
4. Creates blockchain on P-chain via wallet transaction
129+
5. Updates sidecar with deployment info
105130
106131
OUTPUT:
107132
108133
On success, displays:
109134
- Blockchain ID
110135
- Chain ID
111-
- RPC endpoints for each validator node
136+
- RPC endpoints
112137
113138
TROUBLESHOOTING:
114139
@@ -140,6 +165,7 @@ NOTES:
140165
cmd.Flags().BoolVarP(&deployDevnet, "devnet", "d", false, "Deploy to devnet")
141166
cmd.Flags().StringVar(&nodeVersion, "node-version", "latest", "Node version to use")
142167
cmd.Flags().DurationVar(&deployTimeout, "timeout", DefaultDeployTimeout, "Maximum time to wait for chain deployment (e.g., 60s, 2m)")
168+
cmd.Flags().StringVar(&deployKeyName, "key", "", "Key name for remote network deployment (from ~/.lux/keys/)")
143169

144170
return cmd
145171
}
@@ -283,6 +309,40 @@ func getVMDisplayName(vm models.VMType) string {
283309
}
284310
}
285311

312+
// getRemoteEndpoint returns the well-known remote API endpoint for a network type.
313+
// Returns empty string for local/custom networks that have no remote endpoint.
314+
func getRemoteEndpoint(network models.Network) string {
315+
return network.Endpoint()
316+
}
317+
318+
// probeRemoteEndpoint checks if a remote network endpoint is alive by hitting /ext/info.
319+
// Returns true if the endpoint responds with HTTP 200.
320+
func probeRemoteEndpoint(endpoint string) bool {
321+
ctx, cancel := context.WithTimeout(context.Background(), RemoteProbeTimeout)
322+
defer cancel()
323+
324+
url := endpoint + "/ext/info"
325+
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, nil)
326+
if err != nil {
327+
return false
328+
}
329+
req.Header.Set("Content-Type", "application/json")
330+
331+
resp, err := http.DefaultClient.Do(req)
332+
if err != nil {
333+
return false
334+
}
335+
defer resp.Body.Close()
336+
337+
// Any response (even 4xx for missing method) means the node is alive
338+
return resp.StatusCode < 500
339+
}
340+
341+
// isRemoteCapableNetwork returns true if the network can be deployed to via remote P-chain API
342+
func isRemoteCapableNetwork(network models.Network) bool {
343+
return network == models.Devnet || network == models.Testnet || network == models.Mainnet
344+
}
345+
286346
func deployToNetwork(chainName string, chainGenesis []byte, sc *models.Sidecar, network models.Network) error {
287347
app.Log.Debug("Deploy to network", "network", network.String())
288348

@@ -304,8 +364,43 @@ func deployToNetwork(chainName string, chainGenesis []byte, sc *models.Sidecar,
304364
// Each network type (custom, testnet, mainnet) has its own state file
305365
networkState, stateErr := app.LoadNetworkStateForType(targetType)
306366
if stateErr != nil {
307-
return fmt.Errorf("failed to load network state: %w\nIs the network running? Start with: lux network start", stateErr)
367+
// State file read error (not just missing) - only fail if no remote fallback
368+
if !isRemoteCapableNetwork(network) {
369+
return fmt.Errorf("failed to load network state: %w\nIs the network running? Start with: lux network start", stateErr)
370+
}
371+
app.Log.Debug("Failed to load network state, will try remote endpoint", "error", stateErr)
372+
networkState = nil
373+
}
374+
375+
// For remote-capable networks, try the remote endpoint if:
376+
// 1. No local state exists, OR
377+
// 2. Local state exists but has a remote API endpoint (e.g., https://...), OR
378+
// 3. Local state claims running but the state file is stale (gRPC server dead)
379+
if isRemoteCapableNetwork(network) {
380+
useRemote := false
381+
var remoteEndpoint string
382+
383+
if networkState == nil || !networkState.Running {
384+
// No local state or not running -- try remote
385+
useRemote = true
386+
remoteEndpoint = getRemoteEndpoint(network)
387+
} else if networkState.APIEndpoint != "" && strings.HasPrefix(networkState.APIEndpoint, "https://") {
388+
// State file points to a remote endpoint already
389+
useRemote = true
390+
remoteEndpoint = networkState.APIEndpoint
391+
}
392+
393+
if useRemote && remoteEndpoint != "" {
394+
ux.Logger.PrintToUser("Probing remote %s endpoint: %s", targetType, remoteEndpoint)
395+
if probeRemoteEndpoint(remoteEndpoint) {
396+
ux.Logger.PrintToUser("Remote %s is alive at %s", targetType, remoteEndpoint)
397+
return deployToRemoteNetwork(chainName, chainGenesis, sc, network, remoteEndpoint)
398+
}
399+
ux.Logger.PrintToUser("Remote endpoint %s is not reachable", remoteEndpoint)
400+
}
308401
}
402+
403+
// Local network path - requires running gRPC netrunner
309404
if networkState == nil || !networkState.Running {
310405
startHint := "lux network start"
311406
switch targetType {
@@ -319,6 +414,11 @@ func deployToNetwork(chainName string, chainGenesis []byte, sc *models.Sidecar,
319414
return fmt.Errorf("no %s network running. Start the network first with: %s", targetType, startHint)
320415
}
321416

417+
return deployToLocalNetwork(chainName, chainGenesis, sc, network, networkState)
418+
}
419+
420+
// deployToLocalNetwork deploys a chain to a locally-running network managed by the CLI's gRPC netrunner.
421+
func deployToLocalNetwork(chainName string, chainGenesis []byte, sc *models.Sidecar, network models.Network, networkState *application.NetworkState) error {
322422
// Log gRPC port being used
323423
app.Log.Debug("Using gRPC port from network state", "port", networkState.GRPCPort, "network", networkState.NetworkType)
324424

@@ -413,6 +513,99 @@ func deployToNetwork(chainName string, chainGenesis []byte, sc *models.Sidecar,
413513
return nil
414514
}
415515

516+
// deployToRemoteNetwork deploys a chain to a remote network via P-chain API transactions.
517+
// This is used when no local gRPC netrunner is running but the remote network is reachable.
518+
func deployToRemoteNetwork(chainName string, chainGenesis []byte, sc *models.Sidecar, network models.Network, endpoint string) error {
519+
ux.Logger.PrintToUser("Deploying to remote %s via P-chain API at %s", network.String(), endpoint)
520+
521+
// Get keychain for signing P-chain transactions
522+
networkID := network.ID()
523+
kc, err := getDeployKeychain(network, networkID)
524+
if err != nil {
525+
return fmt.Errorf("failed to get keychain for deployment: %w\n\nTo fix, set LUX_MNEMONIC or LUX_PRIVATE_KEY env var, or use --key flag", err)
526+
}
527+
528+
// Show the deployer address
529+
addrs := kc.Keychain.Addresses().List()
530+
if len(addrs) == 0 {
531+
return fmt.Errorf("keychain has no addresses")
532+
}
533+
534+
// Create the public deployer
535+
deployer := chain.NewPublicDeployer(app, kc.UsesLedger, kc.Keychain, network)
536+
537+
// Step 1: Create subnet (P-chain transaction)
538+
ux.Logger.PrintToUser("Creating subnet on P-chain...")
539+
controlKeys, err := kc.PChainFormattedStrAddresses()
540+
if err != nil {
541+
return fmt.Errorf("failed to get P-chain addresses: %w", err)
542+
}
543+
ux.Logger.PrintToUser("Control keys: %v", controlKeys)
544+
545+
subnetID, err := deployer.DeployChain(controlKeys, uint32(len(controlKeys)))
546+
if err != nil {
547+
return fmt.Errorf("failed to create subnet: %w", err)
548+
}
549+
ux.Logger.PrintToUser("Subnet created: %s", subnetID.String())
550+
551+
// Step 2: Create blockchain (P-chain transaction)
552+
ux.Logger.PrintToUser("Creating blockchain on subnet %s...", subnetID.String())
553+
isFullySigned, blockchainID, _, _, err := deployer.DeployBlockchain(
554+
controlKeys,
555+
controlKeys,
556+
subnetID,
557+
chainName,
558+
chainGenesis,
559+
)
560+
if err != nil {
561+
return fmt.Errorf("failed to create blockchain: %w", err)
562+
}
563+
if !isFullySigned {
564+
return fmt.Errorf("blockchain transaction requires additional signatures (multisig not yet supported for remote deploy)")
565+
}
566+
567+
ux.Logger.PrintToUser("")
568+
ux.Logger.PrintToUser("Blockchain deployed successfully!")
569+
ux.Logger.PrintToUser(" Subnet ID: %s", subnetID.String())
570+
ux.Logger.PrintToUser(" Blockchain ID: %s", blockchainID.String())
571+
ux.Logger.PrintToUser(" RPC Endpoint: %s/ext/bc/%s/rpc", endpoint, blockchainID.String())
572+
ux.Logger.PrintToUser("")
573+
574+
// Update sidecar with deployment info
575+
if err := app.UpdateSidecarNetworks(sc, network, subnetID, blockchainID); err != nil {
576+
return fmt.Errorf("failed to update sidecar: %w", err)
577+
}
578+
return nil
579+
}
580+
581+
// getDeployKeychain obtains a keychain for remote network deployment.
582+
// Priority:
583+
// 1. --key flag (explicit key name)
584+
// 2. LUX_PRIVATE_KEY env var
585+
// 3. LUX_MNEMONIC env var
586+
// 4. Interactive prompt (if terminal available)
587+
func getDeployKeychain(network models.Network, networkID uint32) (*keychain.Keychain, error) {
588+
// If --key flag specified, use that key
589+
if deployKeyName != "" {
590+
return keychain.GetKeychain(app, true, false, nil, deployKeyName, network, 0)
591+
}
592+
593+
// Try environment variables (LUX_PRIVATE_KEY, LUX_MNEMONIC)
594+
sf, err := key.GetOrCreateLocalKey(networkID)
595+
if err == nil && sf != nil {
596+
kc := sf.KeyChain()
597+
wrappedKc := keychain.WrapSecp256k1fxKeychain(kc)
598+
pAddrs := sf.P()
599+
if len(pAddrs) > 0 {
600+
ux.Logger.PrintToUser("Using key with P-Chain address: %s", pAddrs[0])
601+
}
602+
return keychain.NewKeychain(network, wrappedKc, nil, nil), nil
603+
}
604+
605+
// Fall back to interactive prompt via GetKeychainFromCmdLineFlags
606+
return keychain.GetKeychainFromCmdLineFlags(app, "deploy chain to "+network.String(), network, "", false, false, nil, 0)
607+
}
608+
416609
func checkDeployCompatibility(network localnetworkinterface.StatusChecker, configuredRPCVersion int) (string, error) {
417610
runningVersion, runningRPCVersion, networkRunning, err := network.GetCurrentNetworkVersion()
418611
if err != nil {

cmd/primarycmd/add_validator.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -151,9 +151,9 @@ func addValidator(_ *cobra.Command, _ []string) error {
151151
}
152152
}
153153
case models.Mainnet:
154-
useLedger = true
155-
if keyName != "" {
156-
return ErrStoredKeyOnMainnet
154+
// Lux POA network: allow key-based mainnet operations
155+
if keyName == "" && !useLedger {
156+
useLedger = true
157157
}
158158
default:
159159
return errors.New("unsupported network")
@@ -258,6 +258,9 @@ func estimateAddValidatorFee(network models.Network) uint64 {
258258
const baseFee = 1_000_000 // 0.001 LUX base fee
259259
switch network.Kind() {
260260
case models.Mainnet:
261+
if keyName == "" && !useLedger {
262+
useLedger = true
263+
}
261264
return baseFee * 2 // Higher fee for mainnet
262265
case models.Testnet:
263266
return baseFee

go.mod

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ require (
1010
github.com/aws/aws-sdk-go-v2/service/ec2 v1.200.0
1111
github.com/chelnak/ysmrr v0.6.0
1212
github.com/go-git/go-git/v5 v5.16.4
13+
github.com/hanzoai/insights-go v1.8.2
1314
github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213
1415
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0
1516
github.com/luxfi/config v1.1.2
@@ -31,7 +32,6 @@ require (
3132
github.com/onsi/ginkgo/v2 v2.28.1
3233
github.com/onsi/gomega v1.39.1
3334
github.com/pborman/ansi v1.0.0
34-
github.com/hanzoai/insights-go v1.8.2
3535
github.com/schollz/progressbar/v3 v3.19.0
3636
github.com/shirou/gopsutil v3.21.11+incompatible
3737
github.com/spf13/afero v1.15.0 // indirect
@@ -313,3 +313,5 @@ require (
313313
k8s.io/apimachinery v0.35.1
314314
k8s.io/client-go v0.35.1
315315
)
316+
317+
replace github.com/hanzoai/insights-go => github.com/posthog/posthog-go v1.2.24

0 commit comments

Comments
 (0)