Skip to content

Commit

Permalink
feat: initial work on lm rewards display
Browse files Browse the repository at this point in the history
  • Loading branch information
martines3000 committed Jan 10, 2025
1 parent c28ba9c commit 93c7ad9
Show file tree
Hide file tree
Showing 9 changed files with 230 additions and 30 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ build
dist
.env
.DS_Store
scripts
1 change: 1 addition & 0 deletions apps/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"bignumber.js": "^9.1.1",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"dayjs": "^1.11.13",
"encoding": "^0.1.13",
"fuels": "0.96.1",
"lucide-react": "^0.460.0",
Expand Down
39 changes: 11 additions & 28 deletions apps/frontend/src/components/DashboardView/Stats/InfoBowl.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,11 @@ import {
TooltipProvider,
TooltipTrigger,
} from '@/components/ui/tooltip';
import {
useBorrowRate,
useSupplyRate,
useUserCollateralAssets,
useUserSupplyBorrow,
} from '@/hooks';
import { useUserCollateralAssets, useUserSupplyBorrow } from '@/hooks';
import { useApr } from '@/hooks/useApr';
import { useUserCollateralUtilization } from '@/hooks/useUserCollateralUtilization';
import { cn } from '@/lib/utils';
import { selectMarketMode, useMarketStore } from '@/stores';
import { getBorrowApr, getSupplyApr } from '@/utils';
import { useIsConnected } from '@fuels/react';
import { useMemo } from 'react';
import Wave from 'react-wavify';
Expand All @@ -38,8 +33,7 @@ const WAVE_COLORS = {
export const InfoBowl = () => {
const marketMode = useMarketStore(selectMarketMode);
const { isConnected } = useIsConnected();
const { data: borrowRate, isPending: isPendingBorrowRate } = useBorrowRate();
const { data: supplyRate, isPending: isPendingSupplyRate } = useSupplyRate();

const { data: userSupplyBorrow, isPending: isPendingUserSupplyBorrow } =
useUserSupplyBorrow();
const { data: collateralUtilization } = useUserCollateralUtilization();
Expand Down Expand Up @@ -70,23 +64,12 @@ export const InfoBowl = () => {
return WAVE_COLORS.danger;
}, [collateralUtilization, collateralBalances]);

const borrowApr = useMemo(() => getBorrowApr(borrowRate), [borrowRate]);

const supplyApr = useMemo(() => getSupplyApr(supplyRate), [supplyRate]);
const { data: aprData, isPending: isAprPending } = useApr();

const isLoading = useMemo(() => {
if (!isConnected) return isPendingBorrowRate || isPendingSupplyRate;
return [
isPendingBorrowRate,
isPendingSupplyRate,
isPendingUserSupplyBorrow,
].some((res) => res);
}, [
isConnected,
isPendingBorrowRate,
isPendingSupplyRate,
isPendingUserSupplyBorrow,
]);
if (!isConnected) return isAprPending;
return [isPendingUserSupplyBorrow, isAprPending].some((res) => res);
}, [isConnected, isAprPending, isPendingUserSupplyBorrow]);

return (
<TooltipProvider delayDuration={100}>
Expand Down Expand Up @@ -165,17 +148,17 @@ export const InfoBowl = () => {
)}
{bowlMode === 1 && (
<div className="text-sm sm:text-lg text-primary-foreground font-bold">
Borrow APY
Net Borrow APY
<div className="sm:text-xl text-lg font-semibold">
{borrowApr}
{aprData?.netBorrowApr.times(100).toFixed(2)}%
</div>
</div>
)}
{bowlMode === 0 && (
<div className="text-sm sm:text-lg text-white font-bold">
Supply APY
Net Supply APY
<div className="sm:text-xl text-lg font-semibold">
{supplyApr}
{aprData?.netSupplyApr.times(100).toFixed(2)}%
</div>
</div>
)}
Expand Down
18 changes: 17 additions & 1 deletion apps/frontend/src/configs/envs/mainnet.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { defineConfig } from '../defineConfig';
import type { DeployedMarkets } from '../types';
import type { DeployedMarkets, Rewards } from '../types';

