feat(chain138): Monad CCIP, token aggregation OMNL gates, HYBX client, and PMM deploy updates.
Some checks failed
CI/CD Pipeline / Solidity Contracts (push) Failing after 1m11s
CI/CD Pipeline / Security Scanning (push) Has been cancelled
CI/CD Pipeline / Lint and Format (push) Has been cancelled
CI/CD Pipeline / Terraform Validation (push) Has been cancelled
CI/CD Pipeline / Kubernetes Validation (push) Has been cancelled
Validation / validate-genesis (push) Has been cancelled
Validation / validate-terraform (push) Has been cancelled
Validation / validate-kubernetes (push) Has been cancelled
Validation / validate-smart-contracts (push) Has been cancelled
Validation / validate-security (push) Has been cancelled
Validation / validate-documentation (push) Has been cancelled
Deploy ChainID 138 / Deploy ChainID 138 (push) Failing after 1m4s
HYBX OMNL TypeScript & anchor / token-aggregation build + reconcile artifact (push) Failing after 31s
OMNL reconcile anchor / Run omnl:reconcile and upload artifacts (push) Failing after 29s
Verify Deployment / Verify Deployment (push) Failing after 57s

Relay router, reserve system, oracle publisher, token-aggregation compliance middleware, and Monad deployment scripts.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
defiQUG
2026-06-18 00:11:33 -07:00
parent 1ec308c3a0
commit 11c97777d4
111 changed files with 5237 additions and 432 deletions

View File

@@ -19,13 +19,33 @@ function looksLikeGenericErrorPayload(body: unknown): boolean {
return true;
}
function buildCacheKey(req: Request): string {
const base = `${req.method}:${req.originalUrl}`;
if (req.method !== 'POST' && req.method !== 'PUT' && req.method !== 'PATCH') {
return base;
}
try {
const body = req.body;
if (body == null) {
return base;
}
const serialized = typeof body === 'string' ? body : JSON.stringify(body);
if (!serialized || serialized === '{}' || serialized === '[]') {
return base;
}
return `${base}:${serialized}`;
} catch {
return base;
}
}
export function cacheMiddleware(ttl: number = DEFAULT_TTL) {
return (req: Request, res: Response, next: NextFunction) => {
const bypassCache =
req.query.refresh === '1' ||
req.query.noCache === '1' ||
/\bno-cache\b|\bno-store\b/i.test(req.header('cache-control') || '');
const key = `${req.method}:${req.originalUrl}`;
const key = buildCacheKey(req);
const cached = bypassCache ? undefined : cache.get(key);
if (cached && cached.expiresAt > Date.now()) {

View File

@@ -8,6 +8,9 @@ export function omnlAuditMiddleware(req: Request, res: Response, next: NextFunct
correlationId: String(req.headers[CORRELATION_ID_HEADER] ?? req.headers['x-omnl-trace-id'] ?? ''),
requestId: String(req.headers[REQUEST_ID_HEADER] ?? ''),
});
req.headers[CORRELATION_ID_HEADER] = ctx.correlationId;
req.headers['x-omnl-trace-id'] = ctx.correlationId;
req.headers[REQUEST_ID_HEADER] = ctx.requestId;
const traceId = ctx.correlationId;
res.setHeader('X-OMNL-Trace-Id', traceId);
res.setHeader(CORRELATION_ID_HEADER, ctx.correlationId);

View File

@@ -1,7 +1,9 @@
import type { Request, Response, NextFunction } from 'express';
import { MockComplianceDecisionEngine } from '@dbis/integration-foundation';
import path from 'node:path';
import { createComplianceDecisionEngine } from '../../compliance/volume13-engine.js';
const engine = new MockComplianceDecisionEngine();
const PROXMOX_ROOT = process.env.PROXMOX_ROOT || path.resolve(process.cwd(), '../../../..');
const engine = createComplianceDecisionEngine(PROXMOX_ROOT);
const FINANCIAL_POST_PATHS = [
'/omnl/iso20022/messages',

View File

@@ -245,3 +245,38 @@ describe('Bridge API GRU Transport status', () => {
}
});
});
describe('Bridge API relay lane messages', () => {
let server: ReturnType<typeof createServer>;
let baseUrl: string;
beforeAll(async () => {
const started = await startServer(createApp());
server = started.server;
baseUrl = started.baseUrl;
});
afterAll(async () => {
await new Promise<void>((resolve, reject) => {
server.close((err) => (err ? reject(err) : resolve()));
});
});
it('returns 138-monad relay messages with seed canaries when skipRpc=1', async () => {
const res = await fetch(`${baseUrl}/api/v1/bridge/messages?lane=138-monad&skipRpc=1`);
expect(res.status).toBe(200);
const body = (await res.json()) as Record<string, unknown>;
expect(body.ok).toBe(true);
expect(body.laneId).toBe('138-monad-relay');
expect(body.integration).toMatchObject({ ccipExplorer: false, creRequired: false });
const messages = body.messages as Array<{ messageId: string; status: string }>;
expect(Array.isArray(messages)).toBe(true);
expect(messages.length).toBeGreaterThanOrEqual(2);
expect(messages.some((m) => m.status === 'delivered')).toBe(true);
});
it('rejects unknown lane', async () => {
const res = await fetch(`${baseUrl}/api/v1/bridge/messages?lane=unknown`);
expect(res.status).toBe(400);
});
});

View File

