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

feat(ui): replace hardcoded farms with fetched from the graph #61

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
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
239 changes: 239 additions & 0 deletions src/pages/farm/getFarms.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
import { MASTERCHEF_ADDRESS, Token, WBCH } from '@mistswapdex/sdk';
import { FLEXUSD, MIST } from '../../config/tokens';
import { Chef, PairType } from '../../features/onsen/enum';
import { usePendingSushi, usePositions } from '../../features/onsen/hooks';
import {
useAverageBlockTime,
useEthPrice,
useFarmPairAddresses,
useFarms,
useMasterChefV1HealthCheck,
useMasterChefV1SushiPerBlock,
useMasterChefV1TotalAllocPoint,
useSushiPairs,
useSushiPrice,
} from '../../services/graph'
import { useTokenBalancesWithLoadingIndicator } from '../../state/wallet/hooks';
import { useHardcodedFarms } from './hardcodedFarms';
import { getAddress } from '@ethersproject/address'

function transformFarms(farms, { chainId, filter, mistPriceUSD, bchPriceUSD, averageBlockTime, swapPairs, kashiPairs, masterChefV1SushiPerBlock, masterChefV1TotalAllocPoint, positions }) {
const [v2PairsBalances, fetchingV2PairBalances] = useTokenBalancesWithLoadingIndicator(
MASTERCHEF_ADDRESS[chainId],
farms.map((farm) => new Token(chainId, farm.pair, 18, 'LP', 'LP Token')),
)

if (! fetchingV2PairBalances) {
for (let i=0; i<farms.length; ++i) {
if (v2PairsBalances.hasOwnProperty(farms[i].pair) && farms[i].pool.totalSupply) {
const totalSupply = Number.parseFloat(farms[i].pool.totalSupply.toFixed());
const chefBalance = Number.parseFloat(v2PairsBalances[farms[i].pair].toFixed());

let tvl = 0;
if (farms[i].pool.token0 === MIST[chainId].address) {
const reserve = Number.parseFloat(farms[i].pool.reserves[0].toFixed());
tvl = reserve / totalSupply * chefBalance * mistPriceUSD * 2;
}
else if (farms[i].pool.token1 === MIST[chainId].address) {
const reserve = Number.parseFloat(farms[i].pool.reserves[1].toFixed());
tvl = reserve / totalSupply * chefBalance * mistPriceUSD * 2;
}
else if (farms[i].pool.token0 === FLEXUSD.address) {
const reserve = Number.parseFloat(farms[i].pool.reserves[0].toFixed());
tvl = reserve / totalSupply * chefBalance * 2;
}
else if (farms[i].pool.token1 === FLEXUSD.address) {
const reserve = Number.parseFloat(farms[i].pool.reserves[1].toFixed());
tvl = reserve / totalSupply * chefBalance * 2;
}
else if (farms[i].pool.token0 === WBCH[chainId].address) {
const reserve = Number.parseFloat(farms[i].pool.reserves[0].toFixed());
tvl = reserve / totalSupply * chefBalance * bchPriceUSD * 2;
}
else if (farms[i].pool.token1 === WBCH[chainId].address) {
const reserve = Number.parseFloat(farms[i].pool.reserves[1].toFixed());
tvl = reserve / totalSupply * chefBalance * bchPriceUSD * 2;
}
farms[i].tvl = tvl;
farms[i].chefBalance = chefBalance;
} else {
farms[i].tvl = "0";
farms[i].chefBalance = 0;
}
}
}

const blocksPerDay = 86400 / Number(averageBlockTime)

const map = (pool) => {
// TODO: Account for fees generated in case of swap pairs, and use standard compounding
// algorithm with the same intervals acrosss chains to account for consistency.
// For lending pairs, what should the equivilent for fees generated? Interest gained?
// How can we include this?
// TODO: Deal with inconsistencies between properties on subgraph
pool.owner = pool?.owner || pool?.masterChef
pool.balance = pool?.balance || pool?.slpBalance
const swapPair = swapPairs?.find((pair) => pair.id === pool.pair)
const kashiPair = kashiPairs?.find((pair) => pair.id === pool.pair)
const type = swapPair ? PairType.SWAP : PairType.KASHI
const pair = swapPair || kashiPair
const blocksPerHour = 3600 / averageBlockTime
function getRewards() {
// TODO: Some subgraphs give sushiPerBlock & sushiPerSecond, and mcv2 gives nothing
const sushiPerBlock =
pool?.owner?.sushiPerBlock / 1e18 ||
(pool?.owner?.sushiPerSecond / 1e18) * averageBlockTime ||
masterChefV1SushiPerBlock
const rewardPerBlock = (pool.allocPoint / pool.owner.totalAllocPoint) * sushiPerBlock
const defaultReward = {
token: 'MIST',
icon: 'https://raw.githubusercontent.com/mistswapdex/assets/master/blockchains/smartbch/assets/0x5fA664f69c2A4A3ec94FaC3cBf7049BD9CA73129/logo.png',
rewardPerBlock,
rewardPerDay: rewardPerBlock * blocksPerDay,
rewardPrice: mistPriceUSD,
}
const defaultRewards = [defaultReward]
if (pool.chef === Chef.MASTERCHEF_V2) {
// override for mcv2...
pool.owner.totalAllocPoint = masterChefV1TotalAllocPoint
const icon = ['0', '3', '4', '8'].includes(pool.id)
? `https://raw.githubusercontent.com/mistswapdex/icons/master/token/${pool.rewardToken.symbol.toLowerCase()}.jpg`
: `https://raw.githubusercontent.com/mistswapdex/assets/master/blockchains/smartbch/assets/${getAddress(
pool.rewarder.rewardToken
)}/logo.png`
const decimals = 10 ** pool.rewardToken.decimals
const rewardPerBlock = (pool.rewarder.rewardPerSecond / decimals) * averageBlockTime
const rewardPerDay = (pool.rewarder.rewardPerSecond / decimals) * averageBlockTime * blocksPerDay
const reward = {
token: pool.rewardToken.symbol,
icon: icon,
rewardPerBlock: rewardPerBlock,
rewardPerDay: rewardPerDay,
rewardPrice: pool.rewardToken.derivedETH * bchPriceUSD,
}
return [...defaultRewards, reward]
}
return defaultRewards
}
const rewards = getRewards()
const balance = swapPair ? Number(pool.balance / 1e18) : pool.balance / 10 ** kashiPair.token0.decimals
let tvl = pool.tvl
if (Number(tvl) === 0)
{
tvl = swapPair
? (balance / Number(swapPair.totalSupply)) * Number(swapPair.reserveUSD)
: balance * kashiPair.token0.derivedETH * bchPriceUSD
}
const roiPerBlock =
rewards.reduce((previousValue, currentValue) => {
return previousValue + currentValue.rewardPerBlock * currentValue.rewardPrice
}, 0) / tvl
const roiPerHour = roiPerBlock * blocksPerHour
const roiPerDay = roiPerHour * 24
const roiPerMonth = roiPerDay * 30
const roiPerYear = roiPerMonth * 12
const position = positions.find((position) => position.id === pool.id && position.chef === pool.chef)
return {
...pool,
...position,
pair: {
...pair,
decimals: pair.type === PairType.KASHI ? Number(pair.asset.tokenInfo.decimals) : 18,
type,
},
balance,
roiPerBlock,
roiPerHour,
roiPerDay,
roiPerMonth,
roiPerYear,
rewards,
tvl,
}
}

const FILTER = {
all: (farm) => Number(farm.allocPoint) !== 0,
portfolio: (farm) => Number(farm.pending) !== 0,
past: (farm) => Number(farm.allocPoint) === 0,
// sushi: (farm) => farm.pair.type === PairType.SWAP && farm.allocPoint !== '0',
// kashi: (farm) => farm.pair.type === PairType.KASHI && farm.allocPoint !== '0',
// '2x': (farm) => (farm.chef === Chef.MASTERCHEF_V2) && farm.allocPoint !== '0',
}

return farms
.filter((farm) => {
return (
(swapPairs && swapPairs.find((pair) => pair.id === farm.pair)) ||
(kashiPairs && kashiPairs.find((pair) => pair.id === farm.pair))
)
})
.map(map)
.filter((farm) => {
return filter in FILTER ? FILTER[filter](farm) : true
})
}

