From 06f5d0479d6d1f81c5420b2066d907e1f013e6ca Mon Sep 17 00:00:00 2001 From: Danny Delott Date: Thu, 13 Feb 2025 15:48:10 -0800 Subject: [PATCH 1/5] Stub out /leaderboard route --- .../src/ui/app/Navbar/DevtoolsMenu.tsx | 3 + .../src/ui/app/Navbar/Navbar.tsx | 7 ++ .../src/ui/markets/Leaderboard.tsx | 39 ++++++++++ .../ui/rewards/hooks/useClaimableRewards.ts | 78 +------------------ .../src/ui/rewards/routes.ts | 1 + .../src/ui/routes/leaderboard.tsx | 11 +++ 6 files changed, 63 insertions(+), 76 deletions(-) create mode 100644 apps/hyperdrive-trading/src/ui/markets/Leaderboard.tsx create mode 100644 apps/hyperdrive-trading/src/ui/rewards/routes.ts create mode 100644 apps/hyperdrive-trading/src/ui/routes/leaderboard.tsx diff --git a/apps/hyperdrive-trading/src/ui/app/Navbar/DevtoolsMenu.tsx b/apps/hyperdrive-trading/src/ui/app/Navbar/DevtoolsMenu.tsx index 1c01ab7ff..eda9f41d4 100644 --- a/apps/hyperdrive-trading/src/ui/app/Navbar/DevtoolsMenu.tsx +++ b/apps/hyperdrive-trading/src/ui/app/Navbar/DevtoolsMenu.tsx @@ -17,6 +17,9 @@ export function DevtoolsMenu(): ReactElement { Menu Item Name here */} Zaps + + Leaderboard + Portfolio Rewards diff --git a/apps/hyperdrive-trading/src/ui/app/Navbar/Navbar.tsx b/apps/hyperdrive-trading/src/ui/app/Navbar/Navbar.tsx index 80673905d..7dd2a5fc2 100644 --- a/apps/hyperdrive-trading/src/ui/app/Navbar/Navbar.tsx +++ b/apps/hyperdrive-trading/src/ui/app/Navbar/Navbar.tsx @@ -12,11 +12,13 @@ import { useAnalyticsUrl } from "src/ui/analytics/useMarketAnalyticsUrl"; import { DevtoolsMenu } from "src/ui/app/Navbar/DevtoolsMenu"; import { HyperdriveLogo } from "src/ui/app/Navbar/HyperdriveLogo"; import VersionPicker from "src/ui/base/components/VersionPicker"; +import { useFeatureFlag } from "src/ui/base/featureFlags/featureFlags"; import { useIsTailwindSmallScreen } from "src/ui/base/mediaBreakpoints"; import { LANDING_ROUTE } from "src/ui/landing/routes"; import { POINTS_MARKETS_ROUTE } from "src/ui/markets/routes"; import { MINT_ROUTE } from "src/ui/mint/routes"; import { PORTFOLIO_ROUTE } from "src/ui/portfolio/routes"; +import { POINTS_LEADERBOARD_ROUTE } from "src/ui/rewards/routes"; import { sepolia } from "viem/chains"; import { useChainId } from "wagmi"; @@ -25,6 +27,8 @@ export function Navbar(): ReactElement { const chainId = useChainId(); const isTestnet = isTestnetChain(chainId); const analyticsUrl = useAnalyticsUrl(); + const { isFlagEnabled: isMilesLeaderboardEnabled } = + useFeatureFlag("miles-leaderboard"); return (
@@ -39,6 +43,9 @@ export function Navbar(): ReactElement { + {isMilesLeaderboardEnabled ? ( + + ) : null} {isTestnet ? ( ) : null} diff --git a/apps/hyperdrive-trading/src/ui/markets/Leaderboard.tsx b/apps/hyperdrive-trading/src/ui/markets/Leaderboard.tsx new file mode 100644 index 000000000..989c18f61 --- /dev/null +++ b/apps/hyperdrive-trading/src/ui/markets/Leaderboard.tsx @@ -0,0 +1,39 @@ +import { ReactElement } from "react"; +import { Fade } from "react-awesome-reveal"; +import LoadingState from "src/ui/base/components/LoadingState"; + +export function Leaderboard(): ReactElement | null { + // TODO Implement the hook for fetching leaderboard data + const { leaderboard, status } = { status: "loading", leaderboard: {} }; + + return ( +
+
+

+ Miles Leaderboard +

+

+ Showing the top miles earners across all pools +

+
+
+ {status === "loading" && !leaderboard ? ( +
+ +
+ ) : leaderboard ? ( + +
leaderboard here
+
+ ) : null} +
+
+ ); +} + +function useMilesLeaderboard() { + return; +} diff --git a/apps/hyperdrive-trading/src/ui/rewards/hooks/useClaimableRewards.ts b/apps/hyperdrive-trading/src/ui/rewards/hooks/useClaimableRewards.ts index 12cc97c23..2725e8872 100644 --- a/apps/hyperdrive-trading/src/ui/rewards/hooks/useClaimableRewards.ts +++ b/apps/hyperdrive-trading/src/ui/rewards/hooks/useClaimableRewards.ts @@ -1,13 +1,12 @@ import { parseFixed } from "@delvtech/fixed-point-wasm"; -import { MerklApi } from "@merkl/api"; import { useQuery } from "@tanstack/react-query"; import { makeQueryKey2 } from "src/base/makeQueryKey"; import { rewardsFork } from "src/chains/rewardsFork"; import { ClaimableReward } from "src/rewards/ClaimableReward"; import { HyperdriveRewardsApi } from "src/rewards/generated/HyperdriveRewardsApi"; +import { fetchMileRewards } from "src/rewards/merkl"; import { useFeatureFlag } from "src/ui/base/featureFlags/featureFlags"; -import { Address, Hash } from "viem"; -import { gnosis } from "viem/chains"; +import { Address } from "viem"; import { usePublicClient } from "wagmi"; export function useClaimableRewards({ @@ -69,76 +68,3 @@ async function fetchHyperdriveRewardApi( claimableAmount: parseFixed(r.claimableAmount).bigint.toString(), })); } -/** - * - * Rewards that come from the Merkl.xyz API server. This server - * @param account - */ -const merkl = MerklApi("https://api.merkl.xyz").v4; - -/** - * Merkl Distributor is the contract that you can claim rewards from in the - * Merkl.xyz ecosystem. - * See: https://app.merkl.xyz/status - */ -const MerklDistributorsByChain: Record = { - [gnosis.id]: "0x3Ef3D8bA38EBe18DB133cEc108f4D14CE00Dd9Ae", -}; - -async function fetchMileRewards(account: Address): Promise { - // Merkl.xyz accumulates Miles across all chains and hyperdrives onto Gnosis - // chain only. This makes things easier for turning them into HD later if - // they're all just on one chain. - const chainIds = [gnosis.id]; - - // Request miles earned on each chain. We have to call this once per chain - // since the merkl api is buggy, despite accepting an array of chain ids. If - // this gets fixed, we can remove the Promise.all and simplify this logic. - const mileRewards = ( - await Promise.all( - chainIds.map(async (chainId) => { - const { data } = await merkl - .users({ - address: account, - }) - .rewards.get({ - query: { - chainId: [chainId], - }, - }); - - return { data, chainId }; - }), - ) - ) - .filter( - ({ data }) => - data?.length && - // since we only request a single chain id, we can just grab the first - // data item - data[0].rewards.find( - // Merkl.xyz has something called HYPOINTS too, but we only care about - // Miles - (d) => d.token.symbol === "Miles" && !!Number(d.amount), - ), - ) - .map(({ data, chainId }): ClaimableReward => { - const rewards = data![0].rewards.find( - (d) => d.token.symbol === "Miles" && !!Number(d.amount), - ); - return { - chainId, - merkleType: "MerklXyz", - merkleProof: rewards?.proofs as Hash[], - claimableAmount: rewards?.amount.toString() || "0", - pendingAmount: rewards?.pending.toString() || "0", - merkleProofLastUpdated: 0, - rewardTokenAddress: rewards?.token.address as `0x${string}`, - // TODO: This won't use the same abi as the hyperdrive rewards api, so - // we'll need to account for this somehow - claimContractAddress: MerklDistributorsByChain[chainId], - }; - }) - .flat(); - return mileRewards; -} diff --git a/apps/hyperdrive-trading/src/ui/rewards/routes.ts b/apps/hyperdrive-trading/src/ui/rewards/routes.ts new file mode 100644 index 000000000..b8f75476d --- /dev/null +++ b/apps/hyperdrive-trading/src/ui/rewards/routes.ts @@ -0,0 +1 @@ +export const POINTS_LEADERBOARD_ROUTE = "/leaderboard"; diff --git a/apps/hyperdrive-trading/src/ui/routes/leaderboard.tsx b/apps/hyperdrive-trading/src/ui/routes/leaderboard.tsx new file mode 100644 index 000000000..be5bfebf1 --- /dev/null +++ b/apps/hyperdrive-trading/src/ui/routes/leaderboard.tsx @@ -0,0 +1,11 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { Page } from "src/ui/app/Page"; +import { Leaderboard } from "src/ui/markets/Leaderboard"; +import { POINTS_LEADERBOARD_ROUTE } from "src/ui/rewards/routes"; +export const Route = createFileRoute(POINTS_LEADERBOARD_ROUTE)({ + component: () => ( + + + + ), +}); From cf8a01a27f33b7313368501176bd55470f65db39 Mon Sep 17 00:00:00 2001 From: Danny Delott Date: Thu, 13 Feb 2025 17:34:10 -0800 Subject: [PATCH 2/5] Wire up points leaderboard --- apps/hyperdrive-trading/package.json | 1 + apps/hyperdrive-trading/src/rewards/merkl.ts | 137 +++++++++++++ apps/hyperdrive-trading/src/routeTree.gen.ts | 26 +++ .../src/ui/markets/Leaderboard.tsx | 189 ++++++++++++++++-- yarn.lock | 5 + 5 files changed, 343 insertions(+), 15 deletions(-) create mode 100644 apps/hyperdrive-trading/src/rewards/merkl.ts diff --git a/apps/hyperdrive-trading/package.json b/apps/hyperdrive-trading/package.json index 52b03a001..f1eae801b 100644 --- a/apps/hyperdrive-trading/package.json +++ b/apps/hyperdrive-trading/package.json @@ -32,6 +32,7 @@ "@delvtech/drift": "^0.0.1-beta.11", "@delvtech/drift-viem": "^0.0.1-beta.13", "@delvtech/fixed-point-wasm": "^0.0.7", + "addreth": "3.0.1", "@delvtech/hyperdrive-appconfig": "^0.1.0", "@delvtech/hyperdrive-js": "^0.0.1", "@headlessui/react": "^2.1.5", diff --git a/apps/hyperdrive-trading/src/rewards/merkl.ts b/apps/hyperdrive-trading/src/rewards/merkl.ts new file mode 100644 index 000000000..c76d1112c --- /dev/null +++ b/apps/hyperdrive-trading/src/rewards/merkl.ts @@ -0,0 +1,137 @@ +import { MerklApi } from "@merkl/api"; +import groupBy from "lodash.groupby"; +import { ClaimableReward } from "src/rewards/ClaimableReward"; +import { Address, Hash } from "viem"; +import { gnosis } from "viem/chains"; + +/** + * Merkl.xyz API client + */ +export const merklApi = MerklApi("https://api.merkl.xyz").v4; + +/** + * Returns the list of users with Miles and their balances. + */ +export interface LeaderboardEntry { + address: Address; + balance: bigint; +} +export async function fetchMilesLeaderboard(): Promise { + const opportunitiesResponse = await fetch( + "https://api.merkl.xyz/v4/opportunities/campaigns?tokenAddress=0x79385D4B4c531bBbDa25C4cFB749781Bd9E23039", + ); + const opportunities = (await opportunitiesResponse.json()) as { + campaigns: { campaignId: string }[]; + }[]; + const campaignIds = Array.from( + new Set( + opportunities.flatMap((opportunity) => + opportunity.campaigns.map((campaign) => campaign.campaignId), + ), + ), + ); + + const users = ( + await Promise.all( + campaignIds.map(async (campaignId) => { + const rewardsResponse = await fetch( + `https://api.merkl.xyz/v4/rewards/?chainId=100&campaignId=${campaignId}`, + ); + const rewards = (await rewardsResponse.json()) as { + recipient: Address; + amount: string; + }[]; + return rewards; + }), + ) + ).flat(); + + const rewardsByUser = Object.entries(groupBy(users, (user) => user.recipient)) + .map(([user, rewards]) => { + const totalRewards = rewards.reduce( + (total, reward) => total + BigInt(reward.amount), + 0n, + ); + return { user, totalRewards }; + }) + .filter(({ totalRewards }) => totalRewards > 0n) + .map(({ user, totalRewards }) => ({ + address: user as Address, + balance: totalRewards, + })) + .toSorted((a, b) => Number(b.balance - a.balance)); + + return rewardsByUser; +} + +/** + * Merkl Distributor is the contract that you can claim rewards from in the + * Merkl.xyz ecosystem. + * See: https://app.merkl.xyz/status + */ +export const MerklDistributorsByChain: Record = { + [gnosis.id]: "0x3Ef3D8bA38EBe18DB133cEc108f4D14CE00Dd9Ae", +}; + +/** + * Fetches the number of Miles a user has earned + */ +export async function fetchMileRewards( + account: Address, +): Promise { + // Merkl.xyz accumulates Miles across all chains and hyperdrives onto Gnosis + // chain only. This makes things easier for turning them into HD later if + // they're all just on one chain. + const chainIds = [gnosis.id]; + + // Request miles earned on each chain. We have to call this once per chain + // since the merkl api is buggy, despite accepting an array of chain ids. If + // this gets fixed, we can remove the Promise.all and simplify this logic. + const mileRewards = ( + await Promise.all( + chainIds.map(async (chainId) => { + const { data } = await merklApi + .users({ + address: account, + }) + .rewards.get({ + query: { + chainId: [chainId], + }, + }); + + return { data, chainId }; + }), + ) + ) + .filter( + ({ data }) => + data?.length && + // since we only request a single chain id, we can just grab the first + // data item + data[0].rewards.find( + // Merkl.xyz has something called HYPOINTS too, but we only care about + // Miles + (d) => d.token.symbol === "Miles" && !!Number(d.amount), + ), + ) + .map(({ data, chainId }): ClaimableReward => { + const rewards = data![0].rewards.find( + (d) => d.token.symbol === "Miles" && !!Number(d.amount), + ); + return { + chainId, + merkleType: "MerklXyz", + merkleProof: rewards?.proofs as Hash[], + claimableAmount: rewards?.amount.toString() || "0", + pendingAmount: rewards?.pending.toString() || "0", + merkleProofLastUpdated: 0, + rewardTokenAddress: rewards?.token.address as `0x${string}`, + // TODO: This won't use the same abi as the hyperdrive rewards api, so + // we'll need to account for this somehow + claimContractAddress: MerklDistributorsByChain[chainId], + }; + }) + .flat(); + return mileRewards; +} diff --git a/apps/hyperdrive-trading/src/routeTree.gen.ts b/apps/hyperdrive-trading/src/routeTree.gen.ts index 91b9dc756..a54b59b41 100644 --- a/apps/hyperdrive-trading/src/routeTree.gen.ts +++ b/apps/hyperdrive-trading/src/routeTree.gen.ts @@ -15,6 +15,7 @@ import { Route as ChainlogImport } from "./ui/routes/chainlog"; import { Route as ErrorImport } from "./ui/routes/error"; import { Route as IndexImport } from "./ui/routes/index"; import { Route as IneligibleImport } from "./ui/routes/ineligible"; +import { Route as LeaderboardImport } from "./ui/routes/leaderboard"; import { Route as MarketChainIdAddressImport } from "./ui/routes/market.$chainId.$address"; import { Route as MintImport } from "./ui/routes/mint"; import { Route as PointsmarketsImport } from "./ui/routes/points_markets"; @@ -47,6 +48,12 @@ const MintRoute = MintImport.update({ getParentRoute: () => rootRoute, } as any); +const LeaderboardRoute = LeaderboardImport.update({ + id: "/leaderboard", + path: "/leaderboard", + getParentRoute: () => rootRoute, +} as any); + const IneligibleRoute = IneligibleImport.update({ id: "/ineligible", path: "/ineligible", @@ -109,6 +116,13 @@ declare module "@tanstack/react-router" { preLoaderRoute: typeof IneligibleImport; parentRoute: typeof rootRoute; }; + "/leaderboard": { + id: "/leaderboard"; + path: "/leaderboard"; + fullPath: "/leaderboard"; + preLoaderRoute: typeof LeaderboardImport; + parentRoute: typeof rootRoute; + }; "/mint": { id: "/mint"; path: "/mint"; @@ -154,6 +168,7 @@ export interface FileRoutesByFullPath { "/chainlog": typeof ChainlogRoute; "/error": typeof ErrorRoute; "/ineligible": typeof IneligibleRoute; + "/leaderboard": typeof LeaderboardRoute; "/mint": typeof MintRoute; "/points_markets": typeof PointsmarketsRoute; "/portfolio": typeof PortfolioRoute; @@ -166,6 +181,7 @@ export interface FileRoutesByTo { "/chainlog": typeof ChainlogRoute; "/error": typeof ErrorRoute; "/ineligible": typeof IneligibleRoute; + "/leaderboard": typeof LeaderboardRoute; "/mint": typeof MintRoute; "/points_markets": typeof PointsmarketsRoute; "/portfolio": typeof PortfolioRoute; @@ -179,6 +195,7 @@ export interface FileRoutesById { "/chainlog": typeof ChainlogRoute; "/error": typeof ErrorRoute; "/ineligible": typeof IneligibleRoute; + "/leaderboard": typeof LeaderboardRoute; "/mint": typeof MintRoute; "/points_markets": typeof PointsmarketsRoute; "/portfolio": typeof PortfolioRoute; @@ -193,6 +210,7 @@ export interface FileRouteTypes { | "/chainlog" | "/error" | "/ineligible" + | "/leaderboard" | "/mint" | "/points_markets" | "/portfolio" @@ -204,6 +222,7 @@ export interface FileRouteTypes { | "/chainlog" | "/error" | "/ineligible" + | "/leaderboard" | "/mint" | "/points_markets" | "/portfolio" @@ -215,6 +234,7 @@ export interface FileRouteTypes { | "/chainlog" | "/error" | "/ineligible" + | "/leaderboard" | "/mint" | "/points_markets" | "/portfolio" @@ -228,6 +248,7 @@ export interface RootRouteChildren { ChainlogRoute: typeof ChainlogRoute; ErrorRoute: typeof ErrorRoute; IneligibleRoute: typeof IneligibleRoute; + LeaderboardRoute: typeof LeaderboardRoute; MintRoute: typeof MintRoute; PointsmarketsRoute: typeof PointsmarketsRoute; PortfolioRoute: typeof PortfolioRoute; @@ -240,6 +261,7 @@ const rootRouteChildren: RootRouteChildren = { ChainlogRoute: ChainlogRoute, ErrorRoute: ErrorRoute, IneligibleRoute: IneligibleRoute, + LeaderboardRoute: LeaderboardRoute, MintRoute: MintRoute, PointsmarketsRoute: PointsmarketsRoute, PortfolioRoute: PortfolioRoute, @@ -261,6 +283,7 @@ export const routeTree = rootRoute "/chainlog", "/error", "/ineligible", + "/leaderboard", "/mint", "/points_markets", "/portfolio", @@ -280,6 +303,9 @@ export const routeTree = rootRoute "/ineligible": { "filePath": "ineligible.tsx" }, + "/leaderboard": { + "filePath": "leaderboard.tsx" + }, "/mint": { "filePath": "mint.tsx" }, diff --git a/apps/hyperdrive-trading/src/ui/markets/Leaderboard.tsx b/apps/hyperdrive-trading/src/ui/markets/Leaderboard.tsx index 989c18f61..94e9fb20d 100644 --- a/apps/hyperdrive-trading/src/ui/markets/Leaderboard.tsx +++ b/apps/hyperdrive-trading/src/ui/markets/Leaderboard.tsx @@ -1,10 +1,43 @@ +import { AppConfig, makeAddressUrl } from "@delvtech/hyperdrive-appconfig"; +import { ChevronDownIcon, ChevronUpIcon } from "@heroicons/react/24/outline"; +import { useQuery } from "@tanstack/react-query"; +import { + createColumnHelper, + flexRender, + getCoreRowModel, + getSortedRowModel, + useReactTable, +} from "@tanstack/react-table"; +import { Addreth, ThemeDeclaration } from "addreth"; +import classNames from "classnames"; import { ReactElement } from "react"; -import { Fade } from "react-awesome-reveal"; +import { fetchMilesLeaderboard, LeaderboardEntry } from "src/rewards/merkl"; +import { useAppConfigForConnectedChain } from "src/ui/appconfig/useAppConfigForConnectedChain"; import LoadingState from "src/ui/base/components/LoadingState"; +import { TableSkeleton } from "src/ui/base/components/TableSkeleton"; +import { formatBalance } from "src/ui/base/formatting/formatBalance"; +import { HyperVueMilesIconUrl } from "src/ui/rewards/HyperVueMilesIconUrl"; +import { gnosis } from "viem/chains"; export function Leaderboard(): ReactElement | null { - // TODO Implement the hook for fetching leaderboard data - const { leaderboard, status } = { status: "loading", leaderboard: {} }; + const { leaderboard, status } = useMilesLeaderboard(); + const appConfig = useAppConfigForConnectedChain(); + + const columns = getColumns(appConfig); + const tableInstance = useReactTable({ + columns, + data: leaderboard || [], + getCoreRowModel: getCoreRowModel(), + getSortedRowModel: getSortedRowModel(), + initialState: { + sorting: [ + { + id: "balance", + desc: true, + }, + ], + }, + }); return (
@@ -13,27 +46,153 @@ export function Leaderboard(): ReactElement | null { Miles Leaderboard

- Showing the top miles earners across all pools + Live rankings of the top Miles earners on Hyperdrive. Add liquidity to + any Hyperdrive pool to earn Miles!

-
- {status === "loading" && !leaderboard ? ( -
+
+ {status === "loading" && !leaderboard?.length ? ( +
- ) : leaderboard ? ( - -
leaderboard here
-
- ) : null} + ) : ( + + + {tableInstance.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + const sortDirection = header.column.getIsSorted(); + return ( + + ); + })} + + ))} + + + {status === "loading" ? ( + + ) : ( + tableInstance.getRowModel().rows.map((row) => { + return ( + + <> + {row.getVisibleCells().map((cell) => { + return ( + + ); + })} + + + ); + }) + )} + +
+
+ {flexRender( + header.column.columnDef.header, + header.getContext(), + )} + {sortDirection === "asc" && ( + + )} + {sortDirection === "desc" && ( + + )} +
+
+ {flexRender( + cell.column.columnDef.cell, + cell.getContext(), + )} +
+ )}
); } function useMilesLeaderboard() { - return; + const { data: leaderboard, status } = useQuery({ + queryKey: ["leaderboard"], + queryFn: async () => { + return fetchMilesLeaderboard(); + }, + }); + return { leaderboard, status }; +} + +const columnHelper = createColumnHelper(); + +const addrethTheme: ThemeDeclaration = { + base: "dark", + badgeBackground: "transparent", + fontSize: 16, +}; +function getColumns(appConfig: AppConfig) { + return [ + columnHelper.accessor((row) => row.address, { + header: "Address", + cell: ({ getValue }) => { + const address = getValue(); + return ( + ({ + accountUrl: makeAddressUrl(a, appConfig.chains[gnosis.id]), + name: "Gnosisscan", + })} + /> + ); + }, + }), + columnHelper.accessor((row) => row.balance, { + header: "Miles Earned", + id: "balance", + cell: ({ getValue }) => { + let formatted = formatBalance({ + balance: getValue(), + decimals: 18, + places: 2, + }); + if (formatted === "0.00") { + formatted = "<0.01"; + } + return ( +
+ {" "} + {formatted} +
+ ); + }, + }), + ]; } diff --git a/yarn.lock b/yarn.lock index a0a0893c7..453f4c0a6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8050,6 +8050,11 @@ address@^1.0.1, address@^1.1.2: resolved "https://registry.yarnpkg.com/address/-/address-1.2.2.tgz#2b5248dac5485a6390532c6a517fda2e3faac89e" integrity sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA== +addreth@3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/addreth/-/addreth-3.0.1.tgz#ee9f219d0571255796b8439163da6ead98efc451" + integrity sha512-I+unUQCOilLunbx7xhZl08u/3vfEt7aMfH/kmpmtkT7SmE2s/KSnmJu0QdWWNahlTmD/y5RAG55+RmlSX4tI0w== + aes-js@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/aes-js/-/aes-js-3.0.0.tgz#e21df10ad6c2053295bcbb8dab40b09dbea87e4d" From ccd8098aa5de55075f16f18f6a3476afec1ef994 Mon Sep 17 00:00:00 2001 From: Danny Delott Date: Thu, 13 Feb 2025 17:47:40 -0800 Subject: [PATCH 3/5] Add rank column --- apps/hyperdrive-trading/src/rewards/merkl.ts | 5 ++++- apps/hyperdrive-trading/src/ui/markets/Leaderboard.tsx | 8 ++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/apps/hyperdrive-trading/src/rewards/merkl.ts b/apps/hyperdrive-trading/src/rewards/merkl.ts index c76d1112c..59d4311a0 100644 --- a/apps/hyperdrive-trading/src/rewards/merkl.ts +++ b/apps/hyperdrive-trading/src/rewards/merkl.ts @@ -15,7 +15,9 @@ export const merklApi = MerklApi("https://api.merkl.xyz").v4; export interface LeaderboardEntry { address: Address; balance: bigint; + rank: number; } + export async function fetchMilesLeaderboard(): Promise { const opportunitiesResponse = await fetch( "https://api.merkl.xyz/v4/opportunities/campaigns?tokenAddress=0x79385D4B4c531bBbDa25C4cFB749781Bd9E23039", @@ -59,7 +61,8 @@ export async function fetchMilesLeaderboard(): Promise { address: user as Address, balance: totalRewards, })) - .toSorted((a, b) => Number(b.balance - a.balance)); + .toSorted((a, b) => Number(b.balance - a.balance)) + .map((entry, i) => ({ ...entry, rank: i + 1 })) as LeaderboardEntry[]; return rewardsByUser; } diff --git a/apps/hyperdrive-trading/src/ui/markets/Leaderboard.tsx b/apps/hyperdrive-trading/src/ui/markets/Leaderboard.tsx index 94e9fb20d..d02ed308b 100644 --- a/apps/hyperdrive-trading/src/ui/markets/Leaderboard.tsx +++ b/apps/hyperdrive-trading/src/ui/markets/Leaderboard.tsx @@ -157,6 +157,14 @@ const addrethTheme: ThemeDeclaration = { }; function getColumns(appConfig: AppConfig) { return [ + columnHelper.accessor((row) => row.rank, { + id: "rank", + header: "Rank", + cell: ({ getValue }) => { + const rank = getValue(); + return `#${rank}`; + }, + }), columnHelper.accessor((row) => row.address, { header: "Address", cell: ({ getValue }) => { From 0d2de199d0e2d0ca621dd4ea3329c8b9105ec218 Mon Sep 17 00:00:00 2001 From: Danny Delott Date: Thu, 13 Feb 2025 20:57:04 -0800 Subject: [PATCH 4/5] Update subtitle" --- .../hyperdrive-trading/src/ui/markets/Leaderboard.tsx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/apps/hyperdrive-trading/src/ui/markets/Leaderboard.tsx b/apps/hyperdrive-trading/src/ui/markets/Leaderboard.tsx index d02ed308b..6237a9ce6 100644 --- a/apps/hyperdrive-trading/src/ui/markets/Leaderboard.tsx +++ b/apps/hyperdrive-trading/src/ui/markets/Leaderboard.tsx @@ -12,6 +12,7 @@ import { Addreth, ThemeDeclaration } from "addreth"; import classNames from "classnames"; import { ReactElement } from "react"; import { fetchMilesLeaderboard, LeaderboardEntry } from "src/rewards/merkl"; +import { ExternalLink } from "src/ui/analytics/ExternalLink"; import { useAppConfigForConnectedChain } from "src/ui/appconfig/useAppConfigForConnectedChain"; import LoadingState from "src/ui/base/components/LoadingState"; import { TableSkeleton } from "src/ui/base/components/TableSkeleton"; @@ -46,8 +47,14 @@ export function Leaderboard(): ReactElement | null { Miles Leaderboard

- Live rankings of the top Miles earners on Hyperdrive. Add liquidity to - any Hyperdrive pool to earn Miles! + Live rankings of the top Miles earners on Hyperdrive through the{" "} + + HyperVue Foundation points program +

From a43ca99c7ddc5486ef2f648c744aa74fd3277b95 Mon Sep 17 00:00:00 2001 From: Danny Delott Date: Thu, 13 Feb 2025 21:44:17 -0800 Subject: [PATCH 5/5] Remove feature flag --- apps/hyperdrive-trading/src/ui/app/Navbar/Navbar.tsx | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/apps/hyperdrive-trading/src/ui/app/Navbar/Navbar.tsx b/apps/hyperdrive-trading/src/ui/app/Navbar/Navbar.tsx index 7dd2a5fc2..09fa6bd03 100644 --- a/apps/hyperdrive-trading/src/ui/app/Navbar/Navbar.tsx +++ b/apps/hyperdrive-trading/src/ui/app/Navbar/Navbar.tsx @@ -12,7 +12,6 @@ import { useAnalyticsUrl } from "src/ui/analytics/useMarketAnalyticsUrl"; import { DevtoolsMenu } from "src/ui/app/Navbar/DevtoolsMenu"; import { HyperdriveLogo } from "src/ui/app/Navbar/HyperdriveLogo"; import VersionPicker from "src/ui/base/components/VersionPicker"; -import { useFeatureFlag } from "src/ui/base/featureFlags/featureFlags"; import { useIsTailwindSmallScreen } from "src/ui/base/mediaBreakpoints"; import { LANDING_ROUTE } from "src/ui/landing/routes"; import { POINTS_MARKETS_ROUTE } from "src/ui/markets/routes"; @@ -27,8 +26,6 @@ export function Navbar(): ReactElement { const chainId = useChainId(); const isTestnet = isTestnetChain(chainId); const analyticsUrl = useAnalyticsUrl(); - const { isFlagEnabled: isMilesLeaderboardEnabled } = - useFeatureFlag("miles-leaderboard"); return (
@@ -43,9 +40,7 @@ export function Navbar(): ReactElement { - {isMilesLeaderboardEnabled ? ( - - ) : null} + {isTestnet ? ( ) : null}