feat: expand non-evm relay and route planning support
This commit is contained in:
@@ -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', () => {
|
||||
|
||||
@@ -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`,
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
197
services/token-aggregation/src/config/gru-v2-deployment-pools.ts
Normal file
197
services/token-aggregation/src/config/gru-v2-deployment-pools.ts
Normal 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;
|
||||
}
|
||||
@@ -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(),
|
||||
|
||||
@@ -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'],
|
||||
|
||||
Reference in New Issue
Block a user