Skip to content

Commit ab5272b

Browse files
committed
using family api for fetching usd prices in swaps when possible
1 parent 160fdf6 commit ab5272b

File tree

9 files changed

+302
-5
lines changed

9 files changed

+302
-5
lines changed

.env.development

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,5 @@ NEXT_PUBLIC_ENABLE_STAKING=true
99
NEXT_PUBLIC_ENABLE_GOVERNANCE=true
1010
NEXT_PUBLIC_API_BASEURL=https://aave-api-v2.aave.com
1111
NEXT_PUBLIC_SUBGRAPH_API_KEY=
12+
FAMILY_API_KEY=
13+
FAMILY_API_URL=

.env.example

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,5 @@ ZKSYNC_RPC_API_KEY=
3838
LINEA_RPC_API_KEY=
3939
SONIC_RPC_API_KEY=
4040
CELO_RPC_API_KEY=
41+
FAMILY_API_KEY=
42+
FAMILY_API_URL=

pages/api/prices-proxy.md

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# Family Prices Proxy API
2+
3+
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.
4+
5+
## Environment Variables
6+
7+
The following environment variable must be set:
8+
9+
```
10+
FAMILY_API_KEY=your_family_api_key_here
11+
```
12+
13+
## Endpoint
14+
15+
`POST /api/family-prices-proxy`
16+
17+
## Request Format
18+
19+
```json
20+
{
21+
"tokenIds": ["1:0x0000000000000000000000000000000000000000", "137:0x2791bca1f2de4661ed88a30c99a7a9449aa84174"]
22+
}
23+
```
24+
25+
Where `tokenIds` is an array of strings in the format `{chainId}:{tokenAddress}`.
26+
27+
## Response Format
28+
29+
The proxy returns the same response format as the Family API:
30+
31+
```json
32+
{
33+
"prices": {
34+
"1:0x0000000000000000000000000000000000000000": {
35+
"usd": 2807.04,
36+
"usd_24h_change": null,
37+
"usd_24h_vol": null
38+
},
39+
"137:0x2791bca1f2de4661ed88a30c99a7a9449aa84174": {
40+
"usd": 1.00,
41+
"usd_24h_change": -0.01,
42+
"usd_24h_vol": 1000000
43+
}
44+
}
45+
}
46+
```
47+
48+
## CORS Policy
49+
50+
The proxy implements the same CORS policy as other API routes:
51+
- Allows requests from `https://app.aave.com` and `https://aave.com`
52+
- Allows requests from Vercel deployment URLs matching `*.avaraxyz.vercel.app`
53+
- Can be configured to allow all origins by setting `CORS_DOMAINS_ALLOWED=true`
54+
55+
## Usage
56+
57+
The proxy is used by the `FamilyPricesService` class:
58+
59+
```typescript
60+
const familyService = new FamilyPricesService();
61+
62+
// Get single token price
63+
const price = await familyService.getTokenUsdPrice(1, '0x...');
64+
65+
// Get multiple token prices
66+
const prices = await familyService.getMultipleTokenUsdPrices([
67+
{ chainId: 1, tokenAddress: '0x...' },
68+
{ chainId: 137, tokenAddress: '0x...' }
69+
]);
70+
```
71+
72+
## Error Handling
73+
74+
The proxy handles various error scenarios:
75+
- Missing or invalid API key
76+
- Invalid request format
77+
- Family API errors
78+
- Network errors
79+
80+
All errors are logged server-side and return appropriate HTTP status codes with error messages.

pages/api/prices-proxy.ts

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { NextApiRequest, NextApiResponse } from 'next';
2+
3+
const FAMILY_API_URL = process.env.FAMILY_API_URL;
4+
5+
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
6+
const allowedOrigins = ['https://app.aave.com', 'https://aave.com'];
7+
const origin = req.headers.origin;
8+
9+
const isOriginAllowed = (origin: string | undefined): boolean => {
10+
if (!origin) return false;
11+
12+
if (allowedOrigins.includes(origin)) return true;
13+
14+
// Match any subdomain ending with avaraxyz.vercel.app for deployment urls
15+
const allowedPatterns = [/^https:\/\/.*avaraxyz\.vercel\.app$/];
16+
17+
return allowedPatterns.some((pattern) => pattern.test(origin));
18+
};
19+
20+
if (process.env.CORS_DOMAINS_ALLOWED === 'true') {
21+
res.setHeader('Access-Control-Allow-Origin', '*');
22+
} else if (origin && isOriginAllowed(origin)) {
23+
res.setHeader('Access-Control-Allow-Origin', origin);
24+
}
25+
26+
res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS');
27+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
28+
29+
if (req.method === 'OPTIONS') {
30+
return res.status(200).end();
31+
}
32+
33+
if (req.method !== 'POST') {
34+
return res.status(405).json({ error: 'Method not allowed' });
35+
}
36+
37+
try {
38+
const { tokenIds } = req.body;
39+
40+
if (!tokenIds || !Array.isArray(tokenIds)) {
41+
return res.status(400).json({ error: 'tokenIds array is required' });
42+
}
43+
44+
const familyApiKey = process.env.FAMILY_API_KEY;
45+
if (!familyApiKey || !FAMILY_API_URL) {
46+
console.error('FAMILY_API_KEY or FAMILY_API_URL environment variable is not set');
47+
return res.status(500).json({ error: 'Internal server error' });
48+
}
49+
50+
const requestBody = {
51+
tokenIds,
52+
};
53+
54+
const response = await fetch(FAMILY_API_URL, {
55+
method: 'POST',
56+
headers: {
57+
'Content-Type': 'application/json',
58+
'x-api-key': familyApiKey,
59+
Origin: origin || 'https://app.aave.com',
60+
Referer: 'https://app.aave.com/',
61+
},
62+
body: JSON.stringify(requestBody),
63+
});
64+
65+
if (!response.ok) {
66+
console.error('Family API error:', response.status, response.statusText);
67+
return res.status(response.status).json({
68+
error: 'Failed to fetch prices from Family API',
69+
details: response.statusText,
70+
});
71+
}
72+
73+
const data = await response.json();
74+
return res.status(200).json(data);
75+
} catch (error) {
76+
console.error('Family prices proxy error:', error);
77+
return res.status(500).json({ error: 'Internal server error', details: String(error) });
78+
}
79+
}

