536 lines
18 KiB
TypeScript
536 lines
18 KiB
TypeScript
/**
|
|
* Integration tests for report API (CMC, CoinGecko)
|
|
* Uses native fetch + http server (no deprecated supertest)
|
|
*/
|
|
|
|
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(() => ({
|
|
getToken: jest.fn().mockResolvedValue(null),
|
|
})),
|
|
}));
|
|
jest.mock('../../database/repositories/market-data-repo', () => ({
|
|
MarketDataRepository: jest.fn().mockImplementation(() => ({
|
|
getMarketData: jest.fn().mockResolvedValue(null),
|
|
})),
|
|
}));
|
|
jest.mock('../../database/repositories/pool-repo', () => ({
|
|
PoolRepository: jest.fn().mockImplementation(() => ({
|
|
getPoolsByToken: jest.fn().mockResolvedValue([]),
|
|
getPoolsByChain: jest.fn().mockResolvedValue([]),
|
|
})),
|
|
}));
|
|
jest.mock('../../indexer/cross-chain-indexer', () => ({
|
|
buildCrossChainReport: jest.fn().mockResolvedValue({
|
|
generatedAt: '2026-03-30T00:00:00.000Z',
|
|
chainId: 138,
|
|
crossChainPools: [],
|
|
volumeByLane: [],
|
|
atomicSwapVolume24h: 0,
|
|
bridgeVolume24hTotal: 0,
|
|
events: [],
|
|
}),
|
|
}));
|
|
jest.mock('../middleware/cache');
|
|
|
|
function createApp() {
|
|
const app = express();
|
|
app.use('/api/v1/report', reportRoutes);
|
|
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('Report 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;
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await new Promise<void>((resolve, reject) => {
|
|
server.close((error) => {
|
|
if (error) {
|
|
reject(error);
|
|
return;
|
|
}
|
|
resolve();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('GET /api/v1/report/cmc', () => {
|
|
it('returns 200 with cmc format', async () => {
|
|
const res = await fetch(`${baseUrl}/api/v1/report/cmc?chainId=138`);
|
|
expect(res.status).toBe(200);
|
|
const body = (await res.json()) as Record<string, unknown>;
|
|
expect(body).toHaveProperty('generatedAt');
|
|
expect(body).toHaveProperty('chainId', 138);
|
|
expect(body).toHaveProperty('format', 'coinmarketcap-dex');
|
|
expect(body).toHaveProperty('tokens');
|
|
expect(Array.isArray(body.tokens)).toBe(true);
|
|
});
|
|
|
|
it('accepts chainId 651940', async () => {
|
|
const res = await fetch(`${baseUrl}/api/v1/report/cmc?chainId=651940`);
|
|
expect(res.status).toBe(200);
|
|
const body = (await res.json()) as Record<string, unknown>;
|
|
expect(body.chainId).toBe(651940);
|
|
});
|
|
});
|
|
|
|
describe('GET /api/v1/report/coingecko', () => {
|
|
it('returns 200 with coingecko format', async () => {
|
|
const res = await fetch(`${baseUrl}/api/v1/report/coingecko?chainId=138`);
|
|
expect(res.status).toBe(200);
|
|
const body = (await res.json()) as Record<string, unknown>;
|
|
expect(body).toHaveProperty('generatedAt');
|
|
expect(body).toHaveProperty('chainId', 138);
|
|
expect(body).toHaveProperty('format', 'coingecko-submission');
|
|
expect(body).toHaveProperty('tokens');
|
|
expect(Array.isArray(body.tokens)).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('GET /api/v1/report/all', () => {
|
|
it('includes GRU transport summary for operator visibility', 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>;
|
|
expect(body.gruTransport?.system?.name).toBe('GRU Monetary Transport Layer');
|
|
expect(body.gruTransport?.summary).toMatchObject({
|
|
transportPairs: expect.any(Number),
|
|
runtimeReadyTransportPairs: expect.any(Number),
|
|
});
|
|
expect(body.gruTransport?.gasAssetFamilies).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
familyKey: 'eth_l2',
|
|
backingMode: 'hybrid_cap',
|
|
}),
|
|
])
|
|
);
|
|
});
|
|
|
|
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', () => {
|
|
it('returns both chain summaries and runtime pairs for gas rollout consumers', async () => {
|
|
const res = await fetch(`${baseUrl}/api/v1/report/gas-registry`);
|
|
expect(res.status).toBe(200);
|
|
const body = (await res.json()) as Record<string, any>;
|
|
expect(body.gasAssetFamilies).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
familyKey: 'eth_mainnet',
|
|
}),
|
|
])
|
|
);
|
|
expect(body.runtimePairs).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
key: '138-1-cETH-cWETH',
|
|
familyKey: 'eth_mainnet',
|
|
destinationChainId: 1,
|
|
destinationChainName: 'Ethereum Mainnet',
|
|
wrappedNativeQuoteSymbol: 'WETH',
|
|
stableQuoteSymbol: 'USDC',
|
|
}),
|
|
])
|
|
);
|
|
expect(body.chains).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
chainId: 1,
|
|
}),
|
|
])
|
|
);
|
|
});
|
|
});
|
|
|
|
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`);
|
|
expect(res.status).toBe(200);
|
|
const body = (await res.json()) as Record<string, any>;
|
|
expect(body.tokens).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
symbol: 'cUSDT',
|
|
chainId: 138,
|
|
}),
|
|
expect.objectContaining({
|
|
symbol: 'cUSDT_V2',
|
|
chainId: 138,
|
|
familySymbol: 'cUSDT',
|
|
deploymentVersion: 'v2',
|
|
preferredForX402: true,
|
|
}),
|
|
expect.objectContaining({
|
|
symbol: 'cUSDC',
|
|
chainId: 138,
|
|
}),
|
|
expect.objectContaining({
|
|
symbol: 'cUSDC_V2',
|
|
chainId: 138,
|
|
familySymbol: 'cUSDC',
|
|
deploymentVersion: 'v2',
|
|
preferredForX402: true,
|
|
}),
|
|
])
|
|
);
|
|
});
|
|
|
|
it('surfaces the cUSDW hub asset on Chain 138 and cWUSDW on active edge chains', async () => {
|
|
const chain138Res = await fetch(`${baseUrl}/api/v1/report/token-list?chainId=138`);
|
|
expect(chain138Res.status).toBe(200);
|
|
const chain138Body = (await chain138Res.json()) as Record<string, any>;
|
|
expect(chain138Body.tokens).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
symbol: 'cUSDW',
|
|
chainId: 138,
|
|
}),
|
|
])
|
|
);
|
|
|
|
const bscRes = await fetch(`${baseUrl}/api/v1/report/token-list?chainId=56`);
|
|
expect(bscRes.status).toBe(200);
|
|
const bscBody = (await bscRes.json()) as Record<string, any>;
|
|
expect(bscBody.tokens).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
symbol: 'cWUSDW',
|
|
chainId: 56,
|
|
}),
|
|
])
|
|
);
|
|
});
|
|
|
|
it('surfaces cAUSDT on Chain 138 when configured and cWAUSDT on active edge chains', async () => {
|
|
const chain138Res = await fetch(`${baseUrl}/api/v1/report/token-list?chainId=138`);
|
|
expect(chain138Res.status).toBe(200);
|
|
const chain138Body = (await chain138Res.json()) as Record<string, any>;
|
|
expect(chain138Body.tokens).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
symbol: 'cAUSDT',
|
|
chainId: 138,
|
|
}),
|
|
])
|
|
);
|
|
|
|
const bscRes = await fetch(`${baseUrl}/api/v1/report/token-list?chainId=56`);
|
|
expect(bscRes.status).toBe(200);
|
|
const bscBody = (await bscRes.json()) as Record<string, any>;
|
|
expect(bscBody.tokens).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
symbol: 'cWAUSDT',
|
|
chainId: 56,
|
|
}),
|
|
])
|
|
);
|
|
|
|
const polygonRes = await fetch(`${baseUrl}/api/v1/report/token-list?chainId=137`);
|
|
expect(polygonRes.status).toBe(200);
|
|
const polygonBody = (await polygonRes.json()) as Record<string, any>;
|
|
expect(polygonBody.tokens).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
symbol: 'cWAUSDT',
|
|
chainId: 137,
|
|
}),
|
|
])
|
|
);
|
|
|
|
const avalancheRes = await fetch(`${baseUrl}/api/v1/report/token-list?chainId=43114`);
|
|
expect(avalancheRes.status).toBe(200);
|
|
const avalancheBody = (await avalancheRes.json()) as Record<string, any>;
|
|
expect(avalancheBody.tokens).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
symbol: 'cWAUSDT',
|
|
chainId: 43114,
|
|
}),
|
|
])
|
|
);
|
|
|
|
const celoRes = await fetch(`${baseUrl}/api/v1/report/token-list?chainId=42220`);
|
|
expect(celoRes.status).toBe(200);
|
|
const celoBody = (await celoRes.json()) as Record<string, any>;
|
|
expect(celoBody.tokens).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
symbol: 'cWAUSDT',
|
|
chainId: 42220,
|
|
}),
|
|
])
|
|
);
|
|
});
|
|
|
|
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);
|
|
const chain138Body = (await chain138Res.json()) as Record<string, any>;
|
|
expect(chain138Body.tokens).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
symbol: 'cBTC',
|
|
chainId: 138,
|
|
registryFamily: 'monetary_unit',
|
|
}),
|
|
])
|
|
);
|
|
|
|
const mainnetRes = await fetch(`${baseUrl}/api/v1/report/token-list?chainId=1`);
|
|
expect(mainnetRes.status).toBe(200);
|
|
const mainnetBody = (await mainnetRes.json()) as Record<string, any>;
|
|
expect(mainnetBody.tokens).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
symbol: 'cWBTC',
|
|
chainId: 1,
|
|
registryFamily: 'monetary_unit',
|
|
}),
|
|
])
|
|
);
|
|
});
|
|
|
|
it('surfaces gas-native canonicals on Chain 138 and mirrored cW gas tokens on their public lanes', async () => {
|
|
const chain138Res = await fetch(`${baseUrl}/api/v1/report/token-list?chainId=138`);
|
|
expect(chain138Res.status).toBe(200);
|
|
const chain138Body = (await chain138Res.json()) as Record<string, any>;
|
|
expect(chain138Body.tokens).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
symbol: 'cETH',
|
|
chainId: 138,
|
|
registryFamily: 'gas_native',
|
|
}),
|
|
expect.objectContaining({
|
|
symbol: 'cETHL2',
|
|
chainId: 138,
|
|
registryFamily: 'gas_native',
|
|
}),
|
|
])
|
|
);
|
|
|
|
const optimismRes = await fetch(`${baseUrl}/api/v1/report/token-list?chainId=10`);
|
|
expect(optimismRes.status).toBe(200);
|
|
const optimismBody = (await optimismRes.json()) as Record<string, any>;
|
|
expect(optimismBody.tokens).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
symbol: 'cWETHL2',
|
|
chainId: 10,
|
|
registryFamily: 'gas_native',
|
|
}),
|
|
])
|
|
);
|
|
|
|
const mainnetRes = await fetch(`${baseUrl}/api/v1/report/token-list?chainId=1`);
|
|
expect(mainnetRes.status).toBe(200);
|
|
const mainnetBody = (await mainnetRes.json()) as Record<string, any>;
|
|
expect(mainnetBody.tokens).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
symbol: 'cWETH',
|
|
chainId: 1,
|
|
registryFamily: 'gas_native',
|
|
}),
|
|
])
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('GET /api/v1/report/cw-registry', () => {
|
|
it('reads the live cW registry from deployment-status json when available', async () => {
|
|
const previousPath = process.env.DEPLOYMENT_STATUS_JSON_PATH;
|
|
const tempPath = `/tmp/token-aggregation-cw-registry-${Date.now()}.json`;
|
|
|
|
process.env.DEPLOYMENT_STATUS_JSON_PATH = tempPath;
|
|
await import('fs/promises').then((fs) =>
|
|
fs.writeFile(
|
|
tempPath,
|
|
JSON.stringify(
|
|
{
|
|
version: 'test-1',
|
|
updated: '2026-04-04',
|
|
chains: {
|
|
'56': {
|
|
name: 'BSC',
|
|
cwTokens: {
|
|
cWAUSDT: '0xe1a51Bc037a79AB36767561B147eb41780124934',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
null,
|
|
2
|
|
)
|
|
)
|
|
);
|
|
|
|
try {
|
|
const res = await fetch(`${baseUrl}/api/v1/report/cw-registry?chainId=56`);
|
|
expect(res.status).toBe(200);
|
|
const body = (await res.json()) as Record<string, any>;
|
|
expect(body.source).toBe('deployment-status-file');
|
|
expect(body.complete).toBe(true);
|
|
expect(body.version).toBe('test-1');
|
|
expect(body.chains).toEqual([
|
|
expect.objectContaining({
|
|
chainId: 56,
|
|
name: 'BSC',
|
|
tokens: [
|
|
expect.objectContaining({
|
|
symbol: 'cWAUSDT',
|
|
}),
|
|
],
|
|
}),
|
|
]);
|
|
} 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/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`);
|
|
expect(res.status).toBe(200);
|
|
const body = (await res.json()) as Record<string, any>;
|
|
expect(body.source).toBe('deployment-status-file');
|
|
expect(body.gasAssetFamilies).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
familyKey: 'eth_l2',
|
|
backingMode: 'hybrid_cap',
|
|
}),
|
|
])
|
|
);
|
|
expect(body.chains).toEqual([
|
|
expect.objectContaining({
|
|
chainId: 10,
|
|
families: [
|
|
expect.objectContaining({
|
|
familyKey: 'eth_l2',
|
|
mirroredSymbol: 'cWETHL2',
|
|
dodoPmm: expect.arrayContaining([
|
|
expect.objectContaining({
|
|
quote: 'WETH',
|
|
}),
|
|
]),
|
|
}),
|
|
],
|
|
}),
|
|
]);
|
|
});
|
|
});
|
|
});
|