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:
@@ -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' });
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user