Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"swagger-ui-express": "^5.0.1",
"uuid": "9.0.1",
"winston": "^3.11.0",
"winston-daily-rotate-file": "^5.0.0",
"zod": "^4.4.3"
},
"devDependencies": {
Expand Down
2 changes: 1 addition & 1 deletion src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ const connectDB = async (): Promise<void> => {
};

// Call connectDB but don't listen
if (process.env.NODE_ENV !== 'test' && !process.env.JEST_WORKER_ID) {
if (env.NODE_ENV !== 'test' && !process.env.JEST_WORKER_ID) {
connectDB();
}

Expand Down
175 changes: 175 additions & 0 deletions src/config/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,102 @@ interface EnvConfig {
BULK_UPLOAD_MAX_BYTES: number;
/** Maximum data rows accepted in a single bulk upload. Default: 1000 */
BULK_UPLOAD_MAX_ROWS: number;

// ── Socket.IO transport tuning ────────────────────────────────────
/** Interval (ms) between server-initiated Socket.IO pings. Default: 25000 */
SOCKET_PING_INTERVAL_MS: number;
/** Time (ms) to wait for a pong before considering the peer gone. Default: 20000 */
SOCKET_PING_TIMEOUT_MS: number;
/** Consecutive missed pongs tolerated before disconnecting. Default: 2 */
SOCKET_MAX_MISSED_PONGS: number;
/** Time (ms) a queued socket message waits for an ack before retry. Default: 15000 */
SOCKET_MESSAGE_ACK_TIMEOUT_MS: number;
/** Interval (ms) between periodic socket token expiry checks. Default: 60000 */
SOCKET_TOKEN_CHECK_INTERVAL_MS: number;
/** Grace period (ms) granted after a socket token expires. Default: 30000 */
SOCKET_TOKEN_GRACE_PERIOD_MS: number;
/** Maximum location updates accepted in a single offline-sync batch. Default: 500 */
SYNC_BATCH_SIZE_LIMIT: number;

// ── Driver location ingestion ─────────────────────────────────
/** TTL (s) of the Redis dedup key for a location update. Default: 60 */
LOCATION_DEDUP_TTL_SECONDS: number;
/** Maximum age (ms) of a location update before it is rejected. Default: 300000 */
LOCATION_MAX_AGE_MS: number;
/** Clock-skew tolerance (ms) for future-dated location updates. Default: 30000 */
LOCATION_MAX_FUTURE_MS: number;
/** Default radius (m) used by driver proximity searches. Default: 5000 */
DRIVER_PROXIMITY_DEFAULT_RADIUS_M: number;
/** Hard cap (m) on the radius a proximity search may request. Default: 50000 */
DRIVER_PROXIMITY_MAX_RADIUS_M: number;
/** Maximum number of drivers returned by a proximity search. Default: 50 */
DRIVER_PROXIMITY_MAX_RESULTS: number;
/** Age (s) beyond which a driver location is considered stale. Default: 300 */
DRIVER_LOCATION_STALE_AFTER_SECONDS: number;

// ── ETA cache / routing ──────────────────────────────────────
/** TTL (s) for cached ETA computations. Default: 600 */
ETA_CACHE_TTL_SECONDS: number;
/** Geohash precision used to key the ETA cache. Default: 7 */
ETA_GEOHASH_PRECISION: number;
/** Google Maps Directions API key. Blank disables live routing. */
GOOGLE_MAPS_API_KEY: string;

// ── Lifecycle / jobs ──────────────────────────────────────────
/** Time (ms) allowed for in-flight work to drain on shutdown. Default: 30000 */
SHUTDOWN_TIMEOUT_MS: number;
/** Cron expression driving the escrow monitor job. Default: every 5 minutes */
ESCROW_MONITOR_CRON: string;

// ── Stellar / Soroban network ─────────────────────────────────
/** Target Stellar network. Default: testnet */
STELLAR_NETWORK: 'mainnet' | 'testnet' | 'futurenet';
/** Soroban RPC endpoint. Blank resolves to the default URL for the network. */
SOROBAN_RPC_URL: string;
/** Network passphrase. Blank resolves to the well-known value for the network. */
STELLAR_NETWORK_PASSPHRASE: string;
/** Per-request HTTP timeout (ms) for Soroban RPC calls. Default: 10000 */
SOROBAN_RPC_TIMEOUT_MS: number;
/** Soroban contract id (`C...`) of the escrow contract. Blank disables escrow endpoints. */
SOROBAN_ESCROW_CONTRACT_ID: string;
/** Escrow contract function invoked to lock funds. Default: lock_escrow */
SOROBAN_ESCROW_LOCK_FUNCTION: string;
/** Base fee (stroops) used when building transactions. Default: 100 */
STELLAR_BASE_FEE: string;
/** Validity window (s) of generated unsigned transactions. Default: 300 */
STELLAR_TRANSACTION_TIMEOUT_SECONDS: number;
/** Jitter ratio (0-1) applied to RPC backoff delays. Default: 0.2 */
SOROBAN_RPC_RETRY_JITTER_RATIO: number;

// ── Escrow event indexing ───────────────────────────────────
/** Contract id watched by the escrow event indexer. */
ESCROW_CONTRACT_ID: string;
/** Event topic signalling that an escrow was funded. Default: escrow_funded */
ESCROW_FUNDED_EVENT_TOPIC: string;

// ── Logging ───────────────────────────────────────────────────
/** Directory that rotated log files are written to. Default: logs */
LOG_DIR: string;
/** Maximum size of a single log file before rotation (e.g. "20m"). */
LOG_MAX_SIZE: string;
/** Retention window for rotated log files (e.g. "14d"). */
LOG_MAX_FILES: string;
/** Whether rotated log files are gzipped. Default: true */
LOG_ZIPPED_ARCHIVE: boolean;
/** Disable file transports entirely (useful in containers). Default: false */
LOG_DISABLE_FILE: boolean;

// ── Soroban circuit breaker ───────────────────────────────────
/** Error rate (%) at which the Soroban breaker opens. Default: 50 */
CB_SOROBAN_ERROR_THRESHOLD_PERCENTAGE: number;
/** Window (ms) over which the breaker's error rate is measured. Default: 10000 */
CB_SOROBAN_ROLLING_WINDOW_MS: number;
/** Time (ms) the breaker stays open before probing again. Default: 30000 */
CB_SOROBAN_RESET_TIMEOUT_MS: number;
/** Minimum calls in the window before the breaker may open. Default: 5 */
CB_SOROBAN_VOLUME_THRESHOLD: number;
/** Per-call timeout (ms) enforced by the breaker. Default: 10000 */
CB_SOROBAN_TIMEOUT_MS: number;
}

const envSchema = z.object({
Expand Down Expand Up @@ -98,6 +194,73 @@ const envSchema = z.object({
// ── Bulk delivery CSV import ────────────────────────────────────────────────
BULK_UPLOAD_MAX_BYTES: z.coerce.number().int().min(1024).default(5 * 1024 * 1024),
BULK_UPLOAD_MAX_ROWS: z.coerce.number().int().min(1).max(10000).default(1000),

// ── Socket.IO transport tuning ────────────────────────────────────
SOCKET_PING_INTERVAL_MS: z.coerce.number().int().min(1000).default(25000),
SOCKET_PING_TIMEOUT_MS: z.coerce.number().int().min(1000).default(20000),
SOCKET_MAX_MISSED_PONGS: z.coerce.number().int().min(1).max(10).default(2),
SOCKET_MESSAGE_ACK_TIMEOUT_MS: z.coerce.number().int().min(1000).default(15000),
SOCKET_TOKEN_CHECK_INTERVAL_MS: z.coerce.number().int().min(1000).default(60000),
SOCKET_TOKEN_GRACE_PERIOD_MS: z.coerce.number().int().min(0).default(30000),
SYNC_BATCH_SIZE_LIMIT: z.coerce.number().int().min(1).max(10000).default(500),

// ── Driver location ingestion ─────────────────────────────────
LOCATION_DEDUP_TTL_SECONDS: z.coerce.number().int().min(1).default(60),
LOCATION_MAX_AGE_MS: z.coerce.number().int().min(1000).default(300000),
LOCATION_MAX_FUTURE_MS: z.coerce.number().int().min(0).default(30000),
DRIVER_PROXIMITY_DEFAULT_RADIUS_M: z.coerce.number().int().min(1).default(5000),
DRIVER_PROXIMITY_MAX_RADIUS_M: z.coerce.number().int().min(1).default(50000),
DRIVER_PROXIMITY_MAX_RESULTS: z.coerce.number().int().min(1).max(500).default(50),
DRIVER_LOCATION_STALE_AFTER_SECONDS: z.coerce.number().int().min(1).default(300),

// ── ETA cache / routing ──────────────────────────────────────
ETA_CACHE_TTL_SECONDS: z.coerce.number().int().min(1).default(600),
ETA_GEOHASH_PRECISION: z.coerce.number().int().min(1).max(12).default(7),
GOOGLE_MAPS_API_KEY: z.string().default(''),

// ── Lifecycle / jobs ──────────────────────────────────────────
SHUTDOWN_TIMEOUT_MS: z.coerce.number().int().min(1000).default(30000),
ESCROW_MONITOR_CRON: z.string().trim().min(1).default('*/5 * * * *'),

// ── Stellar / Soroban network ─────────────────────────────────
STELLAR_NETWORK: z
.string()
.trim()
.toLowerCase()
.pipe(z.enum(['mainnet', 'testnet', 'futurenet']))
.default('testnet'),
SOROBAN_RPC_URL: z.string().trim().default(''),
STELLAR_NETWORK_PASSPHRASE: z.string().trim().default(''),
SOROBAN_RPC_TIMEOUT_MS: z.coerce.number().int().min(1000).default(10000),
SOROBAN_ESCROW_CONTRACT_ID: z.string().trim().default(''),
SOROBAN_ESCROW_LOCK_FUNCTION: z.string().trim().min(1).default('lock_escrow'),
STELLAR_BASE_FEE: z.string().trim().min(1).default('100'),
STELLAR_TRANSACTION_TIMEOUT_SECONDS: z.coerce.number().int().min(1).default(300),
SOROBAN_RPC_RETRY_JITTER_RATIO: z.coerce.number().min(0).max(1).default(0.2),

// ── Escrow event indexing ───────────────────────────────────
ESCROW_CONTRACT_ID: z.string().trim().default(''),
ESCROW_FUNDED_EVENT_TOPIC: z.string().trim().min(1).default('escrow_funded'),

// ── Logging ───────────────────────────────────────────────────
LOG_DIR: z.string().trim().min(1).default('logs'),
LOG_MAX_SIZE: z.string().trim().min(1).default('20m'),
LOG_MAX_FILES: z.string().trim().min(1).default('14d'),
LOG_ZIPPED_ARCHIVE: z
.enum(['true', 'false'])
.default('true')
.transform((value) => value === 'true'),
LOG_DISABLE_FILE: z
.enum(['true', 'false'])
.default('false')
.transform((value) => value === 'true'),

// ── Soroban circuit breaker ───────────────────────────────────
CB_SOROBAN_ERROR_THRESHOLD_PERCENTAGE: z.coerce.number().int().min(1).max(100).default(50),
CB_SOROBAN_ROLLING_WINDOW_MS: z.coerce.number().int().min(1000).default(10000),
CB_SOROBAN_RESET_TIMEOUT_MS: z.coerce.number().int().min(1000).default(30000),
CB_SOROBAN_VOLUME_THRESHOLD: z.coerce.number().int().min(1).default(5),
CB_SOROBAN_TIMEOUT_MS: z.coerce.number().int().min(1000).default(10000),
});

let env: EnvConfig;
Expand All @@ -121,4 +284,16 @@ if (env.UPLOAD_STORAGE_DRIVER === 's3' && !env.AWS_S3_BUCKET) {
process.exit(1);
}

if (env.DRIVER_PROXIMITY_DEFAULT_RADIUS_M > env.DRIVER_PROXIMITY_MAX_RADIUS_M) {
console.error(
'❌ DRIVER_PROXIMITY_DEFAULT_RADIUS_M cannot exceed DRIVER_PROXIMITY_MAX_RADIUS_M',
);
process.exit(1);
}

if (env.SOROBAN_RPC_RETRY_BASE_MS > env.SOROBAN_RPC_RETRY_MAX_MS) {
console.error('❌ SOROBAN_RPC_RETRY_BASE_MS cannot exceed SOROBAN_RPC_RETRY_MAX_MS');
process.exit(1);
}

export default env;
6 changes: 4 additions & 2 deletions src/config/escrow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
* separate from the generic Stellar/Soroban RPC config since it identifies
* a specific contract instance rather than network connection details.
*/
import env from './env';

export interface EscrowIndexerConfig {
/** Deployed escrow contract id (Soroban "C..." address). */
contractId: string;
Expand All @@ -15,8 +17,8 @@ export interface EscrowIndexerConfig {

function resolveEscrowIndexerConfig(): EscrowIndexerConfig {
return {
contractId: process.env.ESCROW_CONTRACT_ID?.trim() ?? '',
fundedEventTopic: process.env.ESCROW_FUNDED_EVENT_TOPIC?.trim() || 'escrow_funded',
contractId: env.ESCROW_CONTRACT_ID,
fundedEventTopic: env.ESCROW_FUNDED_EVENT_TOPIC,
};
}

Expand Down
Loading