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,
};
}

View File

@@ -0,0 +1,258 @@
import {
getCanonicalTokenByAddress,
getCanonicalTokenBySymbol,
getTokenRegistryFamily,
resolveCanonicalQuoteAddress,
} from './canonical-tokens';
describe('canonical cW token catalog', () => {
it('models cWUSDT, cWUSDC, cWAUSDT, and cWUSDW as first-class wrapped GRU transport assets', () => {
const cwUsdt = getCanonicalTokenBySymbol(56, 'cWUSDT');
expect(cwUsdt).toMatchObject({
symbol: 'cWUSDT',
type: 'w',
currencyCode: 'USD',
});
expect(cwUsdt?.addresses[56]).toBe('0x9a1D0dBEE997929ED02fD19E0E199704d20914dB');
expect(getCanonicalTokenByAddress(56, '0x9a1D0dBEE997929ED02fD19E0E199704d20914dB')?.symbol).toBe('cWUSDT');
const cwUsdc = getCanonicalTokenBySymbol(8453, 'cWUSDC');
expect(cwUsdc).toMatchObject({
symbol: 'cWUSDC',
type: 'w',
currencyCode: 'USD',
});
expect(cwUsdc?.addresses[8453]).toBe('0x377a5FaA3162b3Fc6f4e267301A3c817bAd18105');
expect(getCanonicalTokenByAddress(8453, '0x377a5FaA3162b3Fc6f4e267301A3c817bAd18105')?.symbol).toBe('cWUSDC');
const cwAusdt = getCanonicalTokenBySymbol(56, 'cWAUSDT');
expect(cwAusdt).toMatchObject({
symbol: 'cWAUSDT',
type: 'w',
currencyCode: 'USD',
});
expect(cwAusdt?.addresses[56]).toBe('0xe1a51Bc037a79AB36767561B147eb41780124934');
expect(getCanonicalTokenByAddress(56, '0xe1a51Bc037a79AB36767561B147eb41780124934')?.symbol).toBe('cWAUSDT');
const cwUsdw = getCanonicalTokenBySymbol(56, 'cWUSDW');
expect(cwUsdw).toMatchObject({
symbol: 'cWUSDW',
type: 'w',
currencyCode: 'USD',
});
expect(cwUsdw?.addresses[56]).toBe('0xC2FA05F12a75Ac84ea778AF9D6935cA807275E55');
expect(getCanonicalTokenByAddress(56, '0xC2FA05F12a75Ac84ea778AF9D6935cA807275E55')?.symbol).toBe('cWUSDW');
});
it('surfaces cUSDW on Chain 138 as the repo-native USDW hub asset', () => {
const cusdw = getCanonicalTokenBySymbol(138, 'cUSDW');
expect(cusdw).toMatchObject({
symbol: 'cUSDW',
type: 'base',
currencyCode: 'USD',
decimals: 6,
});
expect(cusdw?.addresses[138]).toBe('0xcA6BFa614935f1AB71c9aB106bAA6FBB6057095e');
});
it('models cBTC and cWBTC as GRU monetary-unit assets with satoshi precision', () => {
const cbtc = getCanonicalTokenBySymbol(138, 'cBTC');
expect(cbtc).toMatchObject({
symbol: 'cBTC',
type: 'base',
currencyCode: 'BTC',
decimals: 8,
});
expect(cbtc?.addresses[138]).toBe('0xcb7c000000000000000000000000000000000138');
expect(getTokenRegistryFamily(cbtc!)).toBe('monetary_unit');
const cwbtc = getCanonicalTokenBySymbol(1, 'cWBTC');
expect(cwbtc).toMatchObject({
symbol: 'cWBTC',
type: 'w',
currencyCode: 'BTC',
decimals: 8,
});
expect(cwbtc?.addresses[1]).toBe('0xcb7c000000000000000000000000000000000001');
expect(getCanonicalTokenByAddress(1, '0xcb7c000000000000000000000000000000000001')?.symbol).toBe('cWBTC');
expect(getTokenRegistryFamily(cwbtc!)).toBe('monetary_unit');
});
it('models gas-native families on Chain 138 and their public cW mirrors with gas-native metadata', () => {
const ceth = getCanonicalTokenBySymbol(138, 'cETH');
expect(ceth).toMatchObject({
symbol: 'cETH',
type: 'base',
currencyCode: 'ETH',
decimals: 18,
});
expect(ceth?.addresses[138]).toBe('0xce7e00000000000000000000000000000000008a');
expect(getTokenRegistryFamily(ceth!)).toBe('gas_native');
const cethL2 = getCanonicalTokenBySymbol(138, 'cETHL2');
expect(cethL2).toMatchObject({
symbol: 'cETHL2',
type: 'base',
currencyCode: 'ETH',
decimals: 18,
});
expect(cethL2?.addresses[138]).toBe('0xce7200000000000000000000000000000000008a');
const cweth = getCanonicalTokenBySymbol(1, 'cWETH');
expect(cweth).toMatchObject({
symbol: 'cWETH',
type: 'w',
currencyCode: 'ETH',
});
expect(cweth?.addresses[1]).toBe('0xce7e000000000000000000000000000000000001');
const cwethL2 = getCanonicalTokenBySymbol(10, 'cWETHL2');
expect(cwethL2).toMatchObject({
symbol: 'cWETHL2',
type: 'w',
currencyCode: 'ETH',
});
expect(cwethL2?.addresses[10]).toBe('0xce7200000000000000000000000000000000000a');
expect(getCanonicalTokenByAddress(10, '0xce7200000000000000000000000000000000000a')?.symbol).toBe('cWETHL2');
expect(getTokenRegistryFamily(cwethL2!)).toBe('gas_native');
});
it('surfaces cAUSDT on Chain 138 from env and keeps cWAUSDT fallback mirrors on active public chains', () => {
const previousBase = process.env.CAUSDT_ADDRESS_138;
process.env.CAUSDT_ADDRESS_138 = '0x2222222222222222222222222222222222222222';
jest.resetModules();
const reloaded = require('./canonical-tokens') as typeof import('./canonical-tokens');
const causdt = reloaded.getCanonicalTokenBySymbol(138, 'cAUSDT');
const polygonCwAusdt = reloaded.getCanonicalTokenBySymbol(137, 'cWAUSDT');
const avalancheCwAusdt = reloaded.getCanonicalTokenBySymbol(43114, 'cWAUSDT');
const celoCwAusdt = reloaded.getCanonicalTokenBySymbol(42220, 'cWAUSDT');
expect(causdt).toMatchObject({
symbol: 'cAUSDT',
type: 'base',
currencyCode: 'USD',
decimals: 6,
});
expect(causdt?.addresses[138]).toBe('0x2222222222222222222222222222222222222222');
expect(reloaded.getCanonicalTokenByAddress(138, '0x2222222222222222222222222222222222222222')?.symbol).toBe('cAUSDT');
expect(polygonCwAusdt).toMatchObject({
symbol: 'cWAUSDT',
type: 'w',
currencyCode: 'USD',
});
expect(polygonCwAusdt?.addresses[137]).toBe('0xf12e262F85107df26741726b074606CaFa24AAe7');
expect(avalancheCwAusdt).toMatchObject({
symbol: 'cWAUSDT',
type: 'w',
currencyCode: 'USD',
});
expect(avalancheCwAusdt?.addresses[43114]).toBe('0xff3084410A732231472Ee9f93F5855dA89CC5254');
expect(celoCwAusdt).toMatchObject({
symbol: 'cWAUSDT',
type: 'w',
currencyCode: 'USD',
});
expect(celoCwAusdt?.addresses[42220]).toBe('0xC158b6cD3A3088C52F797D41f5Aa02825361629e');
if (previousBase === undefined) {
delete process.env.CAUSDT_ADDRESS_138;
} else {
process.env.CAUSDT_ADDRESS_138 = previousBase;
}
jest.resetModules();
});
it('picks up Polygon cWUSDW from env as soon as the wrapped mirror is deployed', () => {
const previous = process.env.CWUSDW_ADDRESS_137;
process.env.CWUSDW_ADDRESS_137 = '0x1111111111111111111111111111111111111111';
jest.resetModules();
const reloaded = require('./canonical-tokens') as typeof import('./canonical-tokens');
const polygonCwUsdw = reloaded.getCanonicalTokenBySymbol(137, 'cWUSDW');
expect(polygonCwUsdw).toMatchObject({
symbol: 'cWUSDW',
type: 'w',
currencyCode: 'USD',
});
expect(polygonCwUsdw?.addresses[137]).toBe('0x1111111111111111111111111111111111111111');
expect(reloaded.getCanonicalTokenByAddress(137, '0x1111111111111111111111111111111111111111')?.symbol).toBe('cWUSDW');
if (previous === undefined) {
delete process.env.CWUSDW_ADDRESS_137;
} else {
process.env.CWUSDW_ADDRESS_137 = previous;
}
jest.resetModules();
});
it('models the Alltra gold exception as env-gated cAXAUC/cAXAUT and cWAXAUC/cWAXAUT on chain 651940', () => {
const previousCaxauc = process.env.CAXAUC_ADDRESS_651940;
const previousCwaxauc = process.env.CWAXAUC_ADDRESS_651940;
process.env.CAXAUC_ADDRESS_651940 = '0x3333333333333333333333333333333333333333';
process.env.CWAXAUC_ADDRESS_651940 = '0x4444444444444444444444444444444444444444';
jest.resetModules();
const reloaded = require('./canonical-tokens') as typeof import('./canonical-tokens');
const caxauc = reloaded.getCanonicalTokenBySymbol(651940, 'cAXAUC');
const cwaxauc = reloaded.getCanonicalTokenBySymbol(651940, 'cWAXAUC');
const cxaucOnAlltra = reloaded.getCanonicalTokenBySymbol(651940, 'cXAUC');
expect(caxauc).toMatchObject({
symbol: 'cAXAUC',
type: 'base',
currencyCode: 'XAU',
});
expect(caxauc?.addresses[651940]).toBe('0x3333333333333333333333333333333333333333');
expect(reloaded.getCanonicalTokenByAddress(651940, '0x3333333333333333333333333333333333333333')?.symbol).toBe('cAXAUC');
expect(cwaxauc).toMatchObject({
symbol: 'cWAXAUC',
type: 'w',
currencyCode: 'XAU',
});
expect(cwaxauc?.addresses[651940]).toBe('0x4444444444444444444444444444444444444444');
expect(reloaded.getCanonicalTokenByAddress(651940, '0x4444444444444444444444444444444444444444')?.symbol).toBe('cWAXAUC');
expect(cxaucOnAlltra).toBeUndefined();
if (previousCaxauc === undefined) {
delete process.env.CAXAUC_ADDRESS_651940;
} else {
process.env.CAXAUC_ADDRESS_651940 = previousCaxauc;
}
if (previousCwaxauc === undefined) {
delete process.env.CWAXAUC_ADDRESS_651940;
} else {
process.env.CWAXAUC_ADDRESS_651940 = previousCwaxauc;
}
jest.resetModules();
});
it('surfaces Chain 138 V2 x402 deployments explicitly and resolves quote fallback to V1 liquidity', () => {
const cusdtV1 = getCanonicalTokenBySymbol(138, 'cUSDT');
const cusdtV2 = getCanonicalTokenBySymbol(138, 'cUSDT_V2');
const v2Addr = cusdtV2?.addresses[138];
expect(cusdtV2).toMatchObject({
symbol: 'cUSDT_V2',
familySymbol: 'cUSDT',
deploymentVersion: 'v2',
preferredForX402: true,
liquiditySourceSymbol: 'cUSDT',
});
expect(v2Addr && String(v2Addr).trim()).toBeTruthy();
expect(getCanonicalTokenByAddress(138, String(v2Addr))?.symbol).toBe('cUSDT_V2');
const quoteResolution = resolveCanonicalQuoteAddress(138, String(v2Addr));
expect(quoteResolution).toMatchObject({
requestedSymbol: 'cUSDT_V2',
lookupSymbol: 'cUSDT',
usedFallback: true,
});
expect(quoteResolution.lookupAddress).toBe(String(cusdtV1?.addresses[138] || '').toLowerCase());
});
});

View File

