Files
smom-dbis-138/services/token-aggregation/src/indexer/cross-chain-indexer.ts
defiQUG 76aa419320 feat: bridges, PMM, flash workflow, token-aggregation, and deployment docs
- CCIP/trustless bridge contracts, GRU tokens, DEX/PMM tests, reserve vault.
- Token-aggregation service routes, planner, chain config, relay env templates.
- Config snapshots and multi-chain deployment markdown updates.
- gitignore services/btc-intake/dist/ (tsc output); do not track dist.

Run forge build && forge test before deploy (large solc graph).

Made-with: Cursor
2026-04-07 23:40:52 -07:00

480 lines
16 KiB
TypeScript

/**
* Cross-chain indexer: fetches bridge/swap events and aggregates volume for CMC/CoinGecko reporting.
* Queries Chain 138 (and optionally 651940) for CrossChainTransferInitiated, SwapAndBridgeExecuted,
* LockForAlltra, AlltraBridgeInitiated, etc.
*/
import { ethers } from 'ethers';
import { getChainConfig } from '../config/chains';
import { CHAIN_138_BRIDGES, BridgeConfig, BridgeLane } from '../config/cross-chain-bridges';
import { logger } from '../utils/logger';
export interface CrossChainEvent {
txHash: string;
blockNumber: number;
timestamp: number;
sourceChainId: number;
destChainId: number;
destChainName: string;
bridgeType: string;
tokenSymbol?: string;
amountWei: string;
amountUsd?: number;
sender?: string;
recipient?: string;
messageId?: string;
}
export interface CrossChainVolumeByLane {
sourceChainId: number;
destChainId: number;
destChainName: string;
bridgeType: string;
tokenSymbol?: string;
volume24hWei: string;
volume7dWei: string;
volume30dWei: string;
txCount24h: number;
txCount7d: number;
txCount30d: number;
}
export interface CrossChainReport {
generatedAt: string;
crossChainPools: Array<{
type: string;
sourceChainId: number;
destChainId: number;
destChainName: string;
bridgeAddress: string;
tokenSymbol?: string;
bridgeType: string;
tvlUsd?: number;
isActive: boolean;
}>;
volumeByLane: CrossChainVolumeByLane[];
atomicSwapVolume24h: number;
bridgeVolume24hTotal: number;
events: CrossChainEvent[];
}
const CCIP_TRANSFER_ABI = [
'event CrossChainTransferInitiated(bytes32 indexed messageId, address indexed sender, uint64 indexed destinationChainSelector, address recipient, uint256 amount, uint256 nonce)',
'event CrossChainTransferCompleted(bytes32 indexed messageId, uint64 indexed sourceChainSelector, address indexed recipient, uint256 amount)',
];
const SWAP_BRIDGE_ABI = [
'event SwapAndBridgeExecuted(address indexed sourceToken, address indexed bridgeableToken, uint256 amountIn, uint256 amountToBridge, uint64 destinationChainSelector, address recipient, bytes32 messageId)',
];
const ALLTRA_LOCK_ABI = [
'event LockForAlltra(bytes32 indexed requestId, address indexed sender, address indexed token, uint256 amount, address recipient, uint256 sourceChainId)',
'event UnlockOnAlltra(bytes32 indexed requestId, address indexed recipient, address indexed token, uint256 amount)',
];
const ALLTRA_ADAPTER_ABI = [
'event AlltraBridgeInitiated(bytes32 indexed requestId, address indexed sender, address indexed token, uint256 amount, address recipient)',
'event AlltraBridgeConfirmed(bytes32 indexed requestId, address indexed recipient, address indexed token, uint256 amount)',
];
const UNIVERAL_CCIP_ABI = [
'event BridgeExecuted(bytes32 indexed messageId, address indexed token, address indexed sender, uint256 amount, uint64 destinationChain, address recipient, bool usedPMM)',
'event MessageReceived(bytes32 indexed messageId, uint64 indexed sourceChainSelector, address sender, address token, uint256 amount)',
];
const CROSS_CHAIN_QUERY_FALLBACK_BLOCK_SPAN = Math.max(
1,
Number(process.env.CROSS_CHAIN_QUERY_FALLBACK_BLOCK_SPAN || 5000)
);
function nowSec(): number {
return Math.floor(Date.now() / 1000);
}
function msAgo(hours: number): number {
return nowSec() - hours * 3600;
}
function isRpcRangeLimitError(error: unknown): boolean {
const parts = [
typeof error === 'object' && error ? (error as { message?: string }).message : '',
typeof error === 'object' && error ? (error as { shortMessage?: string }).shortMessage : '',
typeof error === 'object' && error
? ((error as { error?: { message?: string } }).error?.message ?? '')
: '',
];
return parts.some((part) =>
typeof part === 'string' && /range limit|exceeds maximum rpc range/i.test(part)
);
}
async function queryFilterWithRangeFallback(
contract: ethers.Contract,
filter: ReturnType<ethers.Contract['filters'][string]> | { address: string },
fromBlock: number,
toBlock: number
): Promise<ethers.EventLog[]> {
try {
return (await contract.queryFilter(filter as never, fromBlock, toBlock)) as ethers.EventLog[];
} catch (error) {
if (!isRpcRangeLimitError(error) || fromBlock >= toBlock) {
throw error;
}
}
const logs: ethers.EventLog[] = [];
for (
let start = fromBlock;
start <= toBlock;
start += CROSS_CHAIN_QUERY_FALLBACK_BLOCK_SPAN
) {
const end = Math.min(start + CROSS_CHAIN_QUERY_FALLBACK_BLOCK_SPAN - 1, toBlock);
const chunk = (await contract.queryFilter(filter as never, start, end)) as ethers.EventLog[];
logs.push(...chunk);
}
return logs;
}
/** Fetch CrossChainTransferInitiated events from CCIP WETH bridges */
async function fetchCCIPEvents(
provider: ethers.JsonRpcProvider,
bridge: BridgeConfig,
fromBlock: number,
toBlock: number
): Promise<CrossChainEvent[]> {
const events: CrossChainEvent[] = [];
try {
const contract = new ethers.Contract(bridge.address, CCIP_TRANSFER_ABI, provider);
const filter = contract.filters.CrossChainTransferInitiated();
const logs = await queryFilterWithRangeFallback(contract, filter, fromBlock, toBlock);
for (const log of logs) {
const args = (log as ethers.EventLog).args as unknown as { messageId: string; sender: string; destinationChainSelector: bigint; recipient: string; amount: bigint };
const lane = bridge.lanes.find((l: BridgeLane) => l.destSelector === args.destinationChainSelector?.toString());
const destChainId = lane?.destChainId ?? 0;
const destChainName = lane?.destChainName ?? `Chain ${args.destinationChainSelector}`;
events.push({
txHash: log.transactionHash,
blockNumber: log.blockNumber,
timestamp: 0,
sourceChainId: bridge.chainId,
destChainId,
destChainName,
bridgeType: bridge.type,
tokenSymbol: bridge.tokenSymbol,
amountWei: args.amount?.toString() ?? '0',
sender: args.sender,
recipient: args.recipient,
messageId: args.messageId,
});
}
} catch (err) {
logger.warn(`Cross-chain indexer: CCIP events for ${bridge.address} failed:`, err);
}
return events;
}
/** Fetch SwapAndBridgeExecuted (optional - contract may not be deployed) */
async function fetchSwapBridgeEvents(
provider: ethers.JsonRpcProvider,
address: string | undefined,
chainId: number,
fromBlock: number,
toBlock: number
): Promise<CrossChainEvent[]> {
if (!address || address === '0x0000000000000000000000000000000000000000') return [];
const events: CrossChainEvent[] = [];
try {
const contract = new ethers.Contract(address, SWAP_BRIDGE_ABI, provider);
const filter = contract.filters.SwapAndBridgeExecuted();
const logs = await queryFilterWithRangeFallback(contract, filter, fromBlock, toBlock);
for (const log of logs) {
const args = (log as ethers.EventLog).args as unknown as { sourceToken: string; bridgeableToken: string; amountIn: bigint; amountToBridge: bigint; destinationChainSelector: bigint; recipient: string; messageId: string };
const selector = args.destinationChainSelector?.toString();
const destChainId = selector === '5009297550715157269' ? 1 : 0;
const destChainName = destChainId === 1 ? 'Ethereum Mainnet' : `Selector ${selector}`;
events.push({
txHash: log.transactionHash,
blockNumber: log.blockNumber,
timestamp: 0,
sourceChainId: chainId,
destChainId,
destChainName,
bridgeType: 'swap_bridge',
amountWei: args.amountToBridge?.toString() ?? '0',
recipient: args.recipient,
messageId: args.messageId,
});
}
} catch {
// Contract may not exist
}
return events;
}
/** Fetch LockForAlltra (AlltraCustomBridge) or AlltraBridgeInitiated (AlltraAdapter) */
async function fetchAlltraEvents(
provider: ethers.JsonRpcProvider,
bridge: BridgeConfig,
fromBlock: number,
toBlock: number
): Promise<CrossChainEvent[]> {
const events: CrossChainEvent[] = [];
const lane = bridge.lanes[0];
const destChainId = lane?.destChainId ?? 651940;
const destChainName = lane?.destChainName ?? 'ALL Mainnet';
try {
const lockContract = new ethers.Contract(bridge.address, ALLTRA_LOCK_ABI, provider);
const lockLogs = await queryFilterWithRangeFallback(
lockContract,
lockContract.filters.LockForAlltra(),
fromBlock,
toBlock
).catch(() => []);
for (const log of lockLogs) {
const args = (log as ethers.EventLog).args as unknown as { requestId: string; sender: string; token: string; amount: bigint; recipient: string };
events.push({
txHash: log.transactionHash,
blockNumber: log.blockNumber,
timestamp: 0,
sourceChainId: bridge.chainId,
destChainId,
destChainName,
bridgeType: 'alltra',
amountWei: args.amount?.toString() ?? '0',
sender: args.sender,
recipient: args.recipient,
});
}
} catch {
// Try AlltraAdapter (AlltraBridgeInitiated) if LockForAlltra not present
}
try {
const adapterContract = new ethers.Contract(bridge.address, ALLTRA_ADAPTER_ABI, provider);
const initLogs = await queryFilterWithRangeFallback(
adapterContract,
adapterContract.filters.AlltraBridgeInitiated(),
fromBlock,
toBlock
).catch(() => []);
for (const log of initLogs) {
const args = (log as ethers.EventLog).args as unknown as { requestId: string; sender: string; token: string; amount: bigint; recipient: string };
events.push({
txHash: log.transactionHash,
blockNumber: log.blockNumber,
timestamp: 0,
sourceChainId: bridge.chainId,
destChainId,
destChainName,
bridgeType: 'alltra',
amountWei: args.amount?.toString() ?? '0',
sender: args.sender,
recipient: args.recipient,
});
}
} catch (err) {
logger.warn(`Cross-chain indexer: Alltra events for ${bridge.address} failed:`, err);
}
return events;
}
/** Fetch BridgeExecuted from UniversalCCIPBridge */
async function fetchUniversalCCIPEvents(
provider: ethers.JsonRpcProvider,
bridge: BridgeConfig,
fromBlock: number,
toBlock: number
): Promise<CrossChainEvent[]> {
const events: CrossChainEvent[] = [];
try {
const contract = new ethers.Contract(bridge.address, UNIVERAL_CCIP_ABI, provider);
const filter = contract.filters.BridgeExecuted?.() ?? { address: bridge.address };
const logs = await queryFilterWithRangeFallback(contract, filter, fromBlock, toBlock);
for (const log of logs) {
const args = (log as ethers.EventLog).args as unknown as { messageId: string; token: string; sender: string; amount: bigint; destinationChain: bigint; recipient: string };
const selector = args.destinationChain?.toString();
const lane = bridge.lanes.find((l: BridgeLane) => l.destSelector === selector);
const destChainId = lane?.destChainId ?? (selector === '5009297550715157269' ? 1 : 651940);
const destChainName = lane?.destChainName ?? `Chain ${selector}`;
events.push({
txHash: log.transactionHash,
blockNumber: log.blockNumber,
timestamp: 0,
sourceChainId: bridge.chainId,
destChainId,
destChainName,
bridgeType: 'universal_ccip',
amountWei: args.amount?.toString() ?? '0',
sender: args.sender,
recipient: args.recipient,
messageId: args.messageId,
});
}
} catch (err) {
logger.warn(`Cross-chain indexer: UniversalCCIP events for ${bridge.address} failed:`, err);
}
return events;
}
/** Aggregate events into volume by lane */
function aggregateVolumeByLane(
events: CrossChainEvent[],
window24h: number,
window7d: number,
window30d: number
): CrossChainVolumeByLane[] {
const byKey = new Map<string, { lane: CrossChainVolumeByLane; events: CrossChainEvent[] }>();
for (const e of events) {
const key = `${e.sourceChainId}-${e.destChainId}-${e.bridgeType}-${e.tokenSymbol ?? ''}`;
if (!byKey.has(key)) {
byKey.set(key, {
lane: {
sourceChainId: e.sourceChainId,
destChainId: e.destChainId,
destChainName: e.destChainName,
bridgeType: e.bridgeType,
tokenSymbol: e.tokenSymbol,
volume24hWei: '0',
volume7dWei: '0',
volume30dWei: '0',
txCount24h: 0,
txCount7d: 0,
txCount30d: 0,
},
events: [],
});
}
byKey.get(key)!.events.push(e);
}
const result: CrossChainVolumeByLane[] = [];
for (const { lane, events: laneEvents } of byKey.values()) {
let v24 = BigInt(0);
let v7 = BigInt(0);
let v30 = BigInt(0);
let c24 = 0;
let c7 = 0;
let c30 = 0;
for (const e of laneEvents) {
const amt = BigInt(e.amountWei);
if (e.timestamp >= window24h) {
v24 += amt;
c24++;
}
if (e.timestamp >= window7d) {
v7 += amt;
c7++;
}
if (e.timestamp >= window30d) {
v30 += amt;
c30++;
}
}
// If timestamps are 0, assume all events are in window (we don't have block->time easily)
if (laneEvents.length > 0 && laneEvents.every((x) => x.timestamp === 0)) {
v24 = laneEvents.reduce((s, x) => s + BigInt(x.amountWei), BigInt(0));
v7 = v24;
v30 = v24;
c24 = laneEvents.length;
c7 = c24;
c30 = c24;
}
lane.volume24hWei = v24.toString();
lane.volume7dWei = v7.toString();
lane.volume30dWei = v30.toString();
lane.txCount24h = c24;
lane.txCount7d = c7;
lane.txCount30d = c30;
result.push(lane);
}
return result;
}
/** Build full cross-chain report */
export async function buildCrossChainReport(chainId: number = 138): Promise<CrossChainReport> {
const config = getChainConfig(chainId);
if (!config) {
return {
generatedAt: new Date().toISOString(),
crossChainPools: [],
volumeByLane: [],
atomicSwapVolume24h: 0,
bridgeVolume24hTotal: 0,
events: [],
};
}
const provider = new ethers.JsonRpcProvider(config.rpcUrl);
const currentBlock = await provider.getBlockNumber();
const blocksPerDay = Math.floor((24 * 3600) / config.blockTime);
const fromBlock = Math.max(0, currentBlock - blocksPerDay * 30);
const window24h = msAgo(24);
const window7d = msAgo(7 * 24);
const window30d = msAgo(30 * 24);
const allEvents: CrossChainEvent[] = [];
const bridges = CHAIN_138_BRIDGES.filter((b: BridgeConfig) => b.chainId === chainId);
for (const bridge of bridges) {
if (bridge.type === 'ccip_weth9' || bridge.type === 'ccip_weth10') {
const evts = await fetchCCIPEvents(provider, bridge, fromBlock, currentBlock);
allEvents.push(...evts);
} else if (bridge.type === 'alltra') {
const evts = await fetchAlltraEvents(provider, bridge, fromBlock, currentBlock);
allEvents.push(...evts);
} else if (bridge.type === 'universal_ccip') {
const evts = await fetchUniversalCCIPEvents(provider, bridge, fromBlock, currentBlock);
allEvents.push(...evts);
}
}
const swapBridgeAddr = process.env.SWAP_BRIDGE_COORDINATOR_ADDRESS;
const swapEvts = await fetchSwapBridgeEvents(provider, swapBridgeAddr, chainId, fromBlock, currentBlock);
allEvents.push(...swapEvts);
const volumeByLane = aggregateVolumeByLane(allEvents, window24h, window7d, window30d);
let bridgeVolume24hTotal = 0;
for (const v of volumeByLane) {
const amt = parseFloat(v.volume24hWei) / 1e18;
bridgeVolume24hTotal += amt; // Approximate; real USD would need price oracle
}
const crossChainPools = bridges.map((b: BridgeConfig) =>
b.lanes.map((lane: BridgeLane) => ({
type: b.type,
sourceChainId: b.chainId,
destChainId: lane.destChainId,
destChainName: lane.destChainName,
bridgeAddress: b.address,
tokenSymbol: b.tokenSymbol,
bridgeType: lane.bridgeType ?? b.type,
isActive: true,
}))
).flat();
return {
generatedAt: new Date().toISOString(),
crossChainPools,
volumeByLane,
atomicSwapVolume24h: swapEvts.reduce((s, e) => s + parseFloat(e.amountWei) / 1e18, 0),
bridgeVolume24hTotal,
events: allEvents.slice(0, 500), // Limit for response size
};
}