Skip to content

feat: soft-delete (active flag) for staking pools; retire r3to & Stakin by The Tie#209

Merged
frankmeds merged 11 commits into
mainfrom
feat/staking-pool-soft-delete
Jun 9, 2026
Merged

feat: soft-delete (active flag) for staking pools; retire r3to & Stakin by The Tie#209
frankmeds merged 11 commits into
mainfrom
feat/staking-pool-soft-delete

Conversation

@frankmeds

@frankmeds frankmeds commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Context (DEVOPS-365)

Removing a pool from stakingPoolsConfigForChainId is a hard delete: it stops the wallet data fetchers from ever querying that delegator contract, so any wallet with a still-bonded position permanently loses the UI to unstake() / claim(). This PR adds a reusable soft-delete: an optional active?: boolean on StakingPoolDefinition (undefined/true = active, false = retired).

A retired pool is hidden from "Available to stake" but every withdrawal / claim / reward path keeps working, so a delegator can always exit.

What changed

Schema + decision (single source of truth)

  • src/misc/stakingPoolsConfig.ts: add active?: boolean (JSDoc); export pure helpers isStakingPoolActive and isPoolVisibleInStakeSelector.

Hide from the selector, keep the exit path

  • src/contexts/stakingPoolsStorage.tsx: derive + expose activeStakingPoolsData (memoized) from combinedStakingPoolsData using the userData-aware helper — a retired pool stays visible only while the connected wallet still has a bonded stake. All unstake/claim/reward combiners keep using the unfiltered list.
  • src/components/stakingPoolsList.tsx: the "Available to stake" selector consumes activeStakingPoolsData.

Never stake / re-stake into a retired pool

  • src/components/stakingPoolDetailsView.tsx: omit the Stake tab (grid collapses to 2 cols) and default the pane to Unstake/Claim for retired pools.
  • src/components/stakingCalculator.tsx: defensive canStake=false guard with a "retired" tooltip.
  • src/components/withdrawUnstakedZilPanel.tsx, src/components/withdrawZilView.tsx: disable "Stake Reward" (re-stake) for retired pools while keeping "Claim Reward" enabled.
  • src/contexts/stakingOperations.tsx: defensive guards in stake / stakeReward.

In-app retirement notice

  • src/misc/stakingPoolsConfig.ts: optional retirementNotice?: string + helper getPoolRetirementNotice / DEFAULT_RETIREMENT_NOTICE (custom copy per pool, generic default otherwise).
  • src/components/stakingPoolCard.tsx: a "Retired" tag on the pool card in the list.
  • src/components/stakingPoolDetailsView.tsx: a notice banner above the tabs in the retired pool's details view.

Retirements applied

  • r3to (0x2b5e…cc3F) and Stakin by The Tie (0xba66…1Ec8) set to active: false (relates to DEVOPS-367).

Tooling & docs

  • Vitest + unit tests for the filtering and retirement-notice decisions (src/misc/__tests__/stakingPoolsConfig.test.ts); test / test:watch scripts.
  • docker-compose.yml: local dev (mocked, hot reload) and prod (mainnet, standalone build) profiles; ENV_FILE override.
  • images/frontend/Dockerfile: install sharp in the standalone runner so next/image optimization works in production (pre-existing gap surfaced by running the standalone build locally).
  • CLAUDE.md: repo orientation (architecture, commands, conventions, gotchas, runbooks).
  • src/script/recoverPoolDelegators.ts: recover the wallets currently delegating to a non-liquid pool (Otterscan ots_ index + getDelegatedAmount, cross-checked vs getDelegatedTotal) — useful when retiring a pool.

Verification

  • Unit tests (10): active default; active:false hidden when no bonded stake; retired + bonded stays visible; merge paths keep retired pools; retirement notice (active → none, retired → default, retired → custom).
  • E2E (Docker dev + prod profiles): retired pool hidden without a bonded stake; with a bonded wallet it reappears with Unstake/Claim only (no Stake); "Stake Reward" disabled, "Claim Reward" enabled; the "Retired" card tag and the details banner render. Confirmed against real mainnet data using a recovered r3to delegator address.
  • recoverPoolDelegators.ts verified against r3to: recovers the 4 current delegators (100/100/100/50 = 350 ZIL = getDelegatedTotal).
  • tsc --noEmit adds no new errors (13 pre-existing *.svg errors on main are unrelated); eslint clean; both compose profiles serve :3000.

