From c361224a7b11b471bdef8918ab2e90c44780d2e0 Mon Sep 17 00:00:00 2001 From: Danny Delott Date: Tue, 19 Nov 2024 11:17:13 -0800 Subject: [PATCH 1/4] Add dummy rewards response --- .../ui/portfolio/rewards/useRewardsData.ts | 57 ++++++++++++++++++- 1 file changed, 55 insertions(+), 2 deletions(-) diff --git a/apps/hyperdrive-trading/src/ui/portfolio/rewards/useRewardsData.ts b/apps/hyperdrive-trading/src/ui/portfolio/rewards/useRewardsData.ts index 47ad0054a..c95c1f102 100644 --- a/apps/hyperdrive-trading/src/ui/portfolio/rewards/useRewardsData.ts +++ b/apps/hyperdrive-trading/src/ui/portfolio/rewards/useRewardsData.ts @@ -2,8 +2,60 @@ import { useQuery } from "@tanstack/react-query"; import { makeQueryKey } from "src/base/makeQueryKey"; import { Address } from "viem"; +interface RewardsResponse { + userAddress: Address; + rewards: Reward[]; +} + +interface Reward { + chainId: number; + claimContract: Address; + claimable: string; + total: string; + claimed: string; + 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: 8543, + claimContract: "0x0000", + claimable: "1000000", + total: "1000000", + claimed: "0", + claimableLastUpdated: 123456789, + rewardToken: "0xTOKEN_A", + merkleProof: ["0xProof", "0xProof", "0xProof"], + merkleProofLastUpdated: 123892327, + }, + { + // rewards are accumulating, but the merkle root hasn't been added + // to the claimContract yet + chainId: 8543, + claimContract: "0x0000", + total: "1000000", + claimed: "0", + claimable: "0", + claimableLastUpdated: 123456789, + rewardToken: "0xTOKEN_B", + merkleProof: null, + merkleProofLastUpdated: 123892327, + }, + ], + }; + + return dummyRewardsResponse; +} + export function useRewardsData({ account }: { account: Address | undefined }): { - rewards: unknown[] | undefined; + rewards: Reward[] | undefined; rewardsStatus: "error" | "success" | "loading"; } { const queryEnabled = !!account; @@ -12,7 +64,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, From 10992234837fe2dd765004a571d32fd7c7ca3427 Mon Sep 17 00:00:00 2001 From: Danny Delott Date: Tue, 19 Nov 2024 11:17:40 -0800 Subject: [PATCH 2/4] Break out the PortfolioTableHeading to own component --- .../ui/portfolio/PortfolioTableHeading.tsx | 18 +++++++++++++ .../src/ui/portfolio/PositionTableHeading.tsx | 25 +++++++++++-------- .../ui/portfolio/rewards/RewardsContainer.tsx | 9 +++++-- 3 files changed, 40 insertions(+), 12 deletions(-) create mode 100644 apps/hyperdrive-trading/src/ui/portfolio/PortfolioTableHeading.tsx 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..cc8dd97a5 --- /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..40cebbe09 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,19 @@ 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..fd1cf0a16 100644 --- a/apps/hyperdrive-trading/src/ui/portfolio/rewards/RewardsContainer.tsx +++ b/apps/hyperdrive-trading/src/ui/portfolio/rewards/RewardsContainer.tsx @@ -3,6 +3,7 @@ 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 { useAccount } from "wagmi"; @@ -39,7 +40,7 @@ export function RewardsContainer(): ReactElement { const hasClaimableRewards = rewards?.some((reward) => reward); - if (!hasClaimableRewards) { + if (hasClaimableRewards) { return ( TODO; + return ( + + + + ); } From 56495f27d4d9929178fbc5af764cd133eadb3666 Mon Sep 17 00:00:00 2001 From: Danny Delott Date: Tue, 19 Nov 2024 12:04:34 -0800 Subject: [PATCH 3/4] Show rewards in portfolio --- apps/hyperdrive-trading/package.json | 2 + .../ui/portfolio/rewards/RewardsContainer.tsx | 33 ++- .../portfolio/rewards/RewardsTableDesktop.tsx | 190 ++++++++++++++++++ .../ui/portfolio/rewards/useRewardsData.ts | 47 +++-- 4 files changed, 249 insertions(+), 23 deletions(-) create mode 100644 apps/hyperdrive-trading/src/ui/portfolio/rewards/RewardsTableDesktop.tsx 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/rewards/RewardsContainer.tsx b/apps/hyperdrive-trading/src/ui/portfolio/rewards/RewardsContainer.tsx index fd1cf0a16..2fec0f1cd 100644 --- a/apps/hyperdrive-trading/src/ui/portfolio/rewards/RewardsContainer.tsx +++ b/apps/hyperdrive-trading/src/ui/portfolio/rewards/RewardsContainer.tsx @@ -1,16 +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 ; @@ -38,9 +41,9 @@ export function RewardsContainer(): ReactElement { ); } - const hasClaimableRewards = rewards?.some((reward) => reward); + const hasClaimableRewards = !rewards || rewards?.some((reward) => reward); - if (hasClaimableRewards) { + if (!hasClaimableRewards) { return ( 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..58c590d71 --- /dev/null +++ b/apps/hyperdrive-trading/src/ui/portfolio/rewards/RewardsTableDesktop.tsx @@ -0,0 +1,190 @@ +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) => ( + + ))} + + ))} + + + + {tableInstance.getRowModel().rows.map((row, index) => { + const isLastRow = + index === tableInstance.getRowModel().rows.length - 1; + return ( + + {row.getVisibleCells().map((cell, cellIndex) => ( + + ))} + + ); + })} + +
+
+ {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 */} + +
+ {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 c95c1f102..2567da650 100644 --- a/apps/hyperdrive-trading/src/ui/portfolio/rewards/useRewardsData.ts +++ b/apps/hyperdrive-trading/src/ui/portfolio/rewards/useRewardsData.ts @@ -1,18 +1,21 @@ +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"; interface RewardsResponse { userAddress: Address; rewards: Reward[]; } -interface Reward { +export interface Reward { chainId: number; claimContract: Address; - claimable: string; - total: string; - claimed: string; + claimable: bigint; + total: bigint; + claimed: bigint; claimableLastUpdated: number; rewardToken: Address; merkleProof: string[] | null; @@ -25,26 +28,30 @@ function getDummyRewardsResponse(account: Address) { rewards: [ { // rewards for this user that they can claim - chainId: 8543, - claimContract: "0x0000", - claimable: "1000000", - total: "1000000", - claimed: "0", + chainId: base.id, + claimContract: zeroAddress, + claimable: parseFixed("1000000").bigint, + total: parseFixed("1000000").bigint, + claimed: parseFixed("0").bigint, claimableLastUpdated: 123456789, - rewardToken: "0xTOKEN_A", + 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: 8543, - claimContract: "0x0000", - total: "1000000", - claimed: "0", - claimable: "0", + chainId: base.id, + claimContract: zeroAddress, + total: parseFixed("1000000").bigint, + claimed: parseFixed("0").bigint, + claimable: parseFixed("0").bigint, claimableLastUpdated: 123456789, - rewardToken: "0xTOKEN_B", + rewardToken: appConfig.tokens.find( + (token) => token.chainId === 8453 && token.symbol === "USDC", + )!.address, merkleProof: null, merkleProofLastUpdated: 123892327, }, @@ -54,7 +61,11 @@ function getDummyRewardsResponse(account: Address) { return dummyRewardsResponse; } -export function useRewardsData({ account }: { account: Address | undefined }): { +export function usePortfolioRewardsData({ + account, +}: { + account: Address | undefined; +}): { rewards: Reward[] | undefined; rewardsStatus: "error" | "success" | "loading"; } { From aab44f7fc34d0d666fb3871b1cab7ddb660dea8f Mon Sep 17 00:00:00 2001 From: Danny Delott Date: Tue, 19 Nov 2024 13:26:40 -0800 Subject: [PATCH 4/4] Simplify markup --- .../src/ui/portfolio/PortfolioTableHeading.tsx | 4 ++-- .../src/ui/portfolio/PositionTableHeading.tsx | 8 +++----- .../src/ui/portfolio/rewards/RewardsTableDesktop.tsx | 4 +--- 3 files changed, 6 insertions(+), 10 deletions(-) diff --git a/apps/hyperdrive-trading/src/ui/portfolio/PortfolioTableHeading.tsx b/apps/hyperdrive-trading/src/ui/portfolio/PortfolioTableHeading.tsx index cc8dd97a5..61ade2181 100644 --- a/apps/hyperdrive-trading/src/ui/portfolio/PortfolioTableHeading.tsx +++ b/apps/hyperdrive-trading/src/ui/portfolio/PortfolioTableHeading.tsx @@ -9,8 +9,8 @@ export function PortfolioTableHeading({ }): ReactElement { return (
-
-
{leftElement}
+
+ {leftElement}
{rightElement}
diff --git a/apps/hyperdrive-trading/src/ui/portfolio/PositionTableHeading.tsx b/apps/hyperdrive-trading/src/ui/portfolio/PositionTableHeading.tsx index 40cebbe09..c7832f6eb 100644 --- a/apps/hyperdrive-trading/src/ui/portfolio/PositionTableHeading.tsx +++ b/apps/hyperdrive-trading/src/ui/portfolio/PositionTableHeading.tsx @@ -18,15 +18,13 @@ export function PositionTableHeading({ return ( + <> -

- {hyperdriveName ?? hyperdrive.name} -

-
+ {hyperdriveName ?? hyperdrive.name} + } rightElement={rightElement} /> diff --git a/apps/hyperdrive-trading/src/ui/portfolio/rewards/RewardsTableDesktop.tsx b/apps/hyperdrive-trading/src/ui/portfolio/rewards/RewardsTableDesktop.tsx index 58c590d71..7fdb097e6 100644 --- a/apps/hyperdrive-trading/src/ui/portfolio/rewards/RewardsTableDesktop.tsx +++ b/apps/hyperdrive-trading/src/ui/portfolio/rewards/RewardsTableDesktop.tsx @@ -39,9 +39,7 @@ export function RewardsTableDesktop({ {headerGroup.headers.map((header, headerIndex) => (