diff --git a/apps/hyperdrive-trading/package.json b/apps/hyperdrive-trading/package.json
index 28b09ef63..37f8a4123 100644
--- a/apps/hyperdrive-trading/package.json
+++ b/apps/hyperdrive-trading/package.json
@@ -45,6 +45,7 @@
"@tanstack/query-core": "^4.36.1",
"@types/d3-format": "^3.0.4",
"@types/lodash.sortby": "^4.7.9",
+ "@types/lodash.groupby": "^4.6.9",
"@uniswap/token-lists": "^1.0.0-beta.34",
"@usecapsule/rainbowkit-wallet": "^0.9.4",
"@usecapsule/react-sdk": "^3.17.0",
@@ -61,6 +62,7 @@
"graphql": "^16.9.0",
"graphql-request": "^7.1.0",
"lodash.sortby": "^4.7.0",
+ "lodash.groupby": "^4.6.0",
"process": "^0.11.10",
"react": "^18.2.0",
"react-dom": "^18.2.0",
diff --git a/apps/hyperdrive-trading/src/ui/portfolio/PortfolioTableHeading.tsx b/apps/hyperdrive-trading/src/ui/portfolio/PortfolioTableHeading.tsx
new file mode 100644
index 000000000..61ade2181
--- /dev/null
+++ b/apps/hyperdrive-trading/src/ui/portfolio/PortfolioTableHeading.tsx
@@ -0,0 +1,18 @@
+import { ReactElement, ReactNode } from "react";
+
+export function PortfolioTableHeading({
+ leftElement,
+ rightElement,
+}: {
+ leftElement: ReactNode;
+ rightElement: ReactNode;
+}): ReactElement {
+ return (
+
+
+ {leftElement}
+
+ {rightElement}
+
+ );
+}
diff --git a/apps/hyperdrive-trading/src/ui/portfolio/PositionTableHeading.tsx b/apps/hyperdrive-trading/src/ui/portfolio/PositionTableHeading.tsx
index a48720639..c7832f6eb 100644
--- a/apps/hyperdrive-trading/src/ui/portfolio/PositionTableHeading.tsx
+++ b/apps/hyperdrive-trading/src/ui/portfolio/PositionTableHeading.tsx
@@ -1,6 +1,7 @@
import { HyperdriveConfig } from "@delvtech/hyperdrive-appconfig";
import { ReactElement, ReactNode } from "react";
import { AssetStack } from "src/ui/markets/AssetStack";
+import { PortfolioTableHeading } from "src/ui/portfolio/PortfolioTableHeading";
export function PositionTableHeading({
hyperdrive,
@@ -15,15 +16,17 @@ export function PositionTableHeading({
rightElement: ReactNode;
}): ReactElement {
return (
-
-
-
-
{hyperdriveName ?? hyperdrive.name}
-
- {rightElement}
-
+
+
+ {hyperdriveName ?? hyperdrive.name}
+ >
+ }
+ rightElement={rightElement}
+ />
);
}
diff --git a/apps/hyperdrive-trading/src/ui/portfolio/rewards/RewardsContainer.tsx b/apps/hyperdrive-trading/src/ui/portfolio/rewards/RewardsContainer.tsx
index 414424731..2fec0f1cd 100644
--- a/apps/hyperdrive-trading/src/ui/portfolio/rewards/RewardsContainer.tsx
+++ b/apps/hyperdrive-trading/src/ui/portfolio/rewards/RewardsContainer.tsx
@@ -1,15 +1,19 @@
+import { appConfig } from "@delvtech/hyperdrive-appconfig";
import { Link } from "@tanstack/react-router";
+import groupBy from "lodash.groupby";
import { ReactElement } from "react";
import LoadingState from "src/ui/base/components/LoadingState";
import { NonIdealState } from "src/ui/base/components/NonIdealState";
import { NoWalletConnected } from "src/ui/portfolio/NoWalletConnected";
+import { PortfolioTableHeading } from "src/ui/portfolio/PortfolioTableHeading";
import { PositionContainer } from "src/ui/portfolio/PositionContainer";
-import { useRewardsData } from "src/ui/portfolio/rewards/useRewardsData";
+import { RewardsTableDesktop } from "src/ui/portfolio/rewards/RewardsTableDesktop";
+import { usePortfolioRewardsData } from "src/ui/portfolio/rewards/useRewardsData";
import { useAccount } from "wagmi";
export function RewardsContainer(): ReactElement {
const { address: account } = useAccount();
- const { rewards, rewardsStatus } = useRewardsData({ account });
+ const { rewards, rewardsStatus } = usePortfolioRewardsData({ account });
if (!account) {
return ;
@@ -37,7 +41,7 @@ export function RewardsContainer(): ReactElement {
);
}
- const hasClaimableRewards = rewards?.some((reward) => reward);
+ const hasClaimableRewards = !rewards || rewards?.some((reward) => reward);
if (!hasClaimableRewards) {
return (
@@ -59,5 +63,29 @@ export function RewardsContainer(): ReactElement {
);
}
- return TODO;
+ const rewardsByChain = groupBy(rewards, (reward) => reward.chainId);
+
+ return (
+
+ {Object.entries(rewardsByChain).map(([chainId, rewards]) => {
+ const chainInfo = appConfig.chains[+chainId];
+ return (
+
+
+
+
+
+ {chainInfo.name} Rewards
+
+ }
+ rightElement={null}
+ />
+
+
+ );
+ })}
+
+ );
}
diff --git a/apps/hyperdrive-trading/src/ui/portfolio/rewards/RewardsTableDesktop.tsx b/apps/hyperdrive-trading/src/ui/portfolio/rewards/RewardsTableDesktop.tsx
new file mode 100644
index 000000000..7fdb097e6
--- /dev/null
+++ b/apps/hyperdrive-trading/src/ui/portfolio/rewards/RewardsTableDesktop.tsx
@@ -0,0 +1,188 @@
+import { appConfig, getToken } from "@delvtech/hyperdrive-appconfig";
+import { ChevronDownIcon, ChevronUpIcon } from "@heroicons/react/24/outline";
+import {
+ createColumnHelper,
+ flexRender,
+ getCoreRowModel,
+ getPaginationRowModel,
+ getSortedRowModel,
+ useReactTable,
+} from "@tanstack/react-table";
+import classNames from "classnames";
+import { ReactElement } from "react";
+import { Pagination } from "src/ui/base/components/Pagination";
+import { formatBalance } from "src/ui/base/formatting/formatBalance";
+import { Reward } from "src/ui/portfolio/rewards/useRewardsData";
+import { Address } from "viem";
+
+export function RewardsTableDesktop({
+ account,
+ rewards,
+}: {
+ account: Address;
+ rewards: Reward[] | undefined;
+}): ReactElement {
+ const tableInstance = useReactTable({
+ columns: getColumns(),
+ data: rewards || [],
+ getCoreRowModel: getCoreRowModel(),
+ getSortedRowModel: getSortedRowModel(),
+ getPaginationRowModel: getPaginationRowModel(),
+ });
+
+ return (
+
+
+
+ {tableInstance.getHeaderGroups().map((headerGroup) => (
+
+ {headerGroup.headers.map((header, headerIndex) => (
+
+
+ {flexRender(
+ header.column.columnDef.header,
+ header.getContext(),
+ )}
+ {{
+ asc: ,
+ desc: ,
+ }[header.column.getIsSorted() as string] ?? null}
+
+ {/* Custom border with inset for the first and last header cells */}
+
+ |
+ ))}
+
+ ))}
+
+
+
+ {tableInstance.getRowModel().rows.map((row, index) => {
+ const isLastRow =
+ index === tableInstance.getRowModel().rows.length - 1;
+ return (
+
+ {row.getVisibleCells().map((cell, cellIndex) => (
+
+ {flexRender(cell.column.columnDef.cell, cell.getContext())}
+ {!isLastRow && (
+
+ )}
+ |
+ ))}
+
+ );
+ })}
+
+
+ {tableInstance.getFilteredRowModel().rows.length > 10 ? (
+
+ ) : null}
+
+ );
+}
+
+const columnHelper = createColumnHelper();
+
+function getColumns() {
+ return [
+ columnHelper.display({
+ id: "asset",
+ header: "Asset",
+ cell: ({ row }) => {
+ const token = getToken({
+ appConfig,
+ chainId: row.original.chainId,
+ tokenAddress: row.original.rewardToken,
+ })!;
+ return (
+
+
+ {token.name}
+
+ );
+ },
+ }),
+ columnHelper.display({
+ id: "claimable",
+ header: "Claimable",
+ cell: ({ row }) => {
+ const token = getToken({
+ appConfig,
+ chainId: row.original.chainId,
+ tokenAddress: row.original.rewardToken,
+ })!;
+ return (
+
+
+ {formatBalance({
+ balance: row.original.claimable || 0n,
+ decimals: token.decimals,
+ places: token.places,
+ })}{" "}
+ {token.symbol}
+
+
+ );
+ },
+ }),
+ columnHelper.display({
+ id: "claim",
+ cell: ({ row }) => {
+ return (
+
+ );
+ },
+ }),
+ ];
+}
diff --git a/apps/hyperdrive-trading/src/ui/portfolio/rewards/useRewardsData.ts b/apps/hyperdrive-trading/src/ui/portfolio/rewards/useRewardsData.ts
index 47ad0054a..2567da650 100644
--- a/apps/hyperdrive-trading/src/ui/portfolio/rewards/useRewardsData.ts
+++ b/apps/hyperdrive-trading/src/ui/portfolio/rewards/useRewardsData.ts
@@ -1,9 +1,72 @@
+import { parseFixed } from "@delvtech/fixed-point-wasm";
+import { appConfig } from "@delvtech/hyperdrive-appconfig";
import { useQuery } from "@tanstack/react-query";
import { makeQueryKey } from "src/base/makeQueryKey";
-import { Address } from "viem";
+import { Address, zeroAddress } from "viem";
+import { base } from "viem/chains";
-export function useRewardsData({ account }: { account: Address | undefined }): {
- rewards: unknown[] | undefined;
+interface RewardsResponse {
+ userAddress: Address;
+ rewards: Reward[];
+}
+
+export interface Reward {
+ chainId: number;
+ claimContract: Address;
+ claimable: bigint;
+ total: bigint;
+ claimed: bigint;
+ claimableLastUpdated: number;
+ rewardToken: Address;
+ merkleProof: string[] | null;
+ merkleProofLastUpdated: number;
+}
+
+function getDummyRewardsResponse(account: Address) {
+ const dummyRewardsResponse: RewardsResponse = {
+ userAddress: account,
+ rewards: [
+ {
+ // rewards for this user that they can claim
+ chainId: base.id,
+ claimContract: zeroAddress,
+ claimable: parseFixed("1000000").bigint,
+ total: parseFixed("1000000").bigint,
+ claimed: parseFixed("0").bigint,
+ claimableLastUpdated: 123456789,
+ rewardToken: appConfig.tokens.find(
+ (token) => token.chainId === 8453 && token.symbol === "MORPHO",
+ )!.address,
+ merkleProof: ["0xProof", "0xProof", "0xProof"],
+ merkleProofLastUpdated: 123892327,
+ },
+ {
+ // rewards are accumulating, but the merkle root hasn't been added
+ // to the claimContract yet
+ chainId: base.id,
+ claimContract: zeroAddress,
+ total: parseFixed("1000000").bigint,
+ claimed: parseFixed("0").bigint,
+ claimable: parseFixed("0").bigint,
+ claimableLastUpdated: 123456789,
+ rewardToken: appConfig.tokens.find(
+ (token) => token.chainId === 8453 && token.symbol === "USDC",
+ )!.address,
+ merkleProof: null,
+ merkleProofLastUpdated: 123892327,
+ },
+ ],
+ };
+
+ return dummyRewardsResponse;
+}
+
+export function usePortfolioRewardsData({
+ account,
+}: {
+ account: Address | undefined;
+}): {
+ rewards: Reward[] | undefined;
rewardsStatus: "error" | "success" | "loading";
} {
const queryEnabled = !!account;
@@ -12,7 +75,8 @@ export function useRewardsData({ account }: { account: Address | undefined }): {
queryFn: queryEnabled
? async () => {
// TODO: Fetch rewards from server
- return [];
+ const rewardsResponse = getDummyRewardsResponse(account);
+ return rewardsResponse.rewards;
}
: undefined,
enabled: queryEnabled,