Files
zeekay 724e24dd80 cli: decomplect — pkg/network + Profile/Spec/Env types + lux info verb
Sweep "brand" out of the API entirely. Package and types now match the
domain vocabulary the user sees in chain.yaml + CLI output:

  pkg/brand → pkg/network         (one package per concept)
  Brand     → Spec                (parsed chain.yaml)
  NetCfg    → Env                 (per-env block)
  RuntimeProfile → Profile
  RuntimeProfile.Brand → Profile.Name
  ChainLock → Lock
  $LUX_BRAND_PATH → $LUX_NETWORK_PATH

Surface BOTH identifiers distinctly — they are deliberately different:
  NetworkID         uint32  — validator wire, --network-id, info.getNetworkID
  PrimaryEvmChainID uint64  — C-chain EIP-155, eth_chainId, MetaMask

Lux brand keeps them apart (NID 1 / EVM 96369). Sovereign-L1 brand
forks (Zoo, Hanzo, Pars, Osage, Liquidity) collapse them by convention
(Zoo NID == EVM == 200200). The validator no longer enforces equality;
both are surfaced in lux ps and lux info.

New verb:
  lux info <name>/<env>   full Profile dump (both IDs, ports, paths,
                          snapshot name, service label, RPC endpoints,
                          live probe state)

lux ps output now shows NETWORK_ID and EVM_CHAIN_ID as separate columns.

chain.lock manifest now records both IDs so booting against a foreign
EVM-chain-ID dataDir is also rejected (previously only checked
networkID).
2026-06-16 21:03:14 -07:00

93 lines
2.7 KiB
Go

package network
import (
"fmt"
"os"
"path/filepath"
"gopkg.in/yaml.v3"
)
// ParseFile loads and validates a network's chain.yaml.
func ParseFile(path string) (*Spec, error) {
abs, err := filepath.Abs(path)
if err != nil {
return nil, err
}
data, err := os.ReadFile(abs) //nolint:gosec
if err != nil {
return nil, fmt.Errorf("read %s: %w", abs, err)
}
var s Spec
if err := yaml.Unmarshal(data, &s); err != nil {
return nil, fmt.Errorf("parse %s: %w", abs, err)
}
if s.Version != SchemaVersion {
return nil, fmt.Errorf("%s: unsupported chain.yaml version %q (want %q)", abs, s.Version, SchemaVersion)
}
s.SourcePath = abs
if err := s.validate(); err != nil {
return nil, fmt.Errorf("%s: %w", abs, err)
}
return &s, nil
}
func (s *Spec) validate() error {
if s.Network.Slug == "" {
return fmt.Errorf("network.slug required")
}
if len(s.Networks) == 0 {
return fmt.Errorf("networks block must list at least one env")
}
seenPort := map[int]string{}
seenNID := map[uint32]string{}
for env, e := range s.Networks {
if e.NetworkID == 0 {
return fmt.Errorf("networks.%s.networkID required", env)
}
if e.HTTPPort == 0 {
return fmt.Errorf("networks.%s.httpPort required", env)
}
if e.StakingPort == 0 {
return fmt.Errorf("networks.%s.stakingPort required", env)
}
if e.HTTPPort == e.StakingPort {
return fmt.Errorf("networks.%s: httpPort and stakingPort collide on %d", env, e.HTTPPort)
}
if prev, dup := seenPort[e.HTTPPort]; dup {
return fmt.Errorf("networks.%s.httpPort %d collides with %s", env, e.HTTPPort, prev)
}
if prev, dup := seenPort[e.StakingPort]; dup {
return fmt.Errorf("networks.%s.stakingPort %d collides with %s", env, e.StakingPort, prev)
}
if prev, dup := seenNID[e.NetworkID]; dup {
return fmt.Errorf("networks.%s.networkID %d collides with %s", env, e.NetworkID, prev)
}
seenPort[e.HTTPPort] = env + ".http"
seenPort[e.StakingPort] = env + ".staking"
seenNID[e.NetworkID] = env
// NetworkID and PrimaryEvmChainID are DISTINCT identifiers. Lux
// keeps them apart by design (NID 1 / EVM 96369). Sovereign-L1
// brand forks collapse them by convention. No equality check;
// the CLI surfaces both.
}
return nil
}
// At returns the per-env block, error if env not declared.
func (s *Spec) At(env string) (Env, error) {
e, ok := s.Networks[env]
if !ok {
envs := make([]string, 0, len(s.Networks))
for k := range s.Networks {
envs = append(envs, k)
}
return Env{}, fmt.Errorf("network %s has no env %q (have: %v)", s.Network.Slug, env, envs)
}
return e, nil
}
// Dir returns the directory containing this network's chain.yaml.
func (s *Spec) Dir() string { return filepath.Dir(s.SourcePath) }