src/components/transactions/Switch/BaseSwitchModalContent.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,8 @@ export const BaseSwitchModalContent = ({
318318
destDecimals: selectedOutputToken.decimals,
319319
inputSymbol: selectedInputToken.symbol,
320320
outputSymbol: selectedOutputToken.symbol,
321+
isInputTokenCustom: !!selectedInputToken.extensions?.isUserCustom,
322+
isOutputTokenCustom: !!selectedOutputToken.extensions?.isUserCustom,
321323
user,
322324
options: {
323325
partner: 'aave-widget',

src/components/transactions/Switch/switch.types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ export type SwitchParams = {
1919
inputSymbol?: string;
2020
outputSymbol?: string;
2121

22+
isInputTokenCustom?: boolean;
23+
isOutputTokenCustom?: boolean;
24+
2225
setError?: (error: TxErrorType) => void;
2326
};
2427

src/hooks/switch/cowprotocol.rates.ts

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { ChainId } from '@aave/contract-helpers';
12
import {
23
OrderKind,
34
QuoteAndPost,
@@ -14,8 +15,10 @@ import { isChainIdSupportedByCoWProtocol } from 'src/components/transactions/Swi
1415
import { SwitchParams, SwitchRatesType } from 'src/components/transactions/Switch/switch.types';
1516
import { getEthersProvider } from 'src/libs/web3-data-provider/adapters/EthersAdapter';
1617
import { CoWProtocolPricesService } from 'src/services/CoWProtocolPricesService';
18+
import { FamilyPricesService } from 'src/services/FamilyPricesService';
1719
import { getErrorTextFromError, TxAction } from 'src/ui-config/errorMapping';
1820
import { wagmiConfig } from 'src/ui-config/wagmiConfig';
21+
import { getNetworkConfig } from 'src/utils/marketsAndNetworksConfig';
1922

2023
export async function getCowProtocolSellRates({
2124
chainId,
@@ -28,8 +31,11 @@ export async function getCowProtocolSellRates({
2831
inputSymbol,
2932
outputSymbol,
3033
setError,
34+
isInputTokenCustom,
35+
isOutputTokenCustom,
3136
}: SwitchParams): Promise<SwitchRatesType> {
3237
const cowProtocolPricesService = new CoWProtocolPricesService();
38+
const familyPricesService = new FamilyPricesService();
3339
const tradingSdk = new TradingSdk({ chainId });
3440

3541
let orderBookQuote: QuoteAndPost | undefined;
@@ -53,6 +59,9 @@ export async function getCowProtocolSellRates({
5359

5460
const provider = await getEthersProvider(wagmiConfig, { chainId });
5561
const signer = provider?.getSigner();
62+
const isMainnet =
63+
!getNetworkConfig(chainId as unknown as ChainId).isTestnet &&
64+
!getNetworkConfig(chainId as unknown as ChainId).isFork;
5665

5766
if (!inputSymbol || !outputSymbol) {
5867
throw new Error('No input or output symbol provided');
@@ -76,14 +85,19 @@ export async function getCowProtocolSellRates({
7685
console.error(cowError);
7786
throw new Error(cowError?.body?.errorType);
7887
}),
79-
// CoW Quote doesn't return values in USD, so we need to fetch the price from the API separately
80-
cowProtocolPricesService.getTokenUsdPrice(chainId, srcTokenWrapped).catch((cowError) => {
88+
((isInputTokenCustom || !isMainnet)
89+
? cowProtocolPricesService.getTokenUsdPrice(chainId, srcTokenWrapped)
90+
: familyPricesService.getTokenUsdPrice(chainId, srcTokenWrapped)
91+
).catch((cowError) => {
8192
console.error(cowError);
82-
throw new Error(cowError?.body?.errorType);
93+
throw new Error('No price found for token, please try another token');
8394
}),
84-
cowProtocolPricesService.getTokenUsdPrice(chainId, destTokenWrapped).catch((cowError) => {
95+
((isOutputTokenCustom || !isMainnet)
96+
? cowProtocolPricesService.getTokenUsdPrice(chainId, destTokenWrapped)
97+
: familyPricesService.getTokenUsdPrice(chainId, destTokenWrapped)
98+
).catch((cowError) => {
8599
console.error(cowError);
86-
throw new Error(cowError?.body?.errorType);
100+
throw new Error('No price found for token, please try another token');
87101
}),
88102
]);
89103

src/hooks/switch/useMultiProviderSwitchRates.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ export const useMultiProviderSwitchRates = ({
1313
destToken,
1414
user,
1515
inputSymbol,
16+
isInputTokenCustom,
17+
isOutputTokenCustom,
1618
outputSymbol,
1719
srcDecimals,
1820
destDecimals,
@@ -41,6 +43,8 @@ export const useMultiProviderSwitchRates = ({
4143
destDecimals,
4244
inputSymbol,
4345
outputSymbol,
46+
isInputTokenCustom,
47+
isOutputTokenCustom,
4448
});
4549
case 'paraswap':
4650
return await getParaswapSellRates({

src/services/FamilyPricesService.ts

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import { API_ETH_MOCK_ADDRESS } from '@aave/contract-helpers';
2+
import { zeroAddress } from 'viem';
3+
4+
type FamilyPricesResponse = {
5+
prices: {
6+
[tokenId: string]: {
7+
usd: number;
8+
};
9+
};
10+
};
11+
export class FamilyPricesService {
12+
private proxyUrl = '/api/prices-proxy';
13+
14+
/**
15+
* Fetches the USD price of a token from the Family API via proxy.
16+
* @param chainId - The ID of the blockchain network.
17+
* @param tokenAddress - The address of the token.
18+
* @returns The USD price of the token.
19+
*/
20+
async getTokenUsdPrice(chainId: number, tokenAddress: string): Promise<string | undefined> {
21+
try {
22+
const apiExpectedAddress =
23+
tokenAddress.toLowerCase() === API_ETH_MOCK_ADDRESS.toLowerCase()
24+
? zeroAddress
25+
: tokenAddress;
26+
27+
const tokenId = `${chainId}:${apiExpectedAddress}`.toLowerCase();
28+
29+
const response = await fetch(this.proxyUrl, {
30+
method: 'POST',
31+
headers: {
32+
'Content-Type': 'application/json',
33+
},
34+
body: JSON.stringify({
35+
tokenIds: [tokenId],
36+
}),
37+
});
38+
39+
if (!response.ok) {
40+
throw new Error(`Failed to fetch USD price: ${response.statusText}`);
41+
}
42+
43+
const data = (await response.json()) as FamilyPricesResponse;
44+
45+
if (data?.prices && data.prices[tokenId]?.usd !== undefined) {
46+
return data.prices[tokenId].usd.toString();
47+
}
48+
49+
return undefined;
50+
} catch (error) {
51+
console.error('Error fetching token USD price:', error);
52+
return undefined;
53+
}
54+
}
55+
56+
/**
57+
* Fetches the USD prices of multiple tokens from the Family API via proxy.
58+
* @param tokenRequests - Array of objects with chainId and tokenAddress.
59+
* @returns Map of tokenId to USD price.
60+
*/
61+
async getMultipleTokenUsdPrices(
62+
tokenRequests: Array<{ chainId: number; tokenAddress: string }>
63+
): Promise<Map<string, string>> {
64+
const priceMap = new Map<string, string>();
65+
66+
try {
67+
if (tokenRequests.length === 0) {
68+
return priceMap;
69+
}
70+
71+
const tokenIds = tokenRequests.map(({ chainId, tokenAddress }) => {
72+
const apiExpectedAddress =
73+
tokenAddress.toLowerCase() === API_ETH_MOCK_ADDRESS.toLowerCase()
74+
? zeroAddress
75+
: tokenAddress;
76+
return `${chainId}:${apiExpectedAddress}`;
77+
});
78+
79+
const response = await fetch(this.proxyUrl, {
80+
method: 'POST',
81+
headers: {
82+
'Content-Type': 'application/json',
83+
},
84+
body: JSON.stringify({
85+
tokenIds,
86+
}),
87+
});
88+
89+
if (!response.ok) {
90+
throw new Error(`Failed to fetch USD prices: ${response.statusText}`);
91+
}
92+
93+
const data = (await response.json()) as FamilyPricesResponse;
94+
95+
if (data?.prices) {
96+
Object.entries(data.prices).forEach(
97+
([tokenId, priceData]: [string, FamilyPricesResponse['prices'][string]]) => {
98+
if (priceData?.usd !== undefined) {
99+
priceMap.set(tokenId, priceData.usd.toString());
100+
}
101+
}
102+
);
103+
}
104+
105+
return priceMap;
106+
} catch (error) {
107+
console.error('Error fetching multiple token USD prices:', error);
108+
return priceMap;
109+
}
110+
}
111+
}

0 commit comments

Comments
 (0)