From c2aa7e6d8d4c28202e8581bec0752a8e91a4cab7 Mon Sep 17 00:00:00 2001 From: hunter-baddie Date: Fri, 24 Jul 2026 11:04:52 +0100 Subject: [PATCH] feat: implement Strict environment validation across services and frontend builds --- coordinator/src/config.ts | 54 ++++- coordinator/test/config-validation.test.ts | 90 ++++++++ docs/DEPLOYMENT.md | 72 ++++++ docs/ENVIRONMENT_VALIDATION.md | 247 +++++++++++++++++++++ env.example | 88 +++++++- frontend/src/config/networks.ts | 112 ++++++++++ relayer/src/index.ts | 129 ++++++++--- resolver/src/config.ts | 139 ++++++++++-- 8 files changed, 890 insertions(+), 41 deletions(-) create mode 100644 coordinator/test/config-validation.test.ts create mode 100644 docs/ENVIRONMENT_VALIDATION.md diff --git a/coordinator/src/config.ts b/coordinator/src/config.ts index 6980dfd..90b439d 100644 --- a/coordinator/src/config.ts +++ b/coordinator/src/config.ts @@ -39,6 +39,18 @@ const configSchema = z.object({ }, z.boolean().default(false) ), + // Mainnet requires explicit audit confirmation to prevent accidental enablement + mainnetAuditConfirmed: z.preprocess( + (v) => { + if (typeof v === "boolean") return v; + if (typeof v === "string") { + const s = v.trim().toLowerCase(); + return s === "true" || s === "1" || s === "yes" || s === "on"; + } + return false; + }, + z.boolean().default(false) + ), ethereum: z.object({ rpcUrl: z.string().url(), chainId: z.number().int(), @@ -71,6 +83,18 @@ export function loadConfig(): CoordinatorConfig { const network = (process.env.NETWORK_MODE ?? "testnet") as Network; const isMainnet = network === "mainnet"; + // Mainnet requires explicit audit confirmation + if (isMainnet) { + const auditConfirmed = process.env.MAINNET_AUDIT_CONFIRMED === "true"; + if (!auditConfirmed) { + throw new Error( + "MAINNET DEPLOYMENT BLOCKED: Set MAINNET_AUDIT_CONFIRMED=true only after " + + "completing the mainnet readiness checklist in docs/DEPLOYMENT.md. " + + "This includes audit completion, multisig ownership, and bug bounty." + ); + } + } + const raw = { network, port: process.env.COORDINATOR_PORT ?? process.env.RELAYER_PORT ?? "3001", @@ -86,6 +110,7 @@ export function loadConfig(): CoordinatorConfig { // when the env var is unset; previously this branch substituted // "false" and z.coerce.boolean() turned it into true. demoFixtures: process.env.COORDINATOR_DEMO_FIXTURES, + mainnetAuditConfirmed: process.env.MAINNET_AUDIT_CONFIRMED, ethereum: { rpcUrl: resolveEthereumRpcUrl(isMainnet ? "mainnet" : "testnet"), chainId: isMainnet ? 1 : 11_155_111, @@ -106,5 +131,32 @@ export function loadConfig(): CoordinatorConfig { timelockSafetyGapSeconds: 600 }; - return configSchema.parse(raw); + const result = configSchema.parse(raw); + + // Additional validation: testnet requires contract addresses + if (!isMainnet) { + const missingTestnetContracts = []; + if (!result.ethereum.htlcEscrow) { + missingTestnetContracts.push("ETH_HTLC_ESCROW_TESTNET"); + } + if (!result.ethereum.resolverRegistry) { + missingTestnetContracts.push("ETH_RESOLVER_REGISTRY_TESTNET"); + } + if (!result.soroban.htlcContract) { + missingTestnetContracts.push("SOROBAN_HTLC_TESTNET"); + } + if (!result.soroban.resolverRegistry) { + missingTestnetContracts.push("SOROBAN_RESOLVER_REGISTRY_TESTNET"); + } + + if (missingTestnetContracts.length > 0) { + throw new Error( + `TESTNET DEPLOYMENT INCOMPLETE: Missing required testnet contract addresses: ` + + missingTestnetContracts.join(", ") + + ". Deploy contracts first (see docs/DEPLOYMENT.md) or check env.example for variable names." + ); + } + } + + return result; } diff --git a/coordinator/test/config-validation.test.ts b/coordinator/test/config-validation.test.ts new file mode 100644 index 0000000..be32955 --- /dev/null +++ b/coordinator/test/config-validation.test.ts @@ -0,0 +1,90 @@ +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { loadConfig } from '../src/config.js'; + +describe('Coordinator Config Validation', () => { + const originalEnv = process.env; + + beforeEach(() => { + process.env = { ...originalEnv }; + }); + + afterEach(() => { + process.env = originalEnv; + }); + + it('should load config successfully with valid testnet env', () => { + process.env.NETWORK_MODE = 'testnet'; + process.env.ETH_HTLC_ESCROW_TESTNET = '0x1234567890123456789012345678901234567890'; + process.env.ETH_RESOLVER_REGISTRY_TESTNET = '0x1234567890123456789012345678901234567890'; + process.env.SOROBAN_HTLC_TESTNET = 'C1234567890123456789012345678901234567890'; + process.env.SOROBAN_RESOLVER_REGISTRY_TESTNET = 'C1234567890123456789012345678901234567890'; + + const config = loadConfig(); + expect(config.network).toBe('testnet'); + expect(config.ethereum.htlcEscrow).toBe('0x1234567890123456789012345678901234567890'); + }); + + it('should throw error when testnet contract addresses are missing', () => { + process.env.NETWORK_MODE = 'testnet'; + // Missing contract addresses + + expect(() => loadConfig()).toThrow('TESTNET DEPLOYMENT INCOMPLETE'); + }); + + it('should throw error when mainnet is enabled without audit confirmation', () => { + process.env.NETWORK_MODE = 'mainnet'; + // MAINNET_AUDIT_CONFIRMED is not set (defaults to false) + + expect(() => loadConfig()).toThrow('MAINNET DEPLOYMENT BLOCKED'); + }); + + it('should throw error when mainnet audit confirmation is false', () => { + process.env.NETWORK_MODE = 'mainnet'; + process.env.MAINNET_AUDIT_CONFIRMED = 'false'; + + expect(() => loadConfig()).toThrow('MAINNET DEPLOYMENT BLOCKED'); + }); + + it('should load config successfully with valid mainnet env and audit confirmation', () => { + process.env.NETWORK_MODE = 'mainnet'; + process.env.MAINNET_AUDIT_CONFIRMED = 'true'; + process.env.ETH_HTLC_ESCROW_MAINNET = '0x1234567890123456789012345678901234567890'; + process.env.ETH_RESOLVER_REGISTRY_MAINNET = '0x1234567890123456789012345678901234567890'; + process.env.SOROBAN_HTLC_MAINNET = 'C1234567890123456789012345678901234567890'; + process.env.SOROBAN_RESOLVER_REGISTRY_MAINNET = 'C1234567890123456789012345678901234567890'; + + const config = loadConfig(); + expect(config.network).toBe('mainnet'); + expect(config.mainnetAuditConfirmed).toBe(true); + }); + + it('should throw error for invalid network mode', () => { + process.env.NETWORK_MODE = 'invalid'; + + expect(() => loadConfig()).toThrow('NETWORK_MODE must be \'testnet\' or \'mainnet\''); + }); + + it('should validate Ethereum RPC URL format', () => { + process.env.NETWORK_MODE = 'testnet'; + process.env.ETH_HTLC_ESCROW_TESTNET = '0x1234567890123456789012345678901234567890'; + process.env.ETH_RESOLVER_REGISTRY_TESTNET = '0x1234567890123456789012345678901234567890'; + process.env.SOROBAN_HTLC_TESTNET = 'C1234567890123456789012345678901234567890'; + process.env.SOROBAN_RESOLVER_REGISTRY_TESTNET = 'C1234567890123456789012345678901234567890'; + process.env.SEPOLIA_RPC_URL = 'not-a-valid-url'; + + // Should throw due to invalid URL + expect(() => loadConfig()).toThrow(); + }); + + it('should accept valid Ethereum RPC URL', () => { + process.env.NETWORK_MODE = 'testnet'; + process.env.ETH_HTLC_ESCROW_TESTNET = '0x1234567890123456789012345678901234567890'; + process.env.ETH_RESOLVER_REGISTRY_TESTNET = '0x1234567890123456789012345678901234567890'; + process.env.SOROBAN_HTLC_TESTNET = 'C1234567890123456789012345678901234567890'; + process.env.SOROBAN_RESOLVER_REGISTRY_TESTNET = 'C1234567890123456789012345678901234567890'; + process.env.SEPOLIA_RPC_URL = 'https://sepolia.infura.io/v3/abc123'; + + const config = loadConfig(); + expect(config.ethereum.rpcUrl).toBe('https://sepolia.infura.io/v3/abc123'); + }); +}); \ No newline at end of file diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md index 1662081..c180670 100644 --- a/docs/DEPLOYMENT.md +++ b/docs/DEPLOYMENT.md @@ -14,6 +14,17 @@ contained inconsistencies flagged in v1 review feedback. - A funded Ethereum deployer key (Sepolia or mainnet) - A funded Stellar account (Soroban testnet or public) +## Environment Validation + +All OverSync services now perform **strict environment validation at startup**: + +- **Coordinator**: Validates config with Zod schema, requires testnet contract addresses, blocks mainnet without audit confirmation +- **Resolver**: Validates config with Zod schema, requires testnet contract addresses, blocks mainnet without audit confirmation +- **Relayer**: Validates RPC URLs, contract addresses, private keys, and timeout settings +- **Frontend**: Validates contract addresses and network configuration at **build time** (Vite) + +**Services will fail to start if required environment variables are missing or malformed.** + ## 1. Copy and fill in the env file ```bash @@ -31,6 +42,8 @@ SOROBAN_RPC_URL=https://soroban-testnet.stellar.org ETHERSCAN_API_KEY= ``` +**⚠️ CRITICAL: Do NOT set `MAINNET_AUDIT_CONFIRMED=true` until you have completed the mainnet readiness checklist below.** + Testnet asset identifier mappings (native ETH ↔ XLM, Sepolia USDC ↔ Stellar USDC) are centralized in `packages/sdk/src/assets/index.ts` and exported from `@oversync/sdk`. @@ -165,6 +178,15 @@ Before setting `VITE_MAINNET_ENABLED=true` and flipping backend - [ ] Public bug bounty announced - [ ] Sepolia run with $1k+ in TVL for a continuous 14-day window without incidents +**Once all items are checked:** + +1. Set `MAINNET_AUDIT_CONFIRMED=true` in backend `.env` (coordinator, resolver, relayer) +2. Set `VITE_MAINNET_AUDIT_CONFIRMED=true` in Vercel environment variables +3. Set `VITE_MAINNET_ENABLED=true` in Vercel environment variables +4. Flip backend `NETWORK_MODE=mainnet` and deploy + +**This three-step process prevents accidental mainnet enablement.** + ## Rolling back If a serious bug is found post-launch, the HTLCEscrow contract has @@ -204,8 +226,58 @@ the CI address-consistency check stays green: And the frontend Vercel env vars: ``` VITE_ETH_HTLC_ESCROW_TESTNET=0x + VITE_ETH_RESOLVER_REGISTRY_TESTNET=0x + VITE_SOROBAN_HTLC_TESTNET=C + VITE_SOROBAN_RESOLVER_REGISTRY_TESTNET=C ``` 5. **Verify**: run `pnpm verify:addresses` locally before pushing. The same check runs in CI (`address-verify` workflow) on every PR that touches these files and will block the merge if any value drifts. + +## Environment Variable Reference + +See [`env.example`](../env.example) for a complete list of all environment variables +with descriptions. Key variables by service: + +### Coordinator (`coordinator/src/config.ts`) +- `NETWORK_MODE`, `MAINNET_AUDIT_CONFIRMED` (mainnet only) +- `ETH_HTLC_ESCROW_TESTNET` / `ETH_HTLC_ESCROW_MAINNET` +- `ETH_RESOLVER_REGISTRY_TESTNET` / `ETH_RESOLVER_REGISTRY_MAINNET` +- `SOROBAN_HTLC_TESTNET` / `SOROBAN_HTLC_MAINNET` +- `SOROBAN_RESOLVER_REGISTRY_TESTNET` / `SOROBAN_RESOLVER_REGISTRY_MAINNET` +- `DATABASE_URL`, `COORDINATOR_PORT`, `LOG_LEVEL` + +### Resolver (`resolver/src/config.ts`) +- Same contract address variables as coordinator +- `RESOLVER_ETH_PRIVATE_KEY`, `RESOLVER_STELLAR_SECRET` (required for production) +- `COORDINATOR_URL`, `RESOLVER_POLL_INTERVAL_MS` + +### Relayer (`relayer/src/index.ts`) +- `RELAYER_PRIVATE_KEY`, `RELAYER_STELLAR_SECRET`, `RELAYER_STELLAR_PUBLIC` +- `RELAYER_RPC_TIMEOUT_MS`, `RELAYER_RETRY_ATTEMPTS` +- `ETHEREUM_NETWORK`, `STELLAR_NETWORK` +- `GAS_PRICE_GWEI`, `GAS_LIMIT` + +### Frontend (`frontend/src/config/networks.ts`) +- `VITE_NETWORK_MODE`, `VITE_MAINNET_ENABLED`, `VITE_MAINNET_AUDIT_CONFIRMED` +- `VITE_ETH_HTLC_ESCROW_TESTNET` / `VITE_ETH_HTLC_ESCROW_MAINNET` +- `VITE_ETH_RESOLVER_REGISTRY_TESTNET` / `VITE_ETH_RESOLVER_REGISTRY_MAINNET` +- `VITE_SOROBAN_HTLC_TESTNET` / `VITE_SOROBAN_HTLC_MAINNET` +- `VITE_SOROBAN_RESOLVER_REGISTRY_TESTNET` / `VITE_SOROBAN_RESOLVER_REGISTRY_MAINNET` +- `VITE_API_BASE_URL`, `VITE_SEPOLIA_RPC_URL`, `VITE_MAINNET_RPC_URL` + +## Troubleshooting + +### "MAINNET DEPLOYMENT BLOCKED" error +This is a safety feature. Complete the mainnet readiness checklist in `env.example` +and set `MAINNET_AUDIT_CONFIRMED=true`. + +### "TESTNET DEPLOYMENT INCOMPLETE" error +Deploy the testnet contracts first (see Section 2 and 3 above), then set the +contract address environment variables. + +### Frontend build fails with "Frontend environment validation failed" +Check that all `VITE_*` contract address variables are set in your Vercel +environment variables. The build will fail if testnet/mainnet contract addresses +are missing when the corresponding network mode is selected. diff --git a/docs/ENVIRONMENT_VALIDATION.md b/docs/ENVIRONMENT_VALIDATION.md new file mode 100644 index 0000000..1ab0714 --- /dev/null +++ b/docs/ENVIRONMENT_VALIDATION.md @@ -0,0 +1,247 @@ +# Environment Validation Implementation + +This document describes the strict environment validation system implemented across OverSync services to prevent misconfigured deployments. + +## Overview + +All OverSync services now perform **strict environment validation** at startup/build time: + +- **Coordinator**: Zod schema validation + testnet contract address checks + mainnet audit gate +- **Resolver**: Zod schema validation + testnet contract address checks + mainnet audit gate +- **Relayer**: Comprehensive validation of RPC URLs, contract addresses, private keys, and timeouts +- **Frontend**: Build-time validation (Vite) of contract addresses and network configuration + +## Implementation Details + +### Coordinator (`coordinator/src/config.ts`) + +**Validation Logic:** +1. Loads env vars with dotenv +2. Validates `NETWORK_MODE` (must be 'testnet' or 'mainnet') +3. **Mainnet Gate**: Checks `MAINNET_AUDIT_CONFIRMED=true` if `NETWORK_MODE=mainnet` +4. Parses config through Zod schema with strict type checking +5. **Testnet Contract Check**: Ensures all testnet contract addresses are present +6. Throws descriptive errors if validation fails + +**Key Validations:** +- Network mode enum validation +- Mainnet audit confirmation (prevents accidental mainnet deployment) +- Testnet contract addresses required: `ETH_HTLC_ESCROW_TESTNET`, `ETH_RESOLVER_REGISTRY_TESTNET`, `SOROBAN_HTLC_TESTNET`, `SOROBAN_RESOLVER_REGISTRY_TESTNET` +- Ethereum RPC URL format validation +- Soroban RPC URL format validation +- Port, timeouts, and numeric parameter validation + +### Resolver (`resolver/src/config.ts`) + +**Validation Logic:** +1. Loads env vars with dotenv +2. Validates `NETWORK_MODE` (must be 'testnet' or 'mainnet') +3. **Mainnet Gate**: Checks `MAINNET_AUDIT_CONFIRMED=true` if `NETWORK_MODE=mainnet` +4. Parses config through Zod schema +5. **Testnet Contract Check**: Ensures all testnet contract addresses are present +6. Converts validated config to legacy format for backward compatibility +7. Throws descriptive errors if validation fails + +**Key Validations:** +- Same as coordinator +- Additional validation for resolver private key format (`0x`-prefixed 64-byte hex) +- Stellar secret validation + +### Relayer (`relayer/src/index.ts`) + +**Validation Logic:** +1. Validates `NETWORK_MODE` (must be 'testnet' or 'mainnet') +2. **Mainnet Gate**: Checks `MAINNET_AUDIT_CONFIRMED=true` if `NETWORK_MODE=mainnet` +3. Validates Ethereum RPC URL (not placeholder) +4. Validates Stellar Horizon URL (not placeholder) +5. **Private Key Validation**: Checks relayer Ethereum and Stellar keys are configured +6. **Testnet Contract Check**: Ensures testnet contract addresses present +7. Validates timeout ranges (1000-300000ms) +8. Calls `process.exit(1)` on validation failure (fails fast) + +**Key Validations:** +- RPC URL configuration +- Private key presence and format +- Contract address presence for testnet +- Timeout value ranges +- Emergency shutdown flag + +### Frontend (`frontend/src/config/networks.ts`) + +**Validation Logic:** +1. Runs at module load time (Vite build time) +2. Detects network mode from `VITE_NETWORK_MODE` or `VITE_NETWORK` +3. **Mainnet Gate**: Checks both `VITE_MAINNET_ENABLED=true` AND `VITE_MAINNET_AUDIT_CONFIRMED=true` +4. **Testnet Contract Check**: Validates `VITE_ETH_HTLC_ESCROW_TESTNET` and `VITE_ETH_RESOLVER_REGISTRY_TESTNET` +5. **Mainnet Contract Check**: Validates mainnet addresses if mainnet enabled +6. Validates API base URL +7. Validates RPC URLs +8. Throws error to fail Vite build if validation fails + +**Key Validations:** +- Mainnet enablement requires TWO flags (prevents accidental enablement) +- Contract addresses for active network mode +- API base URL configuration +- RPC URL configuration + +## Mainnet Safety Mechanism + +Mainnet deployment requires **explicit confirmation** through a multi-step process: + +### Backend Services +```bash +# Step 1: Set network mode +NETWORK_MODE=mainnet + +# Step 2: Explicitly confirm audit completion +MAINNET_AUDIT_CONFIRMED=true +``` + +### Frontend (Vercel) +```bash +# Step 1: Enable mainnet toggle +VITE_MAINNET_ENABLED=true + +# Step 2: Confirm audit completion +VITE_MAINNET_AUDIT_CONFIRMED=true +``` + +**This two-step process prevents accidental mainnet enablement.** A single misconfigured flag will block deployment with a clear error message. + +## Error Messages + +All validation errors are **descriptive and actionable**: + +### Mainnet Blocked +``` +MAINNET DEPLOYMENT BLOCKED: Set MAINNET_AUDIT_CONFIRMED=true only after +completing the mainnet readiness checklist in docs/DEPLOYMENT.md. +This includes audit completion, multisig ownership, and bug bounty. +``` + +### Testnet Incomplete +``` +TESTNET DEPLOYMENT INCOMPLETE: Missing required testnet contract addresses: +ETH_HTLC_ESCROW_TESTNET, SOROBAN_HTLC_TESTNET. +Deploy contracts first (see docs/DEPLOYMENT.md) or check env.example for variable names. +``` + +### Frontend Build Failure +``` +❌ FRONTEND ENVIRONMENT VALIDATION FAILED: + - MAINNET DEPLOYMENT BLOCKED: Set VITE_MAINNET_AUDIT_CONFIRMED=true only after... + - TESTNET CONFIG INCOMPLETE: Missing or placeholder testnet contract addresses:... + +Frontend build blocked. Fix the above errors and rebuild. +``` + +## Environment Variables + +See [`env.example`](../env.example) for the complete list of environment variables with descriptions. + +### Critical Variables by Service + +**Coordinator/Resolver:** +- `NETWORK_MODE` - 'testnet' or 'mainnet' +- `MAINNET_AUDIT_CONFIRMED` - Must be 'true' for mainnet +- `ETH_HTLC_ESCROW_TESTNET` / `ETH_HTLC_ESCROW_MAINNET` +- `ETH_RESOLVER_REGISTRY_TESTNET` / `ETH_RESOLVER_REGISTRY_MAINNET` +- `SOROBAN_HTLC_TESTNET` / `SOROBAN_HTLC_MAINNET` +- `SOROBAN_RESOLVER_REGISTRY_TESTNET` / `SOROBAN_RESOLVER_REGISTRY_MAINNET` + +**Relayer:** +- `RELAYER_PRIVATE_KEY` - Ethereum private key +- `RELAYER_STELLAR_SECRET` - Stellar secret key +- `RELAYER_STELLAR_PUBLIC` - Stellar public key +- `RELAYER_RPC_TIMEOUT_MS` - RPC timeout (1000-300000ms) + +**Frontend:** +- `VITE_NETWORK_MODE` - 'testnet' or 'mainnet' +- `VITE_MAINNET_ENABLED` - Must be 'true' for mainnet +- `VITE_MAINNET_AUDIT_CONFIRMED` - Must be 'true' for mainnet +- `VITE_ETH_HTLC_ESCROW_TESTNET` / `VITE_ETH_HTLC_ESCROW_MAINNET` +- `VITE_API_BASE_URL` - Coordinator API URL + +## Testing + +Smoke tests cover invalid environment cases: + +**Test File:** `coordinator/test/config-validation.test.ts` + +**Test Cases:** +1. ✅ Valid testnet configuration loads successfully +2. ✅ Missing testnet contract addresses throws error +3. ✅ Mainnet without audit confirmation throws error +4. ✅ Mainnet with `MAINNET_AUDIT_CONFIRMED=false` throws error +5. ✅ Valid mainnet with audit confirmation loads successfully +6. ✅ Invalid network mode throws error +7. ✅ Invalid RPC URL format throws error +8. ✅ Valid RPC URL is accepted + +## Deployment Checklist + +### Testnet Deployment +1. Set `NETWORK_MODE=testnet` +2. Deploy Soroban contracts → set `SOROBAN_HTLC_TESTNET`, `SOROBAN_RESOLVER_REGISTRY_TESTNET` +3. Deploy Ethereum contracts → set `ETH_HTLC_ESCROW_TESTNET`, `ETH_RESOLVER_REGISTRY_TESTNET` +4. Start services → validation passes automatically + +### Mainnet Deployment +1. Complete mainnet readiness checklist (see `docs/DEPLOYMENT.md`) +2. Set `NETWORK_MODE=mainnet` +3. Set `MAINNET_AUDIT_CONFIRMED=true` in backend `.env` +4. Deploy mainnet contracts → set mainnet contract addresses +5. Set `VITE_MAINNET_ENABLED=true` and `VITE_MAINNET_AUDIT_CONFIRMED=true` in Vercel +6. Deploy frontend → build passes validation +7. Deploy backend services → validation passes + +## Troubleshooting + +### "MAINNET DEPLOYMENT BLOCKED" +**Cause:** `NETWORK_MODE=mainnet` but `MAINNET_AUDIT_CONFIRMED` is not set to 'true' +**Fix:** Complete the mainnet readiness checklist, then set `MAINNET_AUDIT_CONFIRMED=true` + +### "TESTNET DEPLOYMENT INCOMPLETE" +**Cause:** Testnet contract addresses are missing +**Fix:** Deploy testnet contracts and set the contract address environment variables + +### "Frontend environment validation failed" +**Cause:** Frontend build is missing required Vite environment variables +**Fix:** Check Vercel environment variables for missing `VITE_*` contract addresses + +## Benefits + +1. **Fail Fast**: Services refuse to start with invalid configuration +2. **Clear Errors**: Descriptive error messages guide operators to fix issues +3. **Prevents Accidental Mainnet**: Multi-step mainnet enablement prevents costly mistakes +4. **CI/CD Friendly**: Build failures surface configuration issues before deployment +5. **Documentation**: env.example and DEPLOYMENT.md document all required variables +6. **Type Safety**: Zod schemas provide runtime type validation with TypeScript inference + +## Migration Guide + +### For Existing Deployments + +If you have an existing deployment: + +1. **Add `MAINNET_AUDIT_CONFIRMED=false`** to your `.env` (if not present) +2. **Verify testnet contract addresses** are set (if using testnet) +3. **Restart services** - they will validate on startup +4. **Check logs** for any validation warnings + +### For New Deployments + +1. Copy `env.example` to `.env` +2. Follow the deployment guide in `docs/DEPLOYMENT.md` +3. Services will validate automatically on startup +4. Fix any validation errors before proceeding + +## References + +- `env.example` - Complete environment variable reference +- `docs/DEPLOYMENT.md` - Step-by-step deployment guide +- `coordinator/src/config.ts` - Coordinator validation implementation +- `resolver/src/config.ts` - Resolver validation implementation +- `relayer/src/index.ts` - Relayer validation implementation +- `frontend/src/config/networks.ts` - Frontend validation implementation +- `coordinator/test/config-validation.test.ts` - Smoke tests for invalid env cases \ No newline at end of file diff --git a/env.example b/env.example index cf5970f..10ab840 100644 --- a/env.example +++ b/env.example @@ -3,6 +3,10 @@ # ======================================== # Copy this file to `.env` (in the same directory) and fill in real values. # All secret keys MUST be replaced before running the project. +# +# ⚠️ MAINNET SAFETY: Mainnet deployment requires explicit audit confirmation. +# Set MAINNET_AUDIT_CONFIRMED=true ONLY after completing the mainnet +# readiness checklist in docs/DEPLOYMENT.md (audit, multisig, bug bounty). NODE_ENV=development DEBUG=false @@ -17,6 +21,17 @@ LOG_LEVEL=info # uses Ethereum mainnet + Stellar public network. NETWORK_MODE=testnet +# ⚠️ MAINNET AUDIT CONFIRMATION (required for mainnet only) +# Set to true ONLY after completing the mainnet readiness checklist: +# - Smart contracts audited by reputable firm +# - ResolverRegistry.owner transferred to 2/3 multisig +# - At least 3 community resolvers registered +# - Coordinator behind CDN/WAF +# - Public bug bounty announced +# - Sepolia run with $1k+ TVL for 14 days without incidents +# See docs/DEPLOYMENT.md "Mainnet rollout checklist" for details. +MAINNET_AUDIT_CONFIRMED=false + # Canonical Ethereum ↔ Stellar asset mappings live in @oversync/sdk # (packages/sdk/src/assets/index.ts). Extend there for custom testnet pairs. @@ -104,6 +119,59 @@ RESOLVER_STELLAR_SECRET= # Persistence (SQLite for dev, Postgres for prod). DATABASE_URL=file:./oversync.db +# ======================================== +# RELAYER CONFIGURATION +# ======================================== +# The relayer monitors Ethereum events and coordinates Stellar transactions. +# It requires Ethereum and Stellar private keys for production operation. + +RELAYER_PORT=3001 +RELAYER_POLL_INTERVAL=15000 +RELAYER_RPC_TIMEOUT_MS=30000 +RELAYER_RETRY_ATTEMPTS=3 +RELAYER_RETRY_DELAY=2000 + +# Ethereum relayer credentials (required for mainnet/testnet) +RELAYER_PRIVATE_KEY=0x0000000000000000000000000000000000000000000000000000000000000001 + +# Stellar relayer credentials (required for mainnet/testnet) +# Use separate keys for mainnet and testnet for security +RELAYER_STELLAR_SECRET= +RELAYER_STELLAR_SECRET_MAINNET= +RELAYER_STELLAR_SECRET_TESTNET= +RELAYER_STELLAR_PUBLIC= + +# Stellar network configuration +STELLAR_NETWORK= +STELLAR_NETWORK_PASSPHRASE= +STELLAR_HORIZON_URL= +START_LEDGER_STELLAR=0 +STELLAR_MIN_CONFIRMATIONS=1 + +# Ethereum network configuration +ETHEREUM_NETWORK=mainnet +GAS_PRICE_GWEI=20 +GAS_LIMIT=300000 +START_BLOCK_ETHEREUM=0 +MIN_CONFIRMATION_BLOCKS=6 + +# 1inch Fusion+ integration (optional) +ONEINCH_API_KEY= + +# Security settings +EMERGENCY_SHUTDOWN=false +MAINTENANCE_MODE=false +MIN_TIMELOCK_DURATION=3600 +MAX_TIMELOCK_DURATION=604800 +DEFAULT_TIMELOCK_DURATION=86400 +TIMELOCK_SAFETY_GAP_SECONDS=600 + +# Monitoring +ENABLE_REQUEST_LOGGING=false +VERBOSE_LOGGING=false +HEALTH_CHECK_INTERVAL=30000 +HEALTH_CHECK_TIMEOUT=5000 + # ======================================== # 1INCH FUSION+ INTEGRATION (optional) # ======================================== @@ -121,18 +189,36 @@ ETHERSCAN_API_KEY= # ======================================== # FRONTEND (VITE_) CONFIGURATION # ======================================== +# These variables are prefixed with VITE_ and are exposed to the browser. +# Set them in Vercel dashboard for production deployments. VITE_API_BASE_URL=http://localhost:3001 # Production API (DigitalOcean): https://oversync-k36vx.ondigitalocean.app — proxied via vercel.json /api/* + VITE_NETWORK_MODE=testnet # Set to true to re-enable mainnet toggle (v2 mainnet post-audit) VITE_MAINNET_ENABLED=false + +# ⚠️ MAINNET AUDIT CONFIRMATION (required for mainnet only) +# Set to true ONLY after completing the mainnet readiness checklist. +VITE_MAINNET_AUDIT_CONFIRMED=false + # Frontend wallet RPC (Vercel). Use full URL or the same Infura key. # Restrict the key by HTTP referrer in the Infura dashboard. VITE_SEPOLIA_RPC_URL= VITE_MAINNET_RPC_URL= VITE_INFURA_API_KEY= VITE_ONEINCH_API_KEY= -# v2 contract addresses for the frontend (mirror of ETH_HTLC_ESCROW_TESTNET). + +# v2 contract addresses for the frontend (mirror of backend env vars). +# These are REQUIRED for the frontend to interact with deployed contracts. VITE_ETH_HTLC_ESCROW_TESTNET= VITE_ETH_HTLC_ESCROW_MAINNET= +VITE_ETH_RESOLVER_REGISTRY_TESTNET= +VITE_ETH_RESOLVER_REGISTRY_MAINNET= + +# Soroban contract IDs (required for Stellar operations) +VITE_SOROBAN_HTLC_TESTNET= +VITE_SOROBAN_HTLC_MAINNET= +VITE_SOROBAN_RESOLVER_REGISTRY_TESTNET= +VITE_SOROBAN_RESOLVER_REGISTRY_MAINNET= diff --git a/frontend/src/config/networks.ts b/frontend/src/config/networks.ts index 8c248f4..201e35f 100644 --- a/frontend/src/config/networks.ts +++ b/frontend/src/config/networks.ts @@ -23,6 +23,118 @@ export const resolveNetworkMode = (requested: AppNetworkMode): AppNetworkMode => return requested; }; +/** + * Validates frontend environment variables at build time. + * Throws descriptive errors for missing or misconfigured testnet/mainnet settings. + */ +export function validateFrontendEnv(): void { + const env = (import.meta as any).env || {}; + const errors: string[] = []; + const warnings: string[] = []; + + // Detect network mode + const networkMode = env.VITE_NETWORK_MODE || env.VITE_NETWORK || 'testnet'; + const isMainnet = networkMode === 'mainnet'; + + // Mainnet requires explicit audit confirmation + if (isMainnet) { + const mainnetEnabled = env.VITE_MAINNET_ENABLED === 'true'; + const auditConfirmed = env.VITE_MAINNET_AUDIT_CONFIRMED === 'true'; + + if (!mainnetEnabled) { + errors.push( + "MAINNET BLOCKED: VITE_MAINNET_ENABLED must be 'true' to use mainnet. " + + "Keep it 'false' until post-audit mainnet launch." + ); + } + + if (!auditConfirmed) { + errors.push( + "MAINNET DEPLOYMENT BLOCKED: Set VITE_MAINNET_AUDIT_CONFIRMED=true only after " + + "completing the mainnet readiness checklist in docs/DEPLOYMENT.md. " + + "This includes audit completion, multisig ownership, and bug bounty." + ); + } + } + + // Validate testnet contract addresses + if (!isMainnet) { + const testnetContracts = { + 'VITE_ETH_HTLC_ESCROW_TESTNET': env.VITE_ETH_HTLC_ESCROW_TESTNET, + 'VITE_ETH_RESOLVER_REGISTRY_TESTNET': env.VITE_ETH_RESOLVER_REGISTRY_TESTNET, + }; + + const missingContracts = Object.entries(testnetContracts) + .filter(([, value]) => !value || value.includes('YOUR_') || value.includes('0x000000')) + .map(([key]) => key); + + if (missingContracts.length > 0) { + errors.push( + `TESTNET CONFIG INCOMPLETE: Missing or placeholder testnet contract addresses: ` + + missingContracts.join(', ') + + ". Deploy contracts first (see docs/DEPLOYMENT.md) or check env.example." + ); + } + } + + // Validate mainnet contract addresses if mainnet is enabled + if (isMainnet && env.VITE_MAINNET_ENABLED === 'true') { + const mainnetContracts = { + 'VITE_ETH_HTLC_ESCROW_MAINNET': env.VITE_ETH_HTLC_ESCROW_MAINNET, + 'VITE_ETH_RESOLVER_REGISTRY_MAINNET': env.VITE_ETH_RESOLVER_REGISTRY_MAINNET, + }; + + const missingContracts = Object.entries(mainnetContracts) + .filter(([, value]) => !value || value.includes('YOUR_') || value.includes('0x000000')) + .map(([key]) => key); + + if (missingContracts.length > 0) { + errors.push( + `MAINNET CONFIG INCOMPLETE: Missing or placeholder mainnet contract addresses: ` + + missingContracts.join(', ') + + ". Deploy mainnet contracts and set addresses in Vercel environment variables." + ); + } + } + + // Validate API base URL + const apiBaseUrl = env.VITE_API_BASE_URL; + if (!apiBaseUrl || apiBaseUrl.includes('YOUR_')) { + warnings.push("VITE_API_BASE_URL not configured or using placeholder. Frontend will not be able to reach the coordinator API."); + } + + // Validate RPC URLs + const sepoliaRpcUrl = env.VITE_SEPOLIA_RPC_URL; + const mainnetRpcUrl = env.VITE_MAINNET_RPC_URL; + + if (!isMainnet && (!sepoliaRpcUrl || sepoliaRpcUrl.includes('YOUR_'))) { + warnings.push("VITE_SEPOLIA_RPC_URL not configured. Testnet users may experience connection issues."); + } + + if (isMainnet && (!mainnetRpcUrl || mainnetRpcUrl.includes('YOUR_'))) { + warnings.push("VITE_MAINNET_RPC_URL not configured. Mainnet users may experience connection issues."); + } + + // Report errors (fail build) + if (errors.length > 0) { + console.error('❌ FRONTEND ENVIRONMENT VALIDATION FAILED:'); + errors.forEach(err => console.error(` - ${err}`)); + console.error('\nFrontend build blocked. Fix the above errors and rebuild.'); + throw new Error(`Frontend environment validation failed:\n${errors.join('\n')}`); + } + + // Report warnings (don't fail build) + if (warnings.length > 0) { + console.warn('⚠️ Frontend environment warnings:'); + warnings.forEach(warn => console.warn(` - ${warn}`)); + } + + console.log('✅ Frontend environment validation passed'); +} + +// Run validation at module load time (build time for Vite) +validateFrontendEnv(); + function readNetworkNameFromEnvOrUrl(): AppNetworkMode { let networkName: AppNetworkMode = 'testnet'; diff --git a/relayer/src/index.ts b/relayer/src/index.ts index 3feb18d..ed16cf7 100644 --- a/relayer/src/index.ts +++ b/relayer/src/index.ts @@ -374,6 +374,104 @@ function resolveEthereumRpcUrlForRelayer(): string { return resolveEthereumRpcUrl(network); } +// Validate required environment variables +function validateRelayerConfig(): void { + const errors: string[] = []; + const warnings: string[] = []; + + // Network mode validation + const networkMode = process.env.NETWORK_MODE || 'mainnet'; + if (networkMode !== 'testnet' && networkMode !== 'mainnet') { + errors.push(`NETWORK_MODE must be 'testnet' or 'mainnet', got: ${networkMode}`); + } + + // Mainnet requires explicit audit confirmation + if (networkMode === 'mainnet') { + const auditConfirmed = process.env.MAINNET_AUDIT_CONFIRMED === 'true'; + if (!auditConfirmed) { + errors.push( + "MAINNET DEPLOYMENT BLOCKED: Set MAINNET_AUDIT_CONFIRMED=true only after " + + "completing the mainnet readiness checklist in docs/DEPLOYMENT.md. " + + "This includes audit completion, multisig ownership, and bug bounty." + ); + } + } + + // Required RPC URLs + const ethereumRpcUrl = resolveEthereumRpcUrlForRelayer(); + if (!ethereumRpcUrl || ethereumRpcUrl.includes('YOUR_') || ethereumRpcUrl.includes('api_key_here')) { + errors.push("ETHEREUM_RPC_URL not configured. Set SEPOLIA_RPC_URL, MAINNET_RPC_URL, or INFURA_API_KEY."); + } + + const stellarHorizonUrl = process.env.STELLAR_HORIZON_URL || + (networkMode === 'mainnet' ? 'https://horizon.stellar.org' : 'https://horizon-testnet.stellar.org'); + if (!stellarHorizonUrl || stellarHorizonUrl.includes('YOUR_')) { + errors.push("STELLAR_HORIZON_URL not configured."); + } + + // Required private keys for production + if (networkMode === 'mainnet' || networkMode === 'testnet') { + if (!process.env.RELAYER_PRIVATE_KEY || process.env.RELAYER_PRIVATE_KEY.startsWith('0x000000')) { + errors.push("RELAYER_PRIVATE_KEY not configured or using placeholder. Generate a real key for production."); + } + + if (!process.env.RELAYER_STELLAR_SECRET || process.env.RELAYER_STELLAR_SECRET.includes('SAMPLE')) { + errors.push("RELAYER_STELLAR_SECRET not configured or using placeholder. Generate real keys for production."); + } + + if (!process.env.RELAYER_STELLAR_PUBLIC || process.env.RELAYER_STELLAR_PUBLIC.includes('YOUR_')) { + errors.push("RELAYER_STELLAR_PUBLIC not configured."); + } + } + + // Testnet requires contract addresses + if (networkMode === 'testnet') { + const missingContracts = []; + if (!process.env.ETH_HTLC_ESCROW_TESTNET) { + missingContracts.push("ETH_HTLC_ESCROW_TESTNET"); + } + if (!process.env.ETH_RESOLVER_REGISTRY_TESTNET) { + missingContracts.push("ETH_RESOLVER_REGISTRY_TESTNET"); + } + if (!process.env.SOROBAN_HTLC_TESTNET) { + missingContracts.push("SOROBAN_HTLC_TESTNET"); + } + if (!process.env.SOROBAN_RESOLVER_REGISTRY_TESTNET) { + missingContracts.push("SOROBAN_RESOLVER_REGISTRY_TESTNET"); + } + + if (missingContracts.length > 0) { + errors.push( + `TESTNET DEPLOYMENT INCOMPLETE: Missing required testnet contract addresses: ` + + missingContracts.join(", ") + + ". Deploy contracts first (see docs/DEPLOYMENT.md)." + ); + } + } + + // Timeout validation + const rpcTimeoutMs = Number(process.env.RELAYER_RPC_TIMEOUT_MS) || 30000; + if (rpcTimeoutMs < 1000 || rpcTimeoutMs > 300000) { + warnings.push(`RELAYER_RPC_TIMEOUT_MS=${rpcTimeoutMs} is outside recommended range (1000-300000ms)`); + } + + // Report errors + if (errors.length > 0) { + console.error('❌ RELAYER CONFIGURATION ERRORS:'); + errors.forEach(err => console.error(` - ${err}`)); + console.error('\nRelayer startup blocked. Fix the above errors and restart.'); + process.exit(1); + } + + // Report warnings + if (warnings.length > 0) { + console.warn('⚠️ Relayer configuration warnings:'); + warnings.forEach(warn => console.warn(` - ${warn}`)); + } + + console.log('✅ Relayer configuration validation passed'); +} + // Relayer configuration from environment variables export const RELAYER_CONFIG = { // Service settings @@ -455,31 +553,10 @@ export const RELAYER_CONFIG = { } }; -// Validate required environment variables +// Legacy validation function (kept for backward compatibility) function validateConfig() { - const requiredVars = [ - 'ETHEREUM_RPC_URL', - 'STELLAR_HORIZON_URL', - ]; - - const missingVars = requiredVars.filter(varName => !process.env[varName] || process.env[varName]?.includes('YOUR_')); - - if (missingVars.length > 0) { - console.warn('⚠️ Missing or placeholder environment variables:'); - missingVars.forEach(varName => { - console.warn(` - ${varName}`); - }); - console.warn(' Please copy env.template to .env and configure properly'); - } - - // Check for placeholder private keys - if (process.env.RELAYER_PRIVATE_KEY?.startsWith('0x000000')) { - console.warn('⚠️ Using placeholder private key - generate a real key for production'); - } - - if (process.env.RELAYER_STELLAR_SECRET?.includes('SAMPLE')) { - console.warn('⚠️ Using placeholder Stellar secret - generate real keys for production'); - } + console.warn('⚠️ validateConfig() is deprecated. Use validateRelayerConfig() instead.'); + validateRelayerConfig(); } /** @@ -531,8 +608,8 @@ async function initializeRelayer() { app.use(express.json({ limit: '10mb' })); app.use(express.urlencoded({ extended: true })); - // Validate configuration - validateConfig(); + // Validate configuration (fails fast on invalid config) + validateRelayerConfig(); // Display configuration console.log(`🌐 Environment: ${RELAYER_CONFIG.nodeEnv}`); diff --git a/resolver/src/config.ts b/resolver/src/config.ts index 9098fac..3807778 100644 --- a/resolver/src/config.ts +++ b/resolver/src/config.ts @@ -1,6 +1,8 @@ import { config as dotenvConfig } from "dotenv"; import { resolve } from "node:path"; +import { z } from "zod"; import { getLogger } from "./logger.js"; // Import the updated logger framework +import { resolveEthereumRpcUrl } from "./ethereum-rpc-url.js"; dotenvConfig({ path: resolve(process.cwd(), ".env") }); @@ -32,7 +34,56 @@ export interface ResolverConfig { soroban: SorobanConfig; } -import { resolveEthereumRpcUrl } from "./ethereum-rpc-url.js"; +const configSchema = z.object({ + network: z.enum(["testnet", "mainnet"]).default("testnet"), + pollIntervalMs: z.coerce.number().int().positive().default(15_000), + coordinatorUrl: z.string().url().default("http://localhost:3001"), + logLevel: z.enum(["trace", "debug", "info", "warn", "error"]).default("info"), + // Mainnet requires explicit audit confirmation to prevent accidental enablement + mainnetAuditConfirmed: z.preprocess( + (v) => { + if (typeof v === "boolean") return v; + if (typeof v === "string") { + const s = v.trim().toLowerCase(); + return s === "true" || s === "1" || s === "yes" || s === "on"; + } + return false; + }, + z.boolean().default(false) + ), + ethereum: z.object({ + rpcUrl: z.string().url(), + chainId: z.number().int(), + htlcEscrow: z + .string() + .regex(/^0x[0-9a-fA-F]{40}$/) + .optional() + .or(z.literal("")) + .transform((v) => (v ? (v as `0x${string}`) : null)), + resolverRegistry: z + .string() + .regex(/^0x[0-9a-fA-F]{40}$/) + .optional() + .or(z.literal("")) + .transform((v) => (v ? (v as `0x${string}`) : null)), + resolverPrivateKey: z + .string() + .regex(/^0x[0-9a-fA-F]{64}$/) + .optional() + .or(z.literal("")) + .transform((v) => (v ? (v as `0x${string}`) : null)) + }), + soroban: z.object({ + rpcUrl: z.string().url(), + horizonUrl: z.string().url(), + networkPassphrase: z.string(), + htlc: z.string().optional().transform((v) => v ?? null), + resolverRegistry: z.string().optional().transform((v) => v ?? null), + resolverSecret: z.string().optional().transform((v) => v ?? null) + }) +}); + +export type ResolverConfigValidated = z.infer; function requireEnv(name: string): string { const v = process.env[name]; @@ -59,20 +110,32 @@ export function loadConfig(): ResolverConfig { const isMainnet = network === "mainnet"; - const config: ResolverConfig = { + // Mainnet requires explicit audit confirmation + if (isMainnet) { + const auditConfirmed = process.env.MAINNET_AUDIT_CONFIRMED === "true"; + if (!auditConfirmed) { + throw new Error( + "MAINNET DEPLOYMENT BLOCKED: Set MAINNET_AUDIT_CONFIRMED=true only after " + + "completing the mainnet readiness checklist in docs/DEPLOYMENT.md. " + + "This includes audit completion, multisig ownership, and bug bounty." + ); + } + } + + const raw = { network, - pollIntervalMs: Number(process.env.RESOLVER_POLL_INTERVAL_MS ?? 15_000), + pollIntervalMs: process.env.RESOLVER_POLL_INTERVAL_MS ?? "15000", coordinatorUrl: process.env.COORDINATOR_URL ?? "http://localhost:3001", - logLevel: (process.env.LOG_LEVEL as ResolverConfig["logLevel"]) ?? "info", + logLevel: process.env.LOG_LEVEL ?? "info", + mainnetAuditConfirmed: process.env.MAINNET_AUDIT_CONFIRMED, ethereum: { rpcUrl: resolveEthereumRpcUrl(isMainnet ? "mainnet" : "testnet"), chainId: isMainnet ? 1 : 11_155_111, - htlcEscrow: optionalAddress(isMainnet ? "ETH_HTLC_ESCROW_MAINNET" : "ETH_HTLC_ESCROW_TESTNET"), - resolverRegistry: optionalAddress( - isMainnet ? "ETH_RESOLVER_REGISTRY_MAINNET" : "ETH_RESOLVER_REGISTRY_TESTNET" - ), + htlcEscrow: process.env[isMainnet ? "ETH_HTLC_ESCROW_MAINNET" : "ETH_HTLC_ESCROW_TESTNET"] ?? "", + resolverRegistry: + process.env[isMainnet ? "ETH_RESOLVER_REGISTRY_MAINNET" : "ETH_RESOLVER_REGISTRY_TESTNET"] ?? "", resolverPrivateKey: - (process.env.RESOLVER_ETH_PRIVATE_KEY as `0x${string}` | undefined) ?? null + (process.env.RESOLVER_ETH_PRIVATE_KEY as `0x${string}` | undefined) ?? "" }, soroban: { rpcUrl: @@ -84,12 +147,62 @@ export function loadConfig(): ResolverConfig { networkPassphrase: isMainnet ? "Public Global Stellar Network ; September 2015" : "Test SDF Network ; September 2015", - htlc: process.env[isMainnet ? "SOROBAN_HTLC_MAINNET" : "SOROBAN_HTLC_TESTNET"] ?? null, + htlc: process.env[isMainnet ? "SOROBAN_HTLC_MAINNET" : "SOROBAN_HTLC_TESTNET"] ?? "", resolverRegistry: process.env[ isMainnet ? "SOROBAN_RESOLVER_REGISTRY_MAINNET" : "SOROBAN_RESOLVER_REGISTRY_TESTNET" - ] ?? null, - resolverSecret: process.env.RESOLVER_STELLAR_SECRET ?? null + ] ?? "", + resolverSecret: process.env.RESOLVER_STELLAR_SECRET ?? "" + } + }; + + const result = configSchema.parse(raw); + + // Additional validation: testnet requires contract addresses + if (!isMainnet) { + const missingTestnetContracts = []; + if (!result.ethereum.htlcEscrow) { + missingTestnetContracts.push("ETH_HTLC_ESCROW_TESTNET"); + } + if (!result.ethereum.resolverRegistry) { + missingTestnetContracts.push("ETH_RESOLVER_REGISTRY_TESTNET"); + } + if (!result.soroban.htlc) { + missingTestnetContracts.push("SOROBAN_HTLC_TESTNET"); + } + if (!result.soroban.resolverRegistry) { + missingTestnetContracts.push("SOROBAN_RESOLVER_REGISTRY_TESTNET"); + } + + if (missingTestnetContracts.length > 0) { + throw new Error( + `TESTNET DEPLOYMENT INCOMPLETE: Missing required testnet contract addresses: ` + + missingTestnetContracts.join(", ") + + ". Deploy contracts first (see docs/DEPLOYMENT.md) or check env.example for variable names." + ); + } + } + + // Convert to legacy ResolverConfig format for backward compatibility + const config: ResolverConfig = { + network: result.network, + pollIntervalMs: result.pollIntervalMs, + coordinatorUrl: result.coordinatorUrl, + logLevel: result.logLevel, + ethereum: { + rpcUrl: result.ethereum.rpcUrl, + chainId: result.ethereum.chainId, + htlcEscrow: result.ethereum.htlcEscrow, + resolverRegistry: result.ethereum.resolverRegistry, + resolverPrivateKey: result.ethereum.resolverPrivateKey + }, + soroban: { + rpcUrl: result.soroban.rpcUrl, + networkPassphrase: result.soroban.networkPassphrase, + horizonUrl: result.soroban.horizonUrl, + htlc: result.soroban.htlc, + resolverRegistry: result.soroban.resolverRegistry, + resolverSecret: result.soroban.resolverSecret } }; @@ -106,4 +219,4 @@ export function loadConfig(): ResolverConfig { logger.info({ msg: "OverSync active module runtime mappings configuration payload", runtimeConfig: config }); return config; -} \ No newline at end of file +}