diff --git a/contracts/teachlink/src/analytics.rs b/contracts/teachlink/src/analytics.rs index 2cd44e1f..68e69f3e 100644 --- a/contracts/teachlink/src/analytics.rs +++ b/contracts/teachlink/src/analytics.rs @@ -8,8 +8,8 @@ use crate::storage::{BRIDGE_METRICS, CHAIN_METRICS, DAILY_VOLUMES}; use crate::types::{BridgeMetrics, ChainMetrics}; use soroban_sdk::{Address, Bytes, Env, Map, Vec}; -/// Metrics update interval (1 hour) -pub const METRICS_UPDATE_INTERVAL: u64 = 3_600; +/// Metrics update interval — re-exported from config for backward compatibility. +pub use crate::config::METRICS_UPDATE_INTERVAL; /// Analytics Manager pub struct AnalyticsManager; diff --git a/contracts/teachlink/src/atomic_swap.rs b/contracts/teachlink/src/atomic_swap.rs index d9f5b0fc..82ffa1cf 100644 --- a/contracts/teachlink/src/atomic_swap.rs +++ b/contracts/teachlink/src/atomic_swap.rs @@ -47,14 +47,12 @@ use crate::storage::{ATOMIC_SWAPS, SWAP_COUNTER, SWAP_GUARD, SWAP_TIMELOCK_SEQ}; use crate::types::{AtomicSwap, SwapStatus}; use soroban_sdk::{symbol_short, vec, Address, Bytes, Env, IntoVal, Map, Vec}; -/// Minimum timelock duration (1 hour) -pub const MIN_TIMELOCK: u64 = 3_600; - -/// Maximum timelock duration (7 days) -pub const MAX_TIMELOCK: u64 = 604_800; - -/// Hash length (32 bytes for SHA256) -pub const HASH_LENGTH: u32 = 32; +/// Minimum timelock duration — re-exported from config. +pub use crate::config::SWAP_MIN_TIMELOCK as MIN_TIMELOCK; +/// Maximum timelock duration — re-exported from config. +pub use crate::config::SWAP_MAX_TIMELOCK as MAX_TIMELOCK; +/// Required hash length — re-exported from config. +pub use crate::config::SWAP_HASH_LENGTH as HASH_LENGTH; /// Atomic Swap Manager pub struct AtomicSwapManager; diff --git a/contracts/teachlink/src/audit.rs b/contracts/teachlink/src/audit.rs index d00c0052..616ad35b 100644 --- a/contracts/teachlink/src/audit.rs +++ b/contracts/teachlink/src/audit.rs @@ -35,11 +35,10 @@ use crate::storage::{AUDIT_COUNTER, AUDIT_RECORDS, COMPLIANCE_REPORTS}; use crate::types::{AuditRecord, ComplianceReport, OperationType}; use soroban_sdk::{Address, Bytes, Env, Map, Vec}; -/// Maximum audit records to store -pub const MAX_AUDIT_RECORDS: u64 = 100_000; - -/// Compliance report period (7 days) -pub const COMPLIANCE_PERIOD: u64 = 604_800; +/// Maximum audit records to store — re-exported from config. +pub use crate::config::AUDIT_MAX_RECORDS as MAX_AUDIT_RECORDS; +/// Compliance report period — re-exported from config. +pub use crate::config::AUDIT_COMPLIANCE_PERIOD as COMPLIANCE_PERIOD; /// Audit Manager pub struct AuditManager; diff --git a/contracts/teachlink/src/config.rs b/contracts/teachlink/src/config.rs new file mode 100644 index 00000000..20e304f4 --- /dev/null +++ b/contracts/teachlink/src/config.rs @@ -0,0 +1,184 @@ +//! Centralized configuration for the TeachLink contract. +//! +//! All tunable constants live here. Modules import from this file instead of +//! defining their own `pub const` values, ensuring a single source of truth. +//! +//! # Sections +//! - [Analytics](#analytics) +//! - [Atomic Swaps](#atomic-swaps) +//! - [Audit & Compliance](#audit--compliance) +//! - [BFT Consensus](#bft-consensus) +//! - [Emergency & Circuit Breaker](#emergency--circuit-breaker) +//! - [Ledger Time](#ledger-time) +//! - [Liquidity & Fees](#liquidity--fees) +//! - [Message Passing](#message-passing) +//! - [Multichain](#multichain) +//! - [Network Recovery](#network-recovery) +//! - [Notifications](#notifications) +//! - [Performance Cache](#performance-cache) +//! - [Rate Limiting](#rate-limiting) +//! - [Slashing](#slashing) +//! - [Sustainability](#sustainability) +//! - [Upgrade](#upgrade) + +// ===== Analytics ===== + +/// How often bridge metrics may be updated (seconds). Default: 1 hour. +pub const METRICS_UPDATE_INTERVAL: u64 = 3_600; + +// ===== Atomic Swaps ===== + +/// Minimum timelock duration for an atomic swap (seconds). Default: 1 hour. +pub const SWAP_MIN_TIMELOCK: u64 = 3_600; + +/// Maximum timelock duration for an atomic swap (seconds). Default: 7 days. +pub const SWAP_MAX_TIMELOCK: u64 = 604_800; + +/// Required byte length of a hashlock preimage hash. +pub const SWAP_HASH_LENGTH: u32 = 32; + +// ===== Audit & Compliance ===== + +/// Maximum number of audit records retained before circular-buffer wrap. +pub const AUDIT_MAX_RECORDS: u64 = 100_000; + +/// Default compliance report period (seconds). Default: 7 days. +pub const AUDIT_COMPLIANCE_PERIOD: u64 = 604_800; + +// ===== BFT Consensus ===== + +/// Minimum validator stake (stroops, 6-decimal). Default: 100 tokens. +pub const BFT_MIN_VALIDATOR_STAKE: i128 = 100_000_000; + +/// Proposal expiry window (seconds). Default: 24 hours. +pub const BFT_PROPOSAL_TIMEOUT: u64 = 86_400; + +/// Number of consensus rounds per validator rotation epoch. +pub const BFT_ROTATION_EPOCH_ROUNDS: u64 = 100; + +/// Minimum reputation score for a validator to remain active (0-100). +pub const BFT_MIN_ACTIVE_REPUTATION: u32 = 40; + +// ===== Emergency & Circuit Breaker ===== + +/// Number of seats on the security council. +pub const EMERGENCY_SECURITY_COUNCIL_SIZE: u32 = 5; + +/// Daily volume tracking window (seconds). Default: 24 hours. +pub const EMERGENCY_DAILY_VOLUME_RESET: u64 = 86_400; + +// ===== Ledger Time ===== + +/// Estimated seconds per Stellar ledger (used for lag calculations). +pub const LEDGER_EST_SECS: u64 = 5; + +// ===== Liquidity & Fees ===== + +/// Base bridge fee in basis points. Default: 0.10%. +pub const LIQUIDITY_BASE_FEE_BPS: i128 = 10; + +/// Maximum bridge fee in basis points. Default: 5%. +pub const LIQUIDITY_MAX_FEE_BPS: i128 = 500; + +/// Minimum bridge fee in basis points. Default: 0.01%. +pub const LIQUIDITY_MIN_FEE_BPS: i128 = 1; + +/// Pool utilization threshold (basis points) above which dynamic fees apply. +pub const LIQUIDITY_UTILIZATION_THRESHOLD: u32 = 8_000; + +// ===== Message Passing ===== + +/// Default cross-chain packet timeout (seconds). Default: 24 hours. +pub const MSG_DEFAULT_PACKET_TIMEOUT: u64 = 86_400; + +/// Maximum packet delivery retry attempts. +pub const MSG_MAX_RETRY_ATTEMPTS: u32 = 5; + +/// Base delay between packet retries (seconds). Default: 5 minutes. +pub const MSG_RETRY_DELAY_BASE: u64 = 300; + +// ===== Multichain ===== + +/// Maximum number of supported external chains. +pub const MULTICHAIN_MAX_CHAINS: u32 = 100; + +/// Maximum number of registered multi-chain assets. +pub const MULTICHAIN_MAX_ASSETS: u32 = 1_000; + +// ===== Network Recovery ===== + +/// Maximum automatic retry attempts for a failed operation. +pub const RECOVERY_MAX_RETRY_ATTEMPTS: u32 = 5; + +/// Initial exponential-backoff delay (seconds). Default: 1 minute. +pub const RECOVERY_INITIAL_BACKOFF_SECS: u64 = 60; + +/// Maximum backoff delay (seconds). Default: 1 hour. +pub const RECOVERY_MAX_BACKOFF_SECS: u64 = 3_600; + +/// Backoff multiplier applied on each retry. +pub const RECOVERY_BACKOFF_MULTIPLIER: u64 = 2; + +// ===== Notifications ===== + +/// Sentinel value indicating immediate (non-scheduled) delivery. +pub const NOTIF_IMMEDIATE_DELIVERY: u64 = 0; + +/// Minimum scheduling delay for a notification (seconds). Default: 1 minute. +pub const NOTIF_MIN_DELAY_SECS: u64 = 60; + +/// Maximum scheduling delay for a notification (seconds). Default: 30 days. +pub const NOTIF_MAX_DELAY_SECS: u64 = 86_400 * 30; + +/// Maximum notifications processed per batch. +pub const NOTIF_BATCH_SIZE: u32 = 100; + +/// Default TTL for notification event storage (seconds). Default: 7 days. +pub const NOTIF_DEFAULT_EVENT_TTL_SECS: u64 = 86_400 * 7; + +// ===== Performance Cache ===== + +/// Bridge summary cache TTL (seconds). Default: 1 hour. +pub const PERF_CACHE_TTL_SECS: u64 = 3_600; + +/// Maximum chains included in the cached top-by-volume list. +pub const PERF_MAX_TOP_CHAINS: u32 = 20; + +// ===== Rate Limiting ===== + +/// Default maximum calls allowed per rate-limit window. +pub const RATE_LIMIT_DEFAULT_MAX_CALLS: u32 = 100; + +/// Default rate-limit window size in ledgers. +pub const RATE_LIMIT_DEFAULT_WINDOW_LEDGERS: u32 = 600; + +// ===== Slashing ===== + +/// Slash percentage for double-vote offence (basis points). Default: 50%. +pub const SLASH_DOUBLE_VOTE_BPS: u32 = 5_000; + +/// Slash percentage for invalid-signature offence (basis points). Default: 10%. +pub const SLASH_INVALID_SIGNATURE_BPS: u32 = 1_000; + +/// Slash percentage for inactivity offence (basis points). Default: 5%. +pub const SLASH_INACTIVITY_BPS: u32 = 500; + +/// Slash percentage for byzantine behaviour (basis points). Default: 100%. +pub const SLASH_BYZANTINE_BPS: u32 = 10_000; + +/// Slash percentage for malicious behaviour (basis points). Default: 100%. +pub const SLASH_MALICIOUS_BPS: u32 = 10_000; + +// ===== Sustainability ===== + +/// Minimum content tokens minted to reach full content-creation score. +pub const SUSTAIN_CONTENT_SCORE_CAP: u64 = 1_000; + +/// Minimum active users to reach full user-adoption score. +pub const SUSTAIN_USER_SCORE_CAP: u64 = 1_000; + +// ===== Upgrade ===== + +/// Window within which a contract upgrade may be rolled back (seconds). +/// Default: 30 days. +pub const UPGRADE_ROLLBACK_WINDOW_SECS: u64 = 86_400 * 30; diff --git a/contracts/teachlink/src/emergency.rs b/contracts/teachlink/src/emergency.rs index cb20ce14..7eff0dc6 100644 --- a/contracts/teachlink/src/emergency.rs +++ b/contracts/teachlink/src/emergency.rs @@ -11,11 +11,10 @@ use crate::storage::{CIRCUIT_BREAKERS, CIRCUIT_RESET_SEQ, EMERGENCY_STATE, PAUSE use crate::types::{CircuitBreaker, EmergencyState}; use soroban_sdk::{Address, Bytes, Env, Map, Vec}; -/// Authorized pausers (admin + security council) -pub const SECURITY_COUNCIL_SIZE: u32 = 5; - -/// Daily volume reset period (24 hours) -pub const DAILY_VOLUME_RESET: u64 = 86_400; +/// Authorized pausers (admin + security council) — re-exported from config. +pub use crate::config::EMERGENCY_SECURITY_COUNCIL_SIZE as SECURITY_COUNCIL_SIZE; +/// Daily volume reset period — re-exported from config. +pub use crate::config::EMERGENCY_DAILY_VOLUME_RESET as DAILY_VOLUME_RESET; /// Emergency Manager pub struct EmergencyManager; diff --git a/contracts/teachlink/src/ledger_time.rs b/contracts/teachlink/src/ledger_time.rs index 32e09535..9a604472 100644 --- a/contracts/teachlink/src/ledger_time.rs +++ b/contracts/teachlink/src/ledger_time.rs @@ -5,11 +5,8 @@ use soroban_sdk::Env; -/// Conservative estimate of seconds per ledger close on Stellar. -/// -/// Used only to derive a *fallback* ledger-sequence deadline when code is otherwise -/// timestamp-gated. -pub const EST_SECS_PER_LEDGER: u64 = 5; +/// Conservative estimate of seconds per ledger close on Stellar — re-exported from config. +pub use crate::config::LEDGER_EST_SECS as EST_SECS_PER_LEDGER; pub fn seconds_to_ledger_delta(seconds: u64) -> u32 { // Ceil division to avoid shortening timeouts. diff --git a/contracts/teachlink/src/liquidity.rs b/contracts/teachlink/src/liquidity.rs index 67fee397..8dcbe59b 100644 --- a/contracts/teachlink/src/liquidity.rs +++ b/contracts/teachlink/src/liquidity.rs @@ -74,17 +74,14 @@ use crate::types::{BridgeFeeStructure, LPPosition, LiquidityPool}; use crate::validation::NumberValidator; use soroban_sdk::{Address, Env, Map, Vec}; -/// Base fee in basis points (0.1%) -pub const BASE_FEE_BPS: i128 = 10; - -/// Maximum fee in basis points (5%) -pub const MAX_FEE_BPS: i128 = 500; - -/// Minimum fee in basis points (0.01%) -pub const MIN_FEE_BPS: i128 = 1; - -/// Liquidity utilization threshold for dynamic pricing (80%) -pub const UTILIZATION_THRESHOLD: u32 = 8000; +/// Base fee in basis points — re-exported from config. +pub use crate::config::LIQUIDITY_BASE_FEE_BPS as BASE_FEE_BPS; +/// Maximum fee in basis points — re-exported from config. +pub use crate::config::LIQUIDITY_MAX_FEE_BPS as MAX_FEE_BPS; +/// Minimum fee in basis points — re-exported from config. +pub use crate::config::LIQUIDITY_MIN_FEE_BPS as MIN_FEE_BPS; +/// Utilization threshold for dynamic pricing — re-exported from config. +pub use crate::config::LIQUIDITY_UTILIZATION_THRESHOLD as UTILIZATION_THRESHOLD; /// Congestion multiplier steps pub const CONGESTION_STEP_1: u32 = 5000; // 50% utilization diff --git a/contracts/teachlink/src/message_passing.rs b/contracts/teachlink/src/message_passing.rs index 25a713e5..f807a521 100644 --- a/contracts/teachlink/src/message_passing.rs +++ b/contracts/teachlink/src/message_passing.rs @@ -12,14 +12,12 @@ use crate::types::{CrossChainPacket, MessageReceipt, PacketStatus}; use crate::validation::NumberValidator; use soroban_sdk::{Bytes, Env, Map, Vec}; -/// Default packet timeout (24 hours) -pub const DEFAULT_PACKET_TIMEOUT: u64 = 86_400; - -/// Maximum retry attempts -pub const MAX_RETRY_ATTEMPTS: u32 = 5; - -/// Retry delay in seconds (exponential backoff) -pub const RETRY_DELAY_BASE: u64 = 300; // 5 minutes +/// Default packet timeout — re-exported from config. +pub use crate::config::MSG_DEFAULT_PACKET_TIMEOUT as DEFAULT_PACKET_TIMEOUT; +/// Maximum retry attempts — re-exported from config. +pub use crate::config::MSG_MAX_RETRY_ATTEMPTS as MAX_RETRY_ATTEMPTS; +/// Retry delay base — re-exported from config. +pub use crate::config::MSG_RETRY_DELAY_BASE as RETRY_DELAY_BASE; /// Message Passing Manager pub struct MessagePassing; diff --git a/contracts/teachlink/src/multichain.rs b/contracts/teachlink/src/multichain.rs index cde17fb8..1aa3e7ec 100644 --- a/contracts/teachlink/src/multichain.rs +++ b/contracts/teachlink/src/multichain.rs @@ -10,11 +10,10 @@ use crate::types::{ChainAssetInfo, ChainConfig, MultiChainAsset}; use crate::validation::NumberValidator; use soroban_sdk::{Address, Bytes, Env, Map, Vec}; -/// Maximum number of supported chains -pub const MAX_SUPPORTED_CHAINS: u32 = 100; - -/// Maximum number of multi-chain assets -pub const MAX_MULTI_CHAIN_ASSETS: u32 = 1000; +/// Maximum number of supported chains — re-exported from config. +pub use crate::config::MULTICHAIN_MAX_CHAINS as MAX_SUPPORTED_CHAINS; +/// Maximum number of multi-chain assets — re-exported from config. +pub use crate::config::MULTICHAIN_MAX_ASSETS as MAX_MULTI_CHAIN_ASSETS; /// Multi-Chain Manager pub struct MultiChainManager; diff --git a/contracts/teachlink/src/network_recovery.rs b/contracts/teachlink/src/network_recovery.rs index 124f0233..ef7cb3e2 100644 --- a/contracts/teachlink/src/network_recovery.rs +++ b/contracts/teachlink/src/network_recovery.rs @@ -12,11 +12,11 @@ pub const RECOVERY_STATE: Symbol = soroban_sdk::symbol_short!("rec_state"); pub const RECOVERY_NOTIFICATIONS: Symbol = soroban_sdk::symbol_short!("rec_notif"); pub const FALLBACK_ACTIVE: Symbol = soroban_sdk::symbol_short!("fb_active"); -/// Retry configuration constants -pub const MAX_RETRY_ATTEMPTS: u32 = 5; -pub const INITIAL_BACKOFF_SECONDS: u64 = 60; // 1 minute -pub const MAX_BACKOFF_SECONDS: u64 = 3600; // 1 hour -pub const BACKOFF_MULTIPLIER: u64 = 2; +/// Retry configuration constants — re-exported from config. +pub use crate::config::RECOVERY_MAX_RETRY_ATTEMPTS as MAX_RETRY_ATTEMPTS; +pub use crate::config::RECOVERY_INITIAL_BACKOFF_SECS as INITIAL_BACKOFF_SECONDS; +pub use crate::config::RECOVERY_MAX_BACKOFF_SECS as MAX_BACKOFF_SECONDS; +pub use crate::config::RECOVERY_BACKOFF_MULTIPLIER as BACKOFF_MULTIPLIER; #[contracttype] #[derive(Clone, Debug, Eq, PartialEq)] diff --git a/contracts/teachlink/src/notification.rs b/contracts/teachlink/src/notification.rs index ba02b614..47fde14f 100644 --- a/contracts/teachlink/src/notification.rs +++ b/contracts/teachlink/src/notification.rs @@ -21,14 +21,13 @@ use crate::types::{ }; use soroban_sdk::{contracttype, vec, Address, Bytes, Env, IntoVal, Map, String, Vec}; -/// Notification delivery intervals (in seconds) -pub const IMMEDIATE_DELIVERY: u64 = 0; -pub const MIN_DELAY_SECONDS: u64 = 60; // 1 minute -pub const MAX_DELAY_SECONDS: u64 = 86400 * 30; // 30 days -pub const BATCH_SIZE: u32 = 100; - -/// Event queue cleanup configuration -pub const DEFAULT_EVENT_TTL_SECONDS: u64 = 86400 * 7; // 7 days +/// Notification delivery intervals — re-exported from config. +pub use crate::config::NOTIF_IMMEDIATE_DELIVERY as IMMEDIATE_DELIVERY; +pub use crate::config::NOTIF_MIN_DELAY_SECS as MIN_DELAY_SECONDS; +pub use crate::config::NOTIF_MAX_DELAY_SECS as MAX_DELAY_SECONDS; +pub use crate::config::NOTIF_BATCH_SIZE as BATCH_SIZE; +/// Event queue cleanup configuration — re-exported from config. +pub use crate::config::NOTIF_DEFAULT_EVENT_TTL_SECS as DEFAULT_EVENT_TTL_SECONDS; pub const DEFAULT_MAX_QUEUE_SIZE: u32 = 10000; pub const CLEANUP_INTERVAL_SECONDS: u64 = 3600; // 1 hour diff --git a/contracts/teachlink/src/performance.rs b/contracts/teachlink/src/performance.rs index 594802c3..81fc2a5c 100644 --- a/contracts/teachlink/src/performance.rs +++ b/contracts/teachlink/src/performance.rs @@ -12,11 +12,10 @@ use crate::storage::{PERF_CACHE, PERF_TS}; use crate::types::CachedBridgeSummary; use soroban_sdk::{Address, Env}; -/// Cache TTL in ledger seconds (1 hour). -pub const CACHE_TTL_SECS: u64 = 3_600; - -/// Max chains to include in cached top-by-volume (bounds gas). -pub const MAX_TOP_CHAINS: u32 = 20; +/// Cache TTL in ledger seconds — re-exported from config. +pub use crate::config::PERF_CACHE_TTL_SECS as CACHE_TTL_SECS; +/// Max chains to include in cached top-by-volume — re-exported from config. +pub use crate::config::PERF_MAX_TOP_CHAINS as MAX_TOP_CHAINS; /// Performance cache manager. pub struct PerformanceManager; diff --git a/contracts/teachlink/src/rate_limiting.rs b/contracts/teachlink/src/rate_limiting.rs index 3c9a781c..c729e591 100644 --- a/contracts/teachlink/src/rate_limiting.rs +++ b/contracts/teachlink/src/rate_limiting.rs @@ -34,9 +34,9 @@ pub struct RateLimitState { pub call_count: u32, } -/// Default per-user limit: 100 calls per ~600 ledgers (~1 hour at 6 s/ledger). -pub const DEFAULT_MAX_CALLS: u32 = 100; -pub const DEFAULT_WINDOW_LEDGERS: u32 = 600; +/// Default per-user limit — re-exported from config. +pub use crate::config::RATE_LIMIT_DEFAULT_MAX_CALLS as DEFAULT_MAX_CALLS; +pub use crate::config::RATE_LIMIT_DEFAULT_WINDOW_LEDGERS as DEFAULT_WINDOW_LEDGERS; pub struct RateLimiter; diff --git a/contracts/teachlink/src/slashing.rs b/contracts/teachlink/src/slashing.rs index 4e5215bf..74ba5357 100644 --- a/contracts/teachlink/src/slashing.rs +++ b/contracts/teachlink/src/slashing.rs @@ -54,12 +54,12 @@ use crate::types::{RewardType, SlashingReason, SlashingRecord, ValidatorInfo, Va use crate::validation::{NumberValidator, ValidationError}; use soroban_sdk::{Address, Env, Map, Vec}; -/// Slashing percentages (in basis points, 10000 = 100%) -pub const SLASHING_PERCENTAGE_DOUBLE_VOTE: u32 = 5000; // 50% -pub const SLASHING_PERCENTAGE_INVALID_SIGNATURE: u32 = 1000; // 10% -pub const SLASHING_PERCENTAGE_INACTIVITY: u32 = 500; // 5% -pub const SLASHING_PERCENTAGE_BYZANTINE: u32 = 10000; // 100% -pub const SLASHING_PERCENTAGE_MALICIOUS: u32 = 10000; // 100% +/// Slashing percentages (basis points) — re-exported from config. +pub use crate::config::SLASH_DOUBLE_VOTE_BPS as SLASHING_PERCENTAGE_DOUBLE_VOTE; +pub use crate::config::SLASH_INVALID_SIGNATURE_BPS as SLASHING_PERCENTAGE_INVALID_SIGNATURE; +pub use crate::config::SLASH_INACTIVITY_BPS as SLASHING_PERCENTAGE_INACTIVITY; +pub use crate::config::SLASH_BYZANTINE_BPS as SLASHING_PERCENTAGE_BYZANTINE; +pub use crate::config::SLASH_MALICIOUS_BPS as SLASHING_PERCENTAGE_MALICIOUS; /// Inactivity threshold (in seconds, 7 days) pub const INACTIVITY_THRESHOLD: u64 = 604_800; diff --git a/indexer/src/app.module.ts b/indexer/src/app.module.ts index a62ca7f7..a5c2d8bb 100644 --- a/indexer/src/app.module.ts +++ b/indexer/src/app.module.ts @@ -3,6 +3,7 @@ import { CacheModule } from '@nestjs/cache-manager'; import { ConfigModule } from '@nestjs/config'; import { ScheduleModule } from '@nestjs/schedule'; import configuration from './config/configuration'; +import { AppConfigModule } from './config/config.module'; import { DatabaseModule } from '@database/database.module'; import { HorizonModule } from '@horizon/horizon.module'; import { EventsModule } from '@events/events.module'; @@ -17,8 +18,9 @@ import { PerformanceModule } from './performance/performance.module'; isGlobal: true, load: [configuration], }), + AppConfigModule, CacheModule.register({ - ttl: 60 * 1000, // 60s for dashboard/analytics cache + ttl: 60 * 1000, max: 500, isGlobal: true, }), diff --git a/indexer/src/config/config.manager.ts b/indexer/src/config/config.manager.ts new file mode 100644 index 00000000..0213a1ef --- /dev/null +++ b/indexer/src/config/config.manager.ts @@ -0,0 +1,175 @@ +import { Injectable, Logger, OnModuleInit } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; + +/** + * Validated, typed snapshot of all indexer configuration. + * + * Every field has a documented source env-var and default value. + * Validation runs at startup and on every hot-reload. + */ +export interface IndexerConfig { + /** Stellar network name. Env: STELLAR_NETWORK. Default: testnet */ + stellarNetwork: string; + /** Horizon base URL. Env: HORIZON_URL */ + horizonUrl: string; + /** Soroban RPC URL. Env: SOROBAN_RPC_URL */ + sorobanRpcUrl: string; + /** TeachLink contract ID. Env: TEACHLINK_CONTRACT_ID */ + teachlinkContractId: string; + + /** PostgreSQL host. Env: DB_HOST. Default: localhost */ + dbHost: string; + /** PostgreSQL port. Env: DB_PORT. Default: 5432 */ + dbPort: number; + /** PostgreSQL username. Env: DB_USERNAME. Default: teachlink */ + dbUsername: string; + /** PostgreSQL password. Env: DB_PASSWORD */ + dbPassword: string; + /** PostgreSQL database name. Env: DB_DATABASE. Default: teachlink_indexer */ + dbDatabase: string; + /** Auto-synchronize schema. Env: DB_SYNCHRONIZE. Default: false */ + dbSynchronize: boolean; + /** Enable query logging. Env: DB_LOGGING. Default: false */ + dbLogging: boolean; + + /** Event poll interval (ms). Env: INDEXER_POLL_INTERVAL. Default: 5000 */ + pollIntervalMs: number; + /** Starting ledger. Env: INDEXER_START_LEDGER. Default: latest */ + startLedger: string; + /** Events per batch. Env: INDEXER_BATCH_SIZE. Default: 100 */ + batchSize: number; + /** Seconds before indexer is considered stale. Env: INDEXER_STALE_AFTER_SECONDS. Default: 900 */ + staleAfterSeconds: number; + + /** Node environment. Env: NODE_ENV. Default: development */ + nodeEnv: string; + /** HTTP port. Env: PORT. Default: 3000 */ + port: number; + /** Log level. Env: LOG_LEVEL. Default: debug */ + logLevel: string; +} + +/** Validation errors collected during config load. */ +export interface ConfigValidationError { + field: string; + message: string; +} + +/** + * ConfigManager centralizes all configuration access, validates values at + * startup, and supports hot-reload by re-reading from ConfigService on demand. + * + * ## Hot-reload + * Call `reload()` to re-validate and refresh the in-memory snapshot without + * restarting the process. Useful when env vars are updated at runtime (e.g. + * via a secrets manager sidecar). + * + * ## Validation rules + * - `horizonUrl` and `sorobanRpcUrl` must be non-empty strings starting with http + * - `dbPort` must be 1–65535 + * - `pollIntervalMs` must be >= 1000 ms + * - `batchSize` must be 1–10000 + * - `port` must be 1–65535 + */ +@Injectable() +export class ConfigManager implements OnModuleInit { + private readonly logger = new Logger(ConfigManager.name); + private snapshot: IndexerConfig; + + constructor(private readonly configService: ConfigService) {} + + onModuleInit(): void { + this.snapshot = this.load(); + const errors = this.validate(this.snapshot); + if (errors.length > 0) { + for (const e of errors) { + this.logger.error(`Config validation failed [${e.field}]: ${e.message}`); + } + throw new Error( + `Configuration is invalid. Fix the following: ${errors.map((e) => e.field).join(', ')}`, + ); + } + this.logger.log( + `Configuration loaded (network=${this.snapshot.stellarNetwork}, env=${this.snapshot.nodeEnv})`, + ); + } + + /** + * Re-read all values from ConfigService, validate, and update the snapshot. + * Returns validation errors if any; an empty array means success. + */ + reload(): ConfigValidationError[] { + const next = this.load(); + const errors = this.validate(next); + if (errors.length === 0) { + this.snapshot = next; + this.logger.log('Configuration hot-reloaded successfully'); + } else { + this.logger.warn( + `Hot-reload aborted — validation errors: ${errors.map((e) => e.field).join(', ')}`, + ); + } + return errors; + } + + /** Return the current validated configuration snapshot. */ + get(): IndexerConfig { + return this.snapshot; + } + + // ===== Private ===== + + private load(): IndexerConfig { + return { + stellarNetwork: this.configService.get('stellar.network', 'testnet'), + horizonUrl: this.configService.get('stellar.horizonUrl', ''), + sorobanRpcUrl: this.configService.get('stellar.sorobanRpcUrl', ''), + teachlinkContractId: this.configService.get('contract.teachlinkContractId', ''), + + dbHost: this.configService.get('database.host', 'localhost'), + dbPort: this.configService.get('database.port', 5432), + dbUsername: this.configService.get('database.username', 'teachlink'), + dbPassword: this.configService.get('database.password', ''), + dbDatabase: this.configService.get('database.database', 'teachlink_indexer'), + dbSynchronize: this.configService.get('database.synchronize', false), + dbLogging: this.configService.get('database.logging', false), + + pollIntervalMs: this.configService.get('indexer.pollInterval', 5000), + startLedger: this.configService.get('indexer.startLedger', 'latest'), + batchSize: this.configService.get('indexer.batchSize', 100), + staleAfterSeconds: this.configService.get('indexer.staleAfterSeconds', 900), + + nodeEnv: this.configService.get('app.nodeEnv', 'development'), + port: this.configService.get('app.port', 3000), + logLevel: this.configService.get('app.logLevel', 'debug'), + }; + } + + private validate(c: IndexerConfig): ConfigValidationError[] { + const errors: ConfigValidationError[] = []; + + if (!c.horizonUrl.startsWith('http')) { + errors.push({ field: 'horizonUrl', message: 'Must be a valid http(s) URL' }); + } + if (!c.sorobanRpcUrl.startsWith('http')) { + errors.push({ field: 'sorobanRpcUrl', message: 'Must be a valid http(s) URL' }); + } + if (c.dbPort < 1 || c.dbPort > 65535) { + errors.push({ field: 'dbPort', message: 'Must be between 1 and 65535' }); + } + if (!c.dbPassword) { + errors.push({ field: 'dbPassword', message: 'DB_PASSWORD must be set' }); + } + if (c.pollIntervalMs < 1000) { + errors.push({ field: 'pollIntervalMs', message: 'Must be >= 1000 ms' }); + } + if (c.batchSize < 1 || c.batchSize > 10000) { + errors.push({ field: 'batchSize', message: 'Must be between 1 and 10000' }); + } + if (c.port < 1 || c.port > 65535) { + errors.push({ field: 'port', message: 'Must be between 1 and 65535' }); + } + + return errors; + } +} diff --git a/indexer/src/config/config.module.ts b/indexer/src/config/config.module.ts new file mode 100644 index 00000000..03ec430d --- /dev/null +++ b/indexer/src/config/config.module.ts @@ -0,0 +1,22 @@ +import { Module } from '@nestjs/common'; +import { ConfigModule as NestConfigModule } from '@nestjs/config'; +import configuration from './configuration'; +import { ConfigManager } from './config.manager'; + +/** + * Provides centralized, validated configuration to the rest of the application. + * + * Import this module once in AppModule. Inject `ConfigManager` wherever you + * need typed config access or want to trigger a hot-reload. + */ +@Module({ + imports: [ + NestConfigModule.forRoot({ + isGlobal: true, + load: [configuration], + }), + ], + providers: [ConfigManager], + exports: [ConfigManager], +}) +export class AppConfigModule {} diff --git a/indexer/src/reporting/reporting.controller.ts b/indexer/src/reporting/reporting.controller.ts index cd62218e..cd5c6c6f 100644 --- a/indexer/src/reporting/reporting.controller.ts +++ b/indexer/src/reporting/reporting.controller.ts @@ -12,6 +12,7 @@ import { DashboardService } from './dashboard.service'; import { ReportExportService, ExportFormat } from './report-export.service'; import { AlertService } from './alert.service'; import { ReportType } from '@database/entities/dashboard-snapshot.entity'; +import { ConfigManager } from '../config/config.manager'; /** * API for advanced analytics and reporting dashboard: @@ -27,6 +28,7 @@ export class ReportingController { private dashboardService: DashboardService, private reportExportService: ReportExportService, private alertService: AlertService, + private configManager: ConfigManager, ) {} /** Current aggregate metrics for dashboard visualization */ @@ -41,6 +43,25 @@ export class ReportingController { return this.dashboardService.getSustainabilitySnapshot(); } + /** Return the current validated configuration snapshot */ + @Get('config') + getConfig() { + const c = this.configManager.get(); + // Omit sensitive fields before returning + const { dbPassword, ...safe } = c; + return safe; + } + + /** Hot-reload configuration from environment without restarting */ + @Post('config/reload') + reloadConfig() { + const errors = this.configManager.reload(); + if (errors.length > 0) { + return { success: false, errors }; + } + return { success: true }; + } + /** Generate and persist a report snapshot (manual trigger) */ @Post('reports/snapshots') async generateSnapshot( diff --git a/indexer/src/reporting/reporting.module.ts b/indexer/src/reporting/reporting.module.ts index 03586135..f7f1a778 100644 --- a/indexer/src/reporting/reporting.module.ts +++ b/indexer/src/reporting/reporting.module.ts @@ -10,6 +10,8 @@ import { AlertLog, } from '@database/entities'; import { PerformanceModule } from '../performance/performance.module'; +import { AppConfigModule } from '../config/config.module'; +import { ConfigManager } from '../config/config.manager'; import { DashboardService } from './dashboard.service'; import { ReportExportService } from './report-export.service'; import { ReportSchedulerService } from './report-scheduler.service'; @@ -19,6 +21,7 @@ import { ReportingController } from './reporting.controller'; @Module({ imports: [ PerformanceModule, + AppConfigModule, TypeOrmModule.forFeature([ BridgeTransaction, Escrow, @@ -30,7 +33,7 @@ import { ReportingController } from './reporting.controller'; ]), ], controllers: [ReportingController], - providers: [DashboardService, ReportExportService, ReportSchedulerService, AlertService], + providers: [DashboardService, ReportExportService, ReportSchedulerService, AlertService, ConfigManager], exports: [DashboardService, ReportExportService, AlertService], }) export class ReportingModule {}