- RelayService: chunked eth_getLogs + adaptive split for strict RPCs - config: explicit START_BLOCK=latest vs numeric - README: topology, START_BLOCK, fund script, cast examples - .env.bsc.example: committed template (secrets stay in .env.bsc) Made-with: Cursor
245 lines
10 KiB
JavaScript
245 lines
10 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';
|
||
|
||
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 */ }
|
||
|
||
// 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 pair.addressMapFromTo;
|
||
}
|
||
const fromFile = getRelayTokenMapping && getRelayTokenMapping();
|
||
if (sourceChainId === 138 && destinationChainId === 1 && fromFile && Object.keys(fromFile).length > 0) {
|
||
return 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 {
|
||
'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 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'
|
||
);
|
||
}
|
||
|
||
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: process.env.DEST_RPC_URL || getMainnetRpcUrl(),
|
||
relayRouterAddress:
|
||
process.env.DEST_RELAY_ROUTER ||
|
||
process.env.CCIP_RELAY_ROUTER_MAINNET ||
|
||
process.env.RELAY_ROUTER_MAINNET ||
|
||
'0xAd9A228CcEB4cbB612cD165FFB72fE090ff10Afb',
|
||
relayBridgeAddress:
|
||
getDestinationRelayBridgeAddress(),
|
||
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
|
||
}
|
||
};
|
||
|
||
/**
|
||
* 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.');
|
||
}
|