@@ -10,6 +10,7 @@ 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 { buildRelayLaneReport } from '../../indexer/relay-lane-indexer';
import { logger } from '../../utils/logger';
const router: Router = Router();
@@ -224,6 +225,41 @@ router.get('/metrics', (_req: Request, res: Response) => {
});
});
/**
* GET /api/v1/bridge/messages?lane=138-monad
* Custom relay lane tracker (Chain 138 ↔ Monad). Not indexed by ccip.chain.link.
*/
router.get('/messages', async (req: Request, res: Response) => {
const lane = String(req.query.lane ?? '138-monad').trim().toLowerCase();
if (lane !== '138-monad' && lane !== 'monad-138' && lane !== '138-monad-relay') {
return res.status(400).json({
ok: false,
error: 'Unsupported lane',
supported: ['138-monad', 'monad-138', '138-monad-relay'],
});
}
const limit = Math.min(500, Math.max(1, Number(req.query.limit ?? 100) || 100));
const fromBlockDays = Math.min(90, Math.max(1, Number(req.query.days ?? 30) || 30));
const skipRpc = req.query.skipRpc === '1' || req.query.skipRpc === 'true';
try {
const report = await buildRelayLaneReport({ fromBlockDays, limit, skipRpc });
res.set('Cache-Control', 'public, max-age=30, stale-while-revalidate=120');
return res.json({
ok: true,
lane,
...report,
});
} catch (err) {
logger.error('bridge/messages failed:', err);
return res.status(503).json({
ok: false,
error: 'Relay lane indexer unavailable',
});
}
});
router.get('/preflight', (_req: Request, res: Response) => {
const gruTransport = buildGruTransportStatus();
if (!gruTransport) {

View File

@@ -140,6 +140,49 @@ describe('Config API runtime networks loader', () => {
}
});
it('merges built-in oracles when runtime networks file omits them', async () => {
const tempPath = path.join('/tmp', `token-aggregation-networks-no-oracles-${Date.now()}.json`);
await fs.writeFile(
tempPath,
JSON.stringify({
version: { major: 1, minor: 2, patch: 0 },
chains: [
{
chainId: '0x8a',
chainIdDecimal: 138,
chainName: 'DeFi Oracle Meta Mainnet',
rpcUrls: ['https://rpc-http-pub.d-bis.org'],
nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 },
blockExplorerUrls: ['https://explorer.d-bis.org'],
iconUrls: ['https://raw.githubusercontent.com/ethereum/ethereum.org/main/static/images/eth-diamond-black.png'],
},
],
})
);
process.env.NETWORKS_JSON_PATH = tempPath;
try {
const cfgRes = await fetch(`${baseUrl}/api/v1/config?chainId=138`);
expect(cfgRes.status).toBe(200);
const cfgBody = (await cfgRes.json()) as Record<string, any>;
expect(cfgBody.oracles).toEqual(
expect.arrayContaining([
expect.objectContaining({ name: 'ETH/USD (aggregator)' }),
])
);
const networksRes = await fetch(`${baseUrl}/api/v1/networks`);
const networksBody = (await networksRes.json()) as Record<string, any>;
const chain138 = networksBody.networks.find((entry: Record<string, any>) => entry.chainIdDecimal === 138);
expect(chain138.oracles?.length).toBeGreaterThan(0);
expect(chain138.iconUrls).toEqual(
expect.arrayContaining(['https://explorer.d-bis.org/token-icons/chain-138.png'])
);
} finally {
await fs.unlink(tempPath).catch(() => undefined);
}
});
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);
@@ -162,7 +205,10 @@ describe('Config API runtime networks loader', () => {
expect(metamaskBody.addEthereumChain).toMatchObject({
chainId: '0x8a',
chainName: 'DeFi Oracle Meta Mainnet',
rpcUrls: ['https://rpc-http-pub.d-bis.org'],
});
expect(metamaskBody.addEthereumChain).not.toHaveProperty('chainIdDecimal');
expect(metamaskBody.addEthereumChain).not.toHaveProperty('oracles');
expect(metamaskBody.watchAssets).toEqual(
expect.arrayContaining([
expect.objectContaining({
@@ -187,35 +233,45 @@ describe('Config API runtime networks loader', () => {
image: expect.stringMatching(/^https:\/\/d-bis\.org\/tokens\/weth\.svg\?v=20260510$/),
}),
}),
expect.objectContaining({
type: 'ERC20',
options: expect.objectContaining({
symbol: 'cWEMIX',
image: expect.stringMatching(/^https:\/\/d-bis\.org\/tokens\/cwemix\.svg\?v=20260510$/),
}),
}),
])
);
const placeholderSymbols = ['cAVAX', 'cBNB', 'cETH', 'cETHL2', 'cPOL', 'cCRO', 'cXDAI', 'cCELO', 'cWEMIX'];
for (const symbol of placeholderSymbols) {
expect(
metamaskBody.watchAssets.some((entry: Record<string, any>) => entry.options?.symbol === symbol),
).toBe(false);
}
expect(metamaskBody.watchAssets).toEqual(
expect.arrayContaining([
expect.objectContaining({
type: 'ERC20',
options: expect.objectContaining({
address: '0x219522c60e83dEe01FC5b0329d6fA8fD84b9D13d',
address: '0xf22258f57794CC8E06237084b353Ab30fFfa640b',
symbol: 'cUSDC',
}),
metadata: expect.objectContaining({
catalogSymbol: 'cUSDC_V2',
familySymbol: 'cUSDC',
deploymentVersion: 'v2',
catalogSymbol: 'cUSDC',
}),
}),
])
);
const cusdcEntries = metamaskBody.watchAssets.filter(
(entry: Record<string, any>) => entry.options?.symbol === 'cUSDC',
);
const cusdtEntries = metamaskBody.watchAssets.filter(
(entry: Record<string, any>) => entry.options?.symbol === 'cUSDT',
);
expect(cusdcEntries).toHaveLength(1);
expect(cusdtEntries).toHaveLength(1);
expect(
metamaskBody.watchAssets.some(
(entry: Record<string, any>) => entry.options?.symbol === 'cUSDC_V2' || entry.options?.symbol === 'cUSDT_V2'
)
(entry: Record<string, any>) => entry.metadata?.catalogSymbol === 'cUSDC_V2',
),
).toBe(false);
expect(
metamaskBody.watchAssets.some(
(entry: Record<string, any>) => entry.options?.symbol === 'cUSDC_V2' || entry.options?.symbol === 'cUSDT_V2',
),
).toBe(false);
});

View File

