feat: soft-delete (active flag) for staking pools; retire r3to & Stakin by The Tie#209
Merged
Conversation
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
approved these changes
Jun 9, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Context (DEVOPS-365)
Removing a pool from
stakingPoolsConfigForChainIdis 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 tounstake()/claim(). This PR adds a reusable soft-delete: an optionalactive?: booleanonStakingPoolDefinition(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: addactive?: boolean(JSDoc); export pure helpersisStakingPoolActiveandisPoolVisibleInStakeSelector.Hide from the selector, keep the exit path
src/contexts/stakingPoolsStorage.tsx: derive + exposeactiveStakingPoolsData(memoized) fromcombinedStakingPoolsDatausing 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 consumesactiveStakingPoolsData.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: defensivecanStake=falseguard 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 instake/stakeReward.In-app retirement notice
src/misc/stakingPoolsConfig.ts: optionalretirementNotice?: string+ helpergetPoolRetirementNotice/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
0x2b5e…cc3F) and Stakin by The Tie (0xba66…1Ec8) set toactive: false(relates to DEVOPS-367).Tooling & docs
src/misc/__tests__/stakingPoolsConfig.test.ts);test/test:watchscripts.docker-compose.yml: localdev(mocked, hot reload) andprod(mainnet, standalone build) profiles;ENV_FILEoverride.images/frontend/Dockerfile: installsharpin the standalone runner sonext/imageoptimization 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 (Otterscanots_index +getDelegatedAmount, cross-checked vsgetDelegatedTotal) — useful when retiring a pool.Verification
active:falsehidden when no bonded stake; retired + bonded stays visible; merge paths keep retired pools; retirement notice (active → none, retired → default, retired → custom).recoverPoolDelegators.tsverified against r3to: recovers the 4 current delegators (100/100/100/50 = 350 ZIL =getDelegatedTotal).tsc --noEmitadds no new errors (13 pre-existing*.svgerrors onmainare unrelated);eslintclean; both compose profiles serve:3000.