feat: expand non-evm relay and route planning support

This commit is contained in:
defiQUG
2026-04-18 12:05:34 -07:00
parent da78073104
commit 843cdbf71c
113 changed files with 8542 additions and 222 deletions

View File

@@ -6,6 +6,7 @@
import { createServer } from 'http';
import express from 'express';
import reportRoutes from './report';
import { getCanonicalTokenBySymbol } from '../../config/canonical-tokens';
jest.mock('../../database/repositories/token-repo', () => ({
TokenRepository: jest.fn().mockImplementation(() => ({
@@ -124,6 +125,30 @@ describe('Report API', () => {
])
);
});
it('fills canonical fallback usd pricing when market data is absent', async () => {
const weth = getCanonicalTokenBySymbol(138, 'WETH');
expect(weth?.addresses[138]).toBeTruthy();
const wethAddress = String(weth?.addresses[138]).toLowerCase();
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 tokens138 = body.tokens?.['138'];
expect(Array.isArray(tokens138)).toBe(true);
const wethEntry = tokens138.find((token: Record<string, any>) => token.address === wethAddress);
expect(wethEntry).toMatchObject({
symbol: 'WETH',
decimals: 18,
market: expect.objectContaining({
priceUsd: 2490,
volume24h: 0,
liquidityUsd: 0,
lastUpdated: '2026-04-15T00:00:00.000Z',
}),
});
});
});
describe('GET /api/v1/report/gas-registry', () => {
@@ -413,6 +438,68 @@ describe('Report API', () => {
});
});
describe('GET /api/v1/report/gru-v2-pmm-pools', () => {
it('returns resolved PMM pools from deployment-status when file is set', async () => {
const previousPath = process.env.DEPLOYMENT_STATUS_JSON_PATH;
const tempPath = `/tmp/token-aggregation-gru-v2-pmm-${Date.now()}.json`;
process.env.DEPLOYMENT_STATUS_JSON_PATH = tempPath;
await import('fs/promises').then((fs) =>
fs.writeFile(
tempPath,
JSON.stringify(
{
version: 'test-gru-pools',
updated: '2026-04-18',
homeChainId: 138,
chains: {
'1': {
name: 'Ethereum Mainnet',
cwTokens: { cWUSDT: '0xaf5017d0163ecb99d9b5d94e3b4d7b09af44d8ae' },
anchorAddresses: { USDC: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48' },
pmmPools: [
{
base: 'cWUSDT',
quote: 'USDC',
poolAddress: '0x1111111111111111111111111111111111111111',
feeBps: 3,
role: 'public_routing',
publicRoutingEnabled: true,
},
],
},
},
},
null,
2
)
)
);
try {
const res = await fetch(`${baseUrl}/api/v1/report/gru-v2-pmm-pools?chainId=1`);
expect(res.status).toBe(200);
const body = (await res.json()) as Record<string, unknown>;
expect(body.source).toBe('deployment-status-file');
expect(body.complete).toBe(true);
expect(body.version).toBe('test-gru-pools');
expect(Array.isArray(body.pools)).toBe(true);
expect((body.pools as unknown[]).length).toBeGreaterThanOrEqual(1);
expect((body.pools as Array<{ poolAddress: string }>)[0]).toMatchObject({
poolAddress: '0x1111111111111111111111111111111111111111',
section: 'pmmPools',
});
} finally {
await import('fs/promises').then((fs) => fs.unlink(tempPath).catch(() => undefined));
if (previousPath === undefined) {
delete process.env.DEPLOYMENT_STATUS_JSON_PATH;
} else {
process.env.DEPLOYMENT_STATUS_JSON_PATH = previousPath;
}
}
});
});
describe('GET /api/v1/report/gas-registry', () => {
it('reads the live gas rollout registry from deployment-status json when available', async () => {
const res = await fetch(`${baseUrl}/api/v1/report/gas-registry?chainId=10`);

View File

@@ -26,6 +26,8 @@ import {
loadDeploymentStatusFile,
type CwRegistryChain,
} from '../../config/deployment-status';
import { getGruV2DeploymentPoolRows } from '../../config/gru-v2-deployment-pools';
import { getCanonicalPriceSnapshotGeneratedAt, getCanonicalPriceUsd } from '../../services/canonical-price-oracle';
const router: Router = Router();
const tokenRepo = new TokenRepository();
@@ -94,6 +96,8 @@ async function buildTokenReport(chainId: number) {
})
);
const fallbackPriceUsd = getCanonicalPriceUsd(chainId, address);
out.push({
chainId,
address: address.toLowerCase(),
@@ -110,7 +114,7 @@ async function buildTokenReport(chainId: number) {
liquiditySourceSymbol: spec.liquiditySourceSymbol,
market: marketData
? {
priceUsd: marketData.priceUsd,
priceUsd: marketData.priceUsd ?? fallbackPriceUsd,
volume24h: marketData.volume24h,
volume7d: marketData.volume7d,
volume30d: marketData.volume30d,
@@ -118,6 +122,15 @@ async function buildTokenReport(chainId: number) {
liquidityUsd: marketData.liquidityUsd,
lastUpdated: marketData.lastUpdated?.toISOString() ?? '',
}
: fallbackPriceUsd !== undefined
? {
priceUsd: fallbackPriceUsd,
volume24h: 0,
volume7d: 0,
volume30d: 0,
liquidityUsd: 0,
lastUpdated: `${getCanonicalPriceSnapshotGeneratedAt()}T00:00:00.000Z`,
}
: undefined,
pools: resolvedPools.map((p) => ({
poolAddress: p.poolAddress,
@@ -543,6 +556,36 @@ router.get('/cw-registry', async (req: Request, res: Response) => {
}
});
/** GET /report/gru-v2-pmm-pools — all GRU v2 PMM pools from deployment-status (stable, volatile, gas) with resolved token addresses. */
router.get('/gru-v2-pmm-pools', async (req: Request, res: Response) => {
try {
const chainIdParam = req.query.chainId as string | undefined;
const chainIdFilter = chainIdParam ? parseInt(chainIdParam, 10) : null;
const fileBacked = loadDeploymentStatusFile();
let pools = getGruV2DeploymentPoolRows();
if (chainIdFilter && !Number.isNaN(chainIdFilter)) {
pools = pools.filter((p) => p.chainId === chainIdFilter);
}
res.set('Cache-Control', 'public, max-age=0, must-revalidate');
res.json({
generatedAt: new Date().toISOString(),
source: fileBacked ? 'deployment-status-file' : 'none',
complete: !!fileBacked,
version: fileBacked?.data.version,
updated: fileBacked?.data.updated,
lastModified: fileBacked?.lastModified,
homeChainId: fileBacked?.data.homeChainId,
count: pools.length,
pools,
});
} catch (error) {
logger.error('Error building report/gru-v2-pmm-pools:', error);
res.status(500).json({ error: 'Internal server error', pools: [] });
}
});
/** GET /report/gas-registry — live gas-family rollout registry from deployment-status.json plus GRU transport metadata. */
router.get('/gas-registry', async (req: Request, res: Response) => {
try {

View File

@@ -0,0 +1,219 @@
import { createServer } from 'http';
import express from 'express';
import { getCanonicalTokenBySymbol } from '../../config/canonical-tokens';
const mockGetTokens = jest.fn();
const mockGetToken = jest.fn();
const mockSearchTokens = jest.fn();
const mockGetMarketData = jest.fn();
const mockGetPoolsByToken = jest.fn();
const mockGetPool = jest.fn();
const mockGetLiveDodoPools = jest.fn();
const mockResolveTokenDisplay = jest.fn();
const mockResolvePoolTokenDisplays = jest.fn();
const mockGetTokenByContract = jest.fn();
jest.mock('../../database/repositories/token-repo', () => ({
TokenRepository: jest.fn().mockImplementation(() => ({
getTokens: mockGetTokens,
getToken: mockGetToken,
searchTokens: mockSearchTokens,
})),
}));
jest.mock('../../database/repositories/market-data-repo', () => ({
MarketDataRepository: jest.fn().mockImplementation(() => ({
getMarketData: mockGetMarketData,
})),
}));
jest.mock('../../database/repositories/pool-repo', () => ({
PoolRepository: jest.fn().mockImplementation(() => ({
getPoolsByToken: mockGetPoolsByToken,
getPool: mockGetPool,
})),
}));
jest.mock('../../indexer/ohlcv-generator', () => ({
OHLCVGenerator: jest.fn().mockImplementation(() => ({
getOHLCV: jest.fn().mockResolvedValue([]),
})),
}));
const mockGetMarketDataAdapter = jest.fn();
jest.mock('../../adapters/coingecko-adapter', () => ({
CoinGeckoAdapter: jest.fn().mockImplementation(() => ({
getTokenByContract: mockGetTokenByContract,
getMarketData: mockGetMarketDataAdapter,
getTrending: jest.fn().mockResolvedValue([]),
})),
}));
jest.mock('../../adapters/cmc-adapter', () => ({
CoinMarketCapAdapter: jest.fn().mockImplementation(() => ({
getTokenByContract: mockGetTokenByContract,
getMarketData: mockGetMarketDataAdapter,
})),
}));
jest.mock('../../adapters/dexscreener-adapter', () => ({
DexScreenerAdapter: jest.fn().mockImplementation(() => ({
getTokenByContract: mockGetTokenByContract,
getMarketData: mockGetMarketDataAdapter,
})),
}));
jest.mock('../../services/live-dodo-fallback', () => ({
getLiveDodoPools: (...args: unknown[]) => mockGetLiveDodoPools(...args),
}));
jest.mock('../../services/token-display', () => ({
resolveTokenDisplay: (...args: unknown[]) => mockResolveTokenDisplay(...args),
resolvePoolTokenDisplays: (...args: unknown[]) => mockResolvePoolTokenDisplays(...args),
}));
jest.mock('../middleware/cache');
const tokensRoutes = require('./tokens').default as typeof import('./tokens').default;
function createApp() {
const app = express();
app.use('/api/v1', tokensRoutes);
return app;
}
async function startServer(app: express.Application): Promise<{ server: ReturnType<typeof createServer>; baseUrl: string }> {
const server = createServer(app);
await new Promise<void>((resolve) => server.listen(0, () => resolve()));
const port = (server.address() as { port: number }).port;
return { server, baseUrl: `http://127.0.0.1:${port}` };
}
describe('Tokens API', () => {
let server: ReturnType<typeof createServer>;
let baseUrl: string;
beforeAll(async () => {
const app = createApp();
const started = await startServer(app);
server = started.server;
baseUrl = started.baseUrl;
});
beforeEach(() => {
jest.clearAllMocks();
mockGetMarketDataAdapter.mockResolvedValue(null);
mockGetTokens.mockResolvedValue([]);
mockGetToken.mockResolvedValue(null);
mockSearchTokens.mockResolvedValue([]);
mockGetMarketData.mockResolvedValue(null);
mockGetPoolsByToken.mockResolvedValue([]);
mockGetPool.mockResolvedValue(null);
mockGetLiveDodoPools.mockResolvedValue([]);
mockResolveTokenDisplay.mockResolvedValue({
address: '',
name: 'Unknown Token',
symbol: 'UNKNOWN',
decimals: 18,
source: 'fallback',
});
mockResolvePoolTokenDisplays.mockResolvedValue({
token0: { address: '', symbol: 'UNKNOWN', name: 'Unknown Token', source: 'fallback' },
token1: { address: '', symbol: 'UNKNOWN', name: 'Unknown Token', source: 'fallback' },
});
mockGetTokenByContract.mockResolvedValue(null);
});
afterAll(async () => {
await new Promise<void>((resolve, reject) => {
server.close((error) => {
if (error) {
reject(error);
return;
}
resolve();
});
});
});
it('lists canonical 138 tokens with stable and ETH-family fallback pricing when db market data is missing', async () => {
const usdt = getCanonicalTokenBySymbol(138, 'USDT');
const weth = getCanonicalTokenBySymbol(138, 'WETH');
const weth10 = getCanonicalTokenBySymbol(138, 'WETH10');
expect(usdt?.addresses[138]).toBeTruthy();
expect(weth?.addresses[138]).toBeTruthy();
expect(weth10?.addresses[138]).toBeTruthy();
const res = await fetch(`${baseUrl}/api/v1/tokens?chainId=138&limit=400`);
expect(res.status).toBe(200);
const body = (await res.json()) as Record<string, any>;
expect(body.source).toBe('canonical');
const findByAddress = (address?: string) =>
body.tokens.find((token: Record<string, any>) => token.address === address?.toLowerCase());
expect(findByAddress(usdt?.addresses[138])).toMatchObject({
symbol: 'USDT',
decimals: 6,
market: expect.objectContaining({
priceUsd: 1,
volume24h: 0,
liquidityUsd: 0,
}),
});
expect(findByAddress(weth?.addresses[138])).toMatchObject({
symbol: 'WETH',
decimals: 18,
market: expect.objectContaining({
priceUsd: 2490,
}),
});
expect(findByAddress(weth10?.addresses[138])).toMatchObject({
symbol: 'WETH10',
decimals: 18,
market: expect.objectContaining({
priceUsd: 2490,
}),
});
});
it('fills missing priceUsd on token detail responses while preserving repository market fields', async () => {
const weth10 = getCanonicalTokenBySymbol(138, 'WETH10');
expect(weth10?.addresses[138]).toBeTruthy();
const weth10Address = String(weth10?.addresses[138]).toLowerCase();
mockGetMarketData.mockResolvedValue({
chainId: 138,
tokenAddress: weth10Address,
priceUsd: undefined,
volume24h: 1234,
volume7d: 5678,
volume30d: 9012,
liquidityUsd: 3456,
holdersCount: 78,
transfers24h: 9,
lastUpdated: new Date('2026-04-16T00:00:00.000Z'),
});
const res = await fetch(`${baseUrl}/api/v1/tokens/${weth10Address}?chainId=138`);
expect(res.status).toBe(200);
const body = (await res.json()) as Record<string, any>;
expect(body.token).toMatchObject({
symbol: 'WETH10',
decimals: 18,
market: expect.objectContaining({
priceUsd: 2490,
volume24h: 1234,
liquidityUsd: 3456,
}),
hasDodoPool: false,
});
expect(body.token.canonicalLiquidity).toBeUndefined();
});
});

View File

@@ -16,6 +16,11 @@ import {
resolveCanonicalQuoteAddress,
} from '../../config/canonical-tokens';
import { getLiveDodoPools } from '../../services/live-dodo-fallback';
import {
buildExplorerLinks,
mergeMarketWithValuation,
resolveUsdValuation,
} from '../../services/valuation-precedence';
const router: Router = Router();
const tokenRepo = new TokenRepository();
@@ -26,6 +31,26 @@ const coingeckoAdapter = new CoinGeckoAdapter();
const cmcAdapter = new CoinMarketCapAdapter();
const dexscreenerAdapter = new DexScreenerAdapter();
function buildMarketPricingExplorer(
chainId: number,
displayAddress: string,
lookupAddress: string,
marketData: Awaited<ReturnType<MarketDataRepository['getMarketData']>>,
external: { coingecko?: Awaited<ReturnType<CoinGeckoAdapter['getMarketData']>>; cmc?: Awaited<ReturnType<CoinMarketCapAdapter['getMarketData']>>; dexscreener?: Awaited<ReturnType<DexScreenerAdapter['getMarketData']>> } | null
) {
const pricing = resolveUsdValuation({
chainId,
normalizedAddress: lookupAddress.toLowerCase(),
indexer: marketData,
coingecko: external?.coingecko ?? undefined,
cmc: external?.cmc ?? undefined,
dexscreener: external?.dexscreener ?? undefined,
});
const market = mergeMarketWithValuation(chainId, displayAddress.toLowerCase(), marketData, pricing);
const explorer = buildExplorerLinks(chainId, displayAddress);
return { market, pricing, explorer };
}
function tokenFromCanonical(chainId: number, address: string): Token | null {
const spec = getCanonicalTokenByAddress(chainId, address.toLowerCase());
if (!spec) {
@@ -182,10 +207,20 @@ router.get('/tokens', cacheMiddleware(60 * 1000), async (req: Request, res: Resp
const { tokens, source } = await getTokensWithFallback(chainId, limit, offset);
const tokensWithMarketData = await Promise.all(
tokens.map(async (token) => {
const marketData = await marketDataRepo.getMarketData(chainId, token.address);
const resolution = resolveCanonicalQuoteAddress(chainId, token.address);
const marketData = await marketDataRepo.getMarketData(chainId, resolution.lookupAddress);
const { market, pricing, explorer } = buildMarketPricingExplorer(
chainId,
token.address,
resolution.lookupAddress,
marketData,
null
);
const out: Record<string, unknown> = {
...token,
market: marketData || undefined,
market: market || undefined,
pricing,
explorer,
};
if (includeDodoPool) {
const pools = await getPoolsByTokenWithFallback(chainId, token.address);
@@ -228,13 +263,32 @@ router.get('/tokens/:address', cacheMiddleware(60 * 1000), async (req: Request,
return res.status(404).json({ error: 'Token not found' });
}
const [marketData, pools, coingeckoData, cmcData, dexscreenerData] = await Promise.all([
const [
marketDataRaw,
pools,
coingeckoData,
cmcData,
dexscreenerData,
coingeckoMarket,
cmcMarket,
dexscreenerMarket,
] = await Promise.all([
marketDataRepo.getMarketData(chainId, resolution.lookupAddress),
getPoolsByTokenWithFallback(chainId, normalizedAddress),
coingeckoAdapter.getTokenByContract(chainId, resolution.lookupAddress),
cmcAdapter.getTokenByContract(chainId, resolution.lookupAddress),
dexscreenerAdapter.getTokenByContract(chainId, resolution.lookupAddress),
coingeckoAdapter.getMarketData(chainId, resolution.lookupAddress),
cmcAdapter.getMarketData(chainId, resolution.lookupAddress),
dexscreenerAdapter.getMarketData(chainId, resolution.lookupAddress),
]);
const { market: marketData, pricing, explorer } = buildMarketPricingExplorer(
chainId,
normalizedAddress,
resolution.lookupAddress,
marketDataRaw,
{ coingecko: coingeckoMarket, cmc: cmcMarket, dexscreener: dexscreenerMarket }
);
res.json({
token: {
@@ -243,6 +297,8 @@ router.get('/tokens/:address', cacheMiddleware(60 * 1000), async (req: Request,
totalSupply: token.totalSupply,
},
market: marketData || undefined,
pricing,
explorer,
external: {
coingecko: coingeckoData || undefined,
cmc: cmcData || undefined,