44package chaincmd
55
66import (
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
3543var (
3947 deployDevnet bool
4048 nodeVersion string
4149 deployTimeout time.Duration
50+ deployKeyName string
4251)
4352
4453func 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
7688OPTIONS:
7789
7890 --node-version Specific luxd version to use (default: latest)
91+ --key Key name for remote network deployment (from ~/.lux/keys/)
7992
8093EXAMPLES:
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
97115DEPLOYMENT 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
106131OUTPUT:
107132
108133 On success, displays:
109134 - Blockchain ID
110135 - Chain ID
111- - RPC endpoints for each validator node
136+ - RPC endpoints
112137
113138TROUBLESHOOTING:
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+
286346func 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\n Is 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\n Is 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 \n To 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+
416609func checkDeployCompatibility (network localnetworkinterface.StatusChecker , configuredRPCVersion int ) (string , error ) {
417610 runningVersion , runningRPCVersion , networkRunning , err := network .GetCurrentNetworkVersion ()
418611 if err != nil {
0 commit comments