Skip to content

feat: add prime total rewards card#5626

Open
cuzz-venus wants to merge 3 commits into
feat/prime-leaderboard-tablefrom
feat/prime-total-rewards
Open

feat: add prime total rewards card#5626
cuzz-venus wants to merge 3 commits into
feat/prime-leaderboard-tablefrom
feat/prime-total-rewards

Conversation

@cuzz-venus

@cuzz-venus cuzz-venus commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Jira ticket(s)

VPD-1333

Changes

  • support total reward card
image image image

@changeset-bot

changeset-bot Bot commented Jun 11, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 6eef450

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@vercel

vercel Bot commented Jun 11, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
dapp-preview Ready Ready Preview Jun 17, 2026 10:05am
dapp-testnet Ready Ready Preview Jun 17, 2026 10:05am
venus.io Ready Ready Preview Jun 17, 2026 10:05am

Request Review

@greptile-apps

greptile-apps Bot commented Jun 11, 2026

Copy link
Copy Markdown

Greptile Summary

This PR implements the Prime total rewards card and user rewards card for the Prime Leaderboard page, adding two new section components (TotalRewardsSection, UserRewardsSection), their placeholder data hooks, a shared MarketRewardRow, a MarketActions modal trigger, and two new SVG icons (sparkle, dotShortcut).

  • TotalRewardsCard and UserRewardsCard are fully implemented with per-market breakdowns, progress bars, APY badges, and a working market-actions modal — all gated correctly behind wallet connection in the page.
  • Both data hooks are explicit placeholders (TODO-annotated) using useGetTokens() until the real API lands; the zero-guard for totalRewardsCents in MarketRewardRow (flagged in a previous review) is correctly applied.

Confidence Score: 5/5

The change is safe to merge — it adds new UI-only sections behind a wallet-connection gate with no mutations or contract interactions.

All new components are well-scoped, have accompanying tests, and the previously raised issues (NaN progress bar, missing click handler) are resolved. The two remaining notes are quality/style nits on placeholder code that will be replaced when the API hook lands.

useGetPrimeTotalRewards/index.ts and useGetPrimeUserRewards/index.ts — both import their output types from the UI components they feed, which is worth cleaning up before the real API hook lands.

Important Files Changed

