Depends on: #213. Blocks: #227.
Description
Configuration assumes exactly one chain. AppState holds a single
network: StellarNetwork, one horizon: Horizon, one horizon_url, one friendbot_url; the
ingest supervisor takes one Horizon URL. There is no way to express "Stellar mainnet and Base
and Arbitrum, each with its own endpoint, confirmation depth, and enable flag."
Replace the flat configuration with a per-chain structure that bin/server loads at startup and
resolves into the ChainRegistry from #213.
Requirements and context
- Support N chains where N ≥ 1, configured without recompiling.
- Fail fast and loudly at startup. A chain configured with a bad RPC URL must abort boot with a
clear message, not fail lazily on the first customer deposit. Follow the existing precedent in
CloudinaryConfig::from_env — but note that disabled-when-absent
is right for image uploads and wrong for a chain that has live customer balances.
- Env-var-only config does not scale to N chains with ~8 settings each. Prefer a config file
(TOML) with env-var override for secrets, and document the precedence order.
- Preserve the existing resilience wiring:
RetryPolicy and CircuitBreaker are currently
per-process, and must become per-chain — a degraded Base RPC must not open the circuit on
Stellar. See crates/ingest/tests/supervisor_network_isolation_tests.rs
for the isolation property already asserted.
- Security: RPC URLs frequently embed API keys (Alchemy, Infura). They must never appear in
logs, error messages, or health endpoints. Wrap in a redacting Debug impl.
Suggested execution
Branch: feat/per-chain-configuration
Implement changes
- Define
ChainConfig { chain_id, rpc_url (redacted Debug), enabled, confirmation_depth, poll_interval, retry, circuit, faucet_url }
and AppConfig { chains: Vec<ChainConfig>, .. }.
- Refactor
AppState to hold Arc<ChainRegistry> instead of network/horizon/horizon_url,
keeping accessors for the Stellar path so route handlers change minimally in this PR.
- Add startup validation: chain ids are well-formed and unique, at least one chain is enabled, every
enabled chain's RPC answers a liveness probe within a timeout.
- Move
friendbot_url into the Stellar chain config (it is a Stellar-only concept and does not
belong in shared state).
- Add a
/health detail per chain — reachability and last successful poll, reusing
LastPollTracker. Redact credentials.
Test and commit
- Test config parse: valid multi-chain file; duplicate chain id rejected; unknown chain id rejected;
no-enabled-chains rejected.
- Test env-var override precedence explicitly.
- Test that a redacted RPC URL never appears in
Debug/Display output or in a /health
response — assert on the API-key substring being absent.
- Test that one chain's circuit breaker opening does not affect another's.
- Update
.env.example, README.md, and the deployment section of
docs/architecture.md with a worked multi-chain example.
Example commit message
feat(config): per-chain configuration and runtime registry
Replaces single-chain AppState fields with a ChainRegistry built from
per-chain config, each carrying its own RPC endpoint, confirmation
depth, and resilience policy so a degraded RPC on one chain cannot
open the circuit on another.
RPC URLs use a redacting Debug impl — provider URLs embed API keys.
Refs #216
Guidelines
Keep this PR mechanical. Do not add EVM chains here — this issue delivers the shape, and #217/#218
deliver an adapter to put in it.
Depends on: #213. Blocks: #227.
Description
Configuration assumes exactly one chain.
AppStateholds a singlenetwork: StellarNetwork, onehorizon: Horizon, onehorizon_url, onefriendbot_url; theingest supervisor takes one Horizon URL. There is no way to express "Stellar mainnet and Base
and Arbitrum, each with its own endpoint, confirmation depth, and enable flag."
Replace the flat configuration with a per-chain structure that
bin/serverloads at startup andresolves into the
ChainRegistryfrom #213.Requirements and context
clear message, not fail lazily on the first customer deposit. Follow the existing precedent in
CloudinaryConfig::from_env— but note that disabled-when-absentis right for image uploads and wrong for a chain that has live customer balances.
(TOML) with env-var override for secrets, and document the precedence order.
RetryPolicyandCircuitBreakerare currentlyper-process, and must become per-chain — a degraded Base RPC must not open the circuit on
Stellar. See
crates/ingest/tests/supervisor_network_isolation_tests.rsfor the isolation property already asserted.
logs, error messages, or health endpoints. Wrap in a redacting
Debugimpl.Suggested execution
Branch:
feat/per-chain-configurationImplement changes
ChainConfig { chain_id, rpc_url (redacted Debug), enabled, confirmation_depth, poll_interval, retry, circuit, faucet_url }and
AppConfig { chains: Vec<ChainConfig>, .. }.AppStateto holdArc<ChainRegistry>instead ofnetwork/horizon/horizon_url,keeping accessors for the Stellar path so route handlers change minimally in this PR.
enabled chain's RPC answers a liveness probe within a timeout.
friendbot_urlinto the Stellar chain config (it is a Stellar-only concept and does notbelong in shared state).
/healthdetail per chain — reachability and last successful poll, reusingLastPollTracker. Redact credentials.Test and commit
no-enabled-chains rejected.
Debug/Displayoutput or in a/healthresponse — assert on the API-key substring being absent.
.env.example,README.md, and the deployment section ofdocs/architecture.mdwith a worked multi-chain example.Example commit message
Guidelines
Keep this PR mechanical. Do not add EVM chains here — this issue delivers the shape, and #217/#218
deliver an adapter to put in it.