Skip to content

feat(governance,analytics): implement #469 #470 #471 #468 — API layer… - #565

Merged
james2177 merged 2 commits into
stellar-vortex-protocol:mainfrom
allhandghost:feat/468-469-470-471-governance-analytics
Sep 30, 2026
Merged

james2177 merged 2 commits into
stellar-vortex-protocol:mainfrom
allhandghost:feat/468-469-470-471-governance-analytics

Conversation

@allhandghost

Copy link
Copy Markdown

…, on-chain voting, voting power, market-share analytics

Issue #469 — Replace in-memory governance mock with API-backed data layer

Problem

src/lib/governanceStore.ts held hard-coded proposals and comments in module-level mutable state, meaning comments vanished on reload and were shared across all server renders. Components called the store synchronously which could not represent loading or error states.

What was done

  • Created src/lib/governanceApi.ts with a typed GovernanceApi interface (listProposals, getProposal, listComments, postComment).
  • Implemented HttpGovernanceApi: fetches from relay endpoints (GET /api/governance/proposals, GET /api/governance/proposals/:id, GET /api/governance/proposals/:id/comments, POST /api/governance/proposals/:id/comments). Validates all responses with runtime type guards; throws GovernanceValidationError on shape mismatches.
  • Implemented MockGovernanceApi: uses the same deterministic seed data as the old store but with per-instance isolated comment stores — no shared mutable module state between tests.
  • Adapter factory (createGovernanceApi) selects the implementation from NEXT_PUBLIC_GOVERNANCE_SOURCE ('mock'|'http'); production defaults to 'http' and throws at construction time if NEXT_PUBLIC_API_URL is unset.
  • Created src/lib/governanceApiContext.tsx: React context + provider so tests can inject fake adapters without monkey-patching modules.
  • Created src/hooks/useGovernance.ts with SWR hooks: useProposals(filters), useProposal(id), useProposalComments(id). useProposalComments includes optimistic comment posting with rollback via SWR mutate + rollbackOnError.
  • Migrated GovernancePageClient.tsx: removed direct governanceStore imports, replaced with useProposals(); added loading skeleton, error state, and empty state.
  • Added src/app/governance/loading.tsx: Next.js loading UI with animated skeletons while proposals fetch.
  • Added src/app/governance/error.tsx: Next.js error boundary with a 'Try again' reset button.
  • Added sanitizeText() and validateCommentText() to src/lib/textSafety.ts (previously missing despite being imported).

Issue #470 — On-chain voting: cast For/Against/Abstain with flow states

Problem

The governance page listed proposals but users could not vote. Governance without participation is just a dashboard.

What was done

  • Created src/hooks/useVote.ts implementing the full voting flow state machine: idle → building → reviewing → awaiting-signature → submitting → confirmed, with an 'error' escape state at each step.
  • Guards enforced before flow starts: wallet disconnected, wrong network (checked against NEXT_PUBLIC_NETWORK), zero voting power, deadline passed (with 30-second server-time tolerance), no choice selected.
  • Optimistic tally delta applied immediately after the user confirms, flagged as 'pending: true' until the transaction is confirmed. On failure the delta is rolled back.
  • Double-vote handling: after confirmation confirmedChoice is set and the SWR cache for the proposal is invalidated to fetch authoritative tallies.
  • VoteChoice typed as 'for' | 'against' | 'abstain'; VoteErrorKind discriminates: wallet-disconnected, wrong-network, zero-power, deadline-passed, already-voted, user-rejected, network, generic.
  • classifyVoteError maps raw Error messages to VoteErrorKind.
  • Created src/components/VotePanel.tsx: radio group (For/Against/Abstain with ▲/▼ accessible labels), tally progress bar with optimistic overlay, finality copy ('Votes are final'), multi-step status label, submit button with aria-busy, error dismiss control.
  • VotePanel integrated into ProposalDetailClient: shown only for active proposals, gated behind wallet connect for inactive wallet state.

Issue #471 — Voting-power hydration with snapshot semantics

Problem

Users needed to know before voting whether they could vote and with what weight. Snapshot semantics (power frozen at proposal creation) are a common source of confusion.

What was done

  • Created src/hooks/useVotingPower.ts: useVotingPower(address, proposalId) → { power, isLoading, error, canVote } Power shape: { own, delegatedIn, delegatedOut, total, snapshotAt }. SWR cache keyed by [address, proposalId]; a wallet address change automatically invalidates the cache (different key). Adapter duck-typed: if the GovernanceApi instance exposes getVotingPower it is called; otherwise a graceful zero-power fallback is returned so the page renders without blocking.
  • formatVotingPower(amount) helper: uses compact notation for values ≥ 1,000 (e.g. '125K') with the full precise value on title/aria-label for hover/focus (WCAG accessible).
  • Created src/components/VotingPowerPanel.tsx: shows total weight, own / delegatedIn / delegatedOut breakdown (hidden when all zero), snapshot date tooltip explaining semantics, and a 0-power guidance block explaining how to acquire tokens or request delegation.
  • VotingPowerPanel integrated into ProposalDetailClient sidebar, visible when wallet is connected.