Filename Overview
apps/evm/src/pages/PrimeLeaderboard/useGetPrimeTotalRewards/index.ts New placeholder hook for total Prime rewards; imports its output type from the UI component rather than defining it — inverted dependency direction.
apps/evm/src/pages/PrimeLeaderboard/useGetPrimeUserRewards/index.ts New placeholder hook for user Prime rewards; same inverted type dependency as useGetPrimeTotalRewards (imports UserMarketReward from UserRewardsCard).
apps/evm/src/pages/PrimeLeaderboard/UserRewardsCard/index.tsx Implements the user Prime rewards card with per-market rows and APY badges; uses a raw hex border colour (#805c4e) that violates the design-system token rule.
apps/evm/src/pages/PrimeLeaderboard/TotalRewardsCard/index.tsx Replaces the stub card with a real layout showing total cycle rewards and per-market breakdown; clean implementation.
apps/evm/src/pages/PrimeLeaderboard/MarketActions/index.tsx New component that looks up the vToken for a given token across pools and opens the OperationForm modal on click; previously flagged missing onClick is now implemented.
apps/evm/src/pages/PrimeLeaderboard/MarketRewardRow/index.tsx Shared row component for both cards; zero-guard for totalRewardsCents is in place (addresses previous review comment).
apps/evm/src/pages/PrimeLeaderboard/index.tsx Wires TotalRewardsSection/UserRewardsSection into the page, correctly gating UserRewardsSection behind a connected wallet.

Reviews (4): Last reviewed commit: "feat: support market modal in prime lead..." | Re-trigger Greptile

<div className="h-1.5 w-18 overflow-hidden rounded-full bg-lightGrey">
<div
className="h-full rounded-full bg-green"
style={{ width: `${Math.min(100, (rewardsCents / totalRewardsCents) * 100)}%` }}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 When totalRewardsCents is 0, the expression rewardsCents / totalRewardsCents produces either Infinity (when rewardsCents > 0) or NaN (when both are 0). Math.min(100, Infinity) resolves correctly to 100, but Math.min(100, NaN) returns NaN, making the inline style width: NaN% — an invalid CSS value that the browser silently ignores, leaving the bar collapsed.

Suggested change
style={{ width: `${Math.min(100, (rewardsCents / totalRewardsCents) * 100)}%` }}
style={{
width: `${totalRewardsCents > 0 ? Math.min(100, (rewardsCents / totalRewardsCents) * 100) : 0}%`,
}}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — added a zero-total guard so the bar renders at 0% instead of producing a NaN width.

Comment on lines +21 to +23
const marketRewards = tokens
.slice(0, placeholderMarketRewardsCents.length)
.map((token, index) => ({ token, rewardsCents: placeholderMarketRewardsCents[index] }));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Placeholder tokens are unrelated to Prime markets

useGetTokens() returns all tokens registered in the app's token list, and .slice(0, 2) picks whichever two happen to be first — typically generic tokens like BTC/ETH that have nothing to do with Prime reward markets. The card will render the wrong token icons and symbols paired with arbitrary cent values, actively misleading users. Even as temporary placeholder data, pairing hardcoded amounts with arbitrary tokens is likely to produce a confusing UI on any real environment. Consider substituting a static fallback (e.g. hardcoded token stubs or an empty array) until the real API hook is wired up.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are placeholder tokens until the API hook lands; added a TODO noting they're unrelated to the real Prime markets and will be replaced.

@github-actions

github-actions Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Coverage Report for ./apps/evm

Status Category Percentage Covered / Total
🔵 Lines 81.64% 47998 / 58788
🔵 Statements 81.64% 47998 / 58788
🔵 Functions 62.66% 668 / 1066
🔵 Branches 72.96% 5455 / 7476
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
apps/evm/src/components/Icon/icons/dotShortcut.tsx 100% 50% 100% 100%
apps/evm/src/components/Icon/icons/index.ts 99.02% 0% 100% 99.02% 1
apps/evm/src/components/Icon/icons/sparkle.tsx 21.42% 100% 0% 21.42% 5-16
apps/evm/src/pages/PrimeLeaderboard/index.tsx 100% 60% 100% 100%
apps/evm/src/pages/PrimeLeaderboard/MarketActions/index.tsx 100% 85.71% 33.33% 100%
apps/evm/src/pages/PrimeLeaderboard/MarketRewardRow/index.tsx 100% 0% 100% 100%
apps/evm/src/pages/PrimeLeaderboard/TotalRewardsCard/index.tsx 100% 0% 100% 100%
apps/evm/src/pages/PrimeLeaderboard/TotalRewardsSection/index.tsx 100% 0% 100% 100%
apps/evm/src/pages/PrimeLeaderboard/UserRewardsCard/index.tsx 100% 83.33% 100% 100%
apps/evm/src/pages/PrimeLeaderboard/UserRewardsSection/index.tsx 58.33% 0% 100% 58.33% 25-44
apps/evm/src/pages/PrimeLeaderboard/useGetPrimeTotalRewards/index.ts 92.3% 50% 100% 92.3% 1
apps/evm/src/pages/PrimeLeaderboard/useGetPrimeUserRewards/index.ts 94.44% 50% 100% 94.44% 1
Generated in workflow #13655 for commit 6eef450 by the Vitest Coverage Report Action

@cuzz-venus

Copy link
Copy Markdown
Contributor Author

@greptile review again

@cuzz-venus

Copy link
Copy Markdown
Contributor Author

@greptile review again

@cuzz-venus cuzz-venus force-pushed the feat/prime-leaderboard-table branch from e6cf09b to 7b2c2d9 Compare June 16, 2026 10:01
@cuzz-venus

Copy link
Copy Markdown
Contributor Author

@greptile review again

Drive the user rewards card headline by Prime eligibility (rewards amount,
eligible-to-supply prompt, or not-eligible prompt) and render per-market
Prime APYs with the shared Apy component.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Comment on lines +18 to +29
const { data: getPoolsData } = useGetPools({ accountAddress });
const [isModalOpen, setIsModalOpen] = useState(false);

const market = getPoolsData?.pools
.flatMap(pool =>
pool.assets.map(asset => ({ asset, poolComptrollerAddress: pool.comptrollerAddress })),
)
.find(({ asset }) => areAddressesEqual(asset.vToken.underlyingToken.address, token.address));

if (!market) {
return undefined;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All you need from the market is poolComptrollerAddress and vToken. Since you already have this information from the parent that renders the modal, you could just pass them through props instead of having to go through all pools and assets.

Comment on lines +29 to +31
<div className="ml-1 h-1.5 w-1/4 shrink-0 overflow-hidden rounded-full bg-lightGrey">
<div className="h-full rounded-full bg-green" style={{ width: `${progressPercentage}%` }} />
</div>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment on lines +61 to +66
{marketRewards.map(({ token, rewardsCents }) => {
const asset = getPoolsData?.pools
.flatMap(pool => pool.assets)
.find(poolAsset =>
areAddressesEqual(poolAsset.vToken.underlyingToken.address, token.address),
);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm generally not a fan of having inline logic inside the return statement, because it tends to complicate the code.

In this case, the parent that renders this component already has access to the asset item you're looking for here, so you could just pass it through props.

{content ?? (
<div className="flex items-center gap-x-3">
<span className="flex size-10 shrink-0 items-center justify-center rounded-lg border border-[#805c4e]">
<img src={primeLogoSrc} alt="" className="h-5" />

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's add content for this alt property

<div className="flex items-center gap-x-3">
{isPrime ? (
<span className="flex size-10 shrink-0 items-center justify-center rounded-lg border border-[#805c4e]">
<img src={primeLogoSrc} alt="" className="h-5" />

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's add content for this alt property

token: Token;
}

export const MarketActions: React.FC<MarketActionsProps> = ({ token }) => {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd recommend using a more explicit name for this. Maybe MarketActionsButton or something that makes it obvious we're rendering a CTA (otherwise the plural name could give the impression we're rendering a list of elements).

import { Icon, Modal } from 'components';
import { useTranslation } from 'libs/translations';
import { useAccountAddress } from 'libs/wallet';
import { OperationForm } from 'pages/Market/OperationForm';

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OperationForm should be moved to the containers directory, since it's now shared by two pages.

Comment on lines +42 to +54
{isModalOpen && (
<Modal
isOpen
title={market.asset.vToken.underlyingToken.symbol}
handleClose={() => setIsModalOpen(false)}
>
<OperationForm
vToken={market.asset.vToken}
poolComptrollerAddress={market.poolComptrollerAddress}
onSubmitSuccess={() => setIsModalOpen(false)}
/>
</Modal>
)}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I recommend creating a MarketFormModal component (you can name it whatever you want) inside the containers directory to render this DOM, so that it can be shared with the MarketTable component (https://github.com/VenusProtocol/venus-protocol-interface/blob/088de2b0451f8a4ef5364cbaa7e3d2133b4d9bc8/apps/evm/src/containers/MarketTable/index.tsx).

This will allow keeping one source of truth and avoid issues such as one we have here which is that if the market has protection mode enabled, it won't be displayed in the title (unlike the modal used inside the MarketTable component):
Image

"userRewards": {
"eligibleMessage": "You are currently eligible for sharing the Prime rewards during this cycle. Supply assets below to share the rewards.",
"marketActions": "Open market actions",
"notEligibleMessage": "You are currently NOT eligible for sharing the Prime rewards during this cycle. Stake XVS to compete for Prime for next cycle.",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This content wasn't proofread it seems:

Suggested change
"notEligibleMessage": "You are currently NOT eligible for sharing the Prime rewards during this cycle. Stake XVS to compete for Prime for next cycle.",
"notEligibleMessage": "You are currently NOT eligible for sharing the Prime rewards during this cycle. Stake XVS to compete for Prime in the next cycle.",

export { default as lightning2 } from './lightning2';
export { default as graph } from './graph';
export { default as star } from './star';
export { default as sparkle } from './sparkle';

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This icon isn't used anywhere.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think about moving this component to the containers directory and reusing it in the MarketTable component (https://github.com/VenusProtocol/venus-protocol-interface/blob/088de2b0451f8a4ef5364cbaa7e3d2133b4d9bc8/apps/evm/src/containers/MarketTable/index.tsx) which renders the same UI using the renderRowControl property?

<button
type="button"
aria-label={t('primeLeaderboard.userRewards.marketActions')}
className="ml-2 shrink-0"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To make it a bit more lively:

Suggested change
className="ml-2 shrink-0"
className="ml-2 shrink-0 transition-colors cursor-pointer hover:text-white"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants