mirror of
https://github.com/luxfi/explorer.git
synced 2026-09-24 00:25:12 +00:00
`ChainConfig.Enabled` existed on the struct, was written in every chains.yaml
we ship, and was consulted by nothing. Only `Graph.Enabled` and the subgraph
flag were ever read (supervisor.go). So `enabled: false` registered the chain,
spawned its indexer and polled its RPC exactly like `enabled: true` — a switch
that looked like a switch and was wired to nothing.
Found by using it. lux-mainnet's config lists spc, which is a sovereign L1 and
has never been a chain on that network. Setting `enabled: false` and rolling
the config changed nothing: the pod still logged
[registry] added chain spc (…/v1/bc/spc/rpc) source=config
[spc/amm] [indexer] starting
[evm] Failed to get latest block: http status 404
at 1,800 errors an hour, against a chain that was already switched off.
Honoured in ChainRegistry.Add, which is the single door — config, env, mDNS
and the admin API all arrive there — so this is the whole of what the field
means, in one place. A disabled chain is not registered, so it does not index,
mounts no routes, and does not reach the SPA's chain list (which reads
registry.List()).
Enabled is now *bool because absent must not mean off. Every config written
while the field was inert says `enabled: true` out of convention; a plain bool
would read a future config that simply omits the key as false and make that
chain vanish with no error and no log. Nil is no opinion; only an explicit
`enabled: false` disables.
Tests: registration gated in all three states (true / false / absent), a
disabled chain proven not to reach the supervisor or the chain list, and
LoadConfig proven to decode into the pointer — a wrong yaml tag would leave it
nil and silently re-enable everything it disables. Verified by defeating the
check: the false and supervisor cases fail, the absent case still passes.
Also: the Dockerfile now runs vet + tests in the builder stage. This repo has
exactly one workflow, release.yml, which builds on a v* tag; nothing ran the
test files at all. Putting them where cgo and sqlite-dev already exist costs
no new job, no new runner dependency, and means a release that fails them
never becomes an image.
Co-authored-by: Hanzo Dev <[email protected]>
139 lines
6.2 KiB
Go
139 lines
6.2 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"regexp"
|
|
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
// Config is the top-level explorer configuration loaded from chains.yaml.
|
|
type Config struct {
|
|
DataDir string `yaml:"data_dir"`
|
|
HTTPAddr string `yaml:"http_addr"`
|
|
StaticDir string `yaml:"static_dir"` // overlay dir for SPA; empty = embedded only
|
|
BrandDefault Brand `yaml:"brand_default"`
|
|
Networks []Network `yaml:"networks"`
|
|
Chains []ChainConfig `yaml:"chains"`
|
|
}
|
|
|
|
// ChainConfig defines a single chain plus per-chain customization. The
|
|
// admin/registry/mdns layer also uses this shape; fields beyond Slug/Name/
|
|
// ChainID/RPC/Type/Default are optional and apply when the chain is loaded
|
|
// from a config file.
|
|
type ChainConfig struct {
|
|
Slug string `json:"slug" yaml:"slug"`
|
|
Name string `json:"name" yaml:"name"`
|
|
ChainID int64 `json:"chain_id" yaml:"chain_id"`
|
|
Type string `json:"type" yaml:"type"` // evm, dag, linear, solana, bitcoin, cosmos
|
|
// RPC is the address THIS PROCESS dials. In-cluster that is normally a
|
|
// service-mesh name (luxd-headless.lux-mainnet.svc.cluster.local:9630),
|
|
// which is correct for the indexer and USELESS to a browser.
|
|
RPC string `json:"rpc" yaml:"rpc"`
|
|
// PublicRPC is what the browser is told to use. Split from RPC because the
|
|
// two have opposite requirements and one field cannot satisfy both: the
|
|
// SPA was being handed `http://luxd-headless…svc.cluster.local:9630/...`,
|
|
// which no browser can resolve, so every wallet/network action against the
|
|
// default chain failed. Leave empty when RPC is already public — see
|
|
// browserReachable() in frontend.go, which falls back to RPC in that case.
|
|
PublicRPC string `json:"public_rpc,omitempty" yaml:"public_rpc,omitempty"`
|
|
WS string `json:"ws" yaml:"ws"`
|
|
CoinSymbol string `json:"coin" yaml:"coin"`
|
|
PoolManager string `json:"pool_manager" yaml:"pool_manager"` // DEX settlement precompile (0x9999); empty => indexer default
|
|
// Enabled gates the chain entirely: a disabled chain is never registered,
|
|
// so it is not indexed, mounts no routes, and does not reach the SPA's
|
|
// chain list. See ChainRegistry.Add, the single place it is honoured.
|
|
//
|
|
// A pointer, so absent and false are distinguishable. Absent means ON —
|
|
// every config predating this field wrote `enabled: true` by convention
|
|
// while the field was inert, and a plain bool would read a future config
|
|
// that simply omits the key as "off" and make the chain vanish with no
|
|
// error. Nil is no opinion; only an explicit `enabled: false` disables.
|
|
Enabled *bool `json:"enabled,omitempty" yaml:"enabled,omitempty"`
|
|
Default bool `json:"default" yaml:"default"`
|
|
Source string `json:"source" yaml:"-"` // config, env, mdns, admin
|
|
|
|
Indexer IndexerSettings `json:"indexer,omitempty" yaml:"indexer,omitempty"`
|
|
Graph GraphSettings `json:"graph,omitempty" yaml:"graph,omitempty"`
|
|
Brand *Brand `json:"brand,omitempty" yaml:"brand,omitempty"`
|
|
Tokens TokensFeatures `json:"tokens,omitempty" yaml:"tokens,omitempty"`
|
|
}
|
|
|
|
// IndexerSettings overrides indexer behaviour for a single chain.
|
|
type IndexerSettings struct {
|
|
PollInterval string `yaml:"poll_interval" json:"poll_interval,omitempty"` // e.g. "30s"
|
|
StartBlock uint64 `yaml:"start_block" json:"start_block,omitempty"`
|
|
}
|
|
|
|
// GraphSettings configures per-chain GraphQL subgraphs.
|
|
type GraphSettings struct {
|
|
Enabled bool `yaml:"enabled" json:"enabled"`
|
|
Subgraphs []Subgraph `yaml:"subgraphs" json:"subgraphs,omitempty"`
|
|
}
|
|
|
|
// Subgraph mounts a single named GraphQL schema under
|
|
// /v1/graph/{chain-slug}/{subgraph-name}/. Schema is either a built-in name
|
|
// understood by github.com/luxfi/graph/engine.LoadBuiltin (amm, uniswap-v2,
|
|
// uniswap-v3, uniswap-v4, securities, fhe, etc.) or a path to a .graphql file.
|
|
type Subgraph struct {
|
|
Name string `yaml:"name" json:"name"`
|
|
Schema string `yaml:"schema" json:"schema"`
|
|
Enabled bool `yaml:"enabled" json:"enabled"`
|
|
}
|
|
|
|
// Brand is the per-chain (or default) UI branding shown by the SPA. Icon/Logo
|
|
// files are read from disk on every request so a deploy can swap them without
|
|
// rebuilding the binary.
|
|
type Brand struct {
|
|
Name string `yaml:"name" json:"name,omitempty"`
|
|
Coin string `yaml:"coin" json:"coin,omitempty"`
|
|
AccentColor string `yaml:"accent_color" json:"accentColor,omitempty"`
|
|
IconFile string `yaml:"icon_file" json:"-"`
|
|
LogoFile string `yaml:"logo_file" json:"-"`
|
|
IconURL string `yaml:"icon_url" json:"iconUrl,omitempty"`
|
|
LogoURL string `yaml:"logo_url" json:"logoUrl,omitempty"`
|
|
}
|
|
|
|
// TokensFeatures controls which tokens the SPA highlights or hides.
|
|
type TokensFeatures struct {
|
|
Featured []string `yaml:"featured" json:"featured,omitempty"`
|
|
AllowList []string `yaml:"allow_list" json:"allowList,omitempty"`
|
|
BlockList []string `yaml:"block_list" json:"blockList,omitempty"`
|
|
}
|
|
|
|
// Network is a peer explorer the SPA can link to in its network switcher.
|
|
type Network struct {
|
|
Label string `yaml:"label" json:"label"`
|
|
Domain string `yaml:"domain" json:"domain"`
|
|
ChainID int64 `yaml:"chain_id" json:"chainId"`
|
|
}
|
|
|
|
// On reports whether the chain should run. Absent means on; only an explicit
|
|
// `enabled: false` turns a chain off.
|
|
func (c ChainConfig) On() bool { return c.Enabled == nil || *c.Enabled }
|
|
|
|
// slugPattern restricts chain slugs to lowercase alphanumeric + hyphen so they
|
|
// remain safe in URL routes (/v1/indexer/{slug}/) and filesystem paths.
|
|
var slugPattern = regexp.MustCompile(`^[a-z0-9][a-z0-9-]{0,63}$`)
|
|
|
|
// LoadConfig reads chains.yaml, expands $ENV_VAR in RPC/WS, and validates slugs.
|
|
func LoadConfig(path string) (Config, error) {
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return Config{}, fmt.Errorf("read %s: %w", path, err)
|
|
}
|
|
var cfg Config
|
|
if err := yaml.Unmarshal(data, &cfg); err != nil {
|
|
return Config{}, fmt.Errorf("parse %s: %w", path, err)
|
|
}
|
|
for i := range cfg.Chains {
|
|
cfg.Chains[i].RPC = os.Expand(cfg.Chains[i].RPC, os.Getenv)
|
|
cfg.Chains[i].WS = os.Expand(cfg.Chains[i].WS, os.Getenv)
|
|
if !slugPattern.MatchString(cfg.Chains[i].Slug) {
|
|
return Config{}, fmt.Errorf("chain[%d]: invalid slug %q", i, cfg.Chains[i].Slug)
|
|
}
|
|
}
|
|
return cfg, nil
|
|
}
|