@@ -4,14 +4,20 @@
* Addresses can be overridden via env (e.g. CUSDC_ADDRESS_138) or filled by indexer.
*/
import { isISO4217Supported } from './iso4217-symbol-registry';
import { isMonetaryUnitSupported } from './monetary-unit-symbol-registry';
import { loadTokenMappingLoader } from './repo-config-loader';
export type TokenType = 'base' | 'w' | 'asset' | 'debt';
export type TokenRegistryFamily = 'iso4217' | 'commodity' | 'monetary_unit' | 'gas_native' | 'unclassified';
export interface CanonicalTokenSpec {
symbol: string;
name: string;
type: TokenType;
decimals: number;
currencyCode?: string; // ISO-4217 for base/w
currencyCode?: string;
registryFamily?: TokenRegistryFamily;
/** ChainId -> contract address (placeholder or from env) */
addresses: Partial<Record<number, string>>;
description?: string;
@@ -21,14 +27,60 @@ export interface CanonicalTokenSpec {
v1Symbol?: string;
/** v0 symbol alias; on ChainID 138 tokens use v0 only (cUSDC, cUSDT), no chain designator */
v0Alias?: string;
/** Shared family symbol when multiple on-chain deployments exist for the same monetary unit. */
familySymbol?: string;
/** Deployment version when a family has multiple contract surfaces. */
deploymentVersion?: string;
/** Deployment lifecycle status (for example active or staged). */
deploymentStatus?: string;
/** Whether this deployment is the preferred x402 / permit-capable surface. */
preferredForX402?: boolean;
/** Symbol whose current liquidity should be used for quote fallback until cutover. */
liquiditySourceSymbol?: string;
}
interface GruTransportDeployment {
version?: string;
address?: string;
status?: string;
}
interface GruTransportCanonicalToken {
symbol?: string;
activeVersion?: string;
activeAddress?: string;
x402PreferredVersion?: string;
x402PreferredAddress?: string;
deployments?: GruTransportDeployment[];
}
const CHAIN_138 = 138;
const CHAIN_25 = 25; // Cronos
const CHAIN_651940 = 651940;
const LEGACY_CHAIN_ENV_SUFFIX: Partial<Record<number, string>> = {
1: 'MAINNET',
10: 'OPTIMISM',
25: 'CRONOS',
56: 'BSC',
100: 'GNOSIS',
137: 'POLYGON',
42161: 'ARBITRUM',
43114: 'AVALANCHE',
8453: 'BASE',
};
/** L2/mainnet chain IDs for cUSDT/cUSDC multichain (env: CUSDT_ADDRESS_56, CUSDC_ADDRESS_137, etc.) */
const L2_CHAIN_IDS = [1, 56, 137, 10, 42161, 8453, 43114, 25, 100, 42220, 1111] as const;
const GRU_CW_CHAIN_IDS = [1, 56, 137, 10, 42161, 8453, 43114, 100, 42220] as const;
const BTC_CW_CHAIN_IDS = [1, 10, 25, 56, 100, 137, 42161, 42220, 43114, 8453, 1111] as const;
const ETH_MAINNET_CW_CHAIN_IDS = [1] as const;
const ETH_L2_CW_CHAIN_IDS = [10, 42161, 8453] as const;
const BNB_CW_CHAIN_IDS = [56] as const;
const POL_CW_CHAIN_IDS = [137] as const;
const AVAX_CW_CHAIN_IDS = [43114] as const;
const CRO_CW_CHAIN_IDS = [25] as const;
const XDAI_CW_CHAIN_IDS = [100] as const;
const CELO_CW_CHAIN_IDS = [42220] as const;
const WEMIX_CW_CHAIN_IDS = [1111] as const;
/** Verified addresses from CHAIN138_TOKEN_ADDRESSES, .env, and deployment summaries */
const FALLBACK_ADDRESSES: Record<string, Partial<Record<number, string>>> = {
USDC: {
@@ -42,7 +94,7 @@ const FALLBACK_ADDRESSES: Record<string, Partial<Record<number, string>>> = {
[CHAIN_651940]: '0xa95EeD79f84E6A0151eaEb9d441F9Ffd50e8e881', // AUSDC on ALL Mainnet
[1]: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', // Ethereum USDC
[56]: '0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d', // BSC USDC
[137]: '0x3c499c542cEF5E3811e1192ce70d8cC03d5c1369', // Polygon USDC
[137]: '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359', // Polygon USDC
[100]: '0xDDAfbb505ad214D7b80b1f830fcCc89B60fb7A83', // Gnosis USDC
[10]: '0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85', // Optimism USDC
[42161]: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831', // Arbitrum USDC
@@ -67,6 +119,119 @@ const FALLBACK_ADDRESSES: Record<string, Partial<Record<number, string>>> = {
[42220]: '0x48065fbBE25f71C9282ddf5e1cD6D6A887483D5e', // Celo USDT
[1111]: '0xA649325Aa7C5093d12D6F98EB4378deAe68CE23F', // Wemix USDT
},
cUSDC_V2: {
[CHAIN_138]: '0x219522c60e83dEe01FC5b0329d6fA8fD84b9D13d',
},
cUSDT_V2: {
[CHAIN_138]: '0x9FBfab33882Efe0038DAa608185718b772EE5660',
},
cUSDW: {
[CHAIN_138]: '0xcA6BFa614935f1AB71c9aB106bAA6FBB6057095e',
},
cBTC: {
[CHAIN_138]: '0xcb7c000000000000000000000000000000000138',
},
cETH: {
[CHAIN_138]: '0xce7e00000000000000000000000000000000008a',
},
cETHL2: {
[CHAIN_138]: '0xce7200000000000000000000000000000000008a',
},
cBNB: {
[CHAIN_138]: '0xcb6b00000000000000000000000000000000008a',
},
cPOL: {
[CHAIN_138]: '0xc90100000000000000000000000000000000008a',
},
cAVAX: {
[CHAIN_138]: '0xcaaa00000000000000000000000000000000008a',
},
cCRO: {
[CHAIN_138]: '0xcc2000000000000000000000000000000000008a',
},
cXDAI: {
[CHAIN_138]: '0xcda100000000000000000000000000000000008a',
},
cCELO: {
[CHAIN_138]: '0xcce100000000000000000000000000000000008a',
},
cWEMIX: {
[CHAIN_138]: '0xc11100000000000000000000000000000000008a',
},
cWAUSDT: {
[56]: '0xe1a51Bc037a79AB36767561B147eb41780124934',
[137]: '0xf12e262F85107df26741726b074606CaFa24AAe7',
[43114]: '0xff3084410A732231472Ee9f93F5855dA89CC5254',
[42220]: '0xC158b6cD3A3088C52F797D41f5Aa02825361629e',
},
cWUSDC: {
[1]: '0x2de5F116bFcE3d0f922d9C8351e0c5Fc24b9284a',
[56]: '0x5355148C4740fcc3D7a96F05EdD89AB14851206b',
[137]: '0xd6969bC19b53f866C64f2148aE271B2Dae0C58E4',
[100]: '0xd6969bC19b53f866C64f2148aE271B2Dae0C58E4',
[10]: '0x377a5FaA3162b3Fc6f4e267301A3c817bAd18105',
[42161]: '0x0cb0192C056aa425C557BdeAD8E56C7eEabf7acF',
[8453]: '0x377a5FaA3162b3Fc6f4e267301A3c817bAd18105',
[43114]: '0x0C242b513008Cd49C89078F5aFb237A3112251EB',
[42220]: '0x4C38F9A5ed68A04cd28a72E8c68C459Ec34576f3',
},
cWUSDT: {
[1]: '0xaF5017d0163ecb99D9B5D94e3b4D7b09Af44D8AE',
[56]: '0x9a1D0dBEE997929ED02fD19E0E199704d20914dB',
[137]: '0x0cb0192C056aa425C557BdeAD8E56C7eEabf7acF',
[100]: '0x0cb0192C056aa425C557BdeAD8E56C7eEabf7acF',
[10]: '0x04B2AE3c3bb3d70Df506FAd8717b0FBFC78ED7E6',
[42161]: '0x73ADaF7dBa95221c080db5631466d2bC54f6a76B',
[8453]: '0x04B2AE3c3bb3d70Df506FAd8717b0FBFC78ED7E6',
[43114]: '0x8142BA530B08f3950128601F00DaaA678213DFdf',
[42220]: '0x73376eB92c16977B126dB9112936A20Fa0De3442',
},
cWUSDW: {
[56]: '0xC2FA05F12a75Ac84ea778AF9D6935cA807275E55',
[43114]: '0xcfdCe5E660FC2C8052BDfa7aEa1865DD753411Ae',
},
cWBTC: {
[1]: '0xcb7c000000000000000000000000000000000001',
[10]: '0xcb7c00000000000000000000000000000000000a',
[25]: '0xcb7c000000000000000000000000000000000019',
[56]: '0xcb7c000000000000000000000000000000000038',
[100]: '0xcb7c000000000000000000000000000000000064',
[137]: '0xcb7c000000000000000000000000000000000089',
[1111]: '0xcb7c000000000000000000000000000000000457',
[8453]: '0xcb7c000000000000000000000000000000002105',
[42161]: '0xcb7c00000000000000000000000000000000a4b1',
[42220]: '0xcb7c00000000000000000000000000000000a4ec',
[43114]: '0xcb7c00000000000000000000000000000000a86a',
},
cWETH: {
[1]: '0xce7e000000000000000000000000000000000001',
},
cWETHL2: {
[10]: '0xce7200000000000000000000000000000000000a',
[42161]: '0xce7200000000000000000000000000000000a4b1',
[8453]: '0xce72000000000000000000000000000000002105',
},
cWBNB: {
[56]: '0xcb6b000000000000000000000000000000000038',
},
cWPOL: {
[137]: '0xc901000000000000000000000000000000000089',
},
cWAVAX: {
[43114]: '0xcaaa00000000000000000000000000000000a86a',
},
cWCRO: {
[25]: '0xcc20000000000000000000000000000000000019',
},
cWXDAI: {
[100]: '0xcda1000000000000000000000000000000000064',
},
cWCELO: {
[42220]: '0xcce100000000000000000000000000000000a4ec',
},
cWWEMIX: {
[1111]: '0xc111000000000000000000000000000000000457',
},
// Compliant Fiat on Chain 138 — from DeployCompliantFiatTokens (2026-02-27)
cEURC: { [CHAIN_138]: '0x8085961F9cF02b4d800A3c6d386D31da4B34266a' },
cEURT: { [CHAIN_138]: '0xdf4b71c61E5912712C1Bdd451416B9aC26949d72' },
@@ -88,6 +253,50 @@ const FALLBACK_ADDRESSES: Record<string, Partial<Record<number, string>>> = {
CADW: { [CHAIN_25]: '0x328Cd365Bb35524297E68ED28c6fF2C9557d1363' },
};
function normalizeAddress(address?: string | null): string {
return typeof address === 'string' ? address.trim().toLowerCase() : '';
}
function getGruTransportCanonicalToken(symbol: string): GruTransportCanonicalToken | null {
const loader = loadTokenMappingLoader<{
getEnabledCanonicalToken?: (identifier: string) => GruTransportCanonicalToken | null;
}>();
return loader?.getEnabledCanonicalToken?.(symbol) ?? null;
}
function getTransportLookup(symbol: string): { baseSymbol: string; version: 'v1' | 'v2' } | null {
if (symbol === 'cUSDT' || symbol === 'cUSDC') {
return { baseSymbol: symbol, version: 'v1' };
}
if (symbol === 'cUSDT_V2') {
return { baseSymbol: 'cUSDT', version: 'v2' };
}
if (symbol === 'cUSDC_V2') {
return { baseSymbol: 'cUSDC', version: 'v2' };
}
return null;
}
function getTransportDeploymentAddress(symbol: string, version: 'v1' | 'v2'): string | undefined {
const token = getGruTransportCanonicalToken(symbol);
if (!token) return undefined;
const deploymentMatch = Array.isArray(token.deployments)
? token.deployments.find((deployment) => String(deployment.version || '').trim().toLowerCase() === version)
: null;
if (deploymentMatch?.address) return deploymentMatch.address;
if (version === 'v1') {
if (String(token.activeVersion || '').trim().toLowerCase() === 'v1' && token.activeAddress) {
return token.activeAddress;
}
} else if (String(token.x402PreferredVersion || '').trim().toLowerCase() === 'v2' && token.x402PreferredAddress) {
return token.x402PreferredAddress;
}
return undefined;
}
function addr(symbol: string, chainId: number): string | undefined {
if (chainId === CHAIN_138 && symbol === 'USDT') {
return process.env.USDT_ADDRESS_138 || process.env.OFFICIAL_USDT_ADDRESS || FALLBACK_ADDRESSES[symbol]?.[chainId];
@@ -95,9 +304,27 @@ function addr(symbol: string, chainId: number): string | undefined {
if (chainId === CHAIN_138 && symbol === 'USDC') {
return process.env.USDC_ADDRESS_138 || process.env.OFFICIAL_USDC_ADDRESS || FALLBACK_ADDRESSES[symbol]?.[chainId];
}
if (chainId === CHAIN_138) {
const transportLookup = getTransportLookup(symbol);
if (transportLookup) {
const envKey = `${symbol.replace(/-/g, '_').toUpperCase()}_ADDRESS_${chainId}`;
const envVal = process.env[envKey];
if (envVal && envVal.trim() !== '') return envVal;
return (
getTransportDeploymentAddress(transportLookup.baseSymbol, transportLookup.version) ||
FALLBACK_ADDRESSES[symbol]?.[chainId]
);
}
}
const key = `${symbol.replace(/-/g, '_').toUpperCase()}_ADDRESS_${chainId}`;
const envVal = process.env[key];
if (envVal && envVal.trim() !== '') return envVal;
const legacySuffix = LEGACY_CHAIN_ENV_SUFFIX[chainId];
if (legacySuffix) {
const legacyKey = `${symbol.replace(/-/g, '_').toUpperCase()}_${legacySuffix}`;
const legacyVal = process.env[legacyKey];
if (legacyVal && legacyVal.trim() !== '') return legacyVal;
}
return FALLBACK_ADDRESSES[symbol]?.[chainId];
}
@@ -108,8 +335,217 @@ export const CANONICAL_TOKENS: CanonicalTokenSpec[] = [
{ symbol: 'USDT', name: 'Tether USD (Official Mirror)', type: 'base', decimals: 6, currencyCode: 'USD', addresses: { [CHAIN_138]: addr('USDT', CHAIN_138) || '' } },
// Chain 138 v0 only (no X): cUSDC on 138; cXUSDC used only for bridged/origin reference elsewhere. See ISO4217_COMPLIANT_TOKEN_MATRIX.md
{ symbol: 'cUSDC', name: 'USD Coin (Compliant)', type: 'base', decimals: 6, currencyCode: 'USD', v0Alias: 'cUSDC', addresses: { [CHAIN_138]: addr('cUSDC', CHAIN_138) || '', [CHAIN_651940]: addr('cUSDC', CHAIN_651940) || '', ...Object.fromEntries(L2_CHAIN_IDS.map((id) => [id, addr('cUSDC', id)])) } },
{ symbol: 'cUSDC_V2', name: 'USD Coin (Compliant V2)', type: 'base', decimals: 6, currencyCode: 'USD', familySymbol: 'cUSDC', deploymentVersion: 'v2', deploymentStatus: 'staged', preferredForX402: true, liquiditySourceSymbol: 'cUSDC', description: 'Chain 138 x402 / permit-capable V2 deployment. Liquidity and PMM routing remain on cUSDC until cutover.', addresses: { [CHAIN_138]: addr('cUSDC_V2', CHAIN_138) || '' } },
// Chain 138 v0 only (no X): cUSDT on 138; cXUSDT used only for bridged/origin reference elsewhere. See ISO4217_COMPLIANT_TOKEN_MATRIX.md
{ symbol: 'cUSDT', name: 'Tether USD (Compliant)', type: 'base', decimals: 6, currencyCode: 'USD', v0Alias: 'cUSDT', addresses: { [CHAIN_138]: addr('cUSDT', CHAIN_138) || '', [CHAIN_651940]: addr('cUSDT', CHAIN_651940) || '', ...Object.fromEntries(L2_CHAIN_IDS.map((id) => [id, addr('cUSDT', id)])) } },
{ symbol: 'cUSDT_V2', name: 'Tether USD (Compliant V2)', type: 'base', decimals: 6, currencyCode: 'USD', familySymbol: 'cUSDT', deploymentVersion: 'v2', deploymentStatus: 'staged', preferredForX402: true, liquiditySourceSymbol: 'cUSDT', description: 'Chain 138 x402 / permit-capable V2 deployment. Liquidity and PMM routing remain on cUSDT until cutover.', addresses: { [CHAIN_138]: addr('cUSDT_V2', CHAIN_138) || '' } },
{ symbol: 'cAUSDT', name: 'Alltra USD Token (Compliant)', type: 'base', decimals: 6, currencyCode: 'USD', description: 'Live Chain 138 compliant landing asset for the ALL Mainnet AUSDT corridor.', addresses: { [CHAIN_138]: addr('cAUSDT', CHAIN_138) || '' } },
{ symbol: 'cUSDW', name: 'USD W (Compliant)', type: 'base', decimals: 6, currencyCode: 'USD', description: 'Chain 138 repo-native cUSDW hub asset for D-WIN-aligned PMM and cWUSDW transport planning.', addresses: { [CHAIN_138]: addr('cUSDW', CHAIN_138) || '' } },
{
symbol: 'cBTC',
name: 'Bitcoin (Compliant)',
type: 'base',
decimals: 8,
currencyCode: 'BTC',
registryFamily: 'monetary_unit',
description: 'Canonical Chain 138 compliant Bitcoin monetary unit with satoshi-precision accounting and custody-backed mint controls.',
addresses: { [CHAIN_138]: addr('cBTC', CHAIN_138) || '' },
},
{
symbol: 'cETH',
name: 'Ether Mainnet (Compliant)',
type: 'base',
decimals: 18,
currencyCode: 'ETH',
registryFamily: 'gas_native',
description: 'Canonical Chain 138 representation of Ethereum mainnet gas inventory. This family remains isolated from the shared ETH L2 family.',
addresses: { [CHAIN_138]: addr('cETH', CHAIN_138) || '' },
},
{
symbol: 'cETHL2',
name: 'Ether L2 Basket (Compliant)',
type: 'base',
decimals: 18,
currencyCode: 'ETH',
registryFamily: 'gas_native',
description: 'Canonical Chain 138 representation of the shared ETH L2 family across Optimism, Arbitrum, and Base.',
addresses: { [CHAIN_138]: addr('cETHL2', CHAIN_138) || '' },
},
{
symbol: 'cBNB',
name: 'BNB (Compliant)',
type: 'base',
decimals: 18,
currencyCode: 'BNB',
registryFamily: 'gas_native',
description: 'Canonical Chain 138 representation of BNB gas inventory.',
addresses: { [CHAIN_138]: addr('cBNB', CHAIN_138) || '' },
},
{
symbol: 'cPOL',
name: 'POL (Compliant)',
type: 'base',
decimals: 18,
currencyCode: 'POL',
registryFamily: 'gas_native',
description: 'Canonical Chain 138 representation of Polygon gas inventory.',
addresses: { [CHAIN_138]: addr('cPOL', CHAIN_138) || '' },
},
{
symbol: 'cAVAX',
name: 'Avalanche (Compliant)',
type: 'base',
decimals: 18,
currencyCode: 'AVAX',
registryFamily: 'gas_native',
description: 'Canonical Chain 138 representation of Avalanche gas inventory.',
addresses: { [CHAIN_138]: addr('cAVAX', CHAIN_138) || '' },
},
{
symbol: 'cCRO',
name: 'Cronos (Compliant)',
type: 'base',
decimals: 18,
currencyCode: 'CRO',
registryFamily: 'gas_native',
description: 'Canonical Chain 138 representation of Cronos gas inventory.',
addresses: { [CHAIN_138]: addr('cCRO', CHAIN_138) || '' },
},
{
symbol: 'cXDAI',
name: 'xDAI (Compliant)',
type: 'base',
decimals: 18,
currencyCode: 'XDAI',
registryFamily: 'gas_native',
description: 'Canonical Chain 138 representation of Gnosis Chain xDAI gas inventory.',
addresses: { [CHAIN_138]: addr('cXDAI', CHAIN_138) || '' },
},
{
symbol: 'cCELO',
name: 'Celo (Compliant)',
type: 'base',
decimals: 18,
currencyCode: 'CELO',
registryFamily: 'gas_native',
description: 'Canonical Chain 138 representation of Celo gas inventory.',
addresses: { [CHAIN_138]: addr('cCELO', CHAIN_138) || '' },
},
{
symbol: 'cWEMIX',
name: 'Wemix Hub (Compliant)',
type: 'base',
decimals: 18,
currencyCode: 'WEMIX',
registryFamily: 'gas_native',
description: 'Canonical Chain 138 representation of Wemix gas inventory. The public mirror keeps the distinct cWWEMIX symbol to avoid naming collisions.',
addresses: { [CHAIN_138]: addr('cWEMIX', CHAIN_138) || '' },
},
// Public-network transport mirrors for canonical Chain 138 c* assets.
{ symbol: 'cWAUSDT', name: 'Alltra USD Token (Compliant Wrapped ISO-4217 M1)', type: 'w', decimals: 6, currencyCode: 'USD', description: 'Public-network mirrored transport form for the live Chain 138 cAUSDT surface.', addresses: { ...Object.fromEntries(GRU_CW_CHAIN_IDS.map((id) => [id, addr('cWAUSDT', id)])) } },
{ symbol: 'cWUSDC', name: 'USD Coin (Compliant Wrapped ISO-4217 M1)', type: 'w', decimals: 6, currencyCode: 'USD', description: 'Public-network mirrored transport form of canonical Chain 138 cUSDC.', addresses: { ...Object.fromEntries(GRU_CW_CHAIN_IDS.map((id) => [id, addr('cWUSDC', id)])) } },
{ symbol: 'cWUSDT', name: 'Tether USD (Compliant Wrapped ISO-4217 M1)', type: 'w', decimals: 6, currencyCode: 'USD', description: 'Public-network mirrored transport form of canonical Chain 138 cUSDT.', addresses: { ...Object.fromEntries(GRU_CW_CHAIN_IDS.map((id) => [id, addr('cWUSDT', id)])) } },
{ symbol: 'cWUSDW', name: 'USD W (Compliant Wrapped ISO-4217 M1)', type: 'w', decimals: 6, currencyCode: 'USD', description: 'Public-network mirrored transport form of canonical Chain 138 cUSDW.', addresses: { ...Object.fromEntries(GRU_CW_CHAIN_IDS.map((id) => [id, addr('cWUSDW', id)])) } },
{
symbol: 'cWBTC',
name: 'Bitcoin (Compliant Wrapped Monetary Unit)',
type: 'w',
decimals: 8,
currencyCode: 'BTC',
registryFamily: 'monetary_unit',
description: 'Public-network mirrored transport form of canonical Chain 138 cBTC. Distinct from Ethereum WBTC and other third-party wrapped BTC products.',
addresses: { ...Object.fromEntries(BTC_CW_CHAIN_IDS.map((id) => [id, addr('cWBTC', id)])) },
},
{
symbol: 'cWETH',
name: 'Ether Mainnet (Compliant Wrapped)',
type: 'w',
decimals: 18,
currencyCode: 'ETH',
registryFamily: 'gas_native',
description: 'Public-network mirrored transport form of canonical Chain 138 cETH for Ethereum mainnet only.',
addresses: { ...Object.fromEntries(ETH_MAINNET_CW_CHAIN_IDS.map((id) => [id, addr('cWETH', id)])) },
},
{
symbol: 'cWETHL2',
name: 'Ether L2 Basket (Compliant Wrapped)',
type: 'w',
decimals: 18,
currencyCode: 'ETH',
registryFamily: 'gas_native',
description: 'Public-network mirrored transport form of canonical Chain 138 cETHL2 across the approved ETH L2 family.',
addresses: { ...Object.fromEntries(ETH_L2_CW_CHAIN_IDS.map((id) => [id, addr('cWETHL2', id)])) },
},
{
symbol: 'cWBNB',
name: 'BNB (Compliant Wrapped)',
type: 'w',
decimals: 18,
currencyCode: 'BNB',
registryFamily: 'gas_native',
description: 'Public-network mirrored transport form of canonical Chain 138 cBNB.',
addresses: { ...Object.fromEntries(BNB_CW_CHAIN_IDS.map((id) => [id, addr('cWBNB', id)])) },
},
{
symbol: 'cWPOL',
name: 'POL (Compliant Wrapped)',
type: 'w',
decimals: 18,
currencyCode: 'POL',
registryFamily: 'gas_native',
description: 'Public-network mirrored transport form of canonical Chain 138 cPOL.',
addresses: { ...Object.fromEntries(POL_CW_CHAIN_IDS.map((id) => [id, addr('cWPOL', id)])) },
},
{
symbol: 'cWAVAX',
name: 'Avalanche (Compliant Wrapped)',
type: 'w',
decimals: 18,
currencyCode: 'AVAX',
registryFamily: 'gas_native',
description: 'Public-network mirrored transport form of canonical Chain 138 cAVAX.',
addresses: { ...Object.fromEntries(AVAX_CW_CHAIN_IDS.map((id) => [id, addr('cWAVAX', id)])) },
},
{
symbol: 'cWCRO',
name: 'Cronos (Compliant Wrapped)',
type: 'w',
decimals: 18,
currencyCode: 'CRO',
registryFamily: 'gas_native',
description: 'Public-network mirrored transport form of canonical Chain 138 cCRO.',
addresses: { ...Object.fromEntries(CRO_CW_CHAIN_IDS.map((id) => [id, addr('cWCRO', id)])) },
},
{
symbol: 'cWXDAI',
name: 'xDAI (Compliant Wrapped)',
type: 'w',
decimals: 18,
currencyCode: 'XDAI',
registryFamily: 'gas_native',
description: 'Public-network mirrored transport form of canonical Chain 138 cXDAI.',
addresses: { ...Object.fromEntries(XDAI_CW_CHAIN_IDS.map((id) => [id, addr('cWXDAI', id)])) },
},
{
symbol: 'cWCELO',
name: 'Celo (Compliant Wrapped)',
type: 'w',
decimals: 18,
currencyCode: 'CELO',
registryFamily: 'gas_native',
description: 'Public-network mirrored transport form of canonical Chain 138 cCELO.',
addresses: { ...Object.fromEntries(CELO_CW_CHAIN_IDS.map((id) => [id, addr('cWCELO', id)])) },
},
{
symbol: 'cWWEMIX',
name: 'Wemix (Compliant Wrapped)',
type: 'w',
decimals: 18,
currencyCode: 'WEMIX',
registryFamily: 'gas_native',
description: 'Public-network mirrored transport form of canonical Chain 138 cWEMIX. The doubled W preserves the cW* naming discipline and keeps the canonical hub symbol distinct.',
addresses: { ...Object.fromEntries(WEMIX_CW_CHAIN_IDS.map((id) => [id, addr('cWWEMIX', id)])) },
},
{ symbol: 'cEURC', name: 'Euro Coin (Compliant)', type: 'base', decimals: 6, currencyCode: 'EUR', addresses: { [CHAIN_138]: addr('cEURC', CHAIN_138), [CHAIN_651940]: addr('cEURC', CHAIN_651940) } },
{ symbol: 'cEURT', name: 'Tether EUR (Compliant)', type: 'base', decimals: 6, currencyCode: 'EUR', addresses: { [CHAIN_138]: addr('cEURT', CHAIN_138), [CHAIN_651940]: addr('cEURT', CHAIN_651940) } },
{ symbol: 'cGBPC', name: 'Pound Sterling (Compliant)', type: 'base', decimals: 6, currencyCode: 'GBP', addresses: { [CHAIN_138]: addr('cGBPC', CHAIN_138), [CHAIN_651940]: addr('cGBPC', CHAIN_651940) } },
@@ -125,7 +561,7 @@ export const CANONICAL_TOKENS: CanonicalTokenSpec[] = [
decimals: 6,
currencyCode: 'XAU',
description: '1 full token = 1 troy ounce fine gold (10^6 base units = 1 oz).',
addresses: { [CHAIN_138]: addr('cXAUC', CHAIN_138), [CHAIN_651940]: addr('cXAUC', CHAIN_651940) },
addresses: { [CHAIN_138]: addr('cXAUC', CHAIN_138) },
},
{
symbol: 'cXAUT',
@@ -134,7 +570,43 @@ export const CANONICAL_TOKENS: CanonicalTokenSpec[] = [
decimals: 6,
currencyCode: 'XAU',
description: '1 full token = 1 troy ounce fine gold (10^6 base units = 1 oz).',
addresses: { [CHAIN_138]: addr('cXAUT', CHAIN_138), [CHAIN_651940]: addr('cXAUT', CHAIN_651940) },
addresses: { [CHAIN_138]: addr('cXAUT', CHAIN_138) },
},
{
symbol: 'cAXAUC',
name: 'Alltra Gold Coin',
type: 'base',
decimals: 6,
currencyCode: 'XAU',
description: 'Planned ALL Mainnet native-unwrapped gold landing asset for the 138 -> 651940 corridor.',
addresses: { [CHAIN_651940]: addr('cAXAUC', CHAIN_651940) || '' },
},
{
symbol: 'cAXAUT',
name: 'Alltra Tether XAU',
type: 'base',
decimals: 6,
currencyCode: 'XAU',
description: 'Planned ALL Mainnet native-unwrapped XAU token for the 138 -> 651940 corridor.',
addresses: { [CHAIN_651940]: addr('cAXAUT', CHAIN_651940) || '' },
},
{
symbol: 'cWAXAUC',
name: 'Alltra Wrapped Gold Coin',
type: 'w',
decimals: 6,
currencyCode: 'XAU',
description: 'Planned ALL Mainnet bridge-minted wrapped gold representation for inbound Chain 138 cXAUC transport.',
addresses: { [CHAIN_651940]: addr('cWAXAUC', CHAIN_651940) || '' },
},
{
symbol: 'cWAXAUT',
name: 'Alltra Wrapped Tether XAU',
type: 'w',
decimals: 6,
currencyCode: 'XAU',
description: 'Planned ALL Mainnet bridge-minted wrapped XAU representation for inbound Chain 138 cXAUT transport.',
addresses: { [CHAIN_651940]: addr('cWAXAUT', CHAIN_651940) || '' },
},
{ symbol: 'LiXAU', name: 'XAU Liquidity-adjusted', type: 'base', decimals: 6, currencyCode: 'XAU', addresses: { [CHAIN_138]: addr('LiXAU', CHAIN_138), [CHAIN_651940]: addr('LiXAU', CHAIN_651940) } },
// --- ISO-4217 W ---
@@ -183,7 +655,7 @@ export function getCanonicalTokensByChain(chainId: number): CanonicalTokenSpec[]
}
export function getCanonicalTokenByAddress(chainId: number, address: string): CanonicalTokenSpec | undefined {
const lower = address.toLowerCase();
const lower = normalizeAddress(address);
return CANONICAL_TOKENS.find((t) => t.addresses[chainId]?.toLowerCase() === lower);
}
@@ -194,29 +666,78 @@ export function getCanonicalTokenBySymbol(chainId: number, symbol: string): Cano
);
}
export interface CanonicalQuoteAddressResolution {
requestedAddress: string;
requestedSymbol?: string;
lookupAddress: string;
lookupSymbol?: string;
usedFallback: boolean;
}
export function resolveCanonicalQuoteAddress(chainId: number, address: string): CanonicalQuoteAddressResolution {
const requestedAddress = normalizeAddress(address);
const requestedSpec = getCanonicalTokenByAddress(chainId, requestedAddress);
if (!requestedSpec) {
return {
requestedAddress,
lookupAddress: requestedAddress,
usedFallback: false,
};
}
const liquiditySourceSymbol = requestedSpec.liquiditySourceSymbol || requestedSpec.symbol;
const liquiditySpec = getCanonicalTokenBySymbol(chainId, liquiditySourceSymbol) || requestedSpec;
const lookupAddress = normalizeAddress(liquiditySpec.addresses[chainId] || requestedAddress);
return {
requestedAddress,
requestedSymbol: requestedSpec.symbol,
lookupAddress,
lookupSymbol: liquiditySpec.symbol,
usedFallback: lookupAddress !== requestedAddress,
};
}
/** IPFS-hosted logo URLs (Pinata) for Uniswap token list (logoURI).
* Every token must have logoURI for MetaMask to display icons. getLogoUriForSpec resolves
* ac-tokens from base (c*), vdc/sdc from base; unknown symbols fall back to ETH_LOGO. */
const IPFS_GATEWAY = 'https://ipfs.io/ipfs';
const GRU_LOGO_BASE =
'https://raw.githubusercontent.com/Order-of-Hospitallers/proxmox-cp/main/token-lists/logos/gru';
const ETH_LOGO = `${IPFS_GATEWAY}/Qma3FKtLce9MjgJgWbtyCxBiPjJ6xi8jGWUSKNS5Jc2ong`;
const USDC_LOGO = `${IPFS_GATEWAY}/QmNPq4D5JXzurmi9jAhogVMzhAQRk1PZ1r9H3qQUV9gjDm`;
const USDT_LOGO = `${IPFS_GATEWAY}/QmRfhPs9DcyFPpGjKwF6CCoVDWUHSxkQR34n9NK7JSbPCP`;
const USDC_LOGO = `${GRU_LOGO_BASE}/cUSDC.svg`;
const USDT_LOGO = `${GRU_LOGO_BASE}/cUSDT.svg`;
const BTC_LOGO = 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/bitcoin/info/logo.png';
const LOGO_BY_SYMBOL: Record<string, string> = {
USDC: USDC_LOGO,
USDT: USDT_LOGO,
cUSDC: USDC_LOGO,
cUSDT: USDT_LOGO,
cEURC: USDC_LOGO,
cEURT: USDT_LOGO,
cGBPC: `${IPFS_GATEWAY}/QmNQF73WjxU6FwTXNH8PXoDRFaSFKTYQWL7d4Q1kdRVJ4o`,
cGBPT: `${IPFS_GATEWAY}/QmV4frsJmDTWzLdxdj1z81uMqVXcbGpHZLzwkpj6GvEX4k`,
cAUDC: `${IPFS_GATEWAY}/QmPh16PY241zNtePyeK7ep1uf1RcARV2ynGAuRU8U7sSqS`,
cJPYC: `${IPFS_GATEWAY}/Qmb9JmuD9ehaQtTLBBZmAoiAbvE53e3FMjkEty8rvbPf9K`,
cCHFC: `${IPFS_GATEWAY}/QmPh16PY241zNtePyeK7ep1uf1RcARV2ynGAuRU8U7sSqS`,
cCADC: `${IPFS_GATEWAY}/QmPh16PY241zNtePyeK7ep1uf1RcARV2ynGAuRU8U7sSqS`,
cXAUC: ETH_LOGO,
cXAUT: ETH_LOGO,
cUSDC_V2: USDC_LOGO,
cUSDT_V2: USDT_LOGO,
cAUSDT: USDT_LOGO,
cUSDW: USDC_LOGO,
cBTC: BTC_LOGO,
cWAUSDT: USDT_LOGO,
cWBTC: BTC_LOGO,
cWUSDC: USDC_LOGO,
cWUSDT: USDT_LOGO,
cWUSDW: USDC_LOGO,
cEURC: `${GRU_LOGO_BASE}/cEURC.svg`,
cEURT: `${GRU_LOGO_BASE}/cEURT.svg`,
cGBPC: `${GRU_LOGO_BASE}/cGBPC.svg`,
cGBPT: `${GRU_LOGO_BASE}/cGBPT.svg`,
cAUDC: `${GRU_LOGO_BASE}/cAUDC.svg`,
cJPYC: `${GRU_LOGO_BASE}/cJPYC.svg`,
cCHFC: `${GRU_LOGO_BASE}/cCHFC.svg`,
cCADC: `${GRU_LOGO_BASE}/cCADC.svg`,
cXAUC: `${GRU_LOGO_BASE}/cXAUC.svg`,
cXAUT: `${GRU_LOGO_BASE}/cXAUT.svg`,
cAXAUC: `${GRU_LOGO_BASE}/cXAUC.svg`,
cAXAUT: `${GRU_LOGO_BASE}/cXAUT.svg`,
cWAXAUC: `${GRU_LOGO_BASE}/cXAUC.svg`,
cWAXAUT: `${GRU_LOGO_BASE}/cXAUT.svg`,
LiXAU: `${IPFS_GATEWAY}/QmUVY5trUM5N1UnS4abReb66fNzGw7kenjU9AjL7TgR3M1`,
USDW: USDC_LOGO,
EURW: `${IPFS_GATEWAY}/QmPh16PY241zNtePyeK7ep1uf1RcARV2ynGAuRU8U7sSqS`,
@@ -239,3 +760,13 @@ export function getLogoUriForSpec(spec: CanonicalTokenSpec): string {
}
return ETH_LOGO;
}
export function getTokenRegistryFamily(spec: Pick<CanonicalTokenSpec, 'currencyCode' | 'registryFamily'>): TokenRegistryFamily {
if (spec.registryFamily) return spec.registryFamily;
const code = String(spec.currencyCode || '').trim().toUpperCase();
if (!code) return 'unclassified';
if (code === 'XAU') return 'commodity';
if (isISO4217Supported(code)) return 'iso4217';
if (isMonetaryUnitSupported(code)) return 'monetary_unit';
return 'unclassified';
}

View File

@@ -0,0 +1,13 @@
const DEFAULT_CHAIN138_RPC_URL = 'https://rpc-http-pub.d-bis.org';
export function resolveChain138RpcUrl(): string {
return String(
process.env.CHAIN_138_RPC_URL ||
process.env.RPC_URL_138 ||
process.env.RPC_URL_138_PUBLIC ||
process.env.RPC_HTTP_PUB_URL ||
DEFAULT_CHAIN138_RPC_URL
).trim();
}
export { DEFAULT_CHAIN138_RPC_URL };

View File

@@ -1,3 +1,5 @@
import { resolveChain138RpcUrl } from './chain138-rpc';
export interface ChainConfig {
chainId: number;
name: string;
@@ -16,7 +18,7 @@ export const CHAIN_CONFIGS: Record<number, ChainConfig> = {
138: {
chainId: 138,
name: 'DeFi Oracle Meta Mainnet',
rpcUrl: process.env.CHAIN_138_RPC_URL || process.env.RPC_URL_138_PUBLIC || process.env.RPC_URL_138 || 'https://rpc-http-pub.d-bis.org',
rpcUrl: resolveChain138RpcUrl(),
explorerUrl: 'https://explorer.d-bis.org',
nativeCurrency: {
name: 'Ether',

View File

@@ -3,6 +3,8 @@
* Used by cross-chain-indexer for CCIP/Alltra/UniversalCCIP event aggregation.
*/
import { loadTokenMappingLoader } from './repo-config-loader';
export interface BridgeLane {
destSelector: string;
destChainId: number;
@@ -114,6 +116,45 @@ const CCIP_WETH9_138 = envAddr('CCIPWETH9_BRIDGE_CHAIN138') || '0x971cD9D156f193
const CCIP_STABLE_138 = envAnyAddr('CCIP_STABLE_BRIDGE_CHAIN138', 'CCIP_STABLECOIN_BRIDGE_CHAIN138');
const UNIVERSAL_CCIP_138 = envAnyAddr('UNIVERSAL_CCIP_BRIDGE_ADDRESS', 'UNIVERSAL_CCIP_BRIDGE');
interface RepoConfigLoader {
getRoutingRegistryRoutes?: () => RoutingRegistryEntry[];
getActiveTransportPair?: (
fromChainId: number,
toChainId: number,
criteria?: Record<string, unknown>
) => (RoutingRegistryEntry & {
canonicalSymbol?: string;
peer?: {
l1Bridge?: { address?: string; env?: string };
l2Bridge?: { address?: string; env?: string };
};
eligible?: boolean;
}) | null;
resolveConfigRef?: (ref?: { address?: string; env?: string }) => string;
}
function loadRepoConfigLoader(): RepoConfigLoader | null {
return loadTokenMappingLoader<RepoConfigLoader>();
}
function normalizeTransportAsset(asset: string): string {
const normalized = asset.trim().toLowerCase().replace(/[\s_-]/g, '');
if (normalized.startsWith('cw')) {
return `c${normalized.slice(2)}`;
}
return normalized;
}
function resolvePeerBridgeAddress(
loader: RepoConfigLoader | null,
pair: NonNullable<ReturnType<NonNullable<RepoConfigLoader['getActiveTransportPair']>>>,
sourceChainId: number
): string {
const ref = sourceChainId === chainId138 ? pair.peer?.l1Bridge : pair.peer?.l2Bridge;
const resolved = loader?.resolveConfigRef?.(ref);
return resolved || '';
}
/**
* Get routing registry entry for (fromChain, toChain, asset).
* Used by UI and indexer to choose ALT vs CCIP and to fill routing in activity_events.
@@ -125,6 +166,51 @@ export function getRouteFromRegistry(
asset: string = 'WETH',
): RoutingRegistryEntry | null {
if (fromChain === toChain) return null;
const loader = loadRepoConfigLoader();
const normalizedAsset = normalizeTransportAsset(asset);
const activeTransportPair = loader?.getActiveTransportPair?.(fromChain, toChain, { symbol: normalizedAsset });
if (activeTransportPair) {
if (activeTransportPair.eligible) {
const bridgeAddress = resolvePeerBridgeAddress(loader, activeTransportPair, fromChain);
if (bridgeAddress) {
return {
pathType: 'CCIP',
bridgeAddress,
bridgeChainId: fromChain === chainId138 ? chainId138 : fromChain,
label: 'GRUTransport',
fromChain,
toChain,
asset: activeTransportPair.canonicalSymbol || asset,
};
}
}
// Active GRU transport assets must not silently escape into legacy bridge paths.
return null;
}
const registryRoutes = loader?.getRoutingRegistryRoutes?.() || [];
const routeMatch =
registryRoutes.find(
(route) =>
route.fromChain === fromChain &&
route.toChain === toChain &&
typeof route.asset === 'string' &&
route.asset.trim().toLowerCase() === asset.trim().toLowerCase()
) ||
registryRoutes.find(
(route) =>
route.fromChain === fromChain &&
route.toChain === toChain &&
typeof route.asset === 'string' &&
route.asset.trim().toLowerCase() === normalizedAsset
);
if (routeMatch) {
return routeMatch;
}
const is138To651940 = fromChain === 138 && toChain === 651940;
const is651940To138 = fromChain === 651940 && toChain === 138;
if (is138To651940 || is651940To138) {
@@ -139,8 +225,8 @@ export function getRouteFromRegistry(
};
}
if (fromChain === 138 || toChain === 138) {
const normalizedAsset = asset.trim().toUpperCase();
const isStableAsset = STABLE_ASSET_SYMBOLS.has(normalizedAsset);
const legacyNormalizedAsset = asset.trim().toUpperCase();
const isStableAsset = STABLE_ASSET_SYMBOLS.has(legacyNormalizedAsset);
if (isStableAsset) {
if (CCIP_STABLE_138) {
@@ -170,7 +256,7 @@ export function getRouteFromRegistry(
return null;
}
if (normalizedAsset !== 'WETH' && UNIVERSAL_CCIP_138) {
if (legacyNormalizedAsset !== 'WETH' && UNIVERSAL_CCIP_138) {
return {
pathType: 'CCIP',
bridgeAddress: UNIVERSAL_CCIP_138,

View File

@@ -0,0 +1,201 @@
import fs from 'fs';
import path from 'path';
export interface DeploymentStatusFile {
version?: string;
updated?: string;
chains?: Record<
string,
{
name?: string;
cwTokens?: Record<string, string>;
gasMirrors?: Record<string, string>;
gasQuoteAddresses?: Record<string, string>;
gasPmmPools?: Array<Record<string, unknown>>;
gasReferenceVenues?: Array<Record<string, unknown>>;
[key: string]: unknown;
}
>;
[key: string]: unknown;
}
export interface LoadedDeploymentStatus {
data: DeploymentStatusFile;
lastModified?: string;
}
export interface CwRegistryChain {
chainId: number;
chainIdText: string;
name: string;
tokens: Array<{
symbol: string;
address: string;
assetClass?: string;
familyKey?: string;
}>;
}
export interface GasRegistryChain {
chainId: number;
chainIdText: string;
name: string;
families: Array<{
familyKey: string;
mirroredSymbol: string;
mirrorAddress?: string;
dodoPmm: Array<Record<string, unknown>>;
referenceVenues: Array<Record<string, unknown>>;
}>;
}
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 buildDeploymentStatusCandidates(): string[] {
return uniquePaths([
process.env.DEPLOYMENT_STATUS_JSON_PATH,
process.env.CW_REGISTRY_JSON_PATH,
process.env.CROSS_CHAIN_PMM_DEPLOYMENT_STATUS_PATH,
path.resolve(process.cwd(), 'cross-chain-pmm-lps/config/deployment-status.json'),
path.resolve(process.cwd(), '..', 'cross-chain-pmm-lps/config/deployment-status.json'),
path.resolve(process.cwd(), '..', '..', 'cross-chain-pmm-lps/config/deployment-status.json'),
path.resolve(__dirname, '../../../../../cross-chain-pmm-lps/config/deployment-status.json'),
]);
}
export function resolveDeploymentStatusPath(): string | null {
for (const candidate of buildDeploymentStatusCandidates()) {
if (fs.existsSync(candidate)) {
return candidate;
}
}
return null;
}
export function loadDeploymentStatusFile(): LoadedDeploymentStatus | null {
const filePath = resolveDeploymentStatusPath();
if (!filePath) return null;
try {
const raw = fs.readFileSync(filePath, 'utf8');
const stat = fs.statSync(filePath);
return {
data: JSON.parse(raw) as DeploymentStatusFile,
lastModified: stat.mtime.toISOString(),
};
} catch {
return null;
}
}
export function buildCwRegistryChains(data: DeploymentStatusFile): CwRegistryChain[] {
const chains = data.chains ?? {};
const rows: CwRegistryChain[] = [];
for (const [chainIdText, chain] of Object.entries(chains)) {
const gasFamilyByMirror = new Map<string, string>();
for (const pool of chain.gasPmmPools ?? []) {
const familyKey = typeof pool.familyKey === 'string' ? pool.familyKey : '';
const base = typeof pool.base === 'string' ? pool.base : '';
if (familyKey && base) {
gasFamilyByMirror.set(base, familyKey);
}
}
const tokens = [
...Object.entries(chain.cwTokens ?? {})
.filter(([, address]) => typeof address === 'string' && address.trim() !== '')
.map(([symbol, address]) => ({ symbol, address })),
...Object.entries(chain.gasMirrors ?? {})
.filter(([, address]) => typeof address === 'string' && address.trim() !== '')
.map(([symbol, address]) => ({
symbol,
address,
assetClass: 'gas_native',
familyKey: gasFamilyByMirror.get(symbol),
})),
];
if (tokens.length === 0) continue;
rows.push({
chainId: Number(chainIdText),
chainIdText,
name: chain.name || `Chain ${chainIdText}`,
tokens: tokens.sort((a, b) => a.symbol.localeCompare(b.symbol)),
});
}
return rows.sort((a, b) => a.chainId - b.chainId);
}
export function buildGasRegistryChains(data: DeploymentStatusFile): GasRegistryChain[] {
const rows: GasRegistryChain[] = [];
for (const [chainIdText, chain] of Object.entries(data.chains ?? {})) {
const familyMap = new Map<
string,
{
familyKey: string;
mirroredSymbol: string;
mirrorAddress?: string;
dodoPmm: Array<Record<string, unknown>>;
referenceVenues: Array<Record<string, unknown>>;
}
>();
for (const pool of chain.gasPmmPools ?? []) {
const familyKey = typeof pool.familyKey === 'string' ? pool.familyKey : '';
const mirroredSymbol = typeof pool.base === 'string' ? pool.base : '';
if (!familyKey || !mirroredSymbol) continue;
const existing = familyMap.get(familyKey) ?? {
familyKey,
mirroredSymbol,
mirrorAddress: chain.gasMirrors?.[mirroredSymbol],
dodoPmm: [],
referenceVenues: [],
};
existing.dodoPmm.push(pool);
familyMap.set(familyKey, existing);
}
for (const venue of chain.gasReferenceVenues ?? []) {
const familyKey = typeof venue.familyKey === 'string' ? venue.familyKey : '';
if (!familyKey) continue;
const existing = familyMap.get(familyKey) ?? {
familyKey,
mirroredSymbol: typeof venue.base === 'string' ? venue.base : '',
mirrorAddress: typeof venue.base === 'string' ? chain.gasMirrors?.[venue.base] : undefined,
dodoPmm: [],
referenceVenues: [],
};
existing.referenceVenues.push(venue);
familyMap.set(familyKey, existing);
}
const families = Array.from(familyMap.values()).sort((a, b) => a.familyKey.localeCompare(b.familyKey));
if (families.length === 0) continue;
rows.push({
chainId: Number(chainIdText),
chainIdText,
name: chain.name || `Chain ${chainIdText}`,
families,
});
}
return rows.sort((a, b) => a.chainId - b.chainId);
}

View File

@@ -34,13 +34,18 @@ export interface DexFactoryConfig {
custom?: CustomDexConfig[];
}
/** Canonical DODOPMMIntegration on Chain 138 — see docs/11-references/CONTRACT_ADDRESSES_REFERENCE.md */
const CANONICAL_CHAIN138_DODO_PMM_INTEGRATION =
'0x86ADA6Ef91A3B450F89f2b751e93B1b7A3218895';
export const DEX_FACTORIES: Record<number, DexFactoryConfig> = {
138: {
// DODO PMM Integration - index from DODOPMMIntegration or PoolManager
dodo: [
{
poolManager: process.env.CHAIN_138_DODO_POOL_MANAGER || '',
dodoPmmIntegration: process.env.CHAIN_138_DODO_PMM_INTEGRATION || '',
dodoPmmIntegration:
process.env.CHAIN_138_DODO_PMM_INTEGRATION || CANONICAL_CHAIN138_DODO_PMM_INTEGRATION,
dodoVendingMachine: process.env.CHAIN_138_DODO_VENDING_MACHINE || '',
startBlock: 0,
},

View File

@@ -0,0 +1,435 @@
import fs from 'fs';
import os from 'os';
import path from 'path';
import { resolveTokenMappingLoaderPath } from './repo-config-loader';
import { getRouteFromRegistry } from './cross-chain-bridges';
import {
filterPoolsForExposure,
filterPoolsForRouting,
getActiveTransportPairs,
getGasAssetFamilies,
getGasProtocolExposure,
getGasRedeemGroups,
getGruTransportMetadata,
isGasRedemptionPathAllowed,
isPublicPoolActive,
isPublicPoolRoutable,
} from './gru-transport';
describe('GRU Transport overlay', () => {
const originalChain138Bridge = process.env.CHAIN138_L1_BRIDGE;
const originalBscBridge = process.env.CW_BRIDGE_BSC;
const originalStableBridge = process.env.CCIP_STABLE_BRIDGE_CHAIN138;
const originalStablecoinBridge = process.env.CCIP_STABLECOIN_BRIDGE_CHAIN138;
const originalUniversalBridge = process.env.UNIVERSAL_CCIP_BRIDGE_ADDRESS;
const originalUniversalBridgeAlt = process.env.UNIVERSAL_CCIP_BRIDGE;
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 originalGasStrictVerifier = process.env.CW_GAS_STRICT_ESCROW_VERIFIER_CHAIN138;
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 originalMainnetBridge = process.env.CW_BRIDGE_MAINNET;
const originalOptimismBridge = process.env.CW_BRIDGE_OPTIMISM;
const originalEthMainnetOutstanding = process.env.CW_MAX_OUTSTANDING_ETH_MAINNET_MAINNET;
const originalEthMainnetSupply = process.env.CW_GAS_OUTSTANDING_ETH_MAINNET_MAINNET;
const originalEthMainnetEscrowed = process.env.CW_GAS_ESCROWED_ETH_MAINNET_MAINNET;
const originalEthMainnetTreasury = process.env.CW_GAS_TREASURY_BACKED_ETH_MAINNET_MAINNET;
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 originalTokenMappingLoaderPath = process.env.TOKEN_MAPPING_LOADER_PATH;
const originalCwL1Bridge = process.env.CW_L1_BRIDGE;
const originalCwL1BridgeChain138 = process.env.CW_L1_BRIDGE_CHAIN138;
afterEach(() => {
if (originalChain138Bridge === undefined) {
delete process.env.CHAIN138_L1_BRIDGE;
} else {
process.env.CHAIN138_L1_BRIDGE = originalChain138Bridge;
}
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;
}
if (originalBscBridge === undefined) {
delete process.env.CW_BRIDGE_BSC;
} else {
process.env.CW_BRIDGE_BSC = originalBscBridge;
}
if (originalStableBridge === undefined) {
delete process.env.CCIP_STABLE_BRIDGE_CHAIN138;
} else {
process.env.CCIP_STABLE_BRIDGE_CHAIN138 = originalStableBridge;
}
if (originalStablecoinBridge === undefined) {
delete process.env.CCIP_STABLECOIN_BRIDGE_CHAIN138;
} else {
process.env.CCIP_STABLECOIN_BRIDGE_CHAIN138 = originalStablecoinBridge;
}
if (originalUniversalBridge === undefined) {
delete process.env.UNIVERSAL_CCIP_BRIDGE_ADDRESS;
} else {
process.env.UNIVERSAL_CCIP_BRIDGE_ADDRESS = originalUniversalBridge;
}
if (originalUniversalBridgeAlt === undefined) {
delete process.env.UNIVERSAL_CCIP_BRIDGE;
} else {
process.env.UNIVERSAL_CCIP_BRIDGE = originalUniversalBridgeAlt;
}
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;
}
for (const [key, value] of Object.entries({
CW_GAS_STRICT_ESCROW_VERIFIER_CHAIN138: originalGasStrictVerifier,
CW_GAS_HYBRID_CAP_VERIFIER_CHAIN138: originalGasHybridVerifier,
CW_GAS_ESCROW_VAULT_CHAIN138: originalGasEscrowVault,
CW_GAS_TREASURY_SYSTEM: originalGasTreasurySystem,
CW_BRIDGE_MAINNET: originalMainnetBridge,
CW_BRIDGE_OPTIMISM: originalOptimismBridge,
CW_MAX_OUTSTANDING_ETH_MAINNET_MAINNET: originalEthMainnetOutstanding,
CW_GAS_OUTSTANDING_ETH_MAINNET_MAINNET: originalEthMainnetSupply,
CW_GAS_ESCROWED_ETH_MAINNET_MAINNET: originalEthMainnetEscrowed,
CW_GAS_TREASURY_BACKED_ETH_MAINNET_MAINNET: originalEthMainnetTreasury,
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 (originalTokenMappingLoaderPath === undefined) {
delete process.env.TOKEN_MAPPING_LOADER_PATH;
} else {
process.env.TOKEN_MAPPING_LOADER_PATH = originalTokenMappingLoaderPath;
}
});
it('loads GRU Monetary Transport Layer metadata and active transport pairs', () => {
const metadata = getGruTransportMetadata();
expect(metadata).not.toBeNull();
expect(metadata?.system?.name).toBe('GRU Monetary Transport Layer');
expect(metadata?.system?.shortName).toBe('GRU Transport');
const pairs = getActiveTransportPairs();
expect(pairs.length).toBe(44);
expect(pairs.every((pair) => pair.eligible)).toBe(true);
expect(pairs.every((pair) => typeof pair.runtimeReady === 'boolean')).toBe(true);
expect(pairs.some((pair) => pair.key === '138-25-cUSDT-cWUSDT')).toBe(true);
expect(pairs.some((pair) => pair.key === '138-1-cBTC-cWBTC')).toBe(true);
expect(pairs.some((pair) => pair.key === '138-10-cETHL2-cWETHL2')).toBe(true);
expect(pairs.some((pair) => pair.key === '138-1111-cWEMIX-cWWEMIX')).toBe(false);
const bscUsdt = pairs.find((pair) => pair.key === '138-56-cUSDT-cWUSDT');
const mainnetEth = pairs.find((pair) => pair.key === '138-1-cETH-cWETH');
expect(bscUsdt?.bridgeCanonicalAssetVersion).toBe('v1');
expect(bscUsdt?.bridgeMirroredAssetVersion).toBe('v1');
expect(bscUsdt?.destinationChainName).toBe('BSC');
expect(bscUsdt?.destinationChainSelector).toBe('11344663589394136015');
expect(mainnetEth?.destinationChainSelector).toBe('5009297550715157269');
expect(metadata?.counts.enabledDestinationChains).toBe(10);
expect(metadata?.counts.configuredTransportPairs).toBe(45);
expect(metadata?.counts.deferredTransportPairs).toBe(1);
expect(metadata?.counts.gasAssetFamilies).toBe(9);
expect(metadata?.counts.gasTransportPairs).toBe(10);
});
it('publishes gas-family metadata and enforces the ETH split via redeem groups', () => {
const gasFamilies = getGasAssetFamilies();
const redeemGroups = getGasRedeemGroups();
const protocolExposure = getGasProtocolExposure();
expect(gasFamilies).toEqual(
expect.arrayContaining([
expect.objectContaining({
familyKey: 'eth_mainnet',
backingMode: 'strict_escrow',
mirroredSymbol: 'cWETH',
}),
expect.objectContaining({
familyKey: 'eth_l2',
backingMode: 'hybrid_cap',
mirroredSymbol: 'cWETHL2',
}),
])
);
expect(redeemGroups).toEqual(
expect.arrayContaining([
expect.objectContaining({
familyKey: 'eth_l2',
allowedChains: [10, 42161, 8453],
}),
])
);
expect(protocolExposure).toEqual(
expect.arrayContaining([
expect.objectContaining({
key: '10-eth_l2',
chainId: 10,
familyKey: 'eth_l2',
}),
expect.objectContaining({
key: '1111-wemix',
active: false,
status: 'deferred',
}),
])
);
expect(gasFamilies).toEqual(
expect.arrayContaining([
expect.objectContaining({
familyKey: 'wemix',
active: false,
status: 'deferred',
}),
])
);
expect(isGasRedemptionPathAllowed(10, 42161, 'eth_l2')).toBe(true);
expect(isGasRedemptionPathAllowed(1, 10, 'eth_mainnet')).toBe(false);
});
it('keeps Chain 138 pools visible but hides inactive public cW pools', () => {
const pools = [
{
poolAddress: '0x1111111111111111111111111111111111111111',
token0Address: '0x9a1D0dBEE997929ED02fD19E0E199704d20914dB',
token1Address: '0x55d398326f99059fF775485246999027B3197955',
},
{
poolAddress: '0x2222222222222222222222222222222222222222',
token0Address: '0x93E66202A11B1772E55407B32B44e5Cd8eda7f22',
token1Address: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2',
},
];
expect(filterPoolsForExposure(56, [pools[0]])).toEqual([]);
expect(filterPoolsForRouting(56, [pools[0]])).toEqual([]);
expect(filterPoolsForExposure(138, [pools[1]])).toEqual([pools[1]]);
expect(filterPoolsForRouting(138, [pools[1]])).toEqual([pools[1]]);
});
it('marks public cW pools inactive and non-routable until explicitly enabled', () => {
expect(isPublicPoolActive(56, '0x1111111111111111111111111111111111111111')).toBe(false);
expect(isPublicPoolRoutable(56, '0x1111111111111111111111111111111111111111')).toBe(false);
});
it('routes active c* transport through GRU Transport while keeping WETH on legacy lanes', () => {
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 gruRoute = getRouteFromRegistry(138, 56, 'cUSDT');
expect(gruRoute).not.toBeNull();
expect(gruRoute?.label).toBe('GRUTransport');
expect(gruRoute?.bridgeAddress).toBe('0x3333333333333333333333333333333333333333');
const wethRoute = getRouteFromRegistry(138, 56, 'WETH');
expect(wethRoute).not.toBeNull();
expect(wethRoute?.label).toBe('CCIPWETH9Bridge');
});
it('reports runtime-ready transport pairs when bridge and reserve refs resolve', () => {
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 pairs = getActiveTransportPairs();
const bscUsdtPair = pairs.find((pair) => pair.key === '138-56-cUSDT-cWUSDT');
expect(bscUsdtPair?.runtimeBridgeReady).toBe(true);
expect(bscUsdtPair?.runtimeReserveVerifierReady).toBe(true);
expect(bscUsdtPair?.runtimeMaxOutstandingReady).toBe(true);
expect(bscUsdtPair?.runtimeReady).toBe(true);
const metadata = getGruTransportMetadata();
expect(metadata?.counts.runtimeReadyTransportPairs).toBeGreaterThan(0);
});
it('reports missing runtime requirements when bridge and reserve refs are absent', () => {
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 pairs = getActiveTransportPairs();
const bscUsdtPair = pairs.find((pair) => pair.key === '138-56-cUSDT-cWUSDT');
expect(bscUsdtPair?.eligible).toBe(true);
expect(bscUsdtPair?.runtimeReady).toBe(false);
expect(bscUsdtPair?.runtimeMissingRequirements).toEqual(
expect.arrayContaining([
'bridge:l1Bridge',
'bridge:l2Bridge',
'policy:maxOutstanding',
'reserveVerifier:bridgeRef',
'reserveVerifier:vaultRef',
'reserveVerifier:reserveSystemRef',
])
);
});
it('evaluates gas-lane supply accounting separately for strict and hybrid backing', () => {
process.env.CHAIN138_L1_BRIDGE = '0x3333333333333333333333333333333333333333';
process.env.CW_BRIDGE_MAINNET = '0x4444444444444444444444444444444444444444';
process.env.CW_GAS_STRICT_ESCROW_VERIFIER_CHAIN138 = '0x5555555555555555555555555555555555555555';
process.env.CW_GAS_ESCROW_VAULT_CHAIN138 = '0x6666666666666666666666666666666666666666';
process.env.CW_MAX_OUTSTANDING_ETH_MAINNET_MAINNET = '100';
process.env.CW_GAS_OUTSTANDING_ETH_MAINNET_MAINNET = '100';
process.env.CW_GAS_ESCROWED_ETH_MAINNET_MAINNET = '100';
process.env.CW_GAS_TREASURY_BACKED_ETH_MAINNET_MAINNET = '0';
process.env.CW_BRIDGE_OPTIMISM = '0x7777777777777777777777777777777777777777';
process.env.CW_GAS_HYBRID_CAP_VERIFIER_CHAIN138 = '0x8888888888888888888888888888888888888888';
process.env.CW_GAS_TREASURY_SYSTEM = '0x9999999999999999999999999999999999999999';
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';
const pairs = getActiveTransportPairs();
const strictPair = pairs.find((pair) => pair.key === '138-1-cETH-cWETH');
const hybridPair = pairs.find((pair) => pair.key === '138-10-cETHL2-cWETHL2');
expect(strictPair?.runtimeSupplyAccountingReady).toBe(true);
expect(strictPair?.supplyInvariantSatisfied).toBe(true);
expect(strictPair?.runtimeReady).toBe(true);
expect(hybridPair?.runtimeSupplyAccountingReady).toBe(true);
expect(hybridPair?.supplyInvariantSatisfied).toBe(true);
expect(hybridPair?.runtimeReady).toBe(true);
});
it('prefers the active GRU mirrored address even when raw pair ordering is ambiguous', () => {
const loader = require(path.join(process.cwd(), '../../..', 'config', 'token-mapping-loader.cjs')) as {
getMappedAddress: (fromChainId: number, toChainId: number, tokenAddressOnSource: string, jsonPath?: string) => string | undefined;
};
const fixturePath = path.join(os.tmpdir(), `gru-transport-mapping-${Date.now()}.json`);
const canonicalAddress = '0x93E66202A11B1772E55407B32B44e5Cd8eda7f22';
const mirroredAddress = '0x9a1D0dBEE997929ED02fD19E0E199704d20914dB';
const nativeAddress = '0x55d398326f99059fF775485246999027B3197955';
fs.writeFileSync(
fixturePath,
JSON.stringify(
{
pairs: [
{
fromChainId: 138,
toChainId: 56,
tokens: [
{
key: 'Compliant_USDT_cW',
name: 'cUSDT->cWUSDT',
addressFrom: canonicalAddress,
addressTo: mirroredAddress,
},
{
key: 'Compliant_USDT',
name: 'cUSDT',
addressFrom: canonicalAddress,
addressTo: nativeAddress,
},
],
},
],
},
null,
2
)
);
try {
expect(loader.getMappedAddress(138, 56, canonicalAddress, fixturePath)).toBe(mirroredAddress);
} finally {
fs.unlinkSync(fixturePath);
}
});
it('does not fall back to legacy stable bridges for active GRU assets when peer bridges are missing', () => {
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;
process.env.CCIP_STABLE_BRIDGE_CHAIN138 = '0x5555555555555555555555555555555555555555';
process.env.UNIVERSAL_CCIP_BRIDGE_ADDRESS = '0x6666666666666666666666666666666666666666';
jest.resetModules();
const { getRouteFromRegistry: getFreshRouteFromRegistry } = require('./cross-chain-bridges') as typeof import('./cross-chain-bridges');
expect(getFreshRouteFromRegistry(138, 56, 'cUSDT')).toBeNull();
});
it('resolves the token mapping loader from an explicit deployed-layout env override', () => {
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'token-mapping-loader-'));
const configDir = path.join(tempRoot, 'config');
fs.mkdirSync(configDir, { recursive: true });
const loaderPath = path.join(configDir, 'token-mapping-loader.cjs');
fs.writeFileSync(
loaderPath,
'module.exports = { getGruTransportMetadata: () => ({ system: { shortName: "Test" }, counts: { transportPairs: 0 } }), getActiveTransportPairs: () => [] };'
);
process.env.TOKEN_MAPPING_LOADER_PATH = loaderPath;
try {
expect(resolveTokenMappingLoaderPath()).toBe(loaderPath);
expect(getGruTransportMetadata()?.system?.shortName).toBe('Test');
expect(getActiveTransportPairs()).toEqual([]);
} finally {
fs.rmSync(tempRoot, { recursive: true, force: true });
}
});
});

View File

@@ -0,0 +1,338 @@
import { loadTokenMappingLoader } from './repo-config-loader';
export interface ConfigRef {
address?: string | null;
env?: string | null;
}
export interface GruTransportSystemMetadata {
name: string;
shortName: string;
canonicalChainId: number;
canonicalChainName?: string;
transportClass?: string;
publicPoolModel?: string;
hardPegTruth?: string;
wethTransportSeparated?: boolean;
notes?: string;
}
export interface GruTransportMetadata {
system: GruTransportSystemMetadata | null;
terminology: Record<string, string>;
enabledCanonicalTokens: Array<Record<string, unknown>>;
enabledDestinationChains: Array<Record<string, unknown>>;
gasAssetFamilies?: GruTransportGasAssetFamily[];
gasRedeemGroups?: GruTransportGasRedeemGroup[];
gasProtocolExposure?: GruTransportGasProtocolExposure[];
counts: {
enabledCanonicalTokens: number;
enabledDestinationChains: number;
approvedBridgePeers: number;
transportPairs: number;
configuredTransportPairs?: number;
deferredTransportPairs?: number;
gasAssetFamilies?: number;
gasRedeemGroups?: number;
gasProtocolExposure?: number;
gasTransportPairs?: number;
strictEscrowTransportPairs?: number;
hybridCapTransportPairs?: number;
eligibleTransportPairs?: number;
runtimeReadyTransportPairs?: number;
publicPools: number;
activePublicPools?: number;
routablePublicPools?: number;
mcpVisiblePublicPools?: number;
};
}
export interface GruTransportGasAssetFamily {
familyKey: string;
active?: boolean;
status?: string;
canonicalSymbol138: string;
mirroredSymbol: string;
assetClass: string;
originChains: number[];
laneGroup: string;
backingMode: string;
redeemPolicy: string;
wrappedNativeQuoteSymbol: string;
stableQuoteSymbol: string;
referenceVenue: string;
perLaneCaps?: Record<string, string>;
displayAliases?: Record<string, string>;
hubRebalance?: Record<string, unknown>;
}
export interface GruTransportGasRedeemGroup {
key: string;
familyKey: string;
allowedChains: number[];
redeemPolicy: string;
description?: string;
}
export interface GruTransportGasProtocolExposure {
key: string;
chainId: number;
active?: boolean;
status?: string;
familyKey: string;
mirroredSymbol: string;
backingMode: string;
dodoPmm?: Record<string, unknown>;
uniswapV3?: Record<string, unknown>;
balancer?: Record<string, unknown>;
curve?: Record<string, unknown>;
oneInch?: Record<string, unknown>;
}
export interface GruTransportBridgePeer {
key: string;
chainId: number;
chainName: string;
ccipChainSelector?: string;
active?: boolean;
status?: string;
bridgeKind: string;
l1Bridge?: ConfigRef;
l2Bridge?: ConfigRef;
freezeTokenPairRequired?: boolean;
freezeDestinationRequired?: boolean;
}
export interface GruTransportPair {
key: string;
canonicalChainId: number;
destinationChainId: number;
destinationChainName?: string | null;
destinationChainSelector?: string | null;
active?: boolean;
status?: string;
canonicalSymbol: string;
mirroredSymbol: string;
/** From gru-transport-active enabledCanonicalTokens[].bridge.canonicalAssetVersion */
bridgeCanonicalAssetVersion?: string;
/** From gru-transport-active enabledCanonicalTokens[].bridge.mirroredAssetVersion */
bridgeMirroredAssetVersion?: string;
mappingKey: string;
peerKey: string;
phase?: string;
routeDiscoveryEnabled?: boolean;
mcpVisible?: boolean;
reserveVerifierKey?: string;
maxOutstanding?: {
required?: boolean;
amount?: string;
env?: string;
};
publicPoolKeys?: string[];
assetClass?: string;
familyKey?: string;
laneGroup?: string;
backingMode?: string;
redeemPolicy?: string;
wrappedNativeQuoteSymbol?: string;
stableQuoteSymbol?: string;
referenceVenue?: string;
protocolExposureKey?: string;
supplyAccounting?: Record<string, unknown>;
canonicalAddress?: string | null;
mirroredAddress?: string | null;
mirrorDeploymentAddress?: string | null;
peer?: GruTransportBridgePeer | null;
mappingFound?: boolean;
mirrorDeployed?: boolean;
canonicalEnabled?: boolean;
destinationEnabled?: boolean;
bridgeAvailable?: boolean | null;
bridgePeerConfigured?: boolean;
maxOutstandingConfigured?: boolean;
reserveVerifierConfigured?: boolean;
runtimeL1BridgeAddress?: string | null;
runtimeL2BridgeAddress?: string | null;
runtimeBridgeReady?: boolean;
runtimeMaxOutstandingValue?: string | null;
runtimeMaxOutstandingReady?: boolean;
runtimeReserveVerifierBridgeAddress?: string | null;
runtimeReserveVerifierAddress?: string | null;
runtimeReserveVaultAddress?: string | null;
runtimeReserveSystemAddress?: string | null;
runtimeReserveVerifierReady?: boolean;
runtimeOutstandingValue?: string | null;
runtimeEscrowedValue?: string | null;
runtimeTreasuryBackedValue?: string | null;
runtimeTreasuryCapValue?: string | null;
runtimeSupplyAccountingReady?: boolean | null;
supplyInvariantSatisfied?: boolean | null;
protocolExposure?: GruTransportGasProtocolExposure | null;
runtimeMissingRequirements?: string[];
eligibilityBlockers?: string[];
runtimeReady?: boolean;
eligible?: boolean;
}
export interface GruTransportPublicPool {
key: string;
chainId: number;
baseSymbol: string;
quoteSymbol: string;
poolAddress?: string | null;
active?: boolean;
routingEnabled?: boolean;
mcpVisible?: boolean;
phase?: string;
}
export interface PublicPoolLike {
poolAddress: string;
token0Address: string;
token1Address: string;
}
interface GruTransportLoader {
getGruTransportMetadata?: () => GruTransportMetadata | null;
getGasAssetFamilies?: () => GruTransportGasAssetFamily[];
getGasRedeemGroups?: () => GruTransportGasRedeemGroup[];
getGasProtocolExposure?: () => GruTransportGasProtocolExposure[];
isGasRedemptionPathAllowed?: (fromChainId: number, toChainId: number, identifier: string) => boolean;
getActiveTransportPairs?: () => GruTransportPair[];
getActiveTransportPair?: (
fromChainId: number,
toChainId: number,
criteria?: Record<string, unknown>
) => GruTransportPair | null;
getApprovedBridgePeer?: (chainId: number) => GruTransportBridgePeer | null;
getActivePublicPools?: () => GruTransportPublicPool[];
isPublicPoolActive?: (chainId: number, poolAddress: string) => boolean;
isPublicPoolRoutable?: (chainId: number, poolAddress: string) => boolean;
isPublicPoolMcpVisible?: (chainId: number, poolAddress: string) => boolean;
shouldExposePublicPool?: (
chainId: number,
poolAddress: string,
token0Address: string,
token1Address: string
) => boolean;
shouldUsePublicPoolForRouting?: (
chainId: number,
poolAddress: string,
token0Address: string,
token1Address: string
) => boolean;
resolveConfigRef?: (ref: ConfigRef | undefined) => string;
}
function loadGruTransportLoader(): GruTransportLoader | null {
const loader = loadTokenMappingLoader<GruTransportLoader>();
if (loader?.getGruTransportMetadata && loader?.getActiveTransportPairs) {
return loader;
}
return null;
}
export function getGruTransportMetadata(): GruTransportMetadata | null {
const loader = loadGruTransportLoader();
return loader?.getGruTransportMetadata?.() ?? null;
}
export function getGasAssetFamilies(): GruTransportGasAssetFamily[] {
const loader = loadGruTransportLoader();
return loader?.getGasAssetFamilies?.() ?? getGruTransportMetadata()?.gasAssetFamilies ?? [];
}
export function getGasRedeemGroups(): GruTransportGasRedeemGroup[] {
const loader = loadGruTransportLoader();
return loader?.getGasRedeemGroups?.() ?? getGruTransportMetadata()?.gasRedeemGroups ?? [];
}
export function getGasProtocolExposure(): GruTransportGasProtocolExposure[] {
const loader = loadGruTransportLoader();
return loader?.getGasProtocolExposure?.() ?? getGruTransportMetadata()?.gasProtocolExposure ?? [];
}
export function isGasRedemptionPathAllowed(
fromChainId: number,
toChainId: number,
identifier: string
): boolean {
const loader = loadGruTransportLoader();
return loader?.isGasRedemptionPathAllowed?.(fromChainId, toChainId, identifier) ?? false;
}
export function getActiveTransportPairs(): GruTransportPair[] {
const loader = loadGruTransportLoader();
return loader?.getActiveTransportPairs?.() ?? [];
}
export function getActiveTransportPairBySymbol(
fromChainId: number,
toChainId: number,
symbol: string
): GruTransportPair | null {
const loader = loadGruTransportLoader();
return loader?.getActiveTransportPair?.(fromChainId, toChainId, { symbol }) ?? null;
}
export function getApprovedBridgePeerByChain(chainId: number): GruTransportBridgePeer | null {
const loader = loadGruTransportLoader();
return loader?.getApprovedBridgePeer?.(chainId) ?? null;
}
export function resolveConfigRef(ref: ConfigRef | undefined): string {
const loader = loadGruTransportLoader();
return loader?.resolveConfigRef?.(ref) ?? '';
}
export function getActivePublicPools(): GruTransportPublicPool[] {
const loader = loadGruTransportLoader();
return loader?.getActivePublicPools?.() ?? [];
}
export function isPublicPoolActive(chainId: number, poolAddress: string): boolean {
const loader = loadGruTransportLoader();
return loader?.isPublicPoolActive?.(chainId, poolAddress) ?? true;
}
export function isPublicPoolRoutable(chainId: number, poolAddress: string): boolean {
const loader = loadGruTransportLoader();
return loader?.isPublicPoolRoutable?.(chainId, poolAddress) ?? true;
}
export function isPublicPoolMcpVisible(chainId: number, poolAddress: string): boolean {
const loader = loadGruTransportLoader();
return loader?.isPublicPoolMcpVisible?.(chainId, poolAddress) ?? false;
}
export function shouldExposePublicPool(
chainId: number,
poolAddress: string,
token0Address: string,
token1Address: string
): boolean {
const loader = loadGruTransportLoader();
return loader?.shouldExposePublicPool?.(chainId, poolAddress, token0Address, token1Address) ?? true;
}
export function shouldUsePublicPoolForRouting(
chainId: number,
poolAddress: string,
token0Address: string,
token1Address: string
): boolean {
const loader = loadGruTransportLoader();
return loader?.shouldUsePublicPoolForRouting?.(chainId, poolAddress, token0Address, token1Address) ?? true;
}
export function filterPoolsForExposure<T extends PublicPoolLike>(chainId: number, pools: T[]): T[] {
return pools.filter((pool) =>
shouldExposePublicPool(chainId, pool.poolAddress, pool.token0Address, pool.token1Address)
);
}
export function filterPoolsForRouting<T extends PublicPoolLike>(chainId: number, pools: T[]): T[] {
return pools.filter((pool) =>
shouldUsePublicPoolForRouting(chainId, pool.poolAddress, pool.token0Address, pool.token1Address)
);
}

View File

@@ -3,6 +3,8 @@
* Aligns with real-robinhood project_plans and ultra_advanced_global_arbitrage_engine_blueprint.
*/
import { resolveChain138RpcUrl } from './chain138-rpc';
export type ChainGroup = 'hub' | 'edge' | 'althub' | 'external';
export interface HeatmapChain {
@@ -57,9 +59,9 @@ export const DEFAULT_HEATMAP_ASSETS = [
function buildChains(): HeatmapChain[] {
const rpc = (cid: number) =>
process.env[`CHAIN_${cid}_RPC_URL`] ||
process.env[`RPC_URL_138`] ||
'https://rpc.d-bis.org';
cid === 138
? resolveChain138RpcUrl()
: process.env[`CHAIN_${cid}_RPC_URL`] || 'https://rpc.d-bis.org';
const explorer = (cid: number) => {
const urls: Record<number, string> = {
138: 'https://explorer.d-bis.org',

View File

@@ -0,0 +1,23 @@
import {
getMonetaryUnitByCode,
getMonetaryUnitBySymbol,
isMonetaryUnitSupported,
} from './monetary-unit-symbol-registry';
describe('monetary-unit symbol registry', () => {
it('tracks BTC as a non-ISO GRU monetary unit family', () => {
expect(isMonetaryUnitSupported('BTC')).toBe(true);
expect(getMonetaryUnitByCode('BTC')).toMatchObject({
code: 'BTC',
canonicalSymbol: 'cBTC',
wrappedSymbol: 'cWBTC',
mappingKey: 'Compliant_BTC_cW',
decimals: 8,
});
expect(getMonetaryUnitBySymbol('cWBTC')).toMatchObject({
code: 'BTC',
canonicalSymbol: 'cBTC',
wrappedSymbol: 'cWBTC',
});
});
});

View File

@@ -0,0 +1,132 @@
import fs from 'fs';
import path from 'path';
export interface MonetaryUnitSymbolIdentity {
code: string;
canonicalSymbol: string;
wrappedSymbol: string;
mappingKey: string;
decimals: number;
}
interface MonetaryUnitManifestEntry {
code: string;
canonicalSymbol: string;
wrappedSymbol: string;
mappingKey: string;
decimals: number;
}
interface MonetaryUnitManifestFile {
monetaryUnits?: MonetaryUnitManifestEntry[];
}
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 resolveMonetaryUnitManifestPath(): string | null {
const candidates = uniquePaths([
process.env.GRU_MONETARY_UNIT_MANIFEST_PATH,
process.env.MONETARY_UNIT_MANIFEST_JSON_PATH,
path.resolve(process.cwd(), 'config/gru-monetary-unit-manifest.json'),
path.resolve(process.cwd(), '../config/gru-monetary-unit-manifest.json'),
path.resolve(process.cwd(), '../../config/gru-monetary-unit-manifest.json'),
path.resolve('/config/gru-monetary-unit-manifest.json'),
path.resolve(__dirname, '../../../../../config/gru-monetary-unit-manifest.json'),
]);
for (const candidate of candidates) {
if (fs.existsSync(candidate)) return candidate;
}
return null;
}
function loadMonetaryUnitManifest(): MonetaryUnitManifestFile {
const filePath = resolveMonetaryUnitManifestPath();
if (!filePath) return {};
try {
return JSON.parse(fs.readFileSync(filePath, 'utf8')) as MonetaryUnitManifestFile;
} catch {
return {};
}
}
const monetaryUnitManifest = loadMonetaryUnitManifest();
const MONETARY_UNIT_ENTRIES = (
Array.isArray(monetaryUnitManifest.monetaryUnits)
? monetaryUnitManifest.monetaryUnits
: []
).map((entry) => ({
code: String(entry.code || '').trim().toUpperCase(),
canonicalSymbol: String(entry.canonicalSymbol || '').trim(),
wrappedSymbol: String(entry.wrappedSymbol || '').trim(),
mappingKey: String(entry.mappingKey || '').trim(),
decimals: Number(entry.decimals || 0),
}));
export const MONETARY_UNIT_SUPPORTED = MONETARY_UNIT_ENTRIES.map((entry) => entry.code) as string[];
export const MONETARY_UNIT_BY_CODE: Record<string, MonetaryUnitSymbolIdentity> = Object.fromEntries(
MONETARY_UNIT_ENTRIES.map((entry) => [
entry.code,
{
code: entry.code,
canonicalSymbol: entry.canonicalSymbol,
wrappedSymbol: entry.wrappedSymbol,
mappingKey: entry.mappingKey,
decimals: entry.decimals,
},
])
);
export const MONETARY_UNIT_BY_SYMBOL: Record<string, MonetaryUnitSymbolIdentity> = Object.fromEntries(
MONETARY_UNIT_ENTRIES.flatMap((entry) => [
[
entry.canonicalSymbol,
{
code: entry.code,
canonicalSymbol: entry.canonicalSymbol,
wrappedSymbol: entry.wrappedSymbol,
mappingKey: entry.mappingKey,
decimals: entry.decimals,
},
],
[
entry.wrappedSymbol,
{
code: entry.code,
canonicalSymbol: entry.canonicalSymbol,
wrappedSymbol: entry.wrappedSymbol,
mappingKey: entry.mappingKey,
decimals: entry.decimals,
},
],
])
);
export function isMonetaryUnitSupported(code: string): boolean {
return Boolean(MONETARY_UNIT_BY_CODE[String(code || '').trim().toUpperCase()]);
}
export function getMonetaryUnitByCode(code: string): MonetaryUnitSymbolIdentity | undefined {
return MONETARY_UNIT_BY_CODE[String(code || '').trim().toUpperCase()];
}
export function getMonetaryUnitBySymbol(symbol: string): MonetaryUnitSymbolIdentity | undefined {
return MONETARY_UNIT_BY_SYMBOL[String(symbol || '').trim()];
}

View File

@@ -0,0 +1,563 @@
import { AbiCoder } from 'ethers';
import {
PlannerProvider,
ProviderCapabilityRecord,
ProviderPairCapability,
} from '../services/planner-v2-types';
import { encodeChain138DodoV3ProviderData, isChain138DodoV3ExecutionLive } from '../services/dodo-v3-pilot';
import { getChain138PilotVenueEdges } from '../services/chain138-pilot-venues';
import { getChain138RoutingAssets } from './routing-assets';
const abiCoder = AbiCoder.defaultAbiCoder();
const CHAIN_138 = 138;
const CHAIN138_UNISWAP_V3_ROUTER = '0xde9cd8ee2811e6e64a41d5f68be315d33995975e';
const CHAIN138_UNISWAP_V3_QUOTER = '0x6abbb1ceb2468e748a03a00cd6aa9bfe893afa1f';
const CHAIN138_PILOT_BALANCER_VAULT = '0x96423d7c1727698d8a25ebfb88131e9422d1a3c3';
const CHAIN138_PILOT_CURVE_3POOL = '0xe440ec15805be4c7babcd17a63b8c8a08a492e0f';
const CHAIN138_PILOT_ONEINCH_ROUTER = '0x500b84b1bc6f59c1898a5fe538ea20a758757a4f';
const CHAIN138_PILOT_BALANCER_WETH_USDT_POOL_ID = '0x877cd220759e8c94b82f55450c85d382ae06856c426b56d93092a420facbc324';
const CHAIN138_PILOT_BALANCER_WETH_USDC_POOL_ID = '0xd8dfb18a6baf9b29d8c2dbd74639db87ac558af120df5261dab8e2a5de69013b';
function normalizeAddress(value?: string): string {
return String(value || '').trim().toLowerCase();
}
function liveOrPlannedAddress(value?: string): 'live' | 'planned' {
return normalizeAddress(value) ? 'live' : 'planned';
}
function bidirectionalPair(args: {
chainId: number;
provider: PlannerProvider;
tokenASymbol: string;
tokenAAddress: string;
tokenBSymbol: string;
tokenBAddress: string;
status: 'live' | 'planned' | 'blocked';
target?: string;
providerData?: Record<string, unknown>;
providerDataHex?: string;
notes?: string[];
reason?: string;
}): ProviderPairCapability[] {
return [
{
chainId: args.chainId,
provider: args.provider,
legType: 'swap',
status: args.status,
tokenInSymbol: args.tokenASymbol,
tokenInAddress: normalizeAddress(args.tokenAAddress),
tokenOutSymbol: args.tokenBSymbol,
tokenOutAddress: normalizeAddress(args.tokenBAddress),
target: normalizeAddress(args.target),
providerData: args.providerData,
providerDataHex: args.providerDataHex,
notes: args.notes,
reason: args.reason,
},
{
chainId: args.chainId,
provider: args.provider,
legType: 'swap',
status: args.status,
tokenInSymbol: args.tokenBSymbol,
tokenInAddress: normalizeAddress(args.tokenBAddress),
tokenOutSymbol: args.tokenASymbol,
tokenOutAddress: normalizeAddress(args.tokenAAddress),
target: normalizeAddress(args.target),
providerData: args.providerData,
providerDataHex: args.providerDataHex,
notes: args.notes,
reason: args.reason,
},
];
}
function encodeDodoPool(poolAddress: string): string {
return abiCoder.encode(['address'], [poolAddress]);
}
function encodeUniswapRoute(fee: number, quoter: string): string {
return abiCoder.encode(['bytes', 'uint24', 'address', 'bool'], ['0x', fee, quoter, false]);
}
function encodeBalancerRoute(poolId: string): string {
return abiCoder.encode(['bytes32'], [poolId]);
}
function encodeCurveRoute(i: number, j: number, useUnderlying: boolean): string {
return abiCoder.encode(['int128', 'int128', 'bool'], [i, j, useUnderlying]);
}
function encodeOneInchRoute(router: string): string {
return abiCoder.encode(['address', 'address', 'bytes'], [router, router, '0x']);
}
function chain138DodoCapabilities(): ProviderCapabilityRecord {
const assets = getChain138RoutingAssets();
const dodoProvider =
normalizeAddress(process.env.DODO_PMM_PROVIDER_ADDRESS) ||
normalizeAddress(process.env.DODO_PMM_PROVIDER) ||
'0x3f729632e9553ebaccde2e9b4c8f2b285b014f2e';
const stablePool = '0x9e89bae009adf128782e19e8341996c596ac40dc';
const cusdtUsdtPool = '0x866cb44b59303d8dc5f4f9e3e7a8e8b0bf238d66';
const cusdcUsdcPool = '0xc39b7d0f40838cbfb54649d327f49a6dac964062';
const cusdtXaucPool = '0x1aa55e2001e5651349aff5a63fd7a7ae44f0f1b0';
const cusdcXaucPool = '0xea9ac6357cacb42a83b9082b870610363b177cba';
const ceurtXaucPool = '0xba99bc1eaac164569d5aca96c806934ddaf970cf';
const cbtcCusdtPool = normalizeAddress(process.env.CHAIN138_POOL_CBTC_CUSDT);
const cbtcCusdcPool = normalizeAddress(process.env.CHAIN138_POOL_CBTC_CUSDC);
const cbtcXaucPool = normalizeAddress(process.env.CHAIN138_POOL_CBTC_CXAUC);
const wethUsdtPool = normalizeAddress(process.env.CHAIN138_POOL_WETH_USDT);
const wethUsdcPool = normalizeAddress(process.env.CHAIN138_POOL_WETH_USDC);
const pairs: ProviderPairCapability[] = [
...bidirectionalPair({
chainId: CHAIN_138,
provider: 'dodo',
tokenASymbol: 'cUSDT',
tokenAAddress: assets.cUSDT.address,
tokenBSymbol: 'cUSDC',
tokenBAddress: assets.cUSDC.address,
status: 'live',
target: dodoProvider,
providerData: { poolAddress: stablePool },
providerDataHex: encodeDodoPool(stablePool),
notes: ['Canonical stable pool on the official DODO V2 DVM-backed stack.'],
}),
...bidirectionalPair({
chainId: CHAIN_138,
provider: 'dodo',
tokenASymbol: 'cUSDT',
tokenAAddress: assets.cUSDT.address,
tokenBSymbol: 'USDT',
tokenBAddress: assets.USDT.address,
status: 'live',
target: dodoProvider,
providerData: { poolAddress: cusdtUsdtPool },
providerDataHex: encodeDodoPool(cusdtUsdtPool),
notes: ['Canonical stable pool on the official DODO V2 DVM-backed stack.'],
}),
...bidirectionalPair({
chainId: CHAIN_138,
provider: 'dodo',
tokenASymbol: 'cUSDC',
tokenAAddress: assets.cUSDC.address,
tokenBSymbol: 'USDC',
tokenBAddress: assets.USDC.address,
status: 'live',
target: dodoProvider,
providerData: { poolAddress: cusdcUsdcPool },
providerDataHex: encodeDodoPool(cusdcUsdcPool),
notes: ['Canonical stable pool on the official DODO V2 DVM-backed stack.'],
}),
...bidirectionalPair({
chainId: CHAIN_138,
provider: 'dodo',
tokenASymbol: 'cUSDT',
tokenAAddress: assets.cUSDT.address,
tokenBSymbol: 'cXAUC',
tokenBAddress: assets.cXAUC.address,
status: 'live',
target: dodoProvider,
providerData: { poolAddress: cusdtXaucPool },
providerDataHex: encodeDodoPool(cusdtXaucPool),
notes: ['Commodity route; excluded unless policy allows commodity intermediates.'],
}),
...bidirectionalPair({
chainId: CHAIN_138,
provider: 'dodo',
tokenASymbol: 'cUSDC',
tokenAAddress: assets.cUSDC.address,
tokenBSymbol: 'cXAUC',
tokenBAddress: assets.cXAUC.address,
status: 'live',
target: dodoProvider,
providerData: { poolAddress: cusdcXaucPool },
providerDataHex: encodeDodoPool(cusdcXaucPool),
notes: ['Commodity route; excluded unless policy allows commodity intermediates.'],
}),
...bidirectionalPair({
chainId: CHAIN_138,
provider: 'dodo',
tokenASymbol: 'cEURT',
tokenAAddress: assets.cEURT.address,
tokenBSymbol: 'cXAUC',
tokenBAddress: assets.cXAUC.address,
status: 'live',
target: dodoProvider,
providerData: { poolAddress: ceurtXaucPool },
providerDataHex: encodeDodoPool(ceurtXaucPool),
notes: ['Commodity route; excluded unless policy allows commodity intermediates.'],
}),
...bidirectionalPair({
chainId: CHAIN_138,
provider: 'dodo',
tokenASymbol: 'cBTC',
tokenAAddress: assets.cBTC.address,
tokenBSymbol: 'cUSDT',
tokenBAddress: assets.cUSDT.address,
status: liveOrPlannedAddress(cbtcCusdtPool),
target: dodoProvider,
providerData: cbtcCusdtPool ? { poolAddress: cbtcCusdtPool } : undefined,
providerDataHex: cbtcCusdtPool ? encodeDodoPool(cbtcCusdtPool) : undefined,
notes: ['Bitcoin monetary-unit route for the jewelry-box program.'],
reason: cbtcCusdtPool ? undefined : 'Set CHAIN138_POOL_CBTC_CUSDT after the canonical cBTC/cUSDT PMM pool is created and funded.',
}),
...bidirectionalPair({
chainId: CHAIN_138,
provider: 'dodo',
tokenASymbol: 'cBTC',
tokenAAddress: assets.cBTC.address,
tokenBSymbol: 'cUSDC',
tokenBAddress: assets.cUSDC.address,
status: liveOrPlannedAddress(cbtcCusdcPool),
target: dodoProvider,
providerData: cbtcCusdcPool ? { poolAddress: cbtcCusdcPool } : undefined,
providerDataHex: cbtcCusdcPool ? encodeDodoPool(cbtcCusdcPool) : undefined,
notes: ['Bitcoin monetary-unit route for the jewelry-box program.'],
reason: cbtcCusdcPool ? undefined : 'Set CHAIN138_POOL_CBTC_CUSDC after the canonical cBTC/cUSDC PMM pool is created and funded.',
}),
...bidirectionalPair({
chainId: CHAIN_138,
provider: 'dodo',
tokenASymbol: 'cBTC',
tokenAAddress: assets.cBTC.address,
tokenBSymbol: 'cXAUC',
tokenBAddress: assets.cXAUC.address,
status: liveOrPlannedAddress(cbtcXaucPool),
target: dodoProvider,
providerData: cbtcXaucPool ? { poolAddress: cbtcXaucPool } : undefined,
providerDataHex: cbtcXaucPool ? encodeDodoPool(cbtcXaucPool) : undefined,
notes: ['Bitcoin-to-gold route for jewelry-box rebalances; excluded unless policy allows commodity intermediates.'],
reason: cbtcXaucPool ? undefined : 'Set CHAIN138_POOL_CBTC_CXAUC after the canonical cBTC/cXAUC PMM pool is created and funded.',
}),
...bidirectionalPair({
chainId: CHAIN_138,
provider: 'dodo',
tokenASymbol: 'WETH',
tokenAAddress: assets.WETH.address,
tokenBSymbol: 'USDT',
tokenBAddress: assets.USDT.address,
status: wethUsdtPool ? 'live' : 'planned',
target: dodoProvider,
providerData: wethUsdtPool ? { poolAddress: wethUsdtPool } : undefined,
providerDataHex: wethUsdtPool ? encodeDodoPool(wethUsdtPool) : undefined,
notes: ['Phase 1 WETH lane for router-v2 stable execution.'],
reason: wethUsdtPool ? undefined : 'Set CHAIN138_POOL_WETH_USDT after the canonical WETH/USDT pool is created and funded.',
}),
...bidirectionalPair({
chainId: CHAIN_138,
provider: 'dodo',
tokenASymbol: 'WETH',
tokenAAddress: assets.WETH.address,
tokenBSymbol: 'USDC',
tokenBAddress: assets.USDC.address,
status: wethUsdcPool ? 'live' : 'planned',
target: dodoProvider,
providerData: wethUsdcPool ? { poolAddress: wethUsdcPool } : undefined,
providerDataHex: wethUsdcPool ? encodeDodoPool(wethUsdcPool) : undefined,
notes: ['Phase 1 WETH lane for router-v2 stable execution.'],
reason: wethUsdcPool ? undefined : 'Set CHAIN138_POOL_WETH_USDC after the canonical WETH/USDC pool is created and funded.',
}),
];
const livePairs = pairs.filter((pair) => pair.status === 'live');
return {
chainId: CHAIN_138,
provider: 'dodo',
executionMode: 'onchain',
live: livePairs.length > 0,
quoteLive: livePairs.length > 0,
executionLive: livePairs.length > 0,
supportedLegTypes: ['swap'],
pairs,
notes: ['DODO is the first production executor for Chain 138 router-v2 rollout.'],
};
}
function chain138DodoV3Capabilities(): ProviderCapabilityRecord {
const assets = getChain138RoutingAssets();
const enabled = process.env.CHAIN138_ENABLE_DODO_V3_ROUTING !== '0';
const proxy = normalizeAddress(process.env.CHAIN138_D3_PROXY_ADDRESS) || '0xc9a11abb7c63d88546be24d58a6d95e3762cb843';
const pool = normalizeAddress(process.env.CHAIN138_D3_MM_ADDRESS) || '0x6550a3a59070061a262a893a1d6f3f490affdbda';
const status = enabled && proxy && pool ? 'live' : 'planned';
const executionLive = status === 'live' && isChain138DodoV3ExecutionLive();
const pairs = [
...bidirectionalPair({
chainId: CHAIN_138,
provider: 'dodo_v3',
tokenASymbol: 'WETH10',
tokenAAddress: assets.WETH10.address,
tokenBSymbol: 'USDT',
tokenBAddress: assets.USDT.address,
status,
target: proxy,
providerData: status === 'live'
? { poolAddress: pool, proxyAddress: proxy, quoteMethod: 'querySellTokens' }
: undefined,
providerDataHex: executionLive ? encodeChain138DodoV3ProviderData(pool) : undefined,
notes: [
'Canonical Chain 138 DODO v3 / D3MM pilot route.',
executionLive
? 'Planner visibility, quote selection, and EnhancedSwapRouterV2 execution are live for the canonical pilot pair.'
: 'Planner visibility and quote selection are live; EnhancedSwapRouterV2 adapter support is still pending.',
],
reason: status === 'planned'
? 'Set CHAIN138_ENABLE_DODO_V3_ROUTING=1 and ensure CHAIN138_D3_MM_ADDRESS / CHAIN138_D3_PROXY_ADDRESS are configured to expose the pilot venue.'
: undefined,
}),
];
return {
chainId: CHAIN_138,
provider: 'dodo_v3',
executionMode: 'onchain',
live: status === 'live',
quoteLive: status === 'live',
executionLive,
supportedLegTypes: ['swap'],
pairs,
notes: [
executionLive
? 'Private DODO v3 / D3MM Chain 138 pilot is live in planner-v2 visibility and internal execution-plan calldata.'
: 'Private DODO v3 / D3MM Chain 138 pilot promoted into planner-v2 visibility.',
executionLive
? 'Route discovery and execution-plan generation are live for the canonical pilot pair.'
: 'Route discovery is live, but internal execution-plan calldata is intentionally withheld until a dedicated D3 route executor adapter exists.',
],
};
}
function chain138UniswapCapabilities(): ProviderCapabilityRecord {
const assets = getChain138RoutingAssets();
const router = normalizeAddress(process.env.UNISWAP_V3_ROUTER || CHAIN138_UNISWAP_V3_ROUTER);
const quoter = normalizeAddress(process.env.UNISWAP_QUOTER_ADDRESS || process.env.UNISWAP_QUOTER || CHAIN138_UNISWAP_V3_QUOTER);
const wethUsdtFee = Number(process.env.UNISWAP_V3_WETH_USDT_FEE || '500');
const wethUsdcFee = Number(process.env.UNISWAP_V3_WETH_USDC_FEE || '500');
const status = router && quoter ? 'live' : 'planned';
const pairs = [
...bidirectionalPair({
chainId: CHAIN_138,
provider: 'uniswap_v3',
tokenASymbol: 'WETH',
tokenAAddress: assets.WETH.address,
tokenBSymbol: 'USDT',
tokenBAddress: assets.USDT.address,
status,
target: router,
providerData: status === 'live' ? { fee: wethUsdtFee, quoter } : undefined,
providerDataHex: status === 'live' ? encodeUniswapRoute(wethUsdtFee, quoter) : undefined,
notes: ['Canonical Chain 138 upstream-native Uniswap v3 WETH/USDT venue.'],
reason: status === 'planned' ? 'Configure UNISWAP_V3_ROUTER and UNISWAP_QUOTER_ADDRESS after Chain 138 native venue deployment.' : undefined,
}),
...bidirectionalPair({
chainId: CHAIN_138,
provider: 'uniswap_v3',
tokenASymbol: 'WETH',
tokenAAddress: assets.WETH.address,
tokenBSymbol: 'USDC',
tokenBAddress: assets.USDC.address,
status,
target: router,
providerData: status === 'live' ? { fee: wethUsdcFee, quoter } : undefined,
providerDataHex: status === 'live' ? encodeUniswapRoute(wethUsdcFee, quoter) : undefined,
notes: ['Canonical Chain 138 upstream-native Uniswap v3 WETH/USDC venue.'],
reason: status === 'planned' ? 'Configure UNISWAP_V3_ROUTER and UNISWAP_QUOTER_ADDRESS after Chain 138 native venue deployment.' : undefined,
}),
];
return {
chainId: CHAIN_138,
provider: 'uniswap_v3',
executionMode: 'onchain',
live: status === 'live',
quoteLive: status === 'live',
executionLive: status === 'live',
supportedLegTypes: ['swap'],
pairs,
notes: ['Canonical Chain 138 upstream-native Uniswap v3 router/quoter path.'],
};
}
function chain138BalancerCapabilities(): ProviderCapabilityRecord {
const assets = getChain138RoutingAssets();
const vault = normalizeAddress(process.env.BALANCER_VAULT || CHAIN138_PILOT_BALANCER_VAULT);
const wethUsdtPoolId = process.env.BALANCER_WETH_USDT_POOL_ID || CHAIN138_PILOT_BALANCER_WETH_USDT_POOL_ID;
const wethUsdcPoolId = process.env.BALANCER_WETH_USDC_POOL_ID || CHAIN138_PILOT_BALANCER_WETH_USDC_POOL_ID;
const pairs = [
...bidirectionalPair({
chainId: CHAIN_138,
provider: 'balancer',
tokenASymbol: 'WETH',
tokenAAddress: assets.WETH.address,
tokenBSymbol: 'USDT',
tokenBAddress: assets.USDT.address,
status: vault && wethUsdtPoolId ? 'live' : 'planned',
target: vault,
providerData: vault && wethUsdtPoolId ? { poolId: wethUsdtPoolId } : undefined,
providerDataHex: vault && wethUsdtPoolId ? encodeBalancerRoute(wethUsdtPoolId) : undefined,
notes: ['Enabled only after Chain 138 Balancer pool IDs are set.'],
reason: vault && wethUsdtPoolId ? undefined : 'Configure BALANCER_VAULT and BALANCER_WETH_USDT_POOL_ID once the pool exists.',
}),
...bidirectionalPair({
chainId: CHAIN_138,
provider: 'balancer',
tokenASymbol: 'WETH',
tokenAAddress: assets.WETH.address,
tokenBSymbol: 'USDC',
tokenBAddress: assets.USDC.address,
status: vault && wethUsdcPoolId ? 'live' : 'planned',
target: vault,
providerData: vault && wethUsdcPoolId ? { poolId: wethUsdcPoolId } : undefined,
providerDataHex: vault && wethUsdcPoolId ? encodeBalancerRoute(wethUsdcPoolId) : undefined,
notes: ['Enabled only after Chain 138 Balancer pool IDs are set.'],
reason: vault && wethUsdcPoolId ? undefined : 'Configure BALANCER_VAULT and BALANCER_WETH_USDC_POOL_ID once the pool exists.',
}),
];
return {
chainId: CHAIN_138,
provider: 'balancer',
executionMode: 'onchain',
live: pairs.some((pair) => pair.status === 'live'),
quoteLive: pairs.some((pair) => pair.status === 'live'),
executionLive: pairs.some((pair) => pair.status === 'live'),
supportedLegTypes: ['swap'],
pairs,
notes: ['Balancer stays disabled until the minimum viable Chain 138 venue set exists.'],
};
}
function chain138CurveCapabilities(): ProviderCapabilityRecord {
const assets = getChain138RoutingAssets();
const curvePool = normalizeAddress(process.env.CURVE_3POOL || CHAIN138_PILOT_CURVE_3POOL);
const status = liveOrPlannedAddress(curvePool);
const pairs = [
...bidirectionalPair({
chainId: CHAIN_138,
provider: 'curve',
tokenASymbol: 'USDT',
tokenAAddress: assets.USDT.address,
tokenBSymbol: 'USDC',
tokenBAddress: assets.USDC.address,
status,
target: curvePool,
providerData: status === 'live' ? { i: 0, j: 1, useUnderlying: false } : undefined,
providerDataHex: status === 'live' ? encodeCurveRoute(0, 1, false) : undefined,
notes: ['Curve is reserved for stable-stable legs; no direct WETH path is configured.'],
reason: status === 'planned' ? 'Configure CURVE_3POOL once the Chain 138 stable-stable venue is live.' : undefined,
}),
];
return {
chainId: CHAIN_138,
provider: 'curve',
executionMode: 'onchain',
live: status === 'live',
quoteLive: status === 'live',
executionLive: status === 'live',
supportedLegTypes: ['swap'],
pairs,
notes: ['Curve is intentionally constrained to stable-stable execution for router-v2.'],
};
}
function chain138PartnerCapabilities(): ProviderCapabilityRecord {
return {
chainId: CHAIN_138,
provider: 'partner',
executionMode: 'partner',
live: false,
quoteLive: false,
executionLive: false,
supportedLegTypes: ['swap', 'bridge'],
pairs: [],
notes: ['1inch, 0x, and LiFi remain partner payload adapters until explicit Chain 138 live support is verified.'],
};
}
function chain138OneInchCapabilities(): ProviderCapabilityRecord {
const assets = getChain138RoutingAssets();
const router = normalizeAddress(process.env.ONEINCH_ROUTER || CHAIN138_PILOT_ONEINCH_ROUTER);
const status = router ? 'live' : 'planned';
const pairs = [
...bidirectionalPair({
chainId: CHAIN_138,
provider: 'one_inch',
tokenASymbol: 'WETH',
tokenAAddress: assets.WETH.address,
tokenBSymbol: 'USDT',
tokenBAddress: assets.USDT.address,
status,
target: router,
providerData: status === 'live' ? { executor: router, allowanceTarget: router } : undefined,
providerDataHex: status === 'live' ? encodeOneInchRoute(router) : undefined,
notes: ['Enabled after the Chain 138 pilot-compatible 1inch router is deployed and funded.'],
reason: status === 'planned' ? 'Configure ONEINCH_ROUTER once the Chain 138 pilot-compatible router is live.' : undefined,
}),
...bidirectionalPair({
chainId: CHAIN_138,
provider: 'one_inch',
tokenASymbol: 'WETH',
tokenAAddress: assets.WETH.address,
tokenBSymbol: 'USDC',
tokenBAddress: assets.USDC.address,
status,
target: router,
providerData: status === 'live' ? { executor: router, allowanceTarget: router } : undefined,
providerDataHex: status === 'live' ? encodeOneInchRoute(router) : undefined,
notes: ['Enabled after the Chain 138 pilot-compatible 1inch router is deployed and funded.'],
reason: status === 'planned' ? 'Configure ONEINCH_ROUTER once the Chain 138 pilot-compatible router is live.' : undefined,
}),
];
return {
chainId: CHAIN_138,
provider: 'one_inch',
executionMode: 'onchain',
live: status === 'live',
quoteLive: status === 'live',
executionLive: status === 'live',
supportedLegTypes: ['swap'],
pairs,
notes: ['1inch is promoted from partner-only placeholder to an executable Chain 138 pilot-compatible router when configured.'],
};
}
export function getProviderCapabilities(chainId: number): ProviderCapabilityRecord[] {
if (chainId !== CHAIN_138) return [];
return [
chain138DodoCapabilities(),
chain138DodoV3Capabilities(),
chain138UniswapCapabilities(),
chain138BalancerCapabilities(),
chain138CurveCapabilities(),
chain138OneInchCapabilities(),
chain138PartnerCapabilities(),
];
}
export function findProviderPairCapability(
chainId: number,
provider: PlannerProvider,
tokenInAddress: string,
tokenOutAddress: string
): ProviderPairCapability | undefined {
const normalizedIn = normalizeAddress(tokenInAddress);
const normalizedOut = normalizeAddress(tokenOutAddress);
return getProviderCapabilities(chainId)
.find((record) => record.provider === provider)
?.pairs.find(
(pair) =>
pair.tokenInAddress === normalizedIn &&
pair.tokenOutAddress === normalizedOut
);
}

View File

@@ -0,0 +1,53 @@
import fs from 'fs';
import path from 'path';
import { createRequire } from 'module';
const requireHere = createRequire(__filename);
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 buildTokenMappingLoaderCandidates(): string[] {
return uniquePaths([
process.env.TOKEN_MAPPING_LOADER_PATH,
process.env.GRU_TRANSPORT_LOADER_PATH,
process.env.PROXMOX_TOKEN_MAPPING_LOADER_PATH,
path.resolve(process.cwd(), 'config', 'token-mapping-loader.cjs'),
path.resolve(process.cwd(), '..', 'config', 'token-mapping-loader.cjs'),
path.resolve(process.cwd(), '..', '..', 'config', 'token-mapping-loader.cjs'),
path.resolve(process.cwd(), '..', '..', '..', 'config', 'token-mapping-loader.cjs'),
path.resolve(__dirname, '../../../../../config/token-mapping-loader.cjs'),
]);
}
export function resolveTokenMappingLoaderPath(): string | null {
for (const candidate of buildTokenMappingLoaderCandidates()) {
if (fs.existsSync(candidate)) {
return candidate;
}
}
return null;
}
export function loadTokenMappingLoader<T>(): T | null {
const loaderPath = resolveTokenMappingLoaderPath();
if (!loaderPath) return null;
try {
return requireHere(loaderPath) as T;
} catch {
return null;
}
}

View File

@@ -0,0 +1,130 @@
import { getCanonicalTokenByAddress, getCanonicalTokenBySymbol } from './canonical-tokens';
const CHAIN_138 = 138;
const CHAIN_138_WETH = (
process.env.WETH ||
process.env.WETH9 ||
process.env.WETH_ADDRESS_138 ||
'0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2'
).toLowerCase();
const CHAIN_138_WETH10 = (
process.env.WETH10 ||
process.env.WETH10_ADDRESS ||
process.env.WETH10_ADDRESS_138 ||
'0xf4BB2e28688e89fCcE3c0580D37d36A7672E8A9f'
).toLowerCase();
export interface RoutingAssetSpec {
symbol: string;
address: string;
decimals: number;
kind: 'wrapped' | 'stable' | 'compliant' | 'commodity' | 'monetary_unit';
}
function requireSymbolAddress(chainId: number, symbol: string, fallback?: string): string {
const canonical = getCanonicalTokenBySymbol(chainId, symbol)?.addresses?.[chainId];
const value = String(canonical || fallback || '').trim().toLowerCase();
return value;
}
export function getChain138RoutingAssets(): Record<string, RoutingAssetSpec> {
return {
WETH: {
symbol: 'WETH',
address: CHAIN_138_WETH,
decimals: 18,
kind: 'wrapped',
},
WETH10: {
symbol: 'WETH10',
address: CHAIN_138_WETH10,
decimals: 18,
kind: 'wrapped',
},
USDT: {
symbol: 'USDT',
address: requireSymbolAddress(CHAIN_138, 'USDT', '0x004b63A7B5b0E06f6bB6adb4a5F9f590BF3182D1'),
decimals: 6,
kind: 'stable',
},
USDC: {
symbol: 'USDC',
address: requireSymbolAddress(CHAIN_138, 'USDC', '0x71D6687F38b93CCad569Fa6352c876eea967201b'),
decimals: 6,
kind: 'stable',
},
cUSDT: {
symbol: 'cUSDT',
address: requireSymbolAddress(CHAIN_138, 'cUSDT', '0x93E66202A11B1772E55407B32B44e5Cd8eda7f22'),
decimals: 6,
kind: 'compliant',
},
cUSDC: {
symbol: 'cUSDC',
address: requireSymbolAddress(CHAIN_138, 'cUSDC', '0xf22258f57794CC8E06237084b353Ab30fFfa640b'),
decimals: 6,
kind: 'compliant',
},
cBTC: {
symbol: 'cBTC',
address: requireSymbolAddress(CHAIN_138, 'cBTC', '0xcb7c000000000000000000000000000000000138'),
decimals: 8,
kind: 'monetary_unit',
},
cEURT: {
symbol: 'cEURT',
address: requireSymbolAddress(CHAIN_138, 'cEURT', '0xdf4b71c61E5912712C1Bdd451416B9aC26949d72'),
decimals: 6,
kind: 'compliant',
},
cXAUC: {
symbol: 'cXAUC',
address: requireSymbolAddress(CHAIN_138, 'cXAUC', '0x290E52a8819A4fbD0714E517225429aA2B70EC6b'),
decimals: 6,
kind: 'commodity',
},
};
}
export function getDefaultIntermediateAddresses(chainId: number): string[] {
if (chainId !== CHAIN_138) return [];
const assets = getChain138RoutingAssets();
return [
assets.WETH.address,
assets.USDT.address,
assets.USDC.address,
assets.cUSDT.address,
assets.cUSDC.address,
assets.cBTC.address,
];
}
export function getCommodityIntermediateAddresses(chainId: number): string[] {
if (chainId !== CHAIN_138) return [];
return [getChain138RoutingAssets().cXAUC.address];
}
export function getRoutingSymbolForAddress(chainId: number, address: string): string | undefined {
const normalized = address.trim().toLowerCase();
if (chainId === CHAIN_138) {
const assets = Object.values(getChain138RoutingAssets());
const asset = assets.find((entry) => entry.address === normalized);
if (asset) return asset.symbol;
}
return getCanonicalTokenByAddress(chainId, normalized)?.symbol;
}
export function getRoutingAddressForSymbol(chainId: number, symbol: string): string | undefined {
if (chainId === CHAIN_138) {
const assets = getChain138RoutingAssets();
const normalized = symbol.trim().toUpperCase();
if (normalized === 'WETH') {
return assets.WETH.address;
}
if (normalized === 'WETH10') {
return assets.WETH10.address;
}
}
return getCanonicalTokenBySymbol(chainId, symbol)?.addresses?.[chainId]?.toLowerCase();
}

View File

@@ -0,0 +1,68 @@
import { PlannerConstraints, RoutingPolicy } from '../services/planner-v2-types';
import {
getCommodityIntermediateAddresses,
getDefaultIntermediateAddresses,
} from './routing-assets';
const CHAIN_138 = 138;
export function resolveRoutingPolicy(
chainId: number,
constraints: PlannerConstraints = {}
): RoutingPolicy {
const requestedProfile = constraints.complianceProfile || 'standard';
const defaultIntermediates = getDefaultIntermediateAddresses(chainId);
const commodityIntermediates = getCommodityIntermediateAddresses(chainId);
const baseStandard: RoutingPolicy = {
profile: 'standard',
allowedProviders: ['dodo', 'dodo_v3', 'uniswap_v3', 'balancer', 'curve', 'one_inch'],
defaultIntermediateAddresses: defaultIntermediates,
allowBridge: constraints.allowBridge !== false,
allowedBridgeLabels: ['GRUTransport', 'CCIPStableBridge', 'CCIPWETH9Bridge', 'UniversalCCIPBridge', 'AlltraAdapter'],
maxLegs: Math.min(Math.max(constraints.maxLegs || 3, 1), 3),
allowCommodityIntermediates: constraints.allowCommodityIntermediates === true,
notes: ['Standard policy allows live Chain 138 venues, including the DODO v3 pilot, but still requires explicit opt-in for commodity intermediates.'],
};
const baseInstitutional: RoutingPolicy = {
profile: 'institutional',
allowedProviders: ['dodo'],
defaultIntermediateAddresses: defaultIntermediates,
allowBridge: constraints.allowBridge !== false,
allowedBridgeLabels: ['GRUTransport', 'CCIPStableBridge', 'UniversalCCIPBridge', 'AlltraAdapter'],
maxLegs: Math.min(Math.max(constraints.maxLegs || 3, 1), 3),
allowCommodityIntermediates: false,
notes: ['Institutional policy restricts execution to canonical compliant assets and approved bridge rails.'],
};
const basePolicy = requestedProfile === 'institutional' ? baseInstitutional : baseStandard;
const allowedProviders = constraints.allowedProviders?.length
? basePolicy.allowedProviders.filter((provider) => constraints.allowedProviders?.includes(provider))
: basePolicy.allowedProviders;
const allowedIntermediates = constraints.allowedIntermediates?.length
? constraints.allowedIntermediates.map((value) => value.toLowerCase())
: basePolicy.defaultIntermediateAddresses.slice();
if (basePolicy.allowCommodityIntermediates) {
for (const commodity of commodityIntermediates) {
if (!allowedIntermediates.includes(commodity)) {
allowedIntermediates.push(commodity);
}
}
}
return {
...basePolicy,
allowedProviders,
defaultIntermediateAddresses: allowedIntermediates,
allowedBridgeLabels: constraints.preferredBridges?.length
? basePolicy.allowedBridgeLabels.filter((bridge) => constraints.preferredBridges?.includes(bridge))
: basePolicy.allowedBridgeLabels,
notes: [
...basePolicy.notes,
...(chainId === CHAIN_138 ? ['Chain 138 routing defaults to WETH, USDT, USDC, cUSDT, and cUSDC intermediates.'] : []),
],
};
}

View File

@@ -0,0 +1,47 @@
import { PlannerMetricsRepository } from './planner-metrics-repo';
describe('PlannerMetricsRepository', () => {
function makeRepoWithRejectedQuery(error: Error & { code?: string }): PlannerMetricsRepository {
const repo = new PlannerMetricsRepository();
(repo as any).pool = {
query: jest.fn().mockRejectedValue(error),
};
return repo;
}
it('treats transient connection errors as cache misses', async () => {
const repo = makeRepoWithRejectedQuery(
Object.assign(new Error('connect ECONNREFUSED 172.18.0.3:5432'), { code: 'ECONNREFUSED' })
);
await expect(repo.getCachedPlan('request-hash')).resolves.toBeNull();
});
it('skips cache writes when the planner metrics database is temporarily unreachable', async () => {
const repo = makeRepoWithRejectedQuery(
Object.assign(new Error('connect EHOSTUNREACH 76.53.10.36:443'), { code: 'EHOSTUNREACH' })
);
await expect(
repo.cachePlan('request-hash', {
planId: 'plan-1',
generatedAt: new Date().toISOString(),
decision: 'direct-pool',
sourceChainId: 138,
destinationChainId: 138,
tokenIn: '0x1',
tokenOut: '0x2',
estimatedAmountOut: '1000',
minAmountOut: '990',
estimatedGasUsd: 0.1,
legs: [],
alternatives: [],
confidenceScore: 0.9,
riskFlags: [],
selectedRouteReason: 'selected',
rejectedAlternatives: [],
staleness: { maxFreshnessSeconds: 0, hasStaleLeg: false },
})
).resolves.toBeUndefined();
});
});

View File

@@ -0,0 +1,183 @@
import { Pool } from 'pg';
import { getDatabasePool } from '../client';
import { ProviderCapabilityRecord, PlannerResponse } from '../../services/planner-v2-types';
import { logger } from '../../utils/logger';
interface CachedPlanRow {
plan_id: string;
response_json: PlannerResponse;
}
export class PlannerMetricsRepository {
private pool: Pool;
constructor() {
this.pool = getDatabasePool();
}
private isMissingRelationError(error: unknown): boolean {
if (!error || typeof error !== 'object') {
return false;
}
const code = (error as { code?: string }).code;
const message = (error as { message?: string }).message || '';
return code === '42P01' || (message.includes('relation "') && message.includes('" does not exist'));
}
private isTransientConnectionError(error: unknown): boolean {
if (!error || typeof error !== 'object') {
return false;
}
const code = String((error as { code?: string }).code || '');
const message = String((error as { message?: string }).message || '');
return (
['ECONNREFUSED', 'ECONNRESET', 'EHOSTUNREACH', 'ENOTFOUND', 'EAI_AGAIN', 'ETIMEDOUT', '57P01'].includes(code) ||
message.includes('connect ECONNREFUSED') ||
message.includes('connect EHOSTUNREACH') ||
message.includes('Connection terminated unexpectedly')
);
}
private isNonFatalError(error: unknown): boolean {
return this.isMissingRelationError(error) || this.isTransientConnectionError(error);
}
private logSuppressedError(action: string, error: unknown): void {
const message = error instanceof Error ? error.message : String(error);
logger.warn(`Planner metrics ${action} skipped: ${message}`);
}
async getCachedPlan(requestHash: string): Promise<PlannerResponse | null> {
try {
const result = await this.pool.query<CachedPlanRow>(
`SELECT plan_id, response_json
FROM route_plan_cache
WHERE request_hash = $1 AND (expires_at IS NULL OR expires_at > NOW())
ORDER BY created_at DESC
LIMIT 1`,
[requestHash]
);
if (result.rows.length === 0) {
return null;
}
return result.rows[0].response_json;
} catch (error) {
if (this.isNonFatalError(error)) {
this.logSuppressedError('cache lookup', error);
return null;
}
throw error;
}
}
async cachePlan(requestHash: string, response: PlannerResponse, ttlSeconds: number = 30): Promise<void> {
try {
await this.pool.query(
`INSERT INTO route_plan_cache (
plan_id, request_hash, chain_id, destination_chain_id, decision, response_json, expires_at
)
VALUES ($1, $2, $3, $4, $5, $6::jsonb, NOW() + ($7::text || ' seconds')::interval)
ON CONFLICT (plan_id) DO UPDATE SET
request_hash = EXCLUDED.request_hash,
chain_id = EXCLUDED.chain_id,
destination_chain_id = EXCLUDED.destination_chain_id,
decision = EXCLUDED.decision,
response_json = EXCLUDED.response_json,
expires_at = EXCLUDED.expires_at,
created_at = NOW()`,
[
response.planId,
requestHash,
response.sourceChainId,
response.destinationChainId,
response.decision,
JSON.stringify(response),
String(ttlSeconds),
]
);
} catch (error) {
if (this.isNonFatalError(error)) {
this.logSuppressedError('cache write', error);
return;
}
throw error;
}
}
async recordProviderSnapshots(chainId: number, records: ProviderCapabilityRecord[]): Promise<void> {
try {
for (const record of records) {
await this.pool.query(
`INSERT INTO provider_health_snapshots (
chain_id, provider, status, supports_execution, supports_quote, metadata
)
VALUES ($1, $2, $3, $4, $5, $6::jsonb)`,
[
chainId,
record.provider,
record.live ? 'live' : 'planned',
record.executionLive,
record.quoteLive,
JSON.stringify({
executionMode: record.executionMode,
supportedLegTypes: record.supportedLegTypes,
pairs: record.pairs,
notes: record.notes || [],
}),
]
);
}
} catch (error) {
if (this.isNonFatalError(error)) {
this.logSuppressedError('provider snapshot write', error);
return;
}
throw error;
}
}
async recordPlannedRouteMetrics(response: PlannerResponse): Promise<void> {
try {
for (const [index, leg] of response.legs.entries()) {
await this.pool.query(
`INSERT INTO route_execution_metrics (
plan_id, chain_id, provider, hop_index, token_in_address, token_out_address,
estimated_amount_out, status, metadata
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::jsonb)`,
[
response.planId,
leg.sourceChainId,
leg.provider,
index,
leg.tokenInAddress,
leg.tokenOutAddress,
leg.estimatedAmountOut,
'planned',
JSON.stringify({
kind: leg.kind,
target: leg.target,
poolAddress: leg.poolAddress,
providerData: leg.providerData || {},
bridgeType: leg.bridgeType,
bridgeAddress: leg.bridgeAddress,
gasEstimate: leg.gasEstimate,
freshnessSeconds: leg.freshnessSeconds,
notes: leg.notes || [],
}),
]
);
}
} catch (error) {
if (this.isNonFatalError(error)) {
this.logSuppressedError('route metrics write', error);
return;
}
throw error;
}
}
}

View File

@@ -4,6 +4,7 @@ import { existsSync } from 'fs';
import { ApiServer } from './api/server';
import { closeDatabasePool } from './database/client';
import { logger } from './utils/logger';
import { startRouteMatrixScheduler } from './services/route-matrix-scheduler';
// Load smom-dbis-138 root .env first (single source); works from dist/ or src/
const rootEnvCandidates = [
@@ -26,6 +27,7 @@ try {
} catch (_) { /* optional when run outside proxmox repo */ }
const server = new ApiServer();
startRouteMatrixScheduler();
// Start server
server.start().catch((error) => {

View File

@@ -95,11 +95,10 @@ export class ChainIndexer {
try {
// 1. Index pools
logger.info(`Indexing pools for chain ${this.chainId}...`);
await this.poolIndexer.indexAllPools();
const pools = await this.poolIndexer.indexAllPools();
// 2. Discover and index tokens from pools
logger.info(`Discovering tokens for chain ${this.chainId}...`);
const pools = await this.poolIndexer.indexAllPools();
const tokenAddresses = new Set<string>();
pools.forEach((pool) => {
tokenAddresses.add(pool.token0Address);
@@ -110,7 +109,7 @@ export class ChainIndexer {
// 3. Calculate volumes and update market data
logger.info(`Calculating volumes for chain ${this.chainId}...`);
for (const tokenAddress of tokenAddresses) {
await this.updateMarketData(tokenAddress);
await this.updateMarketData(tokenAddress, pools);
}
// 4. Generate OHLCV data
@@ -139,7 +138,7 @@ export class ChainIndexer {
/**
* Update market data for a token
*/
private async updateMarketData(tokenAddress: string): Promise<void> {
private async updateMarketData(tokenAddress: string, pools: Awaited<ReturnType<PoolIndexer['indexAllPools']>>): Promise<void> {
try {
// Calculate on-chain volume
const volumeMetrics = await this.volumeCalculator.calculateTokenVolume(
@@ -158,7 +157,6 @@ export class ChainIndexer {
const externalData = coingeckoData || dexscreenerData || cmcData;
// Get pools for liquidity calculation
const pools = await this.poolIndexer.indexAllPools();
const tokenPools = pools.filter(
(p) => p.token0Address === tokenAddress || p.token1Address === tokenAddress
);

View File

@@ -82,6 +82,11 @@ const UNIVERAL_CCIP_ABI = [
'event MessageReceived(bytes32 indexed messageId, uint64 indexed sourceChainSelector, address sender, address token, uint256 amount)',
];
const CROSS_CHAIN_QUERY_FALLBACK_BLOCK_SPAN = Math.max(
1,
Number(process.env.CROSS_CHAIN_QUERY_FALLBACK_BLOCK_SPAN || 5000)
);
function nowSec(): number {
return Math.floor(Date.now() / 1000);
}
@@ -90,6 +95,47 @@ function msAgo(hours: number): number {
return nowSec() - hours * 3600;
}
function isRpcRangeLimitError(error: unknown): boolean {
const parts = [
typeof error === 'object' && error ? (error as { message?: string }).message : '',
typeof error === 'object' && error ? (error as { shortMessage?: string }).shortMessage : '',
typeof error === 'object' && error
? ((error as { error?: { message?: string } }).error?.message ?? '')
: '',
];
return parts.some((part) =>
typeof part === 'string' && /range limit|exceeds maximum rpc range/i.test(part)
);
}
async function queryFilterWithRangeFallback(
contract: ethers.Contract,
filter: ReturnType<ethers.Contract['filters'][string]> | { address: string },
fromBlock: number,
toBlock: number
): Promise<ethers.EventLog[]> {
try {
return (await contract.queryFilter(filter as never, fromBlock, toBlock)) as ethers.EventLog[];
} catch (error) {
if (!isRpcRangeLimitError(error) || fromBlock >= toBlock) {
throw error;
}
}
const logs: ethers.EventLog[] = [];
for (
let start = fromBlock;
start <= toBlock;
start += CROSS_CHAIN_QUERY_FALLBACK_BLOCK_SPAN
) {
const end = Math.min(start + CROSS_CHAIN_QUERY_FALLBACK_BLOCK_SPAN - 1, toBlock);
const chunk = (await contract.queryFilter(filter as never, start, end)) as ethers.EventLog[];
logs.push(...chunk);
}
return logs;
}
/** Fetch CrossChainTransferInitiated events from CCIP WETH bridges */
async function fetchCCIPEvents(
provider: ethers.JsonRpcProvider,
@@ -101,7 +147,7 @@ async function fetchCCIPEvents(
try {
const contract = new ethers.Contract(bridge.address, CCIP_TRANSFER_ABI, provider);
const filter = contract.filters.CrossChainTransferInitiated();
const logs = await contract.queryFilter(filter, fromBlock, toBlock);
const logs = await queryFilterWithRangeFallback(contract, filter, fromBlock, toBlock);
for (const log of logs) {
const args = (log as ethers.EventLog).args as unknown as { messageId: string; sender: string; destinationChainSelector: bigint; recipient: string; amount: bigint };
@@ -143,7 +189,7 @@ async function fetchSwapBridgeEvents(
try {
const contract = new ethers.Contract(address, SWAP_BRIDGE_ABI, provider);
const filter = contract.filters.SwapAndBridgeExecuted();
const logs = await contract.queryFilter(filter, fromBlock, toBlock);
const logs = await queryFilterWithRangeFallback(contract, filter, fromBlock, toBlock);
for (const log of logs) {
const args = (log as ethers.EventLog).args as unknown as { sourceToken: string; bridgeableToken: string; amountIn: bigint; amountToBridge: bigint; destinationChainSelector: bigint; recipient: string; messageId: string };
@@ -184,7 +230,8 @@ async function fetchAlltraEvents(
try {
const lockContract = new ethers.Contract(bridge.address, ALLTRA_LOCK_ABI, provider);
const lockLogs = await lockContract.queryFilter(
const lockLogs = await queryFilterWithRangeFallback(
lockContract,
lockContract.filters.LockForAlltra(),
fromBlock,
toBlock
@@ -211,7 +258,8 @@ async function fetchAlltraEvents(
try {
const adapterContract = new ethers.Contract(bridge.address, ALLTRA_ADAPTER_ABI, provider);
const initLogs = await adapterContract.queryFilter(
const initLogs = await queryFilterWithRangeFallback(
adapterContract,
adapterContract.filters.AlltraBridgeInitiated(),
fromBlock,
toBlock
@@ -249,7 +297,7 @@ async function fetchUniversalCCIPEvents(
try {
const contract = new ethers.Contract(bridge.address, UNIVERAL_CCIP_ABI, provider);
const filter = contract.filters.BridgeExecuted?.() ?? { address: bridge.address };
const logs = await contract.queryFilter(filter, fromBlock, toBlock);
const logs = await queryFilterWithRangeFallback(contract, filter, fromBlock, toBlock);
for (const log of logs) {
const args = (log as ethers.EventLog).args as unknown as { messageId: string; token: string; sender: string; amount: bigint; destinationChain: bigint; recipient: string };

View File

@@ -0,0 +1,61 @@
const mockQuery = jest.fn();
const mockLoggerInfo = jest.fn();
jest.mock('../database/client', () => ({
getDatabasePool: () => ({ query: mockQuery }),
}));
jest.mock('../utils/logger', () => ({
logger: {
info: mockLoggerInfo,
warn: jest.fn(),
error: jest.fn(),
},
}));
import { OHLCVGenerator } from './ohlcv-generator';
describe('OHLCVGenerator', () => {
beforeEach(() => {
mockQuery.mockReset();
mockLoggerInfo.mockReset();
});
it('returns empty data and logs once when swap tables are not provisioned yet', async () => {
mockQuery.mockRejectedValue({
code: '42P01',
message: 'relation "swap_events" does not exist',
});
const generator = new OHLCVGenerator();
await expect(
generator.generateOHLCV(
138,
'0xToken',
'1h',
new Date('2026-03-01T00:00:00.000Z'),
new Date('2026-03-02T00:00:00.000Z')
)
).resolves.toEqual([]);
await expect(
generator.generateOHLCV(
138,
'0xToken',
'1h',
new Date('2026-03-01T00:00:00.000Z'),
new Date('2026-03-02T00:00:00.000Z')
)
).resolves.toEqual([]);
expect(mockLoggerInfo).toHaveBeenCalledTimes(1);
expect(mockLoggerInfo).toHaveBeenCalledWith(
'Skipping OHLCV generation; relation "swap_events" is not available yet',
expect.objectContaining({
operation: 'OHLCV generation',
relation: 'swap_events',
})
);
});
});

View File

@@ -1,5 +1,6 @@
import { Pool } from 'pg';
import { getDatabasePool } from '../database/client';
import { logger } from '../utils/logger';
export type OHLCVInterval = '5m' | '15m' | '1h' | '4h' | '24h';
@@ -15,11 +16,48 @@ export interface OHLCVData {
export class OHLCVGenerator {
private pool: Pool;
private static readonly missingRelationWarnings = new Set<string>();
constructor() {
this.pool = getDatabasePool();
}
private normalizePoolAddress(poolAddress?: string): string {
return poolAddress?.toLowerCase() || '';
}
private isMissingRelationError(error: unknown): boolean {
if (!error || typeof error !== 'object') {
return false;
}
const code = (error as { code?: string }).code;
const message = (error as { message?: string }).message || '';
return code === '42P01' || (message.includes('relation "') && message.includes('" does not exist'));
}
private getMissingRelationName(error: unknown): string {
const message = typeof error === 'object' && error && 'message' in error
? String((error as { message?: string }).message || '')
: '';
const match = message.match(/relation "([^"]+)"/);
return match?.[1] || 'unknown relation';
}
private logMissingRelationOnce(operation: string, error: unknown): void {
const relation = this.getMissingRelationName(error);
const key = `${operation}:${relation}`;
if (OHLCVGenerator.missingRelationWarnings.has(key)) {
return;
}
OHLCVGenerator.missingRelationWarnings.add(key);
logger.info(`Skipping ${operation}; relation "${relation}" is not available yet`, {
operation,
relation,
});
}
/**
* Generate OHLCV data for a token
*/
@@ -31,63 +69,71 @@ export class OHLCVGenerator {
to: Date,
poolAddress?: string
): Promise<OHLCVData[]> {
const intervalMs = this.getIntervalMs(interval);
try {
const intervalMs = this.getIntervalMs(interval);
// Get swap events for the time range
let query = `
SELECT timestamp, amount_usd, price_usd
FROM swap_events
WHERE chain_id = $1
AND (token0_address = $2 OR token1_address = $2)
AND timestamp >= $3
AND timestamp <= $4
`;
const params: (string | number | Date)[] = [chainId, tokenAddress.toLowerCase(), from, to];
// Get swap events for the time range
let query = `
SELECT timestamp, amount_usd, price_usd
FROM swap_events
WHERE chain_id = $1
AND (token0_address = $2 OR token1_address = $2)
AND timestamp >= $3
AND timestamp <= $4
`;
const params: (string | number | Date)[] = [chainId, tokenAddress.toLowerCase(), from, to];
if (poolAddress) {
query += ` AND pool_address = $5`;
params.push(poolAddress.toLowerCase());
}
query += ` ORDER BY timestamp ASC`;
const result = await this.pool.query(query, params);
if (result.rows.length === 0) {
return [];
}
// Group swaps by interval
const intervals = new Map<number, OHLCVData>();
result.rows.forEach((row) => {
const timestamp = new Date(row.timestamp);
const intervalStart = Math.floor(timestamp.getTime() / intervalMs) * intervalMs;
const price = parseFloat(row.price_usd || '0');
const volume = parseFloat(row.amount_usd || '0');
if (!intervals.has(intervalStart)) {
intervals.set(intervalStart, {
timestamp: new Date(intervalStart),
open: price,
high: price,
low: price,
close: price,
volume: 0,
volumeUsd: 0,
});
if (poolAddress) {
query += ` AND pool_address = $5`;
params.push(poolAddress.toLowerCase());
}
const ohlcv = intervals.get(intervalStart)!;
ohlcv.high = Math.max(ohlcv.high, price);
ohlcv.low = Math.min(ohlcv.low, price);
ohlcv.close = price;
ohlcv.volume += 1;
ohlcv.volumeUsd += volume;
});
query += ` ORDER BY timestamp ASC`;
// Convert map to array and sort by timestamp
return Array.from(intervals.values()).sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
const result = await this.pool.query(query, params);
if (result.rows.length === 0) {
return [];
}
// Group swaps by interval
const intervals = new Map<number, OHLCVData>();
result.rows.forEach((row) => {
const timestamp = new Date(row.timestamp);
const intervalStart = Math.floor(timestamp.getTime() / intervalMs) * intervalMs;
const price = parseFloat(row.price_usd || '0');
const volume = parseFloat(row.amount_usd || '0');
if (!intervals.has(intervalStart)) {
intervals.set(intervalStart, {
timestamp: new Date(intervalStart),
open: price,
high: price,
low: price,
close: price,
volume: 0,
volumeUsd: 0,
});
}
const ohlcv = intervals.get(intervalStart)!;
ohlcv.high = Math.max(ohlcv.high, price);
ohlcv.low = Math.min(ohlcv.low, price);
ohlcv.close = price;
ohlcv.volume += 1;
ohlcv.volumeUsd += volume;
});
// Convert map to array and sort by timestamp
return Array.from(intervals.values()).sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
} catch (error) {
if (this.isMissingRelationError(error)) {
this.logMissingRelationOnce('OHLCV generation', error);
return [];
}
throw error;
}
}
/**
@@ -102,17 +148,19 @@ export class OHLCVGenerator {
): Promise<void> {
if (data.length === 0) return;
const values = data.map((d, i) => {
const base = i * 8;
return `($${base + 1}, $${base + 2}, $${base + 3}, $${base + 4}, $${base + 5}, $${base + 6}, $${base + 7}, $${base + 8})`;
const normalizedPoolAddress = this.normalizePoolAddress(poolAddress);
const values = data.map((_, i) => {
const base = i * 11;
return `($${base + 1}, $${base + 2}, $${base + 3}, $${base + 4}, $${base + 5}, $${base + 6}, $${base + 7}, $${base + 8}, $${base + 9}, $${base + 10}, $${base + 11})`;
});
const params: (string | number | Date | null)[] = [];
const params: (string | number | Date)[] = [];
data.forEach((d) => {
params.push(
chainId,
tokenAddress.toLowerCase(),
poolAddress?.toLowerCase() || null,
normalizedPoolAddress,
interval,
d.open,
d.high,
@@ -124,21 +172,29 @@ export class OHLCVGenerator {
);
});
await this.pool.query(
`INSERT INTO token_ohlcv (
chain_id, token_address, pool_address, interval_type,
open_price, high_price, low_price, close_price, volume, volume_usd, timestamp
)
VALUES ${values.join(', ')}
ON CONFLICT (chain_id, token_address, pool_address, interval_type, timestamp) DO UPDATE SET
open_price = EXCLUDED.open_price,
high_price = EXCLUDED.high_price,
low_price = EXCLUDED.low_price,
close_price = EXCLUDED.close_price,
volume = EXCLUDED.volume,
volume_usd = EXCLUDED.volume_usd`,
params
);
try {
await this.pool.query(
`INSERT INTO token_ohlcv (
chain_id, token_address, pool_address, interval_type,
open_price, high_price, low_price, close_price, volume, volume_usd, timestamp
)
VALUES ${values.join(', ')}
ON CONFLICT (chain_id, token_address, pool_address, interval_type, timestamp) DO UPDATE SET
open_price = EXCLUDED.open_price,
high_price = EXCLUDED.high_price,
low_price = EXCLUDED.low_price,
close_price = EXCLUDED.close_price,
volume = EXCLUDED.volume,
volume_usd = EXCLUDED.volume_usd`,
params
);
} catch (error) {
if (this.isMissingRelationError(error)) {
this.logMissingRelationOnce('OHLCV storage', error);
return;
}
throw error;
}
}
/**
@@ -152,37 +208,45 @@ export class OHLCVGenerator {
to: Date,
poolAddress?: string
): Promise<OHLCVData[]> {
let query = `
SELECT timestamp, open_price, high_price, low_price, close_price, volume, volume_usd
FROM token_ohlcv
WHERE chain_id = $1
AND token_address = $2
AND interval_type = $3
AND timestamp >= $4
AND timestamp <= $5
`;
const params: (string | number | Date)[] = [chainId, tokenAddress.toLowerCase(), interval, from, to];
try {
let query = `
SELECT timestamp, open_price, high_price, low_price, close_price, volume, volume_usd
FROM token_ohlcv
WHERE chain_id = $1
AND token_address = $2
AND interval_type = $3
AND timestamp >= $4
AND timestamp <= $5
`;
const params: (string | number | Date)[] = [chainId, tokenAddress.toLowerCase(), interval, from, to];
if (poolAddress) {
query += ` AND pool_address = $6`;
params.push(poolAddress.toLowerCase());
} else {
query += ` AND pool_address IS NULL`;
if (poolAddress) {
query += ` AND pool_address = $6`;
params.push(this.normalizePoolAddress(poolAddress));
} else {
query += ` AND pool_address = ''`;
}
query += ` ORDER BY timestamp ASC`;
const result = await this.pool.query(query, params);
return result.rows.map((row) => ({
timestamp: row.timestamp,
open: parseFloat(row.open_price),
high: parseFloat(row.high_price),
low: parseFloat(row.low_price),
close: parseFloat(row.close_price),
volume: parseFloat(row.volume || '0'),
volumeUsd: parseFloat(row.volume_usd || '0'),
}));
} catch (error) {
if (this.isMissingRelationError(error)) {
this.logMissingRelationOnce('OHLCV lookup', error);
return [];
}
throw error;
}
query += ` ORDER BY timestamp ASC`;
const result = await this.pool.query(query, params);
return result.rows.map((row) => ({
timestamp: row.timestamp,
open: parseFloat(row.open_price),
high: parseFloat(row.high_price),
low: parseFloat(row.low_price),
close: parseFloat(row.close_price),
volume: parseFloat(row.volume || '0'),
volumeUsd: parseFloat(row.volume_usd || '0'),
}));
}
/**

View File

@@ -2,6 +2,8 @@ import { ethers } from 'ethers';
import { PoolRepository, LiquidityPool, DexType } from '../database/repositories/pool-repo';
import { getDexFactories, UniswapV2Config, UniswapV3Config, DodoConfig } from '../config/dex-factories';
import { logger } from '../utils/logger';
import { shouldExposePublicPool } from '../config/gru-transport';
import { estimateChain138DodoLiquidityUsd } from '../services/chain138-dodo-liquidity';
// UniswapV2 Factory ABI
const UNISWAP_V2_FACTORY_ABI = [
@@ -49,6 +51,8 @@ const DODO_PMM_INTEGRATION_ABI = [
];
export class PoolIndexer {
private static missingDexConfigLogged = new Set<number>();
private static staleDodoPoolsLogged = new Set<string>();
private provider: ethers.JsonRpcProvider;
private poolRepo: PoolRepository;
private chainId: number;
@@ -59,13 +63,35 @@ export class PoolIndexer {
this.poolRepo = new PoolRepository();
}
private async persistPoolIfExposed(pool: LiquidityPool, pools: LiquidityPool[]): Promise<void> {
if (!shouldExposePublicPool(this.chainId, pool.poolAddress, pool.token0Address, pool.token1Address)) {
logger.info(
`Skipping inactive GRU public pool ${pool.poolAddress} on chain ${this.chainId} until gru-transport-active.json marks it active`
);
return;
}
await this.poolRepo.upsertPool(pool);
pools.push(pool);
}
/**
* Index all pools for configured DEX types
*/
async indexAllPools(): Promise<LiquidityPool[]> {
const dexConfig = getDexFactories(this.chainId);
if (!dexConfig) {
logger.warn(`No DEX configuration found for chain ${this.chainId}`);
const hasDexConfig =
!!dexConfig &&
(dexConfig.uniswap_v2?.length ||
dexConfig.uniswap_v3?.length ||
dexConfig.dodo?.length ||
dexConfig.custom?.length);
if (!hasDexConfig) {
if (!PoolIndexer.missingDexConfigLogged.has(this.chainId)) {
logger.info(`Skipping pool indexing for chain ${this.chainId}; no DEX configuration is active yet`);
PoolIndexer.missingDexConfigLogged.add(this.chainId);
}
return [];
}
@@ -133,12 +159,15 @@ export class PoolIndexer {
const quoteReserve = (reservesResult as [bigint, bigint])[1];
const price = typeof priceResult === 'bigint' ? priceResult : BigInt(0);
// totalLiquidityUsd: baseReserve * price (quote per base) + quoteReserve, in 18 decimals then scale
let totalLiquidityUsd = 0;
if (price > 0n) {
const baseValue = (baseReserve * price) / BigInt(1e18);
totalLiquidityUsd = parseFloat(ethers.formatEther(baseValue + quoteReserve));
}
const liquidityUsd = this.chainId === 138
? estimateChain138DodoLiquidityUsd({
token0Address: baseToken,
token1Address: quoteToken,
reserve0: baseReserve,
reserve1: quoteReserve,
price,
})
: { reserve0Usd: 0, reserve1Usd: 0, totalLiquidityUsd: 0 };
const pool: LiquidityPool = {
chainId: this.chainId,
@@ -149,19 +178,24 @@ export class PoolIndexer {
factoryAddress: integrationAddress.toLowerCase(),
reserve0: baseReserve.toString(),
reserve1: quoteReserve.toString(),
reserve0Usd: 0,
reserve1Usd: 0,
totalLiquidityUsd,
reserve0Usd: liquidityUsd.reserve0Usd,
reserve1Usd: liquidityUsd.reserve1Usd,
totalLiquidityUsd: liquidityUsd.totalLiquidityUsd,
volume24h: 0, // No 24h volume from contract; requires event indexer
createdAtBlock: 0,
createdAtTimestamp: createdAt ? new Date(Number(createdAt) * 1000) : new Date(),
lastUpdated: new Date(),
};
await this.poolRepo.upsertPool(pool);
pools.push(pool);
await this.persistPoolIfExposed(pool, pools);
} catch (err) {
logger.warn(`Skipping DODO PMM pool ${poolAddress}; it may have been removed from integration state.`, err);
const stalePoolKey = `${this.chainId}:${poolAddress.toLowerCase()}`;
if (!PoolIndexer.staleDodoPoolsLogged.has(stalePoolKey)) {
logger.info(
`Skipping stale DODO PMM pool ${poolAddress} on chain ${this.chainId}; it is no longer registered in DODOPMMIntegration`
);
PoolIndexer.staleDodoPoolsLogged.add(stalePoolKey);
}
}
}
} catch (error) {
@@ -217,8 +251,7 @@ export class PoolIndexer {
lastUpdated: new Date(),
};
await this.poolRepo.upsertPool(pool);
pools.push(pool);
await this.persistPoolIfExposed(pool, pools);
}
}
} catch (error) {
@@ -273,8 +306,7 @@ export class PoolIndexer {
lastUpdated: new Date(),
};
await this.poolRepo.upsertPool(pool);
pools.push(pool);
await this.persistPoolIfExposed(pool, pools);
}
}
} catch (error) {
@@ -335,8 +367,7 @@ export class PoolIndexer {
lastUpdated: new Date(),
};
await this.poolRepo.upsertPool(pool);
pools.push(pool);
await this.persistPoolIfExposed(pool, pools);
} catch (error) {
logger.error(`Error indexing DODO pool ${poolAddress}:`, error);
}

View File

@@ -0,0 +1,63 @@
const mockQuery = jest.fn();
const mockLoggerInfo = jest.fn();
jest.mock('../database/client', () => ({
getDatabasePool: () => ({ query: mockQuery }),
}));
jest.mock('../utils/logger', () => ({
logger: {
info: mockLoggerInfo,
warn: jest.fn(),
error: jest.fn(),
},
}));
import { VolumeCalculator } from './volume-calculator';
describe('VolumeCalculator', () => {
beforeEach(() => {
mockQuery.mockReset();
mockLoggerInfo.mockReset();
});
it('returns zero metrics and logs once when market-data tables are not provisioned yet', async () => {
mockQuery.mockRejectedValue({
code: '42P01',
message: 'relation "liquidity_pools" does not exist',
});
const calculator = new VolumeCalculator();
await expect(calculator.calculateTokenVolume(138, '0xToken')).resolves.toEqual({
volume5m: 0,
volume1h: 0,
volume24h: 0,
volume7d: 0,
volume30d: 0,
txCount5m: 0,
txCount1h: 0,
txCount24h: 0,
});
await expect(calculator.calculateTokenVolume(138, '0xToken')).resolves.toEqual({
volume5m: 0,
volume1h: 0,
volume24h: 0,
volume7d: 0,
volume30d: 0,
txCount5m: 0,
txCount1h: 0,
txCount24h: 0,
});
expect(mockLoggerInfo).toHaveBeenCalledTimes(1);
expect(mockLoggerInfo).toHaveBeenCalledWith(
'Skipping volume calculation; relation "liquidity_pools" is not available yet',
expect.objectContaining({
operation: 'volume calculation',
relation: 'liquidity_pools',
})
);
});
});

View File

@@ -1,5 +1,6 @@
import { Pool } from 'pg';
import { getDatabasePool } from '../database/client';
import { logger } from '../utils/logger';
export interface VolumeMetrics {
volume5m: number;
@@ -14,11 +15,44 @@ export interface VolumeMetrics {
export class VolumeCalculator {
private pool: Pool;
private static readonly missingRelationWarnings = new Set<string>();
constructor() {
this.pool = getDatabasePool();
}
private isMissingRelationError(error: unknown): boolean {
if (!error || typeof error !== 'object') {
return false;
}
const code = (error as { code?: string }).code;
const message = (error as { message?: string }).message || '';
return code === '42P01' || (message.includes('relation "') && message.includes('" does not exist'));
}
private getMissingRelationName(error: unknown): string {
const message = typeof error === 'object' && error && 'message' in error
? String((error as { message?: string }).message || '')
: '';
const match = message.match(/relation "([^"]+)"/);
return match?.[1] || 'unknown relation';
}
private logMissingRelationOnce(operation: string, error: unknown): void {
const relation = this.getMissingRelationName(error);
const key = `${operation}:${relation}`;
if (VolumeCalculator.missingRelationWarnings.has(key)) {
return;
}
VolumeCalculator.missingRelationWarnings.add(key);
logger.info(`Skipping ${operation}; relation "${relation}" is not available yet`, {
operation,
relation,
});
}
/**
* Calculate volume metrics for a token across all pools
*/
@@ -27,56 +61,55 @@ export class VolumeCalculator {
tokenAddress: string,
now: Date = new Date()
): Promise<VolumeMetrics> {
const intervals = {
'5m': new Date(now.getTime() - 5 * 60 * 1000),
'1h': new Date(now.getTime() - 60 * 60 * 1000),
'24h': new Date(now.getTime() - 24 * 60 * 60 * 1000),
'7d': new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000),
'30d': new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000),
};
// Get all pools for this token
const poolsResult = await this.pool.query(
`SELECT pool_address FROM liquidity_pools
WHERE chain_id = $1 AND (token0_address = $2 OR token1_address = $2)`,
[chainId, tokenAddress.toLowerCase()]
);
const poolAddresses = poolsResult.rows.map((row) => row.pool_address);
if (poolAddresses.length === 0) {
return {
volume5m: 0,
volume1h: 0,
volume24h: 0,
volume7d: 0,
volume30d: 0,
txCount5m: 0,
txCount1h: 0,
txCount24h: 0,
try {
const intervals = {
'5m': new Date(now.getTime() - 5 * 60 * 1000),
'1h': new Date(now.getTime() - 60 * 60 * 1000),
'24h': new Date(now.getTime() - 24 * 60 * 60 * 1000),
'7d': new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000),
'30d': new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000),
};
// Get all pools for this token
const poolsResult = await this.pool.query(
`SELECT pool_address FROM liquidity_pools
WHERE chain_id = $1 AND (token0_address = $2 OR token1_address = $2)`,
[chainId, tokenAddress.toLowerCase()]
);
const poolAddresses = poolsResult.rows.map((row) => row.pool_address);
if (poolAddresses.length === 0) {
return this.zeroMetrics();
}
// Calculate volume for each interval
const [volume5m, volume1h, volume24h, volume7d, volume30d, txCounts] = await Promise.all([
this.calculateVolumeForInterval(chainId, poolAddresses, tokenAddress, intervals['5m'], now),
this.calculateVolumeForInterval(chainId, poolAddresses, tokenAddress, intervals['1h'], now),
this.calculateVolumeForInterval(chainId, poolAddresses, tokenAddress, intervals['24h'], now),
this.calculateVolumeForInterval(chainId, poolAddresses, tokenAddress, intervals['7d'], now),
this.calculateVolumeForInterval(chainId, poolAddresses, tokenAddress, intervals['30d'], now),
this.calculateTxCounts(chainId, poolAddresses, intervals, now),
]);
return {
volume5m,
volume1h,
volume24h,
volume7d,
volume30d,
txCount5m: txCounts['5m'],
txCount1h: txCounts['1h'],
txCount24h: txCounts['24h'],
};
} catch (error) {
if (this.isMissingRelationError(error)) {
this.logMissingRelationOnce('volume calculation', error);
return this.zeroMetrics();
}
throw error;
}
// Calculate volume for each interval
const [volume5m, volume1h, volume24h, volume7d, volume30d, txCounts] = await Promise.all([
this.calculateVolumeForInterval(chainId, poolAddresses, tokenAddress, intervals['5m'], now),
this.calculateVolumeForInterval(chainId, poolAddresses, tokenAddress, intervals['1h'], now),
this.calculateVolumeForInterval(chainId, poolAddresses, tokenAddress, intervals['24h'], now),
this.calculateVolumeForInterval(chainId, poolAddresses, tokenAddress, intervals['7d'], now),
this.calculateVolumeForInterval(chainId, poolAddresses, tokenAddress, intervals['30d'], now),
this.calculateTxCounts(chainId, poolAddresses, intervals, now),
]);
return {
volume5m,
volume1h,
volume24h,
volume7d,
volume30d,
txCount5m: txCounts['5m'],
txCount1h: txCounts['1h'],
txCount24h: txCounts['24h'],
};
}
/**
@@ -164,24 +197,45 @@ export class VolumeCalculator {
interval: '5m' | '1h' | '24h' | '7d' | '30d',
now: Date = new Date()
): Promise<number> {
const intervals = {
'5m': new Date(now.getTime() - 5 * 60 * 1000),
'1h': new Date(now.getTime() - 60 * 60 * 1000),
'24h': new Date(now.getTime() - 24 * 60 * 60 * 1000),
'7d': new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000),
'30d': new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000),
try {
const intervals = {
'5m': new Date(now.getTime() - 5 * 60 * 1000),
'1h': new Date(now.getTime() - 60 * 60 * 1000),
'24h': new Date(now.getTime() - 24 * 60 * 60 * 1000),
'7d': new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000),
'30d': new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000),
};
const result = await this.pool.query(
`SELECT COALESCE(SUM(amount_usd), 0) as total_volume
FROM swap_events
WHERE chain_id = $1
AND pool_address = $2
AND timestamp >= $3
AND timestamp <= $4`,
[chainId, poolAddress.toLowerCase(), intervals[interval], now]
);
return parseFloat(result.rows[0]?.total_volume || '0');
} catch (error) {
if (this.isMissingRelationError(error)) {
this.logMissingRelationOnce('pool volume calculation', error);
return 0;
}
throw error;
}
}
private zeroMetrics(): VolumeMetrics {
return {
volume5m: 0,
volume1h: 0,
volume24h: 0,
volume7d: 0,
volume30d: 0,
txCount5m: 0,
txCount1h: 0,
txCount24h: 0,
};
const result = await this.pool.query(
`SELECT COALESCE(SUM(amount_usd), 0) as total_volume
FROM swap_events
WHERE chain_id = $1
AND pool_address = $2
AND timestamp >= $3
AND timestamp <= $4`,
[chainId, poolAddress.toLowerCase(), intervals[interval], now]
);
return parseFloat(result.rows[0]?.total_volume || '0');
}
}

View File

@@ -0,0 +1,295 @@
import fs from 'fs';
import path from 'path';
import {
AggregatorFamily,
AggregatorRouteMatrix,
AggregatorRouteLeg,
LiveAggregatorRoute,
NonLiveAggregatorRoute,
} from '../config/aggregator-route-matrix';
import { getProviderCapabilities } from '../config/provider-capabilities';
import { resolveChain138RpcUrl } from '../config/chain138-rpc';
import { getChain138RoutingAssets, getRoutingSymbolForAddress } from '../config/routing-assets';
import { RouteGraphBuilder } from './route-graph-builder';
import { PlannerProvider, SwapGraphEdge } from './planner-v2-types';
const PARTNER_FAMILIES: AggregatorFamily[] = ['1inch', '0x', 'LiFi'];
function providerProtocol(provider: PlannerProvider): string {
switch (provider) {
case 'dodo':
return 'dodo_pmm';
case 'dodo_v3':
return 'dodo_v3';
case 'uniswap_v3':
return 'uniswap_v3';
case 'balancer':
return 'balancer';
case 'curve':
return 'curve';
case 'one_inch':
return 'one_inch';
case 'partner':
return 'partner';
}
}
function providerLabel(provider: PlannerProvider): string {
switch (provider) {
case 'dodo':
return 'DODO PMM';
case 'dodo_v3':
return 'DODO V3 / D3MM';
case 'uniswap_v3':
return 'Uniswap V3';
case 'balancer':
return 'Balancer';
case 'curve':
return 'Curve';
case 'one_inch':
return '1inch';
case 'partner':
return 'Partner';
}
}
function normalizeAddress(value?: string): string | undefined {
return value?.trim().toLowerCase() || undefined;
}
function makeRouteId(prefix: string, parts: Array<string | number | undefined>): string {
return [prefix, ...parts]
.filter((part): part is string | number => part !== undefined && part !== null && String(part).trim().length > 0)
.map((part) => String(part).trim().toLowerCase().replace(/[^a-z0-9]+/g, '-'))
.join('-');
}
function dedupeRoutes<T extends { routeId: string }>(routes: T[]): T[] {
const byId = new Map<string, T>();
for (const route of routes) {
byId.set(route.routeId, route);
}
return Array.from(byId.values());
}
function toSwapLeg(edge: SwapGraphEdge): AggregatorRouteLeg {
return {
kind: 'swap',
protocol: providerProtocol(edge.provider),
executor: providerLabel(edge.provider),
executorAddress: normalizeAddress(edge.target),
poolAddress: normalizeAddress(edge.poolAddress),
tokenInAddress: normalizeAddress(edge.tokenInAddress),
tokenOutAddress: normalizeAddress(edge.tokenOutAddress),
reserves: {
reserveIn: edge.reserveIn,
reserveOut: edge.reserveOut,
},
};
}
function buildSwapRoute(homeChainId: number, edge: SwapGraphEdge): LiveAggregatorRoute {
return {
routeId: makeRouteId(`chain-${homeChainId}-swap`, [
edge.provider,
edge.tokenInSymbol || edge.tokenInAddress,
edge.tokenOutSymbol || edge.tokenOutAddress,
edge.poolAddress?.slice(0, 10),
]),
status: 'live',
aggregatorFamilies: PARTNER_FAMILIES,
fromChainId: homeChainId,
toChainId: homeChainId,
tokenInSymbol: edge.tokenInSymbol,
tokenInAddress: normalizeAddress(edge.tokenInAddress),
tokenOutSymbol: edge.tokenOutSymbol,
tokenOutAddress: normalizeAddress(edge.tokenOutAddress),
routeType: 'swap',
hopCount: 1,
label: `${providerLabel(edge.provider)} ${edge.tokenInSymbol || edge.tokenInAddress} -> ${edge.tokenOutSymbol || edge.tokenOutAddress}`,
intermediateSymbols: [],
legs: [toSwapLeg(edge)],
tags: ['planner-v2-generated', edge.provider],
notes: [
'Generated from live planner route graph.',
...(edge.notes || []),
],
};
}
function buildBridgeRoute(args: {
fromChainId: number;
toChainId: number;
assetSymbol: string;
assetAddress?: string;
bridgeType: string;
bridgeAddress: string;
label: string;
notes?: string[];
}): LiveAggregatorRoute {
return {
routeId: makeRouteId('bridge', [
args.fromChainId,
args.toChainId,
args.assetSymbol,
args.label,
]),
status: 'live',
aggregatorFamilies: PARTNER_FAMILIES,
fromChainId: args.fromChainId,
toChainId: args.toChainId,
assetSymbol: args.assetSymbol,
assetAddress: normalizeAddress(args.assetAddress),
routeType: 'bridge',
bridgeType: args.bridgeType,
bridgeAddress: normalizeAddress(args.bridgeAddress),
label: `${args.label} ${args.assetSymbol} ${args.fromChainId} -> ${args.toChainId}`,
tags: ['planner-v2-generated', 'bridge'],
notes: [
'Generated from bridge registry and planner visibility.',
...(args.notes || []),
],
};
}
function resolveDefaultOutputPath(): string {
return path.resolve(process.cwd(), '../../../config/aggregator-route-matrix.json');
}
export class AggregatorRouteMatrixGenerator {
private graphBuilder: RouteGraphBuilder;
constructor(graphBuilder = new RouteGraphBuilder()) {
this.graphBuilder = graphBuilder;
}
async generate(homeChainId: number = 138): Promise<AggregatorRouteMatrix> {
const edges = await this.graphBuilder.buildSwapEdges(homeChainId);
const liveSwapRoutes = dedupeRoutes(edges.map((edge) => buildSwapRoute(homeChainId, edge)));
const routingAssets = Object.values(getChain138RoutingAssets());
const bridgeTargets = [1, 651940];
const liveBridgeRoutes = dedupeRoutes(
bridgeTargets.flatMap((destinationChainId) =>
this.graphBuilder
.buildBridgeCandidates(
homeChainId,
destinationChainId,
routingAssets.map((asset) => asset.symbol)
)
.map((candidate) =>
buildBridgeRoute({
fromChainId: candidate.fromChainId,
toChainId: candidate.toChainId,
assetSymbol: candidate.assetSymbol,
assetAddress: candidate.sourceTokenAddress,
bridgeType: candidate.bridgeType,
bridgeAddress: candidate.bridgeAddress,
label: candidate.bridgeLabel,
notes: candidate.notes,
})
)
)
);
const blockedOrPlannedRoutes: NonLiveAggregatorRoute[] = getProviderCapabilities(homeChainId)
.flatMap((record) =>
record.pairs
.filter((pair): pair is typeof pair & { status: 'planned' | 'blocked' } => pair.status !== 'live')
.map((pair) => ({
routeId: makeRouteId('chain-138-capability', [
record.provider,
pair.status,
pair.tokenInSymbol,
pair.tokenOutSymbol,
]),
status: pair.status,
fromChainId: pair.chainId,
toChainId: pair.chainId,
routeType: pair.legType === 'bridge' ? 'bridge' : 'swap',
reason: pair.reason || pair.notes?.join(' ') || `${record.provider} ${pair.status}`,
tokenInSymbols: [pair.tokenInSymbol, pair.tokenOutSymbol],
}))
);
return {
$schema: 'https://json-schema.org/draft/2020-12/schema',
description: 'Planner-v2-generated aggregator route visibility matrix for Chain 138 and approved bridge lanes.',
version: '2.0.0',
updated: new Date().toISOString(),
homeChainId,
metadata: {
generatedFrom: [
'services/token-aggregation/src/services/route-graph-builder.ts',
'services/token-aggregation/src/config/provider-capabilities.ts',
'services/token-aggregation/src/config/cross-chain-bridges.ts',
],
verification: {
verifiedAt: new Date().toISOString(),
verifiedBy: 'services/token-aggregation planner-v2 generator',
rpc: resolveChain138RpcUrl(),
},
adapterNotes: [
'This file is generated from planner-v2 graph and provider capability truth.',
'Partner payload generation should prefer planner-v2 outputs over this visibility artifact when route inputs are available.',
'Only live routes should be considered executable candidates.',
],
},
chains: {
'138': { name: 'Chain 138' },
'1': { name: 'Ethereum Mainnet' },
'651940': { name: 'ALL Mainnet' },
},
tokens: Object.fromEntries(
routingAssets.map((asset) => [asset.symbol, { address: asset.address, decimals: asset.decimals, kind: asset.kind }])
),
liveSwapRoutes,
liveBridgeRoutes,
blockedOrPlannedRoutes: dedupeRoutes(blockedOrPlannedRoutes),
};
}
async writeToFile(outputPath: string = resolveDefaultOutputPath(), homeChainId: number = 138): Promise<string> {
const matrix = await this.generate(homeChainId);
fs.writeFileSync(outputPath, `${JSON.stringify(matrix, null, 2)}\n`, 'utf8');
return outputPath;
}
}
export function routeFromPlannerLegs(args: {
routeId: string;
fromChainId: number;
toChainId: number;
tokenInAddress?: string;
tokenOutAddress?: string;
assetAddress?: string;
assetSymbol?: string;
routeType: 'swap' | 'bridge';
bridgeType?: string;
bridgeAddress?: string;
label: string;
legs: AggregatorRouteLeg[];
notes?: string[];
}): LiveAggregatorRoute {
return {
routeId: args.routeId,
status: 'live',
aggregatorFamilies: PARTNER_FAMILIES,
fromChainId: args.fromChainId,
toChainId: args.toChainId,
tokenInAddress: normalizeAddress(args.tokenInAddress),
tokenOutAddress: normalizeAddress(args.tokenOutAddress),
tokenInSymbol: args.tokenInAddress ? getRoutingSymbolForAddress(args.fromChainId, args.tokenInAddress) : undefined,
tokenOutSymbol: args.tokenOutAddress ? getRoutingSymbolForAddress(args.toChainId, args.tokenOutAddress) : undefined,
assetAddress: normalizeAddress(args.assetAddress),
assetSymbol: args.assetSymbol,
routeType: args.routeType,
bridgeType: args.bridgeType,
bridgeAddress: normalizeAddress(args.bridgeAddress),
label: args.label,
hopCount: args.routeType === 'swap' ? args.legs.length : undefined,
legs: args.legs,
tags: ['planner-v2-generated'],
notes: args.notes || [],
};
}

View File

@@ -7,6 +7,7 @@
import { PoolRepository } from '../database/repositories/pool-repo';
import { TokenRepository } from '../database/repositories/token-repo';
import { getRoutesList, getChainIds } from '../config/heatmap-chains';
import { filterPoolsForRouting, getActiveTransportPairs } from '../config/gru-transport';
export interface ArbitrageOpportunity {
cycleId: string;
@@ -21,12 +22,18 @@ const poolRepo = new PoolRepository();
const tokenRepo = new TokenRepository();
const HUB_CHAIN = 138;
const EDGE_CHAINS = getChainIds().filter((c) => c !== HUB_CHAIN && c !== 651940);
const EDGE_CHAINS = Array.from(
new Set(
getActiveTransportPairs()
.map((pair) => pair.destinationChainId)
.filter((chainId) => chainId !== HUB_CHAIN && chainId !== 651940)
)
);
/** Same-chain triangle on 138: e.g. cUSDT -> cUSDC -> cUSDT via two pools. */
async function getSameChainCycles(): Promise<ArbitrageOpportunity[]> {
const out: ArbitrageOpportunity[] = [];
const pools = await poolRepo.getPoolsByChain(HUB_CHAIN, 100);
const pools = filterPoolsForRouting(HUB_CHAIN, await poolRepo.getPoolsByChain(HUB_CHAIN, 100));
for (const p of pools) {
const t0 = await tokenRepo.getToken(HUB_CHAIN, p.token0Address);
const t1 = await tokenRepo.getToken(HUB_CHAIN, p.token1Address);
@@ -53,7 +60,8 @@ async function getSameChainCycles(): Promise<ArbitrageOpportunity[]> {
/** Hub-edge-hub: 138 -> edge -> 138 (SBS). */
function getHubEdgeHubCycles(): ArbitrageOpportunity[] {
const out: ArbitrageOpportunity[] = [];
const routes = getRoutesList();
const activeDestinations = new Set(EDGE_CHAINS);
const routes = getRoutesList().filter((route) => activeDestinations.has(route.toChainId));
const hubOut = routes.filter((r) => r.fromChainId === HUB_CHAIN && r.toChainId !== HUB_CHAIN);
for (const r of hubOut.slice(0, 5)) {
out.push({

View File

@@ -0,0 +1,190 @@
import { BestExecutionPlanner } from './best-execution-planner';
import { PlannerMetricsRepository } from '../database/repositories/planner-metrics-repo';
import { RouteGraphBuilder } from './route-graph-builder';
import type { BridgeRouteCandidate, PlannerResponse, SwapGraphEdge } from './planner-v2-types';
const mockDodoV3Quote = jest.fn();
jest.mock('./dodo-v3-pilot', () => ({
__esModule: true,
quoteChain138DodoV3AmountOut: (...args: unknown[]) => mockDodoV3Quote(...args),
encodeChain138DodoV3ProviderData: (poolAddress: string) =>
`0x000000000000000000000000${String(poolAddress).replace(/^0x/, '').toLowerCase()}`,
isChain138DodoV3ExecutionLive: () => true,
}));
class MockPlannerMetricsRepository {
async getCachedPlan(): Promise<PlannerResponse | null> {
return null;
}
async cachePlan(): Promise<void> {}
async recordProviderSnapshots(): Promise<void> {}
async recordPlannedRouteMetrics(): Promise<void> {}
}
class MockGraphBuilder {
private swapEdges: SwapGraphEdge[];
private bridgeCandidates: BridgeRouteCandidate[];
constructor(swapEdges: SwapGraphEdge[] = [], bridgeCandidates: BridgeRouteCandidate[] = []) {
this.swapEdges = swapEdges;
this.bridgeCandidates = bridgeCandidates;
}
async buildSwapEdges(): Promise<SwapGraphEdge[]> {
return this.swapEdges;
}
buildBridgeCandidates(): BridgeRouteCandidate[] {
return this.bridgeCandidates;
}
}
const WETH = '0xc02aaA39b223fe8d0a0e5c4f27ead9083c756cc2'.toLowerCase();
const USDT = '0x004b63a7b5b0e06f6bb6adb4a5f9f590bf3182d1'.toLowerCase();
const USDC = '0x71d6687f38b93ccad569fa6352c876eea967201b'.toLowerCase();
const CUSDT = '0x93e66202a11b1772e55407b32b44e5cd8eda7f22'.toLowerCase();
const CEURT = '0xdf4b71c61e5912712c1bdd451416b9ac26949d72'.toLowerCase();
const CXAUC = '0x290e52a8819a4fbd0714e517225429aa2b70ec6b'.toLowerCase();
const WETH10 = '0xf4bb2e28688e89fcce3c0580d37d36a7672e8a9f'.toLowerCase();
function swapEdge(
tokenInAddress: string,
tokenOutAddress: string,
reserveIn: string,
reserveOut: string,
tokenInSymbol: string,
tokenOutSymbol: string,
provider: SwapGraphEdge['provider'] = 'dodo'
): SwapGraphEdge {
return {
kind: 'swap',
provider,
chainId: 138,
tokenInAddress,
tokenOutAddress,
tokenInSymbol,
tokenOutSymbol,
reserveIn,
reserveOut,
target: '0x3f729632e9553ebaccde2e9b4c8f2b285b014f2e',
poolAddress: '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
providerData: { poolAddress: '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' },
providerDataHex: '0x000000000000000000000000aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
totalLiquidityUsd: 2_000_000,
freshnessSeconds: 60,
notes: [],
};
}
describe('BestExecutionPlanner', () => {
beforeEach(() => {
mockDodoV3Quote.mockReset();
});
it('prefers the direct route when it scores better than multi-hop', async () => {
const graphBuilder = new MockGraphBuilder([
swapEdge(WETH, USDT, '100000000000000000000', '2000000000000', 'WETH', 'USDT'),
swapEdge(WETH, CUSDT, '100000000000000000000', '1900000000000', 'WETH', 'cUSDT'),
swapEdge(CUSDT, USDT, '1900000000000', '1850000000000', 'cUSDT', 'USDT'),
]) as unknown as RouteGraphBuilder;
const planner = new BestExecutionPlanner(graphBuilder, new MockPlannerMetricsRepository() as unknown as PlannerMetricsRepository);
const response = await planner.plan({
sourceChainId: 138,
tokenIn: WETH,
tokenOut: USDT,
amountIn: '1000000000000000000',
});
expect(response.decision).toBe('direct-pool');
expect(response.legs).toHaveLength(1);
expect(response.legs[0].tokenOutAddress).toBe(USDT);
});
it('chooses multi-hop when no direct route exists', async () => {
const graphBuilder = new MockGraphBuilder([
swapEdge(WETH, CUSDT, '100000000000000000000', '1900000000000', 'WETH', 'cUSDT'),
swapEdge(CUSDT, USDC, '1900000000000', '1880000000000', 'cUSDT', 'USDC'),
]) as unknown as RouteGraphBuilder;
const planner = new BestExecutionPlanner(graphBuilder, new MockPlannerMetricsRepository() as unknown as PlannerMetricsRepository);
const response = await planner.plan({
sourceChainId: 138,
tokenIn: WETH,
tokenOut: USDC,
amountIn: '1000000000000000000',
});
expect(response.decision).toBe('multi-hop');
expect(response.legs).toHaveLength(2);
expect(response.legs[0].tokenOutAddress).toBe(CUSDT);
expect(response.legs[1].tokenOutAddress).toBe(USDC);
});
it('blocks commodity intermediates under institutional policy unless explicitly enabled', async () => {
const graphBuilder = new MockGraphBuilder([
swapEdge(CEURT, CXAUC, '1000000000000', '1000000000000', 'cEURT', 'cXAUC'),
swapEdge(CXAUC, CUSDT, '1000000000000', '1000000000000', 'cXAUC', 'cUSDT'),
]) as unknown as RouteGraphBuilder;
const planner = new BestExecutionPlanner(graphBuilder, new MockPlannerMetricsRepository() as unknown as PlannerMetricsRepository);
const response = await planner.plan({
sourceChainId: 138,
tokenIn: CEURT,
tokenOut: CUSDT,
amountIn: '1000000',
constraints: {
complianceProfile: 'institutional',
},
});
expect(response.decision).toBe('unresolved');
expect(response.riskFlags).toContain('no-route');
});
it('returns deterministic plan ids for identical requests', async () => {
const graphBuilder = new MockGraphBuilder([
swapEdge(WETH, USDT, '100000000000000000000', '2000000000000', 'WETH', 'USDT'),
]) as unknown as RouteGraphBuilder;
const planner = new BestExecutionPlanner(graphBuilder, new MockPlannerMetricsRepository() as unknown as PlannerMetricsRepository);
const request = {
sourceChainId: 138,
tokenIn: WETH,
tokenOut: USDT,
amountIn: '1000000000000000000',
};
const first = await planner.plan(request);
const second = await planner.plan(request);
expect(first.planId).toBe(second.planId);
expect(first.legs).toEqual(second.legs);
});
it('returns a live planner route and executable router-v2 calldata for the DODO v3 pilot when execution is enabled', async () => {
mockDodoV3Quote.mockResolvedValue(211660490n);
const graphBuilder = new MockGraphBuilder([
swapEdge(WETH10, USDT, '2010000000000000000', '4978833460', 'WETH10', 'USDT', 'dodo_v3'),
]) as unknown as RouteGraphBuilder;
const planner = new BestExecutionPlanner(graphBuilder, new MockPlannerMetricsRepository() as unknown as PlannerMetricsRepository);
const response = await planner.plan({
sourceChainId: 138,
tokenIn: WETH10,
tokenOut: USDT,
amountIn: '100000000000000000',
});
expect(response.decision).toBe('direct-pool');
expect(response.legs).toHaveLength(1);
expect(response.legs[0].provider).toBe('dodo_v3');
expect(response.estimatedAmountOut).toBe('211660490');
expect(response.routePlan).toBeDefined();
expect(response.routePlan?.legs[0]?.provider).toBe(6);
expect(response.riskFlags).toContain('pilot-venue');
expect(response.riskFlags).not.toContain('manual-execution-only');
});
});

View File

@@ -0,0 +1,783 @@
import { AbiCoder, Contract, JsonRpcProvider, ZeroAddress, formatUnits } from 'ethers';
import { getCanonicalTokenByAddress, resolveCanonicalQuoteAddress } from '../config/canonical-tokens';
import { resolveChain138RpcUrl } from '../config/chain138-rpc';
import { getProviderCapabilities } from '../config/provider-capabilities';
import { resolveRoutingPolicy } from '../config/routing-policies';
import {
getCommodityIntermediateAddresses,
getDefaultIntermediateAddresses,
getRoutingAddressForSymbol,
getRoutingSymbolForAddress,
} from '../config/routing-assets';
import { PlannerMetricsRepository } from '../database/repositories/planner-metrics-repo';
import { RouteGraphBuilder } from './route-graph-builder';
import { quoteChain138DodoV3AmountOut } from './dodo-v3-pilot';
import {
BridgeRouteCandidate,
EncodedBridgeIntentPlan,
EncodedPlannerRoutePlan,
PlannerAlternative,
PlannerDecision,
PlannerLeg,
PlannerProvider,
PlannerRequest,
PlannerResponse,
RoutingPolicy,
SwapGraphEdge,
} from './planner-v2-types';
const abiCoder = AbiCoder.defaultAbiCoder();
const ROUTER_V2_RECIPIENT_PLACEHOLDER = ZeroAddress;
const DEFAULT_INTENT_BRIDGE_COORDINATOR_V2 = normalizeAddress('0x7D0022B7e8360172fd9C0bB6778113b7Ea3674E7');
const PROVIDER_PRIORITY: PlannerProvider[] = ['dodo', 'dodo_v3', 'uniswap_v3', 'balancer', 'curve', 'one_inch', 'partner'];
const PROVIDER_ENUM: Partial<Record<PlannerProvider, number>> = {
dodo: 0,
uniswap_v3: 1,
balancer: 2,
curve: 3,
one_inch: 4,
partner: 5,
dodo_v3: 6,
};
const PROVIDER_GAS_USD: Record<PlannerProvider, number> = {
dodo: 0.22,
dodo_v3: 0.3,
uniswap_v3: 0.28,
balancer: 0.34,
curve: 0.29,
one_inch: 0.48,
partner: 0.55,
};
const uniswapQuoterAbi = [
'function quoteExactInputSingle((address,address,uint256,uint24,uint160) params) view returns (uint256,uint160,uint32,uint256)',
'function quoteExactInputSingle(address tokenIn,address tokenOut,uint24 fee,uint256 amountIn,uint160 sqrtPriceLimitX96) view returns (uint256)',
] as const;
interface RouteCandidate {
decision: PlannerDecision;
estimatedAmountOut: bigint;
estimatedGasUsd: number;
score: number;
legs: PlannerLeg[];
selectedRouteReason: string;
rejectedAlternatives: string[];
riskFlags: string[];
}
function normalizeAddress(value: string): string {
return value.trim().toLowerCase();
}
let sharedProvider: JsonRpcProvider | null = null;
let sharedProviderUrl = '';
function getProvider(): JsonRpcProvider {
const rpcUrl = resolveChain138RpcUrl();
if (!sharedProvider || sharedProviderUrl !== rpcUrl) {
sharedProvider = new JsonRpcProvider(rpcUrl);
sharedProviderUrl = rpcUrl;
}
return sharedProvider;
}
function safeBigInt(value: string): bigint {
return BigInt(value);
}
function quoteAmountOut(amountIn: bigint, reserveIn: bigint, reserveOut: bigint): bigint {
if (amountIn <= 0n || reserveIn <= 0n || reserveOut <= 0n) {
return 0n;
}
const amountInWithFee = amountIn * 997n;
return (reserveOut * amountInWithFee) / (reserveIn * 1000n + amountInWithFee);
}
function sortEdges(edges: SwapGraphEdge[]): SwapGraphEdge[] {
return edges.slice().sort((a, b) => {
const providerDiff = PROVIDER_PRIORITY.indexOf(a.provider) - PROVIDER_PRIORITY.indexOf(b.provider);
if (providerDiff !== 0) return providerDiff;
const liquidityDiff = (b.totalLiquidityUsd || 0) - (a.totalLiquidityUsd || 0);
if (liquidityDiff !== 0) return liquidityDiff;
return a.tokenOutAddress.localeCompare(b.tokenOutAddress);
});
}
function decimalsForAddress(chainId: number, address: string): number {
const spec = getCanonicalTokenByAddress(chainId, address);
if (spec?.decimals) return Number(spec.decimals);
const symbol = getRoutingSymbolForAddress(chainId, address);
if (symbol === 'WETH' || symbol === 'WETH10') return 18;
return 6;
}
function normalizedOutput(chainId: number, address: string, amount: bigint): number {
const decimals = decimalsForAddress(chainId, address);
return Number(formatUnits(amount, decimals));
}
function providerDataHexForEdge(edge: SwapGraphEdge): string {
if (edge.providerDataHex) {
return edge.providerDataHex;
}
if (edge.provider === 'dodo' && edge.poolAddress) {
return abiCoder.encode(['address'], [edge.poolAddress]);
}
return '0x';
}
function providerDataHexForLeg(leg: PlannerLeg): string | undefined {
if (leg.providerDataHex && leg.providerDataHex !== '0x') {
return leg.providerDataHex;
}
if (leg.provider === 'dodo' && leg.poolAddress) {
return abiCoder.encode(['address'], [leg.poolAddress]);
}
return undefined;
}
function buildMinAmountOut(amountOut: bigint, maxSlippageBps: number): bigint {
return (amountOut * BigInt(10_000 - maxSlippageBps)) / 10_000n;
}
function computeConfidence(legs: PlannerLeg[]): number {
const stalePenalty = legs.some((leg) => (leg.freshnessSeconds || 0) > 1800) ? 0.15 : 0;
const hopPenalty = Math.max(0, legs.length - 1) * 0.07;
return Math.max(0.1, Math.min(0.99, 0.92 - stalePenalty - hopPenalty));
}
function computeRiskFlags(legs: PlannerLeg[], policy: RoutingPolicy): string[] {
const flags = new Set<string>();
if (legs.length > 1) {
flags.add('multi-hop');
}
if (legs.some((leg) => leg.kind === 'bridge')) {
flags.add('cross-chain');
}
if (legs.some((leg) => (leg.freshnessSeconds || 0) > 1800)) {
flags.add('stale-liquidity');
}
if (!policy.allowCommodityIntermediates && legs.some((leg) => leg.tokenInSymbol === 'cXAUC' || leg.tokenOutSymbol === 'cXAUC')) {
flags.add('commodity-path-blocked');
}
if (legs.some((leg) => leg.provider === 'dodo_v3')) {
flags.add('pilot-venue');
if (!legs.every((leg) => leg.provider !== 'dodo_v3' || Boolean(providerDataHexForLeg(leg)))) {
flags.add('manual-execution-only');
}
}
return Array.from(flags);
}
async function quoteUniswapV3AmountOut(edge: SwapGraphEdge, amountIn: bigint): Promise<bigint> {
const providerData = (edge.providerData || {}) as { quoter?: string; fee?: number };
const quoter = normalizeAddress(String(providerData.quoter || ''));
const fee = Number(providerData.fee || 3000);
if (!quoter) {
return 0n;
}
const contract = new Contract(quoter, uniswapQuoterAbi, getProvider());
try {
const result = await contract.quoteExactInputSingle([
edge.tokenInAddress,
edge.tokenOutAddress,
amountIn,
fee,
0,
]);
return BigInt(String(Array.isArray(result) ? result[0] : result));
} catch {
try {
const result = await contract['quoteExactInputSingle(address,address,uint24,uint256,uint160)'](
edge.tokenInAddress,
edge.tokenOutAddress,
fee,
amountIn,
0
);
return BigInt(String(result));
} catch {
return 0n;
}
}
}
export class BestExecutionPlanner {
private graphBuilder: RouteGraphBuilder;
private plannerRepo: PlannerMetricsRepository;
constructor(
graphBuilder = new RouteGraphBuilder(),
plannerRepo = new PlannerMetricsRepository()
) {
this.graphBuilder = graphBuilder;
this.plannerRepo = plannerRepo;
}
async plan(request: PlannerRequest): Promise<PlannerResponse> {
const normalizedRequest = {
...request,
tokenIn: normalizeAddress(request.tokenIn),
tokenOut: normalizeAddress(request.tokenOut),
destinationChainId: request.destinationChainId || request.sourceChainId,
};
const policy = resolveRoutingPolicy(normalizedRequest.sourceChainId, normalizedRequest.constraints || {});
const requestHash = this.requestHash(normalizedRequest, policy);
const cached = await this.plannerRepo.getCachedPlan(requestHash);
if (cached) {
return cached;
}
const capabilities = getProviderCapabilities(normalizedRequest.sourceChainId);
await this.plannerRepo.recordProviderSnapshots(normalizedRequest.sourceChainId, capabilities);
const response =
normalizedRequest.destinationChainId === normalizedRequest.sourceChainId
? await this.planOneChain(normalizedRequest, policy)
: await this.planCrossChain(normalizedRequest, policy);
await this.plannerRepo.cachePlan(requestHash, response);
await this.plannerRepo.recordPlannedRouteMetrics(response);
return response;
}
getCapabilities(chainId: number) {
return getProviderCapabilities(chainId);
}
private requestHash(request: PlannerRequest, policy: RoutingPolicy): string {
return JSON.stringify({
request,
policy,
});
}
private async quoteEdgeAmountOut(
edge: SwapGraphEdge,
amountIn: bigint,
quoteCache: Map<string, bigint>
): Promise<bigint> {
if (edge.provider === 'uniswap_v3') {
const cacheKey = [edge.provider, edge.target, edge.tokenInAddress, edge.tokenOutAddress, amountIn.toString()].join(':');
const cached = quoteCache.get(cacheKey);
if (cached !== undefined) {
return cached;
}
const quoted = await quoteUniswapV3AmountOut(edge, amountIn);
if (quoted > 0n) {
quoteCache.set(cacheKey, quoted);
return quoted;
}
const fallback = quoteAmountOut(amountIn, safeBigInt(edge.reserveIn), safeBigInt(edge.reserveOut));
quoteCache.set(cacheKey, fallback);
return fallback;
}
if (edge.provider !== 'dodo_v3') {
return quoteAmountOut(amountIn, safeBigInt(edge.reserveIn), safeBigInt(edge.reserveOut));
}
const cacheKey = [edge.provider, edge.poolAddress, edge.tokenInAddress, edge.tokenOutAddress, amountIn.toString()].join(':');
const cached = quoteCache.get(cacheKey);
if (cached !== undefined) {
return cached;
}
try {
const quoted = await quoteChain138DodoV3AmountOut({
tokenInAddress: edge.tokenInAddress,
tokenOutAddress: edge.tokenOutAddress,
amountIn,
});
quoteCache.set(cacheKey, quoted);
return quoted;
} catch {
quoteCache.set(cacheKey, 0n);
return 0n;
}
}
private async planOneChain(request: PlannerRequest, policy: RoutingPolicy): Promise<PlannerResponse> {
const sourceResolution = resolveCanonicalQuoteAddress(request.sourceChainId, request.tokenIn);
const targetResolution = resolveCanonicalQuoteAddress(request.sourceChainId, request.tokenOut);
const amountIn = safeBigInt(request.amountIn);
const maxSlippageBps = request.constraints?.maxSlippageBps || 100;
const edges = await this.graphBuilder.buildSwapEdges(request.sourceChainId);
const allowedProviders = new Set(policy.allowedProviders);
const allowedTokens = new Set<string>([
sourceResolution.lookupAddress,
targetResolution.lookupAddress,
...policy.defaultIntermediateAddresses,
...(policy.allowCommodityIntermediates ? getCommodityIntermediateAddresses(request.sourceChainId) : []),
]);
const eligibleEdges = sortEdges(
edges.filter((edge) => {
if (!allowedProviders.has(edge.provider)) return false;
if (!edge.target || !providerDataHexForEdge(edge)) return false;
if (edge.tokenOutSymbol === 'cXAUC' || edge.tokenInSymbol === 'cXAUC') {
return policy.allowCommodityIntermediates;
}
return allowedTokens.has(edge.tokenOutAddress) || edge.tokenOutAddress === targetResolution.lookupAddress;
})
);
const byTokenIn = new Map<string, SwapGraphEdge[]>();
for (const edge of eligibleEdges) {
const key = edge.tokenInAddress;
const list = byTokenIn.get(key) || [];
list.push(edge);
byTokenIn.set(key, list);
}
const candidates: RouteCandidate[] = [];
const visited = new Set<string>([sourceResolution.lookupAddress]);
const quoteCache = new Map<string, bigint>();
const dfs = async (
currentToken: string,
currentAmount: bigint,
path: PlannerLeg[],
depth: number
): Promise<void> => {
if (currentToken === targetResolution.lookupAddress && path.length > 0) {
candidates.push(this.toCandidate(path, currentAmount, request.sourceChainId, targetResolution.lookupAddress, policy));
return;
}
if (depth >= policy.maxLegs) {
return;
}
for (const edge of byTokenIn.get(currentToken) || []) {
if (visited.has(edge.tokenOutAddress)) continue;
const quotedAmount = await this.quoteEdgeAmountOut(edge, currentAmount, quoteCache);
if (quotedAmount <= 0n) continue;
const leg: PlannerLeg = {
kind: 'swap',
provider: edge.provider,
sourceChainId: request.sourceChainId,
destinationChainId: request.sourceChainId,
tokenInAddress: edge.tokenInAddress,
tokenOutAddress: edge.tokenOutAddress,
tokenInSymbol: edge.tokenInSymbol,
tokenOutSymbol: edge.tokenOutSymbol,
estimatedAmountIn: currentAmount.toString(),
estimatedAmountOut: quotedAmount.toString(),
minAmountOut: buildMinAmountOut(quotedAmount, maxSlippageBps).toString(),
target: edge.target,
poolAddress: edge.poolAddress,
providerData: edge.providerData,
providerDataHex: providerDataHexForEdge(edge),
gasEstimate: Math.round((PROVIDER_GAS_USD[edge.provider] / 2500) * 1_000_000),
freshnessSeconds: edge.freshnessSeconds,
totalLiquidityUsd: edge.totalLiquidityUsd,
notes: edge.notes,
};
visited.add(edge.tokenOutAddress);
await dfs(edge.tokenOutAddress, quotedAmount, [...path, leg], depth + 1);
visited.delete(edge.tokenOutAddress);
}
};
await dfs(sourceResolution.lookupAddress, amountIn, [], 0);
const sortedCandidates = candidates.sort((a, b) => b.score - a.score);
const best = sortedCandidates[0];
if (!best) {
return this.emptyResponse(request, 'unresolved', 'No eligible one-chain route found with the current policy and provider capabilities.');
}
const routePlan = this.buildEncodedRoutePlan(
request,
best.legs,
sourceResolution.lookupAddress,
targetResolution.lookupAddress
);
return this.toResponse(
request,
best,
sortedCandidates.slice(1, 4),
'One-chain best execution selected from live pool graph.',
routePlan
);
}
private async planCrossChain(request: PlannerRequest, policy: RoutingPolicy): Promise<PlannerResponse> {
const sourceResolution = resolveCanonicalQuoteAddress(request.sourceChainId, request.tokenIn);
const destinationResolution = resolveCanonicalQuoteAddress(request.destinationChainId || request.sourceChainId, request.tokenOut);
const amountIn = safeBigInt(request.amountIn);
const candidateBridgeSymbols = Array.from(
new Set(
policy.defaultIntermediateAddresses
.map((address) => getRoutingSymbolForAddress(request.sourceChainId, address))
.filter((value): value is string => Boolean(value))
)
);
const bridgeCandidates = this.graphBuilder.buildBridgeCandidates(
request.sourceChainId,
request.destinationChainId || request.sourceChainId,
candidateBridgeSymbols
).filter((candidate) => policy.allowedBridgeLabels.includes(candidate.bridgeLabel));
const combined: RouteCandidate[] = [];
for (const bridgeCandidate of bridgeCandidates) {
const sourceCandidate = await this.planSegment(
request.sourceChainId,
sourceResolution.lookupAddress,
bridgeCandidate.sourceTokenAddress,
amountIn,
policy,
Math.min(policy.maxLegs, 2)
);
if (!sourceCandidate) continue;
const bridgeFeeBps = bridgeCandidate.bridgeType === 'CCIP' ? 20 : 25;
const bridgedAmount = buildMinAmountOut(sourceCandidate.estimatedAmountOut, bridgeFeeBps);
const destinationCandidate = await this.planSegment(
request.destinationChainId || request.sourceChainId,
bridgeCandidate.destinationTokenAddress,
destinationResolution.lookupAddress,
bridgedAmount,
policy,
Math.min(policy.maxLegs, 2)
);
const bridgeLeg: PlannerLeg = {
kind: 'bridge',
provider: 'partner',
sourceChainId: request.sourceChainId,
destinationChainId: request.destinationChainId || request.sourceChainId,
tokenInAddress: bridgeCandidate.sourceTokenAddress,
tokenOutAddress: bridgeCandidate.destinationTokenAddress,
tokenInSymbol: bridgeCandidate.assetSymbol,
tokenOutSymbol: bridgeCandidate.assetSymbol,
estimatedAmountIn: sourceCandidate.estimatedAmountOut.toString(),
estimatedAmountOut: bridgedAmount.toString(),
minAmountOut: bridgedAmount.toString(),
bridgeType: bridgeCandidate.bridgeType,
bridgeAddress: bridgeCandidate.bridgeAddress,
gasEstimate: 250000,
notes: bridgeCandidate.notes,
};
const destinationLegs = destinationCandidate?.legs || [];
const estimatedAmountOut = destinationCandidate?.estimatedAmountOut || bridgedAmount;
const legs = [...sourceCandidate.legs, bridgeLeg, ...destinationLegs];
combined.push({
decision: sourceCandidate.legs.length > 0 || destinationLegs.length > 0 ? 'swap-bridge-swap' : 'bridge-only',
estimatedAmountOut,
estimatedGasUsd: sourceCandidate.estimatedGasUsd + 0.45 + (destinationCandidate?.estimatedGasUsd || 0),
score: sourceCandidate.score + (destinationCandidate?.score || normalizedOutput(request.destinationChainId || request.sourceChainId, bridgeCandidate.destinationTokenAddress, bridgedAmount)) - 0.45,
legs,
selectedRouteReason: `Bridge asset ${bridgeCandidate.assetSymbol} selected via ${bridgeCandidate.bridgeLabel}.`,
rejectedAlternatives: [],
riskFlags: ['cross-chain'],
});
}
const sortedCandidates = combined.sort((a, b) => b.score - a.score);
const best = sortedCandidates[0];
if (!best) {
return this.emptyResponse(request, 'unresolved', 'No eligible cross-chain route found with the current policy and bridge registry.');
}
const sourceSwapLegs = best.legs.filter(
(leg) => leg.kind === 'swap' && leg.sourceChainId === request.sourceChainId
);
const destinationSwapLegs = best.legs.filter(
(leg) => leg.kind === 'swap' && leg.sourceChainId === (request.destinationChainId || request.sourceChainId)
);
const bridgeLeg = best.legs.find((leg) => leg.kind === 'bridge');
const intentCoordinator = normalizeAddress(
process.env.INTENT_BRIDGE_COORDINATOR_V2_ADDRESS ||
(request.sourceChainId === 138 ? DEFAULT_INTENT_BRIDGE_COORDINATOR_V2 : '')
);
const sourceRoutePlan = sourceSwapLegs.length > 0
? this.buildEncodedRoutePlan(
request,
sourceSwapLegs,
sourceResolution.lookupAddress,
bridgeLeg?.tokenInAddress || sourceResolution.lookupAddress,
intentCoordinator || ROUTER_V2_RECIPIENT_PLACEHOLDER
)
: undefined;
const destinationRoutePlan = destinationSwapLegs.length > 0
? this.buildEncodedRoutePlan(
{
...request,
sourceChainId: request.destinationChainId || request.sourceChainId,
recipient: request.recipient,
},
destinationSwapLegs,
bridgeLeg?.tokenOutAddress || destinationResolution.lookupAddress,
destinationResolution.lookupAddress
) || this.emptyEncodedRoutePlan(
request.destinationChainId || request.sourceChainId,
bridgeLeg?.tokenOutAddress || destinationResolution.lookupAddress,
destinationResolution.lookupAddress,
destinationSwapLegs[destinationSwapLegs.length - 1]?.estimatedAmountOut || bridgeLeg?.estimatedAmountOut || request.amountIn,
request.recipient || ROUTER_V2_RECIPIENT_PLACEHOLDER
)
: this.emptyEncodedRoutePlan(
request.destinationChainId || request.sourceChainId,
bridgeLeg?.tokenOutAddress || destinationResolution.lookupAddress,
destinationResolution.lookupAddress,
bridgeLeg?.estimatedAmountOut || request.amountIn,
request.recipient || ROUTER_V2_RECIPIENT_PLACEHOLDER
);
const bridgeIntentPlan: EncodedBridgeIntentPlan | undefined =
bridgeLeg && sourceRoutePlan && intentCoordinator
? {
sourcePlan: {
...sourceRoutePlan,
recipient: intentCoordinator,
},
bridgeType: bridgeLeg.bridgeType || 'CCIP',
bridgeData: abiCoder.encode(
['address', 'string'],
[bridgeLeg.bridgeAddress || ZeroAddress, bridgeLeg.bridgeType || 'CCIP']
),
destinationPlan: destinationRoutePlan,
recipient: normalizeAddress(request.recipient || ROUTER_V2_RECIPIENT_PLACEHOLDER),
deadline: String(Math.floor(Date.now() / 1000) + 300),
}
: undefined;
return this.toResponse(
request,
best,
sortedCandidates.slice(1, 4),
'Cross-chain best execution selected from source swap, bridge registry, and destination swap candidates.',
undefined,
bridgeIntentPlan
);
}
private async planSegment(
chainId: number,
tokenIn: string,
tokenOut: string,
amountIn: bigint,
policy: RoutingPolicy,
maxLegs: number
): Promise<RouteCandidate | null> {
if (tokenIn === tokenOut) {
return {
decision: 'direct-pool',
estimatedAmountOut: amountIn,
estimatedGasUsd: 0,
score: normalizedOutput(chainId, tokenOut, amountIn),
legs: [],
selectedRouteReason: 'Token already matches bridge asset or destination asset.',
rejectedAlternatives: [],
riskFlags: [],
};
}
const request: PlannerRequest = {
sourceChainId: chainId,
tokenIn,
tokenOut,
amountIn: amountIn.toString(),
constraints: {
complianceProfile: policy.profile,
allowBridge: false,
maxLegs,
allowedProviders: policy.allowedProviders,
allowedIntermediates: policy.defaultIntermediateAddresses,
allowCommodityIntermediates: policy.allowCommodityIntermediates,
},
};
const response = await this.planOneChain(request, {
...policy,
maxLegs,
});
if (!response.estimatedAmountOut || response.legs.length === 0) {
return null;
}
return {
decision: response.decision,
estimatedAmountOut: safeBigInt(response.estimatedAmountOut),
estimatedGasUsd: response.estimatedGasUsd,
score: normalizedOutput(chainId, tokenOut, safeBigInt(response.estimatedAmountOut)) - response.estimatedGasUsd,
legs: response.legs,
selectedRouteReason: response.selectedRouteReason,
rejectedAlternatives: response.rejectedAlternatives,
riskFlags: response.riskFlags,
};
}
private toCandidate(
legs: PlannerLeg[],
estimatedAmountOut: bigint,
chainId: number,
outputToken: string,
policy: RoutingPolicy
): RouteCandidate {
const estimatedGasUsd = legs.reduce((total, leg) => total + PROVIDER_GAS_USD[leg.provider], 0);
const stalePenalty = legs.reduce((total, leg) => {
const freshness = leg.freshnessSeconds || 0;
return total + (freshness > 1800 ? 0.15 : freshness > 300 ? 0.05 : 0);
}, 0);
const hopPenalty = Math.max(0, legs.length - 1) * 0.03;
const score = normalizedOutput(chainId, outputToken, estimatedAmountOut) - estimatedGasUsd - stalePenalty - hopPenalty;
return {
decision: legs.length === 1 ? 'direct-pool' : 'multi-hop',
estimatedAmountOut,
estimatedGasUsd,
score,
legs,
selectedRouteReason: legs.length === 1
? legs[0].provider === 'dodo_v3'
? 'Selected live DODO v3 / D3MM pilot quote for the requested direct pair.'
: `Selected deepest eligible ${legs[0].provider} pool for the requested pair.`
: `Selected multi-hop path through ${legs.map((leg) => leg.tokenOutSymbol || leg.tokenOutAddress).join(' -> ')}.`,
rejectedAlternatives: [],
riskFlags: computeRiskFlags(legs, policy),
};
}
private toResponse(
request: PlannerRequest,
best: RouteCandidate,
alternatives: RouteCandidate[],
selectedRouteReason: string,
routePlan?: EncodedPlannerRoutePlan,
bridgeIntentPlan?: EncodedBridgeIntentPlan
): PlannerResponse {
const planId = Buffer.from(this.requestHash(request, resolveRoutingPolicy(request.sourceChainId, request.constraints || {}))).toString('base64url');
const alternativePayloads: PlannerAlternative[] = alternatives.map((candidate, index) => ({
routeId: `${planId}-alt-${index + 1}`,
decision: candidate.decision,
estimatedAmountOut: candidate.estimatedAmountOut.toString(),
estimatedGasUsd: Number(candidate.estimatedGasUsd.toFixed(4)),
providerPath: candidate.legs.map((leg) => leg.provider),
legCount: candidate.legs.length,
score: Number(candidate.score.toFixed(6)),
notes: [candidate.selectedRouteReason, ...candidate.riskFlags],
}));
const maxFreshness = best.legs.reduce<number | null>((current, leg) => {
if (leg.freshnessSeconds === null || leg.freshnessSeconds === undefined) return current;
if (current === null) return leg.freshnessSeconds;
return Math.max(current, leg.freshnessSeconds);
}, null);
return {
planId,
generatedAt: new Date().toISOString(),
decision: best.decision,
sourceChainId: request.sourceChainId,
destinationChainId: request.destinationChainId || request.sourceChainId,
tokenIn: request.tokenIn,
tokenOut: request.tokenOut,
estimatedAmountOut: best.estimatedAmountOut.toString(),
minAmountOut: best.legs.length > 0 ? best.legs[best.legs.length - 1].minAmountOut : request.amountIn,
estimatedGasUsd: Number(best.estimatedGasUsd.toFixed(4)),
legs: best.legs,
alternatives: alternativePayloads,
confidenceScore: Number(computeConfidence(best.legs).toFixed(4)),
riskFlags: best.riskFlags,
selectedRouteReason,
rejectedAlternatives: best.rejectedAlternatives,
staleness: {
maxFreshnessSeconds: maxFreshness,
hasStaleLeg: best.legs.some((leg) => (leg.freshnessSeconds || 0) > 300),
},
routePlan,
bridgeIntentPlan,
};
}
private emptyResponse(
request: PlannerRequest,
decision: PlannerDecision,
reason: string
): PlannerResponse {
const planId = Buffer.from(this.requestHash(request, resolveRoutingPolicy(request.sourceChainId, request.constraints || {}))).toString('base64url');
return {
planId,
generatedAt: new Date().toISOString(),
decision,
sourceChainId: request.sourceChainId,
destinationChainId: request.destinationChainId || request.sourceChainId,
tokenIn: request.tokenIn,
tokenOut: request.tokenOut,
estimatedAmountOut: null,
minAmountOut: null,
estimatedGasUsd: 0,
legs: [],
alternatives: [],
confidenceScore: 0,
riskFlags: ['no-route'],
selectedRouteReason: reason,
rejectedAlternatives: [],
staleness: {
maxFreshnessSeconds: null,
hasStaleLeg: false,
},
};
}
private buildEncodedRoutePlan(
request: PlannerRequest,
legs: PlannerLeg[],
inputToken: string,
outputToken: string,
recipientOverride?: string
): EncodedPlannerRoutePlan | undefined {
if (!legs.every((leg) => PROVIDER_ENUM[leg.provider] !== undefined && Boolean(providerDataHexForLeg(leg)))) {
return undefined;
}
const recipient = normalizeAddress(recipientOverride || request.recipient || ROUTER_V2_RECIPIENT_PLACEHOLDER);
const deadline = String(Math.floor(Date.now() / 1000) + 300);
const amountIn = legs[0]?.estimatedAmountIn || request.amountIn;
const minAmountOut = legs[legs.length - 1]?.minAmountOut || request.amountIn;
return {
chainId: request.sourceChainId,
inputToken,
outputToken,
amountIn,
minAmountOut,
recipient,
deadline,
legs: legs.map((leg, index) => ({
provider: PROVIDER_ENUM[leg.provider] as number,
tokenIn: leg.tokenInAddress,
tokenOut: leg.tokenOutAddress,
amountSource: index === 0 ? 0 : 1,
minAmountOut: leg.minAmountOut,
target: leg.target || ZeroAddress,
providerData: providerDataHexForLeg(leg) as string,
})),
};
}
private emptyEncodedRoutePlan(
chainId: number,
inputToken: string,
outputToken: string,
amountIn: string,
recipient: string
): EncodedPlannerRoutePlan {
return {
chainId,
inputToken,
outputToken,
amountIn,
minAmountOut: amountIn,
recipient: normalizeAddress(recipient),
deadline: String(Math.floor(Date.now() / 1000) + 300),
legs: [],
};
}
}

View File

@@ -0,0 +1,58 @@
import { estimateChain138DodoLiquidityUsd } from './chain138-dodo-liquidity';
describe('estimateChain138DodoLiquidityUsd', () => {
it('values Chain 138 stable-to-stable DODO pools with token decimals', () => {
const result = estimateChain138DodoLiquidityUsd({
token0Address: '0xf22258f57794CC8E06237084b353Ab30fFfa640b',
token1Address: '0x71D6687F38b93CCad569Fa6352c876eea967201b',
reserve0: 999_999_997_998n,
reserve1: 999_999_997_998n,
});
expect(result.reserve0Usd).toBeCloseTo(999_999.997998, 6);
expect(result.reserve1Usd).toBeCloseTo(999_999.997998, 6);
expect(result.totalLiquidityUsd).toBeCloseTo(1_999_999.995996, 6);
});
it('values WETH/stable DODO pools with stable decimals and oracle price', () => {
const result = estimateChain138DodoLiquidityUsd({
token0Address: '0xf4BB2e28688e89fCcE3c0580D37d36A7672E8A9f',
token1Address: '0x004b63A7B5b0E06f6bB6adb4a5F9f590BF3182D1',
reserve0: 50n * 10n ** 18n,
reserve1: 105_830n * 10n ** 6n,
price: 2_100n * 10n ** 18n,
});
expect(result.reserve0Usd).toBe(105_000);
expect(result.reserve1Usd).toBe(105_830);
expect(result.totalLiquidityUsd).toBe(210_830);
});
it('keeps non-USD pairs at zero without a usable USD side', () => {
const result = estimateChain138DodoLiquidityUsd({
token0Address: '0xf4BB2e28688e89fCcE3c0580D37d36A7672E8A9f',
token1Address: '0x290E52a8819A4fbD0714E517225429aA2B70EC6b',
reserve0: 10n * 10n ** 18n,
reserve1: 5n * 10n ** 6n,
});
expect(result).toEqual({
reserve0Usd: 0,
reserve1Usd: 0,
totalLiquidityUsd: 0,
});
});
it('values cBTC/stable DODO pools using satoshi precision and the BTC fallback price', () => {
const result = estimateChain138DodoLiquidityUsd({
token0Address: '0xcb7c000000000000000000000000000000000138',
token1Address: '0x004b63A7B5b0E06f6bB6adb4a5F9f590BF3182D1',
reserve0: 2n * 10n ** 8n,
reserve1: 181_000n * 10n ** 6n,
});
expect(result.reserve0Usd).toBe(180_000);
expect(result.reserve1Usd).toBe(181_000);
expect(result.totalLiquidityUsd).toBe(361_000);
});
});

View File

@@ -0,0 +1,113 @@
import { formatUnits } from 'ethers';
import { getCanonicalTokenByAddress } from '../config/canonical-tokens';
const CHAIN_138 = 138;
const DEFAULT_WETH_USD_PRICE = 2100;
const DEFAULT_BTC_USD_PRICE = 90000;
export interface Chain138DodoLiquidityUsd {
reserve0Usd: number;
reserve1Usd: number;
totalLiquidityUsd: number;
}
function normalizeAddress(value?: string): string {
return String(value || '').trim().toLowerCase();
}
function decimalsForAddress(address: string): number {
const spec = getCanonicalTokenByAddress(CHAIN_138, normalizeAddress(address));
return Number(spec?.decimals ?? 18);
}
function isUsdAddress(address: string): boolean {
const spec = getCanonicalTokenByAddress(CHAIN_138, normalizeAddress(address));
return spec?.currencyCode === 'USD';
}
function isWethLikeAddress(address: string): boolean {
const symbol = getCanonicalTokenByAddress(CHAIN_138, normalizeAddress(address))?.symbol?.toUpperCase();
return symbol === 'WETH' || symbol === 'WETH10';
}
function isBtcLikeAddress(address: string): boolean {
const symbol = getCanonicalTokenByAddress(CHAIN_138, normalizeAddress(address))?.symbol?.toUpperCase();
return symbol === 'CBTC';
}
function parseAmount(value: bigint, decimals: number): number {
if (value <= 0n) return 0;
const parsed = Number(formatUnits(value, decimals));
return Number.isFinite(parsed) && parsed > 0 ? parsed : 0;
}
function parsePrice(price?: bigint): number {
if (!price || price <= 0n) return 0;
const parsed = Number(formatUnits(price, 18));
return Number.isFinite(parsed) && parsed > 0 ? parsed : 0;
}
export function estimateChain138DodoLiquidityUsd(args: {
token0Address: string;
token1Address: string;
reserve0: bigint;
reserve1: bigint;
price?: bigint;
}): Chain138DodoLiquidityUsd {
const token0Address = normalizeAddress(args.token0Address);
const token1Address = normalizeAddress(args.token1Address);
const reserve0Amount = parseAmount(args.reserve0, decimalsForAddress(token0Address));
const reserve1Amount = parseAmount(args.reserve1, decimalsForAddress(token1Address));
const price = parsePrice(args.price);
const token0IsUsd = isUsdAddress(token0Address);
const token1IsUsd = isUsdAddress(token1Address);
if (reserve0Amount <= 0 || reserve1Amount <= 0) {
return { reserve0Usd: 0, reserve1Usd: 0, totalLiquidityUsd: 0 };
}
if (token0IsUsd && token1IsUsd) {
return {
reserve0Usd: reserve0Amount,
reserve1Usd: reserve1Amount,
totalLiquidityUsd: reserve0Amount + reserve1Amount,
};
}
if (token1IsUsd) {
const reserve0Usd =
price > 0
? reserve0Amount * price
: isWethLikeAddress(token0Address)
? reserve0Amount * DEFAULT_WETH_USD_PRICE
: isBtcLikeAddress(token0Address)
? reserve0Amount * DEFAULT_BTC_USD_PRICE
: 0;
return {
reserve0Usd,
reserve1Usd: reserve1Amount,
totalLiquidityUsd: reserve0Usd > 0 ? reserve0Usd + reserve1Amount : 0,
};
}
if (token0IsUsd) {
const reserve1Usd =
price > 0
? reserve1Amount / price
: isWethLikeAddress(token1Address)
? reserve1Amount * DEFAULT_WETH_USD_PRICE
: isBtcLikeAddress(token1Address)
? reserve1Amount * DEFAULT_BTC_USD_PRICE
: 0;
return {
reserve0Usd: reserve0Amount,
reserve1Usd,
totalLiquidityUsd: reserve1Usd > 0 ? reserve0Amount + reserve1Usd : 0,
};
}
return { reserve0Usd: 0, reserve1Usd: 0, totalLiquidityUsd: 0 };
}

View File

@@ -0,0 +1,562 @@
import { AbiCoder, Contract, JsonRpcProvider, formatUnits } from 'ethers';
import { resolveChain138RpcUrl } from '../config/chain138-rpc';
import { SwapGraphEdge } from './planner-v2-types';
const CHAIN_138 = 138;
const abiCoder = AbiCoder.defaultAbiCoder();
const DEFAULT_PILOT_UNISWAP_ROUTER = '0xd164d9ccfacf5d9f91698f296ae0cd245d964384';
const DEFAULT_NATIVE_UNISWAP_FACTORY = '0x2f7219276e3ce367db9ec74c1196a8ecee67841c';
const DEFAULT_NATIVE_UNISWAP_ROUTER = '0xde9cd8ee2811e6e64a41d5f68be315d33995975e';
const DEFAULT_NATIVE_UNISWAP_QUOTER = '0x6abbb1ceb2468e748a03a00cd6aa9bfe893afa1f';
const DEFAULT_NATIVE_UNISWAP_WETH_USDT_POOL = '0xa893add35aefe6a6d858eb01828be4592f12c9f5';
const DEFAULT_NATIVE_UNISWAP_WETH_USDC_POOL = '0xec745bfb6b3cd32f102d594e5f432d8d85b19391';
const DEFAULT_BALANCER_VAULT = '0x96423d7c1727698d8a25ebfb88131e9422d1a3c3';
const DEFAULT_CURVE_3POOL = '0xe440ec15805be4c7babcd17a63b8c8a08a492e0f';
const DEFAULT_ONEINCH_ROUTER = '0x500b84b1bc6f59c1898a5fe538ea20a758757a4f';
const DEFAULT_BALANCER_WETH_USDT_POOL_ID = '0x877cd220759e8c94b82f55450c85d382ae06856c426b56d93092a420facbc324';
const DEFAULT_BALANCER_WETH_USDC_POOL_ID = '0xd8dfb18a6baf9b29d8c2dbd74639db87ac558af120df5261dab8e2a5de69013b';
const DEFAULT_PILOT_UNISWAP_FEE = 3000;
const DEFAULT_NATIVE_UNISWAP_FEE = 500;
const DEFAULT_WETH_USD_PRICE = 2100;
const WETH = '0xc02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2'.toLowerCase();
const USDT = '0x004b63A7B5b0E06f6bB6adb4a5F9f590BF3182D1'.toLowerCase();
const USDC = '0x71D6687F38b93CCad569Fa6352c876eea967201b'.toLowerCase();
const uniswapAbi = [
'function getPairReserves(address tokenA,address tokenB,uint24 fee) view returns (uint256 reserveIn,uint256 reserveOut,bool exists)',
];
const nativeUniswapFactoryAbi = [
'function getPool(address tokenA,address tokenB,uint24 fee) view returns (address)',
];
const erc20Abi = [
'function balanceOf(address account) view returns (uint256)',
];
const balancerAbi = [
'function getPoolTokens(bytes32 poolId) view returns (address[] tokens, uint256[] balances, uint256 lastChangeBlock)',
];
const curveAbi = [
'function reserves(uint256 index) view returns (uint256)',
];
const oneInchAbi = [
'function getRouteReserves(address tokenA,address tokenB) view returns (uint256 reserveIn,uint256 reserveOut,bool exists)',
];
function normalizeAddress(value?: string): string {
return String(value || '').trim().toLowerCase();
}
function providerDataHexForUniswap(router: string, quoter: string, fee: number): string {
return abiCoder.encode(['bytes', 'uint24', 'address', 'bool'], ['0x', fee, quoter, false]);
}
function providerDataHexForBalancer(poolId: string): string {
return abiCoder.encode(['bytes32'], [poolId]);
}
function providerDataHexForCurve(): string {
return abiCoder.encode(['int128', 'int128', 'bool'], [0, 1, false]);
}
function providerDataHexForOneInch(router: string): string {
return abiCoder.encode(['address', 'address', 'bytes'], [router, router, '0x']);
}
let sharedProvider: JsonRpcProvider | null = null;
let sharedProviderUrl = '';
function getProvider(rpcUrl: string): JsonRpcProvider {
if (!sharedProvider || sharedProviderUrl !== rpcUrl) {
sharedProvider = new JsonRpcProvider(rpcUrl);
sharedProviderUrl = rpcUrl;
}
return sharedProvider;
}
function totalLiquidityUsdForWethPair(reserveWeth: bigint, reserveStable: bigint): number {
return Number(formatUnits(reserveWeth, 18)) * DEFAULT_WETH_USD_PRICE + Number(formatUnits(reserveStable, 6));
}
function totalLiquidityUsdForStablePair(reserveA: bigint, reserveB: bigint): number {
return Number(formatUnits(reserveA, 6)) + Number(formatUnits(reserveB, 6));
}
function isNativeUniswapConfigured(router: string, quoter: string): boolean {
return Boolean(
process.env.CHAIN138_UNISWAP_V3_NATIVE_FACTORY ||
process.env.CHAIN_138_UNISWAP_V3_FACTORY ||
process.env.CHAIN138_UNISWAP_V3_NATIVE_WETH_USDT_POOL ||
process.env.CHAIN138_UNISWAP_V3_NATIVE_WETH_USDC_POOL ||
router === DEFAULT_NATIVE_UNISWAP_ROUTER ||
quoter === DEFAULT_NATIVE_UNISWAP_QUOTER ||
(router && router !== DEFAULT_PILOT_UNISWAP_ROUTER)
);
}
async function resolveNativeUniswapPoolAddress(args: {
factory: string;
configuredPool: string;
tokenA: string;
tokenB: string;
fee: number;
rpcUrl: string;
}): Promise<string> {
if (args.configuredPool) {
return args.configuredPool;
}
if (!args.factory) {
return '';
}
try {
const factory = new Contract(args.factory, nativeUniswapFactoryAbi, getProvider(args.rpcUrl));
return normalizeAddress(await factory.getPool(args.tokenA, args.tokenB, args.fee));
} catch {
return '';
}
}
async function erc20Balance(token: string, account: string, rpcUrl: string): Promise<bigint> {
const contract = new Contract(token, erc20Abi, getProvider(rpcUrl));
const balance = await contract.balanceOf(account);
return BigInt(String(balance));
}
function buildUniswapEdges(args: {
router: string;
quoter: string;
fee: number;
reserveWeth: bigint;
reserveStable: bigint;
stableAddress: string;
stableSymbol: 'USDT' | 'USDC';
notes: string[];
}): SwapGraphEdge[] {
const edges: SwapGraphEdge[] = [];
if (args.reserveWeth <= 0n || args.reserveStable <= 0n) {
return edges;
}
const providerDataHex = providerDataHexForUniswap(args.router, args.quoter, args.fee);
const liquidity = totalLiquidityUsdForWethPair(args.reserveWeth, args.reserveStable);
edges.push(
{
kind: 'swap',
provider: 'uniswap_v3',
chainId: CHAIN_138,
tokenInAddress: WETH,
tokenOutAddress: args.stableAddress,
tokenInSymbol: 'WETH',
tokenOutSymbol: args.stableSymbol,
reserveIn: args.reserveWeth.toString(),
reserveOut: args.reserveStable.toString(),
target: args.router,
providerData: { fee: args.fee, quoter: args.quoter },
providerDataHex,
totalLiquidityUsd: liquidity,
freshnessSeconds: 0,
notes: args.notes,
},
{
kind: 'swap',
provider: 'uniswap_v3',
chainId: CHAIN_138,
tokenInAddress: args.stableAddress,
tokenOutAddress: WETH,
tokenInSymbol: args.stableSymbol,
tokenOutSymbol: 'WETH',
reserveIn: args.reserveStable.toString(),
reserveOut: args.reserveWeth.toString(),
target: args.router,
providerData: { fee: args.fee, quoter: args.quoter },
providerDataHex,
totalLiquidityUsd: liquidity,
freshnessSeconds: 0,
notes: args.notes,
}
);
return edges;
}
async function getNativeUniswapEdges(args: {
rpcUrl: string;
router: string;
quoter: string;
feeUsdt: number;
feeUsdc: number;
}): Promise<SwapGraphEdge[]> {
const factory = normalizeAddress(
process.env.CHAIN_138_UNISWAP_V3_FACTORY ||
process.env.CHAIN138_UNISWAP_V3_NATIVE_FACTORY ||
DEFAULT_NATIVE_UNISWAP_FACTORY
);
const configuredWethUsdtPool = normalizeAddress(
process.env.UNISWAP_V3_WETH_USDT_POOL ||
process.env.CHAIN138_UNISWAP_V3_NATIVE_WETH_USDT_POOL ||
DEFAULT_NATIVE_UNISWAP_WETH_USDT_POOL
);
const configuredWethUsdcPool = normalizeAddress(
process.env.UNISWAP_V3_WETH_USDC_POOL ||
process.env.CHAIN138_UNISWAP_V3_NATIVE_WETH_USDC_POOL ||
DEFAULT_NATIVE_UNISWAP_WETH_USDC_POOL
);
const [wethUsdtPool, wethUsdcPool] = await Promise.all([
resolveNativeUniswapPoolAddress({
factory,
configuredPool: configuredWethUsdtPool,
tokenA: WETH,
tokenB: USDT,
fee: args.feeUsdt,
rpcUrl: args.rpcUrl,
}),
resolveNativeUniswapPoolAddress({
factory,
configuredPool: configuredWethUsdcPool,
tokenA: WETH,
tokenB: USDC,
fee: args.feeUsdc,
rpcUrl: args.rpcUrl,
}),
]);
const [wethUsdtBalances, wethUsdcBalances] = await Promise.all([
wethUsdtPool
? Promise.all([
erc20Balance(WETH, wethUsdtPool, args.rpcUrl),
erc20Balance(USDT, wethUsdtPool, args.rpcUrl),
])
: Promise.resolve<[bigint, bigint]>([0n, 0n]),
wethUsdcPool
? Promise.all([
erc20Balance(WETH, wethUsdcPool, args.rpcUrl),
erc20Balance(USDC, wethUsdcPool, args.rpcUrl),
])
: Promise.resolve<[bigint, bigint]>([0n, 0n]),
]);
return [
...buildUniswapEdges({
router: args.router,
quoter: args.quoter,
fee: args.feeUsdt,
reserveWeth: wethUsdtBalances[0],
reserveStable: wethUsdtBalances[1],
stableAddress: USDT,
stableSymbol: 'USDT',
notes: ['Chain 138 upstream-native Uniswap v3 WETH/USDT venue.'],
}),
...buildUniswapEdges({
router: args.router,
quoter: args.quoter,
fee: args.feeUsdc,
reserveWeth: wethUsdcBalances[0],
reserveStable: wethUsdcBalances[1],
stableAddress: USDC,
stableSymbol: 'USDC',
notes: ['Chain 138 upstream-native Uniswap v3 WETH/USDC venue.'],
}),
];
}
async function getPilotUniswapEdges(args: {
rpcUrl: string;
router: string;
quoter: string;
feeUsdt: number;
feeUsdc: number;
}): Promise<SwapGraphEdge[]> {
const contract = new Contract(args.router, uniswapAbi, getProvider(args.rpcUrl));
const [wethUsdt, wethUsdc] = await Promise.all([
contract.getPairReserves(WETH, USDT, args.feeUsdt),
contract.getPairReserves(WETH, USDC, args.feeUsdc),
]);
return [
...(Boolean(wethUsdt[2])
? buildUniswapEdges({
router: args.router,
quoter: args.quoter,
fee: args.feeUsdt,
reserveWeth: BigInt(String(wethUsdt[0])),
reserveStable: BigInt(String(wethUsdt[1])),
stableAddress: USDT,
stableSymbol: 'USDT',
notes: ['Chain 138 pilot-compatible Uniswap v3 WETH/USDT venue.'],
})
: []),
...(Boolean(wethUsdc[2])
? buildUniswapEdges({
router: args.router,
quoter: args.quoter,
fee: args.feeUsdc,
reserveWeth: BigInt(String(wethUsdc[0])),
reserveStable: BigInt(String(wethUsdc[1])),
stableAddress: USDC,
stableSymbol: 'USDC',
notes: ['Chain 138 pilot-compatible Uniswap v3 WETH/USDC venue.'],
})
: []),
];
}
async function getUniswapEdges(rpcUrl: string): Promise<SwapGraphEdge[]> {
const router = normalizeAddress(process.env.UNISWAP_V3_ROUTER || DEFAULT_NATIVE_UNISWAP_ROUTER);
const quoter = normalizeAddress(
process.env.UNISWAP_QUOTER_ADDRESS ||
process.env.UNISWAP_QUOTER ||
(router === DEFAULT_PILOT_UNISWAP_ROUTER ? router : DEFAULT_NATIVE_UNISWAP_QUOTER)
);
if (!router || !quoter) {
return [];
}
if (isNativeUniswapConfigured(router, quoter)) {
return getNativeUniswapEdges({
rpcUrl,
router,
quoter,
feeUsdt: Number(process.env.UNISWAP_V3_WETH_USDT_FEE || DEFAULT_NATIVE_UNISWAP_FEE),
feeUsdc: Number(process.env.UNISWAP_V3_WETH_USDC_FEE || DEFAULT_NATIVE_UNISWAP_FEE),
});
}
return getPilotUniswapEdges({
rpcUrl,
router,
quoter,
feeUsdt: Number(process.env.UNISWAP_V3_WETH_USDT_FEE || DEFAULT_PILOT_UNISWAP_FEE),
feeUsdc: Number(process.env.UNISWAP_V3_WETH_USDC_FEE || DEFAULT_PILOT_UNISWAP_FEE),
});
}
async function getBalancerEdges(rpcUrl: string): Promise<SwapGraphEdge[]> {
const vault = normalizeAddress(process.env.BALANCER_VAULT || DEFAULT_BALANCER_VAULT);
const wethUsdtPoolId = process.env.BALANCER_WETH_USDT_POOL_ID || DEFAULT_BALANCER_WETH_USDT_POOL_ID;
const wethUsdcPoolId = process.env.BALANCER_WETH_USDC_POOL_ID || DEFAULT_BALANCER_WETH_USDC_POOL_ID;
if (!vault) {
return [];
}
const contract = new Contract(vault, balancerAbi, getProvider(rpcUrl));
const edges: SwapGraphEdge[] = [];
for (const pair of [
{ poolId: wethUsdtPoolId, stable: USDT, stableSymbol: 'USDT' },
{ poolId: wethUsdcPoolId, stable: USDC, stableSymbol: 'USDC' },
]) {
if (!pair.poolId) continue;
const result = await contract.getPoolTokens(pair.poolId);
const tokens = (result[0] as string[]).map(normalizeAddress);
const balances = (result[1] as bigint[]).map((value) => BigInt(String(value)));
const wethIndex = tokens.indexOf(WETH);
const stableIndex = tokens.indexOf(pair.stable);
if (wethIndex === -1 || stableIndex === -1) continue;
const reserveWeth = balances[wethIndex];
const reserveStable = balances[stableIndex];
const providerDataHex = providerDataHexForBalancer(pair.poolId);
const liquidity = totalLiquidityUsdForWethPair(reserveWeth, reserveStable);
edges.push(
{
kind: 'swap',
provider: 'balancer',
chainId: CHAIN_138,
tokenInAddress: WETH,
tokenOutAddress: pair.stable,
tokenInSymbol: 'WETH',
tokenOutSymbol: pair.stableSymbol,
reserveIn: reserveWeth.toString(),
reserveOut: reserveStable.toString(),
target: vault,
providerData: { poolId: pair.poolId },
providerDataHex,
totalLiquidityUsd: liquidity,
freshnessSeconds: 0,
notes: [`Chain 138 pilot-compatible Balancer ${pair.stableSymbol}/WETH venue.`],
},
{
kind: 'swap',
provider: 'balancer',
chainId: CHAIN_138,
tokenInAddress: pair.stable,
tokenOutAddress: WETH,
tokenInSymbol: pair.stableSymbol,
tokenOutSymbol: 'WETH',
reserveIn: reserveStable.toString(),
reserveOut: reserveWeth.toString(),
target: vault,
providerData: { poolId: pair.poolId },
providerDataHex,
totalLiquidityUsd: liquidity,
freshnessSeconds: 0,
notes: [`Chain 138 pilot-compatible Balancer ${pair.stableSymbol}/WETH venue.`],
}
);
}
return edges;
}
async function getCurveEdges(rpcUrl: string): Promise<SwapGraphEdge[]> {
const curvePool = normalizeAddress(process.env.CURVE_3POOL || DEFAULT_CURVE_3POOL);
if (!curvePool) {
return [];
}
const contract = new Contract(curvePool, curveAbi, getProvider(rpcUrl));
const [reserveUsdtRaw, reserveUsdcRaw] = await Promise.all([contract.reserves(0), contract.reserves(1)]);
const reserveUsdt = BigInt(String(reserveUsdtRaw));
const reserveUsdc = BigInt(String(reserveUsdcRaw));
const providerDataHex = providerDataHexForCurve();
const liquidity = totalLiquidityUsdForStablePair(reserveUsdt, reserveUsdc);
return [
{
kind: 'swap',
provider: 'curve',
chainId: CHAIN_138,
tokenInAddress: USDT,
tokenOutAddress: USDC,
tokenInSymbol: 'USDT',
tokenOutSymbol: 'USDC',
reserveIn: reserveUsdt.toString(),
reserveOut: reserveUsdc.toString(),
target: curvePool,
providerData: { i: 0, j: 1, useUnderlying: false },
providerDataHex,
totalLiquidityUsd: liquidity,
freshnessSeconds: 0,
notes: ['Chain 138 pilot-compatible Curve 3Pool stable/stable venue.'],
},
{
kind: 'swap',
provider: 'curve',
chainId: CHAIN_138,
tokenInAddress: USDC,
tokenOutAddress: USDT,
tokenInSymbol: 'USDC',
tokenOutSymbol: 'USDT',
reserveIn: reserveUsdc.toString(),
reserveOut: reserveUsdt.toString(),
target: curvePool,
providerData: { i: 0, j: 1, useUnderlying: false },
providerDataHex,
totalLiquidityUsd: liquidity,
freshnessSeconds: 0,
notes: ['Chain 138 pilot-compatible Curve 3Pool stable/stable venue.'],
},
];
}
async function getOneInchEdges(rpcUrl: string): Promise<SwapGraphEdge[]> {
const router = normalizeAddress(process.env.ONEINCH_ROUTER || DEFAULT_ONEINCH_ROUTER);
if (!router) {
return [];
}
const contract = new Contract(router, oneInchAbi, getProvider(rpcUrl));
const [wethUsdt, wethUsdc] = await Promise.all([
contract.getRouteReserves(WETH, USDT),
contract.getRouteReserves(WETH, USDC),
]);
const providerDataHex = providerDataHexForOneInch(router);
const edges: SwapGraphEdge[] = [];
if (Boolean(wethUsdt[2])) {
const reserveWeth = BigInt(String(wethUsdt[0]));
const reserveUsdt = BigInt(String(wethUsdt[1]));
const liquidity = totalLiquidityUsdForWethPair(reserveWeth, reserveUsdt);
edges.push(
{
kind: 'swap',
provider: 'one_inch',
chainId: CHAIN_138,
tokenInAddress: WETH,
tokenOutAddress: USDT,
tokenInSymbol: 'WETH',
tokenOutSymbol: 'USDT',
reserveIn: reserveWeth.toString(),
reserveOut: reserveUsdt.toString(),
target: router,
providerData: { executor: router, allowanceTarget: router },
providerDataHex,
totalLiquidityUsd: liquidity,
freshnessSeconds: 0,
notes: ['Chain 138 pilot-compatible 1inch router lane.'],
},
{
kind: 'swap',
provider: 'one_inch',
chainId: CHAIN_138,
tokenInAddress: USDT,
tokenOutAddress: WETH,
tokenInSymbol: 'USDT',
tokenOutSymbol: 'WETH',
reserveIn: reserveUsdt.toString(),
reserveOut: reserveWeth.toString(),
target: router,
providerData: { executor: router, allowanceTarget: router },
providerDataHex,
totalLiquidityUsd: liquidity,
freshnessSeconds: 0,
notes: ['Chain 138 pilot-compatible 1inch router lane.'],
}
);
}
if (Boolean(wethUsdc[2])) {
const reserveWeth = BigInt(String(wethUsdc[0]));
const reserveUsdc = BigInt(String(wethUsdc[1]));
const liquidity = totalLiquidityUsdForWethPair(reserveWeth, reserveUsdc);
edges.push(
{
kind: 'swap',
provider: 'one_inch',
chainId: CHAIN_138,
tokenInAddress: WETH,
tokenOutAddress: USDC,
tokenInSymbol: 'WETH',
tokenOutSymbol: 'USDC',
reserveIn: reserveWeth.toString(),
reserveOut: reserveUsdc.toString(),
target: router,
providerData: { executor: router, allowanceTarget: router },
providerDataHex,
totalLiquidityUsd: liquidity,
freshnessSeconds: 0,
notes: ['Chain 138 pilot-compatible 1inch router lane.'],
},
{
kind: 'swap',
provider: 'one_inch',
chainId: CHAIN_138,
tokenInAddress: USDC,
tokenOutAddress: WETH,
tokenInSymbol: 'USDC',
tokenOutSymbol: 'WETH',
reserveIn: reserveUsdc.toString(),
reserveOut: reserveWeth.toString(),
target: router,
providerData: { executor: router, allowanceTarget: router },
providerDataHex,
totalLiquidityUsd: liquidity,
freshnessSeconds: 0,
notes: ['Chain 138 pilot-compatible 1inch router lane.'],
}
);
}
return edges;
}
export async function getChain138PilotVenueEdges(): Promise<SwapGraphEdge[]> {
const rpcUrl = resolveChain138RpcUrl();
const [uniswapEdges, balancerEdges, curveEdges, oneInchEdges] = await Promise.all([
getUniswapEdges(rpcUrl),
getBalancerEdges(rpcUrl),
getCurveEdges(rpcUrl),
getOneInchEdges(rpcUrl),
]);
return [...uniswapEdges, ...balancerEdges, ...curveEdges, ...oneInchEdges];
}

View File

@@ -0,0 +1,204 @@
import { AbiCoder, Contract, JsonRpcProvider, formatUnits } from 'ethers';
import { resolveChain138RpcUrl } from '../config/chain138-rpc';
import { SwapGraphEdge } from './planner-v2-types';
const CHAIN_138 = 138;
const DEFAULT_D3_POOL = '0x6550A3a59070061a262a893A1D6F3F490afFDBDA';
const DEFAULT_D3_PROXY = '0xc9a11abB7C63d88546Be24D58a6d95e3762cB843';
const DEFAULT_ROUTER_V2 = '0xF1c93F54A5C2fc0d7766Ccb0Ad8f157DFB4C99Ce';
const DEFAULT_WETH10 = '0xf4BB2e28688e89fCcE3c0580D37d36A7672E8A9f';
const DEFAULT_USDT = '0x004b63A7B5b0E06f6bB6adb4a5F9f590BF3182D1';
const abiCoder = AbiCoder.defaultAbiCoder();
const d3MmAbi = [
'function getTokenReserve(address token) view returns (uint256)',
'function querySellTokens(address fromToken,address toToken,uint256 fromAmount) view returns (uint256,uint256,uint256,uint256,uint256)',
] as const;
export interface DodoV3PilotConfig {
enabled: boolean;
chainId: number;
rpcUrl: string;
poolAddress: string;
proxyAddress: string;
weth10Address: string;
usdtAddress: string;
wethUsdPrice: number;
liquidityOverrideUsd?: number;
}
function normalizeAddress(value?: string): string {
return String(value || '').trim().toLowerCase();
}
function normalizeNumber(value?: string): number | undefined {
const parsed = Number(value || '');
return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;
}
export function encodeChain138DodoV3ProviderData(poolAddress: string): string {
return abiCoder.encode(['address'], [normalizeAddress(poolAddress)]);
}
export function isChain138DodoV3ExecutionLive(): boolean {
const enabled = process.env.CHAIN138_ENABLE_DODO_V3_EXECUTION !== '0';
const router = normalizeAddress(process.env.ENHANCED_SWAP_ROUTER_V2_ADDRESS || DEFAULT_ROUTER_V2);
return enabled && Boolean(router);
}
function getPilotConfig(): DodoV3PilotConfig {
return {
enabled: process.env.CHAIN138_ENABLE_DODO_V3_ROUTING !== '0',
chainId: CHAIN_138,
rpcUrl: resolveChain138RpcUrl(),
poolAddress: normalizeAddress(process.env.CHAIN138_D3_MM_ADDRESS || DEFAULT_D3_POOL),
proxyAddress: normalizeAddress(process.env.CHAIN138_D3_PROXY_ADDRESS || DEFAULT_D3_PROXY),
weth10Address: normalizeAddress(process.env.CHAIN138_D3_WETH10_ADDRESS || process.env.WETH10_ADDRESS || DEFAULT_WETH10),
usdtAddress: normalizeAddress(process.env.CHAIN138_D3_USDT_ADDRESS || process.env.USDT_ADDRESS_138 || DEFAULT_USDT),
wethUsdPrice: normalizeNumber(process.env.CHAIN138_D3_PILOT_WETH_USD)?.valueOf() || 2100,
liquidityOverrideUsd: normalizeNumber(process.env.CHAIN138_D3_PILOT_TOTAL_LIQUIDITY_USD),
};
}
let sharedProvider: JsonRpcProvider | null = null;
let sharedProviderUrl = '';
function getProvider(rpcUrl: string): JsonRpcProvider {
if (!sharedProvider || sharedProviderUrl !== rpcUrl) {
sharedProvider = new JsonRpcProvider(rpcUrl);
sharedProviderUrl = rpcUrl;
}
return sharedProvider;
}
function buildNotes(poolAddress: string, proxyAddress: string, executionLive: boolean): string[] {
return [
'DODO v3 / D3MM Chain 138 pilot venue.',
`Canonical private pilot pool ${poolAddress} executes through D3Proxy ${proxyAddress}.`,
executionLive
? 'Planner-v2 exposure and EnhancedSwapRouterV2 internal execution-plan calldata are live for the canonical pilot pair.'
: 'Planner-v2 exposure is live, but EnhancedSwapRouterV2 execution is disabled in the local environment.',
];
}
function approximateLiquidityUsd(args: {
wethReserve: bigint;
usdtReserve: bigint;
wethUsdPrice: number;
liquidityOverrideUsd?: number;
}): number {
if (args.liquidityOverrideUsd && Number.isFinite(args.liquidityOverrideUsd)) {
return args.liquidityOverrideUsd;
}
const wethSideUsd = Number(formatUnits(args.wethReserve, 18)) * args.wethUsdPrice;
const usdtSideUsd = Number(formatUnits(args.usdtReserve, 6));
return Number((wethSideUsd + usdtSideUsd).toFixed(2));
}
function isCanonicalPilotPair(config: DodoV3PilotConfig, tokenInAddress: string, tokenOutAddress: string): boolean {
const tokenIn = normalizeAddress(tokenInAddress);
const tokenOut = normalizeAddress(tokenOutAddress);
return (
(tokenIn === config.weth10Address && tokenOut === config.usdtAddress) ||
(tokenIn === config.usdtAddress && tokenOut === config.weth10Address)
);
}
export async function quoteChain138DodoV3AmountOut(args: {
tokenInAddress: string;
tokenOutAddress: string;
amountIn: bigint | string;
}): Promise<bigint> {
const config = getPilotConfig();
if (!config.enabled || !config.poolAddress || !config.proxyAddress) {
return 0n;
}
if (!isCanonicalPilotPair(config, args.tokenInAddress, args.tokenOutAddress)) {
return 0n;
}
const pool = new Contract(config.poolAddress, d3MmAbi, getProvider(config.rpcUrl));
const quote = await pool.querySellTokens(
normalizeAddress(args.tokenInAddress),
normalizeAddress(args.tokenOutAddress),
BigInt(args.amountIn)
);
return BigInt(String(quote[1]));
}
export async function getChain138DodoV3PilotEdges(): Promise<SwapGraphEdge[]> {
const config = getPilotConfig();
if (!config.enabled || !config.poolAddress || !config.proxyAddress || !config.weth10Address || !config.usdtAddress) {
return [];
}
const executionLive = isChain138DodoV3ExecutionLive();
const pool = new Contract(config.poolAddress, d3MmAbi, getProvider(config.rpcUrl));
const [wethReserveRaw, usdtReserveRaw] = await Promise.all([
pool.getTokenReserve(config.weth10Address),
pool.getTokenReserve(config.usdtAddress),
]);
const wethReserve = BigInt(String(wethReserveRaw));
const usdtReserve = BigInt(String(usdtReserveRaw));
if (wethReserve <= 0n || usdtReserve <= 0n) {
return [];
}
const totalLiquidityUsd = approximateLiquidityUsd({
wethReserve,
usdtReserve,
wethUsdPrice: config.wethUsdPrice,
liquidityOverrideUsd: config.liquidityOverrideUsd,
});
const notes = buildNotes(config.poolAddress, config.proxyAddress, executionLive);
return [
{
kind: 'swap',
provider: 'dodo_v3',
chainId: CHAIN_138,
tokenInAddress: config.weth10Address,
tokenOutAddress: config.usdtAddress,
tokenInSymbol: 'WETH10',
tokenOutSymbol: 'USDT',
reserveIn: wethReserve.toString(),
reserveOut: usdtReserve.toString(),
target: config.proxyAddress,
poolAddress: config.poolAddress,
providerData: {
poolAddress: config.poolAddress,
quoteMethod: 'querySellTokens',
executionTarget: config.proxyAddress,
},
providerDataHex: executionLive ? encodeChain138DodoV3ProviderData(config.poolAddress) : undefined,
totalLiquidityUsd,
freshnessSeconds: 0,
notes,
},
{
kind: 'swap',
provider: 'dodo_v3',
chainId: CHAIN_138,
tokenInAddress: config.usdtAddress,
tokenOutAddress: config.weth10Address,
tokenInSymbol: 'USDT',
tokenOutSymbol: 'WETH10',
reserveIn: usdtReserve.toString(),
reserveOut: wethReserve.toString(),
target: config.proxyAddress,
poolAddress: config.poolAddress,
providerData: {
poolAddress: config.poolAddress,
quoteMethod: 'querySellTokens',
executionTarget: config.proxyAddress,
},
providerDataHex: executionLive ? encodeChain138DodoV3ProviderData(config.poolAddress) : undefined,
totalLiquidityUsd,
freshnessSeconds: 0,
notes,
},
];
}

View File

@@ -0,0 +1,106 @@
import { Interface, ZeroAddress } from 'ethers';
import { BestExecutionPlanner } from './best-execution-planner';
import { EncodedBridgeIntentPlan, EncodedPlannerRoutePlan, PlannerRequest } from './planner-v2-types';
const CHAIN_138 = 138;
const DEFAULT_ROUTER_V2_ADDRESS = '0xf1c93f54a5c2fc0d7766ccb0ad8f157dfb4c99ce';
const DEFAULT_INTENT_BRIDGE_COORDINATOR_V2_ADDRESS = '0x7d0022b7e8360172fd9c0bb6778113b7ea3674e7';
const routeInterface = new Interface([
'function executeRoute((uint256 chainId,address inputToken,address outputToken,uint256 amountIn,uint256 minAmountOut,address recipient,uint256 deadline,(uint8 provider,address tokenIn,address tokenOut,uint8 amountSource,uint256 minAmountOut,address target,bytes providerData)[] legs) plan)',
]);
const intentInterface = new Interface([
'function executeIntent((((uint256 chainId,address inputToken,address outputToken,uint256 amountIn,uint256 minAmountOut,address recipient,uint256 deadline,(uint8 provider,address tokenIn,address tokenOut,uint8 amountSource,uint256 minAmountOut,address target,bytes providerData)[] legs),bytes32 bridgeType,bytes bridgeData,(uint256 chainId,address inputToken,address outputToken,uint256 amountIn,uint256 minAmountOut,address recipient,uint256 deadline,(uint8 provider,address tokenIn,address tokenOut,uint8 amountSource,uint256 minAmountOut,address target,bytes providerData)[] legs),address recipient,uint256 deadline)) intent)',
]);
export interface InternalExecutionPlanV2Result {
generatedAt: string;
plannerResponse: Awaited<ReturnType<BestExecutionPlanner['plan']>>;
execution?: {
kind: 'route' | 'bridge-intent';
contractAddress: string;
functionName: string;
signature: string;
args: [EncodedPlannerRoutePlan] | [EncodedBridgeIntentPlan];
encodedCalldata: string;
};
error?: string;
}
export class InternalExecutionPlanV2Builder {
private planner: BestExecutionPlanner;
constructor(planner = new BestExecutionPlanner()) {
this.planner = planner;
}
async build(request: PlannerRequest): Promise<InternalExecutionPlanV2Result> {
const plannerResponse = await this.planner.plan(request);
const generatedAt = new Date().toISOString();
if (plannerResponse.bridgeIntentPlan) {
const contractAddress = (
process.env.INTENT_BRIDGE_COORDINATOR_V2_ADDRESS ||
(plannerResponse.sourceChainId === CHAIN_138 ? DEFAULT_INTENT_BRIDGE_COORDINATOR_V2_ADDRESS : '')
).trim().toLowerCase();
if (!contractAddress) {
return {
generatedAt,
plannerResponse,
error: 'INTENT_BRIDGE_COORDINATOR_V2_ADDRESS is not configured',
};
}
return {
generatedAt,
plannerResponse,
execution: {
kind: 'bridge-intent',
contractAddress,
functionName: 'executeIntent',
signature: 'executeIntent(((uint256,address,address,uint256,uint256,address,uint256,(uint8,address,address,uint8,uint256,address,bytes)[]),bytes32,bytes,(uint256,address,address,uint256,uint256,address,uint256,(uint8,address,address,uint8,uint256,address,bytes)[]),address,uint256))',
args: [plannerResponse.bridgeIntentPlan],
encodedCalldata: intentInterface.encodeFunctionData('executeIntent', [plannerResponse.bridgeIntentPlan]),
},
};
}
if (plannerResponse.routePlan && plannerResponse.routePlan.legs.length > 0) {
const contractAddress = (
process.env.ENHANCED_SWAP_ROUTER_V2_ADDRESS ||
(plannerResponse.sourceChainId === CHAIN_138 ? DEFAULT_ROUTER_V2_ADDRESS : '')
).trim().toLowerCase();
if (!contractAddress) {
return {
generatedAt,
plannerResponse,
error: 'ENHANCED_SWAP_ROUTER_V2_ADDRESS is not configured',
};
}
return {
generatedAt,
plannerResponse,
execution: {
kind: 'route',
contractAddress,
functionName: 'executeRoute',
signature: 'executeRoute((uint256,address,address,uint256,uint256,address,uint256,(uint8,address,address,uint8,uint256,address,bytes)[]))',
args: [plannerResponse.routePlan],
encodedCalldata: routeInterface.encodeFunctionData('executeRoute', [plannerResponse.routePlan]),
},
};
}
return {
generatedAt,
plannerResponse,
error: plannerResponse.decision === 'bridge-only'
? 'Bridge-only planner result does not require EnhancedSwapRouterV2 calldata'
: plannerResponse.legs.length > 0
? 'Planner route includes one or more providers that are not yet executable through EnhancedSwapRouterV2'
: 'No executable planner route found',
};
}
}

View File

@@ -143,7 +143,7 @@ export function buildInternalExecutionPlan(
const reserveMap = buildPoolReserveMap(matrix.liveSwapRoutes);
const amountIn = BigInt(request.amountIn);
const slippageBps = request.slippageBps || '100';
const executorAddress = route.legs?.[0]?.executorAddress || '0x5BDc62f1ae7D630c37A8B363a1d49845356Ee72d';
const executorAddress = route.legs?.[0]?.executorAddress || '0x86ADA6Ef91A3B450F89f2b751e93B1b7A3218895';
const steps: InternalExecutionStep[] = [];
let currentAmount = amountIn;

View File

@@ -0,0 +1,178 @@
import { ethers } from 'ethers';
import type { LiquidityPool } from '../database/repositories/pool-repo';
import { getChainConfig } from '../config/chains';
import { getDexFactories } from '../config/dex-factories';
import { shouldExposePublicPool } from '../config/gru-transport';
import { logger } from '../utils/logger';
import { estimateChain138DodoLiquidityUsd } from './chain138-dodo-liquidity';
const DODO_PMM_INTEGRATION_ABI = [
'function getAllPools() view returns (address[])',
'function getPoolConfig(address) view returns (tuple(address pool, address baseToken, address quoteToken, uint256 lpFeeRate, uint256 i, uint256 k, bool isOpenTWAP, uint256 createdAt))',
'function getPoolReserves(address) view returns (uint256 baseReserve, uint256 quoteReserve)',
'function getPoolPriceOrOracle(address) view returns (uint256 price)',
];
const DODO_DVM_POOL_ABI = [
'function _BASE_TOKEN_() view returns (address)',
'function _QUOTE_TOKEN_() view returns (address)',
'function getVaultReserve() view returns (uint256 baseReserve, uint256 quoteReserve)',
'function getMidPrice() view returns (uint256)',
];
const CACHE_TTL_MS = 15_000;
const livePoolCache = new Map<number, { expiresAt: number; pools: LiquidityPool[] }>();
interface LivePoolSnapshot {
token0Address: string;
token1Address: string;
reserve0: bigint;
reserve1: bigint;
price: bigint;
createdAt: Date;
}
function configuredIntegrations(chainId: number): string[] {
const dodoConfigs = getDexFactories(chainId)?.dodo || [];
return [...new Set(
dodoConfigs
.map((config) => config.dodoPmmIntegration?.trim().toLowerCase())
.filter((value): value is string => Boolean(value))
)];
}
async function readPoolViaIntegration(
integration: ethers.Contract,
poolAddressRaw: string
): Promise<LivePoolSnapshot> {
const [configResult, reservesResult, priceResult] = await Promise.all([
integration.getPoolConfig(poolAddressRaw),
integration.getPoolReserves(poolAddressRaw),
integration.getPoolPriceOrOracle(poolAddressRaw).catch(() => 0n),
]);
const cfg = configResult as unknown as [
string,
string,
string,
bigint,
bigint,
bigint,
boolean,
bigint,
];
const [reserve0, reserve1] = reservesResult as [bigint, bigint];
return {
token0Address: cfg[1].toLowerCase(),
token1Address: cfg[2].toLowerCase(),
reserve0,
reserve1,
price: typeof priceResult === 'bigint' ? priceResult : 0n,
createdAt: cfg[7] ? new Date(Number(cfg[7]) * 1000) : new Date(),
};
}
async function readPoolDirectly(
provider: ethers.JsonRpcProvider,
poolAddressRaw: string
): Promise<LivePoolSnapshot> {
const pool = new ethers.Contract(poolAddressRaw, DODO_DVM_POOL_ABI, provider);
const [baseToken, quoteToken, reservesResult, midPriceResult] = await Promise.all([
pool._BASE_TOKEN_(),
pool._QUOTE_TOKEN_(),
pool.getVaultReserve(),
pool.getMidPrice().catch(() => 0n),
]);
const [reserve0, reserve1] = reservesResult as [bigint, bigint];
return {
token0Address: String(baseToken).toLowerCase(),
token1Address: String(quoteToken).toLowerCase(),
reserve0,
reserve1,
price: typeof midPriceResult === 'bigint' ? midPriceResult : 0n,
createdAt: new Date(),
};
}
export async function getLiveDodoPools(chainId: number): Promise<LiquidityPool[]> {
const now = Date.now();
const cached = livePoolCache.get(chainId);
if (cached && cached.expiresAt > now) {
return cached.pools;
}
const chainConfig = getChainConfig(chainId);
const integrations = configuredIntegrations(chainId);
if (!chainConfig || integrations.length === 0) {
return [];
}
const provider = new ethers.JsonRpcProvider(chainConfig.rpcUrl);
const poolsByAddress = new Map<string, LiquidityPool>();
for (const integrationAddress of integrations) {
try {
const integration = new ethers.Contract(
integrationAddress,
DODO_PMM_INTEGRATION_ABI,
provider
);
const poolAddresses = (await integration.getAllPools()) as string[];
for (const poolAddressRaw of poolAddresses) {
try {
const poolAddress = poolAddressRaw.toLowerCase();
const snapshot =
await readPoolViaIntegration(integration, poolAddressRaw).catch(async () =>
readPoolDirectly(provider, poolAddressRaw)
);
const { token0Address, token1Address, reserve0, reserve1, price, createdAt } = snapshot;
if (!shouldExposePublicPool(chainId, poolAddress, token0Address, token1Address)) {
continue;
}
const liquidityUsd = chainId === 138
? estimateChain138DodoLiquidityUsd({
token0Address,
token1Address,
reserve0,
reserve1,
price,
})
: { reserve0Usd: 0, reserve1Usd: 0, totalLiquidityUsd: 0 };
poolsByAddress.set(poolAddress, {
chainId,
poolAddress,
token0Address,
token1Address,
dexType: 'dodo',
factoryAddress: integrationAddress,
reserve0: reserve0.toString(),
reserve1: reserve1.toString(),
reserve0Usd: liquidityUsd.reserve0Usd,
reserve1Usd: liquidityUsd.reserve1Usd,
totalLiquidityUsd: liquidityUsd.totalLiquidityUsd,
volume24h: 0,
createdAtBlock: 0,
createdAtTimestamp: createdAt,
lastUpdated: new Date(),
});
} catch (error) {
logger.warn(`Skipping unreadable live DODO pool ${poolAddressRaw} on chain ${chainId}: ${String(error)}`);
}
}
} catch (error) {
logger.warn(`Live DODO fallback failed for chain ${chainId} integration ${integrationAddress}: ${String(error)}`);
}
}
const pools = Array.from(poolsByAddress.values()).sort(
(a, b) => b.totalLiquidityUsd - a.totalLiquidityUsd
);
livePoolCache.set(chainId, { expiresAt: now + CACHE_TTL_MS, pools });
return pools;
}

View File

@@ -0,0 +1,187 @@
export type PlannerProvider = 'dodo' | 'dodo_v3' | 'uniswap_v3' | 'balancer' | 'curve' | 'one_inch' | 'partner';
export type PlannerLegKind = 'swap' | 'bridge';
export type PlannerDecision = 'direct-pool' | 'multi-hop' | 'swap-bridge-swap' | 'bridge-only' | 'unresolved';
export type ComplianceProfile = 'standard' | 'institutional';
export interface PlannerConstraints {
maxSlippageBps?: number;
allowedProviders?: PlannerProvider[];
maxLegs?: number;
allowedIntermediates?: string[];
complianceProfile?: ComplianceProfile;
allowBridge?: boolean;
preferredBridges?: string[];
allowCommodityIntermediates?: boolean;
}
export interface PlannerRequest {
sourceChainId: number;
destinationChainId?: number;
tokenIn: string;
tokenOut: string;
amountIn: string;
recipient?: string;
constraints?: PlannerConstraints;
}
export interface EncodedPlannerRouteLeg {
provider: number;
tokenIn: string;
tokenOut: string;
amountSource: number;
minAmountOut: string;
target: string;
providerData: string;
}
export interface EncodedPlannerRoutePlan {
chainId: number;
inputToken: string;
outputToken: string;
amountIn: string;
minAmountOut: string;
recipient: string;
deadline: string;
legs: EncodedPlannerRouteLeg[];
}
export interface EncodedBridgeIntentPlan {
sourcePlan: EncodedPlannerRoutePlan;
bridgeType: string;
bridgeData: string;
destinationPlan: EncodedPlannerRoutePlan;
recipient: string;
deadline: string;
}
export interface PlannerLeg {
kind: PlannerLegKind;
provider: PlannerProvider;
sourceChainId: number;
destinationChainId: number;
tokenInAddress: string;
tokenOutAddress: string;
tokenInSymbol?: string;
tokenOutSymbol?: string;
estimatedAmountIn: string;
estimatedAmountOut: string;
minAmountOut: string;
target?: string;
poolAddress?: string;
providerData?: Record<string, unknown>;
providerDataHex?: string;
bridgeType?: string;
bridgeAddress?: string;
gasEstimate?: number;
freshnessSeconds?: number | null;
totalLiquidityUsd?: number;
notes?: string[];
}
export interface PlannerAlternative {
routeId: string;
decision: PlannerDecision;
estimatedAmountOut: string;
estimatedGasUsd: number;
providerPath: PlannerProvider[];
legCount: number;
score: number;
notes: string[];
}
export interface PlannerStaleness {
maxFreshnessSeconds: number | null;
hasStaleLeg: boolean;
}
export interface PlannerResponse {
planId: string;
generatedAt: string;
decision: PlannerDecision;
sourceChainId: number;
destinationChainId: number;
tokenIn: string;
tokenOut: string;
estimatedAmountOut: string | null;
minAmountOut: string | null;
estimatedGasUsd: number;
legs: PlannerLeg[];
alternatives: PlannerAlternative[];
confidenceScore: number;
riskFlags: string[];
selectedRouteReason: string;
rejectedAlternatives: string[];
staleness: PlannerStaleness;
routePlan?: EncodedPlannerRoutePlan;
bridgeIntentPlan?: EncodedBridgeIntentPlan;
}
export interface ProviderPairCapability {
chainId: number;
provider: PlannerProvider;
legType: PlannerLegKind;
status: 'live' | 'planned' | 'blocked';
tokenInSymbol: string;
tokenInAddress: string;
tokenOutSymbol: string;
tokenOutAddress: string;
target?: string;
providerData?: Record<string, unknown>;
providerDataHex?: string;
notes?: string[];
reason?: string;
}
export interface ProviderCapabilityRecord {
chainId: number;
provider: PlannerProvider;
executionMode: 'onchain' | 'partner';
live: boolean;
quoteLive: boolean;
executionLive: boolean;
supportedLegTypes: PlannerLegKind[];
pairs: ProviderPairCapability[];
notes?: string[];
}
export interface RoutingPolicy {
profile: ComplianceProfile;
allowedProviders: PlannerProvider[];
defaultIntermediateAddresses: string[];
allowBridge: boolean;
allowedBridgeLabels: string[];
maxLegs: number;
allowCommodityIntermediates: boolean;
notes: string[];
}
export interface SwapGraphEdge {
kind: 'swap';
provider: PlannerProvider;
chainId: number;
tokenInAddress: string;
tokenOutAddress: string;
tokenInSymbol?: string;
tokenOutSymbol?: string;
reserveIn: string;
reserveOut: string;
target?: string;
poolAddress?: string;
providerData?: Record<string, unknown>;
providerDataHex?: string;
totalLiquidityUsd: number;
freshnessSeconds: number | null;
notes: string[];
}
export interface BridgeRouteCandidate {
bridgeType: string;
bridgeAddress: string;
bridgeLabel: string;
assetSymbol: string;
sourceTokenAddress: string;
destinationTokenAddress: string;
fromChainId: number;
toChainId: number;
notes: string[];
}

View File

@@ -0,0 +1,58 @@
import { Contract, JsonRpcProvider } from 'ethers';
const POOL_ABI = [
'function _BASE_TOKEN_() view returns (address)',
'function _QUOTE_TOKEN_() view returns (address)',
'function querySellBase(address,uint256) view returns (uint256,uint256)',
'function querySellQuote(address,uint256) view returns (uint256,uint256)',
];
/**
* PMM / DVM on-chain output for tokenIn amount. Matches DODOPMMIntegration.swapExactIn semantics.
* Returns null if RPC fails, pool is not a DVM, or tokenIn is not base/quote.
*/
export async function pmmQuoteAmountOutFromChain(params: {
rpcUrl: string;
poolAddress: string;
tokenInLookup: string;
amountIn: bigint;
/** Passed to querySell* (MT fee view); default deployer is typical for operator tooling. */
traderForView: string;
}): Promise<bigint | null> {
const { rpcUrl, poolAddress, tokenInLookup, amountIn, traderForView } = params;
try {
const provider = new JsonRpcProvider(rpcUrl);
const pool = new Contract(poolAddress, POOL_ABI, provider);
const base = (await pool._BASE_TOKEN_()).toString().toLowerCase();
const quote = (await pool._QUOTE_TOKEN_()).toString().toLowerCase();
const ti = tokenInLookup.toLowerCase();
if (ti === base) {
const [out] = await pool.querySellBase(traderForView, amountIn);
return BigInt(out.toString());
}
if (ti === quote) {
const [out] = await pool.querySellQuote(traderForView, amountIn);
return BigInt(out.toString());
}
return null;
} catch {
return null;
}
}
/** RPC for PMM eth_call quotes on Chain 138 (optional; unset = skip on-chain override). */
export function resolvePmmQuoteRpcUrl(): string {
return (
process.env.TOKEN_AGGREGATION_PMM_RPC_URL ||
process.env.TOKEN_AGGREGATION_CHAIN138_RPC_URL ||
process.env.RPC_URL_138 ||
''
).trim();
}
export function resolvePmmQuoteTrader(): string {
return (
process.env.TOKEN_AGGREGATION_PMM_QUERY_TRADER ||
'0x4A666F96fC8764181194447A7dFdb7d471b301C8'
).trim();
}

View File

@@ -0,0 +1,76 @@
import type { ResolvedTokenDisplay } from './token-display';
import { classifyPoolDepthStatus, estimateChain138FallbackDepthUsd } from './route-decision-tree';
function token(address: string, symbol: string, decimals: number): ResolvedTokenDisplay {
return {
address,
symbol,
name: symbol,
decimals,
source: 'canonical',
};
}
describe('estimateChain138FallbackDepthUsd', () => {
it('derives tvl from the stable side for funded mixed pairs', () => {
const weth10 = token('0xf4bb2e28688e89fcce3c0580d37d36a7672e8a9f', 'WETH10', 18);
const usdt = token('0x004b63A7B5b0E06f6bB6adb4a5F9f590BF3182D1', 'USDT', 6);
const result = estimateChain138FallbackDepthUsd(
weth10,
50n * 10n ** 18n,
usdt,
200_000n * 10n ** 6n
);
expect(result.tvlUsd).toBe(400_000);
expect(result.estimatedTradeCapacityUsd).toBe(80_000);
});
it('adds both sides for funded stable-to-stable pools', () => {
const cusdcV2 = token('0x219522c60e83dEe01FC5b0329d6fA8fD84b9D13d', 'cUSDC_V2', 6);
const usdc = token('0x71D6687F38b93CCad569Fa6352c876eea967201b', 'USDC', 6);
const result = estimateChain138FallbackDepthUsd(
cusdcV2,
1_000_000n * 10n ** 6n,
usdc,
1_000_000n * 10n ** 6n
);
expect(result.tvlUsd).toBe(2_000_000);
expect(result.estimatedTradeCapacityUsd).toBe(400_000);
});
it('keeps zero-dollar metrics for non-stable or partially funded pools', () => {
const weth = token('0xf4bb2e28688e89fcce3c0580d37d36a7672e8a9f', 'WETH10', 18);
const cxauc = token('0x290E52a8819A4fbD0714E517225429aA2B70EC6b', 'cXAUC', 18);
expect(
estimateChain138FallbackDepthUsd(weth, 10n * 10n ** 18n, cxauc, 5n * 10n ** 18n)
).toEqual({
tvlUsd: 0,
estimatedTradeCapacityUsd: 0,
});
expect(
estimateChain138FallbackDepthUsd(weth, 10n * 10n ** 18n, token('0x004b63A7B5b0E06f6bB6adb4a5F9f590BF3182D1', 'USDT', 6), 0n)
).toEqual({
tvlUsd: 0,
estimatedTradeCapacityUsd: 0,
});
});
});
describe('classifyPoolDepthStatus', () => {
it('marks zero-liquidity pools unavailable even if freshness is recent', () => {
expect(classifyPoolDepthStatus(0, 0, 0)).toBe('unavailable');
expect(classifyPoolDepthStatus(1000, 0, 120)).toBe('unavailable');
});
it('keeps funded pools live or stale based on freshness', () => {
expect(classifyPoolDepthStatus(1000, 200, 60)).toBe('live');
expect(classifyPoolDepthStatus(1000, 200, 900)).toBe('stale');
expect(classifyPoolDepthStatus(1000, 200, 3600)).toBe('unavailable');
});
});

View File

@@ -2,16 +2,18 @@ import { TokenRepository } from '../database/repositories/token-repo';
import { PoolRepository, LiquidityPool, DexType } from '../database/repositories/pool-repo';
import { getChainConfig } from '../config/chains';
import { ResolvedTokenDisplay, resolvePoolTokenDisplays, resolveTokenDisplay } from './token-display';
import { Contract, JsonRpcProvider } from 'ethers';
import { Contract, JsonRpcProvider, formatUnits } from 'ethers';
import { getCanonicalTokenByAddress, getCanonicalTokenBySymbol } from '../config/canonical-tokens';
import { getRouteFromRegistry } from '../config/cross-chain-bridges';
import { filterPoolsForRouting } from '../config/gru-transport';
import { estimateChain138DodoLiquidityUsd } from './chain138-dodo-liquidity';
const CHAIN_138 = 138;
const CHAIN_138_PMM_INTEGRATION =
process.env.CHAIN_138_DODO_PMM_INTEGRATION ||
process.env.DODO_PMM_INTEGRATION_ADDRESS ||
process.env.DODO_PMM_INTEGRATION ||
'0x5BDc62f1ae7D630c37A8B363a1d49845356Ee72d';
'0x86ADA6Ef91A3B450F89f2b751e93B1b7A3218895';
const PMM_ABI = [
'function pools(address,address) view returns (address)',
@@ -21,6 +23,8 @@ const ERC20_ABI = [
'function balanceOf(address) view returns (uint256)',
];
const CHAIN_138_FALLBACK_CAPACITY_RATIO = 0.2;
export type RouteNodeKind =
| 'direct-pool'
| 'bridge'
@@ -97,8 +101,24 @@ export interface RouteDecisionTreeResponse {
missingQuoteTokenPools: MissingQuoteTokenPool[];
}
function normalizedTvlUsd(pool: LiquidityPool): number {
let tvl = Math.max(0, pool.totalLiquidityUsd || 0);
if (pool.chainId === CHAIN_138 && pool.dexType === 'dodo') {
const estimated = estimateChain138DodoLiquidityUsd({
token0Address: pool.token0Address,
token1Address: pool.token1Address,
reserve0: BigInt(pool.reserve0 || '0'),
reserve1: BigInt(pool.reserve1 || '0'),
}).totalLiquidityUsd;
if (estimated > 0) {
tvl = Math.max(tvl, estimated);
}
}
return tvl;
}
function estimateTradeCapacityUsd(pool: LiquidityPool): number {
const tvl = Math.max(0, pool.totalLiquidityUsd || 0);
const tvl = normalizedTvlUsd(pool);
if (tvl === 0) return 0;
const freshnessBoost = pool.lastUpdated
? Math.max(0.25, 1 - (Date.now() - pool.lastUpdated.getTime()) / (60 * 60 * 1000))
@@ -107,26 +127,101 @@ function estimateTradeCapacityUsd(pool: LiquidityPool): number {
return Math.max(0, Math.min(tvl, capacity));
}
export function classifyPoolDepthStatus(
tvlUsd: number,
estimatedTradeCapacityUsd: number,
freshnessSeconds: number | null
): RouteDepthMetrics['status'] {
if (!(tvlUsd > 0) || !(estimatedTradeCapacityUsd > 0)) {
return 'unavailable';
}
if (freshnessSeconds === null) {
return 'unavailable';
}
if (freshnessSeconds < 300) {
return 'live';
}
if (freshnessSeconds < 1800) {
return 'stale';
}
return 'unavailable';
}
function buildDepth(pool: LiquidityPool): RouteDepthMetrics {
const freshnessSeconds = pool.lastUpdated ? Math.max(0, Math.floor((Date.now() - pool.lastUpdated.getTime()) / 1000)) : null;
const status = freshnessSeconds === null
? 'unavailable'
: freshnessSeconds < 300
? 'live'
: freshnessSeconds < 1800
? 'stale'
: 'unavailable';
const tvlUsd = normalizedTvlUsd(pool);
const estimatedTradeCapacityUsd = estimateTradeCapacityUsd(pool);
const status = classifyPoolDepthStatus(tvlUsd, estimatedTradeCapacityUsd, freshnessSeconds);
return {
tvlUsd: pool.totalLiquidityUsd || 0,
tvlUsd,
reserve0: pool.reserve0,
reserve1: pool.reserve1,
estimatedTradeCapacityUsd: estimateTradeCapacityUsd(pool),
estimatedTradeCapacityUsd,
freshnessSeconds,
status,
};
}
function isChain138UsdFallbackToken(token: ResolvedTokenDisplay): boolean {
if (!token?.address) return false;
const spec = getCanonicalTokenByAddress(CHAIN_138, token.address);
return spec?.currencyCode === 'USD';
}
function getChain138FallbackTokenDecimals(token: ResolvedTokenDisplay): number {
const spec = token?.address ? getCanonicalTokenByAddress(CHAIN_138, token.address) : undefined;
return Number(token?.decimals ?? spec?.decimals ?? 18);
}
function parseFallbackTokenAmount(value: bigint, decimals: number): number {
if (value <= 0n) return 0;
const parsed = Number(formatUnits(value, decimals));
return Number.isFinite(parsed) && parsed > 0 ? parsed : 0;
}
export function estimateChain138FallbackDepthUsd(
token0: ResolvedTokenDisplay,
reserve0: bigint,
token1: ResolvedTokenDisplay,
reserve1: bigint
): Pick<RouteDepthMetrics, 'tvlUsd' | 'estimatedTradeCapacityUsd'> {
if (reserve0 <= 0n || reserve1 <= 0n) {
return {
tvlUsd: 0,
estimatedTradeCapacityUsd: 0,
};
}
const reserve0Usd = isChain138UsdFallbackToken(token0)
? parseFallbackTokenAmount(reserve0, getChain138FallbackTokenDecimals(token0))
: 0;
const reserve1Usd = isChain138UsdFallbackToken(token1)
? parseFallbackTokenAmount(reserve1, getChain138FallbackTokenDecimals(token1))
: 0;
let tvlUsd = 0;
if (reserve0Usd > 0 && reserve1Usd > 0) {
tvlUsd = reserve0Usd + reserve1Usd;
} else if (reserve0Usd > 0) {
tvlUsd = reserve0Usd * 2;
} else if (reserve1Usd > 0) {
tvlUsd = reserve1Usd * 2;
}
if (tvlUsd <= 0) {
return {
tvlUsd: 0,
estimatedTradeCapacityUsd: 0,
};
}
return {
tvlUsd,
estimatedTradeCapacityUsd: Math.max(0, Math.min(tvlUsd, tvlUsd * CHAIN_138_FALLBACK_CAPACITY_RATIO)),
};
}
function deriveDecision(
sourceChainId: number,
destinationChainId: number | undefined,
@@ -183,7 +278,10 @@ export class RouteDecisionTreeService {
if (request.tokenOut) acceptableTokenOutAddresses.add(request.tokenOut.toLowerCase());
if (bridgeResolution?.localQuoteAddress) acceptableTokenOutAddresses.add(bridgeResolution.localQuoteAddress.toLowerCase());
const pools = await this.poolRepo.getPoolsByToken(request.chainId, request.tokenIn);
const pools = filterPoolsForRouting(
request.chainId,
await this.poolRepo.getPoolsByToken(request.chainId, request.tokenIn)
);
const directPools = request.tokenOut
? pools.filter((pool) =>
acceptableTokenOutAddresses.has(pool.token0Address.toLowerCase()) ||
@@ -193,7 +291,10 @@ export class RouteDecisionTreeService {
const destinationPools =
request.tokenOut && destinationChainId !== request.chainId
? await this.poolRepo.getPoolsByToken(destinationChainId, request.tokenOut)
? filterPoolsForRouting(
destinationChainId,
await this.poolRepo.getPoolsByToken(destinationChainId, request.tokenOut)
)
: [];
let resolvedPools = await Promise.all(
@@ -238,10 +339,19 @@ export class RouteDecisionTreeService {
})
);
if (request.chainId === CHAIN_138 && destinationChainId === CHAIN_138 && request.tokenOut && resolvedPools.length === 0) {
const shouldTryLiveFallback =
request.chainId === CHAIN_138 &&
destinationChainId === CHAIN_138 &&
request.tokenOut &&
(resolvedPools.length === 0 || resolvedPools.every((pool) => pool.depth.status === 'unavailable'));
if (shouldTryLiveFallback) {
const liveFallbackPool = await this.findLiveDirectPoolFallback(request, sourceTokenIn, sourceTokenOut);
if (liveFallbackPool) {
resolvedPools = [liveFallbackPool, ...resolvedPools];
resolvedPools = [
liveFallbackPool,
...resolvedPools.filter((pool) => pool.poolAddress.toLowerCase() !== liveFallbackPool.poolAddress.toLowerCase()),
];
}
}
@@ -403,17 +513,19 @@ export class RouteDecisionTreeService {
tokenOutContract.balanceOf(poolAddress),
]);
const live = reserve0 > 0n && reserve1 > 0n;
const token1 = sourceTokenOut || await resolveTokenDisplay(this.tokenRepo, request.chainId, request.tokenOut);
const depthUsd = estimateChain138FallbackDepthUsd(sourceTokenIn, reserve0, token1, reserve1);
return {
poolAddress,
dexType: 'dodo',
token0: sourceTokenIn,
token1: sourceTokenOut || await resolveTokenDisplay(this.tokenRepo, request.chainId, request.tokenOut),
token1,
depth: {
tvlUsd: 0,
tvlUsd: depthUsd.tvlUsd,
reserve0: reserve0.toString(),
reserve1: reserve1.toString(),
estimatedTradeCapacityUsd: 0,
estimatedTradeCapacityUsd: depthUsd.estimatedTradeCapacityUsd,
freshnessSeconds: 0,
status: live ? 'live' : 'stale',
},

View File

@@ -0,0 +1,92 @@
import type { LiquidityPool, PoolRepository } from '../database/repositories/pool-repo';
import { RouteGraphBuilder } from './route-graph-builder';
jest.mock('./dodo-v3-pilot', () => ({
__esModule: true,
getChain138DodoV3PilotEdges: jest.fn().mockResolvedValue([]),
isChain138DodoV3ExecutionLive: jest.fn(() => false),
}));
jest.mock('./chain138-pilot-venues', () => ({
__esModule: true,
getChain138PilotVenueEdges: jest.fn().mockResolvedValue([]),
}));
jest.mock('./live-dodo-fallback', () => ({
__esModule: true,
getLiveDodoPools: jest.fn().mockResolvedValue([]),
}));
function pool(overrides: Partial<LiquidityPool>): LiquidityPool {
return {
chainId: 138,
poolAddress: '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
token0Address: '0x93e66202a11b1772e55407b32b44e5cd8eda7f22',
token1Address: '0xf22258f57794cc8e06237084b353ab30fffa640b',
dexType: 'dodo',
reserve0: '1000000',
reserve1: '1000000',
reserve0Usd: 0,
reserve1Usd: 0,
totalLiquidityUsd: 0,
volume24h: 0,
lastUpdated: new Date('2026-04-05T00:00:00Z'),
...overrides,
};
}
class MockPoolRepo {
constructor(private readonly pools: LiquidityPool[]) {}
async getPoolsByChain(): Promise<LiquidityPool[]> {
return this.pools;
}
}
describe('RouteGraphBuilder', () => {
it('filters dust Chain 138 DODO pools from planner visibility', async () => {
const builder = new RouteGraphBuilder(
new MockPoolRepo([
pool({
poolAddress: '0x1111111111111111111111111111111111111111',
token0Address: '0xf22258f57794cc8e06237084b353ab30fffa640b',
token1Address: '0x71d6687f38b93ccad569fa6352c876eea967201b',
reserve0: '999999997998',
reserve1: '999999997998',
}),
pool({
poolAddress: '0x2222222222222222222222222222222222222222',
reserve0: '1001',
reserve1: '1001',
}),
]) as unknown as PoolRepository
);
const edges = await builder.buildSwapEdges(138);
expect(edges).toHaveLength(2);
expect(edges.every((edge) => edge.poolAddress === '0x1111111111111111111111111111111111111111')).toBe(true);
});
it('keeps non-DODO venues visible even when they are small', async () => {
const builder = new RouteGraphBuilder(
new MockPoolRepo([
pool({
poolAddress: '0x3333333333333333333333333333333333333333',
dexType: 'uniswap_v3',
token0Address: '0xc02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2',
token1Address: '0x71D6687F38b93CCad569Fa6352c876eea967201b',
reserve0: '10000000000000000',
reserve1: '10000',
totalLiquidityUsd: 1,
}),
]) as unknown as PoolRepository
);
const edges = await builder.buildSwapEdges(138);
expect(edges).toHaveLength(2);
expect(edges[0].provider).toBe('uniswap_v3');
expect(edges[1].provider).toBe('uniswap_v3');
});
});

View File

@@ -0,0 +1,172 @@
import { AbiCoder } from 'ethers';
import { PoolRepository } from '../database/repositories/pool-repo';
import type { LiquidityPool } from '../database/repositories/pool-repo';
import { filterPoolsForRouting } from '../config/gru-transport';
import { findProviderPairCapability } from '../config/provider-capabilities';
import { getRouteFromRegistry } from '../config/cross-chain-bridges';
import {
getRoutingAddressForSymbol,
getRoutingSymbolForAddress,
} from '../config/routing-assets';
import { getChain138DodoV3PilotEdges } from './dodo-v3-pilot';
import { getChain138PilotVenueEdges } from './chain138-pilot-venues';
import { getLiveDodoPools } from './live-dodo-fallback';
import { estimateChain138DodoLiquidityUsd } from './chain138-dodo-liquidity';
import { BridgeRouteCandidate, PlannerProvider, SwapGraphEdge } from './planner-v2-types';
const abiCoder = AbiCoder.defaultAbiCoder();
const CHAIN_138 = 138;
const CHAIN_138_MIN_LIVE_DODO_LIQUIDITY_USD = 100;
function normalizeAddress(value: string): string {
return value.trim().toLowerCase();
}
function providerFromDexType(dexType: string): PlannerProvider | null {
switch (dexType) {
case 'dodo':
return 'dodo';
case 'dodo_v3':
return 'dodo_v3';
case 'uniswap_v3':
return 'uniswap_v3';
default:
return null;
}
}
function dedupePools<T extends { poolAddress: string }>(items: T[]): T[] {
const byAddress = new Map<string, T>();
for (const item of items) {
byAddress.set(item.poolAddress.toLowerCase(), item);
}
return Array.from(byAddress.values());
}
function isMeaningfullyFundedPool(chainId: number, pool: LiquidityPool): boolean {
if (chainId !== CHAIN_138 || pool.dexType !== 'dodo') {
return true;
}
const reserve0 = BigInt(pool.reserve0 || '0');
const reserve1 = BigInt(pool.reserve1 || '0');
const estimatedUsd = estimateChain138DodoLiquidityUsd({
token0Address: pool.token0Address,
token1Address: pool.token1Address,
reserve0,
reserve1,
}).totalLiquidityUsd;
const totalLiquidityUsd = Math.max(pool.totalLiquidityUsd || 0, estimatedUsd);
return totalLiquidityUsd >= CHAIN_138_MIN_LIVE_DODO_LIQUIDITY_USD;
}
export class RouteGraphBuilder {
private poolRepo: PoolRepository;
constructor(poolRepo = new PoolRepository()) {
this.poolRepo = poolRepo;
}
async buildSwapEdges(chainId: number): Promise<SwapGraphEdge[]> {
let indexedPools: LiquidityPool[] = [];
try {
indexedPools = filterPoolsForRouting(chainId, await this.poolRepo.getPoolsByChain(chainId, 500));
} catch {
indexedPools = [];
}
const liveDodoPools = chainId === 138
? filterPoolsForRouting(chainId, await getLiveDodoPools(chainId))
: [];
const liveDodoV3PilotEdges = chainId === 138
? await getChain138DodoV3PilotEdges()
: [];
const livePilotVenueEdges = chainId === 138
? await getChain138PilotVenueEdges()
: [];
const pools = dedupePools([...indexedPools, ...liveDodoPools]).filter((pool) =>
isMeaningfullyFundedPool(chainId, pool)
);
const poolEdges = pools.flatMap((pool) => {
const provider = providerFromDexType(pool.dexType);
if (!provider) return [];
const token0 = normalizeAddress(pool.token0Address);
const token1 = normalizeAddress(pool.token1Address);
const token0Symbol = getRoutingSymbolForAddress(chainId, token0);
const token1Symbol = getRoutingSymbolForAddress(chainId, token1);
const capability = findProviderPairCapability(chainId, provider, token0, token1)
|| findProviderPairCapability(chainId, provider, token1, token0);
const target = capability?.target || (provider === 'dodo' ? normalizeAddress(process.env.DODO_PMM_PROVIDER_ADDRESS || process.env.DODO_PMM_PROVIDER || '0x3f729632E9553EBacCdE2e9b4c8F2B285b014F2e') : undefined);
const providerData = capability?.providerData || (provider === 'dodo' ? { poolAddress: pool.poolAddress.toLowerCase() } : undefined);
const providerDataHex = capability?.providerDataHex || (provider === 'dodo'
? abiCoder.encode(['address'], [pool.poolAddress.toLowerCase()])
: undefined);
const baseEdge = {
kind: 'swap' as const,
provider,
chainId,
target,
poolAddress: pool.poolAddress.toLowerCase(),
providerData,
providerDataHex,
totalLiquidityUsd: pool.totalLiquidityUsd || 0,
freshnessSeconds: pool.lastUpdated
? Math.max(0, Math.floor((Date.now() - pool.lastUpdated.getTime()) / 1000))
: null,
notes: [],
};
return [
{
...baseEdge,
tokenInAddress: token0,
tokenOutAddress: token1,
tokenInSymbol: token0Symbol,
tokenOutSymbol: token1Symbol,
reserveIn: String(pool.reserve0),
reserveOut: String(pool.reserve1),
},
{
...baseEdge,
tokenInAddress: token1,
tokenOutAddress: token0,
tokenInSymbol: token1Symbol,
tokenOutSymbol: token0Symbol,
reserveIn: String(pool.reserve1),
reserveOut: String(pool.reserve0),
},
];
});
return [...poolEdges, ...liveDodoV3PilotEdges, ...livePilotVenueEdges];
}
buildBridgeCandidates(
fromChainId: number,
toChainId: number,
assetSymbols: string[]
): BridgeRouteCandidate[] {
return assetSymbols.flatMap((symbol) => {
const route = getRouteFromRegistry(fromChainId, toChainId, symbol);
if (!route) return [];
const sourceTokenAddress = getRoutingAddressForSymbol(fromChainId, symbol);
const destinationTokenAddress = getRoutingAddressForSymbol(toChainId, symbol);
if (!sourceTokenAddress || !destinationTokenAddress) return [];
return [{
bridgeType: route.pathType,
bridgeAddress: route.bridgeAddress.toLowerCase(),
bridgeLabel: route.label,
assetSymbol: route.asset || symbol,
sourceTokenAddress: sourceTokenAddress.toLowerCase(),
destinationTokenAddress: destinationTokenAddress.toLowerCase(),
fromChainId,
toChainId,
notes: [`Registry route ${route.label}`],
}];
});
}
}

View File

@@ -0,0 +1,36 @@
import cron from 'node-cron';
import { AggregatorRouteMatrixGenerator } from './aggregator-route-matrix-generator';
import { logger } from '../utils/logger';
export function startRouteMatrixScheduler(): void {
const cronSpec = (process.env.ROUTE_MATRIX_CRON || '').trim();
const generateOnStart = String(process.env.GENERATE_ROUTE_MATRIX_ON_START || 'false').toLowerCase() === 'true';
if (!cronSpec && !generateOnStart) {
return;
}
const generator = new AggregatorRouteMatrixGenerator();
const runGeneration = async () => {
try {
const outputPath = await generator.writeToFile();
logger.info('Planner-v2 route matrix generated', { outputPath });
} catch (error) {
logger.warn('Planner-v2 route matrix generation failed', {
error: error instanceof Error ? error.message : String(error),
});
}
};
if (generateOnStart) {
void runGeneration();
}
if (cronSpec) {
cron.schedule(cronSpec, () => {
void runGeneration();
});
logger.info('Planner-v2 route matrix scheduler enabled', { cronSpec });
}
}