Some checks failed
CI/CD Pipeline / Solidity Contracts (push) Failing after 1m11s
CI/CD Pipeline / Security Scanning (push) Has been cancelled
CI/CD Pipeline / Lint and Format (push) Has been cancelled
CI/CD Pipeline / Terraform Validation (push) Has been cancelled
CI/CD Pipeline / Kubernetes Validation (push) Has been cancelled
Validation / validate-genesis (push) Has been cancelled
Validation / validate-terraform (push) Has been cancelled
Validation / validate-kubernetes (push) Has been cancelled
Validation / validate-smart-contracts (push) Has been cancelled
Validation / validate-security (push) Has been cancelled
Validation / validate-documentation (push) Has been cancelled
Deploy ChainID 138 / Deploy ChainID 138 (push) Failing after 1m4s
HYBX OMNL TypeScript & anchor / token-aggregation build + reconcile artifact (push) Failing after 31s
OMNL reconcile anchor / Run omnl:reconcile and upload artifacts (push) Failing after 29s
Verify Deployment / Verify Deployment (push) Failing after 57s
Relay router, reserve system, oracle publisher, token-aggregation compliance middleware, and Monad deployment scripts. Co-authored-by: Cursor <cursoragent@cursor.com>
369 lines
14 KiB
JavaScript
369 lines
14 KiB
JavaScript
/**
|
||
* 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.');
|
||
}
|