frankmeds added 11 commits June 8, 2026 09:27
Add an optional active?: boolean to StakingPoolDefinition (undefined/true =>
active, false => retired) plus pure helpers isStakingPoolActive and
isPoolVisibleInStakeSelector that encode the selector visibility decision:
a retired pool is hidden from Available to stake unless the wallet still has
a bonded stake in it (so the unstake path survives).

Adds Vitest tooling and unit tests covering the filtering decision.

DEVOPS-365
Derive activeStakingPoolsData from combinedStakingPoolsData using
isPoolVisibleInStakeSelector and expose it from the container; switch the
stakingPoolsList selector to consume it. Retired pools disappear from
Available to stake unless the wallet has a bonded stake in them. All
unstake/claim/reward combiners keep using the unfiltered list.

DEVOPS-365
For pools with active: false the details view omits the Stake tab (grid
collapses to two columns) and defaults the pane to Unstake/Claim, so a
wallet with a bonded position can still unstake/claim but can never add new
stake. stakingCalculator gains a defensive canStake guard with a retired
tooltip as defense-in-depth. unstakingCalculator is unchanged.

DEVOPS-365
Add a root docker-compose.yml with a dev profile (node:20 + npm run dev,
hot reload, bind mount) and a prod profile (builds images/frontend/Dockerfile
standalone, runs node server.js). Both default to .env.mocked and publish
:3000. Env is injected at runtime via /api/config, so no per-env rebuild.

DEVOPS-365
Review (gemini + architecture) found two real issues:
- HIGH: the Stake Reward path could re-stake rewards into a retired pool,
  violating never-stake. Gate the Stake Reward button in
  withdrawUnstakedZilPanel and withdrawZilView (disabled + retired tooltip,
  Claim Reward stays enabled) and add a defensive guard in
  stakingOperations.stake / stakeReward via isStakingPoolActive.
- MAJOR: activeStakingPoolsData was recomputed each render, defeating the
  selector useMemo and re-shuffling pool order. Wrap it in useMemo.

Also: dedupe the details-view pane list, drop dead commented code in
stakingPoolsList, and allow ENV_FILE override of the compose env_file.

Verified e2e in the docker dev container: retired pool hidden without a
bonded stake; kept + Unstake-only (no Stake tab) with a bonded wallet;
Stake Reward disabled with retired tooltip while Claim Reward stays enabled.

DEVOPS-365
Soft-delete both pools: they disappear from Available to stake but any
wallet with a bonded position keeps the unstake/claim path (and cannot
stake or re-stake rewards). Relates to DEVOPS-367.
The prod profile now defaults to .env.mainnet (ZQ2_STAKING_CHAIN_ID=32769),
so `docker compose --profile prod up --build` renders the real mainnet
validator set (Binance, HTX, Avely, ...) read from api.zilliqa.com, instead
of the mock pools. dev stays on .env.mocked. Override either with ENV_FILE=.

Verified: retired r3to and Stakin by The Tie are hidden from Available to
stake while the other mainnet validators render.

DEVOPS-365
Next.js standalone production mode requires the native sharp library for
next/image optimization; it was absent in the runner image, so every
/_next/image request logged a 'sharp is required' error (non-fatal: Next
served the unoptimized original). Install only the musl sharp build into a
temp prefix and merge it into the standalone node_modules, leaving the
traced dependency tree intact.

Verified: sharp present (musl), /_next/image -> 200, zero sharp log errors.

DEVOPS-365
Add an optional retirementNotice?: string to StakingPoolDefinition plus
getPoolRetirementNotice / DEFAULT_RETIREMENT_NOTICE. Retired pools now show:
- a 'Retired' tag on the pool card in the list, and
- a notice banner above the tabs in the details view (custom copy if set,
  otherwise the default message).

Unit tests cover active -> null, retired -> default, retired -> custom.

DEVOPS-365
- CLAUDE.md: repo orientation for contributors / Claude Code (architecture,
  commands, conventions, gotchas, and runbooks).
- src/script/recoverPoolDelegators.ts: recover the wallets currently
  delegating to a non-liquid pool via the Otterscan ots_ index +
  getDelegatedAmount, cross-checked against getDelegatedTotal. Useful when
  retiring a pool (e.g. to notify delegators).
@chetan-zilliqa chetan-zilliqa self-requested a review June 9, 2026 07:50
@chetan-zilliqa chetan-zilliqa self-requested a review June 9, 2026 07:52
@frankmeds frankmeds merged commit 3e2a214 into main Jun 9, 2026
3 checks passed
@frankmeds frankmeds deleted the feat/staking-pool-soft-delete branch June 9, 2026 09:28
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