Skip to content

using family api for token prices in swaps when possible #2500

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jul 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .env.development
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@ 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=
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,5 @@ ZKSYNC_RPC_API_KEY=
LINEA_RPC_API_KEY=
SONIC_RPC_API_KEY=
CELO_RPC_API_KEY=
FAMILY_API_KEY=
FAMILY_API_URL=
80 changes: 80 additions & 0 deletions pages/api/prices-proxy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# 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.
79 changes: 79 additions & 0 deletions pages/api/prices-proxy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
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) });
}
}
2 changes: 2 additions & 0 deletions src/components/transactions/Switch/BaseSwitchModalContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,8 @@ export const BaseSwitchModalContent = ({
destDecimals: selectedOutputToken.decimals,
inputSymbol: selectedInputToken.symbol,
outputSymbol: selectedOutputToken.symbol,
isInputTokenCustom: !!selectedInputToken.extensions?.isUserCustom,
isOutputTokenCustom: !!selectedOutputToken.extensions?.isUserCustom,
user,
options: {
partner: 'aave-widget',
Expand Down
3 changes: 3 additions & 0 deletions src/components/transactions/Switch/switch.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ export type SwitchParams = {
inputSymbol?: string;
outputSymbol?: string;

isInputTokenCustom?: boolean;
isOutputTokenCustom?: boolean;

setError?: (error: TxErrorType) => void;
};

Expand Down
24 changes: 19 additions & 5 deletions src/hooks/switch/cowprotocol.rates.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { ChainId } from '@aave/contract-helpers';
import {
OrderKind,
QuoteAndPost,
Expand All @@ -14,8 +15,10 @@ import { isChainIdSupportedByCoWProtocol } from 'src/components/transactions/Swi
import { SwitchParams, SwitchRatesType } from 'src/components/transactions/Switch/switch.types';
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';

export async function getCowProtocolSellRates({
chainId,
Expand All @@ -28,8 +31,11 @@ export async function getCowProtocolSellRates({
inputSymbol,
outputSymbol,
setError,
isInputTokenCustom,
isOutputTokenCustom,
}: SwitchParams): Promise<SwitchRatesType> {
const cowProtocolPricesService = new CoWProtocolPricesService();
const familyPricesService = new FamilyPricesService();
const tradingSdk = new TradingSdk({ chainId });

let orderBookQuote: QuoteAndPost | undefined;
Expand All @@ -53,6 +59,9 @@ 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');
Expand All @@ -76,14 +85,19 @@ export async function getCowProtocolSellRates({
console.error(cowError);
throw new Error(cowError?.body?.errorType);
}),
// CoW Quote doesn't return values in USD, so we need to fetch the price from the API separately
cowProtocolPricesService.getTokenUsdPrice(chainId, srcTokenWrapped).catch((cowError) => {
(isInputTokenCustom || !isMainnet
? cowProtocolPricesService.getTokenUsdPrice(chainId, srcTokenWrapped)
: familyPricesService.getTokenUsdPrice(chainId, srcTokenWrapped)
).catch((cowError) => {
console.error(cowError);
throw new Error(cowError?.body?.errorType);
throw new Error('No price found for token, please try another token');
}),
cowProtocolPricesService.getTokenUsdPrice(chainId, destTokenWrapped).catch((cowError) => {
(isOutputTokenCustom || !isMainnet
? cowProtocolPricesService.getTokenUsdPrice(chainId, destTokenWrapped)
: familyPricesService.getTokenUsdPrice(chainId, destTokenWrapped)
).catch((cowError) => {
console.error(cowError);
throw new Error(cowError?.body?.errorType);
throw new Error('No price found for token, please try another token');
}),
]);

Expand Down
4 changes: 4 additions & 0 deletions src/hooks/switch/useMultiProviderSwitchRates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ export const useMultiProviderSwitchRates = ({
destToken,
user,
inputSymbol,
isInputTokenCustom,
isOutputTokenCustom,
outputSymbol,
srcDecimals,
destDecimals,
Expand Down Expand Up @@ -41,6 +43,8 @@ export const useMultiProviderSwitchRates = ({
destDecimals,
inputSymbol,
outputSymbol,
isInputTokenCustom,
isOutputTokenCustom,
});
case 'paraswap':
return await getParaswapSellRates({
Expand Down
111 changes: 111 additions & 0 deletions src/services/FamilyPricesService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
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<string | undefined> {
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<Map<string, string>> {
const priceMap = new Map<string, string>();

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;
}
}
}
Loading