@@ -3,6 +3,8 @@ import fs from 'fs';
import os from 'os';
import path from 'path';
import { getNetworks, getConfigByChain, API_VERSION, type NetworkEntry } from '../../config/networks';
import { toWalletAddEthereumChainParams } from '../../lib/wallet-add-ethereum-chain';
import { dedupeWatchAssetEntriesBySymbol, isWalletWatchAssetEligible } from '../../lib/wallet-watch-eligible';
import { getCanonicalTokensByChain, getLogoUriForSpec, getTokenRegistryFamily } from '../../config/canonical-tokens';
import { cacheMiddleware } from '../middleware/cache';
import { fetchRemoteJson } from '../utils/fetch-remote-json';
@@ -72,17 +74,27 @@ function resolveWalletWatchAssetSymbol(spec: { symbol: string; familySymbol?: st
return spec.symbol;
}
const HUB_COMPLIANT_WATCH_SYMBOLS = new Set(['cUSDC', 'cUSDT']);
const HUB_COMPLIANT_WATCH_CHAIN_IDS = new Set([138, 651940]);
function usesGenericEthDiamondIcons(iconUrls: unknown): boolean {
if (!Array.isArray(iconUrls) || iconUrls.length === 0) return true;
return iconUrls.every(
(url) => typeof url === 'string' && url.includes('eth-diamond-black.png'),
);
}
function isWalletWatchAssetEligible(spec: { symbol: string }, chainId: number): boolean {
// Off the hub, cUSDC/cUSDT catalog rows point at official USDC/USDT peg contracts
// (symbol USDC/USDT on-chain). MetaMask rejects wallet_watchAsset when options.symbol
// does not match ERC-20 symbol(). Public wrapped transports are cWUSDC/cWUSDT instead.
if (HUB_COMPLIANT_WATCH_SYMBOLS.has(spec.symbol) && !HUB_COMPLIANT_WATCH_CHAIN_IDS.has(chainId)) {
return false;
}
return true;
function enrichNetworksWithBuiltInMetadata(payload: NetworksPayload): NetworksPayload {
const builtInByChain = new Map(getNetworks().map((entry) => [entry.chainIdDecimal, entry]));
const networks = (payload.networks as NetworkEntry[]).map((network) => {
const builtIn = builtInByChain.get(Number(network.chainIdDecimal));
if (!builtIn) return network;
const oracles =
Array.isArray(network.oracles) && network.oracles.length > 0 ? network.oracles : builtIn.oracles;
const iconUrls =
usesGenericEthDiamondIcons(network.iconUrls) && builtIn.iconUrls?.length
? builtIn.iconUrls
: network.iconUrls;
return { ...network, oracles, iconUrls };
});
return { ...payload, networks };
}
type RuntimeNetworksPayload = {
@@ -117,8 +129,10 @@ function resolveRuntimeNetworksPath(): string | null {
const candidates = uniquePaths([
process.env.NETWORKS_JSON_PATH,
process.env.CONFIG_NETWORKS_JSON_PATH,
'/opt/explorer/config/metamask/DUAL_CHAIN_NETWORKS.json',
path.join(os.homedir(), 'projects/explorer-monorepo/backend/api/rest/config/metamask/DUAL_CHAIN_NETWORKS.json'),
path.join(os.homedir(), 'projects/explorer-monorepo/backend/config/metamask/DUAL_CHAIN_NETWORKS.json')
path.join(os.homedir(), 'projects/explorer-monorepo/backend/config/metamask/DUAL_CHAIN_NETWORKS.json'),
path.join(os.homedir(), 'projects/proxmox/docs/04-configuration/metamask/DUAL_CHAIN_NETWORKS.json'),
]);
for (const candidate of candidates) {
@@ -184,10 +198,10 @@ function loadRuntimeNetworksPayload(): NetworksPayload | null {
async function resolveNetworksPayload(): Promise<NetworksPayload> {
const remotePayload = await loadRemoteNetworksPayload();
if (remotePayload) return remotePayload;
if (remotePayload) return enrichNetworksWithBuiltInMetadata(remotePayload);
const runtimePayload = loadRuntimeNetworksPayload();
if (runtimePayload) return runtimePayload;
if (runtimePayload) return enrichNetworksWithBuiltInMetadata(runtimePayload);
return {
source: 'built-in',
@@ -231,40 +245,54 @@ router.get(['/config/metamask', '/metamask'], cacheMiddleware(5 * 60 * 1000), as
return;
}
const watchAssets = getCanonicalTokensByChain(chainId)
.filter((spec) => isWalletWatchAssetEligible(spec, 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);
const watchAssets = dedupeWatchAssetEntriesBySymbol(
getCanonicalTokensByChain(chainId)
.filter((spec) => isWalletWatchAssetEligible(spec, chainId))
.map((spec) => {
const address = spec.addresses[chainId];
if (!address) return null;
const originalLogoURI = getLogoUriForSpec(spec);
return {
type: 'ERC20' as const,
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((entry): entry is NonNullable<typeof entry> => Boolean(entry)),
);
const publicBase = resolvePublicBaseUrl(req);
res.json({
source: payload.source,
version: payload.version,
chainId,
addEthereumChain: network,
addEthereumChain: toWalletAddEthereumChainParams(network),
watchAssets,
priceFeed: {
iso4217: true,
spotPricesV2: `${publicBase}/token-aggregation/api/v1/prices/metamask/v2/chains/${chainId}/spot-prices?tokenAddresses={addresses}&vsCurrency=usd`,
submissionBundle: `${publicBase}/token-aggregation/api/v1/report/metamask-price-feed?chainId=${chainId}`,
coingeckoReport: `${publicBase}/token-aggregation/api/v1/report/coingecko?chainId=${chainId}`,
note: 'MetaMask native USD display requires chain 138 on price.api.cx.metamask.io or CoinGecko listing; these endpoints supply ISO-4217 valuation until then.',
},
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.',
'MetaMask custom-token prices are controlled by MetaMask and its upstream asset/price providers; spotPricesV2 supplies ISO-4217 USD rates but MetaMask will not read them until chain 138 is registered on price.api.cx.metamask.io or tokens are listed on CoinGecko.',
'MetaMask stores one custom token row per symbol on a network. Staged V2 deployments that share a family symbol with live V1 contracts are omitted here so balances do not disappear after import.',
'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.',
],
});
@@ -287,11 +315,16 @@ router.get('/config', cacheMiddleware(5 * 60 * 1000), async (req: Request, res:
if (chainIdParam) {
const chainId = parseInt(chainIdParam, 10);
const matchingNetwork = networks.find((network) => Number(network.chainIdDecimal) === chainId);
const builtIn = getConfigByChain(chainId);
const runtimeOracles = matchingNetwork?.oracles;
const config = matchingNetwork
? {
oracles: (matchingNetwork.oracles as unknown[]) ?? [],
oracles:
Array.isArray(runtimeOracles) && runtimeOracles.length > 0
? runtimeOracles
: (builtIn?.oracles ?? []),
}
: getConfigByChain(chainId);
: builtIn;
if (!config) {
return res.status(404).json({ error: 'Chain not found', chainId });
}

View File

@@ -0,0 +1,68 @@
import express from 'express';
import metamaskPriceRoutes from './metamask-prices';
import { primeLiveReferencePriceUsd } from '../../services/canonical-price-oracle';
jest.mock('../middleware/cache', () => ({
cacheMiddleware: () => (_req: unknown, _res: unknown, next: () => void) => next(),
}));
jest.mock('../../services/iso4217-fx-refresh', () => ({
ensureIso4217FxPrimed: jest.fn().mockResolvedValue(undefined),
}));
jest.mock('../../services/chain138-onchain-reference-prices', () => ({
refreshChain138LiveReferencePrices: jest.fn().mockResolvedValue(undefined),
getChain138EthUsdOnChain: jest.fn().mockResolvedValue({ priceUsd: 1772, source: 'aggregator' }),
}));
describe('metamask spot prices API', () => {
let baseUrl = '';
let server: ReturnType<express.Application['listen']> | null = null;
beforeAll(async () => {
process.env.TOKEN_AGGREGATION_SKIP_DB_READS = '1';
primeLiveReferencePriceUsd('USD', 1);
primeLiveReferencePriceUsd('AUD', 0.71);
const app = express();
app.use('/api/v1/prices/metamask', metamaskPriceRoutes);
await new Promise<void>((resolve) => {
server = app.listen(0, () => {
const address = server?.address();
const port = typeof address === 'object' && address ? address.port : 0;
baseUrl = `http://127.0.0.1:${port}/api/v1/prices/metamask`;
resolve();
});
});
});
afterAll(async () => {
await new Promise<void>((resolve, reject) => {
if (!server) {
resolve();
return;
}
server.close((error) => (error ? reject(error) : resolve()));
});
});
it('returns MetaMask v2 spot price map for WETH via native oracle on chain 138', async () => {
const address = '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2';
const res = await fetch(
`${baseUrl}/v2/chains/138/spot-prices?tokenAddresses=${address}&vsCurrency=usd`,
);
expect(res.status).toBe(200);
const body = (await res.json()) as Record<string, { usd?: number }>;
expect(body[address]?.usd).toBe(1772);
});
it('returns MetaMask v2 spot price map for canonical cUSDC', async () => {
const address = '0xf22258f57794cc8e06237084b353ab30fffa640b';
const res = await fetch(
`${baseUrl}/v2/chains/138/spot-prices?tokenAddresses=${address}&vsCurrency=usd`,
);
expect(res.status).toBe(200);
const body = (await res.json()) as Record<string, { usd?: number }>;
expect(body[address]?.usd).toBeGreaterThan(0);
});
});

View File

@@ -0,0 +1,118 @@
import { Router, type Request, type Response } from 'express';
import { getCanonicalTokensByChain } from '../../config/canonical-tokens';
import { cacheMiddleware } from '../middleware/cache';
import { resolveIso4217SpotPrice } from '../../services/iso4217-spot-price';
import { refreshChain138LiveReferencePrices } from '../../services/chain138-onchain-reference-prices';
const router: Router = Router();
function parseChainId(value: unknown): number | null {
const chainId = parseInt(String(value ?? ''), 10);
return Number.isFinite(chainId) && chainId > 0 ? chainId : null;
}
function parseTokenAddresses(raw: unknown): string[] {
const values = Array.isArray(raw)
? raw.flatMap((entry) => String(entry).split(','))
: String(raw ?? '')
.split(',')
.map((entry) => entry.trim());
const out: string[] = [];
const seen = new Set<string>();
for (const value of values) {
const normalized = value.trim().toLowerCase();
if (!/^0x[a-f0-9]{40}$/.test(normalized) || seen.has(normalized)) continue;
seen.add(normalized);
out.push(normalized);
if (out.length >= 120) break;
}
return out;
}
function normalizeVsCurrency(raw: unknown): string {
const value = String(raw ?? 'usd')
.trim()
.toLowerCase();
return value || 'usd';
}
async function buildMetamaskSpotPriceMap(
chainId: number,
addresses: string[],
vsCurrency: string,
): Promise<Record<string, Record<string, number>>> {
await refreshChain138LiveReferencePrices(chainId);
const response: Record<string, Record<string, number>> = {};
await Promise.all(
addresses.map(async (address) => {
const spot = await resolveIso4217SpotPrice(chainId, address);
if (spot.priceUsd != null && spot.priceUsd > 0) {
response[address] = { [vsCurrency]: spot.priceUsd };
}
}),
);
return response;
}
/**
* MetaMask Codefi Price API compatible spot prices for ISO-4217 c* tokens.
* MetaMask extension/mobile will not consume this until chain 138 is registered on
* price.api.cx.metamask.io — use this URL in Consensys / CoinGecko submission packets.
*/
router.get(
'/v2/chains/:chainId/spot-prices',
cacheMiddleware(60 * 1000),
async (req: Request, res: Response) => {
try {
const chainId = parseChainId(req.params.chainId);
if (!chainId) {
res.status(400).json({ message: 'Invalid chainId' });
return;
}
const vsCurrency = normalizeVsCurrency(req.query.vsCurrency);
const addresses = parseTokenAddresses(req.query.tokenAddresses);
if (addresses.length === 0) {
res.status(400).json({ message: 'tokenAddresses query parameter is required' });
return;
}
const prices = await buildMetamaskSpotPriceMap(chainId, addresses, vsCurrency);
res.json(prices);
} catch {
res.status(500).json({ message: 'Internal server error' });
}
},
);
router.get(
'/v1/chains/:chainId/spot-prices/:tokenAddress',
cacheMiddleware(60 * 1000),
async (req: Request, res: Response) => {
try {
const chainId = parseChainId(req.params.chainId);
const address = String(req.params.tokenAddress || '').trim().toLowerCase();
if (!chainId || !/^0x[a-f0-9]{40}$/.test(address)) {
res.status(400).json({ message: 'Invalid chainId or tokenAddress' });
return;
}
const vsCurrency = normalizeVsCurrency(req.query.vsCurrency);
const spot = await resolveIso4217SpotPrice(chainId, address);
if (spot.priceUsd == null || !(spot.priceUsd > 0)) {
res.status(404).json({ message: 'Price unavailable' });
return;
}
res.json({ [vsCurrency]: spot.priceUsd });
} catch {
res.status(500).json({ message: 'Internal server error' });
}
},
);
export default router;

View File

@@ -21,6 +21,8 @@ import {
} from '../../services/omnl-iso20022-store';
import { verifyOmnlWebhookSignature } from '../../services/omnl-webhooks';
import { appendOmnlAudit } from '../../services/omnl-audit-log';
import { listOmnlEntities, getOmnlEntity } from '../../services/omnl-entity-registry';
import { validateIso20022Payload } from '../../services/omnl-iso20022-validate';
import { getWeb3ComplianceSummary, buildNotarizationIntent } from '../../services/omnl-web3-compliance';
import {
buildComplianceConsoleSnapshot,
@@ -125,7 +127,21 @@ router.get('/omnl/disclosures/full', omnlSensitiveRouteGuard, async (req: Reques
res.json(await buildFullDisclosure(lineId));
});
router.post('/omnl/iso20022/messages', omnlSensitiveRouteGuard, (req: Request, res: Response) => {
router.get('/omnl/entities', omnlSensitiveRouteGuard, (_req, res) => {
res.json(listOmnlEntities());
});
router.get('/omnl/entities/:clientNumber', omnlSensitiveRouteGuard, (req, res) => {
const n = parseInt(String(req.params.clientNumber), 10);
const entity = getOmnlEntity(n);
if (!entity) {
res.status(404).json({ error: 'not found' });
return;
}
res.json(entity);
});
router.post('/omnl/iso20022/messages', omnlSensitiveRouteGuard, async (req: Request, res: Response) => {
try {
const body = req.body as {
messageType?: Iso20022MessageType;
@@ -139,6 +155,11 @@ router.post('/omnl/iso20022/messages', omnlSensitiveRouteGuard, (req: Request, r
res.status(400).json({ error: 'messageType and payload required' });
return;
}
const schema = await validateIso20022Payload(body.messageType, body.payload);
if (!schema.valid) {
res.status(400).json({ error: 'iso20022_schema_invalid', details: schema.errors });
return;
}
const record = saveIso20022Message({
messageType: body.messageType,
payload: body.payload,

View File

@@ -593,6 +593,20 @@ describe('Report API', () => {
);
});
it('excludes deterministic gas placeholders from wallet-facing token list', async () => {
const res = await fetch(`${baseUrl}/api/v1/report/token-list?chainId=138&wallet=1`);
expect(res.status).toBe(200);
const body = (await res.json()) as Record<string, any>;
const symbols = body.tokens.map((token: Record<string, any>) => token.symbol);
const placeholderSymbols = ['cAVAX', 'cBNB', 'cETH', 'cETHL2', 'cPOL', 'cCRO', 'cXDAI', 'cCELO', 'cWEMIX'];
for (const symbol of placeholderSymbols) {
expect(symbols).not.toContain(symbol);
}
expect(symbols).toEqual(
expect.arrayContaining(['cUSDT', 'cUSDC', 'cBTC', 'WETH'])
);
});
it('surfaces cBTC on Chain 138 and cWBTC on the staged public mesh with monetary-unit metadata', async () => {
const chain138Res = await fetch(`${baseUrl}/api/v1/report/token-list?chainId=138`);
expect(chain138Res.status).toBe(200);
@@ -636,6 +650,7 @@ describe('Report API', () => {
chainId: 138,
extensions: expect.objectContaining({
registryFamily: 'gas_native',
walletWatchEligible: false,
}),
}),
expect.objectContaining({
@@ -643,6 +658,7 @@ describe('Report API', () => {
chainId: 138,
extensions: expect.objectContaining({
registryFamily: 'gas_native',
walletWatchEligible: false,
}),
}),
])

View File

@@ -11,6 +11,7 @@ import { MarketDataRepository } from '../../database/repositories/market-data-re
import { PoolRepository } from '../../database/repositories/pool-repo';
import {
CANONICAL_TOKENS,
getCanonicalTokenByAddress,
getCanonicalTokensByChain,
getLogoUriForSpec,
getTokenRegistryFamily,
@@ -37,6 +38,8 @@ import {
} from '../../config/deployment-status';
import { getGruV2DeploymentPoolRows } from '../../config/gru-v2-deployment-pools';
import { getCanonicalPriceSnapshotGeneratedAt, getCanonicalPriceUsd } from '../../services/canonical-price-oracle';
import { getIso4217FxLastRefreshAt } from '../../services/iso4217-fx-refresh';
import { buildIso4217TokenListExtensions } from '../../services/iso4217-token-metadata';
import { pmmVaultReserveFromChain, resolvePmmQuoteRpcUrl } from '../../services/pmm-onchain-quote';
import {
buildUniswapTokenList,
@@ -46,6 +49,8 @@ import {
import { buildCuratedPoolRegistry } from '../../services/pool-registry-report';
import { enrichCuratedPoolRegistryWithLiveLiquidity } from '../../services/pool-registry-enrichment';
import { buildLpPositionsReport } from '../../services/lp-positions-report';
import { isDeterministicPlaceholderAddress } from '../../lib/deterministic-placeholder-address';
import { isWalletWatchEligibleTokenAddress, isWalletWatchAssetEligible } from '../../lib/wallet-watch-eligible';
const router: Router = Router();
const tokenRepo = new TokenRepository();
@@ -310,11 +315,6 @@ function hasExternalOfficialQuoteLiquidity(token: { chainId: number; symbol: str
return EXTERNAL_OFFICIAL_QUOTE_ASSETS.has(`${token.chainId}:${token.symbol}:${token.address.toLowerCase()}`);
}
function isDeterministicPlaceholderAddress(address: string): boolean {
const normalized = address.toLowerCase();
return /^0x[a-f0-9]{4}0{24,}[a-f0-9]{1,8}$/.test(normalized);
}
function buildSupplyProofEnrichment(
chainId: number,
address: string,
@@ -698,19 +698,52 @@ function finalizeUniswapTokenListResponse(
req: Request,
res: Response,
payload: Record<string, unknown>,
chainIds: number[]
chainIds: number[],
options?: { walletEligibleOnly?: boolean },
): void {
const logoURI = absolutePublicUrl(req, typeof payload.logoURI === 'string' ? payload.logoURI : undefined);
const walletEligibleOnly = options?.walletEligibleOnly === true;
const tokens = Array.isArray(payload.tokens)
? payload.tokens.map((token) => {
const record = token as Record<string, unknown>;
return {
...record,
logoURI:
absolutePublicUrl(req, typeof record.logoURI === 'string' ? record.logoURI : undefined) ??
record.logoURI,
};
})
? payload.tokens
.map((token) => {
const record = token as Record<string, unknown>;
const address = typeof record.address === 'string' ? record.address : undefined;
const walletWatchEligible = isWalletWatchEligibleTokenAddress(address);
const chainId = typeof record.chainId === 'number' ? record.chainId : undefined;
const spec =
chainId && address ? getCanonicalTokenByAddress(chainId, address) : undefined;
const iso4217Extensions = spec ? buildIso4217TokenListExtensions(spec) : {};
const extensions =
record.extensions && typeof record.extensions === 'object' && !Array.isArray(record.extensions)
? (record.extensions as Record<string, unknown>)
: {};
return {
...record,
logoURI:
absolutePublicUrl(req, typeof record.logoURI === 'string' ? record.logoURI : undefined) ??
record.logoURI,
extensions: {
...iso4217Extensions,
...extensions,
walletWatchEligible,
},
};
})
.filter((token) => {
if (!walletEligibleOnly) return true;
const record = token as Record<string, unknown>;
const address = typeof record.address === 'string' ? record.address : undefined;
if (!isWalletWatchEligibleTokenAddress(address)) return false;
const extensions =
record.extensions && typeof record.extensions === 'object' && !Array.isArray(record.extensions)
? (record.extensions as Record<string, unknown>)
: {};
const status = String(extensions.deploymentStatus || '').toLowerCase();
const version = String(extensions.deploymentVersion || '').toLowerCase();
const familySymbol = extensions.familySymbol;
if (status === 'staged' || (version === 'v2' && familySymbol)) return false;
return true;
})
: [];
const displayNames = chainIds.length === 1 && chainIds[0] === 1 ? MAINNET_PUBLIC_DISPLAY_NAMES : undefined;
@@ -1516,6 +1549,60 @@ router.get(
}
);
/** GET /report/metamask-price-feed — MetaMask / Consensys price-provider submission bundle for ISO-4217 c* tokens. */
router.get(
'/metamask-price-feed',
cacheMiddleware(2 * 60 * 1000),
async (req: Request, res: Response) => {
try {
const chainId = parseInt(req.query.chainId as string, 10) || 138;
const publicBase = resolvePublicBaseUrl(req);
const tokens = getCanonicalTokensByChain(chainId)
.map((spec) => {
const address = spec.addresses[chainId];
if (!address) return null;
const priceUsd = getCanonicalPriceUsd(chainId, address);
return {
chainId,
address,
symbol: spec.symbol,
name: spec.name,
decimals: spec.decimals,
priceUsd,
extensions: buildIso4217TokenListExtensions(spec),
};
})
.filter((row): row is NonNullable<typeof row> => Boolean(row));
res.json({
generatedAt: new Date().toISOString(),
schema: 'dbis-metamask-price-feed/v1',
chainId,
chainName: 'DeFi Oracle Meta Mainnet',
assetPlatformId: 'defi-oracle-meta',
iso4217Pricing: true,
fxLastRefreshAt: getIso4217FxLastRefreshAt() || null,
canonicalPriceSnapshot: getCanonicalPriceSnapshotGeneratedAt(),
metamaskPriceApiStatus: 'chain_138_not_registered_on_price_api_cx_metamask_io',
spotPricesV2: `${publicBase}/token-aggregation/api/v1/prices/metamask/v2/chains/${chainId}/spot-prices?tokenAddresses={addresses}&vsCurrency=usd`,
spotPricesV1: `${publicBase}/token-aggregation/api/v1/prices/metamask/v1/chains/${chainId}/spot-prices/{tokenAddress}?vsCurrency=usd`,
coingeckoReport: `${publicBase}/token-aggregation/api/v1/report/coingecko?chainId=${chainId}`,
tokenList: `${publicBase}/token-aggregation/api/v1/report/token-list?chainId=${chainId}`,
tokens,
submissionNotes: [
'Request Consensys to register chainId 138 on price.api.cx.metamask.io pointing at spotPricesV2, or list tokens on CoinGecko under asset platform defi-oracle-meta.',
'c* = Compliant eMoney; XXX = ISO-4217; C = Cash (M1); T = Treasury — coingeckoPriceProxyId is USD display proxy only.',
'Live FX for ISO-4217 fiat codes refreshes every 5 minutes from Frankfurter + CoinGecko reference markets.',
],
});
} catch (error) {
logger.error('Error building report/metamask-price-feed:', error);
res.status(500).json({ error: 'Internal server error' });
}
},
);
/** GET /report/token-price/:symbol — compact reviewer-facing price and evidence snapshot. */
router.get(
'/token-price/:symbol',
@@ -1644,14 +1731,19 @@ router.get(
async (req: Request, res: Response) => {
try {
const chainIdParam = req.query.chainId as string | undefined;
const walletEligibleOnly =
String(req.query.wallet ?? req.query.walletEligible ?? '').trim() === '1' ||
String(req.query.wallet ?? req.query.walletEligible ?? '').trim().toLowerCase() === 'true';
const chainIds = chainIdParam
? [parseInt(chainIdParam, 10)].filter((n) => !isNaN(n))
: getSupportedChainIds();
const finalizeOptions = { walletEligibleOnly };
if (chainIds.length === 1 && chainIds[0] === 138) {
const fileList = loadDbis138TokenListFromFile();
if (fileList) {
return finalizeUniswapTokenListResponse(req, res, fileList, chainIds);
return finalizeUniswapTokenListResponse(req, res, fileList, chainIds, finalizeOptions);
}
}
@@ -1673,7 +1765,8 @@ router.get(
...data,
tokens,
},
chainIds
chainIds,
finalizeOptions,
);
} catch (err) {
logger.error('TOKEN_LIST_JSON_URL fetch failed, using built-in token list:', err);
@@ -1686,24 +1779,25 @@ router.get(
const specs = getCanonicalTokensByChain(chainId);
for (const spec of specs) {
const address = spec.addresses[chainId];
if (address) {
const originalLogoURI = getLogoUriForSpec(spec);
list.push({
chainId,
address,
symbol: spec.symbol,
name: spec.name,
decimals: spec.decimals,
type: spec.type,
logoURI: absoluteLogoUri(req, originalLogoURI, spec.symbol),
originalLogoURI,
registryFamily: getTokenRegistryFamily(spec),
familySymbol: spec.familySymbol,
deploymentVersion: spec.deploymentVersion,
deploymentStatus: spec.deploymentStatus,
preferredForX402: spec.preferredForX402,
});
}
if (!address) continue;
if (walletEligibleOnly && !isWalletWatchAssetEligible(spec, chainId)) continue;
const originalLogoURI = getLogoUriForSpec(spec);
list.push({
chainId,
address,
symbol: spec.symbol,
name: spec.name,
decimals: spec.decimals,
type: spec.type,
logoURI: absoluteLogoUri(req, originalLogoURI, spec.symbol),
originalLogoURI,
registryFamily: getTokenRegistryFamily(spec),
familySymbol: spec.familySymbol,
deploymentVersion: spec.deploymentVersion,
deploymentStatus: spec.deploymentStatus,
preferredForX402: spec.preferredForX402,
extensions: buildIso4217TokenListExtensions(spec),
});
}
}
@@ -1717,7 +1811,8 @@ router.get(
logoURI: 'https://d-bis.org/brand/chain-138.svg',
tokens: list,
},
chainIds
chainIds,
finalizeOptions,
);
} catch (error) {
logger.error('Error building report/token-list:', error);

View File

@@ -79,10 +79,19 @@ jest.mock('../../services/token-display', () => ({
jest.mock('../middleware/cache');
jest.mock('../../services/chain138-onchain-reference-prices', () => ({
refreshChain138LiveReferencePrices: jest.fn().mockResolvedValue(undefined),
}));
jest.mock('../../services/blockscout-token-meta', () => ({
fetchBlockscoutTokenMeta: jest.fn().mockResolvedValue(null),
}));
const tokensRoutes = require('./tokens').default as typeof import('./tokens').default;
function createApp() {
const app = express();
app.use(express.json());
app.use('/api/v1', tokensRoutes);
return app;
}
@@ -429,4 +438,67 @@ describe('Tokens API', () => {
locked: false,
});
});
it('returns distinct market-batch snapshots per requested address set', async () => {
const cusdt = getCanonicalTokenBySymbol(138, 'cUSDT');
const weth = getCanonicalTokenBySymbol(138, 'WETH');
expect(cusdt?.addresses[138]).toBeTruthy();
expect(weth?.addresses[138]).toBeTruthy();
const cusdtAddress = String(cusdt?.addresses[138]).toLowerCase();
const wethAddress = String(weth?.addresses[138]).toLowerCase();
mockGetMarketData.mockImplementation(async (_chainId: number, address: string) => {
if (address === cusdtAddress) {
return {
chainId: 138,
tokenAddress: cusdtAddress,
priceUsd: 1,
volume24h: 0,
volume7d: 0,
volume30d: 0,
liquidityUsd: 1_000_000,
holdersCount: 0,
transfers24h: 0,
lastUpdated: new Date('2026-04-26T03:31:01.988Z'),
};
}
if (address === wethAddress) {
return {
chainId: 138,
tokenAddress: wethAddress,
priceUsd: 1680,
volume24h: 0,
volume7d: 0,
volume30d: 0,
liquidityUsd: 5_000_000,
holdersCount: 0,
transfers24h: 0,
lastUpdated: new Date('2026-04-26T03:31:01.988Z'),
};
}
return null;
});
const first = await fetch(`${baseUrl}/api/v1/tokens/market-batch`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ chainId: 138, addresses: [cusdtAddress] }),
});
expect(first.status).toBe(200);
const firstBody = (await first.json()) as Record<string, any>;
expect(firstBody.requested).toBe(1);
expect(firstBody.snapshots?.[0]?.address).toBe(cusdtAddress);
const second = await fetch(`${baseUrl}/api/v1/tokens/market-batch`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ chainId: 138, addresses: [wethAddress] }),
});
expect(second.status).toBe(200);
const secondBody = (await second.json()) as Record<string, any>;
expect(secondBody.requested).toBe(1);
expect(secondBody.snapshots?.[0]?.address).toBe(wethAddress);
expect(secondBody.snapshots?.[0]?.priceUsd).toBe(1680);
});
});

View File

@@ -13,6 +13,7 @@ import { filterPoolsForExposure, shouldExposePublicPool } from '../../config/gru
import {
getCanonicalTokenByAddress,
getCanonicalTokensByChain,
getCanonicalTokenBySymbol,
resolveCanonicalQuoteAddress,
} from '../../config/canonical-tokens';
import { resolveCanonicalPriceUsd } from '../../services/canonical-price-oracle';
@@ -22,6 +23,9 @@ import {
mergeMarketWithValuation,
resolveUsdValuation,
} from '../../services/valuation-precedence';
import { fetchBlockscoutTokenMeta } from '../../services/blockscout-token-meta';
import { refreshChain138LiveReferencePrices } from '../../services/chain138-onchain-reference-prices';
import { MAX_SINGLE_POOL_TVL_USD, resolveVisibleTokenLiquidityUsd } from '../../services/token-visible-liquidity';
const router: Router = Router();
const tokenRepo = new TokenRepository();
@@ -241,18 +245,215 @@ async function getTokenWithFallback(chainId: number, address: string): Promise<T
livePools.find((pool) => pool.token0Address === resolution.lookupAddress || pool.token1Address === resolution.lookupAddress)
? resolution.lookupAddress
: null;
if (!liveAddress) {
if (liveAddress) {
const display = await resolveTokenDisplay(tokenRepo, chainId, liveAddress);
return {
chainId,
address: normalized,
name: display.name,
symbol: display.symbol,
decimals: display.decimals,
verified: display.source !== 'fallback',
};
}
const blockscoutMeta = await fetchBlockscoutTokenMeta(chainId, normalized);
if (blockscoutMeta) {
return {
chainId,
address: normalized,
name: blockscoutMeta.name,
symbol: blockscoutMeta.symbol,
decimals: blockscoutMeta.decimals,
totalSupply: blockscoutMeta.totalSupply,
verified: false,
};
}
return null;
}
export interface TokenMarketSnapshot {
address: string;
symbol?: string;
name?: string;
decimals?: number;
priceUsd?: number;
liquidityUsd?: number;
volume24h?: number;
lastUpdated?: string;
source?: string;
blockscoutExchangeRate?: number;
pricingKind?: 'spot' | 'lp-share';
isCanonicalClone?: boolean;
}
function isLikelyLpReceiptSymbol(symbol: string | undefined): boolean {
if (!symbol) {
return false;
}
const upper = symbol.toUpperCase();
return upper.includes('DLP') || upper.endsWith('-LP') || upper.startsWith('LP-') || upper === 'LP';
}
async function resolveTokenRow(
chainId: number,
normalizedAddress: string,
hint?: { symbol?: string; name?: string; decimals?: number },
): Promise<Token | null> {
const fromRepo = await getTokenWithFallback(chainId, normalizedAddress);
if (fromRepo) {
return fromRepo;
}
const blockscoutMeta = await fetchBlockscoutTokenMeta(chainId, normalizedAddress);
if (blockscoutMeta) {
return {
chainId,
address: normalizedAddress,
name: blockscoutMeta.name,
symbol: blockscoutMeta.symbol,
decimals: blockscoutMeta.decimals,
totalSupply: blockscoutMeta.totalSupply,
verified: false,
};
}
const canonical = tokenFromCanonical(chainId, normalizedAddress);
if (canonical) {
return canonical;
}
if (hint?.symbol) {
return {
chainId,
address: normalizedAddress,
name: hint.name || hint.symbol,
symbol: hint.symbol,
decimals: Number.isInteger(hint.decimals) ? Number(hint.decimals) : 18,
verified: false,
};
}
return null;
}
async function resolveSymbolFallbackPriceUsd(
chainId: number,
normalizedAddress: string,
symbol: string | undefined,
): Promise<number | undefined> {
if (!symbol) {
return undefined;
}
const symbolSpec = getCanonicalTokenBySymbol(chainId, symbol);
if (!symbolSpec) {
return undefined;
}
const canonicalAddress = symbolSpec.addresses?.[chainId]?.toLowerCase();
if (canonicalAddress && canonicalAddress !== normalizedAddress) {
const canonicalSnapshot = await resolveTokenMarketSnapshot(chainId, canonicalAddress, {
allowSymbolFallback: false,
});
return canonicalSnapshot?.priceUsd;
}
const canonicalPrice = resolveCanonicalPriceUsd(chainId, normalizedAddress);
return canonicalPrice.priceUsd;
}
async function resolveTokenMarketSnapshot(
chainId: number,
address: string,
options?: { allowSymbolFallback?: boolean; hint?: { symbol?: string; name?: string; decimals?: number } },
): Promise<TokenMarketSnapshot | null> {
const normalizedAddress = address.toLowerCase();
const allowSymbolFallback = options?.allowSymbolFallback !== false;
const resolution = resolveCanonicalQuoteAddress(chainId, normalizedAddress);
const token = await resolveTokenRow(chainId, normalizedAddress, options?.hint);
if (!token) {
return null;
}
const display = await resolveTokenDisplay(tokenRepo, chainId, liveAddress);
return {
const canonicalSpec = getCanonicalTokenByAddress(chainId, normalizedAddress);
const symbolSpec = token.symbol ? getCanonicalTokenBySymbol(chainId, token.symbol) : undefined;
const canonicalAddressForSymbol = symbolSpec?.addresses?.[chainId]?.toLowerCase();
const isCanonicalClone = Boolean(
symbolSpec &&
canonicalAddressForSymbol &&
canonicalAddressForSymbol !== normalizedAddress &&
!canonicalSpec,
);
const pricingKind = isLikelyLpReceiptSymbol(token.symbol) ? 'lp-share' : 'spot';
const [marketDataRaw, blockscoutMeta, coingeckoMarket, cmcMarket, dexscreenerMarket] = await Promise.all([
marketDataRepo.getMarketData(chainId, resolution.lookupAddress),
fetchBlockscoutTokenMeta(chainId, normalizedAddress),
coingeckoAdapter.getMarketData(chainId, resolution.lookupAddress),
cmcAdapter.getMarketData(chainId, resolution.lookupAddress),
dexscreenerAdapter.getMarketData(chainId, resolution.lookupAddress),
]);
const externalFeeds = await enrichExternalFeedsWithReferencePrice(chainId, resolution.lookupAddress, {
coingecko: coingeckoMarket,
cmc: cmcMarket,
dexscreener: dexscreenerMarket,
});
const { market: marketData, pricing } = buildMarketPricingExplorer(
chainId,
address: normalized,
name: display.name,
symbol: display.symbol,
decimals: display.decimals,
verified: display.source !== 'fallback',
normalizedAddress,
resolution.lookupAddress,
marketDataRaw,
externalFeeds,
);
const market = normalizePossiblyRawLiquidityUsd(marketData, token);
let priceUsd = market?.priceUsd;
if ((priceUsd == null || !(priceUsd > 0)) && blockscoutMeta?.exchangeRate != null) {
priceUsd = blockscoutMeta.exchangeRate;
}
if (
allowSymbolFallback &&
(priceUsd == null || !(priceUsd > 0)) &&
pricingKind !== 'lp-share'
) {
const fallbackPrice = await resolveSymbolFallbackPriceUsd(chainId, normalizedAddress, token.symbol);
if (fallbackPrice != null && fallbackPrice > 0) {
priceUsd = fallbackPrice;
}
}
const MIN_MEANINGFUL_LIQUIDITY_USD = 1000;
const marketLiquidityRaw =
market?.liquidityUsd != null && market.liquidityUsd > 0 ? market.liquidityUsd : 0;
const marketLiquidityUsd =
marketLiquidityRaw > MAX_SINGLE_POOL_TVL_USD ? 0 : marketLiquidityRaw;
const poolLiquidityUsd = await resolveVisibleTokenLiquidityUsd(chainId, normalizedAddress);
const bestLiquidityUsd = Math.max(
marketLiquidityUsd >= MIN_MEANINGFUL_LIQUIDITY_USD ? marketLiquidityUsd : 0,
poolLiquidityUsd,
);
const liquidityUsd = bestLiquidityUsd > 0 ? bestLiquidityUsd : undefined;
return {
address: normalizedAddress,
symbol: token.symbol,
name: token.name,
decimals: token.decimals,
priceUsd: priceUsd != null && priceUsd > 0 ? priceUsd : undefined,
liquidityUsd,
volume24h: market?.volume24h,
lastUpdated: market?.lastUpdated instanceof Date
? market.lastUpdated.toISOString()
: market?.lastUpdated
? new Date(market.lastUpdated).toISOString()
: undefined,
source: pricing?.sourceLayer || (blockscoutMeta?.exchangeRate != null ? 'blockscout' : undefined),
blockscoutExchangeRate: blockscoutMeta?.exchangeRate,
pricingKind,
isCanonicalClone,
};
}
@@ -517,6 +718,8 @@ router.get('/tokens', cacheMiddleware(60 * 1000), async (req: Request, res: Resp
return res.status(400).json({ error: 'chainId is required' });
}
await refreshChain138LiveReferencePrices(chainId);
const { tokens, source } = await getTokensWithFallback(chainId, limit, offset);
const tokensWithMarketData = await Promise.all(
tokens.map(async (token) => {
@@ -560,6 +763,83 @@ router.get('/tokens', cacheMiddleware(60 * 1000), async (req: Request, res: Resp
}
});
router.post('/tokens/market-batch', cacheMiddleware(30 * 1000), async (req: Request, res: Response) => {
try {
const chainId = parseInt(String(req.body?.chainId ?? req.query.chainId ?? ''), 10);
const rawAddresses = req.body?.addresses;
if (!chainId) {
return res.status(400).json({ error: 'chainId is required' });
}
const hasAddressList = Array.isArray(rawAddresses);
const hasTokenHints = Array.isArray(req.body?.tokens);
if (!hasAddressList && !hasTokenHints) {
return res.status(400).json({ error: 'addresses array or tokens array is required' });
}
await refreshChain138LiveReferencePrices(chainId);
const hintByAddress = new Map<string, { symbol?: string; name?: string; decimals?: number }>();
const rawTokens = req.body?.tokens;
if (Array.isArray(rawTokens)) {
for (const row of rawTokens) {
if (!row || typeof row !== 'object') continue;
const hintAddress = String((row as Record<string, unknown>).address || '').trim().toLowerCase();
if (!/^0x[a-f0-9]{40}$/.test(hintAddress)) continue;
hintByAddress.set(hintAddress, {
symbol: typeof (row as Record<string, unknown>).symbol === 'string'
? String((row as Record<string, unknown>).symbol)
: undefined,
name: typeof (row as Record<string, unknown>).name === 'string'
? String((row as Record<string, unknown>).name)
: undefined,
decimals: Number((row as Record<string, unknown>).decimals) || undefined,
});
}
}
const addressSet = new Set<string>();
if (Array.isArray(rawAddresses)) {
for (const value of rawAddresses) {
if (typeof value === 'string') {
const normalized = value.trim().toLowerCase();
if (/^0x[a-f0-9]{40}$/.test(normalized)) {
addressSet.add(normalized);
}
}
}
}
for (const hintAddress of hintByAddress.keys()) {
addressSet.add(hintAddress);
}
const addresses = [...addressSet].slice(0, 120);
const settled = await Promise.allSettled(
addresses.map(async (address) => resolveTokenMarketSnapshot(chainId, address, {
hint: hintByAddress.get(address),
})),
);
const snapshots: TokenMarketSnapshot[] = [];
for (const row of settled) {
if (row.status === 'fulfilled' && row.value) {
snapshots.push(row.value);
}
}
return res.json({
chainId,
generatedAt: new Date().toISOString(),
requested: addresses.length,
resolved: snapshots.length,
snapshots,
});
} catch (error) {
logger.error('Error fetching token market batch:', error);
return res.status(500).json({ error: 'Internal server error' });
}
});
router.get('/tokens/:address', cacheMiddleware(60 * 1000), async (req: Request, res: Response) => {
try {
const chainId = parseInt(req.query.chainId as string, 10);
@@ -576,6 +856,8 @@ router.get('/tokens/:address', cacheMiddleware(60 * 1000), async (req: Request,
return res.status(404).json({ error: 'Token not found' });
}
await refreshChain138LiveReferencePrices(chainId);
const [
marketDataRaw,
pools,

View File

@@ -27,6 +27,7 @@ import omnlComplianceRoutes from './routes/omnl-compliance-routes';
import omnlTerminalRoutes from './routes/omnl-terminal-routes';
import checkpointRoutes from './routes/checkpoint';
import reserveCapacityRoutes from './routes/reserve-capacity';
import metamaskPriceRoutes from './routes/metamask-prices';
import { MultiChainIndexer } from '../indexer/chain-indexer';
import { OmnlEventPoller } from '../indexer/omnl-event-poller';
import { getDatabasePool } from '../database/client';
@@ -250,6 +251,7 @@ export class ApiServer {
tokenDetail: '/api/v1/tokens/{address}',
quote: '/api/v1/quote',
bridgeRoutes: '/api/v1/bridge/routes',
bridgeMessages: '/api/v1/bridge/messages?lane=138-monad',
bridgeStatus: '/api/v1/bridge/status',
bridgeMetrics: '/api/v1/bridge/metrics',
bridgePreflight: '/api/v1/bridge/preflight',
@@ -260,6 +262,8 @@ export class ApiServer {
reportOfficialProtocols: '/api/v1/report/official-protocols',
reportOpenApi: '/api/v1/report/openapi.json',
reportTokenList: '/api/v1/report/token-list',
metamaskSpotPricesV2: '/api/v1/prices/metamask/v2/chains/{chainId}/spot-prices',
metamaskPriceFeedReport: '/api/v1/report/metamask-price-feed?chainId=138',
reportTokenListEthereumMainnet: '/api/v1/report/token-list/static/ethereum-mainnet',
routesTree: '/api/v1/routes/tree',
plannerProvidersCapabilities: '/api/v2/providers/capabilities',
@@ -292,6 +296,7 @@ export class ApiServer {
this.app.use('/api/v1', partnerPayloadRoutes);
this.app.use('/api/v1', checkpointRoutes);
this.app.use('/api/v1', reserveCapacityRoutes);
this.app.use('/api/v1/prices/metamask', metamaskPriceRoutes);
this.app.use('/api/v1', omnlComplianceRoutes);
this.app.use('/api/v1', omnlTerminalRoutes);
this.app.use('/api/v1', omnlRoutes);

View File

@@ -47,6 +47,7 @@ const WETH9_DESTINATIONS: Record<string, string> = {
'Cronos (25)': '0x3Cc23d086fCcbAe1e5f3FE2bA4A263E1D27d8Cab',
'Celo (42220)': '0xAb57BF30F1354CA0590af22D8974c7f24DB2DbD7',
'Wemix (1111)': '0xD3AD6831aacB5386B8A25BB8D8176a6C8a026f04',
'Monad (143)': '0xC158b6cD3A3088C52F797D41f5Aa02825361629e',
};
const WETH10_DESTINATIONS: Record<string, string> = {