/** * Configuration for CCIP Relay Service */ import dotenv from 'dotenv'; import path from 'path'; import { existsSync } from 'fs'; import { fileURLToPath } from 'url'; import { createRequire } from 'module'; import { parseRpcPool } from './rpcPool.js'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); // Load project root first so PRIVATE_KEY is set, then relay .env const projectEnv = path.resolve(__dirname, '../../../.env'); const relayEnv = path.resolve(__dirname, '../.env'); dotenv.config({ path: projectEnv }); dotenv.config({ path: relayEnv }); const DEFAULT_SOURCE_CHAIN_ID = Number(process.env.SOURCE_CHAIN_ID || '138'); const DEFAULT_DEST_CHAIN_ID = Number(process.env.DEST_CHAIN_ID || '1'); // Fill contract addresses from master JSON (config/smart-contracts-master.json) when not set in .env const repoRoots = [ path.resolve(__dirname, '../../../'), path.resolve(__dirname, '../../../../') ]; function resolveConfigFile(fileName) { for (const root of repoRoots) { const candidate = path.join(root, 'config', fileName); if (existsSync(candidate)) { return candidate; } } return path.join(repoRoots[0], 'config', fileName); } const contractsLoaderPath = resolveConfigFile('contracts-loader.cjs'); const tokenMappingLoaderPath = resolveConfigFile('token-mapping-loader.cjs'); try { const require = createRequire(import.meta.url); const { loadContractsIntoProcessEnv } = require(contractsLoaderPath); if (typeof loadContractsIntoProcessEnv === 'function') { loadContractsIntoProcessEnv([DEFAULT_SOURCE_CHAIN_ID, DEFAULT_DEST_CHAIN_ID]); } } catch (_) { /* run from smom-dbis-138 only: loader not found */ } function getExplicitTokenMappingOverrides() { const overrides = {}; const destinationWeth9 = process.env.DEST_WETH9_ADDRESS || process.env.DEST_WETH_ADDRESS || ''; const destinationLink = process.env.DEST_LINK_ADDRESS || ''; if (destinationWeth9 && !destinationWeth9.includes('${')) { overrides['0xc02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2'.toLowerCase()] = destinationWeth9; const sourceWmon = process.env.SOURCE_WMON_ADDRESS || process.env.SOURCE_WETH9_ADDRESS || ''; if (sourceWmon && !sourceWmon.includes('${')) { overrides[sourceWmon.toLowerCase()] = destinationWeth9; } } if (destinationLink && !destinationLink.includes('${')) { overrides['0xb7721dD53A8c629d9f1Ba31a5819AFe250002b03'.toLowerCase()] = destinationLink; } return overrides; } function withExplicitTokenMappingOverrides(mapping) { return { ...(mapping || {}), ...getExplicitTokenMappingOverrides() }; } // Token mapping for the active source/destination pair: prefer multichain mapping when available. function getTokenMapping() { const sourceChainId = Number(process.env.SOURCE_CHAIN_ID || '138'); const destinationChainId = Number(process.env.DEST_CHAIN_ID || '1'); try { const require = createRequire(import.meta.url); const { getTokenMappingForPair, getRelayTokenMapping } = require(tokenMappingLoaderPath); const pair = getTokenMappingForPair && getTokenMappingForPair(sourceChainId, destinationChainId); if (pair && pair.addressMapFromTo && Object.keys(pair.addressMapFromTo).length > 0) { return withExplicitTokenMappingOverrides(pair.addressMapFromTo); } const fromFile = getRelayTokenMapping && getRelayTokenMapping(); if (sourceChainId === 138 && destinationChainId === 1 && fromFile && Object.keys(fromFile).length > 0) { return withExplicitTokenMappingOverrides(fromFile); } } catch (_) { /* config not available */ } const destinationWeth9 = process.env.DEST_WETH9_ADDRESS || process.env.DEST_WETH_ADDRESS || '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2'; // Fallback keeps WETH and LINK mapping for legacy relay profiles. return withExplicitTokenMappingOverrides({ '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2': destinationWeth9, '0xb7721dD53A8c629d9f1Ba31a5819AFe250002b03': process.env.DEST_LINK_ADDRESS || '0x514910771AF9Ca656af840dff83E8264EcF986CA' }); } // If PRIVATE_KEY still missing, try cwd-relative paths (e.g. run from repo root or relay dir) if (!process.env.PRIVATE_KEY || process.env.PRIVATE_KEY.includes('${')) { const cwd = process.cwd(); const tries = [ path.resolve(cwd, '../../.env'), // cwd is services/relay path.resolve(cwd, 'smom-dbis-138/.env') // cwd is project root (proxmox) ]; for (const p of tries) { if (p !== projectEnv && p !== relayEnv) dotenv.config({ path: p }); } } function getEffectivePrivateKey() { const v = process.env.RELAYER_PRIVATE_KEY || process.env.PRIVATE_KEY || ''; return (v && !v.includes('${')) ? v : (process.env.PRIVATE_KEY || ''); } // Build mainnet RPC URL; use INFURA_PROJECT_SECRET (or METAMASK_SECRET) for Basic Auth when using Infura function getBscRelayRpcUrl() { const relay = process.env.BSC_RELAY_RPC_URL || ''; if (relay && !relay.includes('${')) return relay; const dest = process.env.DEST_RPC_URL || ''; if (dest && !dest.includes('${')) return dest; const fallback = process.env.BSC_RPC_URL || process.env.BSC_MAINNET_RPC || ''; if (fallback && !fallback.includes('${')) return fallback; return ''; } function getDestinationRpcPoolUrls() { const destChainId = process.env.DEST_CHAIN_ID ? parseInt(process.env.DEST_CHAIN_ID, 10) : 1; const poolFromEnv = parseRpcPool( process.env.DEST_RPC_URL_POOL, process.env.RPC_URL_MAINNET_POOL, process.env.DEST_RPC_URL ); if (poolFromEnv.length > 0) return poolFromEnv; if (destChainId === 56) { const bscRelay = getBscRelayRpcUrl(); if (bscRelay) return parseRpcPool(bscRelay); } const explicit = process.env.DEST_RPC_URL || ''; if (explicit && !explicit.includes('${')) return parseRpcPool(explicit); return parseRpcPool(getMainnetRpcUrl()); } function getDestinationRpcUrl() { const pool = getDestinationRpcPoolUrls(); return pool[0] || getMainnetRpcUrl(); } /** Optional protected submit endpoint (Blink / MEV Blocker) for destination relay txs. */ function getDestinationSubmitRpcUrl() { const explicit = process.env.RELAY_DEST_SUBMIT_RPC_URL || process.env.BLINK_RPC_URL || process.env.MEV_BLOCKER_RPC_URL || ''; if (explicit && !explicit.includes('${')) return explicit; return ''; } function getMainnetRpcUrl() { let raw = process.env.RPC_URL_MAINNET || process.env.ETHEREUM_MAINNET_RPC || ''; if (!raw && process.env.INFURA_PROJECT_ID && !String(process.env.INFURA_PROJECT_ID).includes('${')) { raw = `https://mainnet.infura.io/v3/${process.env.INFURA_PROJECT_ID}`; } if (!raw) raw = 'https://ethereum.publicnode.com'; const secret = process.env.INFURA_PROJECT_SECRET || process.env.METAMASK_SECRET || ''; const infuraMatch = raw.match(/^https:\/\/mainnet\.infura\.io\/v3\/([a-f0-9]+)$/i); if (infuraMatch && secret && !secret.includes('${')) { const projectId = infuraMatch[1]; return `https://${encodeURIComponent(projectId)}:${encodeURIComponent(secret)}@mainnet.infura.io/v3/${projectId}`; } return raw; } function getSourceRpcUrl() { return ( process.env.SOURCE_RPC_URL || process.env.RPC_URL_138_PUBLIC || process.env.CHAIN138_RPC_URL_PUBLIC || process.env.RPC_URL_138 || process.env.RPC_URL || 'https://rpc.public-0138.defi-oracle.io' ); } function getDestinationRelayBridgeAddress() { if (Object.prototype.hasOwnProperty.call(process.env, 'DEST_RELAY_BRIDGE')) { return process.env.DEST_RELAY_BRIDGE || ''; } return ( process.env.CCIP_RELAY_BRIDGE_MAINNET || process.env.RELAY_BRIDGE_MAINNET || '0xF9A32F37099c582D28b4dE7Fca6eaC1e5259f939' ); } function getDestinationRelayBridgeLinkAddress() { return ( process.env.DEST_RELAY_BRIDGE_LINK || process.env.CCIP_RELAY_BRIDGE_LINK_MAINNET || '' ); } function getSkipMessageIds() { return new Set( String(process.env.RELAY_SKIP_MESSAGE_IDS || '') .split(',') .map((s) => s.trim().toLowerCase()) .filter(Boolean) ); } export const config = { // Source chain: defaults to Chain 138 but can be overridden for reverse relay profiles. sourceChain: { name: process.env.SOURCE_CHAIN_NAME || 'Chain 138', chainId: DEFAULT_SOURCE_CHAIN_ID, rpcUrl: getSourceRpcUrl(), routerAddress: process.env.SOURCE_ROUTER_ADDRESS || process.env.SOURCE_ROUTER_ADDRESS || process.env.CCIP_ROUTER_CHAIN138 || process.env.CCIP_ROUTER || '0x42DAb7b888Dd382bD5Adcf9E038dBF1fD03b4817', bridgeAddress: process.env.SOURCE_BRIDGE_ADDRESS || process.env.CCIPWETH9_BRIDGE_CHAIN138 || process.env.CCIPWETH9_BRIDGE_CHAIN138_LINK || '0xcacfd227A040002e49e2e01626363071324f820a' }, // Destination chain: defaults to Ethereum Mainnet for backward compatibility. // Override for other chains (e.g. BSC/AVAX) with DEST_* env vars. destinationChain: { name: process.env.DEST_CHAIN_NAME || 'Ethereum Mainnet', chainId: process.env.DEST_CHAIN_ID ? parseInt(process.env.DEST_CHAIN_ID) : 1, rpcUrl: getDestinationRpcUrl(), rpcPoolUrls: getDestinationRpcPoolUrls(), submitRpcUrl: getDestinationSubmitRpcUrl(), // Upstream BSC mainnet RPC for operator tooling (casts, fee probes); relay lane may use ngrok. upstreamRpcUrl: process.env.DEST_RPC_UPSTREAM_URL || process.env.BSC_RPC_URL || process.env.BSC_MAINNET_RPC || '', relayRouterAddress: process.env.DEST_RELAY_ROUTER || process.env.CCIP_RELAY_ROUTER_MAINNET || process.env.RELAY_ROUTER_MAINNET || '0x416564Ab73Ad5710855E98dC7bC7Bff7387285BA', relayBridgeAddress: getDestinationRelayBridgeAddress(), relayBridgeLinkAddress: getDestinationRelayBridgeLinkAddress(), deliveryMode: process.env.DEST_DELIVERY_MODE || 'router', // Optional CSV allowlist for per-message receiver routing. // When set, relay will only forward to bridges in this list. relayBridgeAllowlist: (process.env.DEST_RELAY_BRIDGE_ALLOWLIST || '') .split(',') .map((s) => s.trim().toLowerCase()) .filter(Boolean), chainSelector: BigInt(process.env.DEST_CHAIN_SELECTOR || '5009297550715157269'), weth9Address: process.env.DEST_WETH9_ADDRESS || '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2' }, // Token address mapping for the active relay pair. tokenMapping: getTokenMapping(), // Relay profiles use explicit selectors; default source selector remains 138 for legacy profiles. sourceChainSelector: BigInt(process.env.SOURCE_CHAIN_SELECTOR || '138'), // Relayer configuration (dotenv does not expand ${PRIVATE_KEY}; getter uses effective key) relayer: { get privateKey() { return getEffectivePrivateKey(); }, address: process.env.RELAYER_ADDRESS || '' }, // Monitoring configuration monitoring: { startBlock: (() => { const raw = process.env.START_BLOCK; if (raw === undefined || raw === '') return 'latest'; const s = String(raw).trim().toLowerCase(); if (s === 'latest') return 'latest'; const n = parseInt(raw, 10); return Number.isFinite(n) ? n : 'latest'; })(), pollInterval: process.env.POLL_INTERVAL ? parseInt(process.env.POLL_INTERVAL) : 5000, // 5 seconds confirmationBlocks: process.env.CONFIRMATION_BLOCKS ? parseInt(process.env.CONFIRMATION_BLOCKS) : 1, finalityDelayBlocks: process.env.FINALITY_DELAY_BLOCKS ? parseInt(process.env.FINALITY_DELAY_BLOCKS) : 2, replayWindowBlocks: process.env.REPLAY_WINDOW_BLOCKS ? parseInt(process.env.REPLAY_WINDOW_BLOCKS) : 32 }, // Retry configuration retry: { maxRetries: process.env.MAX_RETRIES ? parseInt(process.env.MAX_RETRIES) : 3, retryDelay: process.env.RETRY_DELAY ? parseInt(process.env.RETRY_DELAY) : 5000 // 5 seconds }, // Parallel destination delivery (NonceManager on submit signer; default 1 = legacy serial) concurrency: { maxConcurrent: (() => { const raw = parseInt(process.env.RELAY_MAX_CONCURRENT || '1', 10); if (!Number.isFinite(raw) || raw < 1) return 1; return Math.min(raw, 12); })() }, queuePersistence: { path: (() => { const raw = process.env.RELAY_QUEUE_STATE_PATH; if (!raw) return path.resolve(__dirname, '../data/queue-state.json'); return path.isAbsolute(raw) ? raw : path.resolve(__dirname, '..', raw); })() }, skipMessageIds: getSkipMessageIds() }; /** * Relay shedding: pause **destination-chain** relay txs (saves Mainnet gas) while still polling * the source router so messages can queue until you turn delivery back on. * * Toggle via env (restart relay service after edits, or rely on your process manager’s reload): * - `RELAY_SHEDDING=1` | `true` | `yes` | `on` → shedding **on** (no `relayMessage` / direct `ccipReceive`) * - `RELAY_DELIVERY_ENABLED=0` | `false` | `no` | `off` → same as shedding on * * Default: shedding **off** (normal operation). * * Re-reads `process.env` on each call so a future SIGHUP/full restart pattern stays simple. */ export function isRelayShedding() { const shed = String(process.env.RELAY_SHEDDING || '').trim().toLowerCase(); if (['1', 'true', 'yes', 'on'].includes(shed)) return true; const del = String(process.env.RELAY_DELIVERY_ENABLED ?? '1').trim().toLowerCase(); if (['0', 'false', 'no', 'off'].includes(del)) return true; return false; } /** Source `eth_getLogs` poll interval while shedding (ms). Min 5000. Default 60000. */ export function getRelaySheddingSourcePollIntervalMs() { const v = parseInt(process.env.RELAY_SHEDDING_SOURCE_POLL_INTERVAL_MS || '60000', 10); return Number.isFinite(v) && v >= 5000 ? v : 60000; } /** Sleep between queue-processor iterations while shedding (ms). Min 1000. Default 5000. */ export function getRelaySheddingQueueIdleMs() { const v = parseInt(process.env.RELAY_SHEDDING_QUEUE_POLL_MS || '5000', 10); return Number.isFinite(v) && v >= 1000 ? v : 5000; } // Validate required configuration (use same helper so we validate the effective key) if (!getEffectivePrivateKey()) { throw new Error('RELAYER_PRIVATE_KEY or PRIVATE_KEY environment variable is required. Set PRIVATE_KEY in smom-dbis-138/.env or RELAYER_PRIVATE_KEY in services/relay/.env'); } // Validate relay addresses (warn but don't fail - they may be set later) if (!config.destinationChain.relayRouterAddress) { console.warn('Warning: Relay router address not configured. Service will not start until configured.'); }