Skip to content

feat(config): per-chain configuration and runtime registry - #237

Open
Nexha-dev wants to merge 1 commit into
Octo-Protocol-org:mainfrom
Nexha-dev:feat/per-chain-configuration
Open

feat(config): per-chain configuration and runtime registry#237
Nexha-dev wants to merge 1 commit into
Octo-Protocol-org:mainfrom
Nexha-dev:feat/per-chain-configuration

Conversation

@Nexha-dev

Copy link
Copy Markdown

Summary

  • Replaces AppState's single-chain fields (network/horizon/horizon_url/friendbot_url) with Arc<ChainRegistry>, built from a new ChainConfig/AppConfig structure — one entry per chain, each carrying its own RPC endpoint, confirmation depth, poll interval, and resilience policy (RetryPolicy + CircuitBreaker), so a degraded RPC on one chain can never open another chain's circuit breaker.
  • Config precedence, documented in crates/api/src/chain_config.rs: a TOML file (CHAIN_CONFIG_PATH, [[chains]] entries — see octo.chains.example.toml) with per-chain OCTO_CHAIN_<CHAIN_ID>_RPC_URL env overrides for secrets; if no TOML file is present, the legacy flat env vars (NETWORK/HORIZON_URL/FRIENDBOT_URL/HORIZON_*) build one implicit Stellar chain, so existing single-chain deployments keep working unmodified.
  • bin/server fails fast and loudly at startup: chain ids must be unique and non-empty, at least one chain must be enabled, and every enabled chain's RPC must pass a liveness probe before the server serves traffic — a bad RPC URL aborts boot instead of failing lazily on the first customer deposit.
  • RPC URLs use a redacting Debug/Display impl (RedactedUrl) — provider URLs (Alchemy/Infura) embed API keys, and reaching the raw value requires an explicit expose_secret() call, so a leak into a log line or error message takes a deliberate choice rather than an accidental {:?}.
  • Adds GET /health/chains, reporting per-chain reachability and last successful poll (reusing the LastPollTracker shape) without ever including the RPC URL.
  • friendbot_url moves into the Stellar chain config — it's a Stellar-only concept and doesn't belong in shared state.
  • AppState::new/new_with_resilience keep their existing signatures (building a single-chain registry internally), so the many existing call sites and tests are unaffected; AppState::from_chain_registry is the new multi-chain construction path bin/server uses.

Scope

Per the issue's guideline, this PR is deliberately mechanical: it delivers the shape for N chains (config, registry, per-chain resilience isolation, redaction, health) and does not add any EVM chain. ChainRegistry here is a lightweight config+resilience registry, not a trait-based chain-adapter registry — the adapter abstraction (#213) can supersede or wrap it later without disturbing the config/validation/isolation work done here.

Test plan

  • cargo fmt --all -- --check
  • cargo clippy --workspace --all-targets --locked -- -D warnings — clean
  • cargo build --workspace --all-targets — clean
  • cargo test --workspace --locked against a live Postgres — clean, including:
    • config parse tests: valid multi-chain file, duplicate chain id rejected, unknown/empty chain id rejected, no-enabled-chains rejected
    • explicit env-var override precedence test
    • a test asserting the RPC URL's API-key substring is absent from Debug/Display output and from the /health/chains response body
    • a test that one chain's circuit breaker opening does not affect another's (ChainRegistry-level isolation)
  • .env.example, README.md, and docs/architecture.md updated with a worked multi-chain example (octo.chains.example.toml)

Closes #216

Replaces single-chain AppState fields (network/horizon/horizon_url/
friendbot_url) with Arc<ChainRegistry>, built from per-chain config,
each carrying its own RPC endpoint, confirmation depth, poll interval,
and resilience policy (RetryPolicy + CircuitBreaker) so a degraded RPC
on one chain can never open another chain's circuit breaker.

Config source, in precedence order: a CHAIN_CONFIG_PATH TOML file
([[chains]] entries, see octo.chains.example.toml) with per-chain
OCTO_CHAIN_<CHAIN_ID>_RPC_URL env overrides for secrets; else the
legacy flat env vars (NETWORK/HORIZON_URL/FRIENDBOT_URL/HORIZON_*)
build one implicit Stellar chain, so existing single-chain
deployments keep working unmodified.

bin/server fails fast at startup: chain ids must be unique and
non-empty, at least one chain must be enabled, and every enabled
chain's RPC must pass a liveness probe before the server serves
traffic. RPC URLs use a redacting Debug/Display impl (RedactedUrl) —
provider URLs embed API keys, and reaching the raw value requires an
explicit expose_secret() call. GET /health/chains reports per-chain
reachability without ever including the RPC URL.

AppState::new/new_with_resilience keep their existing signatures
(building a single-chain registry internally) so the many existing
call sites and tests are unaffected; AppState::from_chain_registry is
the new multi-chain construction path bin/server uses.

ChainRegistry here is deliberately lightweight (config + resilience +
poll-health state only) — the trait-based chain abstraction from the
chain-adapter epic supersedes it once that lands. This PR does not add
EVM support; it delivers the shape for N chains, EVM adapters plug in
later.

Closes Octo-Protocol-org#216
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(config): Per-chain configuration and runtime chain registry

1 participant