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
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:
@@ -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()) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
118
services/token-aggregation/src/api/routes/metamask-prices.ts
Normal file
118
services/token-aggregation/src/api/routes/metamask-prices.ts
Normal 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;
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
}),
|
||||
])
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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> = {
|
||||
|
||||
Reference in New Issue
Block a user