Skip to content

Add contract ABIs for QuantaPool frontend#21

Merged
moscowchill merged 5 commits into
devfrom
claude/quantapool-frontend-design-4xh69j
Jun 10, 2026
Merged

Add contract ABIs for QuantaPool frontend#21
moscowchill merged 5 commits into
devfrom
claude/quantapool-frontend-design-4xh69j

Conversation

@moscowchill

@moscowchill moscowchill commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds frontend/ — a staking web app for QuantaPool (merged into dev).

Stake QRL → receive stQRL, request/claim/cancel withdrawals, and track pool + validator stats. Built to match the MyQRLWallet design system: Vite 7, React 19, TypeScript, MobX, TailwindCSS 4, Radix primitives.

What's included

  • Pages: Stake (home), Withdrawals (request → 128-block delay → FIFO claim, cancel), Stats, How It Works
  • Contract ABIs as TS modules for DepositPoolV2, StQRLV2, ValidatorManager (generated from contracts/solidity; regen instructions in frontend/README.md)
  • Wallet connectivity: QRL Wallet extension via EIP-6963 discovery (theqrl.org rdns) using qrl_requestAccounts / qrl_sendTransaction — same flow as myqrlwallet-frontend. MyQRLWallet mobile WebView detected via User-Agent; structured so the myqrlwallet-connect SDK can slot in later.
  • Per-account activity feed sourced from DepositPool events via the zondscan explorer API
  • Config: testnet v2.2 defaults baked in (src/config/networks.ts mirrors config/testnet-hyperion.json); all endpoints/addresses overridable via VITE_* env vars

Review fixes applied in-branch

  • Bounded withdrawal-history fetch + time rounding (dea9915)
  • zondscan explorer API wiring + earlier code-review fixes (9a269fd)

Testing

  • npm run build (type-check + production build) and npm run lint (zero-warnings) green
  • Exercised against testnet v2.2 contracts via the qrlwallet.com RPC proxy

claude added 4 commits June 9, 2026 21:27
Minimal Lido-style web app for the liquid staking protocol, built on the
MyQRLWallet design system (Vite 7, React 19, TypeScript, MobX, Tailwind 4,
Radix primitives, dark theme + QRL orange).

- Stake page: deposit QRL -> stQRL with live preview, position card, FAQ
- Withdrawals page: request (128-block delay), claim (FIFO), cancel
- Stats page: pool, validator funding progress, rewards, contract links
- How-it-works page: plain-language user documentation
- MobX poolStore: read-only RPC + EIP-6963 QRL Wallet extension signing
  (qrl_requestAccounts / qrl_sendTransaction), receipt polling for 60s blocks
- Testnet contract addresses default from config/testnet-hyperion.json
Correctness (from 7-angle review):
- Show the exact snapshot payout (withdrawalRequests.qrlAmount) for pending
  withdrawals instead of the drifting current share value — claimWithdrawal
  pays the snapshot, not the current rate
- Disable withdrawal requests while the contract is paused (requestWithdrawal
  is whenNotPaused; previously only deposits were gated)
- Guard refreshAccount against resurrecting state after disconnect/account
  switch mid-fetch
- Move parseUnits inside the tx pipeline so invalid amounts surface as a
  failed tx instead of an unhandled rejection
- Serialize transactions at the store level (single pending tx slot)
- Handle provider accountsChanged events (switch account or disconnect)

Efficiency / cleanup:
- Cache contract instances; cache immutable (claimed/cancelled) withdrawal
  requests so the 30s poll stops refetching history
- Skip background refresh while the tab is hidden; receipt polling 10s
- Guard init() against StrictMode double-invoke
- Replace hardcoded #4aafff with the blue-accent theme token everywhere
- stakeableBalance (gas reserve) moved from page into the store
- dismissConnectError store action instead of component-side mutation

Explorer integration:
- Fetch QRL/USD from zondscan /api/overview (same endpoint as myqrlwallet)
  and show USD values for TVL and the user's position
