feat: expand non-evm relay and route planning support

This commit is contained in:
defiQUG
2026-04-18 12:05:34 -07:00
parent da78073104
commit 843cdbf71c
113 changed files with 8542 additions and 222 deletions

View File

@@ -115,6 +115,26 @@ describe('canonical cW token catalog', () => {
expect(cwethL2?.addresses[10]).toBe('0xce7200000000000000000000000000000000000a');
expect(getCanonicalTokenByAddress(10, '0xce7200000000000000000000000000000000000a')?.symbol).toBe('cWETHL2');
expect(getTokenRegistryFamily(cwethL2!)).toBe('gas_native');
const weth = getCanonicalTokenBySymbol(138, 'WETH');
expect(weth).toMatchObject({
symbol: 'WETH',
type: 'w',
currencyCode: 'ETH',
decimals: 18,
});
expect(weth?.addresses[138]).toBe('0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2');
expect(getCanonicalTokenByAddress(138, '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2')?.symbol).toBe('WETH');
const weth10 = getCanonicalTokenBySymbol(138, 'WETH10');
expect(weth10).toMatchObject({
symbol: 'WETH10',
type: 'w',
currencyCode: 'ETH',
decimals: 18,
});
expect(weth10?.addresses[138]).toBe('0xf4BB2e28688e89fCcE3c0580D37d36A7672E8A9f');
expect(getCanonicalTokenByAddress(138, '0xf4BB2e28688e89fCcE3c0580D37d36A7672E8A9f')?.symbol).toBe('WETH10');
});
it('surfaces cAUSDT on Chain 138 from env and keeps cWAUSDT fallback mirrors on active public chains', () => {

View File

@@ -247,6 +247,8 @@ const FALLBACK_ADDRESSES: Record<string, Partial<Record<number, string>>> = {
cCADC: { [CHAIN_138]: '0x54dBd40cF05e15906A2C21f600937e96787f5679' },
cXAUC: { [CHAIN_138]: '0x290E52a8819A4fbD0714E517225429aA2B70EC6b' },
cXAUT: { [CHAIN_138]: '0x94e408E26c6FD8F4ee00b54dF19082FDA07dC96E' },
WETH: { [CHAIN_138]: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2' },
WETH10: { [CHAIN_138]: '0xf4BB2e28688e89fCcE3c0580D37d36A7672E8A9f' },
// ISO-4217W on Cronos (25) — from DeployISO4217WSystem
USDW: { [CHAIN_25]: '0x948690147D2e50ffe50C5d38C14125aD6a9FA036' },
EURW: { [CHAIN_25]: '0x58a8D8F78F1B65c06dAd7542eC46b299629A60dd' },
@@ -450,6 +452,26 @@ export const CANONICAL_TOKENS: CanonicalTokenSpec[] = [
{ 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: 'WETH',
name: 'Wrapped Ether (WETH9)',
type: 'w',
decimals: 18,
currencyCode: 'ETH',
registryFamily: 'gas_native',
description: 'Legacy WETH9 surface used on Chain 138 for canonical ETH swap routing and CCIP WETH9 bridge lanes.',
addresses: { [CHAIN_138]: addr('WETH', CHAIN_138) || '' },
},
{
symbol: 'WETH10',
name: 'Wrapped Ether 10',
type: 'w',
decimals: 18,
currencyCode: 'ETH',
registryFamily: 'gas_native',
description: 'Chain 138 WETH10 pilot wrapped ETH surface used by DODO v3 routing and flash-capable paths.',
addresses: { [CHAIN_138]: addr('WETH10', CHAIN_138) || '' },
},
{
symbol: 'cWBTC',
name: 'Bitcoin (Compliant Wrapped Monetary Unit)',
@@ -728,6 +750,8 @@ const LOGO_BY_SYMBOL: Record<string, string> = {
cWUSDC: USDC_LOGO,
cWUSDT: USDT_LOGO,
cWUSDW: USDC_LOGO,
WETH: ETH_LOGO,
WETH10: ETH_LOGO,
cEURC: `${GRU_LOGO_BASE}/cEURC.svg`,
cEURT: `${GRU_LOGO_BASE}/cEURT.svg`,
cGBPC: `${GRU_LOGO_BASE}/cGBPC.svg`,

View File

@@ -1,4 +1,4 @@
export type DexType = 'uniswap_v2' | 'uniswap_v3' | 'dodo' | 'custom';
export type DexType = 'uniswap_v2' | 'uniswap_v3' | 'sushiswap' | 'dodo' | 'custom';
export interface UniswapV2Config {
factory: string;
@@ -30,6 +30,7 @@ export interface CustomDexConfig {
export interface DexFactoryConfig {
uniswap_v2?: UniswapV2Config[];
uniswap_v3?: UniswapV3Config[];
sushiswap?: UniswapV2Config[];
dodo?: DodoConfig[];
custom?: CustomDexConfig[];
}
@@ -38,6 +39,35 @@ export interface DexFactoryConfig {
const CANONICAL_CHAIN138_DODO_PMM_INTEGRATION =
'0x86ADA6Ef91A3B450F89f2b751e93B1b7A3218895';
function getUniswapV2Config(chainId: number): UniswapV2Config[] | undefined {
const factory = process.env[`CHAIN_${chainId}_UNISWAP_V2_FACTORY`];
if (!factory) return undefined;
return [
{
factory,
router: process.env[`CHAIN_${chainId}_UNISWAP_V2_ROUTER`] || '',
startBlock: parseInt(process.env[`CHAIN_${chainId}_UNISWAP_V2_START_BLOCK`] || '0', 10),
},
];
}
function getDodoConfig(chainId: number): DodoConfig[] | undefined {
const poolManager = process.env[`CHAIN_${chainId}_DODO_POOL_MANAGER`] || '';
const dodoPmmIntegration = process.env[`CHAIN_${chainId}_DODO_PMM_INTEGRATION`] || '';
if (!poolManager && !dodoPmmIntegration) return undefined;
return [
{
poolManager,
dodoPmmIntegration,
dodoVendingMachine: process.env[`CHAIN_${chainId}_DODO_VENDING_MACHINE`] || '',
startBlock: parseInt(process.env[`CHAIN_${chainId}_DODO_START_BLOCK`] || '0', 10),
},
];
}
export const DEX_FACTORIES: Record<number, DexFactoryConfig> = {
138: {
// DODO PMM Integration - index from DODOPMMIntegration or PoolManager
@@ -51,12 +81,13 @@ export const DEX_FACTORIES: Record<number, DexFactoryConfig> = {
},
],
// UniswapV2 - if deployed
uniswap_v2: process.env.CHAIN_138_UNISWAP_V2_FACTORY
uniswap_v2: getUniswapV2Config(138),
sushiswap: process.env.CHAIN_138_SUSHISWAP_FACTORY
? [
{
factory: process.env.CHAIN_138_UNISWAP_V2_FACTORY,
router: process.env.CHAIN_138_UNISWAP_V2_ROUTER || '',
startBlock: parseInt(process.env.CHAIN_138_UNISWAP_V2_START_BLOCK || '0', 10),
factory: process.env.CHAIN_138_SUSHISWAP_FACTORY,
router: process.env.CHAIN_138_SUSHISWAP_ROUTER || '',
startBlock: parseInt(process.env.CHAIN_138_SUSHISWAP_START_BLOCK || '0', 10),
},
]
: undefined,
@@ -74,15 +105,7 @@ export const DEX_FACTORIES: Record<number, DexFactoryConfig> = {
651940: {
// ALL Mainnet - DEX factories to be discovered/configured
// These can be set via environment variables or discovered on-chain
uniswap_v2: process.env.CHAIN_651940_UNISWAP_V2_FACTORY
? [
{
factory: process.env.CHAIN_651940_UNISWAP_V2_FACTORY,
router: process.env.CHAIN_651940_UNISWAP_V2_ROUTER || '',
startBlock: parseInt(process.env.CHAIN_651940_UNISWAP_V2_START_BLOCK || '0', 10),
},
]
: undefined,
uniswap_v2: getUniswapV2Config(651940),
uniswap_v3: process.env.CHAIN_651940_UNISWAP_V3_FACTORY
? [
{
@@ -101,72 +124,61 @@ export const DEX_FACTORIES: Record<number, DexFactoryConfig> = {
},
]
: undefined,
custom: process.env.CHAIN_651940_HYDX_FACTORY
? [
{
factory: process.env.CHAIN_651940_HYDX_FACTORY,
router: process.env.CHAIN_651940_HYDX_ROUTER || '',
startBlock: parseInt(process.env.CHAIN_651940_HYDX_START_BLOCK || '0', 10),
pairCreatedEvent: process.env.CHAIN_651940_HYDX_PAIR_CREATED_EVENT || '',
},
]
: undefined,
},
// cW* edge chains (1, 10, 56, 100, 137): set CHAIN_*_DODO_PMM_INTEGRATION or CHAIN_*_DODO_POOL_MANAGER to index DODO/pools
1: {
dodo:
process.env.CHAIN_1_DODO_PMM_INTEGRATION || process.env.CHAIN_1_DODO_POOL_MANAGER
? [
{
poolManager: process.env.CHAIN_1_DODO_POOL_MANAGER || '',
dodoPmmIntegration: process.env.CHAIN_1_DODO_PMM_INTEGRATION || '',
dodoVendingMachine: process.env.CHAIN_1_DODO_VENDING_MACHINE || '',
startBlock: parseInt(process.env.CHAIN_1_DODO_START_BLOCK || '0', 10),
},
]
: undefined,
uniswap_v2: getUniswapV2Config(1),
dodo: getDodoConfig(1),
},
10: {
dodo:
process.env.CHAIN_10_DODO_PMM_INTEGRATION || process.env.CHAIN_10_DODO_POOL_MANAGER
? [
{
poolManager: process.env.CHAIN_10_DODO_POOL_MANAGER || '',
dodoPmmIntegration: process.env.CHAIN_10_DODO_PMM_INTEGRATION || '',
dodoVendingMachine: process.env.CHAIN_10_DODO_VENDING_MACHINE || '',
startBlock: parseInt(process.env.CHAIN_10_DODO_START_BLOCK || '0', 10),
},
]
: undefined,
uniswap_v2: getUniswapV2Config(10),
dodo: getDodoConfig(10),
},
25: {
uniswap_v2: getUniswapV2Config(25),
dodo: getDodoConfig(25),
},
56: {
dodo:
process.env.CHAIN_56_DODO_PMM_INTEGRATION || process.env.CHAIN_56_DODO_POOL_MANAGER
? [
{
poolManager: process.env.CHAIN_56_DODO_POOL_MANAGER || '',
dodoPmmIntegration: process.env.CHAIN_56_DODO_PMM_INTEGRATION || '',
dodoVendingMachine: process.env.CHAIN_56_DODO_VENDING_MACHINE || '',
startBlock: parseInt(process.env.CHAIN_56_DODO_START_BLOCK || '0', 10),
},
]
: undefined,
uniswap_v2: getUniswapV2Config(56),
dodo: getDodoConfig(56),
},
100: {
dodo:
process.env.CHAIN_100_DODO_PMM_INTEGRATION || process.env.CHAIN_100_DODO_POOL_MANAGER
? [
{
poolManager: process.env.CHAIN_100_DODO_POOL_MANAGER || '',
dodoPmmIntegration: process.env.CHAIN_100_DODO_PMM_INTEGRATION || '',
dodoVendingMachine: process.env.CHAIN_100_DODO_VENDING_MACHINE || '',
startBlock: parseInt(process.env.CHAIN_100_DODO_START_BLOCK || '0', 10),
},
]
: undefined,
uniswap_v2: getUniswapV2Config(100),
dodo: getDodoConfig(100),
},
137: {
dodo:
process.env.CHAIN_137_DODO_PMM_INTEGRATION || process.env.CHAIN_137_DODO_POOL_MANAGER
? [
{
poolManager: process.env.CHAIN_137_DODO_POOL_MANAGER || '',
dodoPmmIntegration: process.env.CHAIN_137_DODO_PMM_INTEGRATION || '',
dodoVendingMachine: process.env.CHAIN_137_DODO_VENDING_MACHINE || '',
startBlock: parseInt(process.env.CHAIN_137_DODO_START_BLOCK || '0', 10),
},
]
: undefined,
uniswap_v2: getUniswapV2Config(137),
dodo: getDodoConfig(137),
},
8453: {
uniswap_v2: getUniswapV2Config(8453),
dodo: getDodoConfig(8453),
},
42161: {
uniswap_v2: getUniswapV2Config(42161),
dodo: getDodoConfig(42161),
},
42220: {
uniswap_v2: getUniswapV2Config(42220),
dodo: getDodoConfig(42220),
},
43114: {
uniswap_v2: getUniswapV2Config(43114),
dodo: getDodoConfig(43114),
},
1111: {
uniswap_v2: getUniswapV2Config(1111),
dodo: getDodoConfig(1111),
},
};
@@ -189,6 +201,8 @@ export function hasDexType(chainId: number, dexType: DexType): boolean {
return !!config.uniswap_v2 && config.uniswap_v2.length > 0;
case 'uniswap_v3':
return !!config.uniswap_v3 && config.uniswap_v3.length > 0;
case 'sushiswap':
return !!config.sushiswap && config.sushiswap.length > 0;
case 'dodo':
return !!config.dodo && config.dodo.length > 0;
case 'custom':
@@ -208,6 +222,7 @@ export function getConfiguredDexTypes(chainId: number): DexType[] {
const types: DexType[] = [];
if (hasDexType(chainId, 'uniswap_v2')) types.push('uniswap_v2');
if (hasDexType(chainId, 'uniswap_v3')) types.push('uniswap_v3');
if (hasDexType(chainId, 'sushiswap')) types.push('sushiswap');
if (hasDexType(chainId, 'dodo')) types.push('dodo');
if (hasDexType(chainId, 'custom')) types.push('custom');

View File

@@ -0,0 +1,83 @@
import type { DeploymentStatusFile } from './deployment-status';
import {
buildGruV2PoolRegistryFromDeploymentData,
preferGruV2OfficialDexPairs,
resolveDeploymentTokenAddress,
} from './gru-v2-deployment-pools';
describe('gru-v2-deployment-pools', () => {
it('resolveDeploymentTokenAddress checks cwTokens, gasMirrors, anchors, gasQuoteAddresses', () => {
const chain = {
cwTokens: { cWUSDT: '0xcc' },
anchorAddresses: { USDC: '0xaa' },
gasQuoteAddresses: { WETH: '0xee' },
};
expect(resolveDeploymentTokenAddress(chain, 'cWUSDT')).toBe('0xcc');
expect(resolveDeploymentTokenAddress(chain, 'USDC')).toBe('0xaa');
expect(resolveDeploymentTokenAddress(chain, 'WETH')).toBe('0xee');
expect(resolveDeploymentTokenAddress(chain, 'MISSING')).toBeNull();
});
it('buildGruV2PoolRegistryFromDeploymentData merges pmmPools, pmmPoolsVolatile, and gasPmmPools', () => {
const data = {
chains: {
'1': {
name: 'Ethereum Mainnet',
cwTokens: { cWUSDT: '0xaf5017d0163ecb99d9b5d94e3b4d7b09af44d8ae' },
anchorAddresses: {
USDC: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
TRUU: '0xdae0fafd65385e7775cf75b1398735155ef6acd2',
},
pmmPools: [
{
base: 'cWUSDT',
quote: 'USDC',
poolAddress: '0x1111111111111111111111111111111111111111',
feeBps: 3,
role: 'public_routing',
publicRoutingEnabled: true,
},
],
pmmPoolsVolatile: [
{
base: 'cWUSDT',
quote: 'TRUU',
poolAddress: '0x2222222222222222222222222222222222222222',
feeBps: 30,
role: 'truu_routing',
},
],
gasMirrors: { cWETH: '0xf6dc5587e18f27adff60e303fdd98f35b50fa8a5' },
gasQuoteAddresses: {
WETH: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2',
USDC: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
},
gasPmmPools: [
{
familyKey: 'eth_mainnet',
base: 'cWETH',
quote: 'USDC',
poolAddress: '0x3333333333333333333333333333333333333333',
feeBps: 30,
role: 'public_routing',
venue: 'dodo_pmm',
},
],
},
},
} as unknown as DeploymentStatusFile;
const { rows, byChainTokenPools } = buildGruV2PoolRegistryFromDeploymentData(data);
expect(rows).toHaveLength(3);
expect(rows.map((r) => r.section).sort()).toEqual(['gasPmmPools', 'pmmPools', 'pmmPoolsVolatile']);
const usdt = '0xaf5017d0163ecb99d9b5d94e3b4d7b09af44d8ae';
expect(byChainTokenPools.get(`1:${usdt}`)?.has('0x1111111111111111111111111111111111111111')).toBe(true);
expect(byChainTokenPools.get(`1:${usdt}`)?.has('0x2222222222222222222222222222222222222222')).toBe(true);
});
it('preferGruV2OfficialDexPairs leaves pairs unchanged when no deployment pools index the token', () => {
const pairs = [{ pairAddress: '0xbb', priceUsd: '1' }];
expect(preferGruV2OfficialDexPairs(99999, '0x0000000000000000000000000000000000000001', pairs)).toEqual(pairs);
});
});

View File

@@ -0,0 +1,197 @@
import type { DeploymentStatusFile } from './deployment-status';
import { loadDeploymentStatusFile, resolveDeploymentStatusPath } from './deployment-status';
export type GruV2PmmSection = 'pmmPools' | 'pmmPoolsVolatile' | 'gasPmmPools';
export interface GruV2DeploymentPoolRow {
chainId: number;
chainName: string;
section: GruV2PmmSection;
baseSymbol: string;
quoteSymbol: string;
baseAddress: string;
quoteAddress: string;
poolAddress: string;
feeBps?: number;
role?: string;
publicRoutingEnabled?: boolean;
familyKey?: string;
venue?: string;
}
interface ChainTokenMaps {
cwTokens?: Record<string, string>;
gasMirrors?: Record<string, string>;
anchorAddresses?: Record<string, string>;
gasQuoteAddresses?: Record<string, string>;
}
/** Resolve a pool leg symbol to an address using deployment-status chain maps (cW*, anchors, gas quotes). */
export function resolveDeploymentTokenAddress(chain: ChainTokenMaps, symbol: string): string | null {
const candidates = [
chain.cwTokens?.[symbol],
chain.gasMirrors?.[symbol],
chain.anchorAddresses?.[symbol],
chain.gasQuoteAddresses?.[symbol],
];
for (const a of candidates) {
if (typeof a === 'string' && a.startsWith('0x')) {
return a.toLowerCase();
}
}
return null;
}
function pushTokenPoolIndex(
byChainTokenPools: Map<string, Set<string>>,
chainId: number,
tokenAddress: string,
poolAddress: string
): void {
const key = `${chainId}:${tokenAddress.toLowerCase()}`;
let set = byChainTokenPools.get(key);
if (!set) {
set = new Set();
byChainTokenPools.set(key, set);
}
set.add(poolAddress.toLowerCase());
}
/**
* Build GRU v2 PMM pool rows and a (chainId:token) → official pool addresses index from deployment-status data.
* Includes stable mesh (`pmmPools`), volatile (`pmmPoolsVolatile`), and gas (`gasPmmPools`) sections.
*/
export function buildGruV2PoolRegistryFromDeploymentData(data: DeploymentStatusFile): {
rows: GruV2DeploymentPoolRow[];
byChainTokenPools: Map<string, Set<string>>;
} {
const rows: GruV2DeploymentPoolRow[] = [];
const byChainTokenPools = new Map<string, Set<string>>();
const sections: GruV2PmmSection[] = ['pmmPools', 'pmmPoolsVolatile', 'gasPmmPools'];
for (const [cid, rawChain] of Object.entries(data.chains ?? {})) {
const chainId = Number(cid);
if (Number.isNaN(chainId)) continue;
const chain = rawChain as ChainTokenMaps & {
name?: string;
pmmPools?: unknown;
pmmPoolsVolatile?: unknown;
gasPmmPools?: unknown;
};
const chainName = typeof chain.name === 'string' ? chain.name : `Chain ${cid}`;
for (const section of sections) {
const arr = chain[section];
if (!Array.isArray(arr)) continue;
for (const raw of arr) {
if (!raw || typeof raw !== 'object') continue;
const pool = raw as Record<string, unknown>;
const poolAddress = typeof pool.poolAddress === 'string' ? pool.poolAddress.trim() : '';
const baseSymbol = typeof pool.base === 'string' ? pool.base : '';
const quoteSymbol = typeof pool.quote === 'string' ? pool.quote : '';
if (!poolAddress.startsWith('0x') || !baseSymbol || !quoteSymbol) continue;
const baseAddress = resolveDeploymentTokenAddress(chain, baseSymbol);
const quoteAddress = resolveDeploymentTokenAddress(chain, quoteSymbol);
if (!baseAddress || !quoteAddress) continue;
const row: GruV2DeploymentPoolRow = {
chainId,
chainName,
section,
baseSymbol,
quoteSymbol,
baseAddress,
quoteAddress,
poolAddress: poolAddress.toLowerCase(),
feeBps: typeof pool.feeBps === 'number' ? pool.feeBps : undefined,
role: typeof pool.role === 'string' ? pool.role : undefined,
publicRoutingEnabled:
typeof pool.publicRoutingEnabled === 'boolean' ? pool.publicRoutingEnabled : undefined,
familyKey: typeof pool.familyKey === 'string' ? pool.familyKey : undefined,
venue: typeof pool.venue === 'string' ? pool.venue : undefined,
};
rows.push(row);
pushTokenPoolIndex(byChainTokenPools, chainId, baseAddress, poolAddress);
pushTokenPoolIndex(byChainTokenPools, chainId, quoteAddress, poolAddress);
}
}
}
rows.sort((a, b) => {
const c = a.chainId - b.chainId;
if (c !== 0) return c;
return a.poolAddress.localeCompare(b.poolAddress);
});
return { rows, byChainTokenPools };
}
let cachedSnapshot: {
sourcePath: string;
lastModified: string;
rows: GruV2DeploymentPoolRow[];
byChainTokenPools: Map<string, Set<string>>;
} | null = null;
function ensureRegistry(): void {
const loaded = loadDeploymentStatusFile();
const sourcePath = resolveDeploymentStatusPath() ?? '';
const lm = loaded?.lastModified ?? '';
if (cachedSnapshot && cachedSnapshot.lastModified === lm && cachedSnapshot.sourcePath === sourcePath) {
return;
}
if (!loaded) {
cachedSnapshot = { sourcePath, lastModified: lm, rows: [], byChainTokenPools: new Map() };
return;
}
const built = buildGruV2PoolRegistryFromDeploymentData(loaded.data);
cachedSnapshot = {
sourcePath,
lastModified: lm,
rows: built.rows,
byChainTokenPools: built.byChainTokenPools,
};
}
/** All resolved GRU v2 PMM pools from `deployment-status.json` (when available). */
export function getGruV2DeploymentPoolRows(): GruV2DeploymentPoolRow[] {
ensureRegistry();
return cachedSnapshot?.rows ?? [];
}
/**
* Official pool contract addresses for this token on this chain (from deployment-status).
* Returns `null` when the registry is empty or this token has no GRU pools — callers should use all DexScreener pairs.
*/
export function getOfficialGruV2PoolAddressesForToken(chainId: number, normalizedTokenAddress: string): Set<string> | null {
ensureRegistry();
if (!cachedSnapshot || cachedSnapshot.rows.length === 0) {
return null;
}
const key = `${chainId}:${normalizedTokenAddress.toLowerCase()}`;
const set = cachedSnapshot.byChainTokenPools.get(key);
if (!set || set.size === 0) {
return null;
}
return set;
}
/**
* When DexScreener returns multiple pairs, prefer rows whose `pairAddress` is an official GRU v2 pool; if none match, keep full list.
*/
export function preferGruV2OfficialDexPairs<T extends { pairAddress: string }>(
chainId: number,
tokenAddress: string,
pairs: T[]
): T[] {
const official = getOfficialGruV2PoolAddressesForToken(chainId, tokenAddress.toLowerCase());
if (!official || official.size === 0) {
return pairs;
}
const preferred = pairs.filter((p) => official.has(p.pairAddress.toLowerCase()));
return preferred.length > 0 ? preferred : pairs;
}

View File

@@ -94,6 +94,10 @@ function encodeOneInchRoute(router: string): string {
return abiCoder.encode(['address', 'address', 'bytes'], [router, router, '0x']);
}
function encodeRouterV2Route(factory: string, router: string): string {
return abiCoder.encode(['address', 'address'], [factory, router]);
}
function chain138DodoCapabilities(): ProviderCapabilityRecord {
const assets = getChain138RoutingAssets();
const dodoProvider =
@@ -384,6 +388,140 @@ function chain138UniswapCapabilities(): ProviderCapabilityRecord {
};
}
function chain138UniswapV2Capabilities(): ProviderCapabilityRecord {
const assets = getChain138RoutingAssets();
const factory = normalizeAddress(process.env.CHAIN_138_UNISWAP_V2_FACTORY);
const router = normalizeAddress(process.env.CHAIN_138_UNISWAP_V2_ROUTER);
const wethUsdtPair = normalizeAddress(process.env.CHAIN138_UNISWAP_V2_NATIVE_WETH_USDT_PAIR);
const wethUsdcPair = normalizeAddress(process.env.CHAIN138_UNISWAP_V2_NATIVE_WETH_USDC_PAIR);
const cusdtCusdcPair = normalizeAddress(process.env.CHAIN138_UNISWAP_V2_NATIVE_CUSDT_CUSDC_PAIR);
const status = factory && router ? 'live' : 'planned';
const pairs = [
...bidirectionalPair({
chainId: CHAIN_138,
provider: 'uniswap_v2',
tokenASymbol: 'WETH',
tokenAAddress: assets.WETH.address,
tokenBSymbol: 'USDT',
tokenBAddress: assets.USDT.address,
status,
target: router,
providerData: status === 'live' ? { factory, router, pair: wethUsdtPair } : undefined,
providerDataHex: status === 'live' ? encodeRouterV2Route(factory, router) : undefined,
notes: ['Canonical Chain 138 native Uniswap v2 WETH/USDT venue.'],
reason: status === 'planned' ? 'Configure CHAIN_138_UNISWAP_V2_FACTORY and CHAIN_138_UNISWAP_V2_ROUTER after Chain 138 native venue deployment.' : undefined,
}),
...bidirectionalPair({
chainId: CHAIN_138,
provider: 'uniswap_v2',
tokenASymbol: 'WETH',
tokenAAddress: assets.WETH.address,
tokenBSymbol: 'USDC',
tokenBAddress: assets.USDC.address,
status,
target: router,
providerData: status === 'live' ? { factory, router, pair: wethUsdcPair } : undefined,
providerDataHex: status === 'live' ? encodeRouterV2Route(factory, router) : undefined,
notes: ['Canonical Chain 138 native Uniswap v2 WETH/USDC venue.'],
reason: status === 'planned' ? 'Configure CHAIN_138_UNISWAP_V2_FACTORY and CHAIN_138_UNISWAP_V2_ROUTER after Chain 138 native venue deployment.' : undefined,
}),
...bidirectionalPair({
chainId: CHAIN_138,
provider: 'uniswap_v2',
tokenASymbol: 'cUSDT',
tokenAAddress: assets.cUSDT.address,
tokenBSymbol: 'cUSDC',
tokenBAddress: assets.cUSDC.address,
status,
target: router,
providerData: status === 'live' ? { factory, router, pair: cusdtCusdcPair } : undefined,
providerDataHex: status === 'live' ? encodeRouterV2Route(factory, router) : undefined,
notes: ['Canonical Chain 138 native Uniswap v2 GRU stable venue.'],
reason: status === 'planned' ? 'Configure CHAIN_138_UNISWAP_V2_FACTORY and CHAIN_138_UNISWAP_V2_ROUTER after Chain 138 native venue deployment.' : undefined,
}),
];
return {
chainId: CHAIN_138,
provider: 'uniswap_v2',
executionMode: 'onchain',
live: status === 'live',
quoteLive: status === 'live',
executionLive: status === 'live',
supportedLegTypes: ['swap'],
pairs,
notes: ['Canonical Chain 138 native Uniswap v2 router/factory path.'],
};
}
function chain138SushiswapCapabilities(): ProviderCapabilityRecord {
const assets = getChain138RoutingAssets();
const factory = normalizeAddress(process.env.CHAIN_138_SUSHISWAP_FACTORY);
const router = normalizeAddress(process.env.CHAIN_138_SUSHISWAP_ROUTER);
const wethUsdtPair = normalizeAddress(process.env.CHAIN138_SUSHISWAP_NATIVE_WETH_USDT_PAIR);
const wethUsdcPair = normalizeAddress(process.env.CHAIN138_SUSHISWAP_NATIVE_WETH_USDC_PAIR);
const cusdtCusdcPair = normalizeAddress(process.env.CHAIN138_SUSHISWAP_NATIVE_CUSDT_CUSDC_PAIR);
const status = factory && router ? 'live' : 'planned';
const pairs = [
...bidirectionalPair({
chainId: CHAIN_138,
provider: 'sushiswap',
tokenASymbol: 'WETH',
tokenAAddress: assets.WETH.address,
tokenBSymbol: 'USDT',
tokenBAddress: assets.USDT.address,
status,
target: router,
providerData: status === 'live' ? { factory, router, pair: wethUsdtPair } : undefined,
providerDataHex: status === 'live' ? encodeRouterV2Route(factory, router) : undefined,
notes: ['Canonical Chain 138 native SushiSwap-compatible WETH/USDT venue.'],
reason: status === 'planned' ? 'Configure CHAIN_138_SUSHISWAP_FACTORY and CHAIN_138_SUSHISWAP_ROUTER after Chain 138 Sushi deployment.' : undefined,
}),
...bidirectionalPair({
chainId: CHAIN_138,
provider: 'sushiswap',
tokenASymbol: 'WETH',
tokenAAddress: assets.WETH.address,
tokenBSymbol: 'USDC',
tokenBAddress: assets.USDC.address,
status,
target: router,
providerData: status === 'live' ? { factory, router, pair: wethUsdcPair } : undefined,
providerDataHex: status === 'live' ? encodeRouterV2Route(factory, router) : undefined,
notes: ['Canonical Chain 138 native SushiSwap-compatible WETH/USDC venue.'],
reason: status === 'planned' ? 'Configure CHAIN_138_SUSHISWAP_FACTORY and CHAIN_138_SUSHISWAP_ROUTER after Chain 138 Sushi deployment.' : undefined,
}),
...bidirectionalPair({
chainId: CHAIN_138,
provider: 'sushiswap',
tokenASymbol: 'cUSDT',
tokenAAddress: assets.cUSDT.address,
tokenBSymbol: 'cUSDC',
tokenBAddress: assets.cUSDC.address,
status,
target: router,
providerData: status === 'live' ? { factory, router, pair: cusdtCusdcPair } : undefined,
providerDataHex: status === 'live' ? encodeRouterV2Route(factory, router) : undefined,
notes: ['Canonical Chain 138 native SushiSwap-compatible GRU stable venue.'],
reason: status === 'planned' ? 'Configure CHAIN_138_SUSHISWAP_FACTORY and CHAIN_138_SUSHISWAP_ROUTER after Chain 138 Sushi deployment.' : undefined,
}),
];
return {
chainId: CHAIN_138,
provider: 'sushiswap',
executionMode: 'onchain',
live: status === 'live',
quoteLive: status === 'live',
executionLive: status === 'live',
supportedLegTypes: ['swap'],
pairs,
notes: ['Canonical Chain 138 native SushiSwap-compatible router/factory path.'],
};
}
function chain138BalancerCapabilities(): ProviderCapabilityRecord {
const assets = getChain138RoutingAssets();
const vault = normalizeAddress(process.env.BALANCER_VAULT || CHAIN138_PILOT_BALANCER_VAULT);
@@ -538,6 +676,8 @@ export function getProviderCapabilities(chainId: number): ProviderCapabilityReco
chain138DodoCapabilities(),
chain138DodoV3Capabilities(),
chain138UniswapCapabilities(),
chain138UniswapV2Capabilities(),
chain138SushiswapCapabilities(),
chain138BalancerCapabilities(),
chain138CurveCapabilities(),
chain138OneInchCapabilities(),

View File

@@ -16,7 +16,7 @@ export function resolveRoutingPolicy(
const baseStandard: RoutingPolicy = {
profile: 'standard',
allowedProviders: ['dodo', 'dodo_v3', 'uniswap_v3', 'balancer', 'curve', 'one_inch'],
allowedProviders: ['dodo', 'dodo_v3', 'uniswap_v3', 'uniswap_v2', 'sushiswap', 'balancer', 'curve', 'one_inch'],
defaultIntermediateAddresses: defaultIntermediates,
allowBridge: constraints.allowBridge !== false,
allowedBridgeLabels: ['GRUTransport', 'CCIPStableBridge', 'CCIPWETH9Bridge', 'UniversalCCIPBridge', 'AlltraAdapter'],