import { loadDotenv } from './env.mjs'; import { DEFAULT_NEAR_INTENTS_PAIR_FILTER } from '../core/pair-filter.mjs'; const DEFAULTS = { nearIntentsWsUrl: 'wss://solver-relay-v2.chaindefuser.com/ws', nearIntentsRpcUrl: 'https://solver-relay-v2.chaindefuser.com/rpc', nearBridgeRpcUrl: 'https://bridge.chaindefuser.com/rpc', nearRpcUrl: 'https://rpc.fastnear.com', nearVerifierContract: 'intents.near', nearIntentsPairFilter: DEFAULT_NEAR_INTENTS_PAIR_FILTER, nearIntentsPairFilterReloadMs: 5_000, nearIntentsControlApiEnabled: true, nearIntentsControlHost: '0.0.0.0', nearIntentsControlPort: 8081, marketReferenceControlPort: 8082, inventorySyncControlPort: 8083, liquidityManagerControlPort: 8084, historyWriterControlPort: 8085, strategyEngineControlPort: 8086, tradeExecutorControlPort: 8087, opsSentinelControlPort: 8088, kafkaBrokers: ['127.0.0.1:9092'], kafkaClientId: 'unrip', kafkaTopicRawNearIntentsQuote: 'raw.near_intents.quote', kafkaTopicNormSwapDemand: 'norm.swap_demand', kafkaTopicRefMarketPrice: 'ref.market_price', kafkaTopicStateIntentInventory: 'state.intent_inventory', kafkaTopicOpsLiquidityAction: 'ops.liquidity_action', kafkaTopicOpsFundingObservation: 'ops.funding_observation', kafkaTopicOpsAlert: 'ops.alert', kafkaTopicDecisionTradeDecision: 'decision.trade_decision', kafkaTopicCmdExecuteTrade: 'cmd.execute_trade', kafkaTopicExecTradeResult: 'exec.trade_result', kafkaConsumerGroupHistory: 'history-writer-v1', kafkaConsumerGroupInventory: 'inventory-sync-v1', kafkaConsumerGroupStrategy: 'strategy-engine-v1', kafkaConsumerGroupExecutor: 'trade-executor-v1', kafkaConsumerGroupOpsSentinel: 'ops-sentinel-v1', executorStateDir: './var/executor-state', liquidityStateDir: './var/liquidity-state', postgresUrl: 'postgresql://unrip:unrip@127.0.0.1:5432/unrip', projectName: 'unrip', projectNamespace: 'unrip', tradingBtcAssetId: 'nep141:btc.omft.near', tradingBtcSymbol: 'BTC', tradingBtcDecimals: 8, tradingBtcChain: 'btc:mainnet', tradingBtcWithdrawAddress: '', tradingEureAssetId: 'nep141:gnosis-0x420ca0f9b9b604ce0fd9c18ef134c705e5fa3430.omft.near', tradingEureSymbol: 'EURe', tradingEureDecimals: 18, tradingEureChain: 'eth:100', tradingEureWithdrawAddress: '', marketReferenceRefreshMs: 5_000, marketReferenceCoinGeckoRefreshMs: 15_000, marketReferenceMaxAgeMs: 30_000, marketReferenceKrakenTickerUrl: 'https://api.kraken.com/0/public/Ticker?pair=XBTEUR', marketReferenceCoinGeckoUrl: 'https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=eur', inventorySyncRefreshMs: 15_000, liquidityRefreshMs: 30_000, strategyGrossThresholdPct: 2, strategyInitialArmed: false, strategyMaxNotionalEure: 5, strategyPriceMaxAgeMs: 30_000, strategyInventoryMaxAgeMs: 30_000, executorInitialArmed: false, executorResponseTimeoutMs: 10_000, withdrawalsFrozen: true, btcFundingObserverEnabled: true, btcFundingObserverBaseUrl: 'https://mempool.space/api', fundingObservationStuckMs: 60 * 60 * 1000, opsSentinelEvaluationMs: 5_000, opsSentinelFundingCreditPendingMs: 5 * 60 * 1000, }; function splitCsv(value) { return String(value || '') .split(',') .map((part) => part.trim()) .filter(Boolean); } function parseNumber(value, fallback) { const parsed = Number(value); return Number.isFinite(parsed) ? parsed : fallback; } function parseBoolean(value, fallback) { if (value == null || value === '') return fallback; const normalized = String(value).trim().toLowerCase(); if (['1', 'true', 'yes', 'on'].includes(normalized)) return true; if (['0', 'false', 'no', 'off'].includes(normalized)) return false; return fallback; } function buildAsset({ assetId, symbol, decimals, chain, withdrawAddress = '' }) { return { assetId, symbol, decimals, chain, withdrawAddress, }; } export function loadConfig({ envPath = '.env' } = {}) { loadDotenv(envPath); const tradingBtc = buildAsset({ assetId: process.env.TRADING_BTC_ASSET_ID || DEFAULTS.tradingBtcAssetId, symbol: process.env.TRADING_BTC_SYMBOL || DEFAULTS.tradingBtcSymbol, decimals: parseNumber(process.env.TRADING_BTC_DECIMALS, DEFAULTS.tradingBtcDecimals), chain: process.env.TRADING_BTC_CHAIN || DEFAULTS.tradingBtcChain, withdrawAddress: process.env.TRADING_BTC_WITHDRAW_ADDRESS || DEFAULTS.tradingBtcWithdrawAddress, }); const tradingEure = buildAsset({ assetId: process.env.TRADING_EURE_ASSET_ID || DEFAULTS.tradingEureAssetId, symbol: process.env.TRADING_EURE_SYMBOL || DEFAULTS.tradingEureSymbol, decimals: parseNumber(process.env.TRADING_EURE_DECIMALS, DEFAULTS.tradingEureDecimals), chain: process.env.TRADING_EURE_CHAIN || DEFAULTS.tradingEureChain, withdrawAddress: process.env.TRADING_EURE_WITHDRAW_ADDRESS || DEFAULTS.tradingEureWithdrawAddress, }); const projectName = process.env.PROJECT_NAME || DEFAULTS.projectName; const projectNamespace = process.env.PROJECT_NAMESPACE || projectName || DEFAULTS.projectNamespace; return { nearIntentsApiKey: process.env.NEAR_INTENTS_API_KEY || '', nearIntentsAccountId: process.env.NEAR_INTENTS_ACCOUNT_ID || '', nearIntentsSignerPrivateKey: process.env.NEAR_INTENTS_SIGNER_PRIVATE_KEY || '', nearIntentsWsUrl: process.env.NEAR_INTENTS_WS_URL || DEFAULTS.nearIntentsWsUrl, nearIntentsRpcUrl: process.env.NEAR_INTENTS_RPC_URL || DEFAULTS.nearIntentsRpcUrl, nearBridgeRpcUrl: process.env.NEAR_INTENTS_BRIDGE_RPC_URL || DEFAULTS.nearBridgeRpcUrl, nearRpcUrl: process.env.NEAR_RPC_URL || DEFAULTS.nearRpcUrl, nearVerifierContract: process.env.NEAR_INTENTS_VERIFIER_CONTRACT || DEFAULTS.nearVerifierContract, nearIntentsPairFilter: process.env.NEAR_INTENTS_PAIR_FILTER || DEFAULTS.nearIntentsPairFilter, nearIntentsPairFilterFile: process.env.NEAR_INTENTS_PAIR_FILTER_FILE || '', nearIntentsPairFilterReloadMs: parseNumber( process.env.NEAR_INTENTS_PAIR_FILTER_RELOAD_MS, DEFAULTS.nearIntentsPairFilterReloadMs, ), nearIntentsControlApiEnabled: parseBoolean( process.env.NEAR_INTENTS_CONTROL_API_ENABLED, DEFAULTS.nearIntentsControlApiEnabled, ), nearIntentsControlHost: process.env.NEAR_INTENTS_CONTROL_HOST || DEFAULTS.nearIntentsControlHost, nearIntentsControlPort: parseNumber( process.env.NEAR_INTENTS_CONTROL_PORT, DEFAULTS.nearIntentsControlPort, ), marketReferenceControlHost: process.env.MARKET_REFERENCE_CONTROL_HOST || DEFAULTS.nearIntentsControlHost, marketReferenceControlPort: parseNumber( process.env.MARKET_REFERENCE_CONTROL_PORT, DEFAULTS.marketReferenceControlPort, ), inventorySyncControlHost: process.env.INVENTORY_SYNC_CONTROL_HOST || DEFAULTS.nearIntentsControlHost, inventorySyncControlPort: parseNumber( process.env.INVENTORY_SYNC_CONTROL_PORT, DEFAULTS.inventorySyncControlPort, ), liquidityManagerControlHost: process.env.LIQUIDITY_MANAGER_CONTROL_HOST || DEFAULTS.nearIntentsControlHost, liquidityManagerControlPort: parseNumber( process.env.LIQUIDITY_MANAGER_CONTROL_PORT, DEFAULTS.liquidityManagerControlPort, ), historyWriterControlHost: process.env.HISTORY_WRITER_CONTROL_HOST || DEFAULTS.nearIntentsControlHost, historyWriterControlPort: parseNumber( process.env.HISTORY_WRITER_CONTROL_PORT, DEFAULTS.historyWriterControlPort, ), opsSentinelControlHost: process.env.OPS_SENTINEL_CONTROL_HOST || DEFAULTS.nearIntentsControlHost, opsSentinelControlPort: parseNumber( process.env.OPS_SENTINEL_CONTROL_PORT, DEFAULTS.opsSentinelControlPort, ), strategyEngineControlHost: process.env.STRATEGY_ENGINE_CONTROL_HOST || DEFAULTS.nearIntentsControlHost, strategyEngineControlPort: parseNumber( process.env.STRATEGY_ENGINE_CONTROL_PORT, DEFAULTS.strategyEngineControlPort, ), tradeExecutorControlHost: process.env.TRADE_EXECUTOR_CONTROL_HOST || DEFAULTS.nearIntentsControlHost, tradeExecutorControlPort: parseNumber( process.env.TRADE_EXECUTOR_CONTROL_PORT, DEFAULTS.tradeExecutorControlPort, ), kafkaBrokers: splitCsv(process.env.KAFKA_BROKERS).length ? splitCsv(process.env.KAFKA_BROKERS) : DEFAULTS.kafkaBrokers, kafkaClientId: process.env.KAFKA_CLIENT_ID || DEFAULTS.kafkaClientId, kafkaTopicRawNearIntentsQuote: process.env.KAFKA_TOPIC_RAW_NEAR_INTENTS_QUOTE || DEFAULTS.kafkaTopicRawNearIntentsQuote, kafkaTopicNormSwapDemand: process.env.KAFKA_TOPIC_NORM_SWAP_DEMAND || DEFAULTS.kafkaTopicNormSwapDemand, kafkaTopicRefMarketPrice: process.env.KAFKA_TOPIC_REF_MARKET_PRICE || DEFAULTS.kafkaTopicRefMarketPrice, kafkaTopicStateIntentInventory: process.env.KAFKA_TOPIC_STATE_INTENT_INVENTORY || DEFAULTS.kafkaTopicStateIntentInventory, kafkaTopicOpsLiquidityAction: process.env.KAFKA_TOPIC_OPS_LIQUIDITY_ACTION || DEFAULTS.kafkaTopicOpsLiquidityAction, kafkaTopicOpsFundingObservation: process.env.KAFKA_TOPIC_OPS_FUNDING_OBSERVATION || DEFAULTS.kafkaTopicOpsFundingObservation, kafkaTopicOpsAlert: process.env.KAFKA_TOPIC_OPS_ALERT || DEFAULTS.kafkaTopicOpsAlert, kafkaTopicDecisionTradeDecision: process.env.KAFKA_TOPIC_DECISION_TRADE_DECISION || DEFAULTS.kafkaTopicDecisionTradeDecision, kafkaTopicCmdExecuteTrade: process.env.KAFKA_TOPIC_CMD_EXECUTE_TRADE || DEFAULTS.kafkaTopicCmdExecuteTrade, kafkaTopicExecTradeResult: process.env.KAFKA_TOPIC_EXEC_TRADE_RESULT || DEFAULTS.kafkaTopicExecTradeResult, kafkaConsumerGroupHistory: process.env.KAFKA_CONSUMER_GROUP_HISTORY || DEFAULTS.kafkaConsumerGroupHistory, kafkaConsumerGroupInventory: process.env.KAFKA_CONSUMER_GROUP_INVENTORY || DEFAULTS.kafkaConsumerGroupInventory, kafkaConsumerGroupStrategy: process.env.KAFKA_CONSUMER_GROUP_STRATEGY || DEFAULTS.kafkaConsumerGroupStrategy, kafkaConsumerGroupExecutor: process.env.KAFKA_CONSUMER_GROUP_EXECUTOR || DEFAULTS.kafkaConsumerGroupExecutor, kafkaConsumerGroupOpsSentinel: process.env.KAFKA_CONSUMER_GROUP_OPS_SENTINEL || DEFAULTS.kafkaConsumerGroupOpsSentinel, executorStateDir: process.env.EXECUTOR_STATE_DIR || DEFAULTS.executorStateDir, liquidityStateDir: process.env.LIQUIDITY_STATE_DIR || DEFAULTS.liquidityStateDir, postgresUrl: process.env.POSTGRES_URL || DEFAULTS.postgresUrl, projectName, projectNamespace, tradingBtc, tradingEure, activePair: `${tradingBtc.assetId}->${tradingEure.assetId}`, activeAssetIds: [tradingBtc.assetId, tradingEure.assetId], assetRegistry: new Map([ [tradingBtc.assetId, tradingBtc], [tradingEure.assetId, tradingEure], ]), marketReferenceRefreshMs: parseNumber( process.env.MARKET_REFERENCE_REFRESH_MS, DEFAULTS.marketReferenceRefreshMs, ), marketReferenceCoinGeckoRefreshMs: parseNumber( process.env.MARKET_REFERENCE_COINGECKO_REFRESH_MS, DEFAULTS.marketReferenceCoinGeckoRefreshMs, ), marketReferenceMaxAgeMs: parseNumber( process.env.MARKET_REFERENCE_MAX_AGE_MS, DEFAULTS.marketReferenceMaxAgeMs, ), marketReferenceKrakenTickerUrl: process.env.MARKET_REFERENCE_KRAKEN_TICKER_URL || DEFAULTS.marketReferenceKrakenTickerUrl, marketReferenceCoinGeckoUrl: process.env.MARKET_REFERENCE_COINGECKO_URL || DEFAULTS.marketReferenceCoinGeckoUrl, inventorySyncRefreshMs: parseNumber( process.env.INVENTORY_SYNC_REFRESH_MS, DEFAULTS.inventorySyncRefreshMs, ), liquidityRefreshMs: parseNumber( process.env.LIQUIDITY_REFRESH_MS, DEFAULTS.liquidityRefreshMs, ), strategyGrossThresholdPct: parseNumber( process.env.STRATEGY_GROSS_THRESHOLD_PCT, DEFAULTS.strategyGrossThresholdPct, ), strategyInitialArmed: parseBoolean( process.env.STRATEGY_INITIAL_ARMED, DEFAULTS.strategyInitialArmed, ), strategyMaxNotionalEure: parseNumber( process.env.STRATEGY_MAX_NOTIONAL_EURE, DEFAULTS.strategyMaxNotionalEure, ), strategyPriceMaxAgeMs: parseNumber( process.env.STRATEGY_PRICE_MAX_AGE_MS, DEFAULTS.strategyPriceMaxAgeMs, ), strategyInventoryMaxAgeMs: parseNumber( process.env.STRATEGY_INVENTORY_MAX_AGE_MS, DEFAULTS.strategyInventoryMaxAgeMs, ), executorInitialArmed: parseBoolean( process.env.EXECUTOR_INITIAL_ARMED, DEFAULTS.executorInitialArmed, ), executorResponseTimeoutMs: parseNumber( process.env.EXECUTOR_RESPONSE_TIMEOUT_MS, DEFAULTS.executorResponseTimeoutMs, ), withdrawalsFrozen: parseBoolean( process.env.LIQUIDITY_WITHDRAWALS_FROZEN, DEFAULTS.withdrawalsFrozen, ), btcFundingObserverEnabled: parseBoolean( process.env.BTC_FUNDING_OBSERVER_ENABLED, DEFAULTS.btcFundingObserverEnabled, ), btcFundingObserverBaseUrl: process.env.BTC_FUNDING_OBSERVER_BASE_URL || DEFAULTS.btcFundingObserverBaseUrl, fundingObservationStuckMs: parseNumber( process.env.FUNDING_OBSERVATION_STUCK_MS, DEFAULTS.fundingObservationStuckMs, ), opsSentinelEvaluationMs: parseNumber( process.env.OPS_SENTINEL_EVALUATION_MS, DEFAULTS.opsSentinelEvaluationMs, ), opsSentinelPriceStaleMs: parseNumber( process.env.OPS_SENTINEL_PRICE_STALE_MS, DEFAULTS.marketReferenceMaxAgeMs, ), opsSentinelInventoryStaleMs: parseNumber( process.env.OPS_SENTINEL_INVENTORY_STALE_MS, DEFAULTS.strategyInventoryMaxAgeMs, ), opsSentinelFundingCreditPendingMs: parseNumber( process.env.OPS_SENTINEL_FUNDING_CREDIT_PENDING_MS, DEFAULTS.opsSentinelFundingCreditPendingMs, ), opsSentinelFundingStuckMs: parseNumber( process.env.OPS_SENTINEL_FUNDING_STUCK_MS, DEFAULTS.fundingObservationStuckMs, ), }; }