Stakers can now pull up their staking history in-app: Deposited,
WithdrawalRequested, WithdrawalClaimed and WithdrawalCancelled events
(all indexed by user) are merged into a newest-first activity card on
the Stake page, each entry linking to the transaction on zondscan, with
a direct 'View on Zondscan' link for the address (also added to the
header address chip). Degrades gracefully when the RPC proxy doesn't
expose log queries.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request introduces the frontend for the QuantaPool liquid staking protocol on the QRL network, including pages for staking, withdrawals, and stats, built with React, MobX, and TailwindCSS. The review feedback focuses on a critical performance optimization to fetch only active/pending withdrawal requests instead of the entire history, thereby preventing excessive RPC overhead and potential browser crashes. Additionally, a bug in the blocksToTime utility was identified and corrected to prevent awkward time formatting.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +562 to +579
const total = Number(asBig(counts.total));
const requests = await Promise.all(
Array.from({ length: total }, (_, id) =>
this.fetchWithdrawalRequest(pool, address, id),
),
);

runInAction(() => {
// The user may have disconnected or switched accounts while we were
// fetching — don't resurrect stale state.
if (!this.account || this.account.address !== address) return;
this.account = {
address,
qrlBalance: asBig(qrlBalance),
shares: asBig(shares),
lockedShares: asBig(lockedShares),
qrlValue: asBig(qrlValue),
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

Fetching all historical withdrawal requests from index 0 to total - 1 results in an $O(N)$ RPC call overhead. For active users with many historical requests, this will trigger hundreds or thousands of concurrent RPC requests, leading to rate-limiting or browser tab crashes. Since completed/cancelled requests are immutable, we should only fetch the active/pending requests from nextIndex (which is total - pending) to total - 1.

    const total = Number(asBig(counts.total));
    const pending = Number(asBig(counts.pending));
    const nextIndex = total - pending;
    const requests = await Promise.all(
      Array.from({ length: pending }, (_, i) =>
        this.fetchWithdrawalRequest(pool, address, nextIndex + i),
      )
    );

    runInAction(() => {
      // The user may have disconnected or switched accounts while we were
      // fetching — don't resurrect stale state.
      if (!this.account || this.account.address !== address) return;
      this.account = {
        address,
        qrlBalance: asBig(qrlBalance),
        shares: asBig(shares),
        lockedShares: asBig(lockedShares),
        qrlValue: asBig(qrlValue),
        completedWithdrawalsCount: nextIndex,
      };

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.

All five review items addressed in dea9915.

Bounded withdrawal fetch (this thread + the 3 related ones): refreshAccount now fetches only the live tail [nextIndex, total) where nextIndex = total - pending. Verified against the contract — getWithdrawalRequestCount returns pending = total - nextWithdrawalIndex, so nextIndex is exactly nextWithdrawalIndex, and everything below it is claimed/cancelled and immutable. The per-poll fan-out is now bounded by pending requests instead of full history. Added completedWithdrawalsCount (= nextIndex) to AccountState and switched the "N completed withdrawals" line to it, dropping the claimed filter over the (no-longer-fully-fetched) array.

blocksToTime: now rounds to whole minutes before splitting hours/minutes, so it can't render ≈ 60m or 1h 60m.

Build + lint are green. (As with PR #20, forge/runtime aren't reachable from my sandbox, but the frontend tsc build and ESLint pass.)


Generated by Claude Code

Comment thread frontend/src/pages/WithdrawalsPage.tsx Outdated
Comment on lines +55 to +57
const pending = poolStore.pendingWithdrawals;
const claimableCount = poolStore.claimableWithdrawals.length;
const claimed = poolStore.withdrawals.filter((w) => w.claimed);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

Remove the unused claimed filter since we will now use the optimized completedWithdrawalsCount from the account state to display the completed withdrawals count.

Suggested change
const pending = poolStore.pendingWithdrawals;
const claimableCount = poolStore.claimableWithdrawals.length;
const claimed = poolStore.withdrawals.filter((w) => w.claimed);
const pending = poolStore.pendingWithdrawals;
const claimableCount = poolStore.claimableWithdrawals.length;

Comment thread frontend/src/pages/WithdrawalsPage.tsx Outdated
Comment on lines +217 to +221
{claimed.length > 0 && (
<p className="text-center text-xs text-muted-foreground">
{claimed.length} completed withdrawal{claimed.length === 1 ? "" : "s"}
</p>
)}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

Use account.completedWithdrawalsCount instead of claimed.length to display the completed withdrawals count, avoiding the need to fetch all historical requests.

Suggested change
{claimed.length > 0 && (
<p className="text-center text-xs text-muted-foreground">
{claimed.length} completed withdrawal{claimed.length === 1 ? "" : "s"}
</p>
)}
{account.completedWithdrawalsCount > 0 && (
<p className="text-center text-xs text-muted-foreground">
{account.completedWithdrawalsCount} completed withdrawal{account.completedWithdrawalsCount === 1 ? "" : "s"}
</p>
)}

Comment on lines +34 to +43
export interface AccountState {
address: string;
qrlBalance: bigint;
/** stQRL share balance (stable; balanceOf semantics). */
shares: bigint;
/** Shares locked by pending withdrawal requests. */
lockedShares: bigint;
/** Current QRL value of all shares. */
qrlValue: bigint;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Add completedWithdrawalsCount to AccountState to keep track of the total number of completed/processed withdrawals without needing to fetch all historical requests via RPC.

Suggested change
export interface AccountState {
address: string;
qrlBalance: bigint;
/** stQRL share balance (stable; balanceOf semantics). */
shares: bigint;
/** Shares locked by pending withdrawal requests. */
lockedShares: bigint;
/** Current QRL value of all shares. */
qrlValue: bigint;
}
export interface AccountState {
address: string;
qrlBalance: bigint;
/** stQRL share balance (stable; balanceOf semantics). */
shares: bigint;
/** Shares locked by pending withdrawal requests. */
lockedShares: bigint;
/** Current QRL value of all shares. */
qrlValue: bigint;
completedWithdrawalsCount: number;
}

Comment on lines +71 to +78
export function blocksToTime(blocks: bigint | number, blockTimeSeconds: number): string {
const totalSeconds = Number(blocks) * blockTimeSeconds;
if (totalSeconds <= 0) return "now";
const hours = Math.floor(totalSeconds / 3600);
const minutes = Math.ceil((totalSeconds % 3600) / 60);
if (hours > 0) return `≈ ${hours}h ${minutes}m`;
return `≈ ${minutes}m`;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The current implementation of blocksToTime can display awkward values like ≈ 60m or ≈ 1h 60m due to rounding with Math.ceil on minutes independently of hours. Converting to total minutes first and then calculating hours and minutes prevents this issue.

Suggested change
export function blocksToTime(blocks: bigint | number, blockTimeSeconds: number): string {
const totalSeconds = Number(blocks) * blockTimeSeconds;
if (totalSeconds <= 0) return "now";
const hours = Math.floor(totalSeconds / 3600);
const minutes = Math.ceil((totalSeconds % 3600) / 60);
if (hours > 0) return `≈ ${hours}h ${minutes}m`;
return `≈ ${minutes}m`;
}
export function blocksToTime(blocks: bigint | number, blockTimeSeconds: number): string {
const totalSeconds = Number(blocks) * blockTimeSeconds;
if (totalSeconds <= 0) return "now";
const totalMinutes = Math.ceil(totalSeconds / 60);
const hours = Math.floor(totalMinutes / 60);
const minutes = totalMinutes % 60;
if (hours > 0) return `≈ ${hours}h ${minutes}m`;
return `≈ ${minutes}m`;
}

…unding

- refreshAccount now fetches only the live withdrawal tail [nextIndex, total)
  instead of every historical request. nextIndex = total - pending equals the
  contract's nextWithdrawalIndex, and indices below it are claimed/cancelled and
  immutable, so the per-poll RPC fan-out is bounded by pending requests rather
  than a user's entire withdrawal history (avoids hundreds of concurrent calls /
  rate-limiting for active accounts).
- track completedWithdrawalsCount (= nextIndex) on AccountState and use it for
  the 'N completed withdrawals' line, instead of filtering the (no-longer-fully-
  fetched) request array for claimed entries.
- blocksToTime: round to whole minutes before splitting hours/minutes so it can
  no longer render '≈ 60m' or '1h 60m'.
@moscowchill moscowchill changed the base branch from main to dev June 10, 2026 06:45
@moscowchill moscowchill merged commit 73c8f09 into dev Jun 10, 2026
2 checks passed
@moscowchill moscowchill deleted the claude/quantapool-frontend-design-4xh69j branch June 10, 2026 08:26
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