From 1f721d8cb6fe88a31619ff8d9ce099b75f9edb62 Mon Sep 17 00:00:00 2001 From: Paul McInnis Date: Tue, 24 Dec 2024 10:59:15 -0500 Subject: [PATCH] feature(bex): pull APY for pools and display as a sortable column --- apps/hub/src/app/pools/PoolsTable.tsx | 8 +- apps/hub/src/app/pools/usePoolTable.tsx | 158 +++++++++++++----- ...s => useRewardVaultAddressesFromTokens.ts} | 6 +- .../berajs/src/hooks/modules/pol/index.ts | 2 +- .../berajs/src/hooks/useIsWhitelistedVault.ts | 1 + 5 files changed, 125 insertions(+), 50 deletions(-) rename packages/berajs/src/hooks/modules/pol/hooks/{useRewardVaultsFromTokens.ts => useRewardVaultAddressesFromTokens.ts} (87%) diff --git a/apps/hub/src/app/pools/PoolsTable.tsx b/apps/hub/src/app/pools/PoolsTable.tsx index 19c13a165..850137830 100755 --- a/apps/hub/src/app/pools/PoolsTable.tsx +++ b/apps/hub/src/app/pools/PoolsTable.tsx @@ -164,7 +164,7 @@ export const PoolSearch = ({ - {data === undefined && isLoading ? ( + {data === undefined || isLoading ? (
{ - router.prefetch(getPoolUrl(row.original)); + router.prefetch(getPoolUrl(row.original.pool)); }} - onRowClick={(row) => router.push(getPoolUrl(row.original))} + onRowClick={(row) => router.push(getPoolUrl(row.original.pool))} wrapperClassName="bg-transparent border-none" showToolbar={true} /> @@ -213,7 +213,7 @@ export const PoolSearch = ({ dynamicFlex variant="ghost" mutedBackgroundOnHead={false} - onRowClick={(row) => router.push(getPoolUrl(row.original))} + onRowClick={(row) => router.push(getPoolUrl(row.original.pool))} wrapperClassName="bg-transparent border-none" showToolbar={true} /> diff --git a/apps/hub/src/app/pools/usePoolTable.tsx b/apps/hub/src/app/pools/usePoolTable.tsx index 84741de6b..9f766edc7 100755 --- a/apps/hub/src/app/pools/usePoolTable.tsx +++ b/apps/hub/src/app/pools/usePoolTable.tsx @@ -1,9 +1,10 @@ -import { useMemo, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { ADDRESS_ZERO, useBgtInflation, useIsWhitelistedVault, - useRewardVaultsFromTokens, + useRewardVaultAddressesFromTokens, + useRewardVaults, } from "@bera/berajs"; import { MinimalPoolInListFragment } from "@bera/graphql/dex/api"; import { @@ -12,6 +13,7 @@ import { useAsyncTable, } from "@bera/shared-ui"; +import { POLLING } from "~/utils/constants"; import { PoolSummary } from "../../components/pools-table-columns"; import { usePools } from "./usePools"; @@ -48,28 +50,56 @@ export const usePoolTable = ({ [pools], ); - // Extract vault addresses from the token addresse and fetch the whitelist statuses for all of those vaults - const { data: rewardVaults } = useRewardVaultsFromTokens({ + // Pull vault addresses from the token addresses via multicall on the reward vault factory + const { data: rewardVaultsAddressMap } = useRewardVaultAddressesFromTokens({ tokenAddresses, }); const vaultAddresses = useMemo( - () => Object.values(rewardVaults ?? {}).filter((v) => v !== ADDRESS_ZERO), - [rewardVaults], + () => + Object.values(rewardVaultsAddressMap ?? {}).filter( + (v) => v !== ADDRESS_ZERO, + ), + [rewardVaultsAddressMap], ); - const { data: whitelistedVaults } = useIsWhitelistedVault(vaultAddresses); + // Pull isWhitelisted via multicall on the vault addresses we got + // NOTE: we prefer to pull isWhitelisted on-chain for availability reasons. + const { data: whitelistedVaultsAddressMap } = + useIsWhitelistedVault(vaultAddresses); - // Map vault whitelist status - const whitelistStatusMap = useMemo(() => { - return new Map( - whitelistedVaults?.map((vault) => [vault.address, vault.isWhitelisted]) || - [], - ); - }, [whitelistedVaults]); + // Pull full Dynamic data etc from the vault via bex subgraph + const { data: rewardVaultMetadata } = useRewardVaults( + // TODO (BFE-444): this should use pagination / an index since this will become a performance issue when we have many pools. + // { + // where: { + // vaultAddressIn: vaultAddresses, + // }, + // }, + {}, + { opts: { refreshInterval: POLLING.SLOW } }, + ); + const gaugeDictionary = rewardVaultMetadata?.gaugeDictionary ?? {}; + + // Combine pools and vaults into unified data structure + const unifiedData = useMemo(() => { + if (!pools) return []; + return pools.map((pool) => { + const rewardVaultAddress = + rewardVaultsAddressMap?.[pool.address.toLowerCase()]; + const rewardVault = rewardVaultAddress + ? gaugeDictionary[rewardVaultAddress.toLowerCase() as `0x${string}`] + : null; + + return { + pool, + vault: rewardVault, + }; + }); + }, [pools, rewardVaultsAddressMap, gaugeDictionary]); - const table = useAsyncTable({ - data: pools ?? [], + const table = useAsyncTable({ + data: unifiedData ?? [], fetchData: async () => {}, additionalTableProps: { initialState: { sorting, pagination: { pageSize: 10, pageIndex: 0 } }, @@ -89,20 +119,29 @@ export const usePoolTable = ({ /> ), cell: ({ row }) => { - const rewardVault = - rewardVaults?.[row.original.address.toLowerCase()]; - const isWhitelistedVault = rewardVault - ? whitelistedVaults?.some( - (vault) => - vault.address.toLowerCase() === rewardVault.toLowerCase() && - vault.isWhitelisted, - ) ?? false - : false; - + const { pool, vault } = row.original; + let isWhitelistedVault = false; + try { + const rewardVault = + rewardVaultsAddressMap?.[pool.address.toLowerCase()]; + isWhitelistedVault = rewardVault + ? whitelistedVaultsAddressMap?.some( + (vault) => + vault.address.toLowerCase() === rewardVault.toLowerCase() && + vault.isWhitelisted, + ) ?? false + : false; + } catch (e) { + console.error( + "Unable to fetch isWhitelistedVault from contract! Falling back to bex api...", + e, + ); + isWhitelistedVault = vault?.isVaultWhitelisted ?? false; // NOTE: this is the gql way as a fallback + } return (
@@ -125,7 +164,7 @@ export const usePoolTable = ({
@@ -136,8 +175,8 @@ export const usePoolTable = ({ }, sortingFn: (rowA, rowB) => { return ( - Number(rowA.original.dynamicData.totalLiquidity ?? "0") - - Number(rowB.original.dynamicData.totalLiquidity ?? "0") + Number(rowA.original.pool.dynamicData?.totalLiquidity ?? "0") - + Number(rowB.original.pool.dynamicData?.totalLiquidity ?? "0") ); }, }, @@ -154,7 +193,7 @@ export const usePoolTable = ({
@@ -166,8 +205,8 @@ export const usePoolTable = ({ }, sortingFn: (rowA, rowB) => { return ( - Number(rowA.original.dynamicData.fees24h ?? "0") - - Number(rowB.original.dynamicData.fees24h ?? "0") + Number(rowA.original.pool.dynamicData?.fees24h ?? "0") - + Number(rowB.original.pool.dynamicData?.fees24h ?? "0") ); }, }, @@ -184,7 +223,7 @@ export const usePoolTable = ({
@@ -196,8 +235,8 @@ export const usePoolTable = ({ }, sortingFn: (rowA, rowB) => { return ( - Number(rowA.original.dynamicData.volume24h ?? "0") - - Number(rowB.original.dynamicData.volume24h ?? "0") + Number(rowA.original.pool.dynamicData?.volume24h ?? "0") - + Number(rowB.original.pool.dynamicData?.volume24h ?? "0") ); }, }, @@ -214,15 +253,16 @@ export const usePoolTable = ({ return (
{ return ( - Number(rowA.original.dynamicData.aprItems?.at(0)?.apr ?? "0") - - Number(rowB.original.dynamicData.aprItems?.at(0)?.apr ?? "0") + Number( + rowA.original.pool.dynamicData?.aprItems?.at(0)?.apr ?? "0", + ) - + Number(rowB.original.pool.dynamicData?.aprItems?.at(0)?.apr ?? "0") ); }, }, + { + accessorKey: "vault.dynamicData.apy", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const { vault } = row.original; + const apy = vault?.dynamicData?.apy; + + return ( +
+
+ {apy !== undefined && apy !== null ? ( + + ) : ( + "—" // Placeholder for missing APY data + )} +
+
+ ); + }, + enableSorting: true, + }, ], }); return { - data: pools, + data: unifiedData, table, search, setSearch, diff --git a/packages/berajs/src/hooks/modules/pol/hooks/useRewardVaultsFromTokens.ts b/packages/berajs/src/hooks/modules/pol/hooks/useRewardVaultAddressesFromTokens.ts similarity index 87% rename from packages/berajs/src/hooks/modules/pol/hooks/useRewardVaultsFromTokens.ts rename to packages/berajs/src/hooks/modules/pol/hooks/useRewardVaultAddressesFromTokens.ts index 5d00fa0b9..348c16017 100644 --- a/packages/berajs/src/hooks/modules/pol/hooks/useRewardVaultsFromTokens.ts +++ b/packages/berajs/src/hooks/modules/pol/hooks/useRewardVaultAddressesFromTokens.ts @@ -6,7 +6,7 @@ import { usePublicClient } from "wagmi"; import { rewardVaultFactoryAbi } from "~/abi"; import { ADDRESS_ZERO } from "~/config"; -export const useRewardVaultsFromTokens = ({ +export const useRewardVaultAddressesFromTokens = ({ tokenAddresses, }: { tokenAddresses: Address[]; @@ -15,12 +15,12 @@ export const useRewardVaultsFromTokens = ({ return useSWRImmutable( client && tokenAddresses.length > 0 - ? ["useRewardVaultsFromTokens", tokenAddresses] + ? ["useRewardVaultAddressesFromTokens", tokenAddresses] : null, async () => { if (!tokenAddresses || tokenAddresses.length === 0) { throw new Error( - "useRewardVaultsFromTokens needs valid token addresses", + "useRewardVaultAddressesFromTokens needs valid token addresses", ); } diff --git a/packages/berajs/src/hooks/modules/pol/index.ts b/packages/berajs/src/hooks/modules/pol/index.ts index ba71b3a7a..5ac5cdd37 100644 --- a/packages/berajs/src/hooks/modules/pol/index.ts +++ b/packages/berajs/src/hooks/modules/pol/index.ts @@ -10,7 +10,7 @@ export { usePollValidatorTokenRewards } from "./hooks/poll-validator-token-rewar export { useUserActiveValidators } from "./hooks/useUserActiveValidators"; export * from "./hooks/useRewardVaultBalanceFromStakingToken"; export { useRewardVaultFromToken } from "./hooks/useRewardVaultFromToken"; -export { useRewardVaultsFromTokens } from "./hooks/useRewardVaultsFromTokens"; +export { useRewardVaultAddressesFromTokens } from "./hooks/useRewardVaultAddressesFromTokens"; export { usePollRewardVault } from "./hooks/poll-reward-vault"; export { useSubgraphUserValidators } from "./hooks/useSubgraphUserValidators"; diff --git a/packages/berajs/src/hooks/useIsWhitelistedVault.ts b/packages/berajs/src/hooks/useIsWhitelistedVault.ts index f88d53f98..7b5c87236 100644 --- a/packages/berajs/src/hooks/useIsWhitelistedVault.ts +++ b/packages/berajs/src/hooks/useIsWhitelistedVault.ts @@ -39,6 +39,7 @@ export const useIsWhitelistedVault = (vaultAddresses: Address[]) => { }, ); + // NOTE: whitelist status can only change via governance proposal, so generally no need to refresh return { ...swrResponse, refresh: () => swrResponse.mutate(),