feat(checkpoint): expose mainnet Etherscan links on Chain 138 tx attestation.
Some checks failed
CI/CD Pipeline / Solidity Contracts (push) Failing after 1m12s
CI/CD Pipeline / Security Scanning (push) Successful in 2m38s
CI/CD Pipeline / Lint and Format (push) Failing after 40s
CI/CD Pipeline / Terraform Validation (push) Failing after 21s
CI/CD Pipeline / Kubernetes Validation (push) Successful in 24s
HYBX OMNL TypeScript & anchor / token-aggregation build + reconcile artifact (push) Failing after 24s
Validation / validate-genesis (push) Successful in 26s
Validation / validate-terraform (push) Failing after 21s
Validation / validate-kubernetes (push) Failing after 9s
Validation / validate-smart-contracts (push) Failing after 9s
Validation / validate-security (push) Failing after 1m7s
Validation / validate-documentation (push) Failing after 15s
Some checks failed
CI/CD Pipeline / Solidity Contracts (push) Failing after 1m12s
CI/CD Pipeline / Security Scanning (push) Successful in 2m38s
CI/CD Pipeline / Lint and Format (push) Failing after 40s
CI/CD Pipeline / Terraform Validation (push) Failing after 21s
CI/CD Pipeline / Kubernetes Validation (push) Successful in 24s
HYBX OMNL TypeScript & anchor / token-aggregation build + reconcile artifact (push) Failing after 24s
Validation / validate-genesis (push) Successful in 26s
Validation / validate-terraform (push) Failing after 21s
Validation / validate-kubernetes (push) Failing after 9s
Validation / validate-smart-contracts (push) Failing after 9s
Validation / validate-security (push) Failing after 1m7s
Validation / validate-documentation (push) Failing after 15s
Resolve etherscanLinks from batch metadata and optional mainnet log scans; serve a fast local-batch attestation path for the explorer CT. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -6,9 +6,22 @@ import { logger } from '../../utils/logger';
|
||||
const router: Router = Router();
|
||||
|
||||
function indexerBase(): string {
|
||||
return (process.env.CHECKPOINT_INDEXER_URL || 'http://127.0.0.1:3099').replace(/\/$/, '');
|
||||
const port = process.env.CHECKPOINT_INDEXER_PORT || '3100';
|
||||
return (
|
||||
process.env.CHECKPOINT_INDEXER_URL || `http://127.0.0.1:${port}`
|
||||
).replace(/\/$/, '');
|
||||
}
|
||||
|
||||
type MainnetAttestationLink = {
|
||||
layer: string;
|
||||
label: string;
|
||||
mainnetTxHash: string;
|
||||
mainnetExplorerUrl: string;
|
||||
contractAddress?: string;
|
||||
logIndex?: number;
|
||||
meta?: Record<string, string>;
|
||||
};
|
||||
|
||||
function batchDir(): string {
|
||||
const custom = process.env.CHECKPOINT_BATCH_DIR?.trim();
|
||||
if (custom) return custom;
|
||||
@@ -49,6 +62,16 @@ function loadLocalAttestation(txHash: string): {
|
||||
batchId: string;
|
||||
batchTotalUsd?: string;
|
||||
leaf: CheckpointLeaf | null;
|
||||
batchMeta: {
|
||||
mainnetAttestation?: {
|
||||
hubTx?: string;
|
||||
mirrorTx?: string;
|
||||
activityRegistryV1Tx?: string;
|
||||
activityRegistryV2Tx?: string;
|
||||
participantSurfaceTx?: string;
|
||||
participantTopLevelTxs?: string[];
|
||||
};
|
||||
} | null;
|
||||
} | null {
|
||||
const dir = batchDir();
|
||||
if (!existsSync(dir)) return null;
|
||||
@@ -64,11 +87,25 @@ function loadLocalAttestation(txHash: string): {
|
||||
batchId?: string;
|
||||
batchTotalUsd?: string;
|
||||
leaves?: CheckpointLeaf[];
|
||||
mainnetAttestation?: {
|
||||
hubTx?: string;
|
||||
mirrorTx?: string;
|
||||
activityRegistryV1Tx?: string;
|
||||
activityRegistryV2Tx?: string;
|
||||
participantSurfaceTx?: string;
|
||||
participantTopLevelTxs?: string[];
|
||||
};
|
||||
};
|
||||
const leaf = findLeaf(raw, want);
|
||||
if (!leaf) continue;
|
||||
const batchId = String(raw.batchId ?? file.replace(/^batch-|\.json$/gi, ''));
|
||||
return { included: true, batchId, batchTotalUsd: raw.batchTotalUsd, leaf };
|
||||
return {
|
||||
included: true,
|
||||
batchId,
|
||||
batchTotalUsd: raw.batchTotalUsd,
|
||||
leaf,
|
||||
batchMeta: raw,
|
||||
};
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
@@ -76,6 +113,51 @@ function loadLocalAttestation(txHash: string): {
|
||||
return null;
|
||||
}
|
||||
|
||||
function linksFromBatchMeta(raw: {
|
||||
mainnetAttestation?: {
|
||||
hubTx?: string;
|
||||
mirrorTx?: string;
|
||||
activityRegistryV1Tx?: string;
|
||||
activityRegistryV2Tx?: string;
|
||||
participantSurfaceTx?: string;
|
||||
participantTopLevelTxs?: string[];
|
||||
};
|
||||
} | null): MainnetAttestationLink[] {
|
||||
const meta = raw?.mainnetAttestation;
|
||||
if (!meta) return [];
|
||||
const rows: Array<{ key: keyof NonNullable<typeof meta>; 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' },
|
||||
];
|
||||
const links: MainnetAttestationLink[] = [];
|
||||
for (const row of rows) {
|
||||
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: `https://etherscan.io/tx/${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: `https://etherscan.io/tx/${hash}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
return links;
|
||||
}
|
||||
|
||||
function leafResponse(leaf: CheckpointLeaf | null) {
|
||||
if (!leaf) return null;
|
||||
return {
|
||||
@@ -92,8 +174,54 @@ function leafResponse(leaf: CheckpointLeaf | null) {
|
||||
};
|
||||
}
|
||||
|
||||
function mergeLinks(...groups: MainnetAttestationLink[][]): MainnetAttestationLink[] {
|
||||
const out: MainnetAttestationLink[] = [];
|
||||
const seen = new Set<string>();
|
||||
for (const group of groups) {
|
||||
for (const link of group) {
|
||||
const key = `${link.layer}:${link.mainnetTxHash}`;
|
||||
if (seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
out.push(link);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
async function fetchIndexerAttestation(
|
||||
txHash: string,
|
||||
timeoutMs: number,
|
||||
): Promise<{
|
||||
txHash?: string;
|
||||
included?: boolean;
|
||||
batchId?: string;
|
||||
payload?: { leaves?: CheckpointLeaf[]; batchTotalUsd?: string };
|
||||
etherscanLinks?: MainnetAttestationLink[];
|
||||
etherscan?: { base?: string; txUrlTemplate?: string };
|
||||
} | null> {
|
||||
const url = `${indexerBase()}/v1/tx/${txHash}/attestation`;
|
||||
try {
|
||||
const ctrl = new AbortController();
|
||||
const timer = setTimeout(() => ctrl.abort(), timeoutMs);
|
||||
const upstream = await fetch(url, { signal: ctrl.signal });
|
||||
clearTimeout(timer);
|
||||
if (!upstream.ok) return null;
|
||||
return (await upstream.json()) as {
|
||||
txHash?: string;
|
||||
included?: boolean;
|
||||
batchId?: string;
|
||||
payload?: { leaves?: CheckpointLeaf[]; batchTotalUsd?: string };
|
||||
etherscanLinks?: MainnetAttestationLink[];
|
||||
etherscan?: { base?: string; txUrlTemplate?: string };
|
||||
};
|
||||
} catch (e) {
|
||||
logger.debug('checkpoint indexer proxy miss', { txHash, error: String(e) });
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /checkpoint/tx/:txHash/attestation — USD-enriched attestation (indexer proxy + local batch fallback).
|
||||
* GET /checkpoint/tx/:txHash/attestation — USD-enriched attestation (local batch first, indexer enrich).
|
||||
*/
|
||||
router.get('/checkpoint/tx/:txHash/attestation', async (req: Request, res: Response) => {
|
||||
const txHash = String(req.params.txHash || '').trim();
|
||||
@@ -101,34 +229,45 @@ router.get('/checkpoint/tx/:txHash/attestation', async (req: Request, res: Respo
|
||||
return res.status(400).json({ error: 'invalid tx hash' });
|
||||
}
|
||||
|
||||
const url = `${indexerBase()}/v1/tx/${txHash}/attestation`;
|
||||
try {
|
||||
const ctrl = new AbortController();
|
||||
const timer = setTimeout(() => ctrl.abort(), 12_000);
|
||||
const upstream = await fetch(url, { signal: ctrl.signal });
|
||||
clearTimeout(timer);
|
||||
if (upstream.ok) {
|
||||
const raw = (await upstream.json()) as {
|
||||
txHash?: string;
|
||||
included?: boolean;
|
||||
batchId?: string;
|
||||
payload?: { leaves?: CheckpointLeaf[]; batchTotalUsd?: string };
|
||||
};
|
||||
const leaf = findLeaf(raw.payload ?? null, txHash);
|
||||
return res.json({
|
||||
txHash: raw.txHash ?? txHash,
|
||||
included: Boolean(raw.included),
|
||||
batchId: raw.batchId ?? '0',
|
||||
batchTotalUsd: raw.payload?.batchTotalUsd,
|
||||
leaf: leafResponse(leaf),
|
||||
source: 'checkpoint-indexer',
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
logger.debug('checkpoint indexer proxy miss', { txHash, error: String(e) });
|
||||
}
|
||||
const etherscanMeta = {
|
||||
base: 'https://etherscan.io',
|
||||
txUrlTemplate: 'https://etherscan.io/tx/{txHash}',
|
||||
};
|
||||
|
||||
const local = loadLocalAttestation(txHash);
|
||||
const metaLinks = local ? linksFromBatchMeta(local.batchMeta) : [];
|
||||
|
||||
// Fast path: synced batch files on the explorer CT already carry mainnet tx hashes.
|
||||
if (local && metaLinks.length > 0) {
|
||||
return res.json({
|
||||
txHash,
|
||||
included: true,
|
||||
batchId: local.batchId,
|
||||
batchTotalUsd: local.batchTotalUsd,
|
||||
leaf: leafResponse(local.leaf),
|
||||
etherscanLinks: metaLinks,
|
||||
etherscan: etherscanMeta,
|
||||
source: 'checkpoint-batch-dir',
|
||||
});
|
||||
}
|
||||
|
||||
const indexerTimeoutMs = Number(process.env.CHECKPOINT_INDEXER_FETCH_TIMEOUT_MS || 2500);
|
||||
const remote = await fetchIndexerAttestation(txHash, indexerTimeoutMs);
|
||||
|
||||
if (remote?.included) {
|
||||
const leaf = findLeaf(remote.payload ?? null, txHash) ?? local?.leaf ?? null;
|
||||
return res.json({
|
||||
txHash: remote.txHash ?? txHash,
|
||||
included: true,
|
||||
batchId: remote.batchId ?? local?.batchId ?? '0',
|
||||
batchTotalUsd: remote.payload?.batchTotalUsd ?? local?.batchTotalUsd,
|
||||
leaf: leafResponse(leaf),
|
||||
etherscanLinks: mergeLinks(metaLinks, remote.etherscanLinks ?? []),
|
||||
etherscan: remote.etherscan ?? etherscanMeta,
|
||||
source: local ? 'checkpoint-batch-dir+indexer' : 'checkpoint-indexer',
|
||||
});
|
||||
}
|
||||
|
||||
if (local) {
|
||||
return res.json({
|
||||
txHash,
|
||||
@@ -136,6 +275,8 @@ router.get('/checkpoint/tx/:txHash/attestation', async (req: Request, res: Respo
|
||||
batchId: local.batchId,
|
||||
batchTotalUsd: local.batchTotalUsd,
|
||||
leaf: leafResponse(local.leaf),
|
||||
etherscanLinks: metaLinks,
|
||||
etherscan: etherscanMeta,
|
||||
source: 'checkpoint-batch-dir',
|
||||
});
|
||||
}
|
||||
@@ -145,6 +286,8 @@ router.get('/checkpoint/tx/:txHash/attestation', async (req: Request, res: Respo
|
||||
included: false,
|
||||
batchId: '0',
|
||||
leaf: null,
|
||||
etherscanLinks: [],
|
||||
etherscan: etherscanMeta,
|
||||
source: 'none',
|
||||
});
|
||||
});
|
||||
|
||||
@@ -123,6 +123,10 @@ export class ApiServer {
|
||||
this.app.use('/static', express.static(publicPath, staticOpts));
|
||||
this.app.use('/omnl/static', express.static(publicPath, staticOpts));
|
||||
this.app.use('/reserve/static', express.static(publicPath, staticOpts));
|
||||
this.app.use(
|
||||
'/chain138-attestation',
|
||||
express.static(path.join(publicPath, 'chain138-attestation'), staticOpts)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user