feat: cSSV Syndicate Boost banner — API-gated eligibility - #142
feat: cSSV Syndicate Boost banner — API-gated eligibility#142jordanssv wants to merge 3 commits into
Conversation
… allowlist
The Genesis campaign banner previously determined eligibility ("OG") from a
bundled 7.4k-address CSV allowlist and applied OG/New-Holder boost tiers with a
50-SSV floor. The campaign now keys eligibility off the June 5 LST snapshot,
exposed via the SSV API.
- Add useGenesisEligibility: queries
{ssvApiBaseUrl}/apr/lst-snapshot/eligible/{address} once per wallet on connect
(react-query, staleTime/gcTime Infinity, no refetch triggers).
- Rewrite calculateBoost to the new single tier set (20/15/10/0%, no floor),
gated on eligibility; ineligible or zero-stake wallets get 0%. Kept in
lockstep with the backend getBoost().
- Banner: replace OG/New-Holder TYPE panel with an Eligible/Not eligible STATUS
panel, update headline copy, retain the boostWalletAddress QA override
(now forces eligibility).
- Remove genesis-allowlist.ts and the 7,472-line allowlist CSV.
- Add genesis-boost unit tests covering tier boundaries and the eligibility gate.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Update user-facing campaign name and links from "Genesis" to "Syndicate": - Banner title "cSSV Genesis Boost" -> "cSSV Syndicate Boost" - Learn-more link -> https://ssv.network/cssv/syndicate - T&C link/label -> cSSV Syndicate Boost Campaign Terms (terms-and-conditions-for-cssv-syndicate-boost-campaign) - Align internal code comments to the new name Internal symbols (GenesisCampaignBanner, VITE_GENESIS_CAMPAIGN_ACTIVE, genesis-* filenames) left unchanged; the staking-header calculator link (/cssv#calculator) is unrelated to the campaign and left as-is. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The campaign text and the right-hand STATUS/BOOST panel were each absolutely positioned independently, with the text using whitespace-nowrap and no right boundary — so the longer Syndicate copy ran underneath the panel. Replace the two free-floating blocks with a single flex row: the text takes the remaining space (flex-1 min-w-0, wraps as needed) and the panel is flex-none, so they can no longer overlap at any width. Decorative layers are unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
sebastian-ssvlabs
left a comment
There was a problem hiding this comment.
Review — Approved ✅
Reworks the staking banner to resolve cSSV Syndicate Boost eligibility from the June 5 LST snapshot API instead of a bundled 7,472-line allowlist CSV. Banner stays behind VITE_GENESIS_CAMPAIGN_ACTIVE (off in stage/prod), so no live behavior change.
Correctness
calculateBoostis clean: ineligible or non-positive stake →0%; otherwise first tier whose inclusive upper bound covers the stake. Boundary tests (5,000 / 12,000 / 20,000 and the.01rollovers) confirm the tiers match the documented backendgetBoost(). ✅useGenesisEligibilitynormalizes the address, fires once per wallet (staleTime/gcTime: Infinity, all refetch triggers off), and degrades safely — API error/unknown chain →isEligible: falserather than throwing (getNetworkConfigByChainIdfalls back toDEFAULT_NETWORK). ✅- Layout refactor to a single flex row (
flex-1 min-w-0text +flex-nonepanel) correctly removes the overlap risk; typecheck/build pass.
Non-blocking nits (optional, no need to address before merge)
- File/hook/queryKey still carry the
genesisname (use-genesis-eligibility,genesis-boost,"genesis-eligibility") while user-facing copy moved to Syndicate. Harmless, but a future rename would keep things consistent. fetch(url, { cache: "no-store" })is slightly redundant alongside the Infinity react-query cache — no impact.
Risk: Low — feature-flagged off; deletion of the allowlist CSV is the bulk of the diff. Confirm the noted follow-up (mainnet snapshot block 25251631 is final) before flipping the flag.
| address: string | ||
| ): Promise<boolean> { | ||
| const baseUrl = getNetworkConfigByChainId(chainId).ssvApiBaseUrl; | ||
| const url = `${baseUrl}/apr/lst-snapshot/eligible/${address}`; |
There was a problem hiding this comment.
Blocking: this path appears not to be exposed by the current gateway. ssvApiBaseUrl resolves to /api/v4/{network}, so this calls /api/v4/{network}/apr/lst-snapshot/eligible/:address. The current ssv-api APR gateway only proxies /apr/current, /apr/latest, /apr/history, and /apr/snapshots/:ownerAddress, while the APR service LST controller is mounted at /api/lst-snapshot/eligible/:walletAddress without /apr. Unless the gateway route ships first, this will 404 and mark every wallet as ineligible.
| : isEligible | ||
| ? "Eligible" | ||
| : "Not eligible"; | ||
| const boostValue = isEligibilityLoading |
There was a problem hiding this comment.
Nit/QA: the override says it forces eligibility, but isEligibilityLoading still controls the label and boost value. A matching boostWalletAddress will still show .../- until the API returns, and the request still fires. If this is meant to be a hard QA override, please treat forcedEligible as non-loading and consider disabling the eligibility query when it matches.
| }); | ||
|
|
||
| return { | ||
| isEligible: data ?? false, |
There was a problem hiding this comment.
Blocking: failures are currently indistinguishable from an ineligible wallet. A non-OK response throws, but the hook returns isEligible: data ?? false; the banner ignores isError and shows Not eligible/0%. With staleTime/gcTime: Infinity and refetch triggers disabled, a transient 404/500/network failure can stick for the session. Please surface a separate unable-to-verify state and allow retry/refetch instead of caching false eligibility.
| </div> | ||
| {/* Content row — text shrinks/wraps to fit; the right panel stays fixed | ||
| width so the two can never overlap. */} | ||
| <div className="absolute inset-0 flex items-center gap-3 pl-[106px] pr-[8px]"> |
There was a problem hiding this comment.
Risk: this fixes overlap by allowing text wrapping, but the parent banner still has a fixed h-[62px] overflow-hidden. On narrower widths, the title plus wrapped copy plus fixed-width panel can exceed 62px and get clipped. Please add a responsive taller state/layout or shorter mobile copy before launch.
Summary
Reworks the staking-page campaign banner for the cSSV Syndicate Boost program. Eligibility is now resolved from the June 5 LST snapshot via the SSV API instead of a bundled hardcoded allowlist, the boost tiers are updated, and the campaign is renamed from "Genesis" to "Syndicate".
The banner remains behind
VITE_GENESIS_CAMPAIGN_ACTIVE(off in stage/prod), so this does not change live behaviour until the flag is flipped.Changes
useGenesisEligibilityhook queries{ssvApiBaseUrl}/apr/lst-snapshot/eligible/{address}exactly once per wallet (react-query,staleTime/gcTime: Infinity, no refetch triggers).getBoost():TYPEpanel →STATUS(Eligible / Not eligible); headline copy updated; retained theboostWalletAddresslocalStorage QA override (now forces eligibility).ssv.network/cssv/syndicate, T&C link/label).flex-1 min-w-0wraps; panelflex-none) so the longer copy can no longer overlap the panel.genesis-allowlist.tsand the 7,472-line allowlist CSV.genesis-boostunit tests covering tier boundaries and the eligibility gate.Verification
pnpm typecheck— clean ✅pnpm test— 55 passed (6 new) ✅pnpm build— succeeds ✅pnpm lintis not runnable on this branch —@typescript-eslintisn't installed andstagehas the same gap, so it's pre-existing and unrelated to these changes.Follow-ups (not in this PR)
VITE_GENESIS_CAMPAIGN_ACTIVE=truein the mainnet env when marketing is ready to launch.25251631) is final.🤖 Generated with Claude Code