feat(token-aggregation): reports, PMM quotes, config; Engine X flash vaults
- Expand token-aggregation API (report routes), canonical tokens, pools - Add flash vault contracts + tests (indexed, DODO cwUSDC, XAUT borrow) - PMM pools JSON, deploy/export scripts, metamask verified list Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -78,6 +78,22 @@ function uniquePaths(paths: Array<string | undefined | null>): string[] {
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Non-empty built-in CCIP / trustless lane counts for /bridge/metrics (telemetry-friendly). */
|
||||
function summarizeBuiltInBridgeLanes() {
|
||||
const payload = buildDefaultBridgeRoutes();
|
||||
const trustlessKeys = payload.routes.trustless ? Object.keys(payload.routes.trustless) : [];
|
||||
const chain138 = payload.chain138Bridges as Record<string, string | undefined>;
|
||||
return {
|
||||
weth9Destinations: Object.keys(payload.routes.weth9).length,
|
||||
weth10Destinations: Object.keys(payload.routes.weth10).length,
|
||||
trustlessDestinations: trustlessKeys.length,
|
||||
chain138ConfiguredBridges: Object.keys(chain138).filter((k) => {
|
||||
const v = chain138[k];
|
||||
return typeof v === 'string' && v.startsWith('0x');
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
function resolveBridgeRoutesPath(): string | null {
|
||||
const candidates = uniquePaths([
|
||||
process.env.BRIDGE_LIST_JSON_PATH,
|
||||
@@ -176,16 +192,35 @@ router.get('/status', (_req: Request, res: Response) => {
|
||||
|
||||
router.get('/metrics', (_req: Request, res: Response) => {
|
||||
const gruTransport = buildGruTransportStatus();
|
||||
const builtIn = summarizeBuiltInBridgeLanes();
|
||||
res.json({
|
||||
ok: true,
|
||||
lanes: [],
|
||||
lanes: [
|
||||
{
|
||||
kind: 'ccip-weth9',
|
||||
label: 'WETH9 bridge destinations (built-in catalog)',
|
||||
count: builtIn.weth9Destinations,
|
||||
},
|
||||
{
|
||||
kind: 'ccip-weth10',
|
||||
label: 'WETH10 bridge destinations (built-in catalog)',
|
||||
count: builtIn.weth10Destinations,
|
||||
},
|
||||
{
|
||||
kind: 'trustless',
|
||||
label: 'Trustless / Lockbox destinations (env-backed)',
|
||||
count: builtIn.trustlessDestinations,
|
||||
},
|
||||
],
|
||||
chain138Bridges: builtIn.chain138ConfiguredBridges,
|
||||
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.',
|
||||
message:
|
||||
'Lane counts reflect the built-in CCIP route catalog plus GRU Transport summary. Use /api/v1/bridge/routes for full JSON and /api/v1/report/cross-chain for volumes.',
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -139,4 +139,83 @@ describe('Config API runtime networks loader', () => {
|
||||
await new Promise<void>((resolve, reject) => remoteServer.close((err) => (err ? reject(err) : resolve())));
|
||||
}
|
||||
});
|
||||
|
||||
it('serves wallet-facing MetaMask aliases with absolute token images', async () => {
|
||||
const networksRes = await fetch(`${baseUrl}/api/v1/config/networks`);
|
||||
expect(networksRes.status).toBe(200);
|
||||
const networksBody = (await networksRes.json()) as Record<string, any>;
|
||||
expect(networksBody.networks).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
chainIdDecimal: 138,
|
||||
iconUrls: expect.arrayContaining([
|
||||
'https://explorer.d-bis.org/token-icons/chain-138.png',
|
||||
'https://explorer.d-bis.org/api/v1/report/logo/chain-138',
|
||||
]),
|
||||
}),
|
||||
])
|
||||
);
|
||||
|
||||
const metamaskRes = await fetch(`${baseUrl}/api/v1/config/metamask?chainId=138`);
|
||||
expect(metamaskRes.status).toBe(200);
|
||||
const metamaskBody = (await metamaskRes.json()) as Record<string, any>;
|
||||
expect(metamaskBody.addEthereumChain).toMatchObject({
|
||||
chainId: '0x8a',
|
||||
chainName: 'DeFi Oracle Meta Mainnet',
|
||||
});
|
||||
expect(metamaskBody.watchAssets).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
type: 'ERC20',
|
||||
options: expect.objectContaining({
|
||||
symbol: 'cUSDC',
|
||||
image: expect.stringMatching(/^https:\/\/127\.0\.0\.1:\d+\/api\/v1\/report\/logo\/cUSDC\?v=20260510$/),
|
||||
}),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
type: 'ERC20',
|
||||
options: expect.objectContaining({
|
||||
address: '0xb7721dD53A8c629d9f1Ba31a5819AFe250002b03',
|
||||
symbol: 'LINK',
|
||||
image: expect.stringMatching(/^https:\/\/127\.0\.0\.1:\d+\/api\/v1\/report\/logo\/LINK\?v=20260510$/),
|
||||
}),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
type: 'ERC20',
|
||||
options: expect.objectContaining({
|
||||
symbol: 'WETH',
|
||||
image: expect.stringMatching(/^https:\/\/127\.0\.0\.1:\d+\/api\/v1\/report\/logo\/ETH\?v=20260510$/),
|
||||
}),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
type: 'ERC20',
|
||||
options: expect.objectContaining({
|
||||
symbol: 'cWEMIX',
|
||||
image: expect.stringMatching(/^https:\/\/127\.0\.0\.1:\d+\/api\/v1\/report\/logo\/WEMIX\?v=20260510$/),
|
||||
}),
|
||||
}),
|
||||
])
|
||||
);
|
||||
expect(metamaskBody.watchAssets).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
type: 'ERC20',
|
||||
options: expect.objectContaining({
|
||||
address: '0x219522c60e83dEe01FC5b0329d6fA8fD84b9D13d',
|
||||
symbol: 'cUSDC',
|
||||
}),
|
||||
metadata: expect.objectContaining({
|
||||
catalogSymbol: 'cUSDC_V2',
|
||||
familySymbol: 'cUSDC',
|
||||
deploymentVersion: 'v2',
|
||||
}),
|
||||
}),
|
||||
])
|
||||
);
|
||||
expect(
|
||||
metamaskBody.watchAssets.some(
|
||||
(entry: Record<string, any>) => entry.options?.symbol === 'cUSDC_V2' || entry.options?.symbol === 'cUSDT_V2'
|
||||
)
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,12 +1,75 @@
|
||||
import { Router, Request, Response } from 'express';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { getNetworks, getConfigByChain, API_VERSION } from '../../config/networks';
|
||||
import { getNetworks, getConfigByChain, API_VERSION, type NetworkEntry } from '../../config/networks';
|
||||
import { getCanonicalTokensByChain, getLogoUriForSpec, getTokenRegistryFamily } from '../../config/canonical-tokens';
|
||||
import { cacheMiddleware } from '../middleware/cache';
|
||||
import { fetchRemoteJson } from '../utils/fetch-remote-json';
|
||||
import { logger } from '../../utils/logger';
|
||||
|
||||
const router: Router = Router();
|
||||
const DEFAULT_PUBLIC_BASE_URL = 'https://explorer.d-bis.org';
|
||||
const DEFAULT_WALLET_METADATA_VERSION = '20260510';
|
||||
|
||||
function resolvePublicBaseUrl(req: Request): string {
|
||||
const configured = (
|
||||
process.env.TOKEN_AGGREGATION_PUBLIC_BASE_URL ??
|
||||
process.env.PUBLIC_API_BASE_URL ??
|
||||
process.env.PUBLIC_BASE_URL ??
|
||||
''
|
||||
).trim();
|
||||
if (configured) return configured.replace(/\/+$/, '');
|
||||
|
||||
const host = String(req.get('x-forwarded-host') || req.get('host') || '').split(',')[0].trim();
|
||||
if (host) {
|
||||
let proto = String(req.get('x-forwarded-proto') || 'https').split(',')[0].trim() || 'https';
|
||||
if (proto === 'http' && !/^(localhost|127\.0\.0\.1|\[::1\])(?::\d+)?$/i.test(host)) {
|
||||
proto = 'https';
|
||||
}
|
||||
return `${proto}://${host}`.replace(/\/+$/, '');
|
||||
}
|
||||
|
||||
return DEFAULT_PUBLIC_BASE_URL;
|
||||
}
|
||||
|
||||
function absolutePublicUrl(req: Request, value: string | undefined): string | undefined {
|
||||
if (!value) return undefined;
|
||||
if (/^https?:\/\//i.test(value)) return value;
|
||||
if (!value.startsWith('/')) return value;
|
||||
return `${resolvePublicBaseUrl(req)}${value}`;
|
||||
}
|
||||
|
||||
function appendWalletMetadataVersion(value: string | undefined): string | undefined {
|
||||
if (!value) return undefined;
|
||||
const version = (process.env.WALLET_METADATA_IMAGE_VERSION || DEFAULT_WALLET_METADATA_VERSION).trim();
|
||||
if (!version) return value;
|
||||
const separator = value.includes('?') ? '&' : '?';
|
||||
return `${value}${separator}v=${encodeURIComponent(version)}`;
|
||||
}
|
||||
|
||||
function localLogoPathForSymbol(symbol: string, originalLogoUri: string): string {
|
||||
if (originalLogoUri.includes('/token-lists/logos/gru/')) {
|
||||
const fileName = originalLogoUri.split('/').pop()?.replace(/\.svg$/i, '');
|
||||
if (fileName) return `/api/v1/report/logo/${fileName}`;
|
||||
}
|
||||
if (originalLogoUri.includes('/blockchains/bitcoin/info/logo.png')) return '/api/v1/report/logo/cWBTC';
|
||||
if (originalLogoUri.includes('/ipfs/')) {
|
||||
const cid = originalLogoUri.split('/').pop();
|
||||
if (cid) return `/api/v1/report/logo/ipfs-${cid}`;
|
||||
}
|
||||
if (symbol === 'cWUSDC') return '/api/v1/report/logo/cUSDC';
|
||||
return originalLogoUri;
|
||||
}
|
||||
|
||||
function resolveWalletWatchAssetSymbol(spec: { symbol: string; familySymbol?: string; deploymentVersion?: string }): string {
|
||||
// MetaMask validates wallet_watchAsset.symbol against ERC-20 symbol().
|
||||
// Staged V2 Chain 138 deployments currently keep the family symbol on-chain
|
||||
// (for example cUSDC), while the catalog symbol distinguishes the row
|
||||
// (for example cUSDC_V2). Keep the catalog identity in metadata and send the
|
||||
// contract-facing symbol in options.symbol so EIP-747 succeeds.
|
||||
if (spec.deploymentVersion && spec.familySymbol) return spec.familySymbol;
|
||||
return spec.symbol;
|
||||
}
|
||||
|
||||
type RuntimeNetworksPayload = {
|
||||
version?: string | { major?: number; minor?: number; patch?: number };
|
||||
@@ -124,16 +187,77 @@ async function resolveNetworksPayload(): Promise<NetworksPayload> {
|
||||
};
|
||||
}
|
||||
|
||||
async function sendNetworks(_req: Request, res: Response): Promise<void> {
|
||||
res.set('Cache-Control', 'public, max-age=0, must-revalidate');
|
||||
const payload = await resolveNetworksPayload();
|
||||
res.json(payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/v1/networks
|
||||
* Full EIP-3085 chain params for wallet_addEthereumChain (Chain 138, 1, 651940).
|
||||
* If NETWORKS_JSON_URL is set (e.g. GitHub raw URL), fetches and returns that JSON; otherwise uses built-in networks.
|
||||
*/
|
||||
router.get('/networks', cacheMiddleware(5 * 60 * 1000), async (req: Request, res: Response) => {
|
||||
router.get(['/networks', '/config/networks', '/metamask/networks'], cacheMiddleware(5 * 60 * 1000), async (req: Request, res: Response) => {
|
||||
try {
|
||||
await sendNetworks(req, res);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/v1/config/metamask
|
||||
* Wallet-facing aliases for Chain 138 add-chain params and watchAsset token entries.
|
||||
*/
|
||||
router.get(['/config/metamask', '/metamask'], cacheMiddleware(5 * 60 * 1000), async (req: Request, res: Response) => {
|
||||
try {
|
||||
res.set('Cache-Control', 'public, max-age=0, must-revalidate');
|
||||
const chainId = parseInt(String(req.query.chainId ?? '138'), 10) || 138;
|
||||
const payload = await resolveNetworksPayload();
|
||||
res.json(payload);
|
||||
const networks = payload.networks as NetworkEntry[];
|
||||
const network = networks.find((entry) => Number(entry.chainIdDecimal) === chainId);
|
||||
if (!network) {
|
||||
res.status(404).json({ error: 'Chain not found', chainId });
|
||||
return;
|
||||
}
|
||||
|
||||
const watchAssets = getCanonicalTokensByChain(chainId)
|
||||
.map((spec) => {
|
||||
const address = spec.addresses[chainId];
|
||||
if (!address) return null;
|
||||
const originalLogoURI = getLogoUriForSpec(spec);
|
||||
return {
|
||||
type: 'ERC20',
|
||||
options: {
|
||||
address,
|
||||
symbol: resolveWalletWatchAssetSymbol(spec),
|
||||
decimals: spec.decimals,
|
||||
image: appendWalletMetadataVersion(absolutePublicUrl(req, localLogoPathForSymbol(spec.symbol, originalLogoURI))),
|
||||
},
|
||||
metadata: {
|
||||
name: spec.name,
|
||||
catalogSymbol: spec.symbol,
|
||||
registryFamily: getTokenRegistryFamily(spec),
|
||||
familySymbol: spec.familySymbol,
|
||||
deploymentVersion: spec.deploymentVersion,
|
||||
deploymentStatus: spec.deploymentStatus,
|
||||
},
|
||||
};
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
res.json({
|
||||
source: payload.source,
|
||||
version: payload.version,
|
||||
chainId,
|
||||
addEthereumChain: network,
|
||||
watchAssets,
|
||||
caveats: [
|
||||
'MetaMask custom-token prices are controlled by MetaMask and its upstream asset/price providers; this endpoint supplies wallet metadata, logo URLs, and token-add payloads but cannot force MetaMask to render fiat prices.',
|
||||
'After metadata changes, remove and re-add the custom network/token in MetaMask or use the companion UI to call wallet_addEthereumChain and wallet_watchAsset again.',
|
||||
],
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
|
||||
@@ -91,6 +91,24 @@ describe('Report API', () => {
|
||||
const body = (await res.json()) as Record<string, unknown>;
|
||||
expect(body.chainId).toBe(651940);
|
||||
});
|
||||
|
||||
it('enriches Mainnet cWUSDC with supply proof fields for CMC reports', async () => {
|
||||
const res = await fetch(`${baseUrl}/api/v1/report/cmc?chainId=1`);
|
||||
expect(res.status).toBe(200);
|
||||
const body = (await res.json()) as Record<string, any>;
|
||||
const cwusdc = body.tokens.find((token: Record<string, any>) => token.symbol === 'cWUSDC');
|
||||
expect(cwusdc).toMatchObject({
|
||||
contract_address: '0x2de5f116bfce3d0f922d9c8351e0c5fc24b9284a',
|
||||
total_supply: 10451316981.309788,
|
||||
total_supply_raw: '10451316981309788',
|
||||
circulating_supply: 10451316981.309788,
|
||||
market_cap: 10451316981.309788,
|
||||
supply_proof_provenance: expect.objectContaining({
|
||||
status: 'ready_for_tracker_review',
|
||||
}),
|
||||
});
|
||||
expect(cwusdc.tracker_caveats).toEqual(expect.arrayContaining([expect.stringContaining('on-chain supply proof')]));
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /api/v1/report/coingecko', () => {
|
||||
@@ -104,6 +122,62 @@ describe('Report API', () => {
|
||||
expect(body).toHaveProperty('tokens');
|
||||
expect(Array.isArray(body.tokens)).toBe(true);
|
||||
});
|
||||
|
||||
it('enriches Mainnet cWUSDC with supply proof, circulating supply, and market cap', async () => {
|
||||
const res = await fetch(`${baseUrl}/api/v1/report/coingecko?chainId=1`);
|
||||
expect(res.status).toBe(200);
|
||||
const body = (await res.json()) as Record<string, any>;
|
||||
const cwusdc = body.tokens.find((token: Record<string, any>) => token.symbol === 'cWUSDC');
|
||||
expect(cwusdc).toMatchObject({
|
||||
contract_address: '0x2de5f116bfce3d0f922d9c8351e0c5fc24b9284a',
|
||||
total_supply: 10451316981.309788,
|
||||
total_supply_raw: '10451316981309788',
|
||||
circulating_supply: 10451316981.309788,
|
||||
circulating_supply_formula: 'circulatingSupply = totalSupply - protocolControlledNonCirculatingBalances',
|
||||
supply_proof_provenance: expect.objectContaining({
|
||||
schema: 'mainnet-cwusdc-supply-proof/v1',
|
||||
referenceBlock: 25047586,
|
||||
}),
|
||||
market_data: expect.objectContaining({
|
||||
current_price: { usd: 1 },
|
||||
market_cap: 10451316981.309788,
|
||||
}),
|
||||
});
|
||||
expect(cwusdc.tracker_caveats).toEqual(expect.arrayContaining([expect.stringContaining('No public tracker')]));
|
||||
});
|
||||
|
||||
it('surfaces GRU v2 deployment-status pools in tracker-facing token reports', async () => {
|
||||
const res = await fetch(`${baseUrl}/api/v1/report/coingecko?chainId=1`);
|
||||
expect(res.status).toBe(200);
|
||||
const body = (await res.json()) as Record<string, any>;
|
||||
const cwusdc = body.tokens.find((token: Record<string, any>) => token.symbol === 'cWUSDC');
|
||||
expect(cwusdc.liquidity_pools).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
pool_address: '0x69776fc607e9eda8042e320e7e43f54d06c68f0e',
|
||||
source: 'gru-v2-deployment-status',
|
||||
status: 'live',
|
||||
role: 'defense',
|
||||
}),
|
||||
])
|
||||
);
|
||||
});
|
||||
|
||||
it('surfaces explicit supply-proof gaps for Mainnet GRU assets without proof artifacts', async () => {
|
||||
const res = await fetch(`${baseUrl}/api/v1/report/coingecko?chainId=1`);
|
||||
expect(res.status).toBe(200);
|
||||
const body = (await res.json()) as Record<string, any>;
|
||||
const cwusdt = body.tokens.find((token: Record<string, any>) => token.symbol === 'cWUSDT');
|
||||
expect(cwusdt).toMatchObject({
|
||||
contract_address: '0xaf5017d0163ecb99d9b5d94e3b4d7b09af44d8ae',
|
||||
supply_proof_provenance: {
|
||||
source: 'missing-supply-proof',
|
||||
status: 'proof_required',
|
||||
},
|
||||
});
|
||||
expect(cwusdt).not.toHaveProperty('total_supply');
|
||||
expect(cwusdt.tracker_caveats).toEqual(expect.arrayContaining([expect.stringContaining('tracker-grade supply proof')]));
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /api/v1/report/all', () => {
|
||||
@@ -149,6 +223,72 @@ describe('Report API', () => {
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
it('includes Mainnet cWUSDC supply proof enrichment in unified reports', async () => {
|
||||
const res = await fetch(`${baseUrl}/api/v1/report/all?chainId=1`);
|
||||
expect(res.status).toBe(200);
|
||||
const body = (await res.json()) as Record<string, any>;
|
||||
const cwusdc = body.tokens?.['1']?.find((token: Record<string, any>) => token.symbol === 'cWUSDC');
|
||||
expect(cwusdc).toMatchObject({
|
||||
totalSupply: '10451316981.309788',
|
||||
totalSupplyRaw: '10451316981309788',
|
||||
circulatingSupply: '10451316981.309788',
|
||||
circulatingSupplyFormula: 'circulatingSupply = totalSupply - protocolControlledNonCirculatingBalances',
|
||||
market: expect.objectContaining({
|
||||
priceUsd: 1,
|
||||
marketCapUsd: 10451316981.309788,
|
||||
}),
|
||||
supplyProofProvenance: expect.objectContaining({
|
||||
schema: 'mainnet-cwusdc-supply-proof/v1',
|
||||
referenceBlock: 25047586,
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
it('distinguishes proof-gated Mainnet cW assets from deterministic placeholder bindings', async () => {
|
||||
const res = await fetch(`${baseUrl}/api/v1/report/all?chainId=1`);
|
||||
expect(res.status).toBe(200);
|
||||
const body = (await res.json()) as Record<string, any>;
|
||||
const proofGated = body.tokens?.['1']?.find((entry: Record<string, any>) => entry.symbol === 'cWUSDT');
|
||||
expect(proofGated).toMatchObject({
|
||||
supplyProofProvenance: {
|
||||
source: 'missing-supply-proof',
|
||||
status: 'proof_required',
|
||||
},
|
||||
});
|
||||
expect(proofGated.totalSupply).toBeUndefined();
|
||||
expect(proofGated.trackerCaveats).toEqual(expect.arrayContaining([expect.stringContaining('proof artifact')]));
|
||||
|
||||
const placeholderSymbols = ['cWBTC', 'cWETH'];
|
||||
for (const symbol of placeholderSymbols) {
|
||||
const token = body.tokens?.['1']?.find((entry: Record<string, any>) => entry.symbol === symbol);
|
||||
expect(token).toMatchObject({
|
||||
supplyProofProvenance: {
|
||||
source: 'deterministic-placeholder-address',
|
||||
status: 'non_reportable_until_erc20_deployed',
|
||||
},
|
||||
});
|
||||
expect(token.totalSupply).toBeUndefined();
|
||||
expect(token.trackerCaveats).toEqual(expect.arrayContaining([expect.stringContaining('deterministic placeholder')]));
|
||||
}
|
||||
});
|
||||
|
||||
it('marks proofless base GRU c assets as proof gated instead of leaving silent supply fields', async () => {
|
||||
const res = await fetch(`${baseUrl}/api/v1/report/all?chainId=138`);
|
||||
expect(res.status).toBe(200);
|
||||
const body = (await res.json()) as Record<string, any>;
|
||||
const cusdc = body.tokens?.['138']?.find((entry: Record<string, any>) => entry.symbol === 'cUSDC');
|
||||
expect(cusdc).toMatchObject({
|
||||
type: 'base',
|
||||
registryFamily: 'iso4217',
|
||||
supplyProofProvenance: {
|
||||
source: 'missing-supply-proof',
|
||||
status: 'proof_required',
|
||||
},
|
||||
});
|
||||
expect(cusdc.totalSupply).toBeUndefined();
|
||||
expect(cusdc.trackerCaveats).toEqual(expect.arrayContaining([expect.stringContaining('tracker-grade supply proof')]));
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /api/v1/report/gas-registry', () => {
|
||||
@@ -185,6 +325,63 @@ describe('Report API', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /api/v1/report/adoption-readiness', () => {
|
||||
it('summarizes proved, proof-gated, pool-indexed, and scoring gates', async () => {
|
||||
const res = await fetch(`${baseUrl}/api/v1/report/adoption-readiness`);
|
||||
expect(res.status).toBe(200);
|
||||
const body = (await res.json()) as Record<string, any>;
|
||||
expect(body.scope).toBe('gru-c-and-cw-assets');
|
||||
expect(body.counts).toMatchObject({
|
||||
candidates: expect.any(Number),
|
||||
reportableCandidates: expect.any(Number),
|
||||
nonReportablePlaceholder: expect.any(Number),
|
||||
proved: expect.any(Number),
|
||||
proofRequired: expect.any(Number),
|
||||
silent: 0,
|
||||
liquidityMissing: expect.any(Number),
|
||||
liquidityMissingWithPools: expect.any(Number),
|
||||
liquidityMissingWithoutPools: expect.any(Number),
|
||||
gruV2PoolsWithStatus: expect.any(Number),
|
||||
});
|
||||
expect(body.institutional.score).toEqual(expect.any(Number));
|
||||
expect(body.cryptoListing.score).toEqual(expect.any(Number));
|
||||
expect(Array.isArray(body.blockerInventory.proofRequiredByChain)).toBe(true);
|
||||
expect(Array.isArray(body.blockerInventory.liquidityMissingByChain)).toBe(true);
|
||||
expect(Array.isArray(body.blockerInventory.liquidityMissingWithPoolsByChain)).toBe(true);
|
||||
expect(Array.isArray(body.blockerInventory.liquidityMissingWithoutPoolsByChain)).toBe(true);
|
||||
expect(Array.isArray(body.blockerInventory.liquidityMissingDetails)).toBe(true);
|
||||
expect(Array.isArray(body.blockerInventory.externalOfficialQuoteLiquidityByChain)).toBe(true);
|
||||
expect(Array.isArray(body.blockerInventory.nonReportablePlaceholderByChain)).toBe(true);
|
||||
expect(Array.isArray(body.blockerInventory.gruV2PoolsMissingStatus)).toBe(true);
|
||||
expect(Array.isArray(body.blockerInventory.notes)).toBe(true);
|
||||
});
|
||||
|
||||
it('does not treat external official USDC/USDT mirrors as GRU liquidity blockers', async () => {
|
||||
const res = await fetch(`${baseUrl}/api/v1/report/adoption-readiness`);
|
||||
expect(res.status).toBe(200);
|
||||
const body = (await res.json()) as Record<string, any>;
|
||||
expect(body.counts.externalOfficialQuoteLiquidity).toBeGreaterThan(0);
|
||||
expect(body.blockerInventory.externalOfficialQuoteLiquidityByChain).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
chainId: 1,
|
||||
symbols: expect.arrayContaining(['cUSDC', 'cUSDT']),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
chainId: 56,
|
||||
symbols: expect.arrayContaining(['cUSDC', 'cUSDT']),
|
||||
}),
|
||||
])
|
||||
);
|
||||
expect(body.blockerInventory.liquidityMissingDetails).not.toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ chainId: 1, symbol: 'cUSDT' }),
|
||||
expect.objectContaining({ chainId: 56, symbol: 'cUSDC' }),
|
||||
])
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /api/v1/report/token-list', () => {
|
||||
it('surfaces both V1 and V2 Chain 138 canonical GRU deployments explicitly', async () => {
|
||||
const res = await fetch(`${baseUrl}/api/v1/report/token-list?chainId=138`);
|
||||
@@ -379,6 +576,17 @@ describe('Report API', () => {
|
||||
])
|
||||
);
|
||||
});
|
||||
|
||||
it('uses packaged DBIS-level local logo assets while preserving original logo references', async () => {
|
||||
const res = await fetch(`${baseUrl}/api/v1/report/token-list?chainId=1`);
|
||||
expect(res.status).toBe(200);
|
||||
const body = (await res.json()) as Record<string, any>;
|
||||
const cwusdc = body.tokens.find((token: Record<string, any>) => token.symbol === 'cWUSDC');
|
||||
expect(cwusdc).toMatchObject({
|
||||
logoURI: expect.stringMatching(/^https:\/\/127\.0\.0\.1:\d+\/api\/v1\/report\/logo\/cUSDC$/),
|
||||
originalLogoURI: expect.stringContaining('/token-lists/logos/gru/cUSDC.svg'),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /api/v1/report/cw-registry', () => {
|
||||
@@ -488,6 +696,8 @@ describe('Report API', () => {
|
||||
expect((body.pools as Array<{ poolAddress: string }>)[0]).toMatchObject({
|
||||
poolAddress: '0x1111111111111111111111111111111111111111',
|
||||
section: 'pmmPools',
|
||||
status: 'routing_enabled',
|
||||
statusReason: expect.stringContaining('public routing is enabled'),
|
||||
});
|
||||
} finally {
|
||||
await import('fs/promises').then((fs) => fs.unlink(tempPath).catch(() => undefined));
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,5 @@
|
||||
import express, { Express, Request, Response, NextFunction } from 'express';
|
||||
import { Server } from 'http';
|
||||
import path from 'path';
|
||||
import { readFileSync, existsSync } from 'fs';
|
||||
import cors from 'cors';
|
||||
@@ -48,6 +49,7 @@ export class ApiServer {
|
||||
private indexerEnabled: boolean;
|
||||
private indexer: MultiChainIndexer | null;
|
||||
private omnlPoller: OmnlEventPoller | null;
|
||||
private server: Server | null;
|
||||
|
||||
private resolveTrustProxySetting(): boolean | number | string {
|
||||
const raw = (process.env.EXPRESS_TRUST_PROXY ?? process.env.TRUST_PROXY ?? '1').trim();
|
||||
@@ -65,6 +67,7 @@ export class ApiServer {
|
||||
this.indexerEnabled = this.resolveFeatureFlag('ENABLE_INDEXER', true);
|
||||
this.indexer = this.indexerEnabled ? new MultiChainIndexer() : null;
|
||||
this.omnlPoller = this.resolveFeatureFlag('ENABLE_OMNL_EVENT_POLLER', false) ? new OmnlEventPoller() : null;
|
||||
this.server = null;
|
||||
|
||||
this.setupMiddleware();
|
||||
this.setupRoutes();
|
||||
@@ -106,6 +109,14 @@ export class ApiServer {
|
||||
});
|
||||
next();
|
||||
});
|
||||
|
||||
const publicPath = path.join(__dirname, '../../public');
|
||||
if (existsSync(publicPath)) {
|
||||
this.app.use('/static', express.static(publicPath, {
|
||||
immutable: true,
|
||||
maxAge: '1d',
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
private setupRoutes(): void {
|
||||
@@ -151,6 +162,39 @@ export class ApiServer {
|
||||
res.type('html').send(readFileSync(dashboardPath, 'utf8'));
|
||||
});
|
||||
|
||||
// Public API catalog (register before routers so GET /api/v1 is not swallowed by middleware-only mounts)
|
||||
const sendApiV1Catalog = (_req: Request, res: Response) => {
|
||||
res.json({
|
||||
service: 'token-aggregation',
|
||||
version: '1.0.0',
|
||||
note: 'Prefix paths with your public API origin (e.g. https://explorer.d-bis.org).',
|
||||
paths: {
|
||||
catalog: '/api/v1',
|
||||
health: '/health',
|
||||
chains: '/api/v1/chains',
|
||||
networks: '/api/v1/networks',
|
||||
config: '/api/v1/config',
|
||||
tokens: '/api/v1/tokens',
|
||||
tokenDetail: '/api/v1/tokens/{address}',
|
||||
quote: '/api/v1/quote',
|
||||
bridgeRoutes: '/api/v1/bridge/routes',
|
||||
bridgeStatus: '/api/v1/bridge/status',
|
||||
bridgeMetrics: '/api/v1/bridge/metrics',
|
||||
bridgePreflight: '/api/v1/bridge/preflight',
|
||||
tokenMappingPairs: '/api/v1/token-mapping/pairs',
|
||||
tokenMappingResolve: '/api/v1/token-mapping/resolve',
|
||||
reportTokenList: '/api/v1/report/token-list',
|
||||
routesTree: '/api/v1/routes/tree',
|
||||
plannerProvidersCapabilities: '/api/v2/providers/capabilities',
|
||||
plannerRoutesPlan: '/api/v2/routes/plan',
|
||||
plannerIntentsPlan: '/api/v2/intents/plan',
|
||||
plannerInternalExecutionPlan: '/api/v2/routes/internal-execution-plan',
|
||||
},
|
||||
});
|
||||
};
|
||||
this.app.get('/api/v1', sendApiV1Catalog);
|
||||
this.app.get('/api/v1/', sendApiV1Catalog);
|
||||
|
||||
// API routes
|
||||
this.app.use('/api/v1', tokenRoutes);
|
||||
this.app.use('/api/v1', configRoutes);
|
||||
@@ -206,21 +250,25 @@ export class ApiServer {
|
||||
|
||||
async start(): Promise<void> {
|
||||
try {
|
||||
// Start server
|
||||
this.server = this.app.listen(this.port, () => {
|
||||
logger.info(`Token Aggregation Service listening on port ${this.port}`);
|
||||
logger.info(`Health check: http://localhost:${this.port}/health`);
|
||||
logger.info(`API: http://localhost:${this.port}/api/v1`);
|
||||
});
|
||||
|
||||
if (this.indexer) {
|
||||
await this.indexer.initialize();
|
||||
await this.indexer.startAll();
|
||||
this.indexer
|
||||
.initialize()
|
||||
.then(() => this.indexer?.startAll())
|
||||
.catch((error) => {
|
||||
logger.error('Token aggregation indexer failed after API startup:', error);
|
||||
});
|
||||
} else {
|
||||
logger.info('Token aggregation indexer disabled by ENABLE_INDEXER flag');
|
||||
}
|
||||
|
||||
this.omnlPoller?.start();
|
||||
|
||||
// Start server
|
||||
this.app.listen(this.port, () => {
|
||||
logger.info(`Token Aggregation Service listening on port ${this.port}`);
|
||||
logger.info(`Health check: http://localhost:${this.port}/health`);
|
||||
logger.info(`API: http://localhost:${this.port}/api/v1`);
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Failed to start server:', error);
|
||||
process.exit(1);
|
||||
@@ -230,6 +278,18 @@ export class ApiServer {
|
||||
async stop(): Promise<void> {
|
||||
this.omnlPoller?.stop();
|
||||
this.indexer?.stopAll();
|
||||
if (this.server) {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
this.server?.close((error) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
this.server = null;
|
||||
}
|
||||
logger.info('Server stopped');
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user