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

@@ -92,6 +92,7 @@ const REFERENCE_SYMBOL_TO_COIN_ID: Record<string, string> = {
CELO: 'celo',
CRO: 'crypto-com-chain',
XDAI: 'xdai',
LINK: 'chainlink',
};
export class CoinGeckoAdapter implements ExternalApiAdapter {
@@ -344,6 +345,10 @@ export class CoinGeckoAdapter implements ExternalApiAdapter {
*/
async getReferenceMarketData(referenceSymbol: string): Promise<MarketData | null> {
const symbol = referenceSymbol.trim().toUpperCase();
if (symbol === 'USD') {
return { priceUsd: 1, lastUpdated: new Date() };
}
const coinId = REFERENCE_SYMBOL_TO_COIN_ID[symbol];
if (!coinId) {
return null;

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> = {

View File

@@ -0,0 +1,118 @@
import type {
ComplianceDecisionEngine,
ComplianceEvaluationInput,
ComplianceEvaluationResult,
} from '@dbis/integration-foundation';
import { MockComplianceDecisionEngine } from '@dbis/integration-foundation';
import fs from 'node:fs';
import path from 'node:path';
type Rule = { id: string; decision?: string; trigger?: { riskHints?: string[]; amountGte?: number } };
type SanctionEntry = { id: string; names?: string[]; active?: boolean };
function loadJson<T>(p: string, fallback: T): T {
try {
return JSON.parse(fs.readFileSync(p, 'utf8')) as T;
} catch {
return fallback;
}
}
export class Volume13ComplianceDecisionEngine implements ComplianceDecisionEngine {
private readonly fallback = new MockComplianceDecisionEngine();
private readonly rules: Rule[];
private readonly sanctions: SanctionEntry[];
private readonly vendor: string;
private auditCounter = 0;
constructor(root: string) {
this.vendor = process.env.COMPLIANCE_VENDOR || 'internal';
const rulesDoc = loadJson<{ rules?: Rule[] }>(
path.join(root, 'config/hybx-omnl-dbis/volume13-aml-decision-rules.v1.json'),
{}
);
const sanctionsDoc = loadJson<{ entries?: SanctionEntry[] }>(
path.join(root, 'config/compliance/internal-sanctions-list.v1.json'),
{}
);
this.rules = rulesDoc.rules ?? [];
this.sanctions = sanctionsDoc.entries ?? [];
}
private auditId(): string {
this.auditCounter += 1;
return `audit-v13-${this.vendor}-${this.auditCounter}`;
}
private sanctioned(entityId: string): boolean {
const n = entityId.toLowerCase();
return this.sanctions.some(
(e) =>
e.active !== false &&
(e.id.toLowerCase() === n || (e.names ?? []).some((x) => n.includes(x.toLowerCase())))
);
}
async evaluateTransaction(input: ComplianceEvaluationInput): Promise<ComplianceEvaluationResult> {
const hints = input.riskHints ?? [];
if (this.sanctioned(input.entityId) || hints.includes('sanctions_hit')) {
return {
decision: 'block',
risk_score: 100,
reason_codes: ['SANCTIONS_HIT'],
evidence_refs: ['volume13-sanctions'],
manual_review_required: true,
audit_event_id: this.auditId(),
};
}
for (const rule of this.rules) {
const t = rule.trigger ?? {};
if (t.riskHints?.some((h) => hints.includes(h))) {
return {
decision: (rule.decision as ComplianceEvaluationResult['decision']) ?? 'review',
risk_score: 80,
reason_codes: [rule.id],
evidence_refs: [rule.id],
manual_review_required: true,
audit_event_id: this.auditId(),
};
}
if (t.amountGte != null && Number(input.amount) >= t.amountGte) {
return {
decision: (rule.decision as ComplianceEvaluationResult['decision']) ?? 'hold',
risk_score: 65,
reason_codes: [rule.id],
evidence_refs: [rule.id],
manual_review_required: true,
audit_event_id: this.auditId(),
};
}
}
return this.fallback.evaluateTransaction(input);
}
async evaluateEntity(entityId: string, tenantId: string): Promise<ComplianceEvaluationResult> {
if (this.sanctioned(entityId)) {
return {
decision: 'reject',
risk_score: 95,
reason_codes: ['ENTITY_SANCTIONS'],
evidence_refs: [entityId],
manual_review_required: true,
audit_event_id: this.auditId(),
};
}
return this.fallback.evaluateEntity(entityId, tenantId);
}
async evaluateCounterparty(counterpartyId: string, tenantId: string): Promise<ComplianceEvaluationResult> {
return this.evaluateEntity(counterpartyId, tenantId);
}
}
export function createComplianceDecisionEngine(proxomoxRoot: string): ComplianceDecisionEngine {
if (process.env.OMNL_COMPLIANCE_GATE !== '1') {
return new MockComplianceDecisionEngine();
}
return new Volume13ComplianceDecisionEngine(proxomoxRoot);
}

View File

@@ -3,7 +3,8 @@
* Source of truth: config/rwa-capital-markets-taxonomy.v1.json
*/
import taxonomy from '../../../../../config/rwa-capital-markets-taxonomy.v1.json';
import { existsSync, readFileSync } from 'fs';
import { resolve } from 'path';
export interface CapitalMarketsFields {
assetClass: string;
@@ -28,8 +29,31 @@ type TaxonomyInstrument = {
defillamaEligible?: boolean;
};
type TaxonomyDocument = {
version: string;
instruments: TaxonomyInstrument[];
};
function projectRoot(): string {
return (
process.env.PROXMOX_ROOT?.trim() ||
process.env.PHOENIX_REPO_ROOT?.trim() ||
resolve(__dirname, '../../../../../..')
);
}
function loadTaxonomyDocument(): TaxonomyDocument {
const path = resolve(projectRoot(), 'config/rwa-capital-markets-taxonomy.v1.json');
if (!existsSync(path)) {
return { version: 'unknown', instruments: [] };
}
return JSON.parse(readFileSync(path, 'utf8')) as TaxonomyDocument;
}
const taxonomy = loadTaxonomyDocument();
const byTicker = new Map<string, CapitalMarketsFields>(
(taxonomy.instruments as TaxonomyInstrument[]).map((row) => [
taxonomy.instruments.map((row) => [
row.ticker,
{
assetClass: row.asset_class,

View File

@@ -141,6 +141,15 @@ export const CHAIN_CONFIGS: Record<number, ChainConfig> = {
blockTime: 2,
confirmations: 1,
},
143: {
chainId: 143,
name: 'Monad Mainnet',
rpcUrl: process.env.CHAIN_143_RPC_URL || process.env.MONAD_MAINNET_RPC || 'https://rpc.monad.xyz',
explorerUrl: 'https://monadscan.com',
nativeCurrency: { name: 'Monad', symbol: 'MON', decimals: 18 },
blockTime: 1,
confirmations: 1,
},
};
export function getChainConfig(chainId: number): ChainConfig | undefined {

View File

@@ -48,6 +48,12 @@ if (envAddr('CCIPWETH9_BRIDGE_CHAIN138')) {
lanes: [
{ destSelector: '5009297550715157269', destChainId: 1, destChainName: 'Ethereum' },
{ destSelector: '16015286601757825753', destChainId: 651940, destChainName: 'ALL Mainnet' },
{
destSelector: '8481857512324358265',
destChainId: 143,
destChainName: 'Monad Mainnet',
bridgeType: 'ccip_relay_monad',
},
],
});
}

View File

@@ -0,0 +1,127 @@
import fs from 'fs';
import path from 'path';
export interface MonadRelayLaneChainSide {
chainId: number;
selector: string;
explorer: string;
rpcEnv?: string;
rpcDefault?: string;
sendBridgeWeth9: string;
relayBridgeInbound?: string;
relayBridgeOutbound?: string;
ccipEventRouter?: string;
ccipRelayRouter?: string;
ccipRouterSend?: string;
hubBridgeWeth9?: string;
}
export interface MonadRelaySeedMessage {
messageId: string;
direction: '138-to-monad' | 'monad-to-138';
status: 'initiated' | 'relaying' | 'delivered' | 'failed';
sourceTxHash?: string;
relayTxHash?: string;
amountWei?: string;
tokenSymbol?: string;
sender?: string;
recipient?: string;
}
export interface MonadRelayLaneConfig {
version: string;
laneId: string;
description: string;
integration?: {
explorerApi?: string;
ccipExplorer?: boolean;
relayService?: string;
creRequired?: boolean;
creNotes?: string;
};
chain138: MonadRelayLaneChainSide;
monad: MonadRelayLaneChainSide;
seedMessages?: MonadRelaySeedMessage[];
}
const BUILTIN_DEFAULTS: MonadRelayLaneConfig = {
version: '2026-06-13',
laneId: '138-monad-relay',
description: 'Custom CCIP relay lane (event-only routers + off-chain relay).',
chain138: {
chainId: 138,
selector: '16015286601757825753',
explorer: 'https://explorer.d-bis.org',
rpcEnv: 'RPC_URL_138',
sendBridgeWeth9: '0xcacfd227A040002e49e2e01626363071324f820a',
relayBridgeInbound: '0xEB87B3bE09Ab383Ea391caf5bCE70B44Dbd80A9B',
ccipEventRouter: '0x42DAb7b888Dd382bD5Adcf9E038dBF1fD03b4817',
ccipRelayRouter: '0xe75d26bc558a28442f30750c6d97bffb46f39abc',
},
monad: {
chainId: 143,
selector: '8481857512324358265',
explorer: 'https://monadscan.com',
rpcEnv: 'MONAD_MAINNET_RPC',
rpcDefault: 'https://rpc.monad.xyz',
sendBridgeWeth9: '0xF9DD3349C7f11812d727Db5335690b1e6AD16a52',
relayBridgeOutbound: '0xC158b6cD3A3088C52F797D41f5Aa02825361629e',
ccipRelayRouter: '0x89dd12025bfCD38A168455A44B400e913ED33BE2',
ccipRouterSend: '0x937824f2516fa58f25aeAb92E7BFf7D74F463B4c',
hubBridgeWeth9: '0x8078A09637e47Fa5Ed34F626046Ea2094a5CDE5e',
},
};
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;
}
export function resolveMonadRelayLaneConfigPath(): string | null {
const candidates = uniquePaths([
process.env.MONAD_RELAY_LANE_JSON_PATH,
path.resolve(process.cwd(), 'config', 'monad-chain138-relay-lane.v1.json'),
path.resolve(process.cwd(), '..', 'config', 'monad-chain138-relay-lane.v1.json'),
path.resolve(process.cwd(), '..', '..', 'config', 'monad-chain138-relay-lane.v1.json'),
path.resolve(process.cwd(), '..', '..', '..', 'config', 'monad-chain138-relay-lane.v1.json'),
path.resolve(__dirname, '../../../../../config/monad-chain138-relay-lane.v1.json'),
]);
for (const candidate of candidates) {
if (fs.existsSync(candidate)) return candidate;
}
return null;
}
export function loadMonadRelayLaneConfig(): MonadRelayLaneConfig {
const configPath = resolveMonadRelayLaneConfigPath();
if (!configPath) return BUILTIN_DEFAULTS;
try {
const raw = fs.readFileSync(configPath, 'utf8');
const parsed = JSON.parse(raw) as MonadRelayLaneConfig;
return {
...BUILTIN_DEFAULTS,
...parsed,
chain138: { ...BUILTIN_DEFAULTS.chain138, ...parsed.chain138 },
monad: { ...BUILTIN_DEFAULTS.monad, ...parsed.monad },
};
} catch {
return BUILTIN_DEFAULTS;
}
}
export function resolveMonadRpcUrl(config: MonadRelayLaneConfig = loadMonadRelayLaneConfig()): string {
const envKey = config.monad.rpcEnv ?? 'MONAD_MAINNET_RPC';
const fromEnv = process.env[envKey]?.trim();
if (fromEnv) return fromEnv;
return config.monad.rpcDefault ?? 'https://rpc.monad.xyz';
}

View File

@@ -25,12 +25,7 @@ export const NETWORKS: NetworkEntry[] = [
chainId: '0x8a',
chainIdDecimal: 138,
chainName: 'DeFi Oracle Meta Mainnet',
rpcUrls: [
'https://rpc-http-pub.d-bis.org',
'https://rpc.d-bis.org',
'https://rpc2.d-bis.org',
'https://rpc.defi-oracle.io',
],
rpcUrls: ['https://rpc-http-pub.d-bis.org'],
nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 },
blockExplorerUrls: ['https://explorer.d-bis.org'],
iconUrls: [
@@ -39,7 +34,8 @@ export const NETWORKS: NetworkEntry[] = [
'https://explorer.d-bis.org/favicon.ico',
],
oracles: [
{ name: 'ETH/USD', address: '0x3304b747e565a97ec8ac220b0b6a1f6ffdb837e6', decimals: 8 },
{ name: 'ETH/USD (proxy)', address: '0x3304b747e565a97ec8ac220b0b6a1f6ffdb837e6', decimals: 8 },
{ name: 'ETH/USD (aggregator)', address: '0x99b3511a2d315a497c8112c1fdd8d508d4b1e506', decimals: 8 },
],
},
{

View File

@@ -6,6 +6,7 @@ import { closeDatabasePool } from './database/client';
import { logger } from './utils/logger';
import { startRouteMatrixScheduler } from './services/route-matrix-scheduler';
import { startGruReserveCapacityScheduler } from './services/gru-reserve-capacity-scheduler';
import { startIso4217FxScheduler } from './services/iso4217-fx-refresh';
// Load smom-dbis-138 root .env first (single source); works from dist/ or src/
const rootEnvCandidates = [
@@ -30,6 +31,7 @@ try {
const server = new ApiServer();
startRouteMatrixScheduler();
startGruReserveCapacityScheduler();
startIso4217FxScheduler();
// Start server
server.start().catch((error) => {

View File

@@ -10,8 +10,11 @@ import { CoinMarketCapAdapter } from '../adapters/cmc-adapter';
import { DexScreenerAdapter } from '../adapters/dexscreener-adapter';
import { logger } from '../utils/logger';
import { getCanonicalPriceUsd } from '../services/canonical-price-oracle';
import { getChain138EthUsdOnChain } from '../services/chain138-onchain-reference-prices';
import { pickExternalMarketDataForIndexer } from '../services/valuation-precedence';
const CHAIN138_WETH = '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2';
export class ChainIndexer {
private chainId: number;
private provider: ethers.JsonRpcProvider;
@@ -171,6 +174,14 @@ export class ChainIndexer {
});
const canonicalPriceUsd = getCanonicalPriceUsd(this.chainId, tokenAddress);
let priceUsd = externalData?.priceUsd ?? canonicalPriceUsd;
if (this.chainId === 138 && tokenAddress.toLowerCase() === CHAIN138_WETH) {
const native = await getChain138EthUsdOnChain();
if (native?.priceUsd != null && native.priceUsd > 0) {
priceUsd = native.priceUsd;
}
}
// Get pools for liquidity calculation
const tokenPools = pools.filter(
(p) => p.token0Address === tokenAddress || p.token1Address === tokenAddress
@@ -181,7 +192,7 @@ export class ChainIndexer {
await this.marketDataRepo.upsertMarketData({
chainId: this.chainId,
tokenAddress,
priceUsd: externalData?.priceUsd ?? canonicalPriceUsd,
priceUsd,
priceChange24h: externalData?.priceChange24h,
volume24h: volumeMetrics.volume24h || externalData?.volume24h || 0,
volume7d: volumeMetrics.volume7d,

View File

@@ -0,0 +1,84 @@
import { mergeRelayLaneMessages } from './relay-lane-indexer';
import type { MonadRelayLaneConfig } from '../config/monad-relay-lane';
const TEST_CONFIG: MonadRelayLaneConfig = {
version: 'test',
laneId: '138-monad-relay',
description: 'test lane',
chain138: {
chainId: 138,
selector: '16015286601757825753',
explorer: 'https://explorer.d-bis.org',
sendBridgeWeth9: '0xcacfd227A040002e49e2e01626363071324f820a',
relayBridgeInbound: '0xEB87B3bE09Ab383Ea391caf5bCE70B44Dbd80A9B',
},
monad: {
chainId: 143,
selector: '8481857512324358265',
explorer: 'https://monadscan.com',
sendBridgeWeth9: '0xF9DD3349C7f11812d727Db5335690b1e6AD16a52',
relayBridgeOutbound: '0xC158b6cD3A3088C52F797D41f5Aa02825361629e',
},
};
describe('mergeRelayLaneMessages', () => {
it('merges initiated and completed by messageId', () => {
const messageId = '0xdebc46c8ce3b4698331768dcddb9c6ee69c7d891fbe48e930f383a27330bf8e8';
const messages = mergeRelayLaneMessages(
TEST_CONFIG,
[
{
messageId,
direction: '138-to-monad',
sourceChainId: 138,
destChainId: 143,
destSelector: '8481857512324358265',
sourceTxHash: '0xd2200edf828a7310aebc28e704f668732a5eb4fcba648b7339aa132a1496dd39',
sourceBlockNumber: 100,
sender: '0x4A666F96fC8764181194447A7dFdb7d471b301C8',
recipient: '0x4A666F96fC8764181194447A7dFdb7d471b301C8',
amountWei: '1000000000000000',
tokenSymbol: 'WETH',
},
],
[
{
messageId,
direction: '138-to-monad',
destChainId: 143,
destTxHash: '0xbd79554a89e2abb3024462009725851f57a54eb8ddfd3d095068607c8a5e2402',
destBlockNumber: 200,
recipient: '0x4A666F96fC8764181194447A7dFdb7d471b301C8',
amountWei: '1000000000000000',
},
],
[]
);
expect(messages).toHaveLength(1);
expect(messages[0].status).toBe('delivered');
expect(messages[0].sourceTxHash).toMatch(/^0x/);
expect(messages[0].relayTxHash).toMatch(/^0x/);
expect(messages[0].explorerLinks.sourceTx).toContain('explorer.d-bis.org');
expect(messages[0].explorerLinks.relayTx).toContain('monadscan.com');
});
it('includes seed messages when RPC data is absent', () => {
const messages = mergeRelayLaneMessages(TEST_CONFIG, [], [], [
{
messageId: '0x82dcbc28d1e7b50a0b9caa1a9415078148dd78b5aeba9a23aa5714ef6cf8b970',
direction: 'monad-to-138',
status: 'delivered',
sourceTxHash: '0x27d4ccf43c8bc17bab56b0a18c830dce21dd7ae175fd24e2261cb3a414f994ae',
relayTxHash: '0x4435ecacc79919f64e6de6cd6b2d454a92580bb72a4c7e320d06cff2354e9567',
amountWei: '1000000000000000',
tokenSymbol: 'WMON',
},
]);
expect(messages).toHaveLength(1);
expect(messages[0].direction).toBe('monad-to-138');
expect(messages[0].explorerLinks.sourceTx).toContain('monadscan.com');
expect(messages[0].explorerLinks.relayTx).toContain('explorer.d-bis.org');
});
});

View File

@@ -0,0 +1,447 @@
/**
* Indexes custom CCIP relay lane messages for Chain 138 ↔ Monad (143).
* Correlates CrossChainTransferInitiated (source) with CrossChainTransferCompleted (destination).
*/
import { ethers } from 'ethers';
import { resolveChain138RpcUrl } from '../config/chain138-rpc';
import {
loadMonadRelayLaneConfig,
MonadRelayLaneConfig,
MonadRelaySeedMessage,
resolveMonadRpcUrl,
} from '../config/monad-relay-lane';
import { logger } from '../utils/logger';
const CCIP_TRANSFER_ABI = [
'event CrossChainTransferInitiated(bytes32 indexed messageId, address indexed sender, uint64 indexed destinationChainSelector, address recipient, uint256 amount, uint256 nonce)',
'event CrossChainTransferCompleted(bytes32 indexed messageId, uint64 indexed sourceChainSelector, address indexed recipient, uint256 amount)',
'function processedTransfers(bytes32 messageId) view returns (bool)',
];
const CROSS_CHAIN_QUERY_FALLBACK_BLOCK_SPAN = Math.max(
1,
Number(process.env.CROSS_CHAIN_QUERY_FALLBACK_BLOCK_SPAN || 5000)
);
export type RelayLaneDirection = '138-to-monad' | 'monad-to-138';
export type RelayLaneStatus = 'initiated' | 'relaying' | 'delivered' | 'failed';
export interface RelayLaneInitiatedEvent {
messageId: string;
direction: RelayLaneDirection;
sourceChainId: number;
destChainId: number;
destSelector: string;
sourceTxHash: string;
sourceBlockNumber: number;
sender: string;
recipient: string;
amountWei: string;
tokenSymbol: string;
}
export interface RelayLaneCompletedEvent {
messageId: string;
direction: RelayLaneDirection;
destChainId: number;
destTxHash: string;
destBlockNumber: number;
recipient: string;
amountWei: string;
}
export interface RelayLaneMessage {
messageId: string;
laneId: string;
direction: RelayLaneDirection;
status: RelayLaneStatus;
sourceChainId: number;
destChainId: number;
tokenSymbol: string;
amountWei: string;
sender?: string;
recipient?: string;
sourceTxHash?: string;
sourceBlockNumber?: number;
relayTxHash?: string;
destTxHash?: string;
destBlockNumber?: number;
explorerLinks: {
sourceTx?: string;
relayTx?: string;
destTx?: string;
};
}
export interface RelayLaneReport {
generatedAt: string;
laneId: string;
description: string;
integration: {
ccipExplorer: boolean;
creRequired: boolean;
relayService?: string;
notes?: string;
};
chains: {
chain138: { chainId: number; selector: string; explorer: string };
monad: { chainId: number; selector: string; explorer: string };
};
contracts: {
chain138SendBridge: string;
chain138RelayBridge: string;
monadSendBridge: string;
monadRelayBridge: string;
};
messages: RelayLaneMessage[];
stats: {
total: number;
delivered: number;
pending: number;
};
}
function txExplorer(base: string, txHash?: string): string | undefined {
if (!txHash?.startsWith('0x')) return undefined;
const trimmed = base.replace(/\/$/, '');
return `${trimmed}/tx/${txHash}`;
}
function isRpcRangeLimitError(error: unknown): boolean {
const parts = [
typeof error === 'object' && error ? (error as { message?: string }).message : '',
typeof error === 'object' && error ? (error as { shortMessage?: string }).shortMessage : '',
typeof error === 'object' && error
? ((error as { error?: { message?: string } }).error?.message ?? '')
: '',
];
return parts.some((part) => typeof part === 'string' && /range limit|exceeds maximum rpc range/i.test(part));
}
async function queryFilterWithRangeFallback(
contract: ethers.Contract,
filter: ReturnType<ethers.Contract['filters'][string]>,
fromBlock: number,
toBlock: number
): Promise<ethers.EventLog[]> {
try {
return (await contract.queryFilter(filter as never, fromBlock, toBlock)) as ethers.EventLog[];
} catch (error) {
if (!isRpcRangeLimitError(error) || fromBlock >= toBlock) throw error;
}
const logs: ethers.EventLog[] = [];
const chunkSpan = Math.min(CROSS_CHAIN_QUERY_FALLBACK_BLOCK_SPAN, toBlock - fromBlock + 1);
for (let start = fromBlock; start <= toBlock; start += chunkSpan) {
const end = Math.min(start + chunkSpan - 1, toBlock);
const chunk = await queryFilterWithRangeFallback(contract, filter, start, end);
logs.push(...chunk);
}
return logs;
}
async function fetchInitiated(
provider: ethers.JsonRpcProvider,
bridgeAddress: string,
destSelector: string,
direction: RelayLaneDirection,
sourceChainId: number,
destChainId: number,
tokenSymbol: string,
fromBlock: number,
toBlock: number
): Promise<RelayLaneInitiatedEvent[]> {
const events: RelayLaneInitiatedEvent[] = [];
try {
const contract = new ethers.Contract(bridgeAddress, CCIP_TRANSFER_ABI, provider);
const filter = contract.filters.CrossChainTransferInitiated();
const logs = await queryFilterWithRangeFallback(contract, filter, fromBlock, toBlock);
for (const log of logs) {
const args = (log as ethers.EventLog).args as unknown as {
messageId: string;
sender: string;
destinationChainSelector: bigint;
recipient: string;
amount: bigint;
};
if (args.destinationChainSelector?.toString() !== destSelector) continue;
events.push({
messageId: args.messageId,
direction,
sourceChainId,
destChainId,
destSelector,
sourceTxHash: log.transactionHash,
sourceBlockNumber: log.blockNumber,
sender: args.sender,
recipient: args.recipient,
amountWei: args.amount?.toString() ?? '0',
tokenSymbol,
});
}
} catch (err) {
logger.warn(`Relay lane indexer: initiated events for ${bridgeAddress} failed:`, err);
}
return events;
}
async function fetchCompleted(
provider: ethers.JsonRpcProvider,
bridgeAddress: string,
sourceSelector: string,
direction: RelayLaneDirection,
destChainId: number,
fromBlock: number,
toBlock: number
): Promise<RelayLaneCompletedEvent[]> {
const events: RelayLaneCompletedEvent[] = [];
try {
const contract = new ethers.Contract(bridgeAddress, CCIP_TRANSFER_ABI, provider);
const filter = contract.filters.CrossChainTransferCompleted();
const logs = await queryFilterWithRangeFallback(contract, filter, fromBlock, toBlock);
for (const log of logs) {
const args = (log as ethers.EventLog).args as unknown as {
messageId: string;
sourceChainSelector: bigint;
recipient: string;
amount: bigint;
};
if (args.sourceChainSelector?.toString() !== sourceSelector) continue;
events.push({
messageId: args.messageId,
direction,
destChainId,
destTxHash: log.transactionHash,
destBlockNumber: log.blockNumber,
recipient: args.recipient,
amountWei: args.amount?.toString() ?? '0',
});
}
} catch (err) {
logger.warn(`Relay lane indexer: completed events for ${bridgeAddress} failed:`, err);
}
return events;
}
/** Merge initiated + completed + seed rows into explorer-facing messages. Exported for tests. */
export function mergeRelayLaneMessages(
config: MonadRelayLaneConfig,
initiated: RelayLaneInitiatedEvent[],
completed: RelayLaneCompletedEvent[],
seeds: MonadRelaySeedMessage[] = []
): RelayLaneMessage[] {
const byId = new Map<string, RelayLaneMessage>();
const upsert = (messageId: string, patch: Partial<RelayLaneMessage> & Pick<RelayLaneMessage, 'direction'>): void => {
const key = messageId.toLowerCase();
const existing = byId.get(key);
const sourceExplorer =
patch.direction === '138-to-monad' ? config.chain138.explorer : config.monad.explorer;
const destExplorer =
patch.direction === '138-to-monad' ? config.monad.explorer : config.chain138.explorer;
const merged: RelayLaneMessage = {
messageId,
laneId: config.laneId,
direction: patch.direction,
status: patch.status ?? existing?.status ?? 'initiated',
sourceChainId: patch.sourceChainId ?? existing?.sourceChainId ?? (patch.direction === '138-to-monad' ? 138 : 143),
destChainId: patch.destChainId ?? existing?.destChainId ?? (patch.direction === '138-to-monad' ? 143 : 138),
tokenSymbol: patch.tokenSymbol ?? existing?.tokenSymbol ?? (patch.direction === '138-to-monad' ? 'WETH' : 'WMON'),
amountWei: patch.amountWei ?? existing?.amountWei ?? '0',
sender: patch.sender ?? existing?.sender,
recipient: patch.recipient ?? existing?.recipient,
sourceTxHash: patch.sourceTxHash ?? existing?.sourceTxHash,
sourceBlockNumber: patch.sourceBlockNumber ?? existing?.sourceBlockNumber,
relayTxHash: patch.relayTxHash ?? existing?.relayTxHash,
destTxHash: patch.destTxHash ?? existing?.destTxHash,
destBlockNumber: patch.destBlockNumber ?? existing?.destBlockNumber,
explorerLinks: {
sourceTx: txExplorer(sourceExplorer, patch.sourceTxHash ?? existing?.sourceTxHash),
relayTx: txExplorer(destExplorer, patch.relayTxHash ?? existing?.relayTxHash ?? patch.destTxHash ?? existing?.destTxHash),
destTx: txExplorer(destExplorer, patch.destTxHash ?? existing?.destTxHash),
},
};
byId.set(key, merged);
};
for (const e of initiated) {
upsert(e.messageId, {
direction: e.direction,
status: 'initiated',
sourceChainId: e.sourceChainId,
destChainId: e.destChainId,
tokenSymbol: e.tokenSymbol,
amountWei: e.amountWei,
sender: e.sender,
recipient: e.recipient,
sourceTxHash: e.sourceTxHash,
sourceBlockNumber: e.sourceBlockNumber,
});
}
for (const e of completed) {
upsert(e.messageId, {
direction: e.direction,
status: 'delivered',
destChainId: e.destChainId,
amountWei: e.amountWei,
recipient: e.recipient,
relayTxHash: e.destTxHash,
destTxHash: e.destTxHash,
destBlockNumber: e.destBlockNumber,
});
}
for (const seed of seeds) {
upsert(seed.messageId, {
direction: seed.direction,
status: seed.status,
tokenSymbol: seed.tokenSymbol,
amountWei: seed.amountWei,
sender: seed.sender,
recipient: seed.recipient,
sourceTxHash: seed.sourceTxHash,
relayTxHash: seed.relayTxHash,
destTxHash: seed.relayTxHash,
});
}
return [...byId.values()].sort((a, b) => {
const blockA = a.sourceBlockNumber ?? a.destBlockNumber ?? 0;
const blockB = b.sourceBlockNumber ?? b.destBlockNumber ?? 0;
return blockB - blockA;
});
}
export interface BuildRelayLaneReportOptions {
fromBlockDays?: number;
limit?: number;
skipRpc?: boolean;
}
export async function buildRelayLaneReport(
options: BuildRelayLaneReportOptions = {}
): Promise<RelayLaneReport> {
const config = loadMonadRelayLaneConfig();
const fromBlockDays = options.fromBlockDays ?? 30;
const limit = options.limit ?? 100;
const seeds = config.seedMessages ?? [];
let initiated: RelayLaneInitiatedEvent[] = [];
let completed: RelayLaneCompletedEvent[] = [];
if (!options.skipRpc) {
try {
const chain138Rpc = resolveChain138RpcUrl();
const monadRpc = resolveMonadRpcUrl(config);
const chain138Provider = new ethers.JsonRpcProvider(chain138Rpc);
const monadProvider = new ethers.JsonRpcProvider(monadRpc);
const [chain138Block, monadBlock] = await Promise.all([
chain138Provider.getBlockNumber(),
monadProvider.getBlockNumber(),
]);
const chain138From = Math.max(0, chain138Block - Math.floor((fromBlockDays * 86400) / 5));
const monadFrom = Math.max(0, monadBlock - Math.floor((fromBlockDays * 86400) / 1));
const [
initiated138,
initiatedMonad,
completedMonad,
completed138,
] = await Promise.all([
fetchInitiated(
chain138Provider,
config.chain138.sendBridgeWeth9,
config.monad.selector,
'138-to-monad',
config.chain138.chainId,
config.monad.chainId,
'WETH',
chain138From,
chain138Block
),
fetchInitiated(
monadProvider,
config.monad.sendBridgeWeth9,
config.chain138.selector,
'monad-to-138',
config.monad.chainId,
config.chain138.chainId,
'WMON',
monadFrom,
monadBlock
),
fetchCompleted(
monadProvider,
config.monad.relayBridgeOutbound ?? '',
config.chain138.selector,
'138-to-monad',
config.monad.chainId,
monadFrom,
monadBlock
),
fetchCompleted(
chain138Provider,
config.chain138.relayBridgeInbound ?? '',
config.monad.selector,
'monad-to-138',
config.chain138.chainId,
chain138From,
chain138Block
),
]);
initiated = [...initiated138, ...initiatedMonad];
completed = [...completedMonad, ...completed138];
} catch (err) {
logger.warn('Relay lane indexer: RPC scan failed, returning seed messages only:', err);
}
}
const messages = mergeRelayLaneMessages(config, initiated, completed, seeds).slice(0, limit);
const delivered = messages.filter((m) => m.status === 'delivered').length;
return {
generatedAt: new Date().toISOString(),
laneId: config.laneId,
description: config.description,
integration: {
ccipExplorer: config.integration?.ccipExplorer ?? false,
creRequired: config.integration?.creRequired ?? false,
relayService: config.integration?.relayService,
notes: config.integration?.creNotes,
},
chains: {
chain138: {
chainId: config.chain138.chainId,
selector: config.chain138.selector,
explorer: config.chain138.explorer,
},
monad: {
chainId: config.monad.chainId,
selector: config.monad.selector,
explorer: config.monad.explorer,
},
},
contracts: {
chain138SendBridge: config.chain138.sendBridgeWeth9,
chain138RelayBridge: config.chain138.relayBridgeInbound ?? '',
monadSendBridge: config.monad.sendBridgeWeth9,
monadRelayBridge: config.monad.relayBridgeOutbound ?? '',
},
messages,
stats: {
total: messages.length,
delivered,
pending: messages.length - delivered,
},
};
}

View File

@@ -0,0 +1,13 @@
import { isDeterministicPlaceholderAddress } from './deterministic-placeholder-address';
describe('isDeterministicPlaceholderAddress', () => {
it('detects GRU transport placeholders on Chain 138', () => {
expect(isDeterministicPlaceholderAddress('0xcaaa00000000000000000000000000000000008a')).toBe(true);
expect(isDeterministicPlaceholderAddress('0xc11100000000000000000000000000000000008a')).toBe(true);
});
it('does not flag live deployed token addresses', () => {
expect(isDeterministicPlaceholderAddress('0x93E66202A11B1772E55407B32B44e5Cd8eda7f22')).toBe(false);
expect(isDeterministicPlaceholderAddress('0xb7721dD53A8c629d9f1Ba31a5819AFe250002b03')).toBe(false);
});
});

View File

@@ -0,0 +1,5 @@
/** GRU transport placeholders (e.g. 0xcaaa…008a) — not deployed ERC-20 contracts. */
export function isDeterministicPlaceholderAddress(address: string): boolean {
const normalized = address.toLowerCase();
return /^0x[a-f0-9]{4}0{24,}[a-f0-9]{1,8}$/.test(normalized);
}

View File

@@ -0,0 +1,28 @@
import { describe, expect, it } from 'vitest';
import { toWalletAddEthereumChainParams } from './wallet-add-ethereum-chain';
describe('toWalletAddEthereumChainParams', () => {
it('strips MetaMask-unsupported keys', () => {
const out = toWalletAddEthereumChainParams({
chainId: '0x8a',
chainIdDecimal: 138,
chainName: 'DeFi Oracle Meta Mainnet',
rpcUrls: ['https://rpc-http-pub.d-bis.org', 'wss://rpc-ws-pub.d-bis.org'],
nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 },
blockExplorerUrls: ['https://explorer.d-bis.org'],
iconUrls: ['https://explorer.d-bis.org/favicon.ico'],
oracles: [{ name: 'ETH/USD', address: '0x3304b747e565a97ec8ac220b0b6a1f6ffdb837e6' }],
} as never);
expect(out).toEqual({
chainId: '0x8a',
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://explorer.d-bis.org/favicon.ico'],
});
expect(out).not.toHaveProperty('chainIdDecimal');
expect(out).not.toHaveProperty('oracles');
});
});

View File

@@ -0,0 +1,38 @@
import type { NetworkEntry } from '../config/networks';
/** EIP-3085 fields accepted by MetaMask `wallet_addEthereumChain` (strict subset). */
export type WalletAddEthereumChainParams = {
chainId: string;
chainName: string;
rpcUrls: string[];
nativeCurrency: { name: string; symbol: string; decimals: number };
blockExplorerUrls?: string[];
iconUrls?: string[];
};
function httpsOnly(urls: string[] | undefined): string[] | undefined {
if (!urls?.length) return undefined;
const filtered = urls.filter((url) => typeof url === 'string' && url.startsWith('https://'));
return filtered.length > 0 ? filtered : undefined;
}
/** Strip `chainIdDecimal`, `oracles`, and other keys MetaMask rejects. */
export function toWalletAddEthereumChainParams(
network: Pick<
NetworkEntry,
'chainId' | 'chainName' | 'rpcUrls' | 'nativeCurrency' | 'blockExplorerUrls' | 'iconUrls'
>
): WalletAddEthereumChainParams {
const rpcUrls = httpsOnly(network.rpcUrls) ?? ['https://rpc-http-pub.d-bis.org'];
const out: WalletAddEthereumChainParams = {
chainId: network.chainId,
chainName: network.chainName,
rpcUrls,
nativeCurrency: network.nativeCurrency,
};
const blockExplorerUrls = httpsOnly(network.blockExplorerUrls);
if (blockExplorerUrls) out.blockExplorerUrls = blockExplorerUrls;
const iconUrls = httpsOnly(network.iconUrls);
if (iconUrls) out.iconUrls = iconUrls;
return out;
}

View File

@@ -0,0 +1,57 @@
import {
isWalletWatchAssetEligible,
isWalletWatchEligibleTokenAddress,
dedupeWatchAssetEntriesBySymbol,
} from './wallet-watch-eligible';
describe('wallet-watch-eligible', () => {
it('excludes deterministic placeholder addresses', () => {
expect(isWalletWatchEligibleTokenAddress('0xcaaa00000000000000000000000000000000008a')).toBe(false);
expect(isWalletWatchEligibleTokenAddress('0x93E66202A11B1772E55407B32B44e5Cd8eda7f22')).toBe(true);
});
it('excludes gas-native placeholder specs on Chain 138', () => {
expect(
isWalletWatchAssetEligible(
{ symbol: 'cAVAX', addresses: { 138: '0xcaaa00000000000000000000000000000000008a' } },
138,
),
).toBe(false);
});
it('excludes staged V2 family deployments that would collide in MetaMask', () => {
expect(
isWalletWatchAssetEligible(
{
symbol: 'cUSDC_V2',
familySymbol: 'cUSDC',
deploymentVersion: 'v2',
deploymentStatus: 'staged',
addresses: { 138: '0x219522c60e83dEe01FC5b0329d6fA8fD84b9D13d' },
},
138,
),
).toBe(false);
expect(
isWalletWatchAssetEligible(
{ symbol: 'cUSDC', addresses: { 138: '0xf22258f57794CC8E06237084b353Ab30fFfa640b' } },
138,
),
).toBe(true);
});
it('dedupes watchAsset rows by wallet symbol', () => {
const deduped = dedupeWatchAssetEntriesBySymbol([
{
options: { symbol: 'cUSDC', address: '0xf22258f57794CC8E06237084b353Ab30fFfa640b' },
metadata: { catalogSymbol: 'cUSDC' },
},
{
options: { symbol: 'cUSDC', address: '0x219522c60e83dEe01FC5b0329d6fA8fD84b9D13d' },
metadata: { catalogSymbol: 'cUSDC_V2', deploymentVersion: 'v2', deploymentStatus: 'staged' },
},
]);
expect(deduped).toHaveLength(1);
expect(deduped[0]?.options.address).toBe('0xf22258f57794CC8E06237084b353Ab30fFfa640b');
});
});

View File

@@ -0,0 +1,68 @@
import { isDeterministicPlaceholderAddress } from './deterministic-placeholder-address';
const HUB_COMPLIANT_WATCH_SYMBOLS = new Set(['cUSDC', 'cUSDT']);
const HUB_COMPLIANT_WATCH_CHAIN_IDS = new Set([138, 651940]);
export type WalletWatchAssetSpec = {
symbol: string;
addresses: Partial<Record<number, string>>;
deploymentStatus?: string;
deploymentVersion?: string;
familySymbol?: string;
};
export function isStagedWalletWatchAssetSpec(spec: WalletWatchAssetSpec): boolean {
const status = String(spec.deploymentStatus || '').trim().toLowerCase();
const version = String(spec.deploymentVersion || '').trim().toLowerCase();
return status === 'staged' || (version === 'v2' && Boolean(spec.familySymbol));
}
export function isWalletWatchAssetEligible(spec: WalletWatchAssetSpec, chainId: number): boolean {
if (HUB_COMPLIANT_WATCH_SYMBOLS.has(spec.symbol) && !HUB_COMPLIANT_WATCH_CHAIN_IDS.has(chainId)) {
return false;
}
if (isStagedWalletWatchAssetSpec(spec)) {
return false;
}
const address = spec.addresses[chainId];
if (address && isDeterministicPlaceholderAddress(address)) {
return false;
}
return true;
}
export function isWalletWatchEligibleTokenAddress(address: string | undefined | null): boolean {
if (!address) return false;
return !isDeterministicPlaceholderAddress(address);
}
export type WalletWatchAssetEntryLike = {
options: { symbol: string; address: string };
metadata?: {
deploymentStatus?: string;
deploymentVersion?: string;
catalogSymbol?: string;
};
};
/** MetaMask keeps one custom token row per symbol — keep the live deployment when duplicates exist. */
export function dedupeWatchAssetEntriesBySymbol<T extends WalletWatchAssetEntryLike>(entries: T[]): T[] {
const rank = (entry: T): number => {
let score = 0;
const status = String(entry.metadata?.deploymentStatus || '').toLowerCase();
const version = String(entry.metadata?.deploymentVersion || '').toLowerCase();
if (status === 'staged') score += 100;
if (version === 'v2') score += 50;
return score;
};
const bySymbol = new Map<string, T>();
for (const entry of entries) {
const key = entry.options.symbol.trim().toLowerCase();
const existing = bySymbol.get(key);
if (!existing || rank(entry) < rank(existing)) {
bySymbol.set(key, entry);
}
}
return Array.from(bySymbol.values());
}

View File

@@ -0,0 +1,63 @@
import { CHAIN_CONFIGS } from '../config/chains';
import { logger } from '../utils/logger';
export interface BlockscoutTokenMeta {
address: string;
name?: string;
symbol?: string;
decimals: number;
totalSupply?: string;
exchangeRate?: number;
}
function parseExchangeRate(value: unknown): number | undefined {
if (value == null) return undefined;
const numeric = typeof value === 'number' ? value : Number(value);
return Number.isFinite(numeric) && numeric > 0 ? numeric : undefined;
}
export async function fetchBlockscoutTokenMeta(
chainId: number,
address: string,
): Promise<BlockscoutTokenMeta | null> {
const explorerUrl = CHAIN_CONFIGS[chainId]?.explorerUrl?.replace(/\/$/, '');
if (!explorerUrl) {
return null;
}
const normalized = address.toLowerCase();
try {
const response = await fetch(`${explorerUrl}/api/v2/tokens/${normalized}`, {
headers: { Accept: 'application/json' },
signal: AbortSignal.timeout(8000),
});
if (!response.ok) {
return null;
}
const raw = (await response.json()) as {
address?: string;
name?: string | null;
symbol?: string | null;
decimals?: string | number | null;
total_supply?: string | null;
exchange_rate?: string | number | null;
};
if (!raw.address) {
return null;
}
return {
address: raw.address.toLowerCase(),
name: raw.name || undefined,
symbol: raw.symbol || undefined,
decimals: Number(raw.decimals ?? 18),
totalSupply: raw.total_supply || undefined,
exchangeRate: parseExchangeRate(raw.exchange_rate),
};
} catch (error) {
logger.warn('Blockscout token meta fetch failed', { chainId, address: normalized, error });
return null;
}
}

View File

@@ -1,5 +1,10 @@
import { getCanonicalTokenBySymbol } from '../config/canonical-tokens';
import { getCanonicalPriceUsd, resolveCanonicalPriceUsd, resolveCanonicalPriceUsdForSpec } from './canonical-price-oracle';
import {
getCanonicalPriceUsd,
primeLiveReferencePriceUsd,
resolveCanonicalPriceUsd,
resolveCanonicalPriceUsdForSpec,
} from './canonical-price-oracle';
describe('canonical-price-oracle', () => {
it('pegs Chain 138 USD-family mirrors to one dollar', () => {
@@ -75,13 +80,25 @@ describe('canonical-price-oracle', () => {
});
it('resolves WETH9 and WETH10 to the ETH peg with 18-decimal canonical metadata', () => {
expect(resolveCanonicalPriceUsd(138, '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2')).toMatchObject({
priceUsd: 2490,
const weth = resolveCanonicalPriceUsd(138, '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2');
expect(weth).toMatchObject({
referenceSymbol: 'ETH',
});
expect(resolveCanonicalPriceUsd(138, '0xf4BB2e28688e89fCcE3c0580D37d36A7672E8A9f')).toMatchObject({
priceUsd: 2490,
expect(weth.priceUsd).toBeGreaterThan(0);
expect(['env', 'on-chain', 'repo-fallback']).toContain(weth.source);
const weth10 = resolveCanonicalPriceUsd(138, '0xf4BB2e28688e89fCcE3c0580D37d36A7672E8A9f');
expect(weth10.referenceSymbol).toBe('ETH');
expect(weth10.priceUsd).toBeGreaterThan(0);
});
it('prefers live on-chain ETH over env when drift exceeds threshold on chain 138', () => {
process.env.CHAIN138_CANONICAL_PRICE_USD_ETH = '2100';
primeLiveReferencePriceUsd('ETH', 1772);
expect(resolveCanonicalPriceUsd(138, '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2')).toMatchObject({
priceUsd: 1772,
referenceSymbol: 'ETH',
source: 'on-chain',
});
});

View File

@@ -3,10 +3,49 @@ import { type CanonicalTokenSpec, getCanonicalTokenByAddress, getCanonicalTokenB
export interface CanonicalPriceResolution {
priceUsd?: number;
referenceSymbol?: string;
source: 'env' | 'repo-fallback' | 'unresolved';
source: 'env' | 'on-chain' | 'repo-fallback' | 'unresolved';
}
const FX_SNAPSHOT_GENERATED_AT = '2026-04-15';
const CHAIN_138 = 138;
/** Max bps drift before live Chain 138 ETH/USD aggregator beats env canonical (default 3%). */
export function readChain138EthOracleDriftBps(): number {
const raw = process.env.TOKEN_AGG_CHAIN138_ETH_ORACLE_DRIFT_BPS;
const n = raw ? parseInt(raw, 10) : NaN;
return Number.isFinite(n) && n >= 0 ? n : 300;
}
export function ethUsdOracleDriftBps(indexerPrice: number, oraclePrice: number): number {
if (!(oraclePrice > 0) || !(indexerPrice > 0)) {
return 0;
}
return (Math.abs(indexerPrice - oraclePrice) / oraclePrice) * 10000;
}
const liveReferencePrices = new Map<string, { priceUsd: number; expiresAt: number }>();
export function primeLiveReferencePriceUsd(
referenceSymbol: string,
priceUsd: number | undefined,
ttlMs = 120_000,
): void {
if (!priceUsd || !(priceUsd > 0)) {
return;
}
liveReferencePrices.set(referenceSymbol.toUpperCase(), {
priceUsd,
expiresAt: Date.now() + ttlMs,
});
}
function readLiveReferencePriceUsd(referenceSymbol: string): number | undefined {
const row = liveReferencePrices.get(referenceSymbol.toUpperCase());
if (!row || row.expiresAt <= Date.now()) {
return undefined;
}
return row.priceUsd;
}
// Repo-local inferred FX snapshot from scripts/lib/extraction_gap_closure.py.
const REPO_FALLBACK_PRICE_USD: Record<string, number> = {
@@ -18,14 +57,19 @@ const REPO_FALLBACK_PRICE_USD: Record<string, number> = {
CHF: 1.2776572668112798,
JPY: 0.006285683794888213,
XAU: 5163.3401260328355,
ETH: 2490,
ETH: 1680,
BTC: 90000,
LINK: 8,
BNB: 610,
POL: 0.78,
AVAX: 48,
CELO: 0.72,
CRO: 0.14,
XDAI: 1,
LIPMG: 1100,
LIBMG1: 4.25,
LIBMG2: 18.5,
LIBMG3: 0.12,
};
function normalizeAddress(value: string): string {
@@ -68,6 +112,11 @@ function resolveReferenceSymbol(spec: CanonicalTokenSpec): string | undefined {
return 'XAU';
}
if (symbol === 'LIPMG') return 'LIPMG';
if (symbol === 'LIBMG1') return 'LIBMG1';
if (symbol === 'LIBMG2') return 'LIBMG2';
if (symbol === 'LIBMG3') return 'LIBMG3';
if (currencyCode) {
return currencyCode;
}
@@ -161,6 +210,15 @@ export function resolveCanonicalPriceUsdForSpec(spec: CanonicalTokenSpec): Canon
};
}
const livePrice = readLiveReferencePriceUsd(referenceSymbol);
if (livePrice !== undefined) {
return {
priceUsd: livePrice,
referenceSymbol,
source: 'on-chain',
};
}
const fallback = REPO_FALLBACK_PRICE_USD[referenceSymbol];
if (fallback !== undefined) {
return {
@@ -181,7 +239,28 @@ export function resolveCanonicalPriceUsd(chainId: number, address: string): Cano
if (!spec) {
return { source: 'unresolved' };
}
return resolveCanonicalPriceUsdForSpec(spec);
const base = resolveCanonicalPriceUsdForSpec(spec);
if (chainId !== CHAIN_138 || base.referenceSymbol !== 'ETH') {
return base;
}
const liveEth = readLiveReferencePriceUsd('ETH');
if (liveEth === undefined) {
return base;
}
if (base.source === 'env' && base.priceUsd !== undefined) {
if (ethUsdOracleDriftBps(base.priceUsd, liveEth) > readChain138EthOracleDriftBps()) {
return { priceUsd: liveEth, referenceSymbol: 'ETH', source: 'on-chain' };
}
return base;
}
if (base.source === 'repo-fallback' || base.source === 'unresolved' || base.priceUsd === undefined) {
return { priceUsd: liveEth, referenceSymbol: 'ETH', source: 'on-chain' };
}
return base;
}
export function getCanonicalPriceUsd(chainId: number, address: string): number | undefined {

View File

@@ -0,0 +1,94 @@
import { ethers } from 'ethers';
import { primeLiveReferencePriceUsd } from './canonical-price-oracle';
const ROUND_DATA_ABI = [
'function latestRoundData() view returns (uint80 roundId,int256 answer,uint256 startedAt,uint256 updatedAt,uint80 answeredInRound)',
];
const DEFAULT_RPC = 'http://192.168.11.211:8545';
const DEFAULT_AGGREGATOR = '0x99b3511a2d315a497c8112c1fdd8d508d4b1e506';
const DEFAULT_PROXY = '0x3304b747e565a97ec8ac220b0b6a1f6ffdb837e6';
interface CachedPrice {
priceUsd: number;
expiresAt: number;
source: 'aggregator' | 'proxy';
}
let cachedEthUsd: CachedPrice | undefined;
function readAggregatorAddress(): string {
return (
process.env.AGGREGATOR_ADDRESS?.trim() ||
process.env.ORACLE_AGGREGATOR_ADDRESS?.trim() ||
DEFAULT_AGGREGATOR
).toLowerCase();
}
function readProxyAddress(): string {
return (
process.env.ORACLE_PROXY_ADDRESS?.trim() ||
process.env.ORACLE_PROXY?.trim() ||
DEFAULT_PROXY
).toLowerCase();
}
function readRpcUrl(): string {
return (
process.env.RPC_URL_138?.trim() ||
process.env.CHAIN138_RPC_URL?.trim() ||
DEFAULT_RPC
);
}
async function readEthUsdFromContract(address: string, rpcUrl: string): Promise<number | undefined> {
const provider = new ethers.JsonRpcProvider(rpcUrl);
const contract = new ethers.Contract(address, ROUND_DATA_ABI, provider);
const result = await contract.latestRoundData();
const answer = Number(result.answer ?? result[1]);
if (!Number.isFinite(answer) || answer <= 0) {
return undefined;
}
return answer / 1e8;
}
/**
* Live ETH/USD from Chain 138 on-chain feeds (aggregator preferred, proxy fallback).
*/
export async function getChain138EthUsdOnChain(): Promise<{ priceUsd: number; source: 'aggregator' | 'proxy' } | undefined> {
if (cachedEthUsd && cachedEthUsd.expiresAt > Date.now()) {
return { priceUsd: cachedEthUsd.priceUsd, source: cachedEthUsd.source };
}
const rpcUrl = readRpcUrl();
const aggregator = readAggregatorAddress();
const proxy = readProxyAddress();
for (const [address, source] of [[aggregator, 'aggregator'], [proxy, 'proxy']] as const) {
try {
const priceUsd = await readEthUsdFromContract(address, rpcUrl);
if (priceUsd != null && priceUsd > 0) {
cachedEthUsd = { priceUsd, expiresAt: Date.now() + 60_000, source };
return { priceUsd, source };
}
} catch {
// try next feed
}
}
return undefined;
}
export function clearChain138OnChainPriceCache(): void {
cachedEthUsd = undefined;
}
export async function refreshChain138LiveReferencePrices(chainId: number): Promise<void> {
if (chainId !== 138) {
return;
}
const eth = await getChain138EthUsdOnChain();
if (eth) {
primeLiveReferencePriceUsd('ETH', eth.priceUsd);
}
}

View File

@@ -18,7 +18,10 @@ const DEFAULT_BALANCER_WETH_USDT_POOL_ID = '0x877cd220759e8c94b82f55450c85d382ae
const DEFAULT_BALANCER_WETH_USDC_POOL_ID = '0xd8dfb18a6baf9b29d8c2dbd74639db87ac558af120df5261dab8e2a5de69013b';
const DEFAULT_PILOT_UNISWAP_FEE = 3000;
const DEFAULT_NATIVE_UNISWAP_FEE = 500;
const DEFAULT_WETH_USD_PRICE = 2100;
const DEFAULT_WETH_USD_PRICE = (() => {
const fromEnv = Number(process.env.CHAIN138_CANONICAL_PRICE_USD_ETH || process.env.CANONICAL_PRICE_USD_ETH || '');
return Number.isFinite(fromEnv) && fromEnv > 0 ? fromEnv : 1732;
})();
const WETH = '0xc02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2'.toLowerCase();
const USDT = '0x004b63A7B5b0E06f6bB6adb4a5F9f590BF3182D1'.toLowerCase();

View File

@@ -55,7 +55,11 @@ function getPilotConfig(): DodoV3PilotConfig {
proxyAddress: normalizeAddress(process.env.CHAIN138_D3_PROXY_ADDRESS || DEFAULT_D3_PROXY),
weth10Address: normalizeAddress(process.env.CHAIN138_D3_WETH10_ADDRESS || process.env.WETH10_ADDRESS || DEFAULT_WETH10),
usdtAddress: normalizeAddress(process.env.CHAIN138_D3_USDT_ADDRESS || process.env.USDT_ADDRESS_138 || DEFAULT_USDT),
wethUsdPrice: normalizeNumber(process.env.CHAIN138_D3_PILOT_WETH_USD)?.valueOf() || 2100,
wethUsdPrice:
normalizeNumber(process.env.CHAIN138_D3_PILOT_WETH_USD)?.valueOf() ||
normalizeNumber(process.env.CHAIN138_CANONICAL_PRICE_USD_ETH)?.valueOf() ||
normalizeNumber(process.env.CANONICAL_PRICE_USD_ETH)?.valueOf() ||
1732,
liquidityOverrideUsd: normalizeNumber(process.env.CHAIN138_D3_PILOT_TOTAL_LIQUIDITY_USD),
};
}

View File

@@ -0,0 +1,140 @@
import axios from 'axios';
import { logger } from '../utils/logger';
import { primeLiveReferencePriceUsd } from './canonical-price-oracle';
const FRANKFURTER_CURRENCIES = ['EUR', 'GBP', 'AUD', 'CAD', 'CHF', 'JPY'] as const;
const COINGECKO_REFERENCE_IDS: Record<string, string> = {
ETH: 'ethereum',
BTC: 'bitcoin',
LINK: 'chainlink',
XAU: 'pax-gold',
};
const COINGECKO_COMMODITY_IDS = {
platinum: 'platinum',
palladium: 'palladium',
copper: 'copper',
nickel: 'nickel',
cobalt: 'cobalt',
} as const;
let lastRefreshAt = 0;
let refreshInFlight: Promise<void> | null = null;
const DEFAULT_TTL_MS = 5 * 60 * 1000;
function coingeckoBaseUrl(): string {
return process.env.COINGECKO_API_KEY
? 'https://pro-api.coingecko.com/api/v3'
: 'https://api.coingecko.com/api/v3';
}
function coingeckoHeaders(): Record<string, string> {
const key = process.env.COINGECKO_API_KEY?.trim();
return key ? { 'x-cg-pro-api-key': key } : {};
}
async function fetchFrankfurterUsdPerUnit(currency: string): Promise<number | undefined> {
try {
const response = await axios.get<{ rates?: { USD?: number } }>(
`https://api.frankfurter.app/latest?from=${encodeURIComponent(currency)}&to=USD`,
{ timeout: 8000 },
);
const usd = response.data?.rates?.USD;
if (usd !== undefined && Number.isFinite(usd) && usd > 0) {
return usd;
}
} catch (error) {
logger.warn(`Frankfurter FX fetch failed for ${currency}:`, error);
}
return undefined;
}
async function fetchCoingeckoUsd(coinId: string): Promise<number | undefined> {
try {
const response = await axios.get<Record<string, { usd?: number }>>(
`${coingeckoBaseUrl()}/simple/price`,
{
timeout: 8000,
headers: coingeckoHeaders(),
params: {
ids: coinId,
vs_currencies: 'usd',
},
},
);
const usd = response.data?.[coinId]?.usd;
if (usd !== undefined && Number.isFinite(usd) && usd > 0) {
return usd;
}
} catch (error) {
logger.warn(`CoinGecko FX fetch failed for ${coinId}:`, error);
}
return undefined;
}
/** Refresh live ISO-4217 USD reference rates used for c* / cW* valuation. */
export async function refreshIso4217FxRates(ttlMs = DEFAULT_TTL_MS): Promise<void> {
primeLiveReferencePriceUsd('USD', 1, ttlMs);
const commodityEntries = await Promise.all(
Object.entries(COINGECKO_COMMODITY_IDS).map(async ([key, coinId]) => {
const priceUsd = await fetchCoingeckoUsd(coinId);
return [key, priceUsd] as const;
}),
);
const commodities = Object.fromEntries(commodityEntries.filter(([, price]) => price != null)) as Partial<
Record<keyof typeof COINGECKO_COMMODITY_IDS, number>
>;
await Promise.all([
...FRANKFURTER_CURRENCIES.map(async (currency) => {
const priceUsd = await fetchFrankfurterUsdPerUnit(currency);
if (priceUsd !== undefined) {
primeLiveReferencePriceUsd(currency, priceUsd, ttlMs);
}
}),
...Object.entries(COINGECKO_REFERENCE_IDS).map(async ([symbol, coinId]) => {
const priceUsd = await fetchCoingeckoUsd(coinId);
if (priceUsd !== undefined) {
primeLiveReferencePriceUsd(symbol, priceUsd, ttlMs);
}
}),
]);
if (commodities.platinum != null && commodities.palladium != null) {
primeLiveReferencePriceUsd('LIPMG', (commodities.platinum + commodities.palladium) / 2, ttlMs);
}
if (commodities.copper != null) {
primeLiveReferencePriceUsd('LIBMG1', commodities.copper, ttlMs);
}
if (commodities.nickel != null && commodities.cobalt != null) {
primeLiveReferencePriceUsd('LIBMG2', commodities.nickel * 0.8 + commodities.cobalt * 0.2, ttlMs);
} else if (commodities.nickel != null) {
primeLiveReferencePriceUsd('LIBMG2', commodities.nickel, ttlMs);
}
lastRefreshAt = Date.now();
}
export async function ensureIso4217FxPrimed(ttlMs = DEFAULT_TTL_MS): Promise<void> {
if (Date.now() - lastRefreshAt < ttlMs) {
return;
}
if (!refreshInFlight) {
refreshInFlight = refreshIso4217FxRates(ttlMs).finally(() => {
refreshInFlight = null;
});
}
await refreshInFlight;
}
export function startIso4217FxScheduler(intervalMs = DEFAULT_TTL_MS): NodeJS.Timeout {
void refreshIso4217FxRates(intervalMs);
return setInterval(() => {
void refreshIso4217FxRates(intervalMs);
}, intervalMs);
}
export function getIso4217FxLastRefreshAt(): number {
return lastRefreshAt;
}

View File

@@ -0,0 +1,118 @@
import { getCanonicalTokenByAddress } from '../config/canonical-tokens';
import { MarketDataRepository } from '../database/repositories/market-data-repo';
import { getChain138EthUsdOnChain } from './chain138-onchain-reference-prices';
import { resolveCanonicalPriceUsd } from './canonical-price-oracle';
import { ensureIso4217FxPrimed } from './iso4217-fx-refresh';
const marketDataRepo = new MarketDataRepository();
export type Iso4217SpotPrice = {
priceUsd?: number;
symbol?: string;
currencyCode?: string;
referenceSymbol?: string;
source: 'market-data' | 'iso4217-oracle' | 'native-oracle' | 'unresolved';
};
const NATIVE_ETH_SENTINELS = new Set([
'0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee',
'0x0000000000000000000000000000000000000000',
]);
const CHAIN138_ETH_USD_ORACLE_PROXY = '0x3304b747e565a97ec8ac220b0b6a1f6ffdb837e6';
const CHAIN138_ETH_USD_ORACLE_AGGREGATOR = '0x99b3511a2d315a497c8112c1fdd8d508d4b1e506';
const CHAIN138_WETH9 = '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2';
function isChain138EthUsdPegToken(chainId: number, normalized: string, symbol?: string): boolean {
if (chainId !== 138) return false;
if (normalized === CHAIN138_WETH9) return true;
const sym = symbol?.toUpperCase();
return sym === 'WETH' || sym === 'WETH9' || sym === 'WETH10' || sym === 'CETH' || sym === 'CETHL2';
}
async function resolveEthUsdFeedPrice(chainId: number, normalized: string, symbol?: string): Promise<Iso4217SpotPrice | undefined> {
if (chainId !== 138) return undefined;
const isFeed =
normalized === CHAIN138_ETH_USD_ORACLE_PROXY ||
normalized === CHAIN138_ETH_USD_ORACLE_AGGREGATOR ||
symbol?.toUpperCase() === 'ETH-USD';
if (!isFeed) return undefined;
const native = await getChain138EthUsdOnChain();
if (native?.priceUsd && native.priceUsd > 0) {
return {
priceUsd: native.priceUsd,
symbol: 'ETH-USD',
currencyCode: 'USD',
referenceSymbol: 'ETH',
source: 'native-oracle',
};
}
return undefined;
}
export async function resolveIso4217SpotPrice(chainId: number, address: string): Promise<Iso4217SpotPrice> {
await ensureIso4217FxPrimed();
const normalized = address.trim().toLowerCase();
if (NATIVE_ETH_SENTINELS.has(normalized)) {
const native = chainId === 138 ? await getChain138EthUsdOnChain() : undefined;
if (native?.priceUsd && native.priceUsd > 0) {
return {
priceUsd: native.priceUsd,
symbol: 'ETH',
currencyCode: 'ETH',
referenceSymbol: 'ETH',
source: 'native-oracle',
};
}
const ethFallback = resolveCanonicalPriceUsd(chainId, normalized);
return {
priceUsd: ethFallback.priceUsd,
symbol: 'ETH',
currencyCode: 'ETH',
referenceSymbol: 'ETH',
source: ethFallback.priceUsd ? 'iso4217-oracle' : 'unresolved',
};
}
const spec = getCanonicalTokenByAddress(chainId, normalized);
const feedPrice = await resolveEthUsdFeedPrice(chainId, normalized, spec?.symbol);
if (feedPrice) {
return feedPrice;
}
if (isChain138EthUsdPegToken(chainId, normalized, spec?.symbol)) {
const native = await getChain138EthUsdOnChain();
if (native?.priceUsd && native.priceUsd > 0) {
return {
priceUsd: native.priceUsd,
symbol: spec?.symbol ?? 'WETH',
currencyCode: spec?.currencyCode ?? 'ETH',
referenceSymbol: 'ETH',
source: 'native-oracle',
};
}
}
const marketData = await marketDataRepo.getMarketData(chainId, normalized);
const canonical = resolveCanonicalPriceUsd(chainId, normalized);
const priceUsd =
marketData?.priceUsd != null && marketData.priceUsd > 0
? marketData.priceUsd
: canonical.priceUsd;
return {
priceUsd,
symbol: spec?.symbol,
currencyCode: spec?.currencyCode,
referenceSymbol: canonical.referenceSymbol,
source:
marketData?.priceUsd != null && marketData.priceUsd > 0
? 'market-data'
: canonical.priceUsd != null
? 'iso4217-oracle'
: 'unresolved',
};
}

View File

@@ -0,0 +1,21 @@
import { buildIso4217TokenListExtensions, resolveCoingeckoPriceProxyId } from './iso4217-token-metadata';
describe('iso4217-token-metadata', () => {
it('maps cUSDC to USD Cash proxy and cUSDT to USD Treasury proxy', () => {
expect(resolveCoingeckoPriceProxyId({ symbol: 'cUSDC', currencyCode: 'USD' })).toBe('usd-coin');
expect(resolveCoingeckoPriceProxyId({ symbol: 'cUSDT', currencyCode: 'USD' })).toBe('tether');
});
it('builds ISO-4217 extensions for compliant fiat tokens', () => {
const extensions = buildIso4217TokenListExtensions({
symbol: 'cAUDC',
currencyCode: 'AUD',
type: 'base',
name: 'Australian Dollar (Compliant)',
});
expect(extensions.iso4217).toBe('AUD');
expect(extensions.gruAssetClass).toBe('C');
expect(extensions.gruLayer).toBe('c');
});
});

View File

@@ -0,0 +1,62 @@
import { type CanonicalTokenSpec } from '../config/canonical-tokens';
/** CoinGecko IDs used only as USD valuation proxies for ISO-4217 c* eMoney (not issuer identity). */
/** USD valuation proxies for CoinGecko / MetaMask submissions — not issuer identity. */
const ISO4217_COINGECKO_PROXY: Record<string, string> = {
USD: 'usd-coin',
EUR: 'euro-coin',
XAU: 'pax-gold',
ETH: 'ethereum',
BTC: 'bitcoin',
LINK: 'chainlink',
};
const USD_TREASURY_SYMBOLS = new Set(['CUSDT', 'CEURT', 'CGBPT', 'CXAUT']);
export function resolveIso4217AssetClassSuffix(symbol: string): 'C' | 'T' | undefined {
const upper = symbol.trim().toUpperCase();
if (upper.length < 2) return undefined;
const suffix = upper.at(-1);
if (suffix === 'C' || suffix === 'T') return suffix;
return undefined;
}
export function resolveCoingeckoPriceProxyId(spec: Pick<CanonicalTokenSpec, 'symbol' | 'currencyCode'>): string | undefined {
const currency = String(spec.currencyCode || '').trim().toUpperCase();
if (currency && ISO4217_COINGECKO_PROXY[currency]) {
const upper = spec.symbol.trim().toUpperCase();
if (currency === 'USD' && USD_TREASURY_SYMBOLS.has(upper)) {
return 'tether';
}
return ISO4217_COINGECKO_PROXY[currency];
}
if (spec.symbol.trim().toUpperCase() === 'LINK') return ISO4217_COINGECKO_PROXY.LINK;
if (spec.symbol.trim().toUpperCase().includes('BTC')) return ISO4217_COINGECKO_PROXY.BTC;
if (spec.symbol.trim().toUpperCase().includes('ETH')) return ISO4217_COINGECKO_PROXY.ETH;
return undefined;
}
export function buildIso4217TokenListExtensions(
spec: Pick<CanonicalTokenSpec, 'symbol' | 'currencyCode' | 'type' | 'name'>,
): Record<string, unknown> {
const currencyCode = String(spec.currencyCode || '').trim().toUpperCase() || undefined;
const assetClass = resolveIso4217AssetClassSuffix(spec.symbol);
const coingeckoPriceProxyId = resolveCoingeckoPriceProxyId(spec);
return {
gruLayer: spec.type === 'w' ? 'cW' : 'c',
iso4217: currencyCode,
gruAssetClass: assetClass,
gruAssetClassLabel:
assetClass === 'C'
? 'Cash (Tokenized M1)'
: assetClass === 'T'
? 'Treasury (Tokenized GILT or equivalent security)'
: undefined,
coingeckoPriceProxyId,
pricingNote:
'Native GRU Compliant eMoney — CoinGecko proxy is for USD display only, not third-party issuer identity.',
};
}

View File

@@ -15,7 +15,7 @@ export function getOmnlApiCatalog(): {
return {
service: 'HYBX OMNL',
basePath: '/api/v1',
version: '1.4.0',
version: '1.5.0',
endpoints: [
{ method: 'GET', path: '/omnl/openapi.json', description: 'OpenAPI 3.0 JSON (Swagger-compatible)', auth: 'none' },
{ method: 'GET', path: '/omnl/catalog', description: 'This catalog (machine-readable)', auth: 'none' },
@@ -23,6 +23,8 @@ export function getOmnlApiCatalog(): {
{ method: 'GET', path: '/omnl/reconcile-anchor', description: 'SHA-256 of canonical IPSAS registry + journal matrix JSON', auth: 'OMNL_API_KEY when OMNL_REQUIRE_API_KEY=1' },
{ method: 'GET', path: '/omnl/reconcile/triple-state', description: 'Fineract + on-chain + custodian reconcile', query: ['lineId'], auth: 'OMNL_API_KEY' },
{ method: 'GET', path: '/omnl/disclosures/full', description: 'IFRS 7/9/13 + IAS 21/37 extracts', query: ['lineId'], auth: 'OMNL_API_KEY' },
{ method: 'GET', path: '/omnl/entities', description: 'Regulated entity registry (Fineract master data)', auth: 'OMNL_API_KEY' },
{ method: 'GET', path: '/omnl/entities/:clientNumber', description: 'Single entity by client number', auth: 'OMNL_API_KEY' },
{ method: 'POST', path: '/omnl/iso20022/messages', description: 'Store ISO 20022 message (10y retention)', auth: 'OMNL_API_KEY' },
{ method: 'GET', path: '/omnl/iso20022/messages', description: 'List stored ISO messages', auth: 'OMNL_API_KEY' },
{ method: 'GET', path: '/omnl/roles/matrix', description: 'Fineract SoD roles matrix JSON', auth: 'OMNL_API_KEY' },

View File

@@ -0,0 +1,53 @@
import { readFileSync, existsSync } from 'node:fs';
import { resolve } from 'node:path';
export type OmnlEntitySummary = {
clientNumber: number;
accountNo: string;
entityName: string;
lei: string;
source: 'fineract_master_data';
};
function proxmoxRoot(): string {
return (
process.env.PROXMOX_ROOT?.trim() ||
process.env.PHOENIX_REPO_ROOT?.trim() ||
resolve(process.cwd(), '../../../..')
);
}
export function listOmnlEntities(): {
source: string;
count: number;
entities: OmnlEntitySummary[];
} {
const path = resolve(
proxmoxRoot(),
'docs/04-configuration/mifos-omnl-central-bank/OMNL_ENTITY_MASTER_DATA.json'
);
if (!existsSync(path)) {
return { source: path, count: 0, entities: [] };
}
const raw = JSON.parse(readFileSync(path, 'utf8')) as {
entities?: Array<{
clientNumber: number;
accountNo: string;
entityName: string;
lei?: string;
}>;
};
const entities = (raw.entities ?? []).map((e) => ({
clientNumber: e.clientNumber,
accountNo: e.accountNo,
entityName: e.entityName,
lei: e.lei ?? '',
source: 'fineract_master_data' as const,
}));
return { source: path, count: entities.length, entities };
}
export function getOmnlEntity(clientNumber: number): OmnlEntitySummary | null {
const { entities } = listOmnlEntities();
return entities.find((e) => e.clientNumber === clientNumber) ?? null;
}

View File

@@ -0,0 +1,10 @@
import { PassthroughSchemaValidator } from '@dbis/integration-foundation';
const validator = new PassthroughSchemaValidator();
export async function validateIso20022Payload(
messageType: string,
payload: string
): Promise<{ valid: boolean; errors: string[] }> {
return validator.validate(payload, messageType);
}

View File

@@ -0,0 +1,25 @@
import { MAX_SINGLE_POOL_TVL_USD, sanePoolTvlUsd } from './token-visible-liquidity';
describe('sanePoolTvlUsd', () => {
it('keeps canonical Chain 138 live pool TVL even when very large', () => {
expect(
sanePoolTvlUsd(138, '0x9e89bae009adf128782e19e8341996c596ac40dc', 33_200_672),
).toBe(33_200_672);
});
it('drops mis-normalized single-pool TVL above the sanity cap', () => {
expect(
sanePoolTvlUsd(138, '0xaae68830a55767722618e869882c6ed064cc1eb2', 51_558_997_888),
).toBe(0);
});
it('allows reasonable non-canonical pool TVL', () => {
expect(
sanePoolTvlUsd(138, '0xb53a0508940b1ff90f1aad4f6cb50a7012fe5593', 10_104_786),
).toBe(10_104_786);
});
it('exports a 500M sanity cap', () => {
expect(MAX_SINGLE_POOL_TVL_USD).toBe(500_000_000);
});
});

View File

@@ -0,0 +1,213 @@
import { formatUnits } from 'ethers';
import { existsSync, readFileSync } from 'fs';
import path from 'path';
import { CHAIN138_CANONICAL_LIVE_POOLS, isChain138CanonicalLivePool } from '../config/chain138-live-dodo-pools';
import { filterPoolsForExposure } from '../config/gru-transport';
import { getGruV2DeploymentPoolRows } from '../config/gru-v2-deployment-pools';
import { resolveCanonicalQuoteAddress } from '../config/canonical-tokens';
import { PoolRepository, LiquidityPool } from '../database/repositories/pool-repo';
import { getLiveDodoPools } from './live-dodo-fallback';
import { pmmVaultReserveFromChain, resolvePmmQuoteRpcUrl } from './pmm-onchain-quote';
import { logger } from '../utils/logger';
const poolRepo = new PoolRepository();
const MAX_SINGLE_POOL_TVL_USD = 500_000_000;
const LIVE_UNISWAP_CATALOG_REL = 'config/live-uniswap-v2-pool-catalog.json';
interface LiveUniswapCatalogRow {
chainId: number;
poolAddress: string;
baseAddress: string;
quoteAddress: string;
totalLiquidityUsd?: number;
}
function loadLiveUniswapCatalogRows(): LiveUniswapCatalogRow[] {
const configured = process.env.TOKEN_AGGREGATION_LIVE_UNISWAP_V2_POOL_CATALOG_JSON?.trim();
const roots = [
configured,
path.join(process.cwd(), LIVE_UNISWAP_CATALOG_REL),
path.join(process.cwd(), 'smom-dbis-138', 'services', 'token-aggregation', LIVE_UNISWAP_CATALOG_REL),
].filter(Boolean) as string[];
for (const candidate of roots) {
if (!existsSync(candidate)) continue;
try {
const parsed = JSON.parse(readFileSync(candidate, 'utf8')) as { pools?: LiveUniswapCatalogRow[] };
return (parsed.pools ?? []).filter(
(row) =>
Number.isFinite(row.chainId) &&
row.poolAddress?.startsWith('0x') &&
row.baseAddress?.startsWith('0x') &&
row.quoteAddress?.startsWith('0x'),
);
} catch (error) {
logger.warn('token-visible-liquidity: unable to parse UniV2 catalog', { candidate, error });
}
}
return [];
}
function matchesTokenAddress(
pool: { token0Address: string; token1Address: string },
lookupAddress: string,
normalizedAddress: string,
): boolean {
return (
pool.token0Address === lookupAddress ||
pool.token1Address === lookupAddress ||
pool.token0Address === normalizedAddress ||
pool.token1Address === normalizedAddress
);
}
function sanePoolTvlUsd(chainId: number, poolAddress: string, totalLiquidityUsd?: number): number {
const tvl = Number(totalLiquidityUsd);
if (!Number.isFinite(tvl) || tvl <= 0) return 0;
if (chainId === 138 && isChain138CanonicalLivePool(poolAddress)) return tvl;
if (tvl > MAX_SINGLE_POOL_TVL_USD) return 0;
return tvl;
}
function mergePoolsByAddress(existing: Map<string, LiquidityPool>, pools: LiquidityPool[]): void {
for (const pool of pools) {
const key = pool.poolAddress.toLowerCase();
const prior = existing.get(key);
const nextTvl = sanePoolTvlUsd(pool.chainId, pool.poolAddress, pool.totalLiquidityUsd);
const priorTvl = prior ? sanePoolTvlUsd(prior.chainId, prior.poolAddress, prior.totalLiquidityUsd) : 0;
if (!prior || nextTvl > priorTvl) {
existing.set(key, { ...pool, totalLiquidityUsd: nextTvl });
}
}
}
async function collectVisiblePools(chainId: number, address: string): Promise<LiquidityPool[]> {
const normalized = address.toLowerCase();
const resolution = resolveCanonicalQuoteAddress(chainId, normalized);
const merged = new Map<string, LiquidityPool>();
try {
const dbPools = filterPoolsForExposure(
chainId,
await poolRepo.getPoolsByToken(chainId, resolution.lookupAddress),
).filter((pool) => matchesTokenAddress(pool, resolution.lookupAddress, normalized));
mergePoolsByAddress(merged, dbPools);
} catch (error) {
logger.warn('token-visible-liquidity: DB pool lookup failed', {
chainId,
address: resolution.lookupAddress,
error,
});
}
try {
const livePools = filterPoolsForExposure(chainId, await getLiveDodoPools(chainId)).filter((pool) =>
matchesTokenAddress(pool, resolution.lookupAddress, normalized),
);
mergePoolsByAddress(merged, livePools);
} catch (error) {
logger.warn('token-visible-liquidity: live DODO lookup failed', { chainId, address: normalized, error });
}
if (chainId === 138 && merged.size === 0) {
const canonicalOnly = filterPoolsForExposure(chainId, await getLiveDodoPools(chainId)).filter(
(pool) =>
CHAIN138_CANONICAL_LIVE_POOLS.some((live) => live.toLowerCase() === pool.poolAddress.toLowerCase()) &&
matchesTokenAddress(pool, resolution.lookupAddress, normalized),
);
mergePoolsByAddress(merged, canonicalOnly);
}
return [...merged.values()];
}
function sumLiveUniswapCatalogLiquidityUsd(chainId: number, tokenAddress: string): number {
const token = tokenAddress.toLowerCase();
return loadLiveUniswapCatalogRows()
.filter(
(row) =>
row.chainId === chainId &&
(row.baseAddress.toLowerCase() === token || row.quoteAddress.toLowerCase() === token),
)
.reduce((sum, row) => {
const tvl = sanePoolTvlUsd(chainId, row.poolAddress, row.totalLiquidityUsd);
return sum + tvl;
}, 0);
}
function sumPoolTvl(chainId: number, pools: LiquidityPool[]): number {
return pools.reduce(
(sum, pool) => sum + sanePoolTvlUsd(chainId, pool.poolAddress, pool.totalLiquidityUsd),
0,
);
}
function fallbackPoolUsdPrice(symbol?: string): number {
const normalized = String(symbol || '').toUpperCase();
if (normalized.includes('XAU')) return Number(process.env.TOKEN_AGGREGATION_XAU_USD || '0') || 0;
if (normalized === 'WETH' || normalized === 'ETH') return Number(process.env.TOKEN_AGGREGATION_ETH_USD || '0') || 0;
return 1;
}
function fallbackPoolDecimals(symbol?: string): number {
const normalized = String(symbol || '').toUpperCase();
if (normalized === 'WETH' || normalized === 'ETH' || normalized === 'LINK') return 18;
return 6;
}
async function sumGruV2DeploymentLiquidityUsd(chainId: number, tokenAddress: string): Promise<number> {
if (chainId !== 138) return 0;
const token = tokenAddress.toLowerCase();
const rpcUrl = resolvePmmQuoteRpcUrl();
if (!rpcUrl) return 0;
const rows = getGruV2DeploymentPoolRows().filter(
(pool) =>
pool.chainId === chainId &&
(pool.baseAddress.toLowerCase() === token || pool.quoteAddress.toLowerCase() === token),
);
let total = 0;
for (const row of rows) {
const reserves = await pmmVaultReserveFromChain({ rpcUrl, poolAddress: row.poolAddress });
if (!reserves) continue;
const baseUnits = Number(formatUnits(reserves.baseReserveRaw, fallbackPoolDecimals(row.baseSymbol)));
const quoteUnits = Number(formatUnits(reserves.quoteReserveRaw, fallbackPoolDecimals(row.quoteSymbol)));
if (!Number.isFinite(baseUnits) || !Number.isFinite(quoteUnits)) continue;
const reserveUsd =
baseUnits * fallbackPoolUsdPrice(row.baseSymbol) + quoteUnits * fallbackPoolUsdPrice(row.quoteSymbol);
const saneReserveUsd = sanePoolTvlUsd(chainId, row.poolAddress, reserveUsd);
if (Number.isFinite(saneReserveUsd) && saneReserveUsd > 0) {
total += saneReserveUsd;
}
}
return total;
}
/** Best-effort visible on-chain / indexed liquidity for wallet and market-batch surfaces. */
export async function resolveVisibleTokenLiquidityUsd(chainId: number, address: string): Promise<number> {
const normalized = address.toLowerCase();
const pools = await collectVisiblePools(chainId, normalized);
if (chainId === 138) {
const canonicalLiveTvl = pools
.filter((pool) => isChain138CanonicalLivePool(pool.poolAddress))
.reduce(
(sum, pool) => sum + sanePoolTvlUsd(chainId, pool.poolAddress, pool.totalLiquidityUsd),
0,
);
const gruTvl = await sumGruV2DeploymentLiquidityUsd(chainId, normalized);
const catalogTvl = sumLiveUniswapCatalogLiquidityUsd(chainId, normalized);
const canonicalFirst = Math.max(canonicalLiveTvl, gruTvl, catalogTvl);
if (canonicalFirst > 0) {
return canonicalFirst;
}
}
const indexedTvl = sumPoolTvl(chainId, pools);
const catalogTvl = sumLiveUniswapCatalogLiquidityUsd(chainId, normalized);
const gruTvl = await sumGruV2DeploymentLiquidityUsd(chainId, normalized);
return Math.max(indexedTvl, catalogTvl, gruTvl);
}
export { sanePoolTvlUsd, MAX_SINGLE_POOL_TVL_USD };

View File

@@ -4,6 +4,9 @@ import {
resolveUsdValuation,
mergeMarketWithValuation,
} from './valuation-precedence';
import { primeLiveReferencePriceUsd } from './canonical-price-oracle';
const CHAIN138_WETH = '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2';
/** Bridged `cW*` on Ethereum; on Chain 138 the native form is `cUSDC` (`c*`), not `cW*`. */
const ETH_MAINNET_CWUSDC = '0x2de5F116bFcE3d0f922d9C8351e0c5Fc24b9284a';
@@ -185,13 +188,14 @@ describe('valuation-precedence', () => {
it('skips fresh indexer when it only mirrors repo-fallback FX and CoinGecko is available', () => {
process.env.TOKEN_AGG_PRICE_INDEXER_MAX_AGE_SECONDS = '3600';
const lu = new Date();
const cxauc = '0x290e52a8819a4fbD0714E517225429aA2B70EC6b';
const v = resolveUsdValuation({
chainId: 138,
normalizedAddress: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2',
normalizedAddress: cxauc,
indexer: {
chainId: 138,
tokenAddress: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2',
priceUsd: 2490,
tokenAddress: cxauc,
priceUsd: 5163.3401260328355,
volume24h: 0,
volume7d: 0,
volume30d: 0,
@@ -207,6 +211,35 @@ describe('valuation-precedence', () => {
expect(v.stale).toBe(false);
});
it('prefers live Chain 138 native oracle over stale PMM indexer when ETH drifts', () => {
process.env.TOKEN_AGG_PRICE_INDEXER_MAX_AGE_SECONDS = '3600';
process.env.CHAIN138_CANONICAL_PRICE_USD_ETH = '2100';
primeLiveReferencePriceUsd('ETH', 1772);
const lu = new Date();
const v = resolveUsdValuation({
chainId: 138,
normalizedAddress: CHAIN138_WETH,
indexer: {
chainId: 138,
tokenAddress: CHAIN138_WETH,
priceUsd: 2100,
volume24h: 0,
volume7d: 0,
volume30d: 0,
liquidityUsd: 10_000_000,
holdersCount: 0,
transfers24h: 0,
lastUpdated: lu,
},
coingecko: { priceUsd: 1760, lastUpdated: new Date() },
});
expect(v.sourceLayer).toBe('native_oracle');
expect(v.priceUsd).toBe(1772);
expect(v.precedenceRank).toBe(0);
});
it('mergeMarketWithValuation applies priced layer onto indexer row', () => {
const pricing = resolveUsdValuation({
chainId: 138,

View File

@@ -2,7 +2,12 @@ import type { MarketData } from '../adapters/base-adapter';
import type { TokenMarketData } from '../database/repositories/token-repo';
import { getChainConfig } from '../config/chains';
import { getCanonicalTokenByAddress } from '../config/canonical-tokens';
import { resolveCanonicalPriceUsd, type CanonicalPriceResolution } from './canonical-price-oracle';
import {
ethUsdOracleDriftBps,
readChain138EthOracleDriftBps,
resolveCanonicalPriceUsd,
type CanonicalPriceResolution,
} from './canonical-price-oracle';
const CHAIN_138 = 138;
@@ -29,6 +34,7 @@ export function isGruV2CwMeshEdgeToken(chainId: number, normalizedAddress: strin
}
export type PriceSourceLayer =
| 'native_oracle'
| 'indexer_market'
| 'external_coingecko'
| 'external_coinmarketcap'
@@ -60,7 +66,7 @@ export interface TokenPricing {
ageSeconds?: number;
indexerLastUpdated?: string;
referenceSymbol?: string;
canonicalResolutionSource?: 'env' | 'repo-fallback' | 'unresolved';
canonicalResolutionSource?: 'env' | 'on-chain' | 'repo-fallback' | 'unresolved';
/** Bridged `cW*` on an edge chain used DexScreener-priority ordering (native `c*` on 138 never sets this). */
gruV2CwEdgeDexPriority?: boolean;
}
@@ -129,10 +135,34 @@ export function resolveUsdValuation(input: ValuationInput): TokenPricing {
const out: Candidate[] = [];
const onChainEthUsd =
chainId === CHAIN_138 &&
referenceSymbol === 'ETH' &&
canonical.source === 'on-chain' &&
canonical.priceUsd !== undefined
? canonical.priceUsd
: undefined;
if (onChainEthUsd !== undefined) {
out.push({
layer: 'native_oracle',
rank: 0,
priceUsd: onChainEthUsd,
stale: false,
asOf: new Date(),
});
}
if (indexer?.priceUsd !== undefined && indexer.priceUsd !== null) {
const lu = indexer.lastUpdated instanceof Date ? indexer.lastUpdated : new Date(indexer.lastUpdated);
const stale =
let stale =
ageMsOf(lu) > maxAgeSeconds * 1000 || indexerMatchesRepoFallbackSnapshot(indexer, canonical);
if (
onChainEthUsd !== undefined &&
ethUsdOracleDriftBps(indexer.priceUsd, onChainEthUsd) > readChain138EthOracleDriftBps()
) {
stale = true;
}
out.push({
layer: 'indexer_market',
rank: 1,