Skip to content
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

Proxy API #729

Draft
wants to merge 16 commits into
base: main
Choose a base branch
from
Draft
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
29 changes: 10 additions & 19 deletions packages/frontend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,6 @@ Open [http://localhost:3000](http://localhost:3000) with your browser to see the

You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file.

[API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`.

The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages.

## Testing

Please note that testing is a WIP. We use [synpress](https://github.com/synthetixio/synpress) but are having a few bugs (i.e., can't use watch mode aka `yarn test:watch` in our package).
Expand All @@ -47,21 +43,6 @@ You also need a `cypress.env.json` file with the following content:
}
```

## Learn More

To learn more about Next.js, take a look at the following resources:

- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.

You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!

## Deploy on Vercel

The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.

Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.

## Store

We use [zustand](https://github.com/pmndrs/zustand) as our primary store for our application. It's easy to use and understand, but there are a few drawbacks.
Expand Down Expand Up @@ -169,3 +150,13 @@ There are 4 different pages:
- my-positions/ lists all of the user open positions
- my-positions/{chainId}-{vaultAddr} manage an open position
```

## API

[API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api](http://localhost:3000/api/hello).

The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages.

We have three serverless functions, using [@vercel/kv](https://vercel.com/docs/storage/vercel-kv) as a Redis db, for storing data from Defillama and different staking services.

![Alt text](public/assets/images/api_flow.svg?raw=true "API flow")
8 changes: 3 additions & 5 deletions packages/frontend/helpers/cache.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { GetLLamaFinancialsResponse } from '@x-fuji/sdk';
import { FinancialsResponse } from '@x-fuji/sdk';

export const CACHE_LIMIT = 1000 * 60 * 15; // Fifteen minutes

Expand Down Expand Up @@ -31,9 +31,7 @@ function init(): Promise<IDBDatabase> {
return dbPromise;
}

export async function setLlamaCache(
data: GetLLamaFinancialsResponse
): Promise<void> {
export async function setLlamaCache(data: FinancialsResponse): Promise<void> {
const db = await init();
const transaction = db.transaction(
['pools', 'lendBorrows', 'metaData'],
Expand All @@ -47,7 +45,7 @@ export async function setLlamaCache(
.put({ name: 'lastUpdated', date: new Date() });
}

export async function getLlamaCache(): Promise<GetLLamaFinancialsResponse> {
export async function getLlamaCache(): Promise<FinancialsResponse> {
return new Promise(async (resolve, reject) => {
const db = await init();
const transaction = db.transaction(['pools', 'lendBorrows'], 'readonly');
Expand Down
4 changes: 2 additions & 2 deletions packages/frontend/helpers/vaults.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import {
AbstractVault,
Address,
FinancialsResponse,
FujiError,
FujiResultPromise,
FujiResultSuccess,
GetLLamaFinancialsResponse,
VaultType,
VaultWithFinancials,
} from '@x-fuji/sdk';
Expand Down Expand Up @@ -100,7 +100,7 @@ export const allAvailableVaults = (): VaultWithFinancials[] => [
];

export const getLlamasWithFinancials =
async (): FujiResultPromise<GetLLamaFinancialsResponse> => {
async (): FujiResultPromise<FinancialsResponse> => {
const lastUpdated = await getCacheLastUpdatedDate();
const cache = await getLlamaCache();
const now = new Date();
Expand Down
24 changes: 24 additions & 0 deletions packages/frontend/middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { NextRequest, NextResponse } from 'next/server';

import { ApiRoute, Status } from './pages/api/helpers/constants';
import { limit } from './pages/api/helpers/limiter';

export default async function middleware(req: NextRequest) {
if (!_isAPIRoute(req.nextUrl.pathname)) {
return NextResponse.next();
}

try {
await limit(req);
return NextResponse.next();
} catch (error) {
console.error(error);
return new NextResponse(`Rate limit exceeded ${req.method}`, {
status: Status.TOO_MANY_REQUESTS,
});
}
}

function _isAPIRoute(route: string): boolean {
return Object.values(ApiRoute).includes(route as ApiRoute);
}
4 changes: 4 additions & 0 deletions packages/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
"@nivo/line": "^0.80.0",
"@sentry/react": "^7.44.2",
"@sentry/tracing": "^7.44.2",
"@upstash/ratelimit": "^0.4.3",
"@vercel/kv": "^0.2.2",
"@web3-onboard/coinbase": "^2.2.4",
"@web3-onboard/core": "^2.20.0",
"@web3-onboard/injected-wallets": "^2.9.0",
Expand All @@ -38,8 +40,10 @@
"@web3-onboard/walletconnect": "^2.3.8",
"@web3-onboard/xdefi": "^2.0.4",
"@x-fuji/sdk": "^0.1.0",
"axios": "^1.4.0",
"debounce": "^1.2.1",
"ethers": "^5.7.0",
"express": "^4.18.2",
"immer": "^9.0.16",
"moment": "^2.29.4",
"next": "^13.2.1",
Expand Down
24 changes: 24 additions & 0 deletions packages/frontend/pages/api/getFinancials.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { LlamaAssetPool } from '@x-fuji/sdk';
import type { NextApiRequest, NextApiResponse } from 'next';

import { Status } from './helpers/constants';
import { getFinancialsAndStakingFromDB } from './helpers/db';

export default async function handler(_: NextApiRequest, res: NextApiResponse) {
try {
const result = await getFinancialsAndStakingFromDB();
// Update pools with staking rewards. Might have to improve based on more checks than just symbol
result.pools.forEach((pool: LlamaAssetPool) => {
const match = result.staking.find((apy) => apy.symbol === pool.symbol);
if (match) {
pool.stakingApy = match.value;
}
});

res
.status(Status.SUCCESS)
.json({ pools: result.pools, lendBorrows: result.lendBorrows });
} catch (error) {
res.status(Status.ERROR).json({ error });
}
}
31 changes: 31 additions & 0 deletions packages/frontend/pages/api/getProviderStats.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import type { NextApiRequest, NextApiResponse } from 'next';

import { getProviderStatsFromAPI } from './helpers/api/defillama';
import { REFRESH_INTERVAL, Status } from './helpers/constants';
import { getProviderStatsFromDB, saveProviderStatsToDB } from './helpers/db';

export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
try {
const poolId = req.query.poolId;
if (!poolId) throw 'No poolId provided';
if (typeof poolId !== 'string') {
throw 'Invalid poolId provided';
}

const currentTime = Date.now();
const dbData = await getProviderStatsFromDB(poolId);
if (dbData && dbData.timestamp > currentTime - REFRESH_INTERVAL) {
console.log('Returning cached pool stats');
return res.status(Status.SUCCESS).json(dbData.data);
}
const stats = await getProviderStatsFromAPI(poolId);
saveProviderStatsToDB(poolId, stats);

res.status(Status.SUCCESS).json(stats);
} catch (error) {
res.status(Status.ERROR).json({ error });
}
}
13 changes: 0 additions & 13 deletions packages/frontend/pages/api/hello.ts

This file was deleted.

45 changes: 45 additions & 0 deletions packages/frontend/pages/api/helpers/api/defillama.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import {
FinancialsResponse,
FinancialsUri,
ProviderStatsResponse,
} from '@x-fuji/sdk';
import axios from 'axios';

import { DEFILLAMA_PROXY, DefillamaUrl } from '../constants';
import { GetLlamaPoolStatsResponse } from '../types';

function defillamaUri(): FinancialsUri {
return {
lendBorrow: DEFILLAMA_PROXY
? `${DEFILLAMA_PROXY}lendBorrow`
: DefillamaUrl.LEND_BORROW.toString(),
pools: DEFILLAMA_PROXY
? `${DEFILLAMA_PROXY}pools`
: DefillamaUrl.POOLS.toString(),
};
}

export async function getFinancialsFromAPI(): Promise<FinancialsResponse> {
const uri = defillamaUri();
try {
console.log('Starting Defillama API requests');
const [lendBorrows, pools] = await Promise.all([
axios.get(uri.lendBorrow).then(({ data }) => data),
axios.get(uri.pools).then(({ data }) => data.data),
]);
console.log('Completed requests to Defillama API');
return { lendBorrows, pools };
} catch (error) {
console.error('Failed to fetch data from API:', error);
throw error;
}
}

export async function getProviderStatsFromAPI(
poolId: string
): Promise<ProviderStatsResponse> {
const stats = await axios
.get<GetLlamaPoolStatsResponse>(DefillamaUrl.PROVIDER_STATS + `/${poolId}`)
.then(({ data }) => data.data);
return { stats };
}
28 changes: 28 additions & 0 deletions packages/frontend/pages/api/helpers/api/staking.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import axios from 'axios';

import { STAKING_URL, StakingService } from '../constants';
import { StakingResponse } from '../types';

export async function getStakingDataFromAPI(): Promise<StakingResponse[]> {
try {
console.log('Starting staking API requests');
const data = await Promise.all([
axios.get(STAKING_URL.MATICX).then(({ data }) => {
return { symbol: StakingService.MATICX, value: data.value };
}),
axios.get(STAKING_URL.WSTETH).then(({ data }) => {
const value = data.data.apr;
return { symbol: StakingService.WSTETH, value, data };
}),
axios.get(STAKING_URL.RETH).then(({ data }) => {
const value = parseFloat(data.yearlyAPR);
return { symbol: StakingService.RETH, value };
}),
]);
console.log('Completed requests to staking API');
return data;
} catch (error) {
console.error('Failed to fetch staking data from API:', error);
throw error;
}
}
42 changes: 42 additions & 0 deletions packages/frontend/pages/api/helpers/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
export const DEFILLAMA_PROXY = process.env.DEFILLAMA_PROXY;

export const FIFTEEN_MINUTES = 15 * 60 * 1000;

export const REFRESH_INTERVAL = FIFTEEN_MINUTES;

export enum Status {
SUCCESS = 200,
TOO_MANY_REQUESTS = 429,
ERROR = 500,
}

export enum ApiRoute {
GET_FINANCIALS = '/api/geFinancials',
UPDATE_FINANCIALS = '/api/updateFinancials',
GET_STATS = '/api/getPoolStats',
}

export enum DefillamaUrl {
LEND_BORROW = 'https://yields.llama.fi/lendBorrow',
POOLS = 'https://yields.llama.fi/pools',
PROVIDER_STATS = 'https://yields.llama.fi/chartLendBorrow',
}

export enum DbKey {
LEND_BORROW = 'lendBorrow',
POOLS = 'pools',
STAKING_APY = 'stakingApy',
PROVIDER_STATS = 'poolStats',
}

export enum StakingService {
MATICX = 'MATICX',
WSTETH = 'WSTETH',
RETH = 'RETH',
}

export const STAKING_URL: Record<StakingService, string> = {
[StakingService.MATICX]: 'https://universe.staderlabs.com/polygon/apy',
[StakingService.WSTETH]: 'https://eth-api.lido.fi/v1/protocol/steth/apr/last',
[StakingService.RETH]: 'https://api.rocketpool.net/api/apr',
};
Loading
Loading