export function getFarmsFromGraph(chainId, filter) {
let farms = []
let [mistPriceUSD, bchPriceUSD] = [0., 0.]
let pairAddresses;
let swapPairs;
let kashiPairs = [] // unused
let averageBlockTime = 0.;
let masterChefV1TotalAllocPoint = 0;
let masterChefV1SushiPerBlock = 0;
const positions = usePositions(chainId);

pairAddresses = useFarmPairAddresses()
swapPairs = useSushiPairs({ subset: pairAddresses, shouldFetch: !!pairAddresses })

farms = useFarms()
farms.forEach(val => {
val.id = Number(val.id)
val.chef = Number(val.chef)

val.pendingSushi = usePendingSushi(val)
val.pending = Number.parseFloat(val.pendingSushi?.toFixed())
})
farms = farms.sort((a, b) => b.allocPoint - a.allocPoint);

[mistPriceUSD, bchPriceUSD] = [
useSushiPrice(),
useEthPrice(),
]

averageBlockTime = useAverageBlockTime()
masterChefV1SushiPerBlock = useMasterChefV1SushiPerBlock()
masterChefV1TotalAllocPoint = useMasterChefV1TotalAllocPoint()

return transformFarms(farms, { chainId, filter, mistPriceUSD, bchPriceUSD, averageBlockTime, swapPairs, kashiPairs, masterChefV1SushiPerBlock, masterChefV1TotalAllocPoint, positions })
}

