import fs from 'fs' import { request, gql } from 'graphql-request' import BigNumber from 'bignumber.js' import { ChainId } from '@pancakeswap-libs/sdk' import chunk from 'lodash/chunk' import { sub, getUnixTime } from 'date-fns' import farmsConfig from '../src/config/constants/farms' const BLOCK_SUBGRAPH_ENDPOINT = 'https://api.thegraph.com/subgraphs/name/pancakeswap/blocks' const STREAMING_FAST_ENDPOINT = 'https://bsc.streamingfast.io/subgraphs/name/pancakeswap/exchange-v2' interface BlockResponse { blocks: { number: string }[] } interface SingleFarmResponse { id: string reserveUSD: string volumeUSD: string } interface FarmsResponse { farmsAtLatestBlock: SingleFarmResponse[] farmsOneWeekAgo: SingleFarmResponse[] } interface AprMap { [key: string]: BigNumber } const getWeekAgoTimestamp = () => { const weekAgo = sub(new Date(), { weeks: 1 }) return getUnixTime(weekAgo) } const LP_HOLDERS_FEE = 0.0017 const WEEKS_IN_A_YEAR = 52.1429 const getBlockAtTimestamp = async (timestamp: number) => { try { const { blocks } = await request( BLOCK_SUBGRAPH_ENDPOINT, `query getBlock($timestampGreater: Int!, $timestampLess: Int!) { blocks(first: 1, where: { timestamp_gt: $timestampGreater, timestamp_lt: $timestampLess }) { number } }`, { timestampGreater: timestamp, timestampLess: timestamp + 600 }, ) return parseInt(blocks[0].number, 10) } catch (error) { throw new Error(`Failed to fetch block number for ${timestamp}\n${error}`) } } const getAprsForFarmGroup = async (addresses: string[], blockWeekAgo: number): Promise => { try { const { farmsAtLatestBlock, farmsOneWeekAgo } = await request( STREAMING_FAST_ENDPOINT, gql` query farmsBulk($addresses: [String]!, $blockWeekAgo: Int!) { farmsAtLatestBlock: pairs(first: 30, where: { id_in: $addresses }) { id volumeUSD reserveUSD } farmsOneWeekAgo: pairs(first: 30, where: { id_in: $addresses }, block: { number: $blockWeekAgo }) { id volumeUSD reserveUSD } } `, { addresses, blockWeekAgo }, ) const aprs: AprMap = farmsAtLatestBlock.reduce((aprMap, farm) => { const farmWeekAgo = farmsOneWeekAgo.find((oldFarm) => oldFarm.id === farm.id) // In case farm is too new to estimate LP APR (i.e. not returned in farmsOneWeekAgo query) - return 0 let lpApr = new BigNumber(0) if (farmWeekAgo) { const volume7d = new BigNumber(farm.volumeUSD).minus(new BigNumber(farmWeekAgo.volumeUSD)) const lpFees7d = volume7d.times(LP_HOLDERS_FEE) const lpFeesInAYear = lpFees7d.times(WEEKS_IN_A_YEAR) // Some untracked pairs like KUN-QSD will report 0 volume if (lpFeesInAYear.gt(0)) { const liquidity = new BigNumber(farm.reserveUSD) lpApr = lpFeesInAYear.times(100).dividedBy(liquidity) } } return { ...aprMap, [farm.id]: lpApr.decimalPlaces(2).toNumber(), } }, {}) return aprs } catch (error) { throw new Error(`Failed to fetch LP APR data: ${error}`) } } const fetchAndUpdateLPsAPR = async () => { // pids before 250 are inactive farms from v1 and failed v2 migration const lowerCaseAddresses = farmsConfig .filter((farm) => farm.pid > 250) .map((farm) => farm.lpAddresses[ChainId.MAINNET].toLowerCase()) console.info(`Fetching farm data for ${lowerCaseAddresses.length} addresses`) // Split it into chunks of 30 addresses to avoid gateway timeout const addressesInGroups = chunk(lowerCaseAddresses, 30) const weekAgoTimestamp = getWeekAgoTimestamp() const blockWeekAgo = await getBlockAtTimestamp(weekAgoTimestamp) let allAprs: AprMap = {} // eslint-disable-next-line no-restricted-syntax for (const groupOfAddresses of addressesInGroups) { // eslint-disable-next-line no-await-in-loop const aprs = await getAprsForFarmGroup(groupOfAddresses, blockWeekAgo) allAprs = { ...allAprs, ...aprs } } fs.writeFile(`src/config/constants/lpAprs.json`, JSON.stringify(allAprs, null, 2), (err) => { if (err) throw err console.info(` ✅ - lpAprs.json has been updated!`) }) } fetchAndUpdateLPsAPR()