Compare commits

...

2 Commits

Author SHA1 Message Date
defiQUG
87544a4835 Drop generated token aggregation lockfile 2026-04-16 11:31:39 -07:00
defiQUG
e4c6514f49 Expand token aggregation route planning 2026-04-16 11:31:23 -07:00
28 changed files with 1419 additions and 96 deletions

View File

@@ -0,0 +1,210 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"schemaVersion": 1,
"description": "Ethereum mainnet Uniswap V3 Quoter v1 legs for Aave V3 top-supply underlyings. Implied USD = USDC out / 10^6 for amountIn of the asset. Extend `assets` as pools change.",
"quoterV1": "0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6",
"chainId": 1,
"referenceStable": {
"symbol": "USDC",
"address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"decimals": 6
},
"weth": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
"feeTiersTry": [100, 500, 3000, 10000],
"assets": [
{
"symbol": "weETH",
"address": "0xCd5fE23C85820F7B72D0926FC9b05b43E359b7ee",
"decimals": 18,
"amountIn": "1000000000000000000",
"quote": {
"type": "path",
"tokens": [
"0xCd5fE23C85820F7B72D0926FC9b05b43E359b7ee",
"0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
"0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
],
"fees": [500, 500],
"label": "weETH->WETH@500->USDC@500"
}
},
{
"symbol": "wstETH",
"address": "0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0",
"decimals": 18,
"amountIn": "1000000000000000000",
"quote": {
"type": "path",
"tokens": [
"0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0",
"0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
"0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
],
"fees": [100, 500],
"label": "wstETH->WETH@100->USDC@500"
}
},
{
"symbol": "WBTC",
"address": "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599",
"decimals": 8,
"amountIn": "100000000",
"quote": {
"type": "path",
"tokens": [
"0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599",
"0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
],
"fees": [500],
"label": "WBTC->USDC@500 (canonical deep pool)"
}
},
{
"symbol": "cbBTC",
"address": "0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf",
"decimals": 8,
"amountIn": "100000000",
"quote": {
"type": "path",
"tokens": [
"0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf",
"0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599",
"0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
],
"fees": [500, 500],
"label": "cbBTC->WBTC@500->USDC@500"
}
},
{
"symbol": "rsETH",
"address": "0xA1290d69c65A6Fe4DF752f95823fae25cB99e5A7",
"decimals": 18,
"amountIn": "1000000000000000000",
"quote": {
"type": "compose_via_weth",
"feeTiersToWeth": [100, 500, 3000, 10000],
"usdcFee": 500,
"label": "rsETH->WETH (best tier) × WETH->USDC@500"
}
},
{
"symbol": "USDT",
"address": "0xdAC17F958D2ee523a2206206994597C13D831ec7",
"decimals": 6,
"amountIn": "1000000",
"quote": {
"type": "single_best",
"tokenOut": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"label": "USDT->USDC (best of fee tiers)"
}
},
{
"symbol": "sUSDe",
"address": "0x9D39A5DE30e57443BfF2A8307A4256c8797A3497",
"decimals": 18,
"amountIn": "1000000000000000000",
"quote": {
"type": "single_best",
"tokenOut": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"label": "sUSDe->USDC (best of fee tiers)"
}
},
{
"symbol": "WETH",
"address": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
"decimals": 18,
"amountIn": "1000000000000000000",
"quote": {
"type": "single_best",
"tokenOut": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"label": "WETH->USDC (best of fee tiers)"
}
},
{
"symbol": "USDC",
"address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"decimals": 6,
"amountIn": "1000000",
"quote": {
"type": "identity_usd",
"label": "1 USDC = 1 USD reference"
}
},
{
"symbol": "USDe",
"address": "0x4c9EDD5852cd905f086C759E8383e09bff1E68B3",
"decimals": 18,
"amountIn": "1000000000000000000",
"quote": {
"type": "single_best",
"tokenOut": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"label": "USDe->USDC (best of fee tiers)"
}
},
{
"symbol": "PT-sUSDE-7MAY2026",
"address": "0x3de0ff76E8b528C092d47b9DaC775931cef80F49",
"decimals": 18,
"amountIn": "1000000000000000000",
"quote": {
"type": "single_best",
"tokenOut": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"label": "PT->USDC (best effort; verify Pendle NAV)"
}
},
{
"symbol": "osETH",
"address": "0xf1C9acDc66974dFB6dEcB12aA385b9cD01190E38",
"decimals": 18,
"amountIn": "1000000000000000000",
"quote": {
"type": "path",
"tokens": [
"0xf1C9acDc66974dFB6dEcB12aA385b9cD01190E38",
"0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
"0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
],
"fees": [500, 500],
"label": "osETH->WETH@500->USDC@500"
}
},
{
"symbol": "GHO",
"address": "0x40D16FC0246aD3160Ccc09B8D0D3A2cD28aE6C2f",
"decimals": 18,
"amountIn": "1000000000000000000",
"quote": {
"type": "single_best",
"tokenOut": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"label": "GHO->USDC (best of fee tiers)"
}
},
{
"symbol": "LBTC",
"address": "0x8236a87084f8B84306f72007F36F2618A5634494",
"decimals": 8,
"amountIn": "100000000",
"quote": {
"type": "path",
"tokens": [
"0x8236a87084f8B84306f72007F36F2618A5634494",
"0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599",
"0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
],
"fees": [500, 500],
"label": "LBTC->WBTC@500->USDC@500"
}
},
{
"symbol": "RLUSD",
"address": "0x8292Bb45bf1Ee4d140127049757C2E0fF06317eD",
"decimals": 18,
"amountIn": "1000000000000000000",
"quote": {
"type": "single_best",
"tokenOut": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"label": "RLUSD->USDC (best of fee tiers)"
}
}
]
}

View File

