import { ethers, type Log } from 'ethers'; import { checkpointIndexerConfig as cfg } from './config'; import { decodePaymentAttestedLogs, PAYMENT_ATTESTED_TOPIC0 } from './iso20022LogDecode'; import { decodeChain138ActivityNotifiedLogs, CHAIN138_ACTIVITY_NOTIFIED_TOPIC0, } from './participantSurfaceLogDecode'; const TRANSACTION_MIRRORED_TOPIC0 = '0xc25ce5062c7e42c68fa21fe088d21e609cc0c61b8bec3180681363bb5cf02a9e'; const PARTICIPANT_CREDITED_TOPIC0 = '0x853ebad0824f583553bcb4360a0df947d39654e86aba8d7c0625499cbf09f0d5'; const PARTICIPANT_DEBITED_TOPIC0 = '0xf122cad7c8ada37ddd6b6ddca0982b9f7be689cde15eedfd767cc207fa7dc2c8'; const CHECKPOINT_SUBMITTED_TOPIC0 = ethers.id( 'CheckpointSubmitted(uint64,uint256,bytes32,uint16,uint32,uint64,address,bytes32)' ); export type MainnetAttestationLink = { layer: string; label: string; mainnetTxHash: string; mainnetExplorerUrl: string; contractAddress?: string; logIndex?: number; meta?: Record; }; type BatchMainnetMeta = { hubTx?: string; mirrorTx?: string; activityRegistryV1Tx?: string; activityRegistryV2Tx?: string; participantSurfaceTx?: string; participantTopLevelTxs?: string[]; }; const META_LAYER_MAP: Array<{ key: keyof BatchMainnetMeta; layer: string; label: string }> = [ { key: 'hubTx', layer: 'checkpointHub', label: 'Checkpoint batch (hub)' }, { key: 'mirrorTx', layer: 'transactionMirror', label: 'Transaction mirror' }, { key: 'activityRegistryV1Tx', layer: 'activityRegistryV1', label: 'Activity registry V1' }, { key: 'activityRegistryV2Tx', layer: 'activityRegistryV2', label: 'ISO-20022 PaymentAttested (V2)' }, { key: 'participantSurfaceTx', layer: 'participantSurface', label: 'Participant surface' }, ]; export function linksFromBatchMeta( raw: { mainnetAttestation?: BatchMainnetMeta } | null ): MainnetAttestationLink[] { const meta = raw?.mainnetAttestation; if (!meta) return []; const links: MainnetAttestationLink[] = []; for (const row of META_LAYER_MAP) { const hash = meta[row.key]; if (typeof hash === 'string' && /^0x[a-fA-F0-9]{64}$/.test(hash)) { links.push({ layer: row.layer, label: row.label, mainnetTxHash: hash, mainnetExplorerUrl: etherscanTx(hash), }); } } if (Array.isArray(meta.participantTopLevelTxs)) { for (const hash of meta.participantTopLevelTxs) { if (!/^0x[a-fA-F0-9]{64}$/.test(hash)) continue; links.push({ layer: 'participantWallet', label: 'Participant wallet (0 ETH attestation tx)', mainnetTxHash: hash, mainnetExplorerUrl: etherscanTx(hash), }); } } return links; } function etherscanTx(hash: string): string { return `https://etherscan.io/tx/${hash}`; } function logRows(logs: Log[]) { return logs.map((log) => ({ topics: log.topics as string[], transactionHash: log.transactionHash, blockNumber: log.blockNumber.toString(), logIndex: log.index.toString(), data: log.data, })); } function uniqLinks(links: MainnetAttestationLink[]): MainnetAttestationLink[] { const seen = new Set(); return links.filter((l) => { const k = `${l.layer}:${l.mainnetTxHash}:${l.logIndex ?? ''}`; if (seen.has(k)) return false; seen.add(k); return true; }); } const LOG_LOOKBACK_BLOCKS = parseInt(process.env.CHECKPOINT_MAINNET_LOGS_LOOKBACK || '120000', 10); async function resolveFromBlock( provider: ethers.JsonRpcProvider, batchId?: string ): Promise { const head = await provider.getBlockNumber(); const fallback = Math.max(0, head - LOG_LOOKBACK_BLOCKS); if (!batchId || batchId === '0' || !cfg.checkpointProxy) return fallback; try { const batchTopic = ethers.zeroPadValue(ethers.toBeHex(BigInt(batchId)), 32); const hubLogs = await provider.getLogs({ address: cfg.checkpointProxy, fromBlock: fallback, toBlock: 'latest', topics: [CHECKPOINT_SUBMITTED_TOPIC0, batchTopic], }); if (hubLogs.length) { return Math.max(0, hubLogs[0].blockNumber - 10); } } catch { /* use fallback */ } return fallback; } export async function resolveTxMainnetLinks( provider: ethers.JsonRpcProvider, txHash: string, batchId?: string ): Promise { const links: MainnetAttestationLink[] = []; const want = txHash.toLowerCase(); const txTopic = ethers.zeroPadValue(txHash, 32); const fromBlock = await resolveFromBlock(provider, batchId); if (batchId && batchId !== '0' && cfg.checkpointProxy) { try { const batchTopic = ethers.zeroPadValue(ethers.toBeHex(BigInt(batchId)), 32); const hubLogs = await provider.getLogs({ address: cfg.checkpointProxy, fromBlock, toBlock: 'latest', topics: [CHECKPOINT_SUBMITTED_TOPIC0, batchTopic], }); for (const log of hubLogs) { links.push({ layer: 'checkpointHub', label: `Checkpoint batch #${batchId}`, mainnetTxHash: log.transactionHash, mainnetExplorerUrl: etherscanTx(log.transactionHash), contractAddress: cfg.checkpointProxy, logIndex: log.index, }); } } catch { /* non-fatal — RPC may prune old logs */ } } try { const mirrorLogs = await provider.getLogs({ address: cfg.legacyMirror, fromBlock, toBlock: 'latest', topics: [TRANSACTION_MIRRORED_TOPIC0, txTopic], }); for (const log of mirrorLogs) { links.push({ layer: 'transactionMirror', label: 'Transaction mirror', mainnetTxHash: log.transactionHash, mainnetExplorerUrl: etherscanTx(log.transactionHash), contractAddress: cfg.legacyMirror, logIndex: log.index, }); } } catch { /* non-fatal */ } if (cfg.addressActivityRegistry) { for (const [topic0, role] of [ [PARTICIPANT_CREDITED_TOPIC0, 'credited'], [PARTICIPANT_DEBITED_TOPIC0, 'debited'], ] as const) { try { const regLogs = await provider.getLogs({ address: cfg.addressActivityRegistry, fromBlock, toBlock: 'latest', topics: [topic0, txTopic], }); for (const log of regLogs) { links.push({ layer: 'activityRegistryV1', label: `Activity registry V1 (${role})`, mainnetTxHash: log.transactionHash, mainnetExplorerUrl: etherscanTx(log.transactionHash), contractAddress: cfg.addressActivityRegistry, logIndex: log.index, meta: { role }, }); } } catch { /* non-fatal */ } } } if (cfg.addressActivityRegistryV2) { try { const v2Logs = await provider.getLogs({ address: cfg.addressActivityRegistryV2, fromBlock, toBlock: 'latest', topics: [PAYMENT_ATTESTED_TOPIC0, txTopic], }); const decoded = decodePaymentAttestedLogs({ status: '1', result: logRows(v2Logs) }); for (const row of decoded) { if (row.chain138TxHash.toLowerCase() !== want) continue; links.push({ layer: 'activityRegistryV2', label: 'ISO-20022 PaymentAttested (V2)', mainnetTxHash: row.mainnetTxHash, mainnetExplorerUrl: row.mainnetExplorerUrl, contractAddress: cfg.addressActivityRegistryV2, logIndex: row.logIndex ? parseInt(row.logIndex, 10) : undefined, meta: { instructionId: row.instructionId, uetr: row.uetr }, }); } } catch { /* non-fatal */ } } if (cfg.participantSurface) { try { const surfaceLogs = await provider.getLogs({ address: cfg.participantSurface, fromBlock, toBlock: 'latest', topics: [CHAIN138_ACTIVITY_NOTIFIED_TOPIC0, null, txTopic], }); const decoded = decodeChain138ActivityNotifiedLogs({ status: '1', result: logRows(surfaceLogs), }); for (const row of decoded) { if (row.chain138TxHash.toLowerCase() !== want) continue; links.push({ layer: 'participantSurface', label: 'Participant surface notification', mainnetTxHash: row.mainnetTxHash, mainnetExplorerUrl: row.mainnetExplorerUrl, contractAddress: cfg.participantSurface, logIndex: row.logIndex ? parseInt(row.logIndex, 10) : undefined, meta: { participant: row.participant, direction: String(row.direction), }, }); } } catch { /* non-fatal */ } } return uniqLinks(links); }