Issue #468 — Market-share and top-movers analytics

Problem

Current analytics only showed point-in-time snapshots. Product and community teams needed to see shifts (which chain is growing, whether one solver is becoming dominant) for governance and growth decisions.

What was done

  • Added computeShareSeries(intents, dimension) to src/lib/analytics.ts: Buckets intents by day; identifies top-N (5) entities by total volume; collapses remainder into 'Other' (deterministic). For each day computes per-entity share as a % of that day's total volume (zero-volume days produce 0 share — never NaN). Returns { entities, series, colors }.
  • Added computeConcentration(intents, dimension) to analytics.ts: Computes Herfindahl–Hirschman Index (HHI = Σ share_i²) scaled 0–10,000. Returns hhi, top1SharePct, top3SharePct, level (competitive/moderate/ concentrated), and ranked entity list.
  • Added computeTopMovers(intents, dimension, windowDays=7): compares share in the last N days vs the preceding N-day window; returns entities ranked by absolute delta with ▲/▼ accessible labels.
  • Chain colours sourced from CHAINS registry; solver/token colours use an 8-colour accessible fallback palette; 'Other' always maps to gray.
  • Added StackedAreaChart to AnalyticsPageClient: SVG stacked area with 100%-normalised toggle, per-entity fill opacity, grid lines, and legend.
  • Added TopMoversPanel: entity, current share %, delta in percentage points with ▲/▼ accessible aria-labels.
  • Added ConcentrationIndicator: HHI badge in a Tooltip explaining the index, top-1/top-3 share cards, top-5 entity mini-list.
  • Added dimension selector (Source Chain / Dest Token / Solver) and normalise toggle in AnalyticsPageClient controlling all three new panels.

Closes #469
Closes #470
Closes #471
Closes #468

Summary

Closes #

Changes

Testing & QA

  • npm run check:editorconfig passes
  • npm run typecheck (tsc --noEmit) passes cleanly
  • npm test and npm run test:coverage pass
  • npm run build passes
  • Verified on local / staging environment with seeded data (where applicable)

Preview

…llar-vortex-protocol#470 stellar-vortex-protocol#471 stellar-vortex-protocol#468 — API layer, on-chain voting, voting power, market-share analytics

## Issue stellar-vortex-protocol#469 — Replace in-memory governance mock with API-backed data layer

### Problem
src/lib/governanceStore.ts held hard-coded proposals and comments in
module-level mutable state, meaning comments vanished on reload and were
shared across all server renders. Components called the store synchronously
which could not represent loading or error states.

### What was done
- Created src/lib/governanceApi.ts with a typed GovernanceApi interface
  (listProposals, getProposal, listComments, postComment).
- Implemented HttpGovernanceApi: fetches from relay endpoints
  (GET /api/governance/proposals, GET /api/governance/proposals/:id,
  GET /api/governance/proposals/:id/comments,
  POST /api/governance/proposals/:id/comments).
  Validates all responses with runtime type guards; throws
  GovernanceValidationError on shape mismatches.
- Implemented MockGovernanceApi: uses the same deterministic seed data as
  the old store but with per-instance isolated comment stores — no shared
  mutable module state between tests.
- Adapter factory (createGovernanceApi) selects the implementation from
  NEXT_PUBLIC_GOVERNANCE_SOURCE ('mock'|'http'); production defaults to
  'http' and throws at construction time if NEXT_PUBLIC_API_URL is unset.
- Created src/lib/governanceApiContext.tsx: React context + provider so
  tests can inject fake adapters without monkey-patching modules.
- Created src/hooks/useGovernance.ts with SWR hooks:
  useProposals(filters), useProposal(id), useProposalComments(id).
  useProposalComments includes optimistic comment posting with rollback
  via SWR mutate + rollbackOnError.
- Migrated GovernancePageClient.tsx: removed direct governanceStore
  imports, replaced with useProposals(); added loading skeleton, error
  state, and empty state.
- Added src/app/governance/loading.tsx: Next.js loading UI with animated
  skeletons while proposals fetch.
- Added src/app/governance/error.tsx: Next.js error boundary with a
  'Try again' reset button.
- Added sanitizeText() and validateCommentText() to
  src/lib/textSafety.ts (previously missing despite being imported).

## Issue stellar-vortex-protocol#470 — On-chain voting: cast For/Against/Abstain with flow states

### Problem
The governance page listed proposals but users could not vote. Governance
without participation is just a dashboard.

### What was done
- Created src/hooks/useVote.ts implementing the full voting flow state
  machine: idle → building → reviewing → awaiting-signature → submitting
  → confirmed, with an 'error' escape state at each step.