@@ -115,6 +115,8 @@ try {
} catch (_) {
process.exit(0);
}
NODE
}
derive_repo_example_env_value() {
local env_key="$1"
@@ -214,6 +216,7 @@ NODE
sync_chain138_public_rpc_env() {
local env_file="$1"
local public_chain138_rpc="${TOKEN_AGG_CHAIN138_RPC_URL:-http://192.168.11.221:8545}"
local token_agg_port="${TOKEN_AGG_PORT:-3001}"
# Explorer-side read services must use the public Chain 138 RPC node, not the
# operator/deploy core RPC.
@@ -230,6 +233,51 @@ sync_chain138_public_rpc_env() {
if [[ -n "${TOKEN_AGGREGATION_PMM_QUERY_TRADER:-}" ]]; then
upsert_env_var "$env_file" "TOKEN_AGGREGATION_PMM_QUERY_TRADER" "$TOKEN_AGGREGATION_PMM_QUERY_TRADER"
fi
upsert_env_var "$env_file" "PORT" "$token_agg_port"
}
sync_token_aggregation_database_env() {
local env_file="$1"
local db_url="${TOKEN_AGG_DATABASE_URL:-${DATABASE_URL:-}}"
local dbis_primary="${DBIS_POSTGRES_PRIMARY:-192.168.11.105}"
if [[ -z "$db_url" ]]; then
db_url="postgresql://dbis:8cba649443f97436db43b34ab2c0e75b5cf15611bef9c099cee6fb22cc3d7771@${dbis_primary}:5432/dbis_core?sslmode=disable"
fi
upsert_env_var "$env_file" "DATABASE_URL" "$db_url"
}
sync_chain138_native_v2_env() {
local env_file="$1"
local key=""
local value=""
local keys=(
CHAIN_138_UNISWAP_V2_FACTORY
CHAIN_138_UNISWAP_V2_ROUTER
CHAIN_138_UNISWAP_V2_START_BLOCK
CHAIN138_UNISWAP_V2_NATIVE_WETH_USDT_PAIR
CHAIN138_UNISWAP_V2_NATIVE_WETH_USDC_PAIR
CHAIN138_UNISWAP_V2_NATIVE_CUSDT_CUSDC_PAIR
CHAIN_138_SUSHISWAP_FACTORY
CHAIN_138_SUSHISWAP_ROUTER
CHAIN_138_SUSHISWAP_START_BLOCK
CHAIN138_SUSHISWAP_NATIVE_WETH_USDT_PAIR
CHAIN138_SUSHISWAP_NATIVE_WETH_USDC_PAIR
CHAIN138_SUSHISWAP_NATIVE_CUSDT_CUSDC_PAIR
)
for key in "${keys[@]}"; do
value="${!key:-}"
if [[ -z "$value" ]]; then
value="$(derive_repo_example_env_value "$key" || true)"
fi
if [[ -n "$value" ]]; then
upsert_env_var "$env_file" "$key" "$value"
else
ensure_env_key "$env_file" "$key"
fi
done
}
if [ ! -d "$SVC_DIR" ]; then
@@ -259,6 +307,8 @@ fi
sync_gru_transport_env .env
sync_chain138_public_rpc_env .env
sync_token_aggregation_database_env .env
sync_chain138_native_v2_env .env
if command -v pnpm >/dev/null 2>&1 && [ -f "$REPO_ROOT/pnpm-lock.yaml" ]; then
(cd "$REPO_ROOT" && pnpm install --filter token-aggregation-service --no-frozen-lockfile 2>/dev/null) || true

View File

@@ -161,6 +161,11 @@ for prefix in "" "/token-aggregation"; do
fi
done
ROOT_V1_CODE=$(curl -sS -o /tmp/ta-root-v1.json -w "%{http_code}" -m 25 \
"${BASE_URL}/api/v1/tokens?chainId=138&limit=1" 2>/dev/null || echo "000")
TOKEN_AGG_V1_CODE=$(curl -sS -o /tmp/ta-tokenagg-v1.json -w "%{http_code}" -m 25 \
"${BASE_URL}/token-aggregation/api/v1/tokens?chainId=138&limit=1" 2>/dev/null || echo "000")
echo ""
echo "Notes:"
echo " - Empty tokens/pools: set DATABASE_URL, then run bash smom-dbis-138/services/token-aggregation/scripts/apply-lightweight-schema.sh for standalone deployments; RPC to 138; PMM integration now defaults on-chain if env unset."
@@ -169,17 +174,16 @@ echo " - bridge/preflight blocked pairs: run bash scripts/verify/check-gru-tran
echo " - gas-registry 404: redeploy token-aggregation from repo (implements GET /api/v1/report/gas-registry)."
echo " - Health: curl -s http://127.0.0.1:3001/health on explorer VM (not always proxied as /health)."
echo " - planner-v2 publishes under /token-aggregation/api/v2/* so it does not collide with Blockscout /api/v2/* on explorer.d-bis.org."
echo " - Apex https://explorer.d-bis.org/api/v1/* returns 400 while /token-aggregation/api/v1/* works: add HTTP+HTTPS location /api/v1/ → token-aggregation (scripts/fix-explorer-http-api-v1-proxy.sh on explorer VM)."
if [[ "$ROOT_V1_CODE" == "400" ]]; then
echo " - Apex https://explorer.d-bis.org/api/v1/* returns 400 while /token-aggregation/api/v1/* works: add HTTP+HTTPS location /api/v1/ → token-aggregation (scripts/fix-explorer-http-api-v1-proxy.sh on explorer VM)."
elif [[ "$ROOT_V1_CODE" == "200" ]]; then
echo " - Apex https://explorer.d-bis.org/api/v1/* is live and proxied to token-aggregation."
fi
echo " - If POST /token-aggregation/api/v2/* returns 405 or HTML instead of JSON, insert the v2 proxy block (scripts/fix-explorer-token-aggregation-api-v2-proxy.sh on VMID 5000)."
echo " - Fresh binary + PMM env: bash scripts/deploy-token-aggregation-for-publication.sh then rsync dist/node_modules/.env to /opt/token-aggregation; systemctl restart token-aggregation (see TOKEN_AGGREGATION_REPORT_API_RUNBOOK.md)."
echo " - DODO v3 pilot routes should return provider=dodo_v3, routePlanPresent=true, and an internal-execution-plan object targeting EnhancedSwapRouterV2 when CHAIN138_ENABLE_DODO_V3_EXECUTION is live."
echo " - The funded canonical cUSDC/USDC DODO pool should report non-zero route-tree depth on Chain 138; if it falls back to near-zero TVL again, check the DODO valuation path and the canonical PMM integration address."
ROOT_V1_CODE=$(curl -sS -o /tmp/ta-root-v1.json -w "%{http_code}" -m 25 \
"${BASE_URL}/api/v1/tokens?chainId=138&limit=1" 2>/dev/null || echo "000")
TOKEN_AGG_V1_CODE=$(curl -sS -o /tmp/ta-tokenagg-v1.json -w "%{http_code}" -m 25 \
"${BASE_URL}/token-aggregation/api/v1/tokens?chainId=138&limit=1" 2>/dev/null || echo "000")
echo ""
echo "== summary =="
echo "root_v1=$ROOT_V1_CODE token_aggregation_v1=$TOKEN_AGG_V1_CODE v2_caps=$V2_CAPS_CODE v2_plan=$V2_PLAN_CODE v2_internal_plan=$V2_INTERNAL_PLAN_CODE"

View File

@@ -0,0 +1,210 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"schemaVersion": 1,
"description": "Ethereum mainnet Uniswap V3 Quoter v1 legs for Aave V3 top-supply underlyings. Implied USD = USDC out / 10^6 for amountIn of the asset. Extend `assets` as pools change.",
"quoterV1": "0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6",
"chainId": 1,
"referenceStable": {
"symbol": "USDC",
"address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"decimals": 6
},
"weth": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
"feeTiersTry": [100, 500, 3000, 10000],
"assets": [
{
"symbol": "weETH",
"address": "0xCd5fE23C85820F7B72D0926FC9b05b43E359b7ee",
"decimals": 18,
"amountIn": "1000000000000000000",
"quote": {
"type": "path",
"tokens": [
"0xCd5fE23C85820F7B72D0926FC9b05b43E359b7ee",
"0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
"0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
],
"fees": [500, 500],
"label": "weETH->WETH@500->USDC@500"
}
},
{
"symbol": "wstETH",
"address": "0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0",
"decimals": 18,
"amountIn": "1000000000000000000",
"quote": {
"type": "path",
"tokens": [
"0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0",
"0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
"0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
],
"fees": [100, 500],
"label": "wstETH->WETH@100->USDC@500"
}
},
{
"symbol": "WBTC",
"address": "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599",
"decimals": 8,
"amountIn": "100000000",
"quote": {
"type": "path",
"tokens": [
"0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599",
"0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
],
"fees": [500],
"label": "WBTC->USDC@500 (canonical deep pool)"
}
},
{
"symbol": "cbBTC",
"address": "0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf",
"decimals": 8,
"amountIn": "100000000",
"quote": {
"type": "path",
"tokens": [
"0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf",
"0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599",
"0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
],
"fees": [500, 500],
"label": "cbBTC->WBTC@500->USDC@500"
}
},
{
"symbol": "rsETH",
"address": "0xA1290d69c65A6Fe4DF752f95823fae25cB99e5A7",
"decimals": 18,
"amountIn": "1000000000000000000",
"quote": {
"type": "compose_via_weth",
"feeTiersToWeth": [100, 500, 3000, 10000],
"usdcFee": 500,
"label": "rsETH->WETH (best tier) × WETH->USDC@500"
}
},
{
"symbol": "USDT",
"address": "0xdAC17F958D2ee523a2206206994597C13D831ec7",
"decimals": 6,
"amountIn": "1000000",
"quote": {
"type": "single_best",
"tokenOut": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"label": "USDT->USDC (best of fee tiers)"
}
},
{
"symbol": "sUSDe",
"address": "0x9D39A5DE30e57443BfF2A8307A4256c8797A3497",
"decimals": 18,
"amountIn": "1000000000000000000",
"quote": {
"type": "single_best",
"tokenOut": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"label": "sUSDe->USDC (best of fee tiers)"
}
},
{
"symbol": "WETH",
"address": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
"decimals": 18,
"amountIn": "1000000000000000000",
"quote": {
"type": "single_best",
"tokenOut": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"label": "WETH->USDC (best of fee tiers)"
}
},
{
"symbol": "USDC",
"address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"decimals": 6,
"amountIn": "1000000",
"quote": {
"type": "identity_usd",
"label": "1 USDC = 1 USD reference"
}
},
{
"symbol": "USDe",
"address": "0x4c9EDD5852cd905f086C759E8383e09bff1E68B3",
"decimals": 18,
"amountIn": "1000000000000000000",
"quote": {
"type": "single_best",
"tokenOut": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"label": "USDe->USDC (best of fee tiers)"
}
},
{
"symbol": "PT-sUSDE-7MAY2026",
"address": "0x3de0ff76E8b528C092d47b9DaC775931cef80F49",
"decimals": 18,
"amountIn": "1000000000000000000",
"quote": {
"type": "single_best",
"tokenOut": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"label": "PT->USDC (best effort; verify Pendle NAV)"
}
},
{
"symbol": "osETH",
"address": "0xf1C9acDc66974dFB6dEcB12aA385b9cD01190E38",
"decimals": 18,
"amountIn": "1000000000000000000",
"quote": {
"type": "path",
"tokens": [
"0xf1C9acDc66974dFB6dEcB12aA385b9cD01190E38",
"0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
"0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
],
"fees": [500, 500],
"label": "osETH->WETH@500->USDC@500"
}
},
{
"symbol": "GHO",
"address": "0x40D16FC0246aD3160Ccc09B8D0D3A2cD28aE6C2f",
"decimals": 18,
"amountIn": "1000000000000000000",
"quote": {
"type": "single_best",
"tokenOut": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"label": "GHO->USDC (best of fee tiers)"
}
},
{
"symbol": "LBTC",
"address": "0x8236a87084f8B84306f72007F36F2618A5634494",
"decimals": 8,
"amountIn": "100000000",
"quote": {
"type": "path",
"tokens": [
"0x8236a87084f8B84306f72007F36F2618A5634494",
"0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599",
"0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
],
"fees": [500, 500],
"label": "LBTC->WBTC@500->USDC@500"
}
},
{
"symbol": "RLUSD",
"address": "0x8292Bb45bf1Ee4d140127049757C2E0fF06317eD",
"decimals": 18,
"amountIn": "1000000000000000000",
"quote": {
"type": "single_best",
"tokenOut": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"label": "RLUSD->USDC (best of fee tiers)"
}
}
]
}

View File

@@ -230,14 +230,29 @@ Configure DEX factory addresses in `src/config/dex-factories.ts` or via environm
```bash
# ChainID 138
CHAIN_138_DODO_POOL_MANAGER=0x...
CHAIN_138_UNISWAP_V2_FACTORY=0x...
CHAIN_138_UNISWAP_V3_FACTORY=0x...
CHAIN_138_UNISWAP_V2_FACTORY=0x0C30F6e67Ab3667fCc2f5CEA8e274ef1FB920279
CHAIN_138_UNISWAP_V2_ROUTER=0x3019A7fDc76ba7F64F18d78e66842760037ee638
CHAIN_138_UNISWAP_V2_START_BLOCK=4041370
CHAIN_138_SUSHISWAP_FACTORY=0x2871207ff0d56089D70c0134d33f1291B6Fce0BE
CHAIN_138_SUSHISWAP_ROUTER=0xB37b93D38559f53b62ab020A14919f2630a1aE34
CHAIN_138_SUSHISWAP_START_BLOCK=4041495
CHAIN_138_UNISWAP_V3_FACTORY=0x2f7219276e3ce367dB9ec74C1196a8ecEe67841C
CHAIN_138_UNISWAP_V3_ROUTER=0xde9cD8ee2811E6E64a41D5F68Be315d33995975E
# ChainID 651940
CHAIN_651940_UNISWAP_V2_FACTORY=0x...
CHAIN_651940_UNISWAP_V2_ROUTER=0x...
CHAIN_651940_UNISWAP_V2_START_BLOCK=0
CHAIN_651940_UNISWAP_V3_FACTORY=0x...
CHAIN_651940_UNISWAP_V3_ROUTER=0x...
CHAIN_651940_UNISWAP_V3_START_BLOCK=0
CHAIN_651940_HYDX_FACTORY=0x...
CHAIN_651940_HYDX_ROUTER=0x...
CHAIN_651940_HYDX_START_BLOCK=0
```
For ALL Mainnet non-DODO discovery, the repo now treats `HYDX` as the canonical custom venue surface when factory/router details are known. The broader `651940` non-DODO inventory is tracked in `config/allmainnet-non-dodo-protocol-surface.json`.
## Monitoring
The service includes:

View File

@@ -175,15 +175,30 @@ For ChainID 138, configure DODO PoolManager address:
```bash
CHAIN_138_DODO_POOL_MANAGER=0x...
CHAIN_138_UNISWAP_V2_FACTORY=0x0C30F6e67Ab3667fCc2f5CEA8e274ef1FB920279
CHAIN_138_UNISWAP_V2_ROUTER=0x3019A7fDc76ba7F64F18d78e66842760037ee638
CHAIN_138_UNISWAP_V2_START_BLOCK=4041370
CHAIN_138_SUSHISWAP_FACTORY=0x2871207ff0d56089D70c0134d33f1291B6Fce0BE
CHAIN_138_SUSHISWAP_ROUTER=0xB37b93D38559f53b62ab020A14919f2630a1aE34
CHAIN_138_SUSHISWAP_START_BLOCK=4041495
```
For ChainID 651940, configure DEX factories as they are discovered:
```bash
CHAIN_651940_UNISWAP_V2_FACTORY=0x...
CHAIN_651940_UNISWAP_V2_ROUTER=0x...
CHAIN_651940_UNISWAP_V2_START_BLOCK=0
CHAIN_651940_UNISWAP_V3_FACTORY=0x...
CHAIN_651940_UNISWAP_V3_ROUTER=0x...
CHAIN_651940_UNISWAP_V3_START_BLOCK=0
CHAIN_651940_HYDX_FACTORY=0x...
CHAIN_651940_HYDX_ROUTER=0x...
CHAIN_651940_HYDX_START_BLOCK=0
```
The canonical ALL Mainnet non-DODO inventory is also tracked in the parent repo at `config/allmainnet-non-dodo-protocol-surface.json`.
## Monitoring
### Health Checks

View File

@@ -6,6 +6,7 @@
import { createServer } from 'http';
import express from 'express';
import reportRoutes from './report';
import { getCanonicalTokenBySymbol } from '../../config/canonical-tokens';
jest.mock('../../database/repositories/token-repo', () => ({
TokenRepository: jest.fn().mockImplementation(() => ({
@@ -124,6 +125,30 @@ describe('Report API', () => {
])
);
});
it('fills canonical fallback usd pricing when market data is absent', async () => {
const weth = getCanonicalTokenBySymbol(138, 'WETH');
expect(weth?.addresses[138]).toBeTruthy();
const wethAddress = String(weth?.addresses[138]).toLowerCase();
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>;
const tokens138 = body.tokens?.['138'];
expect(Array.isArray(tokens138)).toBe(true);
const wethEntry = tokens138.find((token: Record<string, any>) => token.address === wethAddress);
expect(wethEntry).toMatchObject({
symbol: 'WETH',
decimals: 18,
market: expect.objectContaining({
priceUsd: 2490,
volume24h: 0,
liquidityUsd: 0,
lastUpdated: '2026-04-15T00:00:00.000Z',
}),
});
});
});
describe('GET /api/v1/report/gas-registry', () => {

View File

@@ -26,6 +26,7 @@ import {
loadDeploymentStatusFile,
type CwRegistryChain,
} from '../../config/deployment-status';
import { getCanonicalPriceSnapshotGeneratedAt, getCanonicalPriceUsd } from '../../services/canonical-price-oracle';
const router: Router = Router();
const tokenRepo = new TokenRepository();
@@ -94,6 +95,8 @@ async function buildTokenReport(chainId: number) {
})
);
const fallbackPriceUsd = getCanonicalPriceUsd(chainId, address);
out.push({
chainId,
address: address.toLowerCase(),
@@ -110,7 +113,7 @@ async function buildTokenReport(chainId: number) {
liquiditySourceSymbol: spec.liquiditySourceSymbol,
market: marketData
? {
priceUsd: marketData.priceUsd,
priceUsd: marketData.priceUsd ?? fallbackPriceUsd,
volume24h: marketData.volume24h,
volume7d: marketData.volume7d,
volume30d: marketData.volume30d,
@@ -118,6 +121,15 @@ async function buildTokenReport(chainId: number) {
liquidityUsd: marketData.liquidityUsd,
lastUpdated: marketData.lastUpdated?.toISOString() ?? '',
}
: fallbackPriceUsd !== undefined
? {
priceUsd: fallbackPriceUsd,
volume24h: 0,
volume7d: 0,
volume30d: 0,
liquidityUsd: 0,
lastUpdated: `${getCanonicalPriceSnapshotGeneratedAt()}T00:00:00.000Z`,
}
: undefined,
pools: resolvedPools.map((p) => ({
poolAddress: p.poolAddress,

View File

@@ -0,0 +1,213 @@
import { createServer } from 'http';
import express from 'express';
import { getCanonicalTokenBySymbol } from '../../config/canonical-tokens';
const mockGetTokens = jest.fn();
const mockGetToken = jest.fn();
const mockSearchTokens = jest.fn();
const mockGetMarketData = jest.fn();
const mockGetPoolsByToken = jest.fn();
const mockGetPool = jest.fn();
const mockGetLiveDodoPools = jest.fn();
const mockResolveTokenDisplay = jest.fn();
const mockResolvePoolTokenDisplays = jest.fn();
const mockGetTokenByContract = jest.fn();
jest.mock('../../database/repositories/token-repo', () => ({
TokenRepository: jest.fn().mockImplementation(() => ({
getTokens: mockGetTokens,
getToken: mockGetToken,
searchTokens: mockSearchTokens,
})),
}));
jest.mock('../../database/repositories/market-data-repo', () => ({
MarketDataRepository: jest.fn().mockImplementation(() => ({
getMarketData: mockGetMarketData,
})),
}));
jest.mock('../../database/repositories/pool-repo', () => ({
PoolRepository: jest.fn().mockImplementation(() => ({
getPoolsByToken: mockGetPoolsByToken,
getPool: mockGetPool,
})),
}));
jest.mock('../../indexer/ohlcv-generator', () => ({
OHLCVGenerator: jest.fn().mockImplementation(() => ({
getOHLCV: jest.fn().mockResolvedValue([]),
})),
}));
jest.mock('../../adapters/coingecko-adapter', () => ({
CoinGeckoAdapter: jest.fn().mockImplementation(() => ({
getTokenByContract: mockGetTokenByContract,
getTrending: jest.fn().mockResolvedValue([]),
})),
}));
jest.mock('../../adapters/cmc-adapter', () => ({
CoinMarketCapAdapter: jest.fn().mockImplementation(() => ({
getTokenByContract: mockGetTokenByContract,
})),
}));
jest.mock('../../adapters/dexscreener-adapter', () => ({
DexScreenerAdapter: jest.fn().mockImplementation(() => ({
getTokenByContract: mockGetTokenByContract,
})),
}));
jest.mock('../../services/live-dodo-fallback', () => ({
getLiveDodoPools: (...args: unknown[]) => mockGetLiveDodoPools(...args),
}));
jest.mock('../../services/token-display', () => ({
resolveTokenDisplay: (...args: unknown[]) => mockResolveTokenDisplay(...args),
resolvePoolTokenDisplays: (...args: unknown[]) => mockResolvePoolTokenDisplays(...args),
}));
jest.mock('../middleware/cache');
const tokensRoutes = require('./tokens').default as typeof import('./tokens').default;
function createApp() {
const app = express();
app.use('/api/v1', tokensRoutes);
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('Tokens API', () => {
let server: ReturnType<typeof createServer>;
let baseUrl: string;
beforeAll(async () => {
const app = createApp();
const started = await startServer(app);
server = started.server;
baseUrl = started.baseUrl;
});
beforeEach(() => {
jest.clearAllMocks();
mockGetTokens.mockResolvedValue([]);
mockGetToken.mockResolvedValue(null);
mockSearchTokens.mockResolvedValue([]);
mockGetMarketData.mockResolvedValue(null);
mockGetPoolsByToken.mockResolvedValue([]);
mockGetPool.mockResolvedValue(null);
mockGetLiveDodoPools.mockResolvedValue([]);
mockResolveTokenDisplay.mockResolvedValue({
address: '',
name: 'Unknown Token',
symbol: 'UNKNOWN',
decimals: 18,
source: 'fallback',
});
mockResolvePoolTokenDisplays.mockResolvedValue({
token0: { address: '', symbol: 'UNKNOWN', name: 'Unknown Token', source: 'fallback' },
token1: { address: '', symbol: 'UNKNOWN', name: 'Unknown Token', source: 'fallback' },
});
mockGetTokenByContract.mockResolvedValue(null);
});
afterAll(async () => {
await new Promise<void>((resolve, reject) => {
server.close((error) => {
if (error) {
reject(error);
return;
}
resolve();
});
});
});
it('lists canonical 138 tokens with stable and ETH-family fallback pricing when db market data is missing', async () => {
const usdt = getCanonicalTokenBySymbol(138, 'USDT');
const weth = getCanonicalTokenBySymbol(138, 'WETH');
const weth10 = getCanonicalTokenBySymbol(138, 'WETH10');
expect(usdt?.addresses[138]).toBeTruthy();
expect(weth?.addresses[138]).toBeTruthy();
expect(weth10?.addresses[138]).toBeTruthy();
const res = await fetch(`${baseUrl}/api/v1/tokens?chainId=138&limit=400`);
expect(res.status).toBe(200);
const body = (await res.json()) as Record<string, any>;
expect(body.source).toBe('canonical');
const findByAddress = (address?: string) =>
body.tokens.find((token: Record<string, any>) => token.address === address?.toLowerCase());
expect(findByAddress(usdt?.addresses[138])).toMatchObject({
symbol: 'USDT',
decimals: 6,
market: expect.objectContaining({
priceUsd: 1,
volume24h: 0,
liquidityUsd: 0,
}),
});
expect(findByAddress(weth?.addresses[138])).toMatchObject({
symbol: 'WETH',
decimals: 18,
market: expect.objectContaining({
priceUsd: 2490,
}),
});
expect(findByAddress(weth10?.addresses[138])).toMatchObject({
symbol: 'WETH10',
decimals: 18,
market: expect.objectContaining({
priceUsd: 2490,
}),
});
});
it('fills missing priceUsd on token detail responses while preserving repository market fields', async () => {
const weth10 = getCanonicalTokenBySymbol(138, 'WETH10');
expect(weth10?.addresses[138]).toBeTruthy();
const weth10Address = String(weth10?.addresses[138]).toLowerCase();
mockGetMarketData.mockResolvedValue({
chainId: 138,
tokenAddress: weth10Address,
priceUsd: undefined,
volume24h: 1234,
volume7d: 5678,
volume30d: 9012,
liquidityUsd: 3456,
holdersCount: 78,
transfers24h: 9,
lastUpdated: new Date('2026-04-16T00:00:00.000Z'),
});
const res = await fetch(`${baseUrl}/api/v1/tokens/${weth10Address}?chainId=138`);
expect(res.status).toBe(200);
const body = (await res.json()) as Record<string, any>;
expect(body.token).toMatchObject({
symbol: 'WETH10',
decimals: 18,
market: expect.objectContaining({
priceUsd: 2490,
volume24h: 1234,
liquidityUsd: 3456,
}),
hasDodoPool: false,
});
expect(body.token.canonicalLiquidity).toBeUndefined();
});
});

View File

@@ -16,6 +16,7 @@ import {
resolveCanonicalQuoteAddress,
} from '../../config/canonical-tokens';
import { getLiveDodoPools } from '../../services/live-dodo-fallback';
import { getCanonicalPriceSnapshotGeneratedAt, getCanonicalPriceUsd } from '../../services/canonical-price-oracle';
const router: Router = Router();
const tokenRepo = new TokenRepository();
@@ -26,6 +27,40 @@ const coingeckoAdapter = new CoinGeckoAdapter();
const cmcAdapter = new CoinMarketCapAdapter();
const dexscreenerAdapter = new DexScreenerAdapter();
function withCanonicalMarketFallback(
chainId: number,
address: string,
marketData: Awaited<ReturnType<MarketDataRepository['getMarketData']>>
) {
const fallbackPriceUsd = getCanonicalPriceUsd(chainId, address);
if (marketData) {
if (marketData.priceUsd !== undefined || fallbackPriceUsd === undefined) {
return marketData;
}
return {
...marketData,
priceUsd: fallbackPriceUsd,
};
}
if (fallbackPriceUsd === undefined) {
return null;
}
return {
chainId,
tokenAddress: address.toLowerCase(),
priceUsd: fallbackPriceUsd,
volume24h: 0,
volume7d: 0,
volume30d: 0,
liquidityUsd: 0,
holdersCount: 0,
transfers24h: 0,
lastUpdated: new Date(`${getCanonicalPriceSnapshotGeneratedAt()}T00:00:00Z`),
};
}
function tokenFromCanonical(chainId: number, address: string): Token | null {
const spec = getCanonicalTokenByAddress(chainId, address.toLowerCase());
if (!spec) {
@@ -45,10 +80,15 @@ function tokenFromCanonical(chainId: number, address: string): Token | null {
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)
);
let dbPools: LiquidityPool[] = [];
try {
dbPools = filterPoolsForExposure(
chainId,
await poolRepo.getPoolsByToken(chainId, resolution.lookupAddress)
);
} catch (error) {
logger.warn('DB pool lookup failed; using live DODO fallback', { chainId, address: resolution.lookupAddress, error });
}
if (dbPools.length > 0) {
return dbPools;
}
@@ -178,9 +218,10 @@ router.get('/tokens', cacheMiddleware(60 * 1000), async (req: Request, res: Resp
const tokensWithMarketData = await Promise.all(
tokens.map(async (token) => {
const marketData = await marketDataRepo.getMarketData(chainId, token.address);
const market = withCanonicalMarketFallback(chainId, token.address, marketData);
const out: Record<string, unknown> = {
...token,
market: marketData || undefined,
market: market || undefined,
};
if (includeDodoPool) {
const pools = await getPoolsByTokenWithFallback(chainId, token.address);
@@ -223,13 +264,14 @@ router.get('/tokens/:address', cacheMiddleware(60 * 1000), async (req: Request,
return res.status(404).json({ error: 'Token not found' });
}
const [marketData, pools, coingeckoData, cmcData, dexscreenerData] = await Promise.all([
const [marketDataRaw, pools, coingeckoData, cmcData, dexscreenerData] = await Promise.all([
marketDataRepo.getMarketData(chainId, resolution.lookupAddress),
getPoolsByTokenWithFallback(chainId, normalizedAddress),
coingeckoAdapter.getTokenByContract(chainId, resolution.lookupAddress),
cmcAdapter.getTokenByContract(chainId, resolution.lookupAddress),
dexscreenerAdapter.getTokenByContract(chainId, resolution.lookupAddress),
]);
const marketData = withCanonicalMarketFallback(chainId, normalizedAddress, marketDataRaw);
res.json({
token: {

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

@@ -128,6 +128,10 @@ const FALLBACK_ADDRESSES: Record<string, Partial<Record<number, string>>> = {
cUSDW: {
[CHAIN_138]: '0xcA6BFa614935f1AB71c9aB106bAA6FBB6057095e',
},
// ALL / AUSDT corridor hub on Chain 138 (see config/smart-contracts-master.json COMPLIANT_AUSDT_ADDRESS)
cAUSDT: {
[CHAIN_138]: '0x5fdDF65733e3d590463F68f93Cf16E8c04081271',
},
cBTC: {
[CHAIN_138]: '0xcb7c000000000000000000000000000000000138',
},
@@ -243,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' },
@@ -446,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)',
@@ -724,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[];
}
@@ -60,6 +61,15 @@ export const DEX_FACTORIES: Record<number, DexFactoryConfig> = {
},
]
: undefined,
sushiswap: process.env.CHAIN_138_SUSHISWAP_FACTORY
? [
{
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,
// UniswapV3 - if deployed
uniswap_v3: process.env.CHAIN_138_UNISWAP_V3_FACTORY
? [
@@ -101,6 +111,16 @@ 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: {
@@ -189,6 +209,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 +230,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

@@ -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'],

View File

@@ -41,7 +41,7 @@ export interface ApiEndpoint {
export interface DexFactoryConfig {
id?: number;
chainId: number;
dexType: 'uniswap_v2' | 'uniswap_v3' | 'dodo' | 'custom';
dexType: 'uniswap_v2' | 'uniswap_v3' | 'sushiswap' | 'dodo' | 'custom';
factoryAddress: string;
routerAddress?: string;
poolManagerAddress?: string;

View File

@@ -1,7 +1,7 @@
import { Pool } from 'pg';
import { getDatabasePool } from '../client';
export type DexType = 'uniswap_v2' | 'uniswap_v3' | 'dodo' | 'custom';
export type DexType = 'uniswap_v2' | 'uniswap_v3' | 'sushiswap' | 'dodo' | 'custom';
export interface LiquidityPool {
id?: number;

View File

@@ -9,6 +9,7 @@ import { CoinGeckoAdapter } from '../adapters/coingecko-adapter';
import { CoinMarketCapAdapter } from '../adapters/cmc-adapter';
import { DexScreenerAdapter } from '../adapters/dexscreener-adapter';
import { logger } from '../utils/logger';
import { getCanonicalPriceUsd } from '../services/canonical-price-oracle';
export class ChainIndexer {
private chainId: number;
@@ -155,6 +156,7 @@ export class ChainIndexer {
// Merge external data (prefer CoinGecko, fallback to others)
const externalData = coingeckoData || dexscreenerData || cmcData;
const canonicalPriceUsd = getCanonicalPriceUsd(this.chainId, tokenAddress);
// Get pools for liquidity calculation
const tokenPools = pools.filter(
@@ -166,7 +168,7 @@ export class ChainIndexer {
await this.marketDataRepo.upsertMarketData({
chainId: this.chainId,
tokenAddress,
priceUsd: externalData?.priceUsd,
priceUsd: externalData?.priceUsd ?? canonicalPriceUsd,
priceChange24h: externalData?.priceChange24h,
volume24h: volumeMetrics.volume24h || externalData?.volume24h || 0,
volume7d: volumeMetrics.volume7d,

View File

@@ -83,6 +83,7 @@ export class PoolIndexer {
const hasDexConfig =
!!dexConfig &&
(dexConfig.uniswap_v2?.length ||
dexConfig.sushiswap?.length ||
dexConfig.uniswap_v3?.length ||
dexConfig.dodo?.length ||
dexConfig.custom?.length);
@@ -100,7 +101,14 @@ export class PoolIndexer {
// Index UniswapV2 pools
if (dexConfig.uniswap_v2) {
for (const config of dexConfig.uniswap_v2) {
const pools = await this.indexUniswapV2Pools(config);
const pools = await this.indexUniswapV2Pools(config, 'uniswap_v2');
allPools.push(...pools);
}
}
if (dexConfig.sushiswap) {
for (const config of dexConfig.sushiswap) {
const pools = await this.indexUniswapV2Pools(config, 'sushiswap');
allPools.push(...pools);
}
}
@@ -208,7 +216,10 @@ export class PoolIndexer {
/**
* Index UniswapV2 pools from PairCreated events
*/
private async indexUniswapV2Pools(config: UniswapV2Config): Promise<LiquidityPool[]> {
private async indexUniswapV2Pools(
config: UniswapV2Config,
dexType: 'uniswap_v2' | 'sushiswap'
): Promise<LiquidityPool[]> {
const pools: LiquidityPool[] = [];
const factory = new ethers.Contract(config.factory, UNISWAP_V2_FACTORY_ABI, this.provider);
@@ -237,7 +248,7 @@ export class PoolIndexer {
poolAddress: pairAddress.toLowerCase(),
token0Address: token0.toLowerCase(),
token1Address: token1.toLowerCase(),
dexType: 'uniswap_v2',
dexType,
factoryAddress: config.factory.toLowerCase(),
routerAddress: config.router?.toLowerCase(),
reserve0: reserve0.toString(),
@@ -255,7 +266,7 @@ export class PoolIndexer {
}
}
} catch (error) {
logger.error(`Error indexing UniswapV2 pools:`, error);
logger.error(`Error indexing ${dexType} pools:`, error);
}
return pools;
@@ -390,7 +401,7 @@ export class PoolIndexer {
}
try {
if (dexType === 'uniswap_v2') {
if (dexType === 'uniswap_v2' || dexType === 'sushiswap') {
const pair = new ethers.Contract(poolAddress, UNISWAP_V2_PAIR_ABI, this.provider);
const [reserve0, reserve1] = await pair.getReserves();

View File

@@ -23,6 +23,10 @@ function providerProtocol(provider: PlannerProvider): string {
return 'dodo_v3';
case 'uniswap_v3':
return 'uniswap_v3';
case 'uniswap_v2':
return 'uniswap_v2';
case 'sushiswap':
return 'sushiswap';
case 'balancer':
return 'balancer';
case 'curve':
@@ -42,6 +46,10 @@ function providerLabel(provider: PlannerProvider): string {
return 'DODO V3 / D3MM';
case 'uniswap_v3':
return 'Uniswap V3';
case 'uniswap_v2':
return 'Uniswap V2';
case 'sushiswap':
return 'SushiSwap';
case 'balancer':
return 'Balancer';
case 'curve':

View File

@@ -183,7 +183,7 @@ describe('BestExecutionPlanner', () => {
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.routePlan?.legs[0]?.provider).toBe(8);
expect(response.riskFlags).toContain('pilot-venue');
expect(response.riskFlags).not.toContain('manual-execution-only');
});

View File

@@ -29,20 +29,24 @@ import {
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_PRIORITY: PlannerProvider[] = ['dodo', 'dodo_v3', 'uniswap_v3', 'uniswap_v2', 'sushiswap', '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,
uniswap_v2: 2,
sushiswap: 3,
balancer: 4,
curve: 5,
one_inch: 6,
partner: 7,
dodo_v3: 8,
};
const PROVIDER_GAS_USD: Record<PlannerProvider, number> = {
dodo: 0.22,
dodo_v3: 0.3,
uniswap_v3: 0.28,
uniswap_v2: 0.24,
sushiswap: 0.25,
balancer: 0.34,
curve: 0.29,
one_inch: 0.48,

View File

@@ -0,0 +1,99 @@
import { getCanonicalTokenBySymbol } from '../config/canonical-tokens';
import { getCanonicalPriceUsd, resolveCanonicalPriceUsd, resolveCanonicalPriceUsdForSpec } from './canonical-price-oracle';
describe('canonical-price-oracle', () => {
it('pegs Chain 138 USD-family mirrors to one dollar', () => {
expect(getCanonicalPriceUsd(138, '0x71D6687F38b93CCad569Fa6352c876eea967201b')).toBe(1);
expect(getCanonicalPriceUsd(138, '0x93E66202A11B1772E55407B32B44e5Cd8eda7f22')).toBe(1);
expect(getCanonicalPriceUsd(138, '0xf22258f57794CC8E06237084b353Ab30fFfa640b')).toBe(1);
});
it('anchors GRU v2 and lending wrappers to the matching c* ISO-4217 asset family', () => {
const cUsdcV2 = getCanonicalTokenBySymbol(138, 'cUSDC_V2');
expect(cUsdcV2?.addresses[138]).toBeTruthy();
expect(resolveCanonicalPriceUsd(138, String(cUsdcV2?.addresses[138]))).toMatchObject({
priceUsd: 1,
referenceSymbol: 'USD',
});
expect(
resolveCanonicalPriceUsdForSpec({
symbol: 'acUSDC',
name: 'Deposit cUSDC',
type: 'asset',
decimals: 6,
addresses: { 138: '0xac00000000000000000000000000000000000138' },
})
).toMatchObject({
priceUsd: 1,
referenceSymbol: 'USD',
});
expect(
resolveCanonicalPriceUsdForSpec({
symbol: 'vdcEURC',
name: 'Debt cEURC (variable)',
type: 'debt',
decimals: 6,
addresses: { 138: '0xbdc0000000000000000000000000000000000138' },
})
).toMatchObject({
priceUsd: 1.1780,
referenceSymbol: 'EUR',
});
expect(
resolveCanonicalPriceUsdForSpec({
symbol: 'sdcCADC',
name: 'Debt cCADC (stable)',
type: 'debt',
decimals: 6,
addresses: { 138: '0xcdc0000000000000000000000000000000000138' },
})
).toMatchObject({
priceUsd: 0.7255928549430243,
referenceSymbol: 'CAD',
});
});
it('prices fiat GRU W tokens from the same ISO-4217 oracle references as the c* canonicals', () => {
const usdw = getCanonicalTokenBySymbol(25, 'USDW');
const eurw = getCanonicalTokenBySymbol(25, 'EURW');
expect(usdw?.addresses[25]).toBeTruthy();
expect(eurw?.addresses[25]).toBeTruthy();
expect(resolveCanonicalPriceUsd(25, String(usdw?.addresses[25]))).toMatchObject({
priceUsd: 1,
referenceSymbol: 'USD',
});
expect(resolveCanonicalPriceUsd(25, String(eurw?.addresses[25]))).toMatchObject({
priceUsd: 1.1780,
referenceSymbol: 'EUR',
});
});
it('resolves WETH9 and WETH10 to the ETH peg with 18-decimal canonical metadata', () => {
expect(resolveCanonicalPriceUsd(138, '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2')).toMatchObject({
priceUsd: 2490,
referenceSymbol: 'ETH',
});
expect(resolveCanonicalPriceUsd(138, '0xf4BB2e28688e89fCcE3c0580D37d36A7672E8A9f')).toMatchObject({
priceUsd: 2490,
referenceSymbol: 'ETH',
});
});
it('provides repo-local fallback pegs for commodity and monetary-unit canonicals', () => {
expect(resolveCanonicalPriceUsd(138, '0x290E52a8819A4fbD0714E517225429aA2B70EC6b')).toMatchObject({
referenceSymbol: 'XAU',
source: 'repo-fallback',
});
expect(resolveCanonicalPriceUsd(138, '0xcb7c000000000000000000000000000000000138')).toMatchObject({
priceUsd: 90000,
referenceSymbol: 'BTC',
source: 'repo-fallback',
});
});
});

View File

@@ -0,0 +1,193 @@
import { type CanonicalTokenSpec, getCanonicalTokenByAddress, getCanonicalTokenBySymbol } from '../config/canonical-tokens';
export interface CanonicalPriceResolution {
priceUsd?: number;
referenceSymbol?: string;
source: 'env' | 'repo-fallback' | 'unresolved';
}
const FX_SNAPSHOT_GENERATED_AT = '2026-04-15';
// Repo-local inferred FX snapshot from scripts/lib/extraction_gap_closure.py.
const REPO_FALLBACK_PRICE_USD: Record<string, number> = {
USD: 1,
EUR: 1.1780,
GBP: 1.3550353712543854,
AUD: 0.7136366390016357,
CAD: 0.7255928549430243,
CHF: 1.2776572668112798,
JPY: 0.006285683794888213,
XAU: 5163.3401260328355,
ETH: 2490,
BTC: 90000,
BNB: 610,
POL: 0.78,
AVAX: 48,
CELO: 0.72,
CRO: 0.14,
XDAI: 1,
};
function normalizeAddress(value: string): string {
return value.trim().toLowerCase();
}
function readEnvPrice(keys: string[]): number | undefined {
for (const key of keys) {
const raw = process.env[key];
if (!raw || raw.trim() === '') continue;
const parsed = Number(raw);
if (Number.isFinite(parsed) && parsed > 0) {
return parsed;
}
}
return undefined;
}
function resolveReferenceSymbol(spec: CanonicalTokenSpec): string | undefined {
const symbol = spec.symbol.toUpperCase();
const currencyCode = String(spec.currencyCode || '').trim().toUpperCase();
if (
symbol === 'WETH' ||
symbol === 'WETH9' ||
symbol === 'WETH10' ||
symbol === 'CETH' ||
symbol === 'CETHL2' ||
symbol === 'CWETH' ||
symbol === 'CWETHL2'
) {
return 'ETH';
}
if (symbol === 'CBTC' || symbol === 'CWBTC') {
return 'BTC';
}
if (symbol === 'CXAUC' || symbol === 'CXAUT' || symbol === 'CAXAUC' || symbol === 'CAXAUT' || symbol === 'CWAXAUC' || symbol === 'CWAXAUT' || symbol === 'LIXAU') {
return 'XAU';
}
if (currencyCode) {
return currencyCode;
}
return undefined;
}
function resolvePegSourceSpec(spec: CanonicalTokenSpec, seenSymbols: Set<string> = new Set()): CanonicalTokenSpec {
const symbol = spec.symbol.trim();
const symbolUpper = symbol.toUpperCase();
if (seenSymbols.has(symbolUpper)) {
return spec;
}
const nextSeen = new Set(seenSymbols);
nextSeen.add(symbolUpper);
const familySymbol = String(spec.familySymbol || '').trim();
if (familySymbol) {
const familyMatch = getCanonicalTokenByAddressFromSymbolFamily(spec, familySymbol);
if (familyMatch) {
return resolvePegSourceSpec(familyMatch, nextSeen);
}
}
const lendingWrapperMatch = /^(ac|vdc|sdc)(.+)$/i.exec(symbol);
if (lendingWrapperMatch) {
const underlyingSymbol = `c${lendingWrapperMatch[2]}`;
const underlyingMatch = getCanonicalTokenByAddressFromSymbolFamily(spec, underlyingSymbol);
if (underlyingMatch) {
return resolvePegSourceSpec(underlyingMatch, nextSeen);
}
}
return spec;
}
function getCanonicalTokenByAddressFromSymbolFamily(spec: CanonicalTokenSpec, symbol: string): CanonicalTokenSpec | undefined {
for (const [chainIdText, address] of Object.entries(spec.addresses)) {
if (!address || String(address).trim() === '') continue;
const chainId = Number(chainIdText);
if (!Number.isFinite(chainId)) continue;
const familyMatch = getCanonicalTokenBySymbol(chainId, symbol);
if (familyMatch) {
return familyMatch;
}
}
return undefined;
}
function resolveEnvPriceKeys(referenceSymbol: string): string[] {
const symbol = referenceSymbol.toUpperCase();
if (symbol === 'XAU') {
return [
'CHAIN138_CANONICAL_PRICE_USD_XAU',
'CANONICAL_PRICE_USD_XAU',
'XAU_SPOT_USD',
'GOLD_USD_PRICE',
];
}
if (symbol === 'ETH') {
return [
'CHAIN138_CANONICAL_PRICE_USD_ETH',
'CANONICAL_PRICE_USD_ETH',
'ETH_PRICE_USD',
'CHAIN138_D3_PILOT_WETH_USD',
];
}
return [
`CHAIN138_CANONICAL_PRICE_USD_${symbol}`,
`CANONICAL_PRICE_USD_${symbol}`,
`${symbol}_PRICE_USD`,
];
}
export function resolveCanonicalPriceUsdForSpec(spec: CanonicalTokenSpec): CanonicalPriceResolution {
const pegSourceSpec = resolvePegSourceSpec(spec);
const referenceSymbol = resolveReferenceSymbol(pegSourceSpec);
if (!referenceSymbol) {
return { source: 'unresolved' };
}
const envPrice = readEnvPrice(resolveEnvPriceKeys(referenceSymbol));
if (envPrice !== undefined) {
return {
priceUsd: envPrice,
referenceSymbol,
source: 'env',
};
}
const fallback = REPO_FALLBACK_PRICE_USD[referenceSymbol];
if (fallback !== undefined) {
return {
priceUsd: fallback,
referenceSymbol,
source: 'repo-fallback',
};
}
return {
referenceSymbol,
source: 'unresolved',
};
}
export function resolveCanonicalPriceUsd(chainId: number, address: string): CanonicalPriceResolution {
const spec = getCanonicalTokenByAddress(chainId, normalizeAddress(address));
if (!spec) {
return { source: 'unresolved' };
}
return resolveCanonicalPriceUsdForSpec(spec);
}
export function getCanonicalPriceUsd(chainId: number, address: string): number | undefined {
return resolveCanonicalPriceUsd(chainId, address).priceUsd;
}
export function getCanonicalPriceSnapshotGeneratedAt(): string {
return FX_SNAPSHOT_GENERATED_AT;
}

View File

@@ -28,7 +28,20 @@ describe('estimateChain138DodoLiquidityUsd', () => {
expect(result.totalLiquidityUsd).toBe(210_830);
});
it('keeps non-USD pairs at zero without a usable USD side', () => {
it('keeps WETH9 on the ETH peg even when live oracle price is unavailable', () => {
const result = estimateChain138DodoLiquidityUsd({
token0Address: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2',
token1Address: '0x004b63A7B5b0E06f6bB6adb4a5F9f590BF3182D1',
reserve0: 10n * 10n ** 18n,
reserve1: 24_900n * 10n ** 6n,
});
expect(result.reserve0Usd).toBe(24_900);
expect(result.reserve1Usd).toBe(24_900);
expect(result.totalLiquidityUsd).toBe(49_800);
});
it('values non-USD canonical pairs from their repo-local peg references', () => {
const result = estimateChain138DodoLiquidityUsd({
token0Address: '0xf4BB2e28688e89fCcE3c0580D37d36A7672E8A9f',
token1Address: '0x290E52a8819A4fbD0714E517225429aA2B70EC6b',
@@ -36,11 +49,22 @@ describe('estimateChain138DodoLiquidityUsd', () => {
reserve1: 5n * 10n ** 6n,
});
expect(result).toEqual({
reserve0Usd: 0,
reserve1Usd: 0,
totalLiquidityUsd: 0,
expect(result.reserve0Usd).toBe(24_900);
expect(result.reserve1Usd).toBeCloseTo(25_816.700630164178, 6);
expect(result.totalLiquidityUsd).toBeCloseTo(50_716.70063016418, 6);
});
it('values XAU/stable DODO pools from the canonical gold peg', () => {
const result = estimateChain138DodoLiquidityUsd({
token0Address: '0x290E52a8819A4fbD0714E517225429aA2B70EC6b',
token1Address: '0x004b63A7B5b0E06f6bB6adb4a5F9f590BF3182D1',
reserve0: 5n * 10n ** 6n,
reserve1: 25_816n * 10n ** 6n,
});
expect(result.reserve0Usd).toBeCloseTo(25_816.700630164178, 6);
expect(result.reserve1Usd).toBe(25_816);
expect(result.totalLiquidityUsd).toBeCloseTo(51_632.70063016418, 6);
});
it('values cBTC/stable DODO pools using satoshi precision and the BTC fallback price', () => {

View File

@@ -1,9 +1,8 @@
import { formatUnits } from 'ethers';
import { getCanonicalTokenByAddress } from '../config/canonical-tokens';
import { getCanonicalPriceUsd } from './canonical-price-oracle';
const CHAIN_138 = 138;
const DEFAULT_WETH_USD_PRICE = 2100;
const DEFAULT_BTC_USD_PRICE = 90000;
export interface Chain138DodoLiquidityUsd {
reserve0Usd: number;
@@ -20,21 +19,6 @@ function decimalsForAddress(address: string): number {
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));
@@ -60,54 +44,32 @@ export function estimateChain138DodoLiquidityUsd(args: {
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,
};
let token0PriceUsd = getCanonicalPriceUsd(CHAIN_138, token0Address) ?? 0;
let token1PriceUsd = getCanonicalPriceUsd(CHAIN_138, token1Address) ?? 0;
if (price > 0) {
if (token1PriceUsd === 1 && token0PriceUsd !== 1) {
token0PriceUsd = price;
}
if (token0PriceUsd === 1 && token1PriceUsd !== 1) {
token1PriceUsd = price;
}
}
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 (token0PriceUsd <= 0 || token1PriceUsd <= 0) {
return { reserve0Usd: 0, reserve1Usd: 0, totalLiquidityUsd: 0 };
}
if (token0IsUsd) {
const reserve1Usd =
price > 0
? reserve1Amount / price
: isWethLikeAddress(token1Address)
? reserve1Amount * DEFAULT_WETH_USD_PRICE
: isBtcLikeAddress(token1Address)
? reserve1Amount * DEFAULT_BTC_USD_PRICE
: 0;
const reserve0Usd = reserve0Amount * token0PriceUsd;
const reserve1Usd = reserve1Amount * token1PriceUsd;
return {
reserve0Usd: reserve0Amount,
reserve1Usd,
totalLiquidityUsd: reserve1Usd > 0 ? reserve0Amount + reserve1Usd : 0,
};
}
return { reserve0Usd: 0, reserve1Usd: 0, totalLiquidityUsd: 0 };
return {
reserve0Usd,
reserve1Usd,
totalLiquidityUsd: reserve0Usd + reserve1Usd,
};
}

View File

@@ -1,4 +1,13 @@
export type PlannerProvider = 'dodo' | 'dodo_v3' | 'uniswap_v3' | 'balancer' | 'curve' | 'one_inch' | 'partner';
export type PlannerProvider =
| 'dodo'
| 'dodo_v3'
| 'uniswap_v3'
| 'uniswap_v2'
| 'sushiswap'
| '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';

View File

@@ -30,6 +30,10 @@ function providerFromDexType(dexType: string): PlannerProvider | null {
return 'dodo_v3';
case 'uniswap_v3':
return 'uniswap_v3';
case 'uniswap_v2':
return 'uniswap_v2';
case 'sushiswap':
return 'sushiswap';
default:
return null;
}