Skip to content

fix: base cancel-donation countdown on server-confirmed time - #1023

Open
Queen-T16 wants to merge 1 commit into
Iris-IV:mainfrom
Queen-T16:fix/811-donation-grace-period-clock-skew
Open

fix: base cancel-donation countdown on server-confirmed time#1023
Queen-T16 wants to merge 1 commit into
Iris-IV:mainfrom
Queen-T16:fix/811-donation-grace-period-clock-skew

Conversation

@Queen-T16

@Queen-T16 Queen-T16 commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Summary

Closes #811 — [Wallet] CancelDonationBanner countdown does not account for clock drift between client and server.

The grace-period countdown and expiry purge were previously anchored to the client's local Date.now(). If the user's system clock was off, the banner could keep showing time remaining even after the on-chain grace period had actually closed. This PR anchors both the countdown and the purge on a server-confirmed timestamp.

Approach

  • src/hooks/useDonationGracePeriod.ts
    • Added a small server-time sync layer that fetches the server's own timestamp from the existing GET /api/health endpoint (already server-confirmed) and derives a client <-> server clock offset (ms).
    • The offset is applied everywhere the hook reasons about "now":
      • startGracePeriod now sets timestamp/expiresAt from the server-adjusted clock.
      • The periodic expiry purge now filters against the server-adjusted clock, so skewed clients no longer keep expired donations around.
    • Graceful degradation: if the server is unreachable, returns an invalid timestamp, or the computed offset exceeds a generous 24h sanity bound, the hook falls back to the client clock (offset 0) — the app keeps working offline.
    • The server-time request is cached per session (getServerTimeMs), so the hook and banner share a single round-trip. resetServerTimeCache() is exported for tests / re-sync.
  • src/components/CancelDonationBanner.tsx
    • Uses the shared useServerTimeOffset() and computes remainingSeconds from expiresAt - (Date.now() + offsetMs), so the displayed countdown matches the server/on-chain deadline regardless of client clock skew.
  • Tests
    • src/__tests__/hooks/useDonationGracePeriod.test.tsx
    • src/__tests__/components/CancelDonationBanner.test.tsx

Acceptance criteria

  • Countdown based on a server-confirmed timestamp rather than raw Date.now().
  • Handles a significantly skewed client clock gracefully (corrected countdown, safe fallback when the server is unreachable).
  • Tests simulating a skewed client clock (client 5 minutes behind the server) for both the hook and the banner.

Test coverage

New tests simulate a client clock 5 minutes behind the server:

  • Hook: startGracePeriod stamps donations on server time (expiresAt = serverNow + 60s), not client time.
  • Hook: expired donations are purged on server time (not 5 minutes late).
  • Hook: falls back to the client clock when the server is unreachable / returns an invalid timestamp.
  • Banner: shows 60s remaining where a raw client clock would show 360s.
  • Banner: shows 0s (and never negative) once the server-confirmed window has closed.
  • Banner: falls back to the client clock when the server is unreachable.

All new tests pass. Lint is clean for the touched files (one pre-existing onFinalize unused-prop warning remains).

Out of scope

Implementing the broader "Cancel Donation grace period" feature end-to-end (tracked in #640) is intentionally left untouched.

Anchor the grace-period countdown and expiry purge on a server-confirmed
timestamp instead of the raw client clock. Derive a client<->server clock
offset from the /api/health endpoint and apply it in
useDonationGracePeriod (start/purge) and CancelDonationBanner (remaining
seconds), so a skewed system clock no longer shows time remaining after the
on-chain window has closed. Falls back to the client clock when the server
is unreachable.

Adds tests simulating a client clock 5 minutes behind the server for both
the hook and the banner.

Closes Iris-IV#811
@sshdopey

sshdopey commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Auto-review failed (API error). Leaving PR for human review.

@drips-wave

drips-wave Bot commented Aug 1, 2026

Copy link
Copy Markdown

@Queen-T16 Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@davidmaronio

Copy link
Copy Markdown
Contributor

well engineered fix: deriving the offset from /api/health's timestamp with a midpoint latency estimate, a shared cached promise so all consumers make one round-trip, a skew sanity bound, and graceful fallback to the client clock, plus thorough tests for skew, purge, unreachable server, and invalid timestamps. a couple of notes, non-blocking:

  1. src/hooks/useDonationGracePeriod.ts:38: /api/health also pings the soroban rpc with a 5s timeout and returns 503 when degraded. your fetch ignores response.ok (good, the timestamp is still valid on 503), but the sync can take up to the health route's rpc timeout. a lighter timestamp-only endpoint, or a comment noting the tradeoff, would help.
  2. same file: the offset is fetched once per session and never refreshed; fine for a 60s grace window, just worth a comment that resetServerTimeCache exists for re-sync.

gate: branch is in conflict with main; rebase and rerun CI, then good to merge.

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.

[Wallet] CancelDonationBanner countdown does not account for clock drift between client and server

3 participants