Files
zeekayandHanzo Dev 62d5f28c22 A chain address is built in one place
The node moved every chain route from /v1/bc/<alias> to /v1/chain/<alias>
and deleted the old prefix, so every chain call answered 404. The address
was spelled out at 112 sites across 34 files and constants.ChainAliasPrefix
was used at none of them, which is why nothing failed to compile when the
segment moved.

route.Chain(uri, alias) composes it now, and route.Alias reads one back,
replacing three hand-rolled parsers — one of which took the alias by
position in the split path and only worked because the new segment happens
to leave the component count unchanged.

TestChainAddressBuiltOnlyHere walks every .go file in the module and fails
on a chain address in a string literal, so the segment cannot drift back;
prose is free to name a route. TestGuardCatchesEachShape plants each
forbidden shape and requires the guard to find it.

The endpoint box and the eleven-chain table were the same list written
twice; there is one list, rendered once, measured rather than hand-padded.
The generated CLI reference and the faucet manifest compose through the
same function.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-30 15:05:42 -07:00

366 lines
10 KiB
Go

// Copyright (C) 2022-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package chaincmd
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strings"
"time"
"github.com/luxfi/cli/pkg/route"
"github.com/luxfi/cli/pkg/ux"
"github.com/luxfi/sdk/models"
"github.com/spf13/cobra"
)
var importRPC string
func newImportCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "import <chain> <path>",
Short: "Import blocks from RLP file to a running chain",
Long: `Import blocks from an RLP-encoded file to a running chain.
OVERVIEW:
Imports historical blockchain data from RLP files into a running chain.
This is useful for bootstrapping chains with existing state or syncing
from canonical snapshots.
Uses the admin_importChain RPC method. The network must be running and
the admin API must be enabled (default when started via CLI).
CHAIN IDENTIFIERS:
c, C C-Chain (primary EVM chain)
<name> Chain name (looks up blockchain ID from sidecar)
<blockchainID> Direct blockchain ID
NETWORK FLAGS (auto-detects port):
--mainnet, -m Import to mainnet chain (port 9630)
--testnet, -t Import to testnet chain (port 9640)
--devnet, -d Import to devnet chain (port 9650)
Default: auto-detects running network or uses custom (port 9660)
OPTIONS:
--rpc <url> Custom RPC endpoint (overrides network flag)
PREREQUISITES:
1. Network must be running:
lux network start --mainnet
2. RLP file must exist and be readable by the node
EXAMPLES:
# Import C-Chain mainnet blocks
lux chain import c ~/work/lux/state/rlp/lux-mainnet-96369.rlp --mainnet
# Import to custom chain on devnet
lux chain import zoo ~/work/lux/state/rlp/zoo-mainnet-200200.rlp --devnet
# Import with custom RPC endpoint
lux chain import c blocks.rlp --rpc ` + route.Chain("http://localhost:9630", "C") + `/rpc
# Import to blockchain by ID
lux chain import 2ebCneCbwthjQ1rYT41nhd7M76Hc6YmosMAQrTFhBq8qeqh6tt blocks.rlp --mainnet
RLP FILE LOCATIONS:
Canonical RLP files are stored in:
~/work/lux/state/rlp/<network>/<chain>-<chainid>.rlp
Examples:
~/work/lux/state/rlp/lux-mainnet/lux-mainnet-96369.rlp
~/work/lux/state/rlp/zoo-mainnet/zoo-mainnet-200200.rlp
IMPORT PROCESS:
1. Validates file exists
2. Detects or connects to RPC endpoint
3. Gets current block height
4. Calls admin_importChain with file path
5. Monitors import progress
6. Reports final block height and import rate
OUTPUT:
Import complete!
Blocks imported: 1082780
Final height: 1082780
Time: 45m12s
Rate: 399.2 blocks/sec
TROUBLESHOOTING:
"Network not running" → Start network first:
lux network start --mainnet
"RPC connection refused" → Check network is running:
lux network status
"File not found" → Use absolute path or verify file exists
"Import timeout" → Import continues in background, check node logs
NOTES:
- Import runs asynchronously - RPC may timeout but import continues
- Large imports (1M+ blocks) can take 30min - 2hrs depending on hardware
- The node must have read access to the RLP file
- Genesis config must match the RLP file exactly for successful import
- Use 'lux chain export' to create RLP files from running chains`,
Args: cobra.ExactArgs(2),
RunE: runChainImport,
}
cmd.Flags().StringVar(&importRPC, "rpc", "", "Custom RPC endpoint (default: auto-detected)")
return cmd
}
func runChainImport(_ *cobra.Command, args []string) error {
chainArg := args[0]
filePath := args[1]
// Validate file exists
if _, err := os.Stat(filePath); os.IsNotExist(err) {
return fmt.Errorf("RLP file not found: %s", filePath)
}
// Get absolute path for the file
absFilePath, err := filepath.Abs(filePath)
if err != nil {
return fmt.Errorf("failed to get absolute path: %w", err)
}
// Resolve chain name to blockchain ID/path
chainPath, chainDisplay := resolveChain(chainArg)
// Determine base URL based on network target
// Port mapping: mainnet=9630, testnet=9640, devnet=9650, custom=9660
baseURL := "http://localhost:9660" // Default for custom
target := GetNetworkTarget()
switch target {
case NetworkMainnet:
baseURL = "http://localhost:9630" // Mainnet ports
case NetworkTestnet:
baseURL = "http://localhost:9640" // Testnet ports
case NetworkDevnet:
baseURL = "http://localhost:9650" // Devnet ports
case NetworkCustom:
baseURL = "http://localhost:9660" // Custom network ports
}
if importRPC != "" {
baseURL = strings.TrimSuffix(importRPC, route.Chain("", chainPath)+"/rpc")
baseURL = strings.TrimSuffix(baseURL, route.Chain("", chainPath)+"/admin")
baseURL = strings.TrimSuffix(baseURL, "/")
}
rpcEndpoint := route.Chain(baseURL, chainPath) + "/rpc"
adminEndpoint := route.Chain(baseURL, chainPath) + "/admin"
ux.Logger.PrintToUser("Importing blocks to %s...", chainDisplay)
ux.Logger.PrintToUser(" RLP file: %s", absFilePath)
ux.Logger.PrintToUser(" RPC endpoint: %s", rpcEndpoint)
ux.Logger.PrintToUser(" Admin endpoint: %s", adminEndpoint)
// Get current block height before import
beforeHeight, err := getBlockHeight(rpcEndpoint)
if err != nil {
ux.Logger.PrintToUser(" Warning: Could not get current block height: %v", err)
} else {
ux.Logger.PrintToUser(" Current block height: %d", beforeHeight)
}
startTime := time.Now()
// Call admin_importChain on the RPC endpoint (Coreth/geth-style API)
// The admin API is exposed on the main RPC endpoint, not a separate admin endpoint
success, err := callAdminImportChain(rpcEndpoint, absFilePath)
if err != nil {
return fmt.Errorf("import failed: %w", err)
}
elapsed := time.Since(startTime)
if !success {
return fmt.Errorf("import returned false (check node logs for details)")
}
// Get block height after import
afterHeight, err := getBlockHeight(rpcEndpoint)
if err != nil {
ux.Logger.PrintToUser(" Warning: Could not get final block height: %v", err)
ux.Logger.PrintToUser("")
ux.Logger.PrintToUser("Import complete!")
ux.Logger.PrintToUser(" Time: %v", elapsed.Round(time.Second))
} else {
blocksImported := afterHeight - beforeHeight
rate := float64(blocksImported) / elapsed.Seconds()
ux.Logger.PrintToUser("")
ux.Logger.PrintToUser("Import complete!")
ux.Logger.PrintToUser(" Blocks imported: %d", blocksImported)
ux.Logger.PrintToUser(" Final height: %d", afterHeight)
ux.Logger.PrintToUser(" Time: %v", elapsed.Round(time.Second))
if rate > 0 {
ux.Logger.PrintToUser(" Rate: %.1f blocks/sec", rate)
}
}
return nil
}
// resolveChain resolves a chain name to blockchain path and display name
func resolveChain(chain string) (path, display string) {
lower := strings.ToLower(chain)
// C-Chain
if lower == "c" {
return "C", "C-Chain"
}
// Known chain names - try to look up blockchain ID
if lower == "zoo" {
if blockchainID := lookupBlockchainID("zoo"); blockchainID != "" {
return blockchainID, "Zoo Chain"
}
return chain, "Zoo Chain (not deployed)"
}
// Assume it's a blockchain ID
return chain, fmt.Sprintf("Chain %s", chain[:min(len(chain), 12)])
}
// lookupBlockchainID looks up a chain's blockchain ID from sidecar
func lookupBlockchainID(chainName string) string {
// Try to load sidecar for the chain
sc, err := app.LoadSidecar(chainName)
if err != nil {
return ""
}
// Check Local Network deployment
if network, ok := sc.Networks[models.Local.String()]; ok {
return network.BlockchainID.String()
}
return ""
}
func getBlockHeight(rpcEndpoint string) (uint64, error) {
req := map[string]interface{}{
"jsonrpc": "2.0",
"method": "eth_blockNumber",
"params": []interface{}{},
"id": 1,
}
data, err := json.Marshal(req)
if err != nil {
return 0, err
}
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Post(rpcEndpoint, "application/json", bytes.NewBuffer(data))
if err != nil {
return 0, err
}
defer func() { _ = resp.Body.Close() }()
var result map[string]interface{}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return 0, err
}
if errObj, ok := result["error"]; ok {
return 0, fmt.Errorf("RPC error: %v", errObj)
}
heightHex, ok := result["result"].(string)
if !ok {
return 0, fmt.Errorf("invalid result format")
}
var height uint64
_, _ = fmt.Sscanf(heightHex, "0x%x", &height)
return height, nil
}
func callAdminImportChain(rpcEndpoint, filePath string) (bool, error) {
// Coreth/geth-style RPC uses underscore method format: admin_importChain
// The file path is passed as a single string parameter (not a struct)
req := map[string]interface{}{
"jsonrpc": "2.0",
"method": "admin_importChain",
"params": []string{filePath},
"id": 1,
}
data, err := json.Marshal(req)
if err != nil {
return false, fmt.Errorf("failed to marshal request: %w", err)
}
// Use long timeout for large imports
client := &http.Client{
Timeout: 24 * time.Hour,
}
ux.Logger.PrintToUser("Calling admin_importChain (this may take a while for large files)...")
resp, err := client.Post(rpcEndpoint, "application/json", bytes.NewBuffer(data))
if err != nil {
return false, fmt.Errorf("HTTP request failed: %w", err)
}
defer func() { _ = resp.Body.Close() }()
body, err := io.ReadAll(resp.Body)
if err != nil {
return false, fmt.Errorf("failed to read response: %w", err)
}
var result map[string]interface{}
if err := json.Unmarshal(body, &result); err != nil {
return false, fmt.Errorf("failed to parse response: %w (body: %s)", err, string(body))
}
if errObj, ok := result["error"]; ok {
errMap, _ := errObj.(map[string]interface{})
if msg, ok := errMap["message"].(string); ok && strings.Contains(msg, "timed out") {
// The RPC timed out but the import continues in background
ux.Logger.PrintToUser("")
ux.Logger.PrintToUser("Note: RPC response timed out, but import is running in background.")
ux.Logger.PrintToUser("The node is processing blocks asynchronously.")
ux.Logger.PrintToUser("")
ux.Logger.PrintToUser("Check status with:")
ux.Logger.PrintToUser(" lux chain import-status c --mainnet")
ux.Logger.PrintToUser(" # or check node logs")
return true, nil // Don't error out - import is running
}
return false, fmt.Errorf("RPC error: %v", errObj)
}
// Result can be bool or empty on success
if resultVal, ok := result["result"]; ok {
if success, ok := resultVal.(bool); ok {
return success, nil
}
// Some implementations return empty result on success
return true, nil
}
return true, nil
}