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

@@ -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' });
}