export function createMainnetConfig() {
return defineConfig({
Expand Down Expand Up @@ -28,6 +28,7 @@ export function createMainnetConfig() {
markets: markets,
assets: assets,
useBurnerWallet: false,
rewards: rewards,
});
}

Expand All @@ -42,6 +43,21 @@ const markets: DeployedMarkets = {
},
};

const rewards: Rewards = {
USDC: [
{
poolSize: 2000000,
assetId:
'0x1d5d97005e41cae2187a895fd8eab0506111e0e2f3331cd3912c15c24e3c1d82',
supplyRewardPercentage: 0.5,
borrowRewardPercentage: 0.5,
startDate: '2025-01-10T00:00:00Z',
endDate: '2025-01-17T00:00:00Z',
durationInDays: 7,
},
],
};

const assets: Record<string, string> = {
'0xf8f8b6283d7fa5b672b530cbb84fcccb4ff8dc40f8176ef4544ddb1f1952ad07': 'ETH',
'0x286c479da40dc953bddc3bb4c453b608bba2e0ac483b077bd475174115395e6b': 'USDC',
Expand Down
8 changes: 7 additions & 1 deletion apps/frontend/src/configs/envs/testnet.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { defineConfig } from '../defineConfig';
import type { DeployedMarkets } from '../types';
import type { DeployedMarkets, Rewards } from '../types';

export function createTestnetConfig() {
return defineConfig({
Expand Down Expand Up @@ -28,9 +28,15 @@ export function createTestnetConfig() {
baseAssetId:
'0xf8f8b6283d7fa5b672b530cbb84fcccb4ff8dc40f8176ef4544ddb1f1952ad07',
useBurnerWallet: true,
rewards: rewards,
});
}

const rewards: Rewards = {
USDC: [],
USDT: [],
};

const markets: DeployedMarkets = {
USDC: {
oracleAddress:
Expand Down
17 changes: 17 additions & 0 deletions apps/frontend/src/configs/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,21 @@ export const DeployedMarketsSchema = z.record(
})
);

export const RewardsSchema = z.record(
z.string(),
z.array(
z.object({
poolSize: z.number(),
assetId: z.string(),
supplyRewardPercentage: z.number(),
borrowRewardPercentage: z.number(),
startDate: z.string(),
endDate: z.string(),
durationInDays: z.number(),
})
)
);

export const AppConfigSchema = z.object({
env: z.enum(['testnet', 'mainnet']),
client: z.object({
Expand All @@ -33,7 +48,9 @@ export const AppConfigSchema = z.object({
assets: z.record(z.string(), z.string()),
baseAssetId: z.string(),
useBurnerWallet: z.boolean(),
rewards: RewardsSchema,
});

export type AppConfig = z.infer<typeof AppConfigSchema>;
export type DeployedMarkets = z.infer<typeof DeployedMarketsSchema>;
export type Rewards = z.infer<typeof RewardsSchema>;
56 changes: 56 additions & 0 deletions apps/frontend/src/hooks/useApr.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { selectMarket, useMarketStore } from '@/stores';
import { keepPreviousData, useQuery } from '@tanstack/react-query';
import BigNumber from 'bignumber.js';
import { useBorrowRate } from './useBorrowRate';
import { useRewards } from './useRewards';
import { useSupplyRate } from './useSupplyRate';

const COEFFICIENT = BigNumber(365).times(24).times(60).times(60);

export const useApr = (marketParam?: string) => {
const storeMarket = useMarketStore(selectMarket);
const market = marketParam ?? storeMarket;
const { data: rewardsData } = useRewards(market);
const { data: supplyRate } = useSupplyRate(market);
const { data: borrowRate } = useBorrowRate(market);

return useQuery({
queryKey: ['apr', supplyRate, borrowRate, rewardsData],
queryFn: async () => {
if (!supplyRate || !borrowRate || !rewardsData) {
return {
supplyBaseApr: BigNumber(0),
borrowBaseApr: BigNumber(0),
supplyRewardApr: BigNumber(0),
borrowRewardApr: BigNumber(0),
netSupplyApr: BigNumber(0),
netBorrowApr: BigNumber(0),
};
}

const { borrowRewardApr, supplyRewardApr } = rewardsData;

const supplyBaseApr = supplyRate
.times(COEFFICIENT)
.dividedBy(BigNumber(10).pow(18));
const borrowBaseApr = borrowRate
.times(COEFFICIENT)
.dividedBy(BigNumber(10).pow(18));

const netSupplyApr = supplyBaseApr.plus(supplyRewardApr);
const netBorrowApr = borrowBaseApr.minus(borrowRewardApr);

return {
supplyBaseApr,
borrowBaseApr,
supplyRewardApr: supplyRewardApr,
borrowRewardApr: borrowRewardApr,
netSupplyApr,
netBorrowApr,
};
},
enabled: !!supplyRate && !!borrowRate && !!rewardsData,
refetchOnWindowFocus: false,
placeholderData: keepPreviousData,
});
};
117 changes: 117 additions & 0 deletions apps/frontend/src/hooks/useRewards.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { appConfig } from '@/configs';
import { selectMarket, useMarketStore } from '@/stores';
import { useQuery } from '@tanstack/react-query';
import BigNumber from 'bignumber.js';
import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc';
import { useMemo } from 'react';
import { useMarketBasicsWithInterest } from './useMarketBasicsWithInterest';
import { useMarketConfiguration } from './useMarketConfiguration';
import { usePrice } from './usePrice';

dayjs.extend(utc);

const calculateRewardsAprForPool = (
tokenAmount: BigNumber,
tokenPrice: BigNumber,
durationInDays: number,
currentTvl: BigNumber
) => {
return tokenAmount
.times(tokenPrice)
.times(365)
.dividedBy(durationInDays)
.dividedBy(currentTvl);
};

export const useRewards = (marketParam?: string) => {
const storeMarket = useMarketStore(selectMarket);
const market = marketParam ?? storeMarket;

const { data: marketBasics } = useMarketBasicsWithInterest(market);
const { data: priceData } = usePrice(market);
const { data: marketConfiguration } = useMarketConfiguration(market);

return useQuery({
queryKey: [
'rewards',
market,
marketBasics,
priceData?.prices,
marketConfiguration,
],
queryFn: async () => {
if (!marketBasics || !priceData || !marketConfiguration) {
return {
supplyRewardApr: BigNumber(0),
borrowRewardApr: BigNumber(0),
};
}

const rewardsConfig = appConfig.rewards[market];

const today = dayjs().utc().startOf('day');
const activeRewards = rewardsConfig.filter((reward) => {
return (
(today.isAfter(dayjs(reward.startDate).utc().startOf('day')) ||
today.isSame(dayjs(reward.startDate).utc().startOf('day'))) &&
today.isBefore(dayjs(reward.endDate).utc().startOf('day'))
);
});

const { total_borrow_base, total_supply_base } = marketBasics;

const totalBorrowValue = BigNumber(total_borrow_base.toString())
.times(priceData.prices[marketConfiguration.baseToken.bits])
.dividedBy(BigNumber(10).pow(marketConfiguration.baseTokenDecimals));

const totalSupplyValue = BigNumber(total_supply_base.toString())
.times(priceData.prices[marketConfiguration.baseToken.bits])
.dividedBy(BigNumber(10).pow(marketConfiguration.baseTokenDecimals));

return activeRewards
.map((reward) => {
const tokenPrice = priceData.prices[reward.assetId];
const durationInDays = reward.durationInDays;
const supplyRewardPool = BigNumber(reward.poolSize).times(
reward.supplyRewardPercentage
);
const borrowRewardPool = BigNumber(reward.poolSize).times(
reward.borrowRewardPercentage
);

const supplyRewardApr = calculateRewardsAprForPool(
supplyRewardPool,
tokenPrice,
durationInDays,
totalSupplyValue
);
const borrowRewardApr = calculateRewardsAprForPool(
borrowRewardPool,
tokenPrice,
durationInDays,
totalBorrowValue
);

return {
supplyRewardApr,
borrowRewardApr,
};
})
.reduce(
(acc, curr) => {
return {
supplyRewardApr: acc.supplyRewardApr.plus(curr.supplyRewardApr),
borrowRewardApr: acc.borrowRewardApr.plus(curr.borrowRewardApr),
};
},
{
supplyRewardApr: BigNumber(0),
borrowRewardApr: BigNumber(0),
}
);
},
enabled: !!marketBasics && !!priceData && !!marketConfiguration,
refetchOnWindowFocus: false,
});
};
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 93c7ad9

Please sign in to comment.