feat: bridges, PMM, flash workflow, token-aggregation, and deployment docs

- CCIP/trustless bridge contracts, GRU tokens, DEX/PMM tests, reserve vault.
- Token-aggregation service routes, planner, chain config, relay env templates.
- Config snapshots and multi-chain deployment markdown updates.
- gitignore services/btc-intake/dist/ (tsc output); do not track dist.

Run forge build && forge test before deploy (large solc graph).

Made-with: Cursor
This commit is contained in:
defiQUG
2026-04-07 23:40:52 -07:00
parent 0fb7bba07b
commit 76aa419320
289 changed files with 28367 additions and 824 deletions

View File

@@ -10,22 +10,31 @@ const DEFAULT_TTL = 60 * 1000; // 1 minute
export function cacheMiddleware(ttl: number = DEFAULT_TTL) {
return (req: Request, res: Response, next: NextFunction) => {
const bypassCache =
req.query.refresh === '1' ||
req.query.noCache === '1' ||
/\bno-cache\b|\bno-store\b/i.test(req.header('cache-control') || '');
const key = `${req.method}:${req.originalUrl}`;
const cached = cache.get(key);
const cached = bypassCache ? undefined : cache.get(key);
if (cached && cached.expiresAt > Date.now()) {
res.setHeader('X-Token-Aggregation-Cache', 'hit');
return res.json(cached.data);
}
res.setHeader('X-Token-Aggregation-Cache', bypassCache ? 'bypass' : 'miss');
// Store original json method
const originalJson = res.json.bind(res);
// Override json method to cache response
res.json = function (body: unknown) {
cache.set(key, {
data: body,
expiresAt: Date.now() + ttl,
});
if (!bypassCache) {
cache.set(key, {
data: body,
expiresAt: Date.now() + ttl,
});
}
return originalJson(body);
};

View File

@@ -0,0 +1,247 @@
import { createServer } from 'http';
import express from 'express';
import fs from 'fs/promises';
import path from 'path';
import bridgeRoutes from './bridge';
jest.mock('../middleware/cache');
function createApp() {
const app = express();
app.use('/api/v1/bridge', bridgeRoutes);
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('Bridge API GRU Transport status', () => {
let server: ReturnType<typeof createServer>;
let baseUrl: string;
const originalChain138Bridge = process.env.CHAIN138_L1_BRIDGE;
const originalBscBridge = process.env.CW_BRIDGE_BSC;
const originalReserveVerifier = process.env.CW_RESERVE_VERIFIER_CHAIN138;
const originalReserveVault = process.env.CW_STABLECOIN_RESERVE_VAULT;
const originalReserveSystem = process.env.CW_RESERVE_SYSTEM;
const originalMaxOutstanding = process.env.CW_MAX_OUTSTANDING_USDT_BSC;
const originalBridgeListPath = process.env.BRIDGE_LIST_JSON_PATH;
const originalBridgeListUrl = process.env.BRIDGE_LIST_JSON_URL;
const originalCwL1Bridge = process.env.CW_L1_BRIDGE;
const originalCwL1BridgeChain138 = process.env.CW_L1_BRIDGE_CHAIN138;
beforeAll(async () => {
const started = await startServer(createApp());
server = started.server;
baseUrl = started.baseUrl;
});
afterEach(() => {
if (originalChain138Bridge === undefined) {
delete process.env.CHAIN138_L1_BRIDGE;
} else {
process.env.CHAIN138_L1_BRIDGE = originalChain138Bridge;
}
if (originalBscBridge === undefined) {
delete process.env.CW_BRIDGE_BSC;
} else {
process.env.CW_BRIDGE_BSC = originalBscBridge;
}
if (originalReserveVerifier === undefined) {
delete process.env.CW_RESERVE_VERIFIER_CHAIN138;
} else {
process.env.CW_RESERVE_VERIFIER_CHAIN138 = originalReserveVerifier;
}
if (originalReserveVault === undefined) {
delete process.env.CW_STABLECOIN_RESERVE_VAULT;
} else {
process.env.CW_STABLECOIN_RESERVE_VAULT = originalReserveVault;
}
if (originalReserveSystem === undefined) {
delete process.env.CW_RESERVE_SYSTEM;
} else {
process.env.CW_RESERVE_SYSTEM = originalReserveSystem;
}
if (originalMaxOutstanding === undefined) {
delete process.env.CW_MAX_OUTSTANDING_USDT_BSC;
} else {
process.env.CW_MAX_OUTSTANDING_USDT_BSC = originalMaxOutstanding;
}
if (originalBridgeListPath === undefined) {
delete process.env.BRIDGE_LIST_JSON_PATH;
} else {
process.env.BRIDGE_LIST_JSON_PATH = originalBridgeListPath;
}
if (originalBridgeListUrl === undefined) {
delete process.env.BRIDGE_LIST_JSON_URL;
} else {
process.env.BRIDGE_LIST_JSON_URL = originalBridgeListUrl;
}
if (originalCwL1Bridge === undefined) {
delete process.env.CW_L1_BRIDGE;
} else {
process.env.CW_L1_BRIDGE = originalCwL1Bridge;
}
if (originalCwL1BridgeChain138 === undefined) {
delete process.env.CW_L1_BRIDGE_CHAIN138;
} else {
process.env.CW_L1_BRIDGE_CHAIN138 = originalCwL1BridgeChain138;
}
});
afterAll((done) => {
server.close(done);
});
it('returns GRU transport pair runtime readiness on /status', async () => {
process.env.CHAIN138_L1_BRIDGE = '0x3333333333333333333333333333333333333333';
process.env.CW_BRIDGE_BSC = '0x4444444444444444444444444444444444444444';
process.env.CW_RESERVE_VERIFIER_CHAIN138 = '0x5555555555555555555555555555555555555555';
process.env.CW_STABLECOIN_RESERVE_VAULT = '0x6666666666666666666666666666666666666666';
process.env.CW_RESERVE_SYSTEM = '0x7777777777777777777777777777777777777777';
process.env.CW_MAX_OUTSTANDING_USDT_BSC = '1000000';
const res = await fetch(`${baseUrl}/api/v1/bridge/status`);
expect(res.status).toBe(200);
const body = (await res.json()) as Record<string, any>;
expect(body.gruTransport?.summary?.runtimeReadyTransportPairs).toEqual(expect.any(Number));
expect(body.gruTransport?.pairs).toEqual(
expect.arrayContaining([
expect.objectContaining({
key: '138-56-cUSDT-cWUSDT',
runtimeReady: true,
}),
expect.objectContaining({
key: '138-10-cETHL2-cWETHL2',
assetClass: 'gas_native',
familyKey: 'eth_l2',
backingMode: 'hybrid_cap',
}),
])
);
expect(body.gruTransport?.gasAssetFamilies).toEqual(
expect.arrayContaining([
expect.objectContaining({
familyKey: 'eth_mainnet',
backingMode: 'strict_escrow',
}),
])
);
});
it('returns GRU transport summary counts on /metrics', async () => {
const res = await fetch(`${baseUrl}/api/v1/bridge/metrics`);
expect(res.status).toBe(200);
const body = (await res.json()) as Record<string, any>;
expect(body.gruTransport?.summary).toMatchObject({
transportPairs: expect.any(Number),
runtimeReadyTransportPairs: expect.any(Number),
});
});
it('returns blocked pairs with missing requirements on /preflight', async () => {
delete process.env.CHAIN138_L1_BRIDGE;
delete process.env.CW_L1_BRIDGE;
delete process.env.CW_L1_BRIDGE_CHAIN138;
delete process.env.CW_BRIDGE_BSC;
delete process.env.CW_RESERVE_VERIFIER_CHAIN138;
delete process.env.CW_STABLECOIN_RESERVE_VAULT;
delete process.env.CW_RESERVE_SYSTEM;
delete process.env.CW_MAX_OUTSTANDING_USDT_BSC;
const res = await fetch(`${baseUrl}/api/v1/bridge/preflight`);
expect(res.status).toBe(200);
const body = (await res.json()) as Record<string, any>;
expect(body.ok).toBe(false);
expect(body.gruTransport?.blockedPairs).toEqual(
expect.arrayContaining([
expect.objectContaining({
key: '138-56-cUSDT-cWUSDT',
runtimeReady: false,
runtimeMissingRequirements: expect.arrayContaining([
'bridge:l1Bridge',
'bridge:l2Bridge',
]),
}),
expect.objectContaining({
key: '138-10-cETHL2-cWETHL2',
backingMode: 'hybrid_cap',
runtimeMissingRequirements: expect.arrayContaining([
'supplyAccounting:outstanding',
'supplyAccounting:treasuryCap',
]),
}),
])
);
});
it('reads runtime bridge routes from a local json file when configured', async () => {
const tempPath = path.join('/tmp', `bridge-routes-${Date.now()}.json`);
await fs.writeFile(
tempPath,
JSON.stringify({
routes: {
weth9: {
'Dynamic Ethereum Mainnet (1)': '0x9999999999999999999999999999999999999999',
},
weth10: {},
},
chain138Bridges: {
weth9: '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
weth10: '0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',
},
})
);
process.env.BRIDGE_LIST_JSON_PATH = tempPath;
try {
const res = await fetch(`${baseUrl}/api/v1/bridge/routes`);
expect(res.status).toBe(200);
const body = (await res.json()) as Record<string, any>;
expect(body.source).toBe('runtime-file');
expect(body.routes?.weth9).toMatchObject({
'Dynamic Ethereum Mainnet (1)': '0x9999999999999999999999999999999999999999',
});
expect(body.chain138Bridges?.weth9).toBe('0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa');
} finally {
await fs.unlink(tempPath).catch(() => undefined);
}
});
it('falls back to a runtime bridge routes file when BRIDGE_LIST_JSON_URL fails', async () => {
const tempPath = path.join('/tmp', `bridge-routes-fallback-${Date.now()}.json`);
await fs.writeFile(
tempPath,
JSON.stringify({
routes: {
weth9: {
'Fallback Ethereum Mainnet (1)': '0x1234512345123451234512345123451234512345',
},
weth10: {},
},
chain138Bridges: {
weth9: '0xcccccccccccccccccccccccccccccccccccccccc',
weth10: '0xdddddddddddddddddddddddddddddddddddddddd',
},
})
);
process.env.BRIDGE_LIST_JSON_URL = 'http://127.0.0.1:1/bridge-routes.json';
process.env.BRIDGE_LIST_JSON_PATH = tempPath;
try {
const res = await fetch(`${baseUrl}/api/v1/bridge/routes`);
expect(res.status).toBe(200);
const body = (await res.json()) as Record<string, any>;
expect(body.source).toBe('runtime-file');
expect(body.routes?.weth9).toMatchObject({
'Fallback Ethereum Mainnet (1)': '0x1234512345123451234512345123451234512345',
});
expect(body.chain138Bridges?.weth9).toBe('0xcccccccccccccccccccccccccccccccccccccccc');
} finally {
await fs.unlink(tempPath).catch(() => undefined);
}
});
});

View File

@@ -1,25 +1,229 @@
/**
* Bridge API: cross-chain bridge status and metrics.
* GET /api/v1/bridge/status, /api/v1/bridge/metrics — stubbed or delegated to cross-chain report.
* GET /api/v1/bridge/routes — CCIP WETH9/WETH10 + Trustless (Snap / dApps).
* GET /api/v1/bridge/status, /api/v1/bridge/metrics, /api/v1/bridge/preflight — GRU Transport readiness + cross-chain guidance.
*/
import { Router, Request, Response } from 'express';
import fs from 'fs';
import path from 'path';
import { fetchRemoteJson } from '../utils/fetch-remote-json';
import { buildDefaultBridgeRoutes } from '../utils/default-bridge-routes';
import { getActivePublicPools, getActiveTransportPairs, getGruTransportMetadata } from '../../config/gru-transport';
import { logger } from '../../utils/logger';
const router: Router = Router();
function buildGruTransportStatus() {
const metadata = getGruTransportMetadata();
const transportPairs = getActiveTransportPairs();
const publicPools = getActivePublicPools();
if (!metadata) return null;
return {
system: metadata.system,
terminology: metadata.terminology,
summary: metadata.counts,
gasAssetFamilies: metadata.gasAssetFamilies ?? [],
gasRedeemGroups: metadata.gasRedeemGroups ?? [],
gasProtocolExposure: metadata.gasProtocolExposure ?? [],
pairs: transportPairs.map((pair) => ({
key: pair.key,
canonicalChainId: pair.canonicalChainId,
destinationChainId: pair.destinationChainId,
canonicalSymbol: pair.canonicalSymbol,
mirroredSymbol: pair.mirroredSymbol,
assetClass: pair.assetClass ?? null,
familyKey: pair.familyKey ?? null,
laneGroup: pair.laneGroup ?? null,
backingMode: pair.backingMode ?? null,
redeemPolicy: pair.redeemPolicy ?? null,
wrappedNativeQuoteSymbol: pair.wrappedNativeQuoteSymbol ?? null,
stableQuoteSymbol: pair.stableQuoteSymbol ?? null,
referenceVenue: pair.referenceVenue ?? null,
eligible: pair.eligible === true,
runtimeReady: pair.runtimeReady === true,
runtimeBridgeReady: pair.runtimeBridgeReady === true,
runtimeReserveVerifierReady: pair.runtimeReserveVerifierReady === true,
runtimeMaxOutstandingReady: pair.runtimeMaxOutstandingReady === true,
runtimeSupplyAccountingReady: pair.runtimeSupplyAccountingReady ?? null,
supplyInvariantSatisfied: pair.supplyInvariantSatisfied ?? null,
runtimeOutstandingValue: pair.runtimeOutstandingValue ?? null,
runtimeEscrowedValue: pair.runtimeEscrowedValue ?? null,
runtimeTreasuryBackedValue: pair.runtimeTreasuryBackedValue ?? null,
runtimeTreasuryCapValue: pair.runtimeTreasuryCapValue ?? null,
bridgeAvailable: pair.bridgeAvailable ?? null,
protocolExposure: pair.protocolExposure ?? null,
eligibilityBlockers: Array.isArray(pair.eligibilityBlockers) ? pair.eligibilityBlockers : [],
runtimeMissingRequirements: Array.isArray(pair.runtimeMissingRequirements) ? pair.runtimeMissingRequirements : [],
activePublicPoolKeys: Array.isArray(pair.publicPoolKeys) ? pair.publicPoolKeys : [],
})),
publicPools,
};
}
function uniquePaths(paths: Array<string | undefined | null>): string[] {
const seen = new Set<string>();
const out: string[] = [];
for (const candidate of paths) {
if (typeof candidate !== 'string') continue;
const trimmed = candidate.trim();
if (!trimmed || seen.has(trimmed)) continue;
seen.add(trimmed);
out.push(trimmed);
}
return out;
}
function resolveBridgeRoutesPath(): string | null {
const candidates = uniquePaths([
process.env.BRIDGE_LIST_JSON_PATH,
process.env.BRIDGE_ROUTES_JSON_PATH,
path.resolve(process.cwd(), 'config/bridge-routes-chain138-default.json'),
path.resolve(process.cwd(), '../config/bridge-routes-chain138-default.json'),
path.resolve(process.cwd(), '../../config/bridge-routes-chain138-default.json'),
path.resolve(__dirname, '../../../../../config/bridge-routes-chain138-default.json'),
]);
for (const candidate of candidates) {
if (fs.existsSync(candidate)) return candidate;
}
return null;
}
function loadRuntimeBridgeRoutes(): { payload: Record<string, unknown>; lastModified?: string } | null {
const filePath = resolveBridgeRoutesPath();
if (!filePath) return null;
try {
const raw = fs.readFileSync(filePath, 'utf8');
const stat = fs.statSync(filePath);
return {
payload: JSON.parse(raw) as Record<string, unknown>,
lastModified: stat.mtime.toISOString(),
};
} catch {
return null;
}
}
/**
* GET /api/v1/bridge/routes
* Optional BRIDGE_LIST_JSON_URL — remote JSON replaces entire payload (5m cache).
*/
router.get('/routes', async (_req: Request, res: Response) => {
const gruTransportMetadata = getGruTransportMetadata();
res.set('Cache-Control', 'public, max-age=0, must-revalidate');
const url = process.env.BRIDGE_LIST_JSON_URL?.trim();
if (url) {
try {
const data = await fetchRemoteJson(url);
const basePayload = data && typeof data === 'object' ? data : { data };
res.json({
source: 'remote-url',
...basePayload,
gruTransport: gruTransportMetadata
? {
system: gruTransportMetadata.system,
summary: gruTransportMetadata.counts,
activeTransportPairs: getActiveTransportPairs(),
activePublicPools: getActivePublicPools(),
}
: undefined,
});
return;
} catch (e) {
logger.error('BRIDGE_LIST_JSON_URL fetch failed, trying runtime file/built-in routes:', e);
}
}
const runtimePayload = loadRuntimeBridgeRoutes();
if (runtimePayload) {
res.json({
source: 'runtime-file',
lastModified: runtimePayload.lastModified,
...runtimePayload.payload,
gruTransport: gruTransportMetadata
? {
system: gruTransportMetadata.system,
summary: gruTransportMetadata.counts,
activeTransportPairs: getActiveTransportPairs(),
activePublicPools: getActivePublicPools(),
}
: undefined,
});
return;
}
res.json({
source: 'built-in',
...buildDefaultBridgeRoutes(),
});
});
router.get('/status', (_req: Request, res: Response) => {
const gruTransport = buildGruTransportStatus();
res.json({
ok: true,
bridges: [],
message: 'Bridge status: use /api/v1/report/cross-chain for volume/lanes.',
gruTransport,
message: 'Bridge status includes GRU Transport runtime readiness. Use /api/v1/bridge/preflight for missing refs and /api/v1/report/cross-chain for volume/lanes.',
});
});
router.get('/metrics', (_req: Request, res: Response) => {
const gruTransport = buildGruTransportStatus();
res.json({
ok: true,
lanes: [],
message: 'Bridge metrics: use /api/v1/report/cross-chain for aggregated data.',
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.',
});
});
router.get('/preflight', (_req: Request, res: Response) => {
const gruTransport = buildGruTransportStatus();
if (!gruTransport) {
return res.status(503).json({
ok: false,
error: 'GRU transport config not available',
});
}
const blockedPairs = gruTransport.pairs.filter(
(pair) => pair.eligible !== true || pair.runtimeReady !== true
);
const readyPairs = gruTransport.pairs.filter(
(pair) => pair.eligible === true && pair.runtimeReady === true
);
return res.json({
ok: blockedPairs.length === 0,
generatedAt: new Date().toISOString(),
gruTransport: {
system: gruTransport.system,
summary: gruTransport.summary,
blockedPairs,
readyPairs: readyPairs.map((pair) => ({
key: pair.key,
canonicalSymbol: pair.canonicalSymbol,
mirroredSymbol: pair.mirroredSymbol,
destinationChainId: pair.destinationChainId,
assetClass: pair.assetClass ?? null,
familyKey: pair.familyKey ?? null,
backingMode: pair.backingMode ?? null,
redeemPolicy: pair.redeemPolicy ?? null,
supplyInvariantSatisfied: pair.supplyInvariantSatisfied ?? null,
})),
},
});
});

View File

@@ -0,0 +1,142 @@
import { createServer } from 'http';
import express from 'express';
import fs from 'fs/promises';
import path from 'path';
import configRoutes from './config';
jest.mock('../middleware/cache');
function createApp() {
const app = express();
app.use('/api/v1', configRoutes);
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('Config API runtime networks loader', () => {
let server: ReturnType<typeof createServer>;
let baseUrl: string;
const originalNetworksPath = process.env.NETWORKS_JSON_PATH;
const originalNetworksUrl = process.env.NETWORKS_JSON_URL;
beforeAll(async () => {
const started = await startServer(createApp());
server = started.server;
baseUrl = started.baseUrl;
});
afterEach(() => {
if (originalNetworksPath === undefined) {
delete process.env.NETWORKS_JSON_PATH;
} else {
process.env.NETWORKS_JSON_PATH = originalNetworksPath;
}
if (originalNetworksUrl === undefined) {
delete process.env.NETWORKS_JSON_URL;
} else {
process.env.NETWORKS_JSON_URL = originalNetworksUrl;
}
});
afterAll((done) => {
server.close(done);
});
it('serves networks from a runtime file when configured', async () => {
const tempPath = path.join('/tmp', `token-aggregation-networks-${Date.now()}.json`);
await fs.writeFile(
tempPath,
JSON.stringify({
version: { major: 9, minor: 9, patch: 9 },
chains: [
{
chainId: '0x8a',
chainIdDecimal: 138,
chainName: 'Dynamic Chain 138',
rpcUrls: ['https://dynamic-rpc.example'],
nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 },
blockExplorerUrls: ['https://dynamic-explorer.example'],
oracles: [{ name: 'ETH/USD', address: '0x1111111111111111111111111111111111111111' }],
},
],
})
);
process.env.NETWORKS_JSON_PATH = tempPath;
try {
const res = await fetch(`${baseUrl}/api/v1/networks`);
expect(res.status).toBe(200);
const body = (await res.json()) as Record<string, any>;
expect(body.source).toBe('runtime-file');
expect(body.version).toBe('9.9.9');
expect(body.networks).toEqual(
expect.arrayContaining([
expect.objectContaining({
chainName: 'Dynamic Chain 138',
}),
])
);
const cfgRes = await fetch(`${baseUrl}/api/v1/config?chainId=138`);
expect(cfgRes.status).toBe(200);
const cfgBody = (await cfgRes.json()) as Record<string, any>;
expect(cfgBody.source).toBe('runtime-file');
expect(cfgBody.oracles).toEqual(
expect.arrayContaining([
expect.objectContaining({
name: 'ETH/USD',
}),
])
);
} finally {
await fs.unlink(tempPath).catch(() => undefined);
}
});
it('uses the remote networks source for config when NETWORKS_JSON_URL is set', async () => {
const remotePayload = {
version: '7.7.7',
networks: [
{
chainId: '0x8a',
chainIdDecimal: 138,
chainName: 'Remote Chain 138',
rpcUrls: ['https://remote-rpc.example'],
nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 },
blockExplorerUrls: ['https://remote-explorer.example'],
oracles: [{ name: 'BTC/USD', address: '0x2222222222222222222222222222222222222222' }],
},
],
};
const remoteServer = createServer((_req, res) => {
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify(remotePayload));
});
await new Promise<void>((resolve) => remoteServer.listen(0, () => resolve()));
const remotePort = (remoteServer.address() as { port: number }).port;
process.env.NETWORKS_JSON_URL = `http://127.0.0.1:${remotePort}/networks.json`;
try {
const res = await fetch(`${baseUrl}/api/v1/config?chainId=138`);
expect(res.status).toBe(200);
const body = (await res.json()) as Record<string, any>;
expect(body.source).toBe('remote-url');
expect(body.version).toBe('7.7.7');
expect(body.oracles).toEqual(
expect.arrayContaining([
expect.objectContaining({
name: 'BTC/USD',
}),
])
);
} finally {
await new Promise<void>((resolve, reject) => remoteServer.close((err) => (err ? reject(err) : resolve())));
}
});
});

View File

@@ -1,4 +1,6 @@
import { Router, Request, Response } from 'express';
import fs from 'fs';
import path from 'path';
import { getNetworks, getConfigByChain, API_VERSION } from '../../config/networks';
import { cacheMiddleware } from '../middleware/cache';
import { fetchRemoteJson } from '../utils/fetch-remote-json';
@@ -6,6 +8,122 @@ import { logger } from '../../utils/logger';
const router: Router = Router();
type RuntimeNetworksPayload = {
version?: string | { major?: number; minor?: number; patch?: number };
networks?: unknown[];
chains?: unknown[];
};
type NetworksPayload = {
source: 'remote-url' | 'runtime-file' | 'built-in';
version: string;
networks: unknown[];
lastModified?: string;
};
function uniquePaths(paths: Array<string | undefined | null>): string[] {
const seen = new Set<string>();
const out: string[] = [];
for (const candidate of paths) {
if (typeof candidate !== 'string') continue;
const trimmed = candidate.trim();
if (!trimmed || seen.has(trimmed)) continue;
seen.add(trimmed);
out.push(trimmed);
}
return out;
}
function resolveRuntimeNetworksPath(): string | null {
const candidates = uniquePaths([
process.env.NETWORKS_JSON_PATH,
process.env.CONFIG_NETWORKS_JSON_PATH,
path.resolve(process.cwd(), 'explorer-monorepo/backend/api/rest/config/metamask/DUAL_CHAIN_NETWORKS.json'),
path.resolve(process.cwd(), '../explorer-monorepo/backend/api/rest/config/metamask/DUAL_CHAIN_NETWORKS.json'),
path.resolve(process.cwd(), '../../explorer-monorepo/backend/api/rest/config/metamask/DUAL_CHAIN_NETWORKS.json'),
path.resolve(process.cwd(), 'explorer-monorepo/backend/config/metamask/DUAL_CHAIN_NETWORKS.json'),
path.resolve(process.cwd(), '../explorer-monorepo/backend/config/metamask/DUAL_CHAIN_NETWORKS.json'),
path.resolve(process.cwd(), '../../explorer-monorepo/backend/config/metamask/DUAL_CHAIN_NETWORKS.json'),
path.resolve(__dirname, '../../../../../../explorer-monorepo/backend/api/rest/config/metamask/DUAL_CHAIN_NETWORKS.json'),
]);
for (const candidate of candidates) {
if (fs.existsSync(candidate)) return candidate;
}
return null;
}
function normalizeVersion(value: RuntimeNetworksPayload['version']): string {
if (typeof value === 'string' && value.trim() !== '') return value.trim();
if (value && typeof value === 'object') {
const major = 'major' in value ? Number(value.major ?? 0) : 0;
const minor = 'minor' in value ? Number(value.minor ?? 0) : 0;
const patch = 'patch' in value ? Number(value.patch ?? 0) : 0;
return `${major}.${minor}.${patch}`;
}
return API_VERSION;
}
function extractNetworks(payload: RuntimeNetworksPayload | { version?: string; networks?: unknown[] }): unknown[] {
if (Array.isArray(payload.networks)) return payload.networks;
if ('chains' in payload && Array.isArray(payload.chains)) return payload.chains;
return [];
}
async function loadRemoteNetworksPayload(): Promise<NetworksPayload | null> {
const networksJsonUrl = process.env.NETWORKS_JSON_URL?.trim();
if (!networksJsonUrl) return null;
try {
const data = (await fetchRemoteJson(networksJsonUrl)) as RuntimeNetworksPayload;
return {
source: 'remote-url',
version: normalizeVersion(data.version),
networks: extractNetworks(data),
};
} catch (error) {
logger.error('NETWORKS_JSON_URL fetch failed, trying runtime file/built-in networks:', error);
return null;
}
}
function loadRuntimeNetworksPayload(): NetworksPayload | null {
const filePath = resolveRuntimeNetworksPath();
if (!filePath) return null;
try {
const raw = fs.readFileSync(filePath, 'utf8');
const stat = fs.statSync(filePath);
const parsed = JSON.parse(raw) as RuntimeNetworksPayload;
return {
source: 'runtime-file',
version: normalizeVersion(parsed.version),
networks: extractNetworks(parsed),
lastModified: stat.mtime.toISOString(),
};
} catch (error) {
logger.error('NETWORKS_JSON_PATH read failed, using built-in networks:', error);
return null;
}
}
async function resolveNetworksPayload(): Promise<NetworksPayload> {
const remotePayload = await loadRemoteNetworksPayload();
if (remotePayload) return remotePayload;
const runtimePayload = loadRuntimeNetworksPayload();
if (runtimePayload) return runtimePayload;
return {
source: 'built-in',
version: API_VERSION,
networks: getNetworks(),
};
}
/**
* GET /api/v1/networks
* Full EIP-3085 chain params for wallet_addEthereumChain (Chain 138, 1, 651940).
@@ -13,23 +131,9 @@ const router: Router = Router();
*/
router.get('/networks', cacheMiddleware(5 * 60 * 1000), async (req: Request, res: Response) => {
try {
const networksJsonUrl = process.env.NETWORKS_JSON_URL?.trim();
if (networksJsonUrl) {
try {
const data = (await fetchRemoteJson(networksJsonUrl)) as { version?: string; networks?: unknown[] };
return res.json({
version: data.version ?? API_VERSION,
networks: data.networks ?? [],
});
} catch (err) {
logger.error('NETWORKS_JSON_URL fetch failed, using built-in networks:', err);
}
}
const networks = getNetworks();
res.json({
version: API_VERSION,
networks,
});
res.set('Cache-Control', 'public, max-age=0, must-revalidate');
const payload = await resolveNetworksPayload();
res.json(payload);
} catch (error) {
res.status(500).json({ error: 'Internal server error' });
}
@@ -42,21 +146,37 @@ router.get('/networks', cacheMiddleware(5 * 60 * 1000), async (req: Request, res
*/
router.get('/config', cacheMiddleware(5 * 60 * 1000), async (req: Request, res: Response) => {
try {
res.set('Cache-Control', 'public, max-age=0, must-revalidate');
const chainIdParam = req.query.chainId as string | undefined;
const networks = getNetworks();
const payload = await resolveNetworksPayload();
const networks = payload.networks as Array<{ chainIdDecimal?: number; oracles?: unknown[] }>;
if (chainIdParam) {
const chainId = parseInt(chainIdParam, 10);
const config = getConfigByChain(chainId);
const matchingNetwork = networks.find((network) => Number(network.chainIdDecimal) === chainId);
const config = matchingNetwork
? {
oracles: (matchingNetwork.oracles as unknown[]) ?? [],
}
: getConfigByChain(chainId);
if (!config) {
return res.status(404).json({ error: 'Chain not found', chainId });
}
return res.json({ version: API_VERSION, chainId, ...config });
return res.json({
source: payload.source,
version: payload.version,
chainId,
...config,
});
}
const chains = networks.map((n) => ({
chainId: n.chainIdDecimal,
oracles: n.oracles,
chainId: Number(n.chainIdDecimal),
oracles: Array.isArray(n.oracles) ? n.oracles : [],
}));
res.json({ version: API_VERSION, chains });
res.json({
source: payload.source,
version: payload.version,
chains,
});
} catch (error) {
res.status(500).json({ error: 'Internal server error' });
}

View File

@@ -9,6 +9,7 @@ import {
DEFAULT_HEATMAP_ASSETS,
} from '../../config/heatmap-chains';
import { cacheMiddleware } from '../middleware/cache';
import { filterPoolsForExposure, isPublicPoolRoutable } from '../../config/gru-transport';
const router = Router();
const poolRepo = new PoolRepository();
@@ -32,7 +33,7 @@ router.get('/heatmap', cacheMiddleware(60 * 1000), async (req: Request, res: Res
const matrix: number[][] = [];
for (const chainId of chainIds) {
const row: number[] = [];
const pools = await poolRepo.getPoolsByChain(chainId, 500);
const pools = filterPoolsForExposure(chainId, await poolRepo.getPoolsByChain(chainId, 500));
const symbolToTvl: Record<string, number> = {};
for (const sym of assets) symbolToTvl[sym] = 0;
for (const pool of pools) {
@@ -72,7 +73,7 @@ router.get('/pools', cacheMiddleware(60 * 1000), async (req: Request, res: Respo
if (!chainId || isNaN(chainId)) {
return res.status(400).json({ error: 'chainId is required' });
}
const pools = await poolRepo.getPoolsByChain(chainId, 500);
const pools = filterPoolsForExposure(chainId, await poolRepo.getPoolsByChain(chainId, 500));
const list = await Promise.all(
pools.map(async (p) => {
const { token0, token1 } = await resolvePoolTokenDisplays(tokenRepo, chainId, p.token0Address, p.token1Address);
@@ -89,7 +90,7 @@ router.get('/pools', cacheMiddleware(60 * 1000), async (req: Request, res: Respo
reserve1: p.reserve1,
},
isDeployed: true,
isRoutable: true,
isRoutable: isPublicPoolRoutable(chainId, p.poolAddress),
};
})
);

View File

@@ -0,0 +1,112 @@
import { createServer } from 'http';
import express from 'express';
let partnerPayloadRoutes: any;
const mockPlan = jest.fn();
jest.mock('../../services/best-execution-planner', () => ({
__esModule: true,
BestExecutionPlanner: jest.fn().mockImplementation(() => ({
plan: mockPlan,
})),
}));
jest.mock('../../config/aggregator-route-matrix', () => ({
__esModule: true,
loadAggregatorRouteMatrix: jest.fn(() => ({
version: '2.0.0',
updated: '2026-04-02T00:00:00.000Z',
homeChainId: 138,
liveSwapRoutes: [],
liveBridgeRoutes: [],
blockedOrPlannedRoutes: [],
})),
filterLiveAggregatorRoutes: jest.fn(() => []),
}));
jest.mock('../middleware/cache');
function createApp() {
const app = express();
app.use(express.json());
app.use('/api/v1', partnerPayloadRoutes);
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('Partner payload API', () => {
let server: ReturnType<typeof createServer>;
let baseUrl: string;
beforeAll(async () => {
partnerPayloadRoutes = require('./partner-payloads').default;
const started = await startServer(createApp());
server = started.server;
baseUrl = started.baseUrl;
});
beforeEach(() => {
mockPlan.mockReset();
});
afterAll((done) => {
server.close(done);
});
it('prefers planner-v2 routes when concrete route inputs are provided', async () => {
mockPlan.mockResolvedValue({
planId: 'planner-route-1',
generatedAt: new Date().toISOString(),
decision: 'direct-pool',
sourceChainId: 138,
destinationChainId: 138,
tokenIn: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2',
tokenOut: '0x004b63a7b5b0e06f6bb6adb4a5f9f590bf3182d1',
estimatedAmountOut: '1800000000',
minAmountOut: '1782000000',
estimatedGasUsd: 0.22,
confidenceScore: 0.91,
riskFlags: [],
selectedRouteReason: 'Selected deepest eligible dodo pool.',
rejectedAlternatives: [],
staleness: { maxFreshnessSeconds: 15, hasStaleLeg: false },
alternatives: [],
legs: [
{
kind: 'swap',
provider: 'dodo',
sourceChainId: 138,
destinationChainId: 138,
tokenInAddress: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2',
tokenOutAddress: '0x004b63a7b5b0e06f6bb6adb4a5f9f590bf3182d1',
tokenInSymbol: 'WETH',
tokenOutSymbol: 'USDT',
estimatedAmountIn: '1000000000000000000',
estimatedAmountOut: '1800000000',
minAmountOut: '1782000000',
target: '0x86ada6ef91a3b450f89f2b751e93b1b7a3218895',
poolAddress: '0x1111111111111111111111111111111111111111',
providerData: { poolAddress: '0x1111111111111111111111111111111111111111' },
providerDataHex: '0x',
},
],
});
const res = await fetch(
`${baseUrl}/api/v1/routes/partner-payloads?partner=0x&amount=1000000000000000000&fromChainId=138&tokenIn=0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2&tokenOut=0x004b63a7b5b0e06f6bb6adb4a5f9f590bf3182d1&includeUnsupported=true`
);
expect(res.status).toBe(200);
const body = (await res.json()) as Record<string, any>;
expect(body.routeSource).toBe('planner-v2-preferred');
expect(body.count).toBe(1);
expect(body.payloads[0].routeId).toBe('planner-route-1');
expect(mockPlan).toHaveBeenCalled();
});
});

View File

@@ -1,6 +1,8 @@
import { Router, Request, Response } from 'express';
import { cacheMiddleware } from '../middleware/cache';
import {
AggregatorRouteLeg,
LiveAggregatorRoute,
filterLiveAggregatorRoutes,
loadAggregatorRouteMatrix,
} from '../../config/aggregator-route-matrix';
@@ -10,8 +12,11 @@ import {
} from '../../services/partner-payload-adapters';
import { dispatchPartnerPayload } from '../../services/partner-payload-dispatcher';
import { buildInternalExecutionPlan } from '../../services/internal-execution-plan';
import { BestExecutionPlanner } from '../../services/best-execution-planner';
import { routeFromPlannerLegs } from '../../services/aggregator-route-matrix-generator';
const router: Router = Router();
const planner = new BestExecutionPlanner();
interface PartnerPayloadRequestBody {
partner?: string;
@@ -55,9 +60,117 @@ function buildPayloads(args: {
slippagePercent?: string;
slippageBps?: string;
includeUnsupported?: boolean;
routeId?: string;
}) {
return buildPayloadsAsync(args);
}
function dedupeRoutes(routes: LiveAggregatorRoute[]): LiveAggregatorRoute[] {
const byId = new Map<string, LiveAggregatorRoute>();
for (const route of routes) {
byId.set(route.routeId, route);
}
return Array.from(byId.values());
}
function plannerLegsToAggregatorLegs(routeLegs: Array<{
kind: string;
provider: string;
target?: string;
poolAddress?: string;
bridgeAddress?: string;
tokenInAddress: string;
tokenOutAddress: string;
}>): AggregatorRouteLeg[] {
return routeLegs.map((leg) => ({
kind: leg.kind,
protocol: leg.provider,
executor: leg.provider,
executorAddress: leg.target,
poolAddress: leg.poolAddress || leg.bridgeAddress,
tokenInAddress: leg.tokenInAddress,
tokenOutAddress: leg.tokenOutAddress,
}));
}
async function resolvePlannerDerivedRoutes(args: {
amount: string;
fromChainId?: number;
toChainId?: number;
routeType?: string;
tokenIn?: string;
tokenOut?: string;
recipient?: string;
slippageBps?: string;
}): Promise<LiveAggregatorRoute[]> {
if (!args.fromChainId || !args.tokenIn || !args.tokenOut) {
return [];
}
const wantsBridge = Boolean(args.toChainId && args.toChainId !== args.fromChainId);
if (args.routeType === 'swap' && wantsBridge) {
return [];
}
const response = await planner.plan({
sourceChainId: args.fromChainId,
destinationChainId: wantsBridge ? args.toChainId : args.fromChainId,
tokenIn: args.tokenIn,
tokenOut: args.tokenOut,
amountIn: args.amount,
recipient: args.recipient,
constraints: {
allowBridge: wantsBridge || args.routeType === 'bridge' || args.routeType === 'swap-bridge-swap',
maxSlippageBps: args.slippageBps ? Number(args.slippageBps) : undefined,
},
});
if (response.decision === 'unresolved' || response.legs.length === 0) {
return [];
}
const aggregatorLegs = plannerLegsToAggregatorLegs(response.legs);
const bridgeLeg = response.legs.find((leg) => leg.kind === 'bridge');
const routeType = bridgeLeg ? 'bridge' : 'swap';
return [
routeFromPlannerLegs({
routeId: response.planId,
fromChainId: response.sourceChainId,
toChainId: response.destinationChainId,
tokenInAddress: response.legs[0]?.tokenInAddress || args.tokenIn,
tokenOutAddress: response.legs[response.legs.length - 1]?.tokenOutAddress || args.tokenOut,
assetAddress: bridgeLeg?.tokenInAddress,
assetSymbol: bridgeLeg?.tokenInSymbol,
routeType,
bridgeType: bridgeLeg?.bridgeType,
bridgeAddress: bridgeLeg?.bridgeAddress,
label: response.selectedRouteReason,
legs: aggregatorLegs,
notes: response.riskFlags,
}),
];
}
async function buildPayloadsAsync(args: {
partner: PartnerName;
amount: string;
fromChainId?: number;
toChainId?: number;
routeType?: string;
tokenIn?: string;
tokenOut?: string;
takerAddress?: string;
fromAddress?: string;
toAddress?: string;
recipient?: string;
slippagePercent?: string;
slippageBps?: string;
includeUnsupported?: boolean;
routeId?: string;
}) {
const matrix = loadAggregatorRouteMatrix();
if (!matrix) {
if (!matrix && !args.fromChainId) {
return {
error: {
status: 503,
@@ -68,15 +181,21 @@ function buildPayloads(args: {
};
}
const liveRoutes = filterLiveAggregatorRoutes(
[...matrix.liveSwapRoutes, ...matrix.liveBridgeRoutes],
{
fromChainId: args.fromChainId,
toChainId: args.toChainId,
routeType: args.routeType,
tokenIn: args.tokenIn,
tokenOut: args.tokenOut,
}
const plannerRoutes = await resolvePlannerDerivedRoutes(args);
const matrixRoutes = matrix
? filterLiveAggregatorRoutes(
[...matrix.liveSwapRoutes, ...matrix.liveBridgeRoutes],
{
fromChainId: args.fromChainId,
toChainId: args.toChainId,
routeType: args.routeType,
tokenIn: args.tokenIn,
tokenOut: args.tokenOut,
}
)
: [];
const liveRoutes = dedupeRoutes([...plannerRoutes, ...matrixRoutes]).filter((route) =>
args.routeId ? route.routeId === args.routeId : true
);
const payloads = liveRoutes.map((route) =>
@@ -99,6 +218,7 @@ function buildPayloads(args: {
format: 'partner-payload-templates-v1',
partner: args.partner,
amount: args.amount,
routeSource: plannerRoutes.length > 0 ? 'planner-v2-preferred' : 'matrix-fallback',
count: filteredPayloads.length,
supportedCount: payloads.filter((payload) => payload.supported).length,
payloads: filteredPayloads,
@@ -111,7 +231,7 @@ function buildPayloads(args: {
* Returns partner-specific request payload templates generated from live ingestion routes.
* By default returns only supported payloads; pass includeUnsupported=true to inspect all templates.
*/
router.get('/routes/partner-payloads', cacheMiddleware(30 * 1000), (req: Request, res: Response) => {
router.get('/routes/partner-payloads', cacheMiddleware(30 * 1000), async (req: Request, res: Response) => {
const partner = normalizePartner(req.query.partner ? String(req.query.partner) : undefined);
if (!partner) {
return res.status(400).json({
@@ -128,7 +248,7 @@ router.get('/routes/partner-payloads', cacheMiddleware(30 * 1000), (req: Request
});
}
const response = buildPayloads({
const response = await buildPayloads({
partner,
amount,
fromChainId: req.query.fromChainId ? parseInt(String(req.query.fromChainId), 10) : undefined,
@@ -143,6 +263,7 @@ router.get('/routes/partner-payloads', cacheMiddleware(30 * 1000), (req: Request
slippagePercent: req.query.slippagePercent ? String(req.query.slippagePercent) : undefined,
slippageBps: req.query.slippageBps ? String(req.query.slippageBps) : undefined,
includeUnsupported: String(req.query.includeUnsupported ?? 'false').toLowerCase() === 'true',
routeId: req.query.routeId ? String(req.query.routeId) : undefined,
});
if (response.error) {
@@ -156,7 +277,7 @@ router.get('/routes/partner-payloads', cacheMiddleware(30 * 1000), (req: Request
* POST /api/v1/routes/partner-payloads/resolve
* Accepts JSON body and returns only supported partner payloads by default.
*/
router.post('/routes/partner-payloads/resolve', cacheMiddleware(30 * 1000), (req: Request, res: Response) => {
router.post('/routes/partner-payloads/resolve', cacheMiddleware(30 * 1000), async (req: Request, res: Response) => {
const body = (req.body ?? {}) as PartnerPayloadRequestBody;
const partner = normalizePartner(body.partner);
@@ -178,7 +299,7 @@ router.post('/routes/partner-payloads/resolve', cacheMiddleware(30 * 1000), (req
});
}
const response = buildPayloads({
const response = await buildPayloads({
partner,
amount: String(body.amount),
fromChainId: typeof body.fromChainId === 'number' ? body.fromChainId : undefined,
@@ -193,6 +314,7 @@ router.post('/routes/partner-payloads/resolve', cacheMiddleware(30 * 1000), (req
slippagePercent: body.slippagePercent,
slippageBps: body.slippageBps,
includeUnsupported: body.includeUnsupported === true,
routeId: body.routeId,
});
if (response.error) {
@@ -222,7 +344,7 @@ router.post('/routes/partner-payloads/dispatch', async (req: Request, res: Respo
});
}
const response = buildPayloads({
const response = await buildPayloads({
partner,
amount: String(body.amount),
fromChainId: typeof body.fromChainId === 'number' ? body.fromChainId : undefined,
@@ -237,6 +359,7 @@ router.post('/routes/partner-payloads/dispatch', async (req: Request, res: Respo
slippagePercent: body.slippagePercent,
slippageBps: body.slippageBps,
includeUnsupported: true,
routeId: body.routeId,
});
if (response.error) {

View File

@@ -0,0 +1,154 @@
import { createServer } from 'http';
import express from 'express';
let plannerV2Routes: any;
const mockPlan = jest.fn();
const mockCapabilities = jest.fn();
const mockBuild = jest.fn();
jest.mock('../../services/best-execution-planner', () => ({
__esModule: true,
BestExecutionPlanner: jest.fn().mockImplementation(() => ({
plan: mockPlan,
getCapabilities: mockCapabilities,
})),
}));
jest.mock('../../services/internal-execution-plan-v2', () => ({
__esModule: true,
InternalExecutionPlanV2Builder: jest.fn().mockImplementation(() => ({
build: mockBuild,
})),
}));
jest.mock('../middleware/cache');
function createApp() {
const app = express();
app.use(express.json());
app.use('/api/v2', plannerV2Routes);
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('Planner V2 API', () => {
let server: ReturnType<typeof createServer>;
let baseUrl: string;
beforeAll(async () => {
plannerV2Routes = require('./planner-v2').default;
const started = await startServer(createApp());
server = started.server;
baseUrl = started.baseUrl;
});
beforeEach(() => {
mockPlan.mockReset();
mockCapabilities.mockReset();
mockBuild.mockReset();
});
afterAll((done) => {
server.close(done);
});
it('returns provider capabilities', async () => {
mockCapabilities.mockReturnValue([{ provider: 'dodo', live: true }]);
const res = await fetch(`${baseUrl}/api/v2/providers/capabilities?chainId=138`);
expect(res.status).toBe(200);
const body = (await res.json()) as Record<string, any>;
expect(body.providers).toEqual([{ provider: 'dodo', live: true }]);
});
it('returns a planner response for one-chain route planning', async () => {
mockPlan.mockResolvedValue({
planId: 'test-plan',
decision: 'direct-pool',
estimatedAmountOut: '1000',
minAmountOut: '990',
estimatedGasUsd: 0.22,
legs: [],
alternatives: [],
confidenceScore: 0.9,
riskFlags: [],
selectedRouteReason: 'selected',
rejectedAlternatives: [],
staleness: { maxFreshnessSeconds: 12, hasStaleLeg: false },
generatedAt: new Date().toISOString(),
sourceChainId: 138,
destinationChainId: 138,
tokenIn: '0x1',
tokenOut: '0x2',
});
const res = await fetch(`${baseUrl}/api/v2/routes/plan`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
sourceChainId: 138,
tokenIn: '0x1',
tokenOut: '0x2',
amountIn: '1000',
}),
});
expect(res.status).toBe(200);
const body = (await res.json()) as Record<string, any>;
expect(body.planId).toBe('test-plan');
expect(mockPlan).toHaveBeenCalled();
});
it('returns an internal execution plan when the planner can encode calldata', async () => {
mockBuild.mockResolvedValue({
generatedAt: new Date().toISOString(),
plannerResponse: {
planId: 'test-plan',
},
execution: {
kind: 'route',
contractAddress: '0xrouter',
functionName: 'executeRoute',
signature: 'executeRoute((...))',
args: [],
encodedCalldata: '0xdeadbeef',
},
});
const res = await fetch(`${baseUrl}/api/v2/routes/internal-execution-plan`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
sourceChainId: 138,
tokenIn: '0x1',
tokenOut: '0x2',
amountIn: '1000',
}),
});
expect(res.status).toBe(200);
const body = (await res.json()) as Record<string, any>;
expect(body.execution.encodedCalldata).toBe('0xdeadbeef');
});
it('returns a JSON 500 instead of crashing when internal plan generation rejects', async () => {
mockBuild.mockRejectedValue(new Error('planner exploded'));
const res = await fetch(`${baseUrl}/api/v2/routes/internal-execution-plan`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
sourceChainId: 138,
tokenIn: '0x1',
tokenOut: '0x2',
amountIn: '1000',
}),
});
expect(res.status).toBe(500);
await expect(res.json()).resolves.toEqual({ error: 'Internal server error' });
});
});

View File

@@ -0,0 +1,141 @@
import { Router, Request, Response } from 'express';
import { cacheMiddleware } from '../middleware/cache';
import { BestExecutionPlanner } from '../../services/best-execution-planner';
import { InternalExecutionPlanV2Builder } from '../../services/internal-execution-plan-v2';
import { PlannerRequest } from '../../services/planner-v2-types';
import { logger } from '../../utils/logger';
const router = Router();
const planner = new BestExecutionPlanner();
const executionPlanBuilder = new InternalExecutionPlanV2Builder(planner);
function parsePlannerRequest(body: Record<string, unknown>): PlannerRequest {
const constraintsBody = body.constraints && typeof body.constraints === 'object'
? (body.constraints as Record<string, unknown>)
: null;
return {
sourceChainId: Number(body.sourceChainId),
destinationChainId: body.destinationChainId !== undefined ? Number(body.destinationChainId) : undefined,
tokenIn: String(body.tokenIn || ''),
tokenOut: String(body.tokenOut || ''),
amountIn: String(body.amountIn || ''),
recipient: body.recipient ? String(body.recipient) : undefined,
constraints: constraintsBody
? {
maxSlippageBps: typeof constraintsBody.maxSlippageBps === 'number'
? Number(constraintsBody.maxSlippageBps)
: undefined,
allowedProviders: Array.isArray(constraintsBody.allowedProviders)
? (constraintsBody.allowedProviders as string[])
.map((value) => String(value)) as NonNullable<PlannerRequest['constraints']>['allowedProviders']
: undefined,
maxLegs: typeof constraintsBody.maxLegs === 'number'
? Number(constraintsBody.maxLegs)
: undefined,
allowedIntermediates: Array.isArray(constraintsBody.allowedIntermediates)
? (constraintsBody.allowedIntermediates as string[]).map((value) => String(value))
: undefined,
complianceProfile: typeof constraintsBody.complianceProfile === 'string'
? String(constraintsBody.complianceProfile) as NonNullable<PlannerRequest['constraints']>['complianceProfile']
: undefined,
allowBridge: typeof constraintsBody.allowBridge === 'boolean'
? Boolean(constraintsBody.allowBridge)
: undefined,
preferredBridges: Array.isArray(constraintsBody.preferredBridges)
? (constraintsBody.preferredBridges as string[]).map((value) => String(value))
: undefined,
allowCommodityIntermediates: typeof constraintsBody.allowCommodityIntermediates === 'boolean'
? Boolean(constraintsBody.allowCommodityIntermediates)
: undefined,
}
: undefined,
};
}
function validatePlannerRequest(request: PlannerRequest): string | null {
if (!request.sourceChainId || Number.isNaN(request.sourceChainId)) return 'sourceChainId is required';
if (!request.tokenIn) return 'tokenIn is required';
if (!request.tokenOut) return 'tokenOut is required';
if (!request.amountIn) return 'amountIn is required';
try {
BigInt(request.amountIn);
} catch {
return 'amountIn must be an integer string';
}
return null;
}
function handlePlannerFailure(res: Response, action: string, error: unknown) {
logger.error(`Planner v2 ${action} failed`, error);
return res.status(500).json({ error: 'Internal server error' });
}
router.get('/providers/capabilities', cacheMiddleware(15 * 1000), async (req: Request, res: Response) => {
try {
const chainId = Number(req.query.chainId || '138');
return res.json({
generatedAt: new Date().toISOString(),
chainId,
providers: planner.getCapabilities(chainId),
});
} catch (error) {
return handlePlannerFailure(res, 'provider capability lookup', error);
}
});
router.post('/routes/plan', async (req: Request, res: Response) => {
try {
const request = parsePlannerRequest((req.body || {}) as Record<string, unknown>);
const error = validatePlannerRequest(request);
if (error) {
return res.status(400).json({ error });
}
const response = await planner.plan({
...request,
destinationChainId: request.destinationChainId || request.sourceChainId,
});
return res.json(response);
} catch (error) {
return handlePlannerFailure(res, 'route planning', error);
}
});
router.post('/intents/plan', async (req: Request, res: Response) => {
try {
const request = parsePlannerRequest((req.body || {}) as Record<string, unknown>);
const error = validatePlannerRequest(request);
if (error) {
return res.status(400).json({ error });
}
if (!request.destinationChainId || request.destinationChainId === request.sourceChainId) {
return res.status(400).json({ error: 'destinationChainId must be different from sourceChainId for intent planning' });
}
const response = await planner.plan(request);
return res.json(response);
} catch (error) {
return handlePlannerFailure(res, 'intent planning', error);
}
});
router.post('/routes/internal-execution-plan', async (req: Request, res: Response) => {
try {
const request = parsePlannerRequest((req.body || {}) as Record<string, unknown>);
const error = validatePlannerRequest(request);
if (error) {
return res.status(400).json({ error });
}
const result = await executionPlanBuilder.build(request);
if (result.error) {
return res.status(400).json(result);
}
return res.json(result);
} catch (error) {
return handlePlannerFailure(res, 'internal execution plan build', error);
}
});
export default router;

View File

@@ -0,0 +1,142 @@
import { createServer } from 'http';
import express from 'express';
import quoteRoutes from './quote';
import { getCanonicalTokenBySymbol } from '../../config/canonical-tokens';
var mockGetPoolsByToken: jest.Mock;
var mockGetLiveDodoPools: jest.Mock;
jest.mock('../../database/repositories/pool-repo', () => {
mockGetPoolsByToken = jest.fn();
return {
__esModule: true,
PoolRepository: jest.fn().mockImplementation(() => ({
getPoolsByToken: mockGetPoolsByToken,
})),
};
});
jest.mock('../../services/live-dodo-fallback', () => {
mockGetLiveDodoPools = jest.fn();
return {
__esModule: true,
getLiveDodoPools: mockGetLiveDodoPools,
};
});
/** Real GRU loader filters pools by on-file routable addresses; synthetic test pools would be dropped. */
jest.mock('../../config/gru-transport', () => ({
filterPoolsForRouting: <T>(_chainId: number, pools: T[]) => pools,
}));
jest.mock('../middleware/cache');
function createApp() {
const app = express();
app.use('/api/v1', quoteRoutes);
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('Quote API', () => {
let server: ReturnType<typeof createServer>;
let baseUrl: string;
beforeAll(async () => {
const started = await startServer(createApp());
server = started.server;
baseUrl = started.baseUrl;
});
beforeEach(() => {
mockGetPoolsByToken.mockReset();
mockGetLiveDodoPools.mockReset();
});
afterAll((done) => {
server.close(done);
});
it('quotes staged cUSDT_V2 against active cUSDT liquidity on Chain 138', async () => {
const cusdtV2 = getCanonicalTokenBySymbol(138, 'cUSDT_V2');
const cusdtV1 = getCanonicalTokenBySymbol(138, 'cUSDT');
if (!cusdtV2?.addresses[138] || !cusdtV1?.addresses[138]) {
throw new Error('cUSDT_V2 / cUSDT Chain 138 addresses required for this test');
}
const tokenInV2 = String(cusdtV2.addresses[138]);
const cusdtV1Lookup = String(cusdtV1.addresses[138]).toLowerCase();
mockGetPoolsByToken.mockImplementation(async (_chainId: number, tokenAddress: string) => {
if (tokenAddress.toLowerCase() === cusdtV1Lookup) {
return [
{
chainId: 138,
poolAddress: '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
token0Address: cusdtV1Lookup,
token1Address: '0x004b63a7b5b0e06f6bb6adb4a5f9f590bf3182d1',
dexType: 'dodo',
reserve0: '1000000',
reserve1: '1000000',
reserve0Usd: 1000000,
reserve1Usd: 1000000,
totalLiquidityUsd: 2000000,
volume24h: 10000,
lastUpdated: new Date(),
},
];
}
return [];
});
const res = await fetch(
`${baseUrl}/api/v1/quote?chainId=138&tokenIn=${encodeURIComponent(tokenInV2)}&tokenOut=0x004b63A7B5b0E06f6bB6adb4a5F9f590BF3182D1&amountIn=1000`
);
expect(res.status).toBe(200);
const body = (await res.json()) as Record<string, any>;
expect(body.amountOut).toBeTruthy();
expect(['constant-product', 'pmm-onchain']).toContain(body.quoteEngine);
expect(body.poolAddress).toBe('0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa');
expect(body.canonicalLiquidity).toMatchObject({
requestedTokenInSymbol: 'cUSDT_V2',
lookupTokenInSymbol: 'cUSDT',
lookupTokenInAddress: cusdtV1Lookup,
usedFallback: true,
});
expect(mockGetPoolsByToken).toHaveBeenCalledWith(138, cusdtV1Lookup);
});
it('falls back to live DODO pools when indexed liquidity is empty', async () => {
mockGetPoolsByToken.mockResolvedValue([]);
mockGetLiveDodoPools.mockResolvedValue([
{
chainId: 138,
poolAddress: '0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',
token0Address: '0x93e66202a11b1772e55407b32b44e5cd8eda7f22',
token1Address: '0xf22258f57794cc8e06237084b353ab30fffa640b',
dexType: 'dodo',
factoryAddress: '0x86ada6ef91a3b450f89f2b751e93b1b7a3218895',
reserve0: '1000000',
reserve1: '1000000',
reserve0Usd: 1000000,
reserve1Usd: 1000000,
totalLiquidityUsd: 2000000,
volume24h: 0,
lastUpdated: new Date(),
},
]);
const res = await fetch(
`${baseUrl}/api/v1/quote?chainId=138&tokenIn=0x93E66202A11B1772E55407B32B44e5Cd8eda7f22&tokenOut=0xf22258f57794CC8E06237084b353Ab30fFfa640b&amountIn=1000`
);
expect(res.status).toBe(200);
const body = (await res.json()) as Record<string, any>;
expect(body.amountOut).toBeTruthy();
expect(['constant-product', 'pmm-onchain']).toContain(body.quoteEngine);
expect(body.poolAddress).toBe('0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb');
expect(body.executorAddress).toBe('0x86ada6ef91a3b450f89f2b751e93b1b7a3218895');
expect(mockGetLiveDodoPools).toHaveBeenCalledWith(138);
});
});

View File

@@ -2,12 +2,24 @@ import { Router, Request, Response } from 'express';
import { PoolRepository } from '../../database/repositories/pool-repo';
import { cacheMiddleware } from '../middleware/cache';
import { logger } from '../../utils/logger';
import { filterPoolsForRouting } from '../../config/gru-transport';
import { resolveCanonicalQuoteAddress } from '../../config/canonical-tokens';
import { getLiveDodoPools } from '../../services/live-dodo-fallback';
import {
pmmQuoteAmountOutFromChain,
resolvePmmQuoteRpcUrl,
resolvePmmQuoteTrader,
} from '../../services/pmm-onchain-quote';
const router: Router = Router();
const poolRepo = new PoolRepository();
/**
* Uniswap V2-style constant-product quote: amountOut = (reserveOut * amountIn * 997) / (reserveIn * 1000 + amountIn * 997)
*
* Note: DODO PMM pools do not follow this curve; `amountOut` for dexType `dodo` can diverge from
* on-chain `querySellBase` / `querySellQuote`. Clients that set swap minOut from this endpoint alone
* may revert (wallets show failed gas estimation). Prefer on-chain PMM quotes for execution bounds.
*/
function quoteAmountOut(
amountIn: bigint,
@@ -42,6 +54,7 @@ router.get(
return res.status(400).json({
error: 'Missing required query: chainId, tokenIn, tokenOut, amountIn',
amountOut: null,
quoteEngine: null,
});
}
@@ -52,18 +65,37 @@ router.get(
return res.status(400).json({
error: 'Invalid amountIn (must be integer string)',
amountOut: null,
quoteEngine: null,
});
}
if (tokenIn === tokenOut) {
return res.json({ amountOut: amountInRaw, poolAddress: null });
return res.json({
amountOut: amountInRaw,
poolAddress: null,
quoteEngine: 'identity',
});
}
const pools = await poolRepo.getPoolsByToken(chainId, tokenIn);
const pairPools = pools.filter(
const tokenInResolution = resolveCanonicalQuoteAddress(chainId, tokenIn);
const tokenOutResolution = resolveCanonicalQuoteAddress(chainId, tokenOut);
const indexedPoolsRaw = await poolRepo.getPoolsByToken(
chainId,
tokenInResolution.lookupAddress
);
const indexedPools = filterPoolsForRouting(chainId, indexedPoolsRaw ?? []);
const livePools =
indexedPools.length > 0
? []
: filterPoolsForRouting(chainId, (await getLiveDodoPools(chainId)) ?? []).filter(
(pool) =>
pool.token0Address.toLowerCase() === tokenInResolution.lookupAddress ||
pool.token1Address.toLowerCase() === tokenInResolution.lookupAddress
);
const pairPools = [...indexedPools, ...livePools].filter(
(p) =>
p.token0Address.toLowerCase() === tokenOut ||
p.token1Address.toLowerCase() === tokenOut
p.token0Address.toLowerCase() === tokenOutResolution.lookupAddress ||
p.token1Address.toLowerCase() === tokenOutResolution.lookupAddress
);
if (pairPools.length === 0) {
@@ -71,6 +103,18 @@ router.get(
amountOut: null,
error: 'No pool found for this token pair',
poolAddress: null,
quoteEngine: null,
canonicalLiquidity: {
requestedTokenInAddress: tokenInResolution.requestedAddress,
requestedTokenOutAddress: tokenOutResolution.requestedAddress,
requestedTokenInSymbol: tokenInResolution.requestedSymbol,
requestedTokenOutSymbol: tokenOutResolution.requestedSymbol,
lookupTokenInAddress: tokenInResolution.lookupAddress,
lookupTokenOutAddress: tokenOutResolution.lookupAddress,
lookupTokenInSymbol: tokenInResolution.lookupSymbol,
lookupTokenOutSymbol: tokenOutResolution.lookupSymbol,
usedFallback: tokenInResolution.usedFallback || tokenOutResolution.usedFallback,
},
});
}
@@ -79,11 +123,11 @@ router.get(
for (const pool of pairPools) {
const reserveIn =
pool.token0Address.toLowerCase() === tokenIn
pool.token0Address.toLowerCase() === tokenInResolution.lookupAddress
? BigInt(pool.reserve0)
: BigInt(pool.reserve1);
const reserveOut =
pool.token0Address.toLowerCase() === tokenOut
pool.token0Address.toLowerCase() === tokenOutResolution.lookupAddress
? BigInt(pool.reserve0)
: BigInt(pool.reserve1);
const out = quoteAmountOut(amountIn, reserveIn, reserveOut);
@@ -93,16 +137,46 @@ router.get(
}
}
let quoteEngine: 'constant-product' | 'pmm-onchain' = 'constant-product';
const pmmRpc = resolvePmmQuoteRpcUrl();
if (chainId === 138 && bestPool.dexType === 'dodo' && pmmRpc) {
const onChainOut = await pmmQuoteAmountOutFromChain({
rpcUrl: pmmRpc,
poolAddress: bestPool.poolAddress,
tokenInLookup: tokenInResolution.lookupAddress,
amountIn,
traderForView: resolvePmmQuoteTrader(),
});
if (onChainOut !== null && onChainOut > BigInt(0)) {
bestAmountOut = onChainOut;
quoteEngine = 'pmm-onchain';
}
}
res.json({
amountOut: bestAmountOut.toString(),
poolAddress: bestPool.poolAddress,
dexType: bestPool.dexType,
executorAddress: bestPool.factoryAddress || null,
quoteEngine,
canonicalLiquidity: {
requestedTokenInAddress: tokenInResolution.requestedAddress,
requestedTokenOutAddress: tokenOutResolution.requestedAddress,
requestedTokenInSymbol: tokenInResolution.requestedSymbol,
requestedTokenOutSymbol: tokenOutResolution.requestedSymbol,
lookupTokenInAddress: tokenInResolution.lookupAddress,
lookupTokenOutAddress: tokenOutResolution.lookupAddress,
lookupTokenInSymbol: tokenInResolution.lookupSymbol,
lookupTokenOutSymbol: tokenOutResolution.lookupSymbol,
usedFallback: tokenInResolution.usedFallback || tokenOutResolution.usedFallback,
},
});
} catch (error) {
logger.error('Quote error:', error);
res.status(500).json({
error: error instanceof Error ? error.message : 'Internal server error',
amountOut: null,
quoteEngine: null,
});
}
}

View File

@@ -23,6 +23,17 @@ jest.mock('../../database/repositories/pool-repo', () => ({
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() {
@@ -49,8 +60,16 @@ describe('Report API', () => {
baseUrl = started.baseUrl;
});
afterAll((done) => {
server.close(done);
afterAll(async () => {
await new Promise<void>((resolve, reject) => {
server.close((error) => {
if (error) {
reject(error);
return;
}
resolve();
});
});
});
describe('GET /api/v1/report/cmc', () => {
@@ -85,4 +104,345 @@ describe('Report API', () => {
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',
}),
])
);
});
});
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/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',
}),
]),
}),
],
}),
]);
});
});
});

View File

@@ -11,6 +11,7 @@ import {
CANONICAL_TOKENS,
getCanonicalTokensByChain,
getLogoUriForSpec,
getTokenRegistryFamily,
} from '../../config/canonical-tokens';
import { resolvePoolTokenDisplays } from '../../services/token-display';
import { getSupportedChainIds } from '../../config/chains';
@@ -18,6 +19,13 @@ import { cacheMiddleware } from '../middleware/cache';
import { fetchRemoteJson } from '../utils/fetch-remote-json';
import { buildCrossChainReport } from '../../indexer/cross-chain-indexer';
import { logger } from '../../utils/logger';
import { filterPoolsForExposure, getActiveTransportPairs, getGruTransportMetadata } from '../../config/gru-transport';
import {
buildCwRegistryChains,
buildGasRegistryChains,
loadDeploymentStatusFile,
type CwRegistryChain,
} from '../../config/deployment-status';
const router: Router = Router();
const tokenRepo = new TokenRepository();
@@ -35,6 +43,12 @@ async function buildTokenReport(chainId: number) {
type: string;
decimals: number;
currencyCode?: string;
registryFamily?: string;
familySymbol?: string;
deploymentVersion?: string;
deploymentStatus?: string;
preferredForX402?: boolean;
liquiditySourceSymbol?: string;
market?: {
priceUsd?: number;
volume24h: number;
@@ -64,9 +78,10 @@ async function buildTokenReport(chainId: number) {
marketDataRepo.getMarketData(chainId, address),
poolRepo.getPoolsByToken(chainId, address),
]);
const exposedPools = filterPoolsForExposure(chainId, pools);
const resolvedPools = await Promise.all(
pools.map(async (p) => {
exposedPools.map(async (p) => {
const { token0, token1 } = await resolvePoolTokenDisplays(tokenRepo, chainId, p.token0Address, p.token1Address);
return {
poolAddress: p.poolAddress,
@@ -87,6 +102,12 @@ async function buildTokenReport(chainId: number) {
type: spec.type,
decimals: spec.decimals,
currencyCode: spec.currencyCode,
registryFamily: getTokenRegistryFamily(spec),
familySymbol: spec.familySymbol,
deploymentVersion: spec.deploymentVersion,
deploymentStatus: spec.deploymentStatus,
preferredForX402: spec.preferredForX402,
liquiditySourceSymbol: spec.liquiditySourceSymbol,
market: marketData
? {
priceUsd: marketData.priceUsd,
@@ -115,6 +136,87 @@ async function buildTokenReport(chainId: number) {
return out;
}
function describeToken(spec: { currencyCode?: string; registryFamily?: string }): string | undefined {
const family = String(spec.registryFamily || '').trim();
const code = String(spec.currencyCode || '').trim().toUpperCase();
if (!code) return undefined;
if (family === 'gas_native') {
return `Governance-approved gas-native ${code} compliant token`;
}
if (family === 'monetary_unit') {
return `GRU monetary-unit ${code} compliant token`;
}
if (family === 'commodity') {
return `Governance-approved commodity ${code} compliant token`;
}
return `ISO-4217 ${code} compliant token`;
}
function buildGruTransportOverview() {
const gruTransportMetadata = getGruTransportMetadata();
if (!gruTransportMetadata) return undefined;
return {
system: gruTransportMetadata.system,
summary: gruTransportMetadata.counts,
gasAssetFamilies: gruTransportMetadata.gasAssetFamilies ?? [],
gasRedeemGroups: gruTransportMetadata.gasRedeemGroups ?? [],
gasProtocolExposure: gruTransportMetadata.gasProtocolExposure ?? [],
activeTransportPairs: getActiveTransportPairs().map((pair) => ({
key: pair.key,
canonicalSymbol: pair.canonicalSymbol,
mirroredSymbol: pair.mirroredSymbol,
destinationChainId: pair.destinationChainId,
destinationChainName: pair.destinationChainName ?? null,
assetClass: pair.assetClass,
familyKey: pair.familyKey,
backingMode: pair.backingMode,
redeemPolicy: pair.redeemPolicy,
wrappedNativeQuoteSymbol: pair.wrappedNativeQuoteSymbol ?? null,
stableQuoteSymbol: pair.stableQuoteSymbol ?? null,
eligible: pair.eligible === true,
runtimeReady: pair.runtimeReady === true,
supplyInvariantSatisfied: pair.supplyInvariantSatisfied ?? null,
eligibilityBlockers: Array.isArray(pair.eligibilityBlockers)
? pair.eligibilityBlockers
: [],
runtimeMissingRequirements: Array.isArray(pair.runtimeMissingRequirements)
? pair.runtimeMissingRequirements
: [],
})),
};
}
function buildCanonicalCwFallback(chainIdFilter?: number | null): CwRegistryChain[] {
const grouped = new Map<number, CwRegistryChain>();
for (const spec of CANONICAL_TOKENS) {
if (spec.type !== 'w') continue;
for (const [chainIdText, address] of Object.entries(spec.addresses)) {
const chainId = Number(chainIdText);
if (!address || Number.isNaN(chainId)) continue;
if (chainIdFilter && chainId !== chainIdFilter) continue;
const existing = grouped.get(chainId) ?? {
chainId,
chainIdText,
name: `Chain ${chainIdText}`,
tokens: [],
};
existing.tokens.push({ symbol: spec.symbol, address });
grouped.set(chainId, existing);
}
}
return Array.from(grouped.values())
.map((row) => ({
...row,
tokens: row.tokens.sort((a, b) => a.symbol.localeCompare(b.symbol)),
}))
.sort((a, b) => a.chainId - b.chainId);
}
/** GET /report/cross-chain — cross-chain pools, bridge volume, atomic swaps (Chain 138, ALL Mainnet) */
router.get(
'/cross-chain',
@@ -158,10 +260,11 @@ router.get(
for (const chainId of chainIds) {
tokensByChain[chainId] = await buildTokenReport(chainId);
poolsByChain[chainId] = await poolRepo.getPoolsByChain(chainId);
poolsByChain[chainId] = filterPoolsForExposure(chainId, await poolRepo.getPoolsByChain(chainId));
}
const crossChainReport = await buildCrossChainReport(138).catch(() => null);
const gruTransport = buildGruTransportOverview();
const totalLiquidityByChain: Record<number, number> = {};
const totalVolume24hByChain: Record<number, number> = {};
@@ -196,6 +299,7 @@ router.get(
bridgeVolume24hTotal: crossChainReport.bridgeVolume24hTotal,
}
: undefined,
gruTransport,
});
} catch (error) {
logger.error('Error building report/all:', error);
@@ -221,7 +325,7 @@ router.get(
name: t.name,
asset_platform_id: chainId === 138 ? 'defi-oracle-meta' : chainId === 651940 ? 'all-mainnet' : `chain-${chainId}`,
decimals: t.decimals,
description: t.currencyCode ? `ISO-4217 ${t.currencyCode} compliant token` : undefined,
description: describeToken(t),
market_data: t.market
? {
current_price: { usd: t.market.priceUsd },
@@ -364,6 +468,11 @@ router.get(
decimals: number;
type: string;
logoURI: string;
registryFamily?: string;
familySymbol?: string;
deploymentVersion?: string;
deploymentStatus?: string;
preferredForX402?: boolean;
}> = [];
for (const chainId of chainIds) {
@@ -379,6 +488,11 @@ router.get(
decimals: spec.decimals,
type: spec.type,
logoURI: getLogoUriForSpec(spec),
registryFamily: getTokenRegistryFamily(spec),
familySymbol: spec.familySymbol,
deploymentVersion: spec.deploymentVersion,
deploymentStatus: spec.deploymentStatus,
preferredForX402: spec.preferredForX402,
});
}
}
@@ -398,20 +512,112 @@ router.get(
}
);
/** GET /report/cw-registry — live cW* registry from deployment-status.json when available. */
router.get('/cw-registry', async (req: Request, res: Response) => {
try {
const chainIdParam = req.query.chainId as string | undefined;
const chainIdFilter = chainIdParam ? parseInt(chainIdParam, 10) : null;
const fileBackedRegistry = loadDeploymentStatusFile();
let chains = fileBackedRegistry
? buildCwRegistryChains(fileBackedRegistry.data)
: buildCanonicalCwFallback(chainIdFilter);
if (chainIdFilter && !Number.isNaN(chainIdFilter)) {
chains = chains.filter((row) => row.chainId === chainIdFilter);
}
res.set('Cache-Control', 'public, max-age=0, must-revalidate');
res.json({
generatedAt: new Date().toISOString(),
source: fileBackedRegistry ? 'deployment-status-file' : 'canonical-fallback',
complete: !!fileBackedRegistry,
version: fileBackedRegistry?.data.version,
updated: fileBackedRegistry?.data.updated,
lastModified: fileBackedRegistry?.lastModified,
chains,
});
} catch (error) {
logger.error('Error building report/cw-registry:', error);
res.status(500).json({ error: 'Internal server error', chains: [] });
}
});
/** 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 {
const chainIdParam = req.query.chainId as string | undefined;
const chainIdFilter = chainIdParam ? parseInt(chainIdParam, 10) : null;
const fileBackedRegistry = loadDeploymentStatusFile();
const gruTransport = buildGruTransportOverview();
const runtimeGasPairs = getActiveTransportPairs()
.filter((pair) => pair.assetClass === 'gas_native')
.map((pair) => ({
key: pair.key,
destinationChainId: pair.destinationChainId,
destinationChainName: pair.destinationChainName ?? null,
familyKey: pair.familyKey ?? null,
canonicalSymbol: pair.canonicalSymbol,
mirroredSymbol: pair.mirroredSymbol,
wrappedNativeQuoteSymbol: pair.wrappedNativeQuoteSymbol ?? null,
stableQuoteSymbol: pair.stableQuoteSymbol ?? null,
backingMode: pair.backingMode ?? null,
redeemPolicy: pair.redeemPolicy ?? null,
runtimeReady: pair.runtimeReady === true,
supplyInvariantSatisfied: pair.supplyInvariantSatisfied ?? null,
eligibilityBlockers: Array.isArray(pair.eligibilityBlockers) ? pair.eligibilityBlockers : [],
runtimeMissingRequirements: Array.isArray(pair.runtimeMissingRequirements)
? pair.runtimeMissingRequirements
: [],
}));
let chains = fileBackedRegistry ? buildGasRegistryChains(fileBackedRegistry.data) : [];
if (chainIdFilter && !Number.isNaN(chainIdFilter)) {
chains = chains.filter((row) => row.chainId === chainIdFilter);
}
res.set('Cache-Control', 'public, max-age=0, must-revalidate');
res.json({
generatedAt: new Date().toISOString(),
source: fileBackedRegistry ? 'deployment-status-file' : 'transport-config-only',
complete: !!fileBackedRegistry,
version: fileBackedRegistry?.data.version,
updated: fileBackedRegistry?.data.updated,
lastModified: fileBackedRegistry?.lastModified,
gasAssetFamilies: gruTransport?.gasAssetFamilies ?? [],
gasRedeemGroups: gruTransport?.gasRedeemGroups ?? [],
gasProtocolExposure: gruTransport?.gasProtocolExposure ?? [],
runtimePairs: runtimeGasPairs,
chains,
});
} catch (error) {
logger.error('Error building report/gas-registry:', error);
res.status(500).json({ error: 'Internal server error', chains: [] });
}
});
/** GET /report/canonical — raw canonical spec list (no DB merge) */
router.get(
'/canonical',
cacheMiddleware(10 * 60 * 1000),
async (req: Request, res: Response) => {
try {
const gruTransport = buildGruTransportOverview();
res.json({
generatedAt: new Date().toISOString(),
gruTransport,
tokens: CANONICAL_TOKENS.map((t) => ({
symbol: t.symbol,
name: t.name,
type: t.type,
decimals: t.decimals,
currencyCode: t.currencyCode,
registryFamily: getTokenRegistryFamily(t),
familySymbol: t.familySymbol,
deploymentVersion: t.deploymentVersion,
deploymentStatus: t.deploymentStatus,
preferredForX402: t.preferredForX402,
liquiditySourceSymbol: t.liquiditySourceSymbol,
addresses: t.addresses,
})),
});

View File

@@ -0,0 +1,215 @@
import { createServer } from 'http';
import express from 'express';
import tokenMappingRoutes from './token-mapping';
jest.mock('../middleware/cache');
function createApp() {
const app = express();
app.use('/api/v1/token-mapping', tokenMappingRoutes);
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('Token mapping API with GRU Transport overlay', () => {
let server: ReturnType<typeof createServer>;
let baseUrl: string;
const originalChain138Bridge = process.env.CHAIN138_L1_BRIDGE;
const originalBscBridge = process.env.CW_BRIDGE_BSC;
const originalReserveVerifier = process.env.CW_RESERVE_VERIFIER_CHAIN138;
const originalReserveVault = process.env.CW_STABLECOIN_RESERVE_VAULT;
const originalReserveSystem = process.env.CW_RESERVE_SYSTEM;
const originalMaxOutstanding = process.env.CW_MAX_OUTSTANDING_USDT_BSC;
const originalMainnetBridge = process.env.CW_BRIDGE_MAINNET;
const originalBtcMainnetOutstanding = process.env.CW_MAX_OUTSTANDING_BTC_MAINNET;
const originalOptimismBridge = process.env.CW_BRIDGE_OPTIMISM;
const originalGasHybridVerifier = process.env.CW_GAS_HYBRID_CAP_VERIFIER_CHAIN138;
const originalGasEscrowVault = process.env.CW_GAS_ESCROW_VAULT_CHAIN138;
const originalGasTreasurySystem = process.env.CW_GAS_TREASURY_SYSTEM;
const originalEthL2Outstanding = process.env.CW_MAX_OUTSTANDING_ETH_L2_OPTIMISM;
const originalEthL2Supply = process.env.CW_GAS_OUTSTANDING_ETH_L2_OPTIMISM;
const originalEthL2Escrowed = process.env.CW_GAS_ESCROWED_ETH_L2_OPTIMISM;
const originalEthL2Treasury = process.env.CW_GAS_TREASURY_BACKED_ETH_L2_OPTIMISM;
const originalEthL2Cap = process.env.CW_GAS_TREASURY_CAP_ETH_L2_OPTIMISM;
const originalCwL1Bridge = process.env.CW_L1_BRIDGE;
const originalCwL1BridgeChain138 = process.env.CW_L1_BRIDGE_CHAIN138;
beforeAll(async () => {
const started = await startServer(createApp());
server = started.server;
baseUrl = started.baseUrl;
});
afterEach(() => {
if (originalChain138Bridge === undefined) {
delete process.env.CHAIN138_L1_BRIDGE;
} else {
process.env.CHAIN138_L1_BRIDGE = originalChain138Bridge;
}
if (originalBscBridge === undefined) {
delete process.env.CW_BRIDGE_BSC;
} else {
process.env.CW_BRIDGE_BSC = originalBscBridge;
}
if (originalReserveVerifier === undefined) {
delete process.env.CW_RESERVE_VERIFIER_CHAIN138;
} else {
process.env.CW_RESERVE_VERIFIER_CHAIN138 = originalReserveVerifier;
}
if (originalReserveVault === undefined) {
delete process.env.CW_STABLECOIN_RESERVE_VAULT;
} else {
process.env.CW_STABLECOIN_RESERVE_VAULT = originalReserveVault;
}
if (originalReserveSystem === undefined) {
delete process.env.CW_RESERVE_SYSTEM;
} else {
process.env.CW_RESERVE_SYSTEM = originalReserveSystem;
}
if (originalMaxOutstanding === undefined) {
delete process.env.CW_MAX_OUTSTANDING_USDT_BSC;
} else {
process.env.CW_MAX_OUTSTANDING_USDT_BSC = originalMaxOutstanding;
}
if (originalMainnetBridge === undefined) {
delete process.env.CW_BRIDGE_MAINNET;
} else {
process.env.CW_BRIDGE_MAINNET = originalMainnetBridge;
}
if (originalBtcMainnetOutstanding === undefined) {
delete process.env.CW_MAX_OUTSTANDING_BTC_MAINNET;
} else {
process.env.CW_MAX_OUTSTANDING_BTC_MAINNET = originalBtcMainnetOutstanding;
}
for (const [key, value] of Object.entries({
CW_BRIDGE_OPTIMISM: originalOptimismBridge,
CW_GAS_HYBRID_CAP_VERIFIER_CHAIN138: originalGasHybridVerifier,
CW_GAS_ESCROW_VAULT_CHAIN138: originalGasEscrowVault,
CW_GAS_TREASURY_SYSTEM: originalGasTreasurySystem,
CW_MAX_OUTSTANDING_ETH_L2_OPTIMISM: originalEthL2Outstanding,
CW_GAS_OUTSTANDING_ETH_L2_OPTIMISM: originalEthL2Supply,
CW_GAS_ESCROWED_ETH_L2_OPTIMISM: originalEthL2Escrowed,
CW_GAS_TREASURY_BACKED_ETH_L2_OPTIMISM: originalEthL2Treasury,
CW_GAS_TREASURY_CAP_ETH_L2_OPTIMISM: originalEthL2Cap,
})) {
if (value === undefined) {
delete process.env[key];
} else {
process.env[key] = value;
}
}
if (originalCwL1Bridge === undefined) {
delete process.env.CW_L1_BRIDGE;
} else {
process.env.CW_L1_BRIDGE = originalCwL1Bridge;
}
if (originalCwL1BridgeChain138 === undefined) {
delete process.env.CW_L1_BRIDGE_CHAIN138;
} else {
process.env.CW_L1_BRIDGE_CHAIN138 = originalCwL1BridgeChain138;
}
});
afterAll((done) => {
server.close(done);
});
it('returns the active GRU transport overlay', async () => {
const res = await fetch(`${baseUrl}/api/v1/token-mapping/transport/active`);
expect(res.status).toBe(200);
const body = (await res.json()) as Record<string, unknown>;
expect(body.system).toMatchObject({
name: 'GRU Monetary Transport Layer',
shortName: 'GRU Transport',
});
expect(Array.isArray(body.transportPairs)).toBe(true);
expect((body.transportPairs as unknown[]).length).toBeGreaterThan(0);
expect(body.counts).toMatchObject({
transportPairs: expect.any(Number),
runtimeReadyTransportPairs: expect.any(Number),
});
});
it('resolves active cUSDT transport from Chain 138 to BSC', async () => {
process.env.CHAIN138_L1_BRIDGE = '0x3333333333333333333333333333333333333333';
process.env.CW_BRIDGE_BSC = '0x4444444444444444444444444444444444444444';
process.env.CW_RESERVE_VERIFIER_CHAIN138 = '0x5555555555555555555555555555555555555555';
process.env.CW_STABLECOIN_RESERVE_VAULT = '0x6666666666666666666666666666666666666666';
process.env.CW_RESERVE_SYSTEM = '0x7777777777777777777777777777777777777777';
process.env.CW_MAX_OUTSTANDING_USDT_BSC = '1000000';
const source = '0x93E66202A11B1772E55407B32B44e5Cd8eda7f22';
const res = await fetch(
`${baseUrl}/api/v1/token-mapping/resolve?fromChain=138&toChain=56&address=${source}`
);
expect(res.status).toBe(200);
const body = (await res.json()) as Record<string, unknown>;
expect(body.addressOnTarget).toBe('0x9a1D0dBEE997929ED02fD19E0E199704d20914dB');
expect(body.activeTransportEligible).toBe(true);
expect(body.gruTransportRuntimeReady).toBe(true);
expect(body.gruTransportPairKey).toBe('138-56-cUSDT-cWUSDT');
expect(body.gruTransportCanonicalToken).toMatchObject({
symbol: 'cUSDT',
activeVersion: 'v1',
x402PreferredVersion: 'v2',
});
});
it('resolves active cBTC transport from Chain 138 to Ethereum mainnet', async () => {
process.env.CHAIN138_L1_BRIDGE = '0x3333333333333333333333333333333333333333';
process.env.CW_BRIDGE_MAINNET = '0x4444444444444444444444444444444444444444';
process.env.CW_RESERVE_VERIFIER_CHAIN138 = '0x5555555555555555555555555555555555555555';
process.env.CW_STABLECOIN_RESERVE_VAULT = '0x6666666666666666666666666666666666666666';
process.env.CW_RESERVE_SYSTEM = '0x7777777777777777777777777777777777777777';
process.env.CW_MAX_OUTSTANDING_BTC_MAINNET = '2100000000000000';
const source = '0xcb7c000000000000000000000000000000000138';
const res = await fetch(
`${baseUrl}/api/v1/token-mapping/resolve?fromChain=138&toChain=1&address=${source}`
);
expect(res.status).toBe(200);
const body = (await res.json()) as Record<string, unknown>;
expect(body.addressOnTarget).toBe('0xcb7c000000000000000000000000000000000001');
expect(body.activeTransportEligible).toBe(true);
expect(body.gruTransportRuntimeReady).toBe(true);
expect(body.gruTransportPairKey).toBe('138-1-cBTC-cWBTC');
});
it('resolves gas-family transport metadata for the shared ETH L2 lane', async () => {
process.env.CHAIN138_L1_BRIDGE = '0x3333333333333333333333333333333333333333';
process.env.CW_BRIDGE_OPTIMISM = '0x4444444444444444444444444444444444444444';
process.env.CW_GAS_HYBRID_CAP_VERIFIER_CHAIN138 = '0x5555555555555555555555555555555555555555';
process.env.CW_GAS_ESCROW_VAULT_CHAIN138 = '0x6666666666666666666666666666666666666666';
process.env.CW_GAS_TREASURY_SYSTEM = '0x7777777777777777777777777777777777777777';
process.env.CW_MAX_OUTSTANDING_ETH_L2_OPTIMISM = '125';
process.env.CW_GAS_OUTSTANDING_ETH_L2_OPTIMISM = '125';
process.env.CW_GAS_ESCROWED_ETH_L2_OPTIMISM = '100';
process.env.CW_GAS_TREASURY_BACKED_ETH_L2_OPTIMISM = '25';
process.env.CW_GAS_TREASURY_CAP_ETH_L2_OPTIMISM = '25';
/** Matches token-mapping-multichain.json 138→10 Compliant_ETH_L2_cW (not FALLBACK cETHL2 placeholder). */
const source = '0x18a6b163d255cc0cb32b99697843b487d059907d';
const res = await fetch(
`${baseUrl}/api/v1/token-mapping/resolve?fromChain=138&toChain=10&address=${source}`
);
expect(res.status).toBe(200);
const body = (await res.json()) as Record<string, unknown>;
expect(body.addressOnTarget).toBe('0x95007ec50d0766162f77848edf7bdc4eba147fb4');
expect(body.activeTransportEligible).toBe(true);
expect(body.gruTransportRuntimeReady).toBe(true);
expect(body.gruTransportPairKey).toBe('138-10-cETHL2-cWETHL2');
expect(body.gruTransportAssetClass).toBe('gas_native');
expect(body.gruTransportFamilyKey).toBe('eth_l2');
expect(body.gruTransportBackingMode).toBe('hybrid_cap');
expect(body.gruTransportRedeemPolicy).toBe('family_fungible_inventory_gated');
});
});

View File

@@ -5,29 +5,47 @@
*/
import { Router, Request, Response } from 'express';
import path from 'path';
import { createRequire } from 'module';
import { cacheMiddleware } from '../middleware/cache';
import { loadTokenMappingLoader } from '../../config/repo-config-loader';
const router: Router = Router();
/** Repo root (proxmox): when run from token-aggregation cwd, 2 levels up to smom-dbis-138, 1 more to proxmox */
const PROXMOX_ROOT = path.resolve(process.cwd(), '../../..');
const LOADER_PATH = path.join(PROXMOX_ROOT, 'config', 'token-mapping-loader.cjs');
const requireLoader = createRequire(path.join(PROXMOX_ROOT, 'package.json'));
function loadMultichainLoader(): {
getTokenMappingForPair: (from: number, to: number) => { tokens: unknown[]; addressMapFromTo: Record<string, string>; addressMapToFrom: Record<string, string> } | null;
getAllMultichainPairs: () => Array<{ fromChainId: number; toChainId: number; notes?: string }>;
getMappedAddress: (from: number, to: number, addr: string) => string | undefined;
getGruTransportMetadata?: () => {
system: Record<string, unknown> | null;
terminology: Record<string, string>;
enabledCanonicalTokens: Array<Record<string, unknown>>;
enabledDestinationChains: Array<Record<string, unknown>>;
counts: Record<string, number>;
} | null;
getActiveTransportPairs?: () => Array<Record<string, unknown>>;
getActiveTransportPair?: (from: number, to: number, criteria?: Record<string, unknown>) => Record<string, unknown> | null;
getActivePublicPools?: () => Array<Record<string, unknown>>;
getEnabledCanonicalToken?: (identifier: string) => Record<string, unknown> | null;
isGasRedemptionPathAllowed?: (from: number, to: number, identifier: string) => boolean;
} | null {
try {
const loader = requireLoader(LOADER_PATH);
if (loader?.getTokenMappingForPair && loader?.getAllMultichainPairs && loader?.getMappedAddress) {
return loader;
}
} catch {
// config not available when run outside monorepo
const loader = loadTokenMappingLoader<{
getTokenMappingForPair: (from: number, to: number) => { tokens: unknown[]; addressMapFromTo: Record<string, string>; addressMapToFrom: Record<string, string> } | null;
getAllMultichainPairs: () => Array<{ fromChainId: number; toChainId: number; notes?: string }>;
getMappedAddress: (from: number, to: number, addr: string) => string | undefined;
getGruTransportMetadata?: () => {
system: Record<string, unknown> | null;
terminology: Record<string, string>;
enabledCanonicalTokens: Array<Record<string, unknown>>;
enabledDestinationChains: Array<Record<string, unknown>>;
counts: Record<string, number>;
} | null;
getActiveTransportPairs?: () => Array<Record<string, unknown>>;
getActiveTransportPair?: (from: number, to: number, criteria?: Record<string, unknown>) => Record<string, unknown> | null;
getActivePublicPools?: () => Array<Record<string, unknown>>;
getEnabledCanonicalToken?: (identifier: string) => Record<string, unknown> | null;
isGasRedemptionPathAllowed?: (from: number, to: number, identifier: string) => boolean;
}>();
if (loader) {
return loader;
}
return null;
}
@@ -65,12 +83,31 @@ router.get(
});
}
const activePairs = loader.getActiveTransportPairs
? loader
.getActiveTransportPairs()
.filter((pair) => {
const canonicalChainId = Number(pair.canonicalChainId);
const destinationChainId = Number(pair.destinationChainId);
return (
(canonicalChainId === fromChain && destinationChainId === toChain) ||
(canonicalChainId === toChain && destinationChainId === fromChain)
);
})
: [];
return res.json({
fromChainId: fromChain,
toChainId: toChain,
tokens: result.tokens,
addressMapFromTo: result.addressMapFromTo,
addressMapToFrom: result.addressMapToFrom,
gruTransport: loader.getGruTransportMetadata
? {
system: loader.getGruTransportMetadata()?.system ?? null,
activePairs,
}
: undefined,
});
}
);
@@ -90,7 +127,38 @@ router.get(
});
}
const pairs = loader.getAllMultichainPairs();
return res.json({ pairs });
return res.json({
pairs,
gruTransport: loader.getGruTransportMetadata
? {
system: loader.getGruTransportMetadata()?.system ?? null,
activePairs: loader.getActiveTransportPairs ? loader.getActiveTransportPairs() : [],
}
: undefined,
});
}
);
/**
* GET /api/v1/token-mapping/transport/active
* Returns the GRU Monetary Transport Layer overlay as seen by token-aggregation.
*/
router.get(
'/transport/active',
cacheMiddleware(5 * 60 * 1000),
(_req: Request, res: Response) => {
const loader = loadMultichainLoader();
if (!loader || !loader.getGruTransportMetadata || !loader.getActiveTransportPairs) {
return res.status(503).json({
error: 'GRU transport config not available (run from monorepo with config/gru-transport-active.json)',
});
}
return res.json({
...loader.getGruTransportMetadata(),
transportPairs: loader.getActiveTransportPairs(),
publicPools: loader.getActivePublicPools ? loader.getActivePublicPools() : [],
});
}
);
@@ -120,11 +188,64 @@ router.get(
}
const mapped = loader.getMappedAddress(fromChain, toChain, address);
const activeTransportPair = loader.getActiveTransportPair
? loader.getActiveTransportPair(fromChain, toChain, {
address,
targetTokenAddress: mapped ?? null,
})
: null;
const canonicalTokenIdentifier =
(activeTransportPair && typeof activeTransportPair.canonicalSymbol === 'string'
? activeTransportPair.canonicalSymbol
: null) ??
address;
const canonicalToken = loader.getEnabledCanonicalToken
? loader.getEnabledCanonicalToken(canonicalTokenIdentifier)
: null;
return res.json({
fromChainId: fromChain,
toChainId: toChain,
addressOnSource: address,
addressOnTarget: mapped ?? null,
activeTransportEligible: !!activeTransportPair && activeTransportPair.eligible === true,
gruTransportRuntimeReady: !!activeTransportPair && activeTransportPair.runtimeReady === true,
gruTransportPairKey:
activeTransportPair && typeof activeTransportPair.key === 'string' ? activeTransportPair.key : null,
gruTransportAssetClass:
activeTransportPair && typeof activeTransportPair.assetClass === 'string'
? activeTransportPair.assetClass
: null,
gruTransportFamilyKey:
activeTransportPair && typeof activeTransportPair.familyKey === 'string'
? activeTransportPair.familyKey
: null,
gruTransportBackingMode:
activeTransportPair && typeof activeTransportPair.backingMode === 'string'
? activeTransportPair.backingMode
: null,
gruTransportRedeemPolicy:
activeTransportPair && typeof activeTransportPair.redeemPolicy === 'string'
? activeTransportPair.redeemPolicy
: null,
gruTransportWrappedNativeQuoteSymbol:
activeTransportPair && typeof activeTransportPair.wrappedNativeQuoteSymbol === 'string'
? activeTransportPair.wrappedNativeQuoteSymbol
: null,
gruTransportStableQuoteSymbol:
activeTransportPair && typeof activeTransportPair.stableQuoteSymbol === 'string'
? activeTransportPair.stableQuoteSymbol
: null,
gruTransportReferenceVenue:
activeTransportPair && typeof activeTransportPair.referenceVenue === 'string'
? activeTransportPair.referenceVenue
: null,
gruGasRedemptionPathAllowed:
activeTransportPair &&
typeof activeTransportPair.familyKey === 'string' &&
loader.isGasRedemptionPathAllowed
? loader.isGasRedemptionPathAllowed(fromChain, toChain, activeTransportPair.familyKey)
: null,
gruTransportCanonicalToken: canonicalToken,
});
}
);

View File

@@ -1,14 +1,21 @@
import { Router, Request, Response } from 'express';
import { TokenRepository } from '../../database/repositories/token-repo';
import { TokenRepository, Token } from '../../database/repositories/token-repo';
import { MarketDataRepository } from '../../database/repositories/market-data-repo';
import { PoolRepository } from '../../database/repositories/pool-repo';
import { PoolRepository, LiquidityPool } from '../../database/repositories/pool-repo';
import { OHLCVGenerator } from '../../indexer/ohlcv-generator';
import { CoinGeckoAdapter } from '../../adapters/coingecko-adapter';
import { CoinMarketCapAdapter } from '../../adapters/cmc-adapter';
import { DexScreenerAdapter } from '../../adapters/dexscreener-adapter';
import { cacheMiddleware } from '../middleware/cache';
import { resolvePoolTokenDisplays } from '../../services/token-display';
import { resolvePoolTokenDisplays, resolveTokenDisplay } from '../../services/token-display';
import { logger } from '../../utils/logger';
import { filterPoolsForExposure, shouldExposePublicPool } from '../../config/gru-transport';
import {
getCanonicalTokenByAddress,
getCanonicalTokensByChain,
resolveCanonicalQuoteAddress,
} from '../../config/canonical-tokens';
import { getLiveDodoPools } from '../../services/live-dodo-fallback';
const router: Router = Router();
const tokenRepo = new TokenRepository();
@@ -19,6 +26,122 @@ const coingeckoAdapter = new CoinGeckoAdapter();
const cmcAdapter = new CoinMarketCapAdapter();
const dexscreenerAdapter = new DexScreenerAdapter();
function tokenFromCanonical(chainId: number, address: string): Token | null {
const spec = getCanonicalTokenByAddress(chainId, address.toLowerCase());
if (!spec) {
return null;
}
return {
chainId,
address: address.toLowerCase(),
name: spec.name,
symbol: spec.symbol,
decimals: spec.decimals,
verified: true,
};
}
async function getPoolsByTokenWithFallback(chainId: number, address: string): Promise<LiquidityPool[]> {
const normalized = address.toLowerCase();
const resolution = resolveCanonicalQuoteAddress(chainId, normalized);
const dbPools = filterPoolsForExposure(
chainId,
await poolRepo.getPoolsByToken(chainId, resolution.lookupAddress)
);
if (dbPools.length > 0) {
return dbPools;
}
const livePools = filterPoolsForExposure(chainId, await getLiveDodoPools(chainId));
return livePools.filter(
(pool) =>
pool.token0Address === resolution.lookupAddress ||
pool.token1Address === resolution.lookupAddress ||
pool.token0Address === normalized ||
pool.token1Address === normalized
);
}
async function getTokenWithFallback(chainId: number, address: string): Promise<Token | null> {
const normalized = address.toLowerCase();
const token = await tokenRepo.getToken(chainId, normalized);
if (token) {
return token;
}
const canonical = tokenFromCanonical(chainId, normalized);
if (canonical) {
return canonical;
}
const resolution = resolveCanonicalQuoteAddress(chainId, normalized);
const livePools = await getLiveDodoPools(chainId);
const liveAddress =
livePools.find((pool) => pool.token0Address === resolution.lookupAddress || pool.token1Address === resolution.lookupAddress)
? resolution.lookupAddress
: null;
if (!liveAddress) {
return null;
}
const display = await resolveTokenDisplay(tokenRepo, chainId, liveAddress);
return {
chainId,
address: normalized,
name: display.name,
symbol: display.symbol,
decimals: display.decimals,
verified: display.source !== 'fallback',
};
}
async function getTokensWithFallback(
chainId: number,
limit: number,
offset: number
): Promise<{ tokens: Token[]; source: 'db' | 'live-dodo' | 'canonical' }> {
const dbTokens = await tokenRepo.getTokens(chainId, limit, offset);
if (dbTokens.length > 0) {
return { tokens: dbTokens, source: 'db' };
}
const livePools = await getLiveDodoPools(chainId);
if (livePools.length > 0) {
const tokenAddresses = [...new Set(livePools.flatMap((pool) => [pool.token0Address, pool.token1Address]))];
const liveTokens = await Promise.all(
tokenAddresses.map(async (address) => {
const display = await resolveTokenDisplay(tokenRepo, chainId, address);
return {
chainId,
address: display.address,
name: display.name,
symbol: display.symbol,
decimals: display.decimals,
verified: display.source !== 'fallback',
} as Token;
})
);
const sorted = liveTokens.sort((a, b) =>
`${a.symbol || ''}${a.address}`.localeCompare(`${b.symbol || ''}${b.address}`)
);
return { tokens: sorted.slice(offset, offset + limit), source: 'live-dodo' };
}
const canonicalTokens = getCanonicalTokensByChain(chainId)
.map((spec) => ({
chainId,
address: String(spec.addresses[chainId]).toLowerCase(),
name: spec.name,
symbol: spec.symbol,
decimals: spec.decimals,
verified: true,
}) as Token)
.sort((a, b) => `${a.symbol || ''}${a.address}`.localeCompare(`${b.symbol || ''}${b.address}`));
return { tokens: canonicalTokens.slice(offset, offset + limit), source: 'canonical' };
}
router.get('/chains', cacheMiddleware(5 * 60 * 1000), async (req: Request, res: Response) => {
try {
res.json({
@@ -51,7 +174,7 @@ router.get('/tokens', cacheMiddleware(60 * 1000), async (req: Request, res: Resp
return res.status(400).json({ error: 'chainId is required' });
}
const tokens = await tokenRepo.getTokens(chainId, limit, offset);
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);
@@ -60,7 +183,7 @@ router.get('/tokens', cacheMiddleware(60 * 1000), async (req: Request, res: Resp
market: marketData || undefined,
};
if (includeDodoPool) {
const pools = await poolRepo.getPoolsByToken(chainId, token.address);
const pools = await getPoolsByTokenWithFallback(chainId, token.address);
const dodoPool = pools.find((p) => (p.dexType || '').toLowerCase() === 'dodo');
out.hasDodoPool = !!dodoPool;
out.pmmPool = dodoPool?.poolAddress || undefined;
@@ -76,6 +199,7 @@ router.get('/tokens', cacheMiddleware(60 * 1000), async (req: Request, res: Resp
offset,
count: tokensWithMarketData.length,
},
source,
});
} catch (error) {
logger.error('Error fetching tokens:', error);
@@ -92,17 +216,19 @@ router.get('/tokens/:address', cacheMiddleware(60 * 1000), async (req: Request,
return res.status(400).json({ error: 'chainId is required' });
}
const token = await tokenRepo.getToken(chainId, address);
const normalizedAddress = address.toLowerCase();
const resolution = resolveCanonicalQuoteAddress(chainId, normalizedAddress);
const token = await getTokenWithFallback(chainId, normalizedAddress);
if (!token) {
return res.status(404).json({ error: 'Token not found' });
}
const [marketData, pools, coingeckoData, cmcData, dexscreenerData] = await Promise.all([
marketDataRepo.getMarketData(chainId, address),
poolRepo.getPoolsByToken(chainId, address),
coingeckoAdapter.getTokenByContract(chainId, address),
cmcAdapter.getTokenByContract(chainId, address),
dexscreenerAdapter.getTokenByContract(chainId, address),
marketDataRepo.getMarketData(chainId, resolution.lookupAddress),
getPoolsByTokenWithFallback(chainId, normalizedAddress),
coingeckoAdapter.getTokenByContract(chainId, resolution.lookupAddress),
cmcAdapter.getTokenByContract(chainId, resolution.lookupAddress),
dexscreenerAdapter.getTokenByContract(chainId, resolution.lookupAddress),
]);
res.json({
@@ -131,6 +257,15 @@ router.get('/tokens/:address', cacheMiddleware(60 * 1000), async (req: Request,
})),
hasDodoPool: pools.some((p) => (p.dexType || '').toLowerCase() === 'dodo'),
pmmPool: pools.find((p) => (p.dexType || '').toLowerCase() === 'dodo')?.poolAddress || undefined,
canonicalLiquidity:
resolution.usedFallback
? {
requestedAddress: normalizedAddress,
lookupAddress: resolution.lookupAddress,
requestedSymbol: resolution.requestedSymbol,
lookupSymbol: resolution.lookupSymbol,
}
: undefined,
},
});
} catch (error) {
@@ -148,7 +283,7 @@ router.get('/tokens/:address/pools', cacheMiddleware(60 * 1000), async (req: Req
return res.status(400).json({ error: 'chainId is required' });
}
const pools = await poolRepo.getPoolsByToken(chainId, address);
const pools = await getPoolsByTokenWithFallback(chainId, address);
res.json({
pools: await Promise.all(
@@ -280,7 +415,10 @@ router.get('/pools/:poolAddress', cacheMiddleware(60 * 1000), async (req: Reques
}
const pool = await poolRepo.getPool(chainId, poolAddress);
if (!pool) {
if (
!pool ||
!shouldExposePublicPool(chainId, pool.poolAddress, pool.token0Address, pool.token1Address)
) {
return res.status(404).json({ error: 'Pool not found' });
}

View File

@@ -14,6 +14,7 @@ import heatmapRoutes from './routes/heatmap';
import arbitrageRoutes from './routes/arbitrage';
import aggregatorRouteMatrixRoutes from './routes/aggregator-routes';
import partnerPayloadRoutes from './routes/partner-payloads';
import plannerV2Routes from './routes/planner-v2';
import { MultiChainIndexer } from '../indexer/chain-indexer';
import { getDatabasePool } from '../database/client';
import winston from 'winston';
@@ -39,19 +40,42 @@ const logger = winston.createLogger({
export class ApiServer {
private app: Express;
private port: number;
private indexer: MultiChainIndexer;
private indexerEnabled: boolean;
private indexer: MultiChainIndexer | null;
private resolveTrustProxySetting(): boolean | number | string {
const raw = (process.env.EXPRESS_TRUST_PROXY ?? process.env.TRUST_PROXY ?? '1').trim();
const normalized = raw.toLowerCase();
if (normalized === 'true') return true;
if (normalized === 'false') return false;
if (/^\d+$/.test(raw)) return parseInt(raw, 10);
return raw;
}
constructor() {
this.app = express();
this.port = parseInt(process.env.PORT || '3000', 10);
this.indexer = new MultiChainIndexer();
this.indexerEnabled = this.resolveFeatureFlag('ENABLE_INDEXER', true);
this.indexer = this.indexerEnabled ? new MultiChainIndexer() : null;
this.setupMiddleware();
this.setupRoutes();
this.setupErrorHandling();
}
private resolveFeatureFlag(name: string, fallback: boolean): boolean {
const raw = (process.env[name] || '').trim().toLowerCase();
if (!raw) return fallback;
if (['1', 'true', 'yes', 'on'].includes(raw)) return true;
if (['0', 'false', 'no', 'off'].includes(raw)) return false;
return fallback;
}
private setupMiddleware(): void {
const trustProxy = this.resolveTrustProxySetting();
this.app.set('trust proxy', trustProxy);
// CORS
this.app.use(cors());
@@ -69,6 +93,8 @@ export class ApiServer {
this.app.use((req: Request, res: Response, next: NextFunction) => {
logger.info(`${req.method} ${req.path}`, {
ip: req.ip,
forwardedFor: req.get('x-forwarded-for'),
trustProxy,
userAgent: req.get('user-agent'),
});
next();
@@ -88,7 +114,7 @@ export class ApiServer {
timestamp: new Date().toISOString(),
services: {
database: 'connected',
indexer: 'running',
indexer: this.indexerEnabled ? 'running' : 'disabled',
},
});
} catch (error) {
@@ -112,6 +138,7 @@ export class ApiServer {
this.app.use('/api/v1', arbitrageRoutes);
this.app.use('/api/v1', aggregatorRouteMatrixRoutes);
this.app.use('/api/v1', partnerPayloadRoutes);
this.app.use('/api/v2', plannerV2Routes);
// Admin routes (stricter rate limit)
this.app.use('/api/v1/admin', strictRateLimiter, adminRoutes);
@@ -124,6 +151,7 @@ export class ApiServer {
endpoints: {
health: '/health',
api: '/api/v1',
apiV2: '/api/v2',
},
});
});
@@ -148,11 +176,12 @@ export class ApiServer {
async start(): Promise<void> {
try {
// Initialize indexer
await this.indexer.initialize();
// Start indexing
await this.indexer.startAll();
if (this.indexer) {
await this.indexer.initialize();
await this.indexer.startAll();
} else {
logger.info('Token aggregation indexer disabled by ENABLE_INDEXER flag');
}
// Start server
this.app.listen(this.port, () => {
@@ -167,7 +196,7 @@ export class ApiServer {
}
async stop(): Promise<void> {
this.indexer.stopAll();
this.indexer?.stopAll();
logger.info('Server stopped');
}
}

View File

@@ -0,0 +1,84 @@
/**
* Built-in CCIP / Trustless bridge route payload when BRIDGE_LIST_JSON_URL is unset.
* Aligns with MetaMask Snap expectations and docs/07-ccip/CCIP_BRIDGE_MAINNET_CONNECTION.md.
*/
import { getActivePublicPools, getActiveTransportPairs, getGruTransportMetadata } from '../../config/gru-transport';
export interface BridgeRoutesPayload {
routes: {
weth9: Record<string, string>;
weth10: Record<string, string>;
trustless?: Record<string, string>;
};
chain138Bridges: {
weth9: string;
weth10: string;
trustless?: string;
};
tokenMappingApi: {
basePath: string;
pairs: string;
resolve: string;
note: string;
};
gruTransport?: {
system: unknown;
summary?: Record<string, number>;
activeTransportPairs: unknown[];
activePublicPools: unknown[];
};
}
const DEFAULT_WETH9_138 = '0xcacfd227A040002e49e2e01626363071324f820a';
const DEFAULT_WETH10_138 = '0xe0E93247376aa097dB308B92e6Ba36bA015535D0';
const DEFAULT_LOCKBOX_138 = '0xFce6f50B312B3D936Ea9693C5C9531CF92a3324c';
/** Destination-side WETH9 receivers (relay-backed where noted in CCIP docs). */
const WETH9_DESTINATIONS: Record<string, string> = {
'Ethereum Mainnet (1)': '0xF9A32F37099c582D28b4dE7Fca6eaC1e5259f939',
'BNB Chain (56)': '0x886C6A4ABC064dbf74E7caEc460b7eeC31F1b78C',
'Avalanche C-Chain (43114)': '0x3f8C409C6072a2B6a4Ff17071927bA70F80c725F',
};
function envAddr(key: string, fallback: string): string {
const v = process.env[key];
return typeof v === 'string' && v.startsWith('0x') ? v : fallback;
}
export function buildDefaultBridgeRoutes(): BridgeRoutesPayload {
const inboxEth = process.env.INBOX_ETH?.trim();
const trustlessRoutes: Record<string, string> = {};
if (inboxEth?.startsWith('0x')) {
trustlessRoutes['Ethereum Mainnet (1)'] = inboxEth;
}
const gruTransportMetadata = getGruTransportMetadata();
return {
routes: {
weth9: { ...WETH9_DESTINATIONS },
weth10: { ...WETH9_DESTINATIONS },
...(Object.keys(trustlessRoutes).length > 0 ? { trustless: trustlessRoutes } : {}),
},
chain138Bridges: {
weth9: envAddr('CCIPWETH9_BRIDGE_CHAIN138', DEFAULT_WETH9_138),
weth10: envAddr('CCIPWETH10_BRIDGE_CHAIN138', DEFAULT_WETH10_138),
trustless: envAddr('LOCKBOX_138', DEFAULT_LOCKBOX_138),
},
tokenMappingApi: {
basePath: '/api/v1/token-mapping',
pairs: '/api/v1/token-mapping/pairs',
resolve: '/api/v1/token-mapping/resolve',
note: 'Resolve bridged token addresses between chains; requires monorepo config/token-mapping-multichain.json on server.',
},
gruTransport: gruTransportMetadata
? {
system: gruTransportMetadata.system,
summary: gruTransportMetadata.counts,
activeTransportPairs: getActiveTransportPairs(),
activePublicPools: getActivePublicPools(),
}
: undefined,
};
}