From 405022f4d619c37967851252bfd8ec81d2b4f766 Mon Sep 17 00:00:00 2001 From: Martin Grabina Date: Tue, 3 Nov 2026 10:57:07 -0300 Subject: [PATCH] Remove Family price proxy from CoW swap pricing CoW swap quotes now take token USD prices only from CoW's price endpoint (bff.cow.fi), which was already the fallback. Family's backend is being deprecated, and across Aave reserves on CoW chains CoW prices every token Family does and sits closer to the Aave oracle where the two disagree. Drops FamilyPricesService, the /api/prices-proxy route, the FAMILY_API_* env vars, and the custom-token flags that only gated the Family call. --- .env.development | 2 - .env.example | 2 - pages/api/prices-proxy.md | 80 ------------- pages/api/prices-proxy.ts | 79 ------------- .../Swap/helpers/cow/rates.helpers.ts | 40 +------ .../transactions/Swap/hooks/useSwapQuote.ts | 46 ++------ .../transactions/Swap/types/quote.types.ts | 2 - src/services/FamilyPricesService.ts | 111 ------------------ 8 files changed, 15 insertions(+), 347 deletions(-) delete mode 100644 pages/api/prices-proxy.md delete mode 100644 pages/api/prices-proxy.ts delete mode 100644 src/services/FamilyPricesService.ts diff --git a/.env.development b/.env.development index f0a0b4f66f..cfb8e24b2a 100644 --- a/.env.development +++ b/.env.development @@ -9,5 +9,3 @@ NEXT_PUBLIC_ENABLE_STAKING=true NEXT_PUBLIC_ENABLE_GOVERNANCE=true NEXT_PUBLIC_API_BASEURL=https://aave-api-v2.aave.com NEXT_PUBLIC_SUBGRAPH_API_KEY= -FAMILY_API_KEY= -FAMILY_API_URL= diff --git a/.env.example b/.env.example index dff3a73a25..5f0dcb804a 100644 --- a/.env.example +++ b/.env.example @@ -49,8 +49,6 @@ LINEA_RPC_API_KEY= SONIC_RPC_API_KEY= CELO_RPC_API_KEY= MONAD_RPC_API_KEY= -FAMILY_API_KEY= -FAMILY_API_URL= COINGECKO_API_KEY= # TokenLogic REST API (api.tokenlogic.xyz), used by /api/protocol-totals for whole-protocol figures TL_API_KEY= diff --git a/pages/api/prices-proxy.md b/pages/api/prices-proxy.md deleted file mode 100644 index 64c14b8dc9..0000000000 --- a/pages/api/prices-proxy.md +++ /dev/null @@ -1,80 +0,0 @@ -# Family Prices Proxy API - -This API route provides a server-side proxy for the Family API prices endpoint, allowing client-side code to fetch token prices without exposing the API key. - -## Environment Variables - -The following environment variable must be set: - -``` -FAMILY_API_KEY=your_family_api_key_here -``` - -## Endpoint - -`POST /api/family-prices-proxy` - -## Request Format - -```json -{ - "tokenIds": ["1:0x0000000000000000000000000000000000000000", "137:0x2791bca1f2de4661ed88a30c99a7a9449aa84174"] -} -``` - -Where `tokenIds` is an array of strings in the format `{chainId}:{tokenAddress}`. - -## Response Format - -The proxy returns the same response format as the Family API: - -```json -{ - "prices": { - "1:0x0000000000000000000000000000000000000000": { - "usd": 2807.04, - "usd_24h_change": null, - "usd_24h_vol": null - }, - "137:0x2791bca1f2de4661ed88a30c99a7a9449aa84174": { - "usd": 1.00, - "usd_24h_change": -0.01, - "usd_24h_vol": 1000000 - } - } -} -``` - -## CORS Policy - -The proxy implements the same CORS policy as other API routes: -- Allows requests from `https://app.aave.com` and `https://aave.com` -- Allows requests from Vercel deployment URLs matching `*.avaraxyz.vercel.app` -- Can be configured to allow all origins by setting `CORS_DOMAINS_ALLOWED=true` - -## Usage - -The proxy is used by the `FamilyPricesService` class: - -```typescript -const familyService = new FamilyPricesService(); - -// Get single token price -const price = await familyService.getTokenUsdPrice(1, '0x...'); - -// Get multiple token prices -const prices = await familyService.getMultipleTokenUsdPrices([ - { chainId: 1, tokenAddress: '0x...' }, - { chainId: 137, tokenAddress: '0x...' } -]); -``` - -## Error Handling - -The proxy handles various error scenarios: -- Missing or invalid API key -- Invalid request format -- Family API errors -- Network errors - -All errors are logged server-side and return appropriate HTTP status codes with error messages. \ No newline at end of file diff --git a/pages/api/prices-proxy.ts b/pages/api/prices-proxy.ts deleted file mode 100644 index 3fb4d4ffc1..0000000000 --- a/pages/api/prices-proxy.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { NextApiRequest, NextApiResponse } from 'next'; - -const FAMILY_API_URL = process.env.FAMILY_API_URL; - -export default async function handler(req: NextApiRequest, res: NextApiResponse) { - const allowedOrigins = ['https://app.aave.com', 'https://aave.com']; - const origin = req.headers.origin; - - const isOriginAllowed = (origin: string | undefined): boolean => { - if (!origin) return false; - - if (allowedOrigins.includes(origin)) return true; - - // Match any subdomain ending with avaraxyz.vercel.app for deployment urls - const allowedPatterns = [/^https:\/\/.*avaraxyz\.vercel\.app$/]; - - return allowedPatterns.some((pattern) => pattern.test(origin)); - }; - - if (process.env.CORS_DOMAINS_ALLOWED === 'true') { - res.setHeader('Access-Control-Allow-Origin', '*'); - } else if (origin && isOriginAllowed(origin)) { - res.setHeader('Access-Control-Allow-Origin', origin); - } - - res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS'); - res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); - - if (req.method === 'OPTIONS') { - return res.status(200).end(); - } - - if (req.method !== 'POST') { - return res.status(405).json({ error: 'Method not allowed' }); - } - - try { - const { tokenIds } = req.body; - - if (!tokenIds || !Array.isArray(tokenIds)) { - return res.status(400).json({ error: 'tokenIds array is required' }); - } - - const familyApiKey = process.env.FAMILY_API_KEY; - if (!familyApiKey || !FAMILY_API_URL) { - console.error('FAMILY_API_KEY or FAMILY_API_URL environment variable is not set'); - return res.status(500).json({ error: 'Internal server error' }); - } - - const requestBody = { - tokenIds, - }; - - const response = await fetch(FAMILY_API_URL, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'x-api-key': familyApiKey, - Origin: origin || 'https://app.aave.com', - Referer: 'https://app.aave.com/', - }, - body: JSON.stringify(requestBody), - }); - - if (!response.ok) { - console.error('Family API error:', response.status, response.statusText); - return res.status(response.status).json({ - error: 'Failed to fetch prices from Family API', - details: response.statusText, - }); - } - - const data = await response.json(); - return res.status(200).json(data); - } catch (error) { - console.error('Family prices proxy error:', error); - return res.status(500).json({ error: 'Internal server error', details: String(error) }); - } -} diff --git a/src/components/transactions/Swap/helpers/cow/rates.helpers.ts b/src/components/transactions/Swap/helpers/cow/rates.helpers.ts index fa112e35d0..379c7358f8 100644 --- a/src/components/transactions/Swap/helpers/cow/rates.helpers.ts +++ b/src/components/transactions/Swap/helpers/cow/rates.helpers.ts @@ -1,12 +1,9 @@ -import { ChainId } from '@aave/contract-helpers'; import { OrderKind, QuoteAndPost, WRAPPED_NATIVE_CURRENCIES } from '@cowprotocol/cow-sdk'; import { BigNumber } from 'bignumber.js'; import { getEthersProvider } from 'src/libs/web3-data-provider/adapters/EthersAdapter'; import { CoWProtocolPricesService } from 'src/services/CoWProtocolPricesService'; -import { FamilyPricesService } from 'src/services/FamilyPricesService'; import { getErrorTextFromError, TxAction } from 'src/ui-config/errorMapping'; import { wagmiConfig } from 'src/ui-config/wagmiConfig'; -import { getNetworkConfig } from 'src/utils/marketsAndNetworksConfig'; import { COW_PARTNER_FEE, isChainIdSupportedByCoWProtocol } from '../../constants/cow.constants'; import { isNativeToken } from '../../helpers/cow'; @@ -15,33 +12,6 @@ import { getAppDataForQuote } from './adapters.helpers'; import { getCowTradingSdkByChainIdAndAppCode } from './env.helpers'; import { getSlippageSuggestion } from './orders.helpers'; -export const getTokenUsdPrice = async ( - chainId: number, - tokenAddress: string, - isTokenCustom: boolean, - isMainnet: boolean -) => { - const cowProtocolPricesService = new CoWProtocolPricesService(); - const familyPricesService = new FamilyPricesService(); - - try { - let price; - - if (!isTokenCustom && isMainnet) { - price = await familyPricesService.getTokenUsdPrice(chainId, tokenAddress); - } - - if (price) { - return price; - } - - return await cowProtocolPricesService.getTokenUsdPrice(chainId, tokenAddress); - } catch (familyError) { - console.error(familyError); - return undefined; - } -}; - export async function getCowProtocolSellRates({ chainId, amount, @@ -53,8 +23,6 @@ export async function getCowProtocolSellRates({ swapType, inputSymbol, outputSymbol, - isInputTokenCustom, - isOutputTokenCustom, appCode, market, setError, @@ -62,6 +30,7 @@ export async function getCowProtocolSellRates({ invertedQuoteRoute = false, }: ProviderRatesParams): Promise { const tradingSdk = await getCowTradingSdkByChainIdAndAppCode(chainId, appCode); + const cowProtocolPricesService = new CoWProtocolPricesService(); let orderBookQuote: QuoteAndPost | undefined; let srcTokenPriceUsd: string | undefined; let destTokenPriceUsd: string | undefined; @@ -83,9 +52,6 @@ export async function getCowProtocolSellRates({ const provider = await getEthersProvider(wagmiConfig, { chainId }); const signer = provider?.getSigner(); - const isMainnet = - !getNetworkConfig(chainId as unknown as ChainId).isTestnet && - !getNetworkConfig(chainId as unknown as ChainId).isFork; if (!inputSymbol || !outputSymbol) { throw new Error('No input or output symbol provided'); @@ -130,8 +96,8 @@ export async function getCowProtocolSellRates({ 'Failed to get quote from CoW Protocol'; throw new Error(errorMessage); }), - getTokenUsdPrice(chainId, srcTokenWrapped, isInputTokenCustom ?? false, isMainnet), - getTokenUsdPrice(chainId, destTokenWrapped, isOutputTokenCustom ?? false, isMainnet), + cowProtocolPricesService.getTokenUsdPrice(chainId, srcTokenWrapped), + cowProtocolPricesService.getTokenUsdPrice(chainId, destTokenWrapped), ]); if (!srcTokenPriceUsd || !destTokenPriceUsd) { diff --git a/src/components/transactions/Swap/hooks/useSwapQuote.ts b/src/components/transactions/Swap/hooks/useSwapQuote.ts index 414ac20386..1629d91f52 100644 --- a/src/components/transactions/Swap/hooks/useSwapQuote.ts +++ b/src/components/transactions/Swap/hooks/useSwapQuote.ts @@ -19,7 +19,6 @@ import { SwapQuoteType as SwapQuoteType, SwapState, SwapType, - TokenType, } from '../types'; interface TokenSelectionParams { @@ -29,8 +28,6 @@ interface TokenSelectionParams { destDecimals: number; inputSymbol: string; outputSymbol: string; - isInputTokenCustom: boolean; - isOutputTokenCustom: boolean; side: 'buy' | 'sell'; } @@ -68,12 +65,6 @@ const getTokenSelectionForQuote = ( const outputSymbol = invertedQuoteRoute ? state.sourceToken.symbol : state.destinationToken.symbol; - const isInputTokenCustom = invertedQuoteRoute - ? state.destinationToken.tokenType === TokenType.USER_CUSTOM - : state.sourceToken.tokenType === TokenType.USER_CUSTOM; - const isOutputTokenCustom = invertedQuoteRoute - ? state.sourceToken.tokenType === TokenType.USER_CUSTOM - : state.destinationToken.tokenType === TokenType.USER_CUSTOM; const side = invertedQuoteRoute ? (state.side === 'buy' ? 'sell' : 'buy') : state.side; return { @@ -83,8 +74,6 @@ const getTokenSelectionForQuote = ( destDecimals, inputSymbol, outputSymbol, - isInputTokenCustom, - isOutputTokenCustom, side, }; }; @@ -309,27 +298,18 @@ const useMultiProviderSwapQuoteQuery = ({ const appCode = APP_CODE_PER_SWAP_TYPE[params.swapType]; - const { - srcToken, - destToken, - srcDecimals, - destDecimals, - inputSymbol, - outputSymbol, - isInputTokenCustom, - isOutputTokenCustom, - side, - } = useMemo( - () => getTokenSelectionForQuote(requiresQuoteInverted, provider, state), - [ - provider, - state.sourceToken, - state.destinationToken, - state.side, - requiresQuoteInverted, - state.useFlashloan, - ] - ); + const { srcToken, destToken, srcDecimals, destDecimals, inputSymbol, outputSymbol, side } = + useMemo( + () => getTokenSelectionForQuote(requiresQuoteInverted, provider, state), + [ + provider, + state.sourceToken, + state.destinationToken, + state.side, + requiresQuoteInverted, + state.useFlashloan, + ] + ); return useQuery({ queryFn: async () => { @@ -378,8 +358,6 @@ const useMultiProviderSwapQuoteQuery = ({ destDecimals, inputSymbol, outputSymbol, - isInputTokenCustom, - isOutputTokenCustom, appCode, market: currentMarket, setError, diff --git a/src/components/transactions/Swap/types/quote.types.ts b/src/components/transactions/Swap/types/quote.types.ts index fb62eb37d6..728f59add1 100644 --- a/src/components/transactions/Swap/types/quote.types.ts +++ b/src/components/transactions/Swap/types/quote.types.ts @@ -27,8 +27,6 @@ export type ProviderRatesParams = { inputSymbol?: string; outputSymbol?: string; - isInputTokenCustom?: boolean; - isOutputTokenCustom?: boolean; appCode: string; market: CustomMarket; diff --git a/src/services/FamilyPricesService.ts b/src/services/FamilyPricesService.ts deleted file mode 100644 index 138f362f79..0000000000 --- a/src/services/FamilyPricesService.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { API_ETH_MOCK_ADDRESS } from '@aave/contract-helpers'; -import { zeroAddress } from 'viem'; - -type FamilyPricesResponse = { - prices: { - [tokenId: string]: { - usd: number; - }; - }; -}; -export class FamilyPricesService { - private proxyUrl = '/api/prices-proxy'; - - /** - * Fetches the USD price of a token from the Family API via proxy. - * @param chainId - The ID of the blockchain network. - * @param tokenAddress - The address of the token. - * @returns The USD price of the token. - */ - async getTokenUsdPrice(chainId: number, tokenAddress: string): Promise { - try { - const apiExpectedAddress = - tokenAddress.toLowerCase() === API_ETH_MOCK_ADDRESS.toLowerCase() - ? zeroAddress - : tokenAddress; - - const tokenId = `${chainId}:${apiExpectedAddress}`.toLowerCase(); - - const response = await fetch(this.proxyUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - tokenIds: [tokenId], - }), - }); - - if (!response.ok) { - throw new Error(`Failed to fetch USD price: ${response.statusText}`); - } - - const data = (await response.json()) as FamilyPricesResponse; - - if (data?.prices && data.prices[tokenId]?.usd !== undefined) { - return data.prices[tokenId].usd.toString(); - } - - return undefined; - } catch (error) { - console.error('Error fetching token USD price:', error); - return undefined; - } - } - - /** - * Fetches the USD prices of multiple tokens from the Family API via proxy. - * @param tokenRequests - Array of objects with chainId and tokenAddress. - * @returns Map of tokenId to USD price. - */ - async getMultipleTokenUsdPrices( - tokenRequests: Array<{ chainId: number; tokenAddress: string }> - ): Promise> { - const priceMap = new Map(); - - try { - if (tokenRequests.length === 0) { - return priceMap; - } - - const tokenIds = tokenRequests.map(({ chainId, tokenAddress }) => { - const apiExpectedAddress = - tokenAddress.toLowerCase() === API_ETH_MOCK_ADDRESS.toLowerCase() - ? zeroAddress - : tokenAddress; - return `${chainId}:${apiExpectedAddress}`; - }); - - const response = await fetch(this.proxyUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - tokenIds, - }), - }); - - if (!response.ok) { - throw new Error(`Failed to fetch USD prices: ${response.statusText}`); - } - - const data = (await response.json()) as FamilyPricesResponse; - - if (data?.prices) { - Object.entries(data.prices).forEach( - ([tokenId, priceData]: [string, FamilyPricesResponse['prices'][string]]) => { - if (priceData?.usd !== undefined) { - priceMap.set(tokenId, priceData.usd.toString()); - } - } - ); - } - - return priceMap; - } catch (error) { - console.error('Error fetching multiple token USD prices:', error); - return priceMap; - } - } -}