@@ -66,38 +18,6 @@ export const POINTS_COLLATERAL: Point[] = [
export const POINTS_BORROW: Point[] = [
{
id: '1',
- name: 'Activity Points',
- description: (
-
- ),
- icon: SYMBOL_TO_ICON.FUEL,
- displayMultiplier: '4x',
- },
- {
- id: '2',
name: 'SwayPoints',
description: (
@@ -113,38 +33,6 @@ export const POINTS_BORROW: Point[] = [
export const POINTS_LEND: Point[] = [
{
id: '1',
- name: 'Activity Points',
- description: (
-
- By Lending USDC on Swaylend you get{' '}
-
4x Fuel Points Multiplier:
-
-{' '}
-
- 2x for Lending Activity
-
-
-{' '}
-
- 2x for using USDC as Incentivised Asset
-
-
-
- For more details, check out our{' '}
-
- blog post
-
- .
-
- ),
- icon: SYMBOL_TO_ICON.FUEL,
- displayMultiplier: '4x',
- },
- {
- id: '2',
name: 'SwayPoints',
description: (
@@ -156,3 +44,13 @@ export const POINTS_LEND: Point[] = [
displayMultiplier: '3x',
},
];
+
+export const POINTS_LM: Point[] = [
+ {
+ id: '1',
+ name: 'FUEL Token Rewards',
+ description: <>>,
+ icon: SYMBOL_TO_ICON.FUEL,
+ displayMultiplier: undefined,
+ },
+];
diff --git a/apps/frontend/src/components/Providers/FuelProviderWrapper.tsx b/apps/frontend/src/components/Providers/FuelProviderWrapper.tsx
index 3a9df643..453a4a8c 100644
--- a/apps/frontend/src/components/Providers/FuelProviderWrapper.tsx
+++ b/apps/frontend/src/components/Providers/FuelProviderWrapper.tsx
@@ -16,7 +16,7 @@ import {
} from '@fuels/connectors';
import { FuelProvider } from '@fuels/react';
import { CHAIN_IDS, type FuelConnector, Provider } from 'fuels';
-import { type ReactNode } from 'react';
+import type { ReactNode } from 'react';
import { fallback } from 'viem';
import { http, createConfig as createConfigWagmiConfig } from 'wagmi';
import { mainnet, sepolia } from 'wagmi/chains';
diff --git a/apps/frontend/src/configs/envs/mainnet.ts b/apps/frontend/src/configs/envs/mainnet.ts
index 10210c8b..a0ffac00 100644
--- a/apps/frontend/src/configs/envs/mainnet.ts
+++ b/apps/frontend/src/configs/envs/mainnet.ts
@@ -1,5 +1,5 @@
import { defineConfig } from '../defineConfig';
-import type { DeployedMarkets } from '../types';
+import type { DeployedMarkets, Rewards } from '../types';
export function createMainnetConfig() {
return defineConfig({
@@ -28,6 +28,7 @@ export function createMainnetConfig() {
markets: markets,
assets: assets,
useBurnerWallet: false,
+ rewards: rewards,
});
}
@@ -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 = {
'0xf8f8b6283d7fa5b672b530cbb84fcccb4ff8dc40f8176ef4544ddb1f1952ad07': 'ETH',
'0x286c479da40dc953bddc3bb4c453b608bba2e0ac483b077bd475174115395e6b': 'USDC',
diff --git a/apps/frontend/src/configs/envs/testnet.ts b/apps/frontend/src/configs/envs/testnet.ts
index 3bdfc5aa..7f259a7d 100644
--- a/apps/frontend/src/configs/envs/testnet.ts
+++ b/apps/frontend/src/configs/envs/testnet.ts
@@ -1,5 +1,5 @@
import { defineConfig } from '../defineConfig';
-import type { DeployedMarkets } from '../types';
+import type { DeployedMarkets, Rewards } from '../types';
export function createTestnetConfig() {
return defineConfig({
@@ -28,9 +28,15 @@ export function createTestnetConfig() {
baseAssetId:
'0xf8f8b6283d7fa5b672b530cbb84fcccb4ff8dc40f8176ef4544ddb1f1952ad07',
useBurnerWallet: true,
+ rewards: rewards,
});
}
+const rewards: Rewards = {
+ USDC: [],
+ USDT: [],
+};
+
const markets: DeployedMarkets = {
USDC: {
oracleAddress:
diff --git a/apps/frontend/src/configs/types.ts b/apps/frontend/src/configs/types.ts
index b66c56a3..e0847479 100644
--- a/apps/frontend/src/configs/types.ts
+++ b/apps/frontend/src/configs/types.ts
@@ -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({
@@ -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;
export type DeployedMarkets = z.infer;
+export type Rewards = z.infer;
diff --git a/apps/frontend/src/hooks/index.ts b/apps/frontend/src/hooks/index.ts
index 4d813afe..7817b298 100644
--- a/apps/frontend/src/hooks/index.ts
+++ b/apps/frontend/src/hooks/index.ts
@@ -29,3 +29,4 @@ export * from './useTotalReserves';
export * from './useCollateralReserves';
export * from './useMaxWithdrawableCollateral';
export * from './useProvider';
+export * from './useApr';
diff --git a/apps/frontend/src/hooks/useApr.ts b/apps/frontend/src/hooks/useApr.ts
new file mode 100644
index 00000000..d80af7a1
--- /dev/null
+++ b/apps/frontend/src/hooks/useApr.ts
@@ -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,
+ });
+};
diff --git a/apps/frontend/src/hooks/useFuelPoints.ts b/apps/frontend/src/hooks/useFuelPoints.ts
deleted file mode 100644
index e061c8e1..00000000
--- a/apps/frontend/src/hooks/useFuelPoints.ts
+++ /dev/null
@@ -1,62 +0,0 @@
-import { appConfig } from '@/configs';
-import { getFormattedNumber } from '@/utils';
-import { useAccount } from '@fuels/react';
-import { useQuery } from '@tanstack/react-query';
-import BigNumber from 'bignumber.js';
-
-type OblApiResponse = {
- user_address: string;
- total_points: number;
- rank: number;
-};
-
-function randomDouble(min: number, max: number) {
- return Math.random() * (max - min) + min;
-}
-
-export const useFuelPoints = () => {
- const { account } = useAccount();
-
- return useQuery({
- queryKey: ['fuelPoints', account],
- queryFn: async () => {
- if (!account) return getFormattedNumber(BigNumber(0));
-
- // On testnet just return some random data that we can use for testing
- if (appConfig.env === 'testnet') {
- return getFormattedNumber(BigNumber(randomDouble(0, 1000000)));
- }
-
- try {
- const response = await fetch(
- `${appConfig.client.fuelOblApi}/fuel/epoch1_leaderboard?user_address=${account.toLowerCase()}`
- );
-
- if (!response.ok) {
- return getFormattedNumber(BigNumber(0));
- }
-
- const data = (await response.json()) as
- | OblApiResponse[]
- | null
- | undefined;
-
- if (!data || data.length === 0) {
- return getFormattedNumber(BigNumber(0));
- }
-
- const points = data[0].total_points;
-
- if (!points) {
- return getFormattedNumber(BigNumber(0));
- }
-
- return getFormattedNumber(BigNumber(points));
- } catch (e) {
- console.log(e);
- return getFormattedNumber(BigNumber(0));
- }
- },
- enabled: !!account,
- });
-};
diff --git a/apps/frontend/src/hooks/useRewards.ts b/apps/frontend/src/hooks/useRewards.ts
new file mode 100644
index 00000000..4f3e26a9
--- /dev/null
+++ b/apps/frontend/src/hooks/useRewards.ts
@@ -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,
+ });
+};
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index c1868254..789a5499 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -102,6 +102,9 @@ importers:
clsx:
specifier: ^2.1.1
version: 2.1.1
+ dayjs:
+ specifier: ^1.11.13
+ version: 1.11.13
encoding:
specifier: ^0.1.13
version: 0.1.13