export function getFarmsFromRpc(chainId, filter) {
let farms
let [mistPriceUSD, bchPriceUSD] = [0., 0.]
let pairAddresses;
let swapPairs
let kashiPairs = [] // unused
let averageBlockTime = 0.;
let masterChefV1TotalAllocPoint = 0;
let masterChefV1SushiPerBlock = 0;
const positions = usePositions(chainId);

[farms, swapPairs] = useHardcodedFarms(chainId)

const flexUSDMistPool = farms.find((v) => v.pair === '0x437E444365aD9ed788e8f255c908bceAd5AEA645').pool;
const bchFlexUSDPool = farms.find((v) => v.pair === '0x24f011f12Ea45AfaDb1D4245bA15dCAB38B43D13').pool;
if (bchFlexUSDPool.reserves) {
bchPriceUSD = Number.parseFloat(bchFlexUSDPool.reserves[1].toFixed()) / Number.parseFloat(bchFlexUSDPool.reserves[0].toFixed());
}
if (flexUSDMistPool.reserves) {
mistPriceUSD = 1. / ( Number.parseFloat(flexUSDMistPool.reserves[0].toFixed()) / Number.parseFloat(flexUSDMistPool.reserves[1].toFixed()))
}

averageBlockTime = 5.5
masterChefV1SushiPerBlock = 10

return transformFarms(farms, { chainId, filter, mistPriceUSD, bchPriceUSD, averageBlockTime, swapPairs, kashiPairs, masterChefV1SushiPerBlock, masterChefV1TotalAllocPoint, positions })
}
61 changes: 61 additions & 0 deletions src/pages/farm/graphFarms.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { useActiveWeb3React, useFuse } from '../../hooks'

import Container from '../../components/Container'
import FarmList from '../../features/onsen/FarmList'
import Head from 'next/head'
import Menu from '../../features/onsen/FarmMenu'
import React, { useEffect } from 'react'
import Search from '../../components/Search'
import { classNames } from '../../functions'
import { usePositions, usePendingSushi } from '../../features/onsen/hooks'
import { useRouter } from 'next/router'
import { getFarmFilter, useUpdateFarmFilter } from '../../state/user/hooks'
import { useMasterChefV1HealthCheck } from '../../services/graph'
import { getFarmsFromGraph } from './getFarms'

export default function Farm({filter}): JSX.Element {
const { chainId } = useActiveWeb3React()

const data = getFarmsFromGraph(chainId, filter)
const positions = usePositions(chainId)

const options = {
keys: ['pair.id', 'pair.token0.symbol', 'pair.token1.symbol'],
threshold: 0.4,
}

const { result, term, search } = useFuse({
data,
options,
})

return (
<Container id="farm-page" className="lg:grid lg:grid-cols-4 h-full py-4 mx-auto md:py-8 lg:py-12 gap-9" maxWidth="7xl">
<Head>
<title>Farm | Mist</title>
<meta key="description" name="description" content="Farm MIST" />
</Head>
<div className={classNames('px-3 md:px-0 lg:block md:col-span-1')} style={{ maxHeight: '40rem' }}>
<Menu positionsLength={positions.length} />
</div>
<div className={classNames('space-y-6 col-span-4 lg:col-span-3')}>
<Search
search={search}
term={term}
className={classNames('px-3 md:px-0 ')}
inputProps={{
className:
'relative w-full bg-transparent border border-transparent focus:border-gradient-r-blue-pink-dark-900 rounded placeholder-secondary focus:placeholder-primary font-bold text-base px-6 py-3.5',
}}
/>

<div className="hidden md:block flex items-center text-lg font-bold text-high-emphesis whitespace-nowrap">
Farms{' '}
<div className="w-full h-0 ml-4 font-bold bg-transparent border border-b-0 border-transparent rounded text-high-emphesis md:border-gradient-r-blue-pink-dark-800 opacity-20"></div>
</div>

<FarmList farms={result} term={term} />
</div>
</Container>
)
}
Loading