- 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
231 lines
7.9 KiB
TypeScript
231 lines
7.9 KiB
TypeScript
/**
|
|
* Bridge API: cross-chain bridge status and metrics.
|
|
* GET /api/v1/bridge/routes — CCIP WETH9/WETH10 + Trustless (Snap / dApps).
|
|
* GET /api/v1/bridge/status, /api/v1/bridge/metrics, /api/v1/bridge/preflight — GRU Transport readiness + cross-chain guidance.
|
|
*/
|
|
|
|
import { Router, Request, Response } from 'express';
|
|
import fs from 'fs';
|
|
import path from 'path';
|
|
import { fetchRemoteJson } from '../utils/fetch-remote-json';
|
|
import { buildDefaultBridgeRoutes } from '../utils/default-bridge-routes';
|
|
import { getActivePublicPools, getActiveTransportPairs, getGruTransportMetadata } from '../../config/gru-transport';
|
|
import { logger } from '../../utils/logger';
|
|
|
|
const router: Router = Router();
|
|
|
|
function buildGruTransportStatus() {
|
|
const metadata = getGruTransportMetadata();
|
|
const transportPairs = getActiveTransportPairs();
|
|
const publicPools = getActivePublicPools();
|
|
|
|
if (!metadata) return null;
|
|
|
|
return {
|
|
system: metadata.system,
|
|
terminology: metadata.terminology,
|
|
summary: metadata.counts,
|
|
gasAssetFamilies: metadata.gasAssetFamilies ?? [],
|
|
gasRedeemGroups: metadata.gasRedeemGroups ?? [],
|
|
gasProtocolExposure: metadata.gasProtocolExposure ?? [],
|
|
pairs: transportPairs.map((pair) => ({
|
|
key: pair.key,
|
|
canonicalChainId: pair.canonicalChainId,
|
|
destinationChainId: pair.destinationChainId,
|
|
canonicalSymbol: pair.canonicalSymbol,
|
|
mirroredSymbol: pair.mirroredSymbol,
|
|
assetClass: pair.assetClass ?? null,
|
|
familyKey: pair.familyKey ?? null,
|
|
laneGroup: pair.laneGroup ?? null,
|
|
backingMode: pair.backingMode ?? null,
|
|
redeemPolicy: pair.redeemPolicy ?? null,
|
|
wrappedNativeQuoteSymbol: pair.wrappedNativeQuoteSymbol ?? null,
|
|
stableQuoteSymbol: pair.stableQuoteSymbol ?? null,
|
|
referenceVenue: pair.referenceVenue ?? null,
|
|
eligible: pair.eligible === true,
|
|
runtimeReady: pair.runtimeReady === true,
|
|
runtimeBridgeReady: pair.runtimeBridgeReady === true,
|
|
runtimeReserveVerifierReady: pair.runtimeReserveVerifierReady === true,
|
|
runtimeMaxOutstandingReady: pair.runtimeMaxOutstandingReady === true,
|
|
runtimeSupplyAccountingReady: pair.runtimeSupplyAccountingReady ?? null,
|
|
supplyInvariantSatisfied: pair.supplyInvariantSatisfied ?? null,
|
|
runtimeOutstandingValue: pair.runtimeOutstandingValue ?? null,
|
|
runtimeEscrowedValue: pair.runtimeEscrowedValue ?? null,
|
|
runtimeTreasuryBackedValue: pair.runtimeTreasuryBackedValue ?? null,
|
|
runtimeTreasuryCapValue: pair.runtimeTreasuryCapValue ?? null,
|
|
bridgeAvailable: pair.bridgeAvailable ?? null,
|
|
protocolExposure: pair.protocolExposure ?? null,
|
|
eligibilityBlockers: Array.isArray(pair.eligibilityBlockers) ? pair.eligibilityBlockers : [],
|
|
runtimeMissingRequirements: Array.isArray(pair.runtimeMissingRequirements) ? pair.runtimeMissingRequirements : [],
|
|
activePublicPoolKeys: Array.isArray(pair.publicPoolKeys) ? pair.publicPoolKeys : [],
|
|
})),
|
|
publicPools,
|
|
};
|
|
}
|
|
|
|
function uniquePaths(paths: Array<string | undefined | null>): string[] {
|
|
const seen = new Set<string>();
|
|
const out: string[] = [];
|
|
|
|
for (const candidate of paths) {
|
|
if (typeof candidate !== 'string') continue;
|
|
const trimmed = candidate.trim();
|
|
if (!trimmed || seen.has(trimmed)) continue;
|
|
seen.add(trimmed);
|
|
out.push(trimmed);
|
|
}
|
|
|
|
return out;
|
|
}
|
|
|
|
function resolveBridgeRoutesPath(): string | null {
|
|
const candidates = uniquePaths([
|
|
process.env.BRIDGE_LIST_JSON_PATH,
|
|
process.env.BRIDGE_ROUTES_JSON_PATH,
|
|
path.resolve(process.cwd(), 'config/bridge-routes-chain138-default.json'),
|
|
path.resolve(process.cwd(), '../config/bridge-routes-chain138-default.json'),
|
|
path.resolve(process.cwd(), '../../config/bridge-routes-chain138-default.json'),
|
|
path.resolve(__dirname, '../../../../../config/bridge-routes-chain138-default.json'),
|
|
]);
|
|
|
|
for (const candidate of candidates) {
|
|
if (fs.existsSync(candidate)) return candidate;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function loadRuntimeBridgeRoutes(): { payload: Record<string, unknown>; lastModified?: string } | null {
|
|
const filePath = resolveBridgeRoutesPath();
|
|
if (!filePath) return null;
|
|
|
|
try {
|
|
const raw = fs.readFileSync(filePath, 'utf8');
|
|
const stat = fs.statSync(filePath);
|
|
return {
|
|
payload: JSON.parse(raw) as Record<string, unknown>,
|
|
lastModified: stat.mtime.toISOString(),
|
|
};
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* GET /api/v1/bridge/routes
|
|
* Optional BRIDGE_LIST_JSON_URL — remote JSON replaces entire payload (5m cache).
|
|
*/
|
|
router.get('/routes', async (_req: Request, res: Response) => {
|
|
const gruTransportMetadata = getGruTransportMetadata();
|
|
res.set('Cache-Control', 'public, max-age=0, must-revalidate');
|
|
const url = process.env.BRIDGE_LIST_JSON_URL?.trim();
|
|
if (url) {
|
|
try {
|
|
const data = await fetchRemoteJson(url);
|
|
const basePayload = data && typeof data === 'object' ? data : { data };
|
|
res.json({
|
|
source: 'remote-url',
|
|
...basePayload,
|
|
gruTransport: gruTransportMetadata
|
|
? {
|
|
system: gruTransportMetadata.system,
|
|
summary: gruTransportMetadata.counts,
|
|
activeTransportPairs: getActiveTransportPairs(),
|
|
activePublicPools: getActivePublicPools(),
|
|
}
|
|
: undefined,
|
|
});
|
|
return;
|
|
} catch (e) {
|
|
logger.error('BRIDGE_LIST_JSON_URL fetch failed, trying runtime file/built-in routes:', e);
|
|
}
|
|
}
|
|
|
|
const runtimePayload = loadRuntimeBridgeRoutes();
|
|
if (runtimePayload) {
|
|
res.json({
|
|
source: 'runtime-file',
|
|
lastModified: runtimePayload.lastModified,
|
|
...runtimePayload.payload,
|
|
gruTransport: gruTransportMetadata
|
|
? {
|
|
system: gruTransportMetadata.system,
|
|
summary: gruTransportMetadata.counts,
|
|
activeTransportPairs: getActiveTransportPairs(),
|
|
activePublicPools: getActivePublicPools(),
|
|
}
|
|
: undefined,
|
|
});
|
|
return;
|
|
}
|
|
|
|
res.json({
|
|
source: 'built-in',
|
|
...buildDefaultBridgeRoutes(),
|
|
});
|
|
});
|
|
|
|
router.get('/status', (_req: Request, res: Response) => {
|
|
const gruTransport = buildGruTransportStatus();
|
|
res.json({
|
|
ok: true,
|
|
bridges: [],
|
|
gruTransport,
|
|
message: 'Bridge status includes GRU Transport runtime readiness. Use /api/v1/bridge/preflight for missing refs and /api/v1/report/cross-chain for volume/lanes.',
|
|
});
|
|
});
|
|
|
|
router.get('/metrics', (_req: Request, res: Response) => {
|
|
const gruTransport = buildGruTransportStatus();
|
|
res.json({
|
|
ok: true,
|
|
lanes: [],
|
|
gruTransport: gruTransport
|
|
? {
|
|
system: gruTransport.system,
|
|
summary: gruTransport.summary,
|
|
}
|
|
: null,
|
|
message: 'Bridge metrics include GRU Transport summary counts. Use /api/v1/report/cross-chain for aggregated data.',
|
|
});
|
|
});
|
|
|
|
router.get('/preflight', (_req: Request, res: Response) => {
|
|
const gruTransport = buildGruTransportStatus();
|
|
if (!gruTransport) {
|
|
return res.status(503).json({
|
|
ok: false,
|
|
error: 'GRU transport config not available',
|
|
});
|
|
}
|
|
|
|
const blockedPairs = gruTransport.pairs.filter(
|
|
(pair) => pair.eligible !== true || pair.runtimeReady !== true
|
|
);
|
|
const readyPairs = gruTransport.pairs.filter(
|
|
(pair) => pair.eligible === true && pair.runtimeReady === true
|
|
);
|
|
|
|
return res.json({
|
|
ok: blockedPairs.length === 0,
|
|
generatedAt: new Date().toISOString(),
|
|
gruTransport: {
|
|
system: gruTransport.system,
|
|
summary: gruTransport.summary,
|
|
blockedPairs,
|
|
readyPairs: readyPairs.map((pair) => ({
|
|
key: pair.key,
|
|
canonicalSymbol: pair.canonicalSymbol,
|
|
mirroredSymbol: pair.mirroredSymbol,
|
|
destinationChainId: pair.destinationChainId,
|
|
assetClass: pair.assetClass ?? null,
|
|
familyKey: pair.familyKey ?? null,
|
|
backingMode: pair.backingMode ?? null,
|
|
redeemPolicy: pair.redeemPolicy ?? null,
|
|
supplyInvariantSatisfied: pair.supplyInvariantSatisfied ?? null,
|
|
})),
|
|
},
|
|
});
|
|
});
|
|
|
|
export default router;
|