Files
explorer/config.go
zooqueen 72ef1420e9 Routing: use /v1/<svc> prefix, never /api/ (house convention)
The zip front router mounted service reverse-proxies under /api/<prefix>; renamed
to /v1/<prefix> per convention (we never use a /api/ path prefix). Upstream
service-internal /api/v1/... paths (behind the proxy) are unchanged.
2026-06-23 21:26:39 -07:00

160 lines
6.9 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"`
Services ServicesConfig `yaml:"services"`
}
// ServicesConfig configures the in-process Blockscout-rs services that the
// `-tags ffi` build runs as goroutine-driven Tokio threads inside this one
// binary. The zip front router reverse-proxies each enabled service under
// /v1/<prefix>/* → http://127.0.0.1:<port>. In the default (non-FFI) build
// nothing is launched and these proxies are not mounted (see services.go).
type ServicesConfig struct {
// Enabled is the master switch. When false (default) no in-process
// service is started and no proxy is mounted — the explorer is exactly
// the Go-native binary it has always been.
Enabled bool `yaml:"enabled"`
// Services lists each in-process service. Order is irrelevant; the
// reverse-proxy prefix is derived from Prefix (default = Name).
Services []ServiceConfig `yaml:"list"`
}
// ServiceConfig is one in-process Blockscout-rs service.
type ServiceConfig struct {
// Name is the FFI service key (e.g. "sig-provider", "stats"). It selects
// which lux_explorer_start_<name> entrypoint runs and keys the settings
// passed to it.
Name string `yaml:"name"`
// Prefix is the URL path segment the front router proxies under
// (/v1/<prefix>/*). Empty => derived from Name (sig-provider => "sig").
Prefix string `yaml:"prefix"`
// HTTPPort is the localhost port the service's own HTTP server binds.
// Empty => a default is assigned per service (see servicePort).
HTTPPort int `yaml:"http_port"`
// GRPCPort is the localhost port for the service's gRPC server (the
// launcher always binds one; we give it a distinct port to avoid
// collisions). Empty => HTTPPort+1.
GRPCPort int `yaml:"grpc_port"`
// DatabaseURL is the sea-orm connection string for DB-backed services
// (stats, multichain-aggregator). A sqlite URL keeps the single binary
// self-contained. Empty for stateless services (sig-provider, visualizer,
// smart-contract-verifier).
DatabaseURL string `yaml:"database_url"`
// Enabled toggles this individual service. Default true when listed.
Enabled *bool `yaml:"enabled"`
// Settings is an optional raw settings overlay merged into the JSON sent
// to the service (service-specific keys beyond addr/db).
Settings map[string]any `yaml:"settings"`
}
// 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 string `json:"rpc" yaml:"rpc"`
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 bool `json:"enabled" yaml:"enabled"`
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"`
}
// 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
}