{"version":3,"file":"defi-price.mjs","names":[],"sources":["../../../src/tools/defi-price.ts"],"sourcesContent":["/**\n * DeFi Price Tool — token price lookup via multi-source PriceOracle.\n *\n * For well-known tokens: uses PriceOracle (DexScreener + CoinGecko +\n * DeFiLlama + CoinMarketCap) with cross-validation and confidence scoring.\n * For contract addresses / unknown tokens: falls back to DexScreener only.\n */\n\nimport { Type } from '@sinclair/typebox';\nimport { stringEnum, jsonResult, errorResult, readStringParam } from '../lib/tool-helpers.js';\nimport { fetchDexScreener, resolveChain, getTrending as dexGetTrending } from '../services/dexscreener-service.js';\nimport { getPriceOracle } from '../services/price-oracle.js';\nimport type { TokenPrice } from '../lib/types.js';\n\nconst ACTIONS = ['lookup', 'search', 'trending'] as const;\n\nconst DefiPriceSchema = Type.Object({\n  action: stringEnum(ACTIONS, {\n    description: 'lookup: get price for a specific token. search: find tokens by name/symbol. trending: top trending tokens.',\n  }),\n  token: Type.Optional(Type.String({\n    description: 'Token symbol (e.g. \"ETH\", \"USDC\"), name, or contract address (0x...)',\n  })),\n  chain: Type.Optional(Type.String({\n    description: 'Chain to query (default: \"base\"). Options: base, ethereum, arbitrum, optimism, polygon',\n  })),\n});\n\nexport function createDefiPriceTool() {\n  return {\n    name: 'defi_price',\n    label: 'DeFi Price',\n    ownerOnly: false,\n    description:\n      'Look up token prices, search for tokens, and see trending tokens. ' +\n      'Uses cross-validated multi-source oracle (DexScreener, CoinGecko, DeFiLlama, CoinMarketCap) ' +\n      'with confidence scoring and divergence detection. ' +\n      'Supports any ERC-20 token on Base, Ethereum, Arbitrum, Optimism, Polygon.',\n    parameters: DefiPriceSchema,\n    execute: async (_toolCallId: string, args: unknown) => {\n      const params = args as Record<string, unknown>;\n      const action = readStringParam(params, 'action', { required: true })!;\n\n      switch (action) {\n        case 'lookup':\n          return handleLookup(params);\n        case 'search':\n          return handleSearch(params);\n        case 'trending':\n          return handleTrending(params);\n        default:\n          return errorResult(`Unknown action: ${action}`);\n      }\n    },\n  };\n}\n\nasync function handleLookup(params: Record<string, unknown>) {\n  const token = readStringParam(params, 'token', { required: true })!;\n  const chainInput = readStringParam(params, 'chain') || 'base';\n  const chain = resolveChain(chainInput);\n\n  // For contract addresses, use DexScreener directly (oracle doesn't support addresses well)\n  const isAddress = token.startsWith('0x') && token.length === 42;\n\n  if (!isAddress) {\n    // Try PriceOracle first for symbol lookups — cross-validated, multi-source\n    try {\n      const oracle = getPriceOracle();\n      const result = await oracle.getPrice(token, chain);\n\n      if (result.priceUsd > 0) {\n        // Also get DexScreener data for additional fields (liquidity, volume, pair info)\n        let dexData: any = null;\n        try {\n          const data = await fetchDexScreener(`/latest/dex/search?q=${encodeURIComponent(token)}`);\n          const pairs = data?.pairs ?? [];\n          dexData = pairs\n            .filter((p: any) => !chain || p.chainId === chain)\n            .sort((a: any, b: any) => (b.liquidity?.usd ?? 0) - (a.liquidity?.usd ?? 0))[0];\n        } catch {\n          // DexScreener data is supplementary, not required\n        }\n\n        return jsonResult({\n          found: true,\n          symbol: token.toUpperCase(),\n          name: dexData?.baseToken?.name ?? token,\n          address: dexData?.baseToken?.address,\n          priceUsd: result.priceUsd,\n          confidence: result.confidence,\n          sources: result.sources\n            .filter((s) => !s.error)\n            .map((s) => ({ name: s.name, price: s.priceUsd })),\n          divergencePercent: result.divergencePercent,\n          warning: result.warning,\n          change24h: dexData?.priceChange?.h24 ?? undefined,\n          volume24h: dexData?.volume?.h24 ?? undefined,\n          liquidity: dexData?.liquidity?.usd ?? undefined,\n          marketCap: dexData?.marketCap ?? dexData?.fdv ?? undefined,\n          pairAddress: dexData?.pairAddress,\n          dexId: dexData?.dexId,\n          url: dexData?.url,\n        });\n      }\n    } catch {\n      // Oracle failed entirely, fall through to DexScreener-only path\n    }\n  }\n\n  // Fallback: DexScreener-only lookup (for addresses or oracle failures)\n  try {\n    let data: any;\n\n    if (isAddress) {\n      data = await fetchDexScreener(`/tokens/v1/${chain}/${token}`);\n    } else {\n      data = await fetchDexScreener(`/latest/dex/search?q=${encodeURIComponent(token)}`);\n    }\n\n    const pairs = data?.pairs || data || [];\n    if (!pairs.length) {\n      return jsonResult({\n        found: false,\n        query: token,\n        chain,\n        message: `No results found for \"${token}\" on ${chain}. Try a contract address or different chain.`,\n      });\n    }\n\n    const filtered = (Array.isArray(pairs) ? pairs : [pairs])\n      .filter((p: any) => !chain || p.chainId === chain)\n      .sort((a: any, b: any) => (b.liquidity?.usd ?? 0) - (a.liquidity?.usd ?? 0));\n\n    const top = filtered[0];\n    if (!top) {\n      return jsonResult({\n        found: false,\n        query: token,\n        chain,\n        message: `Found matches but none on ${chain}.`,\n      });\n    }\n\n    const result: TokenPrice = {\n      address: top.baseToken?.address ?? token,\n      symbol: top.baseToken?.symbol ?? token,\n      name: top.baseToken?.name ?? '',\n      priceUsd: parseFloat(top.priceUsd ?? '0'),\n      change24h: top.priceChange?.h24 ?? 0,\n      volume24h: top.volume?.h24 ?? 0,\n      liquidity: top.liquidity?.usd ?? 0,\n      marketCap: top.marketCap ?? top.fdv,\n      source: 'dexscreener',\n    };\n\n    return jsonResult({\n      found: true,\n      ...result,\n      confidence: 'single-source',\n      pairAddress: top.pairAddress,\n      dexId: top.dexId,\n      url: top.url,\n    });\n  } catch (err) {\n    return errorResult(`Price lookup failed: ${err instanceof Error ? err.message : String(err)}`);\n  }\n}\n\nasync function handleSearch(params: Record<string, unknown>) {\n  const token = readStringParam(params, 'token', { required: true })!;\n\n  try {\n    const data = await fetchDexScreener(`/latest/dex/search?q=${encodeURIComponent(token)}`);\n    const pairs = data?.pairs ?? [];\n\n    // Deduplicate by base token address, keep highest liquidity\n    const seen = new Map<string, any>();\n    for (const pair of pairs) {\n      const addr = pair.baseToken?.address;\n      if (!addr) continue;\n      const existing = seen.get(addr);\n      if (!existing || (pair.liquidity?.usd ?? 0) > (existing.liquidity?.usd ?? 0)) {\n        seen.set(addr, pair);\n      }\n    }\n\n    const results = Array.from(seen.values())\n      .sort((a, b) => (b.liquidity?.usd ?? 0) - (a.liquidity?.usd ?? 0))\n      .slice(0, 10)\n      .map((p: any) => ({\n        symbol: p.baseToken?.symbol,\n        name: p.baseToken?.name,\n        address: p.baseToken?.address,\n        chain: p.chainId,\n        priceUsd: parseFloat(p.priceUsd ?? '0'),\n        liquidity: p.liquidity?.usd ?? 0,\n        volume24h: p.volume?.h24 ?? 0,\n        url: p.url,\n      }));\n\n    return jsonResult({ query: token, results, count: results.length });\n  } catch (err) {\n    return errorResult(`Search failed: ${err instanceof Error ? err.message : String(err)}`);\n  }\n}\n\nasync function handleTrending(params: Record<string, unknown>) {\n  const chainInput = readStringParam(params, 'chain') || 'base';\n  const chain = resolveChain(chainInput);\n\n  try {\n    // DexScreener doesn't have a direct trending endpoint, but we can\n    // use the token boosts or search for high-volume pairs\n    const data = await fetchDexScreener(`/token-boosts/top/v1`);\n    \n    const results = (data ?? [])\n      .filter((t: any) => !chain || t.chainId === chain)\n      .slice(0, 20)\n      .map((t: any) => ({\n        symbol: t.tokenAddress ? undefined : t.description,\n        address: t.tokenAddress,\n        chain: t.chainId,\n        url: t.url,\n        boostAmount: t.totalAmount,\n      }));\n\n    return jsonResult({\n      chain,\n      trending: results,\n      count: results.length,\n      note: 'Trending tokens by DexScreener boost amount',\n    });\n  } catch (err) {\n    return errorResult(`Trending lookup failed: ${err instanceof Error ? err.message : String(err)}`);\n  }\n}\n"],"mappings":"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"}