- 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
480 lines
16 KiB
TypeScript
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
|
|
};
|
|
}
|