- Guards enforced before flow starts: wallet disconnected, wrong network
  (checked against NEXT_PUBLIC_NETWORK), zero voting power, deadline
  passed (with 30-second server-time tolerance), no choice selected.
- Optimistic tally delta applied immediately after the user confirms,
  flagged as 'pending: true' until the transaction is confirmed. On
  failure the delta is rolled back.
- Double-vote handling: after confirmation confirmedChoice is set and the
  SWR cache for the proposal is invalidated to fetch authoritative tallies.
- VoteChoice typed as 'for' | 'against' | 'abstain'; VoteErrorKind
  discriminates: wallet-disconnected, wrong-network, zero-power,
  deadline-passed, already-voted, user-rejected, network, generic.
- classifyVoteError maps raw Error messages to VoteErrorKind.
- Created src/components/VotePanel.tsx: radio group (For/Against/Abstain
  with ▲/▼ accessible labels), tally progress bar with optimistic overlay,
  finality copy ('Votes are final'), multi-step status label, submit button
  with aria-busy, error dismiss control.
- VotePanel integrated into ProposalDetailClient: shown only for active
  proposals, gated behind wallet connect for inactive wallet state.

## Issue stellar-vortex-protocol#471 — Voting-power hydration with snapshot semantics

### Problem
Users needed to know before voting whether they could vote and with what
weight. Snapshot semantics (power frozen at proposal creation) are a
common source of confusion.

### What was done
- Created src/hooks/useVotingPower.ts:
  useVotingPower(address, proposalId) → { power, isLoading, error, canVote }
  Power shape: { own, delegatedIn, delegatedOut, total, snapshotAt }.
  SWR cache keyed by [address, proposalId]; a wallet address change
  automatically invalidates the cache (different key).
  Adapter duck-typed: if the GovernanceApi instance exposes getVotingPower
  it is called; otherwise a graceful zero-power fallback is returned so the
  page renders without blocking.
- formatVotingPower(amount) helper: uses compact notation for values
  ≥ 1,000 (e.g. '125K') with the full precise value on title/aria-label
  for hover/focus (WCAG accessible).
- Created src/components/VotingPowerPanel.tsx: shows total weight, own /
  delegatedIn / delegatedOut breakdown (hidden when all zero), snapshot
  date tooltip explaining semantics, and a 0-power guidance block
  explaining how to acquire tokens or request delegation.
- VotingPowerPanel integrated into ProposalDetailClient sidebar, visible
  when wallet is connected.

## Issue stellar-vortex-protocol#468 — Market-share and top-movers analytics

### Problem
Current analytics only showed point-in-time snapshots. Product and
community teams needed to see shifts (which chain is growing, whether
one solver is becoming dominant) for governance and growth decisions.

### What was done
- Added computeShareSeries(intents, dimension) to src/lib/analytics.ts:
  Buckets intents by day; identifies top-N (5) entities by total volume;
  collapses remainder into 'Other' (deterministic). For each day computes
  per-entity share as a % of that day's total volume (zero-volume days
  produce 0 share — never NaN). Returns { entities, series, colors }.
- Added computeConcentration(intents, dimension) to analytics.ts:
  Computes Herfindahl–Hirschman Index (HHI = Σ share_i²) scaled 0–10,000.
  Returns hhi, top1SharePct, top3SharePct, level (competitive/moderate/
  concentrated), and ranked entity list.
- Added computeTopMovers(intents, dimension, windowDays=7): compares share
  in the last N days vs the preceding N-day window; returns entities ranked
  by absolute delta with ▲/▼ accessible labels.
- Chain colours sourced from CHAINS registry; solver/token colours use an
  8-colour accessible fallback palette; 'Other' always maps to gray.
- Added StackedAreaChart to AnalyticsPageClient: SVG stacked area with
  100%-normalised toggle, per-entity fill opacity, grid lines, and legend.
- Added TopMoversPanel: entity, current share %, delta in percentage
  points with ▲/▼ accessible aria-labels.
- Added ConcentrationIndicator: HHI badge in a Tooltip explaining the
  index, top-1/top-3 share cards, top-5 entity mini-list.
- Added dimension selector (Source Chain / Dest Token / Solver) and
  normalise toggle in AnalyticsPageClient controlling all three new panels.

Closes stellar-vortex-protocol#469
Closes stellar-vortex-protocol#470
Closes stellar-vortex-protocol#471
Closes stellar-vortex-protocol#468
@drips-wave

drips-wave Bot commented Sep 29, 2026

Copy link
Copy Markdown

@allhandghost 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

…1-governance-analytics

# Conflicts:
#	src/app/analytics/AnalyticsPageClient.tsx
#	src/app/governance/GovernancePageClient.tsx
#	src/app/governance/[id]/ProposalDetailClient.tsx
#	src/lib/textSafety.ts
@james2177
james2177 merged commit b21359f into stellar-vortex-protocol:main Sep 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment