Skip to content

Commit

Permalink
perf(farm-api): kv cache for v3 lp liquidity (#7503)
Browse files Browse the repository at this point in the history
<!--
Before opening a pull request, please read the [contributing
guidelines](https://github.com/pancakeswap/pancake-frontend/blob/develop/CONTRIBUTING.md)
first
-->

<!--
copilot:all
-->
### <samp>🤖 Generated by Copilot at 629bf57</samp>

### Summary
🌐🚀🌊

<!--
1. 🌐 - This emoji represents the Cloudflare KV storage, which is a
global key-value store that can be used to cache and retrieve data
across different regions and edge locations. This emoji is suitable for
the new functionality to cache and retrieve the liquidity data of the V3
farms from the Cloudflare KV storage.
2. 🚀 - This emoji represents the improved performance and reliability of
the V3 farm API endpoints, which can benefit from the caching logic and
reduce the number of requests to the V3 subgraph. This emoji is suitable
for the caching logic added to the V3 farm API endpoints in `v3.ts`.
3. 🌊 - This emoji represents the liquidity data of the V3 farms, which
is the main information that is cached and retrieved from the Cloudflare
KV storage. This emoji is suitable for the new key and the new function
to generate it, and the new methods in the `FarmKV` class.
-->
This pull request enhances the farms API by adding caching functionality
for the V3 farms using Cloudflare KV storage. It modifies the `kv.ts`
and `v3.ts` files in the `apis/farms/src` directory to implement the
caching logic and the key generation for the V3 farms. This aims to
improve the performance and reliability of the V3 farm API endpoints.

> _Sing, O Muse, of the cunning code that cached the V3 farms_
> _And saved them in the Cloudflare KV, the swift and spacious store_
> _That `FarmKV`, the skillful class, could fetch with ease and charm_
> _And serve them to the endpoints in `v3.ts`, the faithful and the
sure_

### Walkthrough
* Add and use `FarmKV` class to cache V3 farm liquidity data to
Cloudflare KV storage
([link](https://github.com/pancakeswap/pancake-frontend/pull/7503/files?diff=unified&w=0#diff-f92f8cf858e10219e605f61b907de60ad84991ed3a05cb7e3ec9f0d6764ba689L8-R24),
[link](https://github.com/pancakeswap/pancake-frontend/pull/7503/files?diff=unified&w=0#diff-f92f8cf858e10219e605f61b907de60ad84991ed3a05cb7e3ec9f0d6764ba689R38),
[link](https://github.com/pancakeswap/pancake-frontend/pull/7503/files?diff=unified&w=0#diff-f92f8cf858e10219e605f61b907de60ad84991ed3a05cb7e3ec9f0d6764ba689L51-R75),
[link](https://github.com/pancakeswap/pancake-frontend/pull/7503/files?diff=unified&w=0#diff-0e56562b1a287487ad933f04eb8867f8afbccfc461716cc24e9ddad41d614126R13),
[link](https://github.com/pancakeswap/pancake-frontend/pull/7503/files?diff=unified&w=0#diff-0e56562b1a287487ad933f04eb8867f8afbccfc461716cc24e9ddad41d614126L207-R245),
[link](https://github.com/pancakeswap/pancake-frontend/pull/7503/files?diff=unified&w=0#diff-0e56562b1a287487ad933f04eb8867f8afbccfc461716cc24e9ddad41d614126L280-R396),
[link](https://github.com/pancakeswap/pancake-frontend/pull/7503/files?diff=unified&w=0#diff-0e56562b1a287487ad933f04eb8867f8afbccfc461716cc24e9ddad41d614126L378-R440))
* Handle errors and fallbacks when querying V3 subgraph for V3 farm
liquidity data
([link](https://github.com/pancakeswap/pancake-frontend/pull/7503/files?diff=unified&w=0#diff-0e56562b1a287487ad933f04eb8867f8afbccfc461716cc24e9ddad41d614126L143-R144),
[link](https://github.com/pancakeswap/pancake-frontend/pull/7503/files?diff=unified&w=0#diff-0e56562b1a287487ad933f04eb8867f8afbccfc461716cc24e9ddad41d614126L154-R155),
[link](https://github.com/pancakeswap/pancake-frontend/pull/7503/files?diff=unified&w=0#diff-0e56562b1a287487ad933f04eb8867f8afbccfc461716cc24e9ddad41d614126L280-R396),
[link](https://github.com/pancakeswap/pancake-frontend/pull/7503/files?diff=unified&w=0#diff-0e56562b1a287487ad933f04eb8867f8afbccfc461716cc24e9ddad41d614126L378-R440))
  • Loading branch information
0xjojoex authored Aug 11, 2023
1 parent 6e8b026 commit ca0f90b
Show file tree
Hide file tree
Showing 6 changed files with 203 additions and 72 deletions.
26 changes: 24 additions & 2 deletions apis/farms/src/kv.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,23 @@ const KV_PREFIX = {
lpApr: 'lpApr:',
farmByPid: 'farmByPid:',
farmList: 'farmList:',
farmV3Liquidity: 'farmV3Liquidity:',
}

export type FarmResult = Array<FarmWithPrices & { cakeApr?: string; lpApr?: number }>

export type FarmV3LiquidityResult = {
tvl: {
token0: string
token1: string
}
formatted: {
token0: string
token1: string
}
updatedAt: string
}

export type SavedFarmResult = {
updatedAt: string
poolLength: number
Expand All @@ -22,6 +35,7 @@ const createKvKey = {
apr: (chainId: number | string) => `${KV_PREFIX.lpApr}${chainId}`,
farm: (chainId: number | string, pid: number | string) => `${KV_PREFIX.farmByPid}${chainId}-${pid}`,
farms: (chainId: number | string) => `${KV_PREFIX.farmList}${chainId}`,
farmsV3Liquidity: (chainId: number | string, address: string) => `${KV_PREFIX.farmV3Liquidity}${chainId}-${address}`,
}

export class FarmKV {
Expand All @@ -48,6 +62,14 @@ export class FarmKV {
return FARMS.put(createKvKey.apr(chainId), stringifyAprMap)
}
}
}

export const farmKV = new FarmKV()
static async getV3Liquidity(chainId: number | string, address: string) {
return FARMS.get<FarmV3LiquidityResult>(createKvKey.farmsV3Liquidity(chainId, address), {
type: 'json',
})
}

static async saveV3Liquidity(chainId: number | string, address: string, result: FarmV3LiquidityResult) {
return FARMS.put(createKvKey.farmsV3Liquidity(chainId, address), JSON.stringify(result))
}
}
228 changes: 162 additions & 66 deletions apis/farms/src/v3.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { z } from 'zod'
import { Address } from 'viem'

import { viemProviders } from './provider'
import { FarmKV } from './kv'

export const V3_SUBGRAPH_CLIENTS = {
[ChainId.ETHEREUM]: new GraphQLClient('https://api.thegraph.com/subgraphs/name/pancakeswap/exchange-v3-eth', {
Expand Down Expand Up @@ -140,7 +141,7 @@ export const handler = async (req: Request, event: FetchEvent) => {
let response: Response | undefined

if (!cacheResponse) {
response = await handler_(req)
response = await handler_(req, event)
if (response.status === 200) {
event.waitUntil(cache.put(event.request, response.clone()))
}
Expand All @@ -151,7 +152,7 @@ export const handler = async (req: Request, event: FetchEvent) => {
return response
}

const handler_ = async (req: Request) => {
const handler_ = async (req: Request, event: FetchEvent) => {
const parsed = zParams.safeParse(req.params)

if (parsed.success === false) {
Expand Down Expand Up @@ -204,6 +205,139 @@ const handler_ = async (req: Request) => {
return error(404, { error: 'PoolInfo not found' })
}

const kvCache = await FarmKV.getV3Liquidity(chainId, address)

if (kvCache) {
// 5 mins
if (new Date().getTime() > new Date(kvCache.updatedAt).getTime() + 1000 * 60 * 5) {
return json(
{
tvl: {
token0: kvCache.tvl.token0,
token1: kvCache.tvl.token1,
},
formatted: {
token0: kvCache.formatted.token0,
token1: kvCache.formatted.token1,
},
updatedAt: kvCache.updatedAt,
},
{
headers: {
'Cache-Control': CACHE_TIME.short,
},
},
)
}
}

try {
const [allocPoint, , , , , totalLiquidity] = poolInfo
const [sqrtPriceX96, tick] = slot0

// don't cache when pool is not active or has no liquidity
if (!allocPoint || !totalLiquidity) {
return json(
{
tvl: {
token0: '0',
token1: '0',
},
formatted: {
token0: '0',
token1: '0',
},
updatedAt: new Date().toISOString(),
},
{
headers: {
'Cache-Control': 'no-cache',
},
},
)
}

const resultTimeout = await Promise.race([
timeout(15),
fetchLiquidityFromSubgraph(chainId, address, masterChefV3Address, tick, sqrtPriceX96),
])

if (!resultTimeout) {
throw new Error('Timeout')
}

const { allActivePositions, result } = resultTimeout

if (allActivePositions.length === 0) {
return json(
{
tvl: {
token0: '0',
token1: '0',
},
formatted: {
token0: '0',
token1: '0',
},
updatedAt: new Date().toISOString(),
},
{
headers: {
'Cache-Control': 'no-cache',
},
},
)
}

if (!result) {
throw new Error('No result')
}

event.waitUntil(FarmKV.saveV3Liquidity(chainId, address, result))

return json(result, {
headers: {
'Cache-Control': allActivePositions.length > 50 ? CACHE_TIME.long : CACHE_TIME.short,
},
})
} catch (e) {
console.error(e)

if (kvCache) {
return json(
{
tvl: {
token0: kvCache.tvl.token0,
token1: kvCache.tvl.token1,
},
formatted: {
token0: kvCache.formatted.token0,
token1: kvCache.formatted.token1,
},
updatedAt: kvCache.updatedAt,
},
{
headers: {
'Cache-Control': CACHE_TIME.short,
},
},
)
}

return error(500, { error: 'Failed to get active liquidity' })
}
}

async function fetchLiquidityFromSubgraph(
chainId: keyof typeof V3_SUBGRAPH_CLIENTS,
address: string,
masterChefV3Address: string,
tick: number,
sqrtPriceX96: bigint,
) {
const updatedAt = new Date().toISOString()
let allActivePositions: any[] = []

const poolTokens = await V3_SUBGRAPH_CLIENTS[chainId].request(
gql`
query pool($poolAddress: String!) {
Expand All @@ -224,36 +358,8 @@ const handler_ = async (req: Request) => {
},
)

const updatedAt = new Date().toISOString()

const [allocPoint, , , , , totalLiquidity] = poolInfo
const [sqrtPriceX96, tick] = slot0

// don't cache when pool is not active or has no liquidity
if (!allocPoint || !totalLiquidity) {
return json(
{
tvl: {
token0: '0',
token1: '0',
},
formatted: {
token0: '0',
token1: '0',
},
updatedAt,
},
{
headers: {
'Cache-Control': 'no-cache',
},
},
)
}

let allActivePositions: any[] = []

async function fetchPositionByMasterChefId(posId = '0') {
// eslint-disable-next-line no-inner-declarations
async function fetchPositionByMasterChefId(posId_ = '0') {
const resp = await V3_SUBGRAPH_CLIENTS[chainId].request(
gql`
query tvl($poolAddress: String!, $owner: String!, $posId: String!, $currentTick: String!) {
Expand All @@ -279,7 +385,7 @@ const handler_ = async (req: Request) => {
poolAddress: address,
owner: masterChefV3Address.toLowerCase(),
currentTick: tick.toString(),
posId,
posId: posId_,
},
)

Expand All @@ -306,24 +412,9 @@ const handler_ = async (req: Request) => {
})

if (allActivePositions.length === 0) {
return json(
{
tvl: {
token0: '0',
token1: '0',
},
formatted: {
token0: '0',
token1: '0',
},
updatedAt,
},
{
headers: {
'Cache-Control': 'no-cache',
},
},
)
return {
allActivePositions,
}
}

const currentTick = tick
Expand Down Expand Up @@ -364,22 +455,27 @@ const handler_ = async (req: Request) => {
totalToken1.toString(),
).toExact()

return json(
{
tvl: {
token0: totalToken0.toString(),
token1: totalToken1.toString(),
},
formatted: {
token0: curr0,
token1: curr1,
},
updatedAt,
const result = {
tvl: {
token0: totalToken0.toString(),
token1: totalToken1.toString(),
},
{
headers: {
'Cache-Control': allActivePositions.length > 50 ? CACHE_TIME.long : CACHE_TIME.short,
},
formatted: {
token0: curr0,
token1: curr1,
},
updatedAt,
}
return {
result,
allActivePositions,
}
}

function timeout(seconds: number) {
return new Promise<null>((resolve) =>
setTimeout(() => {
resolve(null)
}, seconds * 1_000),
)
}
2 changes: 2 additions & 0 deletions apps/web/src/pages/_document.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/* eslint-disable jsx-a11y/iframe-has-title */
import { FARMS_API } from 'config/constants/endpoints'
import Document, { Html, Head, Main, NextScript, DocumentContext } from 'next/document'
import { ServerStyleSheet } from 'styled-components'

Expand Down Expand Up @@ -37,6 +38,7 @@ class MyDocument extends Document {
<link rel="preconnect" href={process.env.NEXT_PUBLIC_NODE_PRODUCTION} />
)}
<link rel="preconnect" href="https://fonts.gstatic.com" />
<link rel="preconnect" href={FARMS_API} />
<link href="https://fonts.googleapis.com/css2?family=Kanit:wght@400;600&amp;display=swap" rel="stylesheet" />
<link rel="shortcut icon" href="/favicon.ico" />
<link rel="apple-touch-icon" href="/logo.png" />
Expand Down
Loading

0 comments on commit ca0f90b

Please sign in to comment.