feat(token-aggregation): reports, PMM quotes, config; Engine X flash vaults
- Expand token-aggregation API (report routes), canonical tokens, pools - Add flash vault contracts + tests (indexed, DODO cwUSDC, XAUT borrow) - PMM pools JSON, deploy/export scripts, metamask verified list Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -78,6 +78,22 @@ function uniquePaths(paths: Array<string | undefined | null>): string[] {
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Non-empty built-in CCIP / trustless lane counts for /bridge/metrics (telemetry-friendly). */
|
||||
function summarizeBuiltInBridgeLanes() {
|
||||
const payload = buildDefaultBridgeRoutes();
|
||||
const trustlessKeys = payload.routes.trustless ? Object.keys(payload.routes.trustless) : [];
|
||||
const chain138 = payload.chain138Bridges as Record<string, string | undefined>;
|
||||
return {
|
||||
weth9Destinations: Object.keys(payload.routes.weth9).length,
|
||||
weth10Destinations: Object.keys(payload.routes.weth10).length,
|
||||
trustlessDestinations: trustlessKeys.length,
|
||||
chain138ConfiguredBridges: Object.keys(chain138).filter((k) => {
|
||||
const v = chain138[k];
|
||||
return typeof v === 'string' && v.startsWith('0x');
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
function resolveBridgeRoutesPath(): string | null {
|
||||
const candidates = uniquePaths([
|
||||
process.env.BRIDGE_LIST_JSON_PATH,
|
||||
@@ -176,16 +192,35 @@ router.get('/status', (_req: Request, res: Response) => {
|
||||
|
||||
router.get('/metrics', (_req: Request, res: Response) => {
|
||||
const gruTransport = buildGruTransportStatus();
|
||||
const builtIn = summarizeBuiltInBridgeLanes();
|
||||
res.json({
|
||||
ok: true,
|
||||
lanes: [],
|
||||
lanes: [
|
||||
{
|
||||
kind: 'ccip-weth9',
|
||||
label: 'WETH9 bridge destinations (built-in catalog)',
|
||||
count: builtIn.weth9Destinations,
|
||||
},
|
||||
{
|
||||
kind: 'ccip-weth10',
|
||||
label: 'WETH10 bridge destinations (built-in catalog)',
|
||||
count: builtIn.weth10Destinations,
|
||||
},
|
||||
{
|
||||
kind: 'trustless',
|
||||
label: 'Trustless / Lockbox destinations (env-backed)',
|
||||
count: builtIn.trustlessDestinations,
|
||||
},
|
||||
],
|
||||
chain138Bridges: builtIn.chain138ConfiguredBridges,
|
||||
gruTransport: gruTransport
|
||||
? {
|
||||
system: gruTransport.system,
|
||||
summary: gruTransport.summary,
|
||||
}
|
||||
: null,
|
||||
message: 'Bridge metrics include GRU Transport summary counts. Use /api/v1/report/cross-chain for aggregated data.',
|
||||
message:
|
||||
'Lane counts reflect the built-in CCIP route catalog plus GRU Transport summary. Use /api/v1/bridge/routes for full JSON and /api/v1/report/cross-chain for volumes.',
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -139,4 +139,83 @@ describe('Config API runtime networks loader', () => {
|
||||
await new Promise<void>((resolve, reject) => remoteServer.close((err) => (err ? reject(err) : resolve())));
|
||||
}
|
||||
});
|
||||
|
||||
it('serves wallet-facing MetaMask aliases with absolute token images', async () => {
|
||||
const networksRes = await fetch(`${baseUrl}/api/v1/config/networks`);
|
||||
expect(networksRes.status).toBe(200);
|
||||
const networksBody = (await networksRes.json()) as Record<string, any>;
|
||||
expect(networksBody.networks).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
chainIdDecimal: 138,
|
||||
iconUrls: expect.arrayContaining([
|
||||
'https://explorer.d-bis.org/token-icons/chain-138.png',
|
||||
'https://explorer.d-bis.org/api/v1/report/logo/chain-138',
|
||||
]),
|
||||
}),
|
||||
])
|
||||
);
|
||||
|
||||
const metamaskRes = await fetch(`${baseUrl}/api/v1/config/metamask?chainId=138`);
|
||||
expect(metamaskRes.status).toBe(200);
|
||||
const metamaskBody = (await metamaskRes.json()) as Record<string, any>;
|
||||
expect(metamaskBody.addEthereumChain).toMatchObject({
|
||||
chainId: '0x8a',
|
||||
chainName: 'DeFi Oracle Meta Mainnet',
|
||||
});
|
||||
expect(metamaskBody.watchAssets).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
type: 'ERC20',
|
||||
options: expect.objectContaining({
|
||||
symbol: 'cUSDC',
|
||||
image: expect.stringMatching(/^https:\/\/127\.0\.0\.1:\d+\/api\/v1\/report\/logo\/cUSDC\?v=20260510$/),
|
||||
}),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
type: 'ERC20',
|
||||
options: expect.objectContaining({
|
||||
address: '0xb7721dD53A8c629d9f1Ba31a5819AFe250002b03',
|
||||
symbol: 'LINK',
|
||||
image: expect.stringMatching(/^https:\/\/127\.0\.0\.1:\d+\/api\/v1\/report\/logo\/LINK\?v=20260510$/),
|
||||
}),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
type: 'ERC20',
|
||||
options: expect.objectContaining({
|
||||
symbol: 'WETH',
|
||||
image: expect.stringMatching(/^https:\/\/127\.0\.0\.1:\d+\/api\/v1\/report\/logo\/ETH\?v=20260510$/),
|
||||
}),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
type: 'ERC20',
|
||||
options: expect.objectContaining({
|
||||
symbol: 'cWEMIX',
|
||||
image: expect.stringMatching(/^https:\/\/127\.0\.0\.1:\d+\/api\/v1\/report\/logo\/WEMIX\?v=20260510$/),
|
||||
}),
|
||||
}),
|
||||
])
|
||||
);
|
||||
expect(metamaskBody.watchAssets).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
type: 'ERC20',
|
||||
options: expect.objectContaining({
|
||||
address: '0x219522c60e83dEe01FC5b0329d6fA8fD84b9D13d',
|
||||
symbol: 'cUSDC',
|
||||
}),
|
||||
metadata: expect.objectContaining({
|
||||
catalogSymbol: 'cUSDC_V2',
|
||||
familySymbol: 'cUSDC',
|
||||
deploymentVersion: 'v2',
|
||||
}),
|
||||
}),
|
||||
])
|
||||
);
|
||||
expect(
|
||||
metamaskBody.watchAssets.some(
|
||||
(entry: Record<string, any>) => entry.options?.symbol === 'cUSDC_V2' || entry.options?.symbol === 'cUSDT_V2'
|
||||
)
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,12 +1,75 @@
|
||||
import { Router, Request, Response } from 'express';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { getNetworks, getConfigByChain, API_VERSION } from '../../config/networks';
|
||||
import { getNetworks, getConfigByChain, API_VERSION, type NetworkEntry } from '../../config/networks';
|
||||
import { getCanonicalTokensByChain, getLogoUriForSpec, getTokenRegistryFamily } from '../../config/canonical-tokens';
|
||||
import { cacheMiddleware } from '../middleware/cache';
|
||||
import { fetchRemoteJson } from '../utils/fetch-remote-json';
|
||||
import { logger } from '../../utils/logger';
|
||||
|
||||
const router: Router = Router();
|
||||
const DEFAULT_PUBLIC_BASE_URL = 'https://explorer.d-bis.org';
|
||||
const DEFAULT_WALLET_METADATA_VERSION = '20260510';
|
||||
|
||||
function resolvePublicBaseUrl(req: Request): string {
|
||||
const configured = (
|
||||
process.env.TOKEN_AGGREGATION_PUBLIC_BASE_URL ??
|
||||
process.env.PUBLIC_API_BASE_URL ??
|
||||
process.env.PUBLIC_BASE_URL ??
|
||||
''
|
||||
).trim();
|
||||
if (configured) return configured.replace(/\/+$/, '');
|
||||
|
||||
const host = String(req.get('x-forwarded-host') || req.get('host') || '').split(',')[0].trim();
|
||||
if (host) {
|
||||
let proto = String(req.get('x-forwarded-proto') || 'https').split(',')[0].trim() || 'https';
|
||||
if (proto === 'http' && !/^(localhost|127\.0\.0\.1|\[::1\])(?::\d+)?$/i.test(host)) {
|
||||
proto = 'https';
|
||||
}
|
||||
return `${proto}://${host}`.replace(/\/+$/, '');
|
||||
}
|
||||
|
||||
return DEFAULT_PUBLIC_BASE_URL;
|
||||
}
|
||||
|
||||
function absolutePublicUrl(req: Request, value: string | undefined): string | undefined {
|
||||
if (!value) return undefined;
|
||||
if (/^https?:\/\//i.test(value)) return value;
|
||||
if (!value.startsWith('/')) return value;
|
||||
return `${resolvePublicBaseUrl(req)}${value}`;
|
||||
}
|
||||
|
||||
function appendWalletMetadataVersion(value: string | undefined): string | undefined {
|
||||
if (!value) return undefined;
|
||||
const version = (process.env.WALLET_METADATA_IMAGE_VERSION || DEFAULT_WALLET_METADATA_VERSION).trim();
|
||||
if (!version) return value;
|
||||
const separator = value.includes('?') ? '&' : '?';
|
||||
return `${value}${separator}v=${encodeURIComponent(version)}`;
|
||||
}
|
||||
|
||||
function localLogoPathForSymbol(symbol: string, originalLogoUri: string): string {
|
||||
if (originalLogoUri.includes('/token-lists/logos/gru/')) {
|
||||
const fileName = originalLogoUri.split('/').pop()?.replace(/\.svg$/i, '');
|
||||
if (fileName) return `/api/v1/report/logo/${fileName}`;
|
||||
}
|
||||
if (originalLogoUri.includes('/blockchains/bitcoin/info/logo.png')) return '/api/v1/report/logo/cWBTC';
|
||||
if (originalLogoUri.includes('/ipfs/')) {
|
||||
const cid = originalLogoUri.split('/').pop();
|
||||
if (cid) return `/api/v1/report/logo/ipfs-${cid}`;
|
||||
}
|
||||
if (symbol === 'cWUSDC') return '/api/v1/report/logo/cUSDC';
|
||||
return originalLogoUri;
|
||||
}
|
||||
|
||||
function resolveWalletWatchAssetSymbol(spec: { symbol: string; familySymbol?: string; deploymentVersion?: string }): string {
|
||||
// MetaMask validates wallet_watchAsset.symbol against ERC-20 symbol().
|
||||
// Staged V2 Chain 138 deployments currently keep the family symbol on-chain
|
||||
// (for example cUSDC), while the catalog symbol distinguishes the row
|
||||
// (for example cUSDC_V2). Keep the catalog identity in metadata and send the
|
||||
// contract-facing symbol in options.symbol so EIP-747 succeeds.
|
||||
if (spec.deploymentVersion && spec.familySymbol) return spec.familySymbol;
|
||||
return spec.symbol;
|
||||
}
|
||||
|
||||
type RuntimeNetworksPayload = {
|
||||
version?: string | { major?: number; minor?: number; patch?: number };
|
||||
@@ -124,16 +187,77 @@ async function resolveNetworksPayload(): Promise<NetworksPayload> {
|
||||
};
|
||||
}
|
||||
|
||||
async function sendNetworks(_req: Request, res: Response): Promise<void> {
|
||||
res.set('Cache-Control', 'public, max-age=0, must-revalidate');
|
||||
const payload = await resolveNetworksPayload();
|
||||
res.json(payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/v1/networks
|
||||
* Full EIP-3085 chain params for wallet_addEthereumChain (Chain 138, 1, 651940).
|
||||
* If NETWORKS_JSON_URL is set (e.g. GitHub raw URL), fetches and returns that JSON; otherwise uses built-in networks.
|
||||
*/
|
||||
router.get('/networks', cacheMiddleware(5 * 60 * 1000), async (req: Request, res: Response) => {
|
||||
router.get(['/networks', '/config/networks', '/metamask/networks'], cacheMiddleware(5 * 60 * 1000), async (req: Request, res: Response) => {
|
||||
try {
|
||||
await sendNetworks(req, res);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/v1/config/metamask
|
||||
* Wallet-facing aliases for Chain 138 add-chain params and watchAsset token entries.
|
||||
*/
|
||||
router.get(['/config/metamask', '/metamask'], cacheMiddleware(5 * 60 * 1000), async (req: Request, res: Response) => {
|
||||
try {
|
||||
res.set('Cache-Control', 'public, max-age=0, must-revalidate');
|
||||
const chainId = parseInt(String(req.query.chainId ?? '138'), 10) || 138;
|
||||
const payload = await resolveNetworksPayload();
|
||||
res.json(payload);
|
||||
const networks = payload.networks as NetworkEntry[];
|
||||
const network = networks.find((entry) => Number(entry.chainIdDecimal) === chainId);
|
||||
if (!network) {
|
||||
res.status(404).json({ error: 'Chain not found', chainId });
|
||||
return;
|
||||
}
|
||||
|
||||
const watchAssets = getCanonicalTokensByChain(chainId)
|
||||
.map((spec) => {
|
||||
const address = spec.addresses[chainId];
|
||||
if (!address) return null;
|
||||
const originalLogoURI = getLogoUriForSpec(spec);
|
||||
return {
|
||||
type: 'ERC20',
|
||||
options: {
|
||||
address,
|
||||
symbol: resolveWalletWatchAssetSymbol(spec),
|
||||
decimals: spec.decimals,
|
||||
image: appendWalletMetadataVersion(absolutePublicUrl(req, localLogoPathForSymbol(spec.symbol, originalLogoURI))),
|
||||
},
|
||||
metadata: {
|
||||
name: spec.name,
|
||||
catalogSymbol: spec.symbol,
|
||||
registryFamily: getTokenRegistryFamily(spec),
|
||||
familySymbol: spec.familySymbol,
|
||||
deploymentVersion: spec.deploymentVersion,
|
||||
deploymentStatus: spec.deploymentStatus,
|
||||
},
|
||||
};
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
res.json({
|
||||
source: payload.source,
|
||||
version: payload.version,
|
||||
chainId,
|
||||
addEthereumChain: network,
|
||||
watchAssets,
|
||||
caveats: [
|
||||
'MetaMask custom-token prices are controlled by MetaMask and its upstream asset/price providers; this endpoint supplies wallet metadata, logo URLs, and token-add payloads but cannot force MetaMask to render fiat prices.',
|
||||
'After metadata changes, remove and re-add the custom network/token in MetaMask or use the companion UI to call wallet_addEthereumChain and wallet_watchAsset again.',
|
||||
],
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
|
||||
@@ -91,6 +91,24 @@ describe('Report API', () => {
|
||||
const body = (await res.json()) as Record<string, unknown>;
|
||||
expect(body.chainId).toBe(651940);
|
||||
});
|
||||
|
||||
it('enriches Mainnet cWUSDC with supply proof fields for CMC reports', async () => {
|
||||
const res = await fetch(`${baseUrl}/api/v1/report/cmc?chainId=1`);
|
||||
expect(res.status).toBe(200);
|
||||
const body = (await res.json()) as Record<string, any>;
|
||||
const cwusdc = body.tokens.find((token: Record<string, any>) => token.symbol === 'cWUSDC');
|
||||
expect(cwusdc).toMatchObject({
|
||||
contract_address: '0x2de5f116bfce3d0f922d9c8351e0c5fc24b9284a',
|
||||
total_supply: 10451316981.309788,
|
||||
total_supply_raw: '10451316981309788',
|
||||
circulating_supply: 10451316981.309788,
|
||||
market_cap: 10451316981.309788,
|
||||
supply_proof_provenance: expect.objectContaining({
|
||||
status: 'ready_for_tracker_review',
|
||||
}),
|
||||
});
|
||||
expect(cwusdc.tracker_caveats).toEqual(expect.arrayContaining([expect.stringContaining('on-chain supply proof')]));
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /api/v1/report/coingecko', () => {
|
||||
@@ -104,6 +122,62 @@ describe('Report API', () => {
|
||||
expect(body).toHaveProperty('tokens');
|
||||
expect(Array.isArray(body.tokens)).toBe(true);
|
||||
});
|
||||
|
||||
it('enriches Mainnet cWUSDC with supply proof, circulating supply, and market cap', async () => {
|
||||
const res = await fetch(`${baseUrl}/api/v1/report/coingecko?chainId=1`);
|
||||
expect(res.status).toBe(200);
|
||||
const body = (await res.json()) as Record<string, any>;
|
||||
const cwusdc = body.tokens.find((token: Record<string, any>) => token.symbol === 'cWUSDC');
|
||||
expect(cwusdc).toMatchObject({
|
||||
contract_address: '0x2de5f116bfce3d0f922d9c8351e0c5fc24b9284a',
|
||||
total_supply: 10451316981.309788,
|
||||
total_supply_raw: '10451316981309788',
|
||||
circulating_supply: 10451316981.309788,
|
||||
circulating_supply_formula: 'circulatingSupply = totalSupply - protocolControlledNonCirculatingBalances',
|
||||
supply_proof_provenance: expect.objectContaining({
|
||||
schema: 'mainnet-cwusdc-supply-proof/v1',
|
||||
referenceBlock: 25047586,
|
||||
}),
|
||||
market_data: expect.objectContaining({
|
||||
current_price: { usd: 1 },
|
||||
market_cap: 10451316981.309788,
|
||||
}),
|
||||
});
|
||||
expect(cwusdc.tracker_caveats).toEqual(expect.arrayContaining([expect.stringContaining('No public tracker')]));
|
||||
});
|
||||
|
||||
it('surfaces GRU v2 deployment-status pools in tracker-facing token reports', async () => {
|
||||
const res = await fetch(`${baseUrl}/api/v1/report/coingecko?chainId=1`);
|
||||
expect(res.status).toBe(200);
|
||||
const body = (await res.json()) as Record<string, any>;
|
||||
const cwusdc = body.tokens.find((token: Record<string, any>) => token.symbol === 'cWUSDC');
|
||||
expect(cwusdc.liquidity_pools).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
pool_address: '0x69776fc607e9eda8042e320e7e43f54d06c68f0e',
|
||||
source: 'gru-v2-deployment-status',
|
||||
status: 'live',
|
||||
role: 'defense',
|
||||
}),
|
||||
])
|
||||
);
|
||||
});
|
||||
|
||||
it('surfaces explicit supply-proof gaps for Mainnet GRU assets without proof artifacts', async () => {
|
||||
const res = await fetch(`${baseUrl}/api/v1/report/coingecko?chainId=1`);
|
||||
expect(res.status).toBe(200);
|
||||
const body = (await res.json()) as Record<string, any>;
|
||||
const cwusdt = body.tokens.find((token: Record<string, any>) => token.symbol === 'cWUSDT');
|
||||
expect(cwusdt).toMatchObject({
|
||||
contract_address: '0xaf5017d0163ecb99d9b5d94e3b4d7b09af44d8ae',
|
||||
supply_proof_provenance: {
|
||||
source: 'missing-supply-proof',
|
||||
status: 'proof_required',
|
||||
},
|
||||
});
|
||||
expect(cwusdt).not.toHaveProperty('total_supply');
|
||||
expect(cwusdt.tracker_caveats).toEqual(expect.arrayContaining([expect.stringContaining('tracker-grade supply proof')]));
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /api/v1/report/all', () => {
|
||||
@@ -149,6 +223,72 @@ describe('Report API', () => {
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
it('includes Mainnet cWUSDC supply proof enrichment in unified reports', async () => {
|
||||
const res = await fetch(`${baseUrl}/api/v1/report/all?chainId=1`);
|
||||
expect(res.status).toBe(200);
|
||||
const body = (await res.json()) as Record<string, any>;
|
||||
const cwusdc = body.tokens?.['1']?.find((token: Record<string, any>) => token.symbol === 'cWUSDC');
|
||||
expect(cwusdc).toMatchObject({
|
||||
totalSupply: '10451316981.309788',
|
||||
totalSupplyRaw: '10451316981309788',
|
||||
circulatingSupply: '10451316981.309788',
|
||||
circulatingSupplyFormula: 'circulatingSupply = totalSupply - protocolControlledNonCirculatingBalances',
|
||||
market: expect.objectContaining({
|
||||
priceUsd: 1,
|
||||
marketCapUsd: 10451316981.309788,
|
||||
}),
|
||||
supplyProofProvenance: expect.objectContaining({
|
||||
schema: 'mainnet-cwusdc-supply-proof/v1',
|
||||
referenceBlock: 25047586,
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
it('distinguishes proof-gated Mainnet cW assets from deterministic placeholder bindings', async () => {
|
||||
const res = await fetch(`${baseUrl}/api/v1/report/all?chainId=1`);
|
||||
expect(res.status).toBe(200);
|
||||
const body = (await res.json()) as Record<string, any>;
|
||||
const proofGated = body.tokens?.['1']?.find((entry: Record<string, any>) => entry.symbol === 'cWUSDT');
|
||||
expect(proofGated).toMatchObject({
|
||||
supplyProofProvenance: {
|
||||
source: 'missing-supply-proof',
|
||||
status: 'proof_required',
|
||||
},
|
||||
});
|
||||
expect(proofGated.totalSupply).toBeUndefined();
|
||||
expect(proofGated.trackerCaveats).toEqual(expect.arrayContaining([expect.stringContaining('proof artifact')]));
|
||||
|
||||
const placeholderSymbols = ['cWBTC', 'cWETH'];
|
||||
for (const symbol of placeholderSymbols) {
|
||||
const token = body.tokens?.['1']?.find((entry: Record<string, any>) => entry.symbol === symbol);
|
||||
expect(token).toMatchObject({
|
||||
supplyProofProvenance: {
|
||||
source: 'deterministic-placeholder-address',
|
||||
status: 'non_reportable_until_erc20_deployed',
|
||||
},
|
||||
});
|
||||
expect(token.totalSupply).toBeUndefined();
|
||||
expect(token.trackerCaveats).toEqual(expect.arrayContaining([expect.stringContaining('deterministic placeholder')]));
|
||||
}
|
||||
});
|
||||
|
||||
it('marks proofless base GRU c assets as proof gated instead of leaving silent supply fields', async () => {
|
||||
const res = await fetch(`${baseUrl}/api/v1/report/all?chainId=138`);
|
||||
expect(res.status).toBe(200);
|
||||
const body = (await res.json()) as Record<string, any>;
|
||||
const cusdc = body.tokens?.['138']?.find((entry: Record<string, any>) => entry.symbol === 'cUSDC');
|
||||
expect(cusdc).toMatchObject({
|
||||
type: 'base',
|
||||
registryFamily: 'iso4217',
|
||||
supplyProofProvenance: {
|
||||
source: 'missing-supply-proof',
|
||||
status: 'proof_required',
|
||||
},
|
||||
});
|
||||
expect(cusdc.totalSupply).toBeUndefined();
|
||||
expect(cusdc.trackerCaveats).toEqual(expect.arrayContaining([expect.stringContaining('tracker-grade supply proof')]));
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /api/v1/report/gas-registry', () => {
|
||||
@@ -185,6 +325,63 @@ describe('Report API', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /api/v1/report/adoption-readiness', () => {
|
||||
it('summarizes proved, proof-gated, pool-indexed, and scoring gates', async () => {
|
||||
const res = await fetch(`${baseUrl}/api/v1/report/adoption-readiness`);
|
||||
expect(res.status).toBe(200);
|
||||
const body = (await res.json()) as Record<string, any>;
|
||||
expect(body.scope).toBe('gru-c-and-cw-assets');
|
||||
expect(body.counts).toMatchObject({
|
||||
candidates: expect.any(Number),
|
||||
reportableCandidates: expect.any(Number),
|
||||
nonReportablePlaceholder: expect.any(Number),
|
||||
proved: expect.any(Number),
|
||||
proofRequired: expect.any(Number),
|
||||
silent: 0,
|
||||
liquidityMissing: expect.any(Number),
|
||||
liquidityMissingWithPools: expect.any(Number),
|
||||
liquidityMissingWithoutPools: expect.any(Number),
|
||||
gruV2PoolsWithStatus: expect.any(Number),
|
||||
});
|
||||
expect(body.institutional.score).toEqual(expect.any(Number));
|
||||
expect(body.cryptoListing.score).toEqual(expect.any(Number));
|
||||
expect(Array.isArray(body.blockerInventory.proofRequiredByChain)).toBe(true);
|
||||
expect(Array.isArray(body.blockerInventory.liquidityMissingByChain)).toBe(true);
|
||||
expect(Array.isArray(body.blockerInventory.liquidityMissingWithPoolsByChain)).toBe(true);
|
||||
expect(Array.isArray(body.blockerInventory.liquidityMissingWithoutPoolsByChain)).toBe(true);
|
||||
expect(Array.isArray(body.blockerInventory.liquidityMissingDetails)).toBe(true);
|
||||
expect(Array.isArray(body.blockerInventory.externalOfficialQuoteLiquidityByChain)).toBe(true);
|
||||
expect(Array.isArray(body.blockerInventory.nonReportablePlaceholderByChain)).toBe(true);
|
||||
expect(Array.isArray(body.blockerInventory.gruV2PoolsMissingStatus)).toBe(true);
|
||||
expect(Array.isArray(body.blockerInventory.notes)).toBe(true);
|
||||
});
|
||||
|
||||
it('does not treat external official USDC/USDT mirrors as GRU liquidity blockers', async () => {
|
||||
const res = await fetch(`${baseUrl}/api/v1/report/adoption-readiness`);
|
||||
expect(res.status).toBe(200);
|
||||
const body = (await res.json()) as Record<string, any>;
|
||||
expect(body.counts.externalOfficialQuoteLiquidity).toBeGreaterThan(0);
|
||||
expect(body.blockerInventory.externalOfficialQuoteLiquidityByChain).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
chainId: 1,
|
||||
symbols: expect.arrayContaining(['cUSDC', 'cUSDT']),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
chainId: 56,
|
||||
symbols: expect.arrayContaining(['cUSDC', 'cUSDT']),
|
||||
}),
|
||||
])
|
||||
);
|
||||
expect(body.blockerInventory.liquidityMissingDetails).not.toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ chainId: 1, symbol: 'cUSDT' }),
|
||||
expect.objectContaining({ chainId: 56, symbol: 'cUSDC' }),
|
||||
])
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /api/v1/report/token-list', () => {
|
||||
it('surfaces both V1 and V2 Chain 138 canonical GRU deployments explicitly', async () => {
|
||||
const res = await fetch(`${baseUrl}/api/v1/report/token-list?chainId=138`);
|
||||
@@ -379,6 +576,17 @@ describe('Report API', () => {
|
||||
])
|
||||
);
|
||||
});
|
||||
|
||||
it('uses packaged DBIS-level local logo assets while preserving original logo references', async () => {
|
||||
const res = await fetch(`${baseUrl}/api/v1/report/token-list?chainId=1`);
|
||||
expect(res.status).toBe(200);
|
||||
const body = (await res.json()) as Record<string, any>;
|
||||
const cwusdc = body.tokens.find((token: Record<string, any>) => token.symbol === 'cWUSDC');
|
||||
expect(cwusdc).toMatchObject({
|
||||
logoURI: expect.stringMatching(/^https:\/\/127\.0\.0\.1:\d+\/api\/v1\/report\/logo\/cUSDC$/),
|
||||
originalLogoURI: expect.stringContaining('/token-lists/logos/gru/cUSDC.svg'),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /api/v1/report/cw-registry', () => {
|
||||
@@ -488,6 +696,8 @@ describe('Report API', () => {
|
||||
expect((body.pools as Array<{ poolAddress: string }>)[0]).toMatchObject({
|
||||
poolAddress: '0x1111111111111111111111111111111111111111',
|
||||
section: 'pmmPools',
|
||||
status: 'routing_enabled',
|
||||
statusReason: expect.stringContaining('public routing is enabled'),
|
||||
});
|
||||
} finally {
|
||||
await import('fs/promises').then((fs) => fs.unlink(tempPath).catch(() => undefined));
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,5 @@
|
||||
import express, { Express, Request, Response, NextFunction } from 'express';
|
||||
import { Server } from 'http';
|
||||
import path from 'path';
|
||||
import { readFileSync, existsSync } from 'fs';
|
||||
import cors from 'cors';
|
||||
@@ -48,6 +49,7 @@ export class ApiServer {
|
||||
private indexerEnabled: boolean;
|
||||
private indexer: MultiChainIndexer | null;
|
||||
private omnlPoller: OmnlEventPoller | null;
|
||||
private server: Server | null;
|
||||
|
||||
private resolveTrustProxySetting(): boolean | number | string {
|
||||
const raw = (process.env.EXPRESS_TRUST_PROXY ?? process.env.TRUST_PROXY ?? '1').trim();
|
||||
@@ -65,6 +67,7 @@ export class ApiServer {
|
||||
this.indexerEnabled = this.resolveFeatureFlag('ENABLE_INDEXER', true);
|
||||
this.indexer = this.indexerEnabled ? new MultiChainIndexer() : null;
|
||||
this.omnlPoller = this.resolveFeatureFlag('ENABLE_OMNL_EVENT_POLLER', false) ? new OmnlEventPoller() : null;
|
||||
this.server = null;
|
||||
|
||||
this.setupMiddleware();
|
||||
this.setupRoutes();
|
||||
@@ -106,6 +109,14 @@ export class ApiServer {
|
||||
});
|
||||
next();
|
||||
});
|
||||
|
||||
const publicPath = path.join(__dirname, '../../public');
|
||||
if (existsSync(publicPath)) {
|
||||
this.app.use('/static', express.static(publicPath, {
|
||||
immutable: true,
|
||||
maxAge: '1d',
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
private setupRoutes(): void {
|
||||
@@ -151,6 +162,39 @@ export class ApiServer {
|
||||
res.type('html').send(readFileSync(dashboardPath, 'utf8'));
|
||||
});
|
||||
|
||||
// Public API catalog (register before routers so GET /api/v1 is not swallowed by middleware-only mounts)
|
||||
const sendApiV1Catalog = (_req: Request, res: Response) => {
|
||||
res.json({
|
||||
service: 'token-aggregation',
|
||||
version: '1.0.0',
|
||||
note: 'Prefix paths with your public API origin (e.g. https://explorer.d-bis.org).',
|
||||
paths: {
|
||||
catalog: '/api/v1',
|
||||
health: '/health',
|
||||
chains: '/api/v1/chains',
|
||||
networks: '/api/v1/networks',
|
||||
config: '/api/v1/config',
|
||||
tokens: '/api/v1/tokens',
|
||||
tokenDetail: '/api/v1/tokens/{address}',
|
||||
quote: '/api/v1/quote',
|
||||
bridgeRoutes: '/api/v1/bridge/routes',
|
||||
bridgeStatus: '/api/v1/bridge/status',
|
||||
bridgeMetrics: '/api/v1/bridge/metrics',
|
||||
bridgePreflight: '/api/v1/bridge/preflight',
|
||||
tokenMappingPairs: '/api/v1/token-mapping/pairs',
|
||||
tokenMappingResolve: '/api/v1/token-mapping/resolve',
|
||||
reportTokenList: '/api/v1/report/token-list',
|
||||
routesTree: '/api/v1/routes/tree',
|
||||
plannerProvidersCapabilities: '/api/v2/providers/capabilities',
|
||||
plannerRoutesPlan: '/api/v2/routes/plan',
|
||||
plannerIntentsPlan: '/api/v2/intents/plan',
|
||||
plannerInternalExecutionPlan: '/api/v2/routes/internal-execution-plan',
|
||||
},
|
||||
});
|
||||
};
|
||||
this.app.get('/api/v1', sendApiV1Catalog);
|
||||
this.app.get('/api/v1/', sendApiV1Catalog);
|
||||
|
||||
// API routes
|
||||
this.app.use('/api/v1', tokenRoutes);
|
||||
this.app.use('/api/v1', configRoutes);
|
||||
@@ -206,21 +250,25 @@ export class ApiServer {
|
||||
|
||||
async start(): Promise<void> {
|
||||
try {
|
||||
// Start server
|
||||
this.server = this.app.listen(this.port, () => {
|
||||
logger.info(`Token Aggregation Service listening on port ${this.port}`);
|
||||
logger.info(`Health check: http://localhost:${this.port}/health`);
|
||||
logger.info(`API: http://localhost:${this.port}/api/v1`);
|
||||
});
|
||||
|
||||
if (this.indexer) {
|
||||
await this.indexer.initialize();
|
||||
await this.indexer.startAll();
|
||||
this.indexer
|
||||
.initialize()
|
||||
.then(() => this.indexer?.startAll())
|
||||
.catch((error) => {
|
||||
logger.error('Token aggregation indexer failed after API startup:', error);
|
||||
});
|
||||
} else {
|
||||
logger.info('Token aggregation indexer disabled by ENABLE_INDEXER flag');
|
||||
}
|
||||
|
||||
this.omnlPoller?.start();
|
||||
|
||||
// Start server
|
||||
this.app.listen(this.port, () => {
|
||||
logger.info(`Token Aggregation Service listening on port ${this.port}`);
|
||||
logger.info(`Health check: http://localhost:${this.port}/health`);
|
||||
logger.info(`API: http://localhost:${this.port}/api/v1`);
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Failed to start server:', error);
|
||||
process.exit(1);
|
||||
@@ -230,6 +278,18 @@ export class ApiServer {
|
||||
async stop(): Promise<void> {
|
||||
this.omnlPoller?.stop();
|
||||
this.indexer?.stopAll();
|
||||
if (this.server) {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
this.server?.close((error) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
this.server = null;
|
||||
}
|
||||
logger.info('Server stopped');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,6 +44,41 @@ describe('canonical cW token catalog', () => {
|
||||
expect(getCanonicalTokenByAddress(56, '0xC2FA05F12a75Ac84ea778AF9D6935cA807275E55')?.symbol).toBe('cWUSDW');
|
||||
});
|
||||
|
||||
it('keeps Cronos and the broader wrapped fiat/commodity family in the canonical cW mesh', () => {
|
||||
const cronosCwUsdc = getCanonicalTokenBySymbol(25, 'cWUSDC');
|
||||
expect(cronosCwUsdc).toMatchObject({
|
||||
symbol: 'cWUSDC',
|
||||
type: 'w',
|
||||
currencyCode: 'USD',
|
||||
});
|
||||
expect(cronosCwUsdc?.addresses[25]).toBe('0x932566E5bB6BEBF6B035B94f3DE1f75f126304Ec');
|
||||
expect(getCanonicalTokenByAddress(25, '0x932566E5bB6BEBF6B035B94f3DE1f75f126304Ec')?.symbol).toBe('cWUSDC');
|
||||
|
||||
const expected = [
|
||||
['cWEURC', 'EUR', '0x7574d37F42528B47c88962931e48FC61608a4050'],
|
||||
['cWEURT', 'EUR', '0x9f833b4f1012F52eb3317b09922a79c6EdFca77D'],
|
||||
['cWGBPC', 'GBP', '0xe5c65A76A541368d3061fe9E7A2140cABB903dbF'],
|
||||
['cWGBPT', 'GBP', '0xBb58fa16bAc8E789f09C14243adEE6480D8213A2'],
|
||||
['cWAUDC', 'AUD', '0xff3084410A732231472Ee9f93F5855dA89CC5254'],
|
||||
['cWJPYC', 'JPY', '0x52aD62B8bD01154e2A4E067F8Dc4144C9988d203'],
|
||||
['cWCHFC', 'CHF', '0xB55F49D6316322d5caA96D34C6e4b1003BD3E670'],
|
||||
['cWCADC', 'CAD', '0x32aD687F24F77bF8C86605c202c829163Ac5Ab36'],
|
||||
['cWXAUC', 'XAU', '0xf1B771c95573113E993374c0c7cB2dc1a7908B12'],
|
||||
['cWXAUT', 'XAU', '0xD517C0cF7013f988946A468c880Cc9F8e2A4BCbE'],
|
||||
] as const;
|
||||
|
||||
for (const [symbol, currencyCode, cronosAddress] of expected) {
|
||||
const token = getCanonicalTokenBySymbol(25, symbol);
|
||||
expect(token).toMatchObject({
|
||||
symbol,
|
||||
type: 'w',
|
||||
currencyCode,
|
||||
});
|
||||
expect(token?.addresses[25]).toBe(cronosAddress);
|
||||
expect(getCanonicalTokenByAddress(25, cronosAddress)?.symbol).toBe(symbol);
|
||||
}
|
||||
});
|
||||
|
||||
it('surfaces cUSDW on Chain 138 as the repo-native USDW hub asset', () => {
|
||||
const cusdw = getCanonicalTokenBySymbol(138, 'cUSDW');
|
||||
expect(cusdw).toMatchObject({
|
||||
|
||||
@@ -70,7 +70,7 @@ const LEGACY_CHAIN_ENV_SUFFIX: Partial<Record<number, string>> = {
|
||||
};
|
||||
/** L2/mainnet chain IDs for cUSDT/cUSDC multichain (env: CUSDT_ADDRESS_56, CUSDC_ADDRESS_137, etc.) */
|
||||
const L2_CHAIN_IDS = [1, 56, 137, 10, 42161, 8453, 43114, 25, 100, 42220, 1111] as const;
|
||||
const GRU_CW_CHAIN_IDS = [1, 56, 137, 10, 42161, 8453, 43114, 100, 42220] as const;
|
||||
const GRU_CW_CHAIN_IDS = [1, 10, 25, 56, 100, 137, 8453, 42161, 42220, 43114] as const;
|
||||
const BTC_CW_CHAIN_IDS = [1, 10, 25, 56, 100, 137, 42161, 42220, 43114, 8453, 1111] as const;
|
||||
const ETH_MAINNET_CW_CHAIN_IDS = [1] as const;
|
||||
const ETH_L2_CW_CHAIN_IDS = [10, 42161, 8453] as const;
|
||||
@@ -165,33 +165,156 @@ const FALLBACK_ADDRESSES: Record<string, Partial<Record<number, string>>> = {
|
||||
cWAUSDT: {
|
||||
[56]: '0xe1a51Bc037a79AB36767561B147eb41780124934',
|
||||
[137]: '0xf12e262F85107df26741726b074606CaFa24AAe7',
|
||||
[43114]: '0xff3084410A732231472Ee9f93F5855dA89CC5254',
|
||||
[42220]: '0xC158b6cD3A3088C52F797D41f5Aa02825361629e',
|
||||
[43114]: '0xff3084410A732231472Ee9f93F5855dA89CC5254',
|
||||
},
|
||||
cWUSDC: {
|
||||
[1]: '0x2de5F116bFcE3d0f922d9C8351e0c5Fc24b9284a',
|
||||
[56]: '0x5355148C4740fcc3D7a96F05EdD89AB14851206b',
|
||||
[137]: '0xd6969bC19b53f866C64f2148aE271B2Dae0C58E4',
|
||||
[100]: '0xd6969bC19b53f866C64f2148aE271B2Dae0C58E4',
|
||||
[10]: '0x377a5FaA3162b3Fc6f4e267301A3c817bAd18105',
|
||||
[42161]: '0x0cb0192C056aa425C557BdeAD8E56C7eEabf7acF',
|
||||
[25]: '0x932566E5bB6BEBF6B035B94f3DE1f75f126304Ec',
|
||||
[56]: '0x5355148C4740fcc3D7a96F05EdD89AB14851206b',
|
||||
[100]: '0xd6969bC19b53f866C64f2148aE271B2Dae0C58E4',
|
||||
[137]: '0xd6969bC19b53f866C64f2148aE271B2Dae0C58E4',
|
||||
[8453]: '0x377a5FaA3162b3Fc6f4e267301A3c817bAd18105',
|
||||
[43114]: '0x0C242b513008Cd49C89078F5aFb237A3112251EB',
|
||||
[42161]: '0x0cb0192C056aa425C557BdeAD8E56C7eEabf7acF',
|
||||
[42220]: '0x4C38F9A5ed68A04cd28a72E8c68C459Ec34576f3',
|
||||
[43114]: '0x0C242b513008Cd49C89078F5aFb237A3112251EB',
|
||||
},
|
||||
cWUSDT: {
|
||||
[1]: '0xaF5017d0163ecb99D9B5D94e3b4D7b09Af44D8AE',
|
||||
[56]: '0x9a1D0dBEE997929ED02fD19E0E199704d20914dB',
|
||||
[137]: '0x0cb0192C056aa425C557BdeAD8E56C7eEabf7acF',
|
||||
[100]: '0x0cb0192C056aa425C557BdeAD8E56C7eEabf7acF',
|
||||
[10]: '0x04B2AE3c3bb3d70Df506FAd8717b0FBFC78ED7E6',
|
||||
[42161]: '0x73ADaF7dBa95221c080db5631466d2bC54f6a76B',
|
||||
[25]: '0x72948a7a813B60b37Cd0c920C4657DbFF54312b8',
|
||||
[56]: '0x9a1D0dBEE997929ED02fD19E0E199704d20914dB',
|
||||
[100]: '0x0cb0192C056aa425C557BdeAD8E56C7eEabf7acF',
|
||||
[137]: '0x0cb0192C056aa425C557BdeAD8E56C7eEabf7acF',
|
||||
[8453]: '0x04B2AE3c3bb3d70Df506FAd8717b0FBFC78ED7E6',
|
||||
[43114]: '0x8142BA530B08f3950128601F00DaaA678213DFdf',
|
||||
[42161]: '0x73ADaF7dBa95221c080db5631466d2bC54f6a76B',
|
||||
[42220]: '0x73376eB92c16977B126dB9112936A20Fa0De3442',
|
||||
[43114]: '0x8142BA530B08f3950128601F00DaaA678213DFdf',
|
||||
},
|
||||
cWEURC: {
|
||||
[1]: '0xD4aEAa8cD3fB41Dc8437FaC7639B6d91B60A5e8d',
|
||||
[10]: '0x4ab39b5bab7b463435209a9039bd40cf241f5a82',
|
||||
[25]: '0x7574d37F42528B47c88962931e48FC61608a4050',
|
||||
[56]: '0x50b073d0D1D2f002745cb9FC28a057d5be84911c',
|
||||
[100]: '0x25603ae4bff0b71d637b3573d1b6657f5f6d17ef',
|
||||
[137]: '0x3CD9ee18db7ad13616FCC1c83bC6098e03968E66',
|
||||
[8453]: '0xcb145ba9a370681e3545f60e55621ebf218b1031',
|
||||
[42161]: '0x2a0023ad5ce1ac6072b454575996dffb1bb11b16',
|
||||
[42220]: '0xb6D2f38b9015F32ccE8818509c712264E7fceeD3',
|
||||
[43114]: '0x84353ed1f0c7a703a17abad19b0db15bc9a5e3e5',
|
||||
},
|
||||
cWEURT: {
|
||||
[1]: '0x855d74FFB6CF75721a9bAbc8B2ed35c8119241dC',
|
||||
[10]: '0x6f521cd9fcf7884cd4e9486c7790e818638e09dd',
|
||||
[25]: '0x9f833b4f1012F52eb3317b09922a79c6EdFca77D',
|
||||
[56]: '0x1ED9E491A5eCd53BeF21962A5FCE24880264F63f',
|
||||
[100]: '0x8e54c52d34a684e22865ac9f2d7c27c30561a7b9',
|
||||
[137]: '0xBeF5A0Bcc0E77740c910f197138cdD90F98d2427',
|
||||
[8453]: '0x73e0cf8bf861d376b3a4c87c136f975027f045ff',
|
||||
[42161]: '0x22b98130ab4d9c355512b25ade4c35e75a4e7e89',
|
||||
[42220]: '0x7e6fB8D80f81430e560F8232b2A4fd06249d74ce',
|
||||
[43114]: '0xfc7d256e48253f7a7e08f0e55b9ff7039eb2524c',
|
||||
},
|
||||
cWGBPC: {
|
||||
[1]: '0xc074007dc0bfb384b1cf6426a56287ed23fe4d52',
|
||||
[10]: '0x3f8c409c6072a2b6a4ff17071927ba70f80c725f',
|
||||
[25]: '0xe5c65A76A541368d3061fe9E7A2140cABB903dbF',
|
||||
[56]: '0x8b6EE72001cAFcb21D56a6c4686D6Db951d499A6',
|
||||
[100]: '0x4d9bc6c74ba65e37c4139f0aec9fc5ddff28dcc4',
|
||||
[137]: '0x948690147D2e50ffe50C5d38C14125aD6a9FA036',
|
||||
[8453]: '0x2a0023ad5ce1ac6072b454575996dffb1bb11b16',
|
||||
[42161]: '0xa846aead3071df1b6439d5d813156ace7c2c1da1',
|
||||
[42220]: '0xE37c332a88f112F9e039C5d92D821402A89c7052',
|
||||
[43114]: '0xbdf0c4ea1d81e8e769b0f41389a2c733e3ff723e',
|
||||
},
|
||||
cWGBPT: {
|
||||
[1]: '0x1dDF9970F01c76A692Fdba2706203E6f16e0C46F',
|
||||
[10]: '0x456373d095d6b9260f01709f93fccf1d8aa14d11',
|
||||
[25]: '0xBb58fa16bAc8E789f09C14243adEE6480D8213A2',
|
||||
[56]: '0xA6eFb8783C8ad2740ec880e46D4f7E608E893B1B',
|
||||
[100]: '0x9f6d2578003fe04e58a9819a4943732f2a203a61',
|
||||
[137]: '0x58a8D8F78F1B65c06dAd7542eC46b299629A60dd',
|
||||
[8453]: '0x22b98130ab4d9c355512b25ade4c35e75a4e7e89',
|
||||
[42161]: '0x29828e9ab2057cd3df3c9211455ae1f76e53d2af',
|
||||
[42220]: '0x1dBa81f91f1BeC47FFf60eC3e7DeD780ad9968E3',
|
||||
[43114]: '0x4611d3424e059392a52b957e508273bc761c80f2',
|
||||
},
|
||||
cWAUDC: {
|
||||
[1]: '0x5020Db641B3Fc0dAbBc0c688C845bc4E3699f35F',
|
||||
[10]: '0x25603ae4bff0b71d637b3573d1b6657f5f6d17ef',
|
||||
[25]: '0xff3084410A732231472Ee9f93F5855dA89CC5254',
|
||||
[56]: '0x7062f35567BBAb4d98dc33af03B0d14Df42294D5',
|
||||
[100]: '0xddc4063f770f7c49d00b5a10fb552e922aa39b2c',
|
||||
[137]: '0xFb4B6Cc81211F7d886950158294A44C312abCA29',
|
||||
[8453]: '0xa846aead3071df1b6439d5d813156ace7c2c1da1',
|
||||
[42161]: '0xc1535e88578d984f12eab55863376b8d8b9fb05a',
|
||||
[42220]: '0x2d3a2ED4Ca4d69912d217c305EE921609F7906A8',
|
||||
[43114]: '0x04e1e22b0d41e99f4275bd40a50480219bc9a223',
|
||||
},
|
||||
cWJPYC: {
|
||||
[1]: '0x07EEd0D7dD40984e47B9D3a3bdded1c536435582',
|
||||
[10]: '0x8e54c52d34a684e22865ac9f2d7c27c30561a7b9',
|
||||
[25]: '0x52aD62B8bD01154e2A4E067F8Dc4144C9988d203',
|
||||
[56]: '0x5fbCE65524211BC1bFb0309fd9EE09E786c6D097',
|
||||
[100]: '0x145e8e8c49b6a021969dd9d2c01c8fea44374f61',
|
||||
[137]: '0xf9f5D0ACD71C76F9476F10B3F3d3E201F0883C68',
|
||||
[8453]: '0x29828e9ab2057cd3df3c9211455ae1f76e53d2af',
|
||||
[42161]: '0xdc383c489533a4dd9a6bd3007386e25d5078b878',
|
||||
[42220]: '0x0b39F47D2E68aB0eB18d4b637Bbd1dD8E97cFbB5',
|
||||
[43114]: '0x3714b1a312e0916c7dcdc4edf480fc0339e59a59',
|
||||
},
|
||||
cWCHFC: {
|
||||
[1]: '0x0F91C5E6Ddd46403746aAC970D05d70FFe404780',
|
||||
[10]: '0x4d9bc6c74ba65e37c4139f0aec9fc5ddff28dcc4',
|
||||
[25]: '0xB55F49D6316322d5caA96D34C6e4b1003BD3E670',
|
||||
[56]: '0xD9f8710caeeBA3b3D423D7D14a918701426B5ef3',
|
||||
[100]: '0x46d90d7947f1139477c206c39268923b99cf09e4',
|
||||
[137]: '0xeE17bB0322383fecCA2784fbE2d4CD7d02b1905B',
|
||||
[8453]: '0xc1535e88578d984f12eab55863376b8d8b9fb05a',
|
||||
[42161]: '0x7e4b4682453bcce19ec903fb69153d3031986bc4',
|
||||
[42220]: '0x8142BA530B08f3950128601F00DaaA678213DFdf',
|
||||
[43114]: '0xc2fa05f12a75ac84ea778af9d6935ca807275e55',
|
||||
},
|
||||
cWCADC: {
|
||||
[1]: '0x209FE32fe7B541751D190ae4e50cd005DcF8EDb4',
|
||||
[10]: '0x9f6d2578003fe04e58a9819a4943732f2a203a61',
|
||||
[25]: '0x32aD687F24F77bF8C86605c202c829163Ac5Ab36',
|
||||
[56]: '0x9AE7a6B311584D60Fa93f973950d609061875775',
|
||||
[100]: '0xa7133c78e0ec74503a5941bcbd44257615b6b4f6',
|
||||
[137]: '0xc9750828124D4c10e7a6f4B655cA8487bD3842EB',
|
||||
[8453]: '0xdc383c489533a4dd9a6bd3007386e25d5078b878',
|
||||
[42161]: '0xcc6ae6016d564e9ab82aaff44d65e05a9b18951c',
|
||||
[42220]: '0x0C242b513008Cd49C89078F5aFb237A3112251EB',
|
||||
[43114]: '0x1872e033b30f3ce0498847926857433e0146394e',
|
||||
},
|
||||
cWXAUC: {
|
||||
[1]: '0x572Be0fa8CA0534d642A567CEDb398B771D8a715',
|
||||
[10]: '0xddc4063f770f7c49d00b5a10fb552e922aa39b2c',
|
||||
[25]: '0xf1B771c95573113E993374c0c7cB2dc1a7908B12',
|
||||
[56]: '0xCB145bA9A370681e3545F60e55621eBf218B1031',
|
||||
[100]: '0x23873b85cfeb343eb952618e8c9e9bfb7f6a0d45',
|
||||
[137]: '0x328Cd365Bb35524297E68ED28c6fF2C9557d1363',
|
||||
[8453]: '0x7e4b4682453bcce19ec903fb69153d3031986bc4',
|
||||
[42161]: '0xa7762b63c4871581885ad17c5714ebb286a7480b',
|
||||
[42220]: '0x61D642979eD75c1325f35b9275C5A7FE97F22451',
|
||||
[43114]: '0x4f95297c23d9f4a1032b1c6a2e553225cb175bee',
|
||||
},
|
||||
cWXAUT: {
|
||||
[1]: '0xACE1DBF857549a11aF1322e1f91F2F64b029c906',
|
||||
[10]: '0x145e8e8c49b6a021969dd9d2c01c8fea44374f61',
|
||||
[25]: '0xD517C0cF7013f988946A468c880Cc9F8e2A4BCbE',
|
||||
[56]: '0x73E0CF8BF861D376B3a4C87c136F975027f045ff',
|
||||
[100]: '0xc6189d404dc60cae7b48e2190e44770a03193e5f',
|
||||
[137]: '0x9e6044d730d4183bF7a666293d257d035Fba6d44',
|
||||
[8453]: '0xcc6ae6016d564e9ab82aaff44d65e05a9b18951c',
|
||||
[42161]: '0x66568899ffe8f00b25dc470e878b65a478994e76',
|
||||
[42220]: '0x30751782486eed825187C1EAe5DE4b4baD428AaE',
|
||||
[43114]: '0xd2b4dbf2f6bd6704e066d752eec61fb0be953fd3',
|
||||
},
|
||||
cWUSDW: {
|
||||
[56]: '0xC2FA05F12a75Ac84ea778AF9D6935cA807275E55',
|
||||
[42220]: '0x176a1b6Aa59F24B3aa65F2b697AB262Bca9093B5',
|
||||
[43114]: '0xcfdCe5E660FC2C8052BDfa7aEa1865DD753411Ae',
|
||||
},
|
||||
cWBTC: {
|
||||
@@ -236,7 +359,7 @@ const FALLBACK_ADDRESSES: Record<string, Partial<Record<number, string>>> = {
|
||||
cWWEMIX: {
|
||||
[1111]: '0xc111000000000000000000000000000000000457',
|
||||
},
|
||||
// Compliant Fiat on Chain 138 — from DeployCompliantFiatTokens (2026-02-27)
|
||||
// Compliant Fiat on Chain 138 - from DeployCompliantFiatTokens (2026-02-27)
|
||||
cEURC: { [CHAIN_138]: '0x8085961F9cF02b4d800A3c6d386D31da4B34266a' },
|
||||
cEURT: { [CHAIN_138]: '0xdf4b71c61E5912712C1Bdd451416B9aC26949d72' },
|
||||
cGBPC: { [CHAIN_138]: '0x003960f16D9d34F2e98d62723B6721Fb92074aD2' },
|
||||
@@ -247,9 +370,10 @@ const FALLBACK_ADDRESSES: Record<string, Partial<Record<number, string>>> = {
|
||||
cCADC: { [CHAIN_138]: '0x54dBd40cF05e15906A2C21f600937e96787f5679' },
|
||||
cXAUC: { [CHAIN_138]: '0x290E52a8819A4fbD0714E517225429aA2B70EC6b' },
|
||||
cXAUT: { [CHAIN_138]: '0x94e408E26c6FD8F4ee00b54dF19082FDA07dC96E' },
|
||||
LINK: { [CHAIN_138]: '0xb7721dD53A8c629d9f1Ba31a5819AFe250002b03' },
|
||||
WETH: { [CHAIN_138]: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2' },
|
||||
WETH10: { [CHAIN_138]: '0xf4BB2e28688e89fCcE3c0580D37d36A7672E8A9f' },
|
||||
// ISO-4217W on Cronos (25) — from DeployISO4217WSystem
|
||||
// ISO-4217W on Cronos (25) - from DeployISO4217WSystem
|
||||
USDW: { [CHAIN_25]: '0x948690147D2e50ffe50C5d38C14125aD6a9FA036' },
|
||||
EURW: { [CHAIN_25]: '0x58a8D8F78F1B65c06dAd7542eC46b299629A60dd' },
|
||||
GBPW: { [CHAIN_25]: '0xFb4B6Cc81211F7d886950158294A44C312abCA29' },
|
||||
@@ -451,6 +575,16 @@ export const CANONICAL_TOKENS: CanonicalTokenSpec[] = [
|
||||
{ symbol: 'cWAUSDT', name: 'Alltra USD Token (Compliant Wrapped ISO-4217 M1)', type: 'w', decimals: 6, currencyCode: 'USD', description: 'Public-network mirrored transport form for the live Chain 138 cAUSDT surface.', addresses: { ...Object.fromEntries(GRU_CW_CHAIN_IDS.map((id) => [id, addr('cWAUSDT', id)])) } },
|
||||
{ symbol: 'cWUSDC', name: 'USD Coin (Compliant Wrapped ISO-4217 M1)', type: 'w', decimals: 6, currencyCode: 'USD', description: 'Public-network mirrored transport form of canonical Chain 138 cUSDC.', addresses: { ...Object.fromEntries(GRU_CW_CHAIN_IDS.map((id) => [id, addr('cWUSDC', id)])) } },
|
||||
{ symbol: 'cWUSDT', name: 'Tether USD (Compliant Wrapped ISO-4217 M1)', type: 'w', decimals: 6, currencyCode: 'USD', description: 'Public-network mirrored transport form of canonical Chain 138 cUSDT.', addresses: { ...Object.fromEntries(GRU_CW_CHAIN_IDS.map((id) => [id, addr('cWUSDT', id)])) } },
|
||||
{ symbol: 'cWEURC', name: 'Euro Coin (Compliant Wrapped ISO-4217 M1)', type: 'w', decimals: 6, currencyCode: 'EUR', description: 'Public-network mirrored transport form of canonical Chain 138 cEURC.', addresses: { ...Object.fromEntries(GRU_CW_CHAIN_IDS.map((id) => [id, addr('cWEURC', id)])) } },
|
||||
{ symbol: 'cWEURT', name: 'Tether EUR (Compliant Wrapped ISO-4217 M1)', type: 'w', decimals: 6, currencyCode: 'EUR', description: 'Public-network mirrored transport form of canonical Chain 138 cEURT.', addresses: { ...Object.fromEntries(GRU_CW_CHAIN_IDS.map((id) => [id, addr('cWEURT', id)])) } },
|
||||
{ symbol: 'cWGBPC', name: 'Pound Sterling (Compliant Wrapped ISO-4217 M1)', type: 'w', decimals: 6, currencyCode: 'GBP', description: 'Public-network mirrored transport form of canonical Chain 138 cGBPC.', addresses: { ...Object.fromEntries(GRU_CW_CHAIN_IDS.map((id) => [id, addr('cWGBPC', id)])) } },
|
||||
{ symbol: 'cWGBPT', name: 'Tether GBP (Compliant Wrapped ISO-4217 M1)', type: 'w', decimals: 6, currencyCode: 'GBP', description: 'Public-network mirrored transport form of canonical Chain 138 cGBPT.', addresses: { ...Object.fromEntries(GRU_CW_CHAIN_IDS.map((id) => [id, addr('cWGBPT', id)])) } },
|
||||
{ symbol: 'cWAUDC', name: 'Australian Dollar (Compliant Wrapped ISO-4217 M1)', type: 'w', decimals: 6, currencyCode: 'AUD', description: 'Public-network mirrored transport form of canonical Chain 138 cAUDC.', addresses: { ...Object.fromEntries(GRU_CW_CHAIN_IDS.map((id) => [id, addr('cWAUDC', id)])) } },
|
||||
{ symbol: 'cWJPYC', name: 'Japanese Yen (Compliant Wrapped ISO-4217 M1)', type: 'w', decimals: 6, currencyCode: 'JPY', description: 'Public-network mirrored transport form of canonical Chain 138 cJPYC.', addresses: { ...Object.fromEntries(GRU_CW_CHAIN_IDS.map((id) => [id, addr('cWJPYC', id)])) } },
|
||||
{ symbol: 'cWCHFC', name: 'Swiss Franc (Compliant Wrapped ISO-4217 M1)', type: 'w', decimals: 6, currencyCode: 'CHF', description: 'Public-network mirrored transport form of canonical Chain 138 cCHFC.', addresses: { ...Object.fromEntries(GRU_CW_CHAIN_IDS.map((id) => [id, addr('cWCHFC', id)])) } },
|
||||
{ symbol: 'cWCADC', name: 'Canadian Dollar (Compliant Wrapped ISO-4217 M1)', type: 'w', decimals: 6, currencyCode: 'CAD', description: 'Public-network mirrored transport form of canonical Chain 138 cCADC.', addresses: { ...Object.fromEntries(GRU_CW_CHAIN_IDS.map((id) => [id, addr('cWCADC', id)])) } },
|
||||
{ symbol: 'cWXAUC', name: 'Gold (Compliant Wrapped ISO-4217 M1)', type: 'w', decimals: 6, currencyCode: 'XAU', registryFamily: 'commodity', description: 'Public-network mirrored transport form of canonical Chain 138 cXAUC.', addresses: { ...Object.fromEntries(GRU_CW_CHAIN_IDS.map((id) => [id, addr('cWXAUC', id)])) } },
|
||||
{ symbol: 'cWXAUT', name: 'Tether XAU (Compliant Wrapped ISO-4217 M1)', type: 'w', decimals: 6, currencyCode: 'XAU', registryFamily: 'commodity', description: 'Public-network mirrored transport form of canonical Chain 138 cXAUT.', addresses: { ...Object.fromEntries(GRU_CW_CHAIN_IDS.map((id) => [id, addr('cWXAUT', id)])) } },
|
||||
{ symbol: 'cWUSDW', name: 'USD W (Compliant Wrapped ISO-4217 M1)', type: 'w', decimals: 6, currencyCode: 'USD', description: 'Public-network mirrored transport form of canonical Chain 138 cUSDW.', addresses: { ...Object.fromEntries(GRU_CW_CHAIN_IDS.map((id) => [id, addr('cWUSDW', id)])) } },
|
||||
{
|
||||
symbol: 'WETH',
|
||||
@@ -472,6 +606,16 @@ export const CANONICAL_TOKENS: CanonicalTokenSpec[] = [
|
||||
description: 'Chain 138 WETH10 pilot wrapped ETH surface used by DODO v3 routing and flash-capable paths.',
|
||||
addresses: { [CHAIN_138]: addr('WETH10', CHAIN_138) || '' },
|
||||
},
|
||||
{
|
||||
symbol: 'LINK',
|
||||
name: 'Chainlink Token',
|
||||
type: 'base',
|
||||
decimals: 18,
|
||||
currencyCode: 'LINK',
|
||||
registryFamily: 'unclassified',
|
||||
description: 'Chain 138 LINK token used for CCIP and oracle fee accounting.',
|
||||
addresses: { [CHAIN_138]: addr('LINK', CHAIN_138) || '' },
|
||||
},
|
||||
{
|
||||
symbol: 'cWBTC',
|
||||
name: 'Bitcoin (Compliant Wrapped Monetary Unit)',
|
||||
@@ -730,7 +874,18 @@ export function resolveCanonicalQuoteAddress(chainId: number, address: string):
|
||||
const IPFS_GATEWAY = 'https://ipfs.io/ipfs';
|
||||
const GRU_LOGO_BASE =
|
||||
'https://raw.githubusercontent.com/Order-of-Hospitallers/proxmox-cp/main/token-lists/logos/gru';
|
||||
const ETH_LOGO = `${IPFS_GATEWAY}/Qma3FKtLce9MjgJgWbtyCxBiPjJ6xi8jGWUSKNS5Jc2ong`;
|
||||
const ETH_LOGO = '/api/v1/report/logo/ETH';
|
||||
const LINK_LOGO = '/api/v1/report/logo/LINK';
|
||||
const GAS_NATIVE_LOGO_BY_CODE: Record<string, string> = {
|
||||
ETH: ETH_LOGO,
|
||||
BNB: '/api/v1/report/logo/BNB',
|
||||
POL: '/api/v1/report/logo/POL',
|
||||
AVAX: '/api/v1/report/logo/AVAX',
|
||||
CRO: '/api/v1/report/logo/CRO',
|
||||
XDAI: '/api/v1/report/logo/XDAI',
|
||||
CELO: '/api/v1/report/logo/CELO',
|
||||
WEMIX: '/api/v1/report/logo/WEMIX',
|
||||
};
|
||||
const USDC_LOGO = `${GRU_LOGO_BASE}/cUSDC.svg`;
|
||||
const USDT_LOGO = `${GRU_LOGO_BASE}/cUSDT.svg`;
|
||||
const BTC_LOGO = 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/bitcoin/info/logo.png';
|
||||
@@ -750,8 +905,10 @@ const LOGO_BY_SYMBOL: Record<string, string> = {
|
||||
cWUSDC: USDC_LOGO,
|
||||
cWUSDT: USDT_LOGO,
|
||||
cWUSDW: USDC_LOGO,
|
||||
cWEMIX: GAS_NATIVE_LOGO_BY_CODE.WEMIX,
|
||||
WETH: ETH_LOGO,
|
||||
WETH10: ETH_LOGO,
|
||||
LINK: LINK_LOGO,
|
||||
cEURC: `${GRU_LOGO_BASE}/cEURC.svg`,
|
||||
cEURT: `${GRU_LOGO_BASE}/cEURT.svg`,
|
||||
cGBPC: `${GRU_LOGO_BASE}/cGBPC.svg`,
|
||||
@@ -781,6 +938,15 @@ export function getLogoUriForSpec(spec: CanonicalTokenSpec): string {
|
||||
if (spec.logoUrl) return spec.logoUrl;
|
||||
const bySymbol = LOGO_BY_SYMBOL[spec.symbol];
|
||||
if (bySymbol) return bySymbol;
|
||||
const gasLogo = spec.registryFamily === 'gas_native' && spec.currencyCode
|
||||
? GAS_NATIVE_LOGO_BY_CODE[spec.currencyCode.toUpperCase()]
|
||||
: undefined;
|
||||
if (gasLogo) return gasLogo;
|
||||
if (spec.symbol.startsWith('cW')) {
|
||||
const hubSymbol = `c${spec.symbol.slice(2)}`;
|
||||
const hubSpec = CANONICAL_TOKENS.find((t) => t.symbol === hubSymbol);
|
||||
if (hubSpec && hubSpec.symbol !== spec.symbol) return getLogoUriForSpec(hubSpec);
|
||||
}
|
||||
if (spec.symbol.startsWith('ac')) return getLogoUriForSpec(CANONICAL_TOKENS.find((t) => t.symbol === spec.symbol.replace('ac', 'c')) || spec);
|
||||
if (spec.symbol.startsWith('vdc') || spec.symbol.startsWith('sdc')) {
|
||||
const base = spec.symbol.replace(/^(vd|sd)c/, 'c');
|
||||
|
||||
@@ -81,7 +81,7 @@ export const CHAIN_CONFIGS: Record<number, ChainConfig> = {
|
||||
137: {
|
||||
chainId: 137,
|
||||
name: 'Polygon',
|
||||
rpcUrl: process.env.CHAIN_137_RPC_URL || 'https://polygon-rpc.com',
|
||||
rpcUrl: process.env.CHAIN_137_RPC_URL || 'https://polygon-bor-rpc.publicnode.com',
|
||||
explorerUrl: 'https://polygonscan.com',
|
||||
nativeCurrency: { name: 'MATIC', symbol: 'MATIC', decimals: 18 },
|
||||
blockTime: 2,
|
||||
|
||||
@@ -69,6 +69,9 @@ function buildDeploymentStatusCandidates(): string[] {
|
||||
process.env.DEPLOYMENT_STATUS_JSON_PATH,
|
||||
process.env.CW_REGISTRY_JSON_PATH,
|
||||
process.env.CROSS_CHAIN_PMM_DEPLOYMENT_STATUS_PATH,
|
||||
process.env.PROXMOX_REPO_ROOT
|
||||
? path.resolve(process.env.PROXMOX_REPO_ROOT, 'cross-chain-pmm-lps/config/deployment-status.json')
|
||||
: undefined,
|
||||
path.resolve(process.cwd(), 'cross-chain-pmm-lps/config/deployment-status.json'),
|
||||
path.resolve(process.cwd(), '..', 'cross-chain-pmm-lps/config/deployment-status.json'),
|
||||
path.resolve(process.cwd(), '..', '..', 'cross-chain-pmm-lps/config/deployment-status.json'),
|
||||
|
||||
@@ -7,6 +7,8 @@ export interface GruV2DeploymentPoolRow {
|
||||
chainId: number;
|
||||
chainName: string;
|
||||
section: GruV2PmmSection;
|
||||
status: 'live' | 'routing_enabled' | 'configured' | 'proof_required';
|
||||
statusReason: string;
|
||||
baseSymbol: string;
|
||||
quoteSymbol: string;
|
||||
baseAddress: string;
|
||||
@@ -101,6 +103,16 @@ export function buildGruV2PoolRegistryFromDeploymentData(data: DeploymentStatusF
|
||||
chainId,
|
||||
chainName,
|
||||
section,
|
||||
status:
|
||||
pool.publicRoutingEnabled === true
|
||||
? 'routing_enabled'
|
||||
: poolAddress.startsWith('0x')
|
||||
? 'live'
|
||||
: 'configured',
|
||||
statusReason:
|
||||
pool.publicRoutingEnabled === true
|
||||
? 'Pool address is configured in deployment-status and public routing is enabled.'
|
||||
: 'Pool address is configured in deployment-status; routing enablement is not asserted.',
|
||||
baseSymbol,
|
||||
quoteSymbol,
|
||||
baseAddress,
|
||||
|
||||
@@ -34,8 +34,9 @@ export const NETWORKS: NetworkEntry[] = [
|
||||
nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 },
|
||||
blockExplorerUrls: ['https://explorer.d-bis.org'],
|
||||
iconUrls: [
|
||||
'https://explorer.d-bis.org/api/v1/report/logo/chain-138',
|
||||
'https://explorer.d-bis.org/token-icons/chain-138.png',
|
||||
'https://explorer.d-bis.org/favicon.ico',
|
||||
'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png',
|
||||
],
|
||||
oracles: [
|
||||
{ name: 'ETH/USD', address: '0x3304b747e565a97ec8ac220b0b6a1f6ffdb837e6', decimals: 8 },
|
||||
@@ -87,7 +88,13 @@ export const NETWORKS: NetworkEntry[] = [
|
||||
chainId: '0x89',
|
||||
chainIdDecimal: 137,
|
||||
chainName: 'Polygon',
|
||||
rpcUrls: ['https://polygon-rpc.com', 'https://rpc.ankr.com/polygon'],
|
||||
rpcUrls: [
|
||||
'https://polygon-bor-rpc.publicnode.com',
|
||||
'https://1rpc.io/matic',
|
||||
'https://polygon.drpc.org',
|
||||
'https://polygon-rpc.com',
|
||||
'https://rpc.ankr.com/polygon',
|
||||
],
|
||||
nativeCurrency: { name: 'MATIC', symbol: 'MATIC', decimals: 18 },
|
||||
blockExplorerUrls: ['https://polygonscan.com'],
|
||||
iconUrls: ['https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/polygon/info/logo.png'],
|
||||
|
||||
@@ -26,6 +26,7 @@ export function getDatabasePool(): Pool {
|
||||
connectionString: process.env.DATABASE_URL,
|
||||
min: parseInt(process.env.DATABASE_POOL_MIN || '2', 10),
|
||||
max: parseInt(process.env.DATABASE_POOL_MAX || '10', 10),
|
||||
connectionTimeoutMillis: parseInt(process.env.DATABASE_CONNECTION_TIMEOUT_MS || '3000', 10),
|
||||
};
|
||||
|
||||
// If connectionString is not provided, use individual config
|
||||
|
||||
@@ -19,7 +19,16 @@ export class MarketDataRepository {
|
||||
return code === '42P01' || (message.includes('relation "') && message.includes('" does not exist'));
|
||||
}
|
||||
|
||||
private shouldUseReadFallback(error: unknown): boolean {
|
||||
if (this.isMissingRelationError(error)) return true;
|
||||
if (String(process.env.TOKEN_AGGREGATION_DB_READ_FALLBACK ?? '1').toLowerCase() === '0') return false;
|
||||
const message = (error as { message?: string })?.message || '';
|
||||
const code = (error as { code?: string })?.code || '';
|
||||
return ['ETIMEDOUT', 'ECONNREFUSED', 'ENOTFOUND'].includes(code) || /timeout|connect/i.test(message);
|
||||
}
|
||||
|
||||
async getMarketData(chainId: number, tokenAddress: string): Promise<TokenMarketData | null> {
|
||||
if (String(process.env.TOKEN_AGGREGATION_SKIP_DB_READS ?? '0').toLowerCase() === '1') return null;
|
||||
try {
|
||||
const result = await this.pool.query(
|
||||
`SELECT chain_id, token_address, price_usd, price_change_24h, volume_24h, volume_7d, volume_30d,
|
||||
@@ -49,7 +58,7 @@ export class MarketDataRepository {
|
||||
lastUpdated: row.last_updated,
|
||||
};
|
||||
} catch (error) {
|
||||
if (this.isMissingRelationError(error)) {
|
||||
if (this.shouldUseReadFallback(error)) {
|
||||
return null;
|
||||
}
|
||||
throw error;
|
||||
@@ -99,6 +108,7 @@ export class MarketDataRepository {
|
||||
}
|
||||
|
||||
async getTopTokensByVolume(chainId: number, limit: number = 50): Promise<TokenMarketData[]> {
|
||||
if (String(process.env.TOKEN_AGGREGATION_SKIP_DB_READS ?? '0').toLowerCase() === '1') return [];
|
||||
try {
|
||||
const result = await this.pool.query(
|
||||
`SELECT chain_id, token_address, price_usd, price_change_24h, volume_24h, volume_7d, volume_30d,
|
||||
@@ -125,7 +135,7 @@ export class MarketDataRepository {
|
||||
lastUpdated: row.last_updated,
|
||||
}));
|
||||
} catch (error) {
|
||||
if (this.isMissingRelationError(error)) {
|
||||
if (this.shouldUseReadFallback(error)) {
|
||||
return [];
|
||||
}
|
||||
throw error;
|
||||
@@ -133,6 +143,7 @@ export class MarketDataRepository {
|
||||
}
|
||||
|
||||
async getTopTokensByLiquidity(chainId: number, limit: number = 50): Promise<TokenMarketData[]> {
|
||||
if (String(process.env.TOKEN_AGGREGATION_SKIP_DB_READS ?? '0').toLowerCase() === '1') return [];
|
||||
try {
|
||||
const result = await this.pool.query(
|
||||
`SELECT chain_id, token_address, price_usd, price_change_24h, volume_24h, volume_7d, volume_30d,
|
||||
@@ -159,7 +170,7 @@ export class MarketDataRepository {
|
||||
lastUpdated: row.last_updated,
|
||||
}));
|
||||
} catch (error) {
|
||||
if (this.isMissingRelationError(error)) {
|
||||
if (this.shouldUseReadFallback(error)) {
|
||||
return [];
|
||||
}
|
||||
throw error;
|
||||
|
||||
@@ -53,7 +53,16 @@ export class PoolRepository {
|
||||
return code === '42P01' || message.includes('relation "') && message.includes('" does not exist');
|
||||
}
|
||||
|
||||
private shouldUseReadFallback(error: unknown): boolean {
|
||||
if (this.isMissingRelationError(error)) return true;
|
||||
if (String(process.env.TOKEN_AGGREGATION_DB_READ_FALLBACK ?? '1').toLowerCase() === '0') return false;
|
||||
const message = (error as { message?: string })?.message || '';
|
||||
const code = (error as { code?: string })?.code || '';
|
||||
return ['ETIMEDOUT', 'ECONNREFUSED', 'ENOTFOUND'].includes(code) || /timeout|connect/i.test(message);
|
||||
}
|
||||
|
||||
async getPool(chainId: number, poolAddress: string): Promise<LiquidityPool | null> {
|
||||
if (String(process.env.TOKEN_AGGREGATION_SKIP_DB_READS ?? '0').toLowerCase() === '1') return null;
|
||||
try {
|
||||
const result = await this.pool.query(
|
||||
`SELECT id, chain_id, pool_address, token0_address, token1_address, dex_type,
|
||||
@@ -70,7 +79,7 @@ export class PoolRepository {
|
||||
|
||||
return this.mapRowToPool(result.rows[0]);
|
||||
} catch (error) {
|
||||
if (this.isMissingRelationError(error)) {
|
||||
if (this.shouldUseReadFallback(error)) {
|
||||
return null;
|
||||
}
|
||||
throw error;
|
||||
@@ -78,6 +87,7 @@ export class PoolRepository {
|
||||
}
|
||||
|
||||
async getPoolsByChain(chainId: number, limit: number = 500): Promise<LiquidityPool[]> {
|
||||
if (String(process.env.TOKEN_AGGREGATION_SKIP_DB_READS ?? '0').toLowerCase() === '1') return [];
|
||||
try {
|
||||
const result = await this.pool.query(
|
||||
`SELECT id, chain_id, pool_address, token0_address, token1_address, dex_type,
|
||||
@@ -91,7 +101,7 @@ export class PoolRepository {
|
||||
);
|
||||
return result.rows.map((row) => this.mapRowToPool(row));
|
||||
} catch (error) {
|
||||
if (this.isMissingRelationError(error)) {
|
||||
if (this.shouldUseReadFallback(error)) {
|
||||
return [];
|
||||
}
|
||||
throw error;
|
||||
@@ -99,6 +109,7 @@ export class PoolRepository {
|
||||
}
|
||||
|
||||
async getPoolsByToken(chainId: number, tokenAddress: string): Promise<LiquidityPool[]> {
|
||||
if (String(process.env.TOKEN_AGGREGATION_SKIP_DB_READS ?? '0').toLowerCase() === '1') return [];
|
||||
try {
|
||||
const result = await this.pool.query(
|
||||
`SELECT id, chain_id, pool_address, token0_address, token1_address, dex_type,
|
||||
@@ -112,7 +123,7 @@ export class PoolRepository {
|
||||
|
||||
return result.rows.map((row) => this.mapRowToPool(row));
|
||||
} catch (error) {
|
||||
if (this.isMissingRelationError(error)) {
|
||||
if (this.shouldUseReadFallback(error)) {
|
||||
return [];
|
||||
}
|
||||
throw error;
|
||||
|
||||
@@ -46,7 +46,16 @@ export class TokenRepository {
|
||||
return code === '42P01' || (message.includes('relation "') && message.includes('" does not exist'));
|
||||
}
|
||||
|
||||
private shouldUseReadFallback(error: unknown): boolean {
|
||||
if (this.isMissingRelationError(error)) return true;
|
||||
if (String(process.env.TOKEN_AGGREGATION_DB_READ_FALLBACK ?? '1').toLowerCase() === '0') return false;
|
||||
const message = (error as { message?: string })?.message || '';
|
||||
const code = (error as { code?: string })?.code || '';
|
||||
return ['ETIMEDOUT', 'ECONNREFUSED', 'ENOTFOUND'].includes(code) || /timeout|connect/i.test(message);
|
||||
}
|
||||
|
||||
async getToken(chainId: number, address: string): Promise<Token | null> {
|
||||
if (String(process.env.TOKEN_AGGREGATION_SKIP_DB_READS ?? '0').toLowerCase() === '1') return null;
|
||||
try {
|
||||
const result = await this.pool.query(
|
||||
`SELECT chain_id, address, name, symbol, decimals, total_supply, logo_url, website_url, description, verified
|
||||
@@ -73,7 +82,7 @@ export class TokenRepository {
|
||||
verified: row.verified,
|
||||
};
|
||||
} catch (error) {
|
||||
if (this.isMissingRelationError(error)) {
|
||||
if (this.shouldUseReadFallback(error)) {
|
||||
return null;
|
||||
}
|
||||
throw error;
|
||||
@@ -81,6 +90,7 @@ export class TokenRepository {
|
||||
}
|
||||
|
||||
async getTokens(chainId: number, limit: number = 50, offset: number = 0): Promise<Token[]> {
|
||||
if (String(process.env.TOKEN_AGGREGATION_SKIP_DB_READS ?? '0').toLowerCase() === '1') return [];
|
||||
try {
|
||||
const result = await this.pool.query(
|
||||
`SELECT chain_id, address, name, symbol, decimals, total_supply, logo_url, website_url, description, verified
|
||||
@@ -104,7 +114,7 @@ export class TokenRepository {
|
||||
verified: row.verified,
|
||||
}));
|
||||
} catch (error) {
|
||||
if (this.isMissingRelationError(error)) {
|
||||
if (this.shouldUseReadFallback(error)) {
|
||||
return [];
|
||||
}
|
||||
throw error;
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Contract, JsonRpcProvider } from 'ethers';
|
||||
const POOL_ABI = [
|
||||
'function _BASE_TOKEN_() view returns (address)',
|
||||
'function _QUOTE_TOKEN_() view returns (address)',
|
||||
'function getVaultReserve() view returns (uint256,uint256)',
|
||||
'function querySellBase(address,uint256) view returns (uint256,uint256)',
|
||||
'function querySellQuote(address,uint256) view returns (uint256,uint256)',
|
||||
];
|
||||
@@ -40,6 +41,25 @@ export async function pmmQuoteAmountOutFromChain(params: {
|
||||
}
|
||||
}
|
||||
|
||||
/** Best-effort reserve read for DODO-style PMM/DVM pools. */
|
||||
export async function pmmVaultReserveFromChain(params: {
|
||||
rpcUrl: string;
|
||||
poolAddress: string;
|
||||
}): Promise<{ baseReserveRaw: bigint; quoteReserveRaw: bigint } | null> {
|
||||
const { rpcUrl, poolAddress } = params;
|
||||
try {
|
||||
const provider = new JsonRpcProvider(rpcUrl);
|
||||
const pool = new Contract(poolAddress, POOL_ABI, provider);
|
||||
const [baseReserve, quoteReserve] = await pool.getVaultReserve();
|
||||
return {
|
||||
baseReserveRaw: BigInt(baseReserve.toString()),
|
||||
quoteReserveRaw: BigInt(quoteReserve.toString()),
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** RPC for PMM eth_call quotes on Chain 138 (optional; unset = skip on-chain override). */
|
||||
export function resolvePmmQuoteRpcUrl(): string {
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user