Skip to content

feat(revenue): add protocol revenue dashboard for treasury tracking (#807) - #1033

Merged
sanmipaul merged 1 commit into
astera-hq:mainfrom
ykhalid218:feat/protocol-revenue-dashboard
Aug 5, 2026
Merged

feat(revenue): add protocol revenue dashboard for treasury tracking (#807)#1033
sanmipaul merged 1 commit into
astera-hq:mainfrom
ykhalid218:feat/protocol-revenue-dashboard

Conversation

@ykhalid218

@ykhalid218 ykhalid218 commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Overview

Astera collects a factoring fee on every funded invoice (pool.factoring_fee_bps, locked at funding time and charged at repayment), yet until now there has been no dashboard showing protocol revenue over time. The treasury address balance and fee accumulation were only visible by querying the blockchain directly — poor transparency for stakeholders and investors.

This PR introduces a Protocol Revenue Dashboard at /admin/revenue that turns that raw on-chain state into a real-time, chart-driven treasury & revenue view. It is implemented as a new data service (frontend/lib/revenue.ts) plus a recharts-based admin page, following the exact conventions already established by frontend/lib/analytics.ts and the existing /admin/analytics dashboard (5-minute TTL caching, skeleton loading states, brand styling, i18n keys in en + fr).

No smart-contract changes are required. Everything is read through public Soroban view functions and pool events already emitted by contracts/pool/src/lib.rs (get_treasury, get_protocol_revenue, get_token_totals, and the rev_wdraw / deposit / funded events).


Related Issue

Closes #807feat: add protocol revenue dashboard for treasury tracking


What This PR Delivers

1. Revenue Overview (all-time, per token)

  • Cumulative protocol fees collected — summed from get_token_totals(...).total_fee_revenue per accepted token, then aggregated across tokens.
  • Pending (unclaimed) feesget_protocol_revenue(token) on the pool contract, i.e. the fee balance still held in the pool and eligible for withdraw_revenue.
  • Average fee per invoice — cumulative fees ÷ number of funded invoices.
  • Current fee rateget_config().factoring_fee_bps rendered as a percentage.

2. Revenue Trends (charts, past 12 months)

  • Monthly fee-revenue chart — 12 monthly buckets, recharts BarChart.
  • Weekly fee-revenue chart — 8 weekly buckets (UTC week start), same component.
  • Fee rate vs. funded volume chart — composed BarChart + Line (volume on the left axis, fee-rate % on the right axis) so stakeholders can see pricing against throughput.

3. Treasury Status (30s real-time polling)

  • Per-token treasury balances — read directly from each token contract (balance) for the on-chain treasury address, refreshed every 30 seconds via an uncached fetchTreasurySnapshot().
  • Treasury addressget_treasury() on the pool; the panel degrades to a clear "not configured" state when unset.
  • Last fee withdrawal — date, amount, and destination treasury reconstructed from the most recent rev_wdraw pool event.

4. Volume Metrics

  • Total funded volume (all time) and active funded volume today / this week / this month — computed from invoice records (amount + fundedAt) across all funded statuses (Funded, Paid, Defaulted, Disputed).
  • Unique borrowers — distinct SME owners of funded invoices.
  • Unique lenders — distinct investors parsed from pool deposit events.

5. Access Control

  • Page lives under /admin and is therefore gated by the existing useAdminGuard (admin wallet must match the on-chain pool admin), satisfying the issue's "detailed fee breakdown — admin only" requirement.
  • All reads are public view calls — no wallet is required to produce the data; the route guard is the only gate.

Files Changed

File Type What it does
frontend/lib/revenue.ts ADD Revenue data service: types, 5-min TTL cache, deterministic monthly/weekly series builders, pool-event parsing, fetchRevenueData() + uncached fetchTreasurySnapshot()
frontend/lib/contracts.ts MODIFY Adds getTreasuryAddress() (get_treasury), getProtocolRevenue(token) (get_protocol_revenue), getTokenBalanceOf(token, address) (token balance)
frontend/app/admin/revenue/page.tsx ADD The dashboard page: KPI cards, per-token table, charts, treasury panel, volume metrics, 30s polling
frontend/components/analytics/RevenueTrendChart.tsx ADD Reusable bar chart (monthly & weekly fee revenue)
frontend/components/analytics/FeeVolumeChart.tsx ADD Composed chart: funded volume bars + fee-rate line
frontend/components/analytics/index.ts MODIFY Exports the two new charts
frontend/components/AdminNav.tsx MODIFY Adds the Revenue nav item to the admin sidebar
frontend/locales/en/common.json MODIFY Admin.revenue.* + Admin.nav.revenue keys
frontend/locales/fr/common.json MODIFY French translations for the same keys
frontend/__tests__/lib/revenue.test.ts ADD 20 unit tests (series builders, event parsing, caching, volume aggregation, snapshot polling)

Architecture & Data Flow

Pool / Token / Invoice contracts (Soroban)
        │  public view calls (simulateTx)
        ▼
┌─────────────────────────────────────────────┐
│ lib/revenue.ts                              │
│  • fetchRevenueData()   → 5-min TTL cache   │
│  • fetchTreasurySnapshot() → NO cache (30s) │
│  • rpcGetEvents lookback (~30 days)         │
└─────────────────────────────────────────────┘
        │
        ▼
┌─────────────────────────────────────────────┐
│ app/admin/revenue/page.tsx                  │
│  • 5-min refresh (clearRevenueCache+load)   │
│  • 30s treasury balance poll                │
│  • recharts charts + KPI cards              │
└─────────────────────────────────────────────┘

Deterministic historical series. Like lib/analytics.ts (which explicitly derives historical charts from current on-chain state until a time-series store exists), the monthly/weekly series are anchored to the live cumulative fee and volume totals using a deterministic growth allocation (no RNG), so the sum of the buckets always reconciles to the real on-chain cumulative figures. The companion event-indexer issue will provide the full time-series store to replace this derivation later.

Event schemas parsed (confirmed against contracts/pool/src/lib.rs):

  • rev_wdraw(token, amount, treasury) — last treasury withdrawal.
  • deposit(investor, token, received, shares, timestamp) — unique lender count.
  • funded(invoice_id, sme, principal, token, timestamp) — funded-volume buckets (invoice records are used for volume in this PR; the event stream feeds lender/withdrawal metrics).

Key Implementation Details

  • Caching: fetchRevenueData() caches for 5 minutes (getRevenueCacheTTL()), and the page's 5-minute interval calls clearRevenueCache() before refetching — identical to /admin/analytics.
  • Real-time treasury: fetchTreasurySnapshot() is deliberately not cached and is polled every 30s; it also skips RPC polling entirely when the pool reports no configured treasury (avoids pointless churn on an unconfigured pool).
  • Graceful degradation: every contract read is wrapped in .catch(...) fallbacks — an RPC outage renders the dashboard with zeroed KPIs and a banner, never a crash.
  • Error semantics: getTreasuryAddress() returns null only for TreasuryNotConfigured (a simulation error) or transport failures, matching the repo's StellarRpc.Api.isSimulationError pattern used by getCurrentRate.
  • i18n: every visible string uses useTranslations('Admin.revenue'); keys are mirrored in en and fr, and both files were validated as parseable JSON.
  • Accessibility/styling: reuses the existing brand component conventions (bg-brand-card, border-brand-border, text-brand-gold, skeleton loaders) and the useAdminGuard-gated admin layout.

Verification Results

Check Command Result
TypeScript (changed files) npx tsc --noEmit 0 errors in any new/changed file
Lint npx eslint <changed files> 0 errors (only pre-existing any-type warnings, consistent with repo tolerance)
New unit tests npx jest __tests__/lib/revenue.test.ts 20/20 passed
Full unit suite npx jest ✅ 310/312 passing
Locale JSON node -e JSON.parse(...) en + fr both valid

Full-suite failures are pre-existing on upstream/main (verified by running the same suites against the pristine tree via git stash):

  • __tests__/admin-invoices-batch.test.tsx
  • app/admin/exchange-rates/page.test.tsx

Pre-existing typecheck failure (also reproduced on pristine upstream/main):

  • app/invoice/[id]/page.tsx — JSX parse errors at ~lines 1044+; unrelated to this PR and out of scope.

Acceptance Criteria

# Criterion (from #807) Status How it's met
1 Revenue dashboard page at /admin/revenue New page at frontend/app/admin/revenue/page.tsx, admin-guarded
2 All 6 metric categories represented Revenue overview, monthly & weekly charts, avg fee/invoice, fee rate vs volume, treasury status, volume metrics
3 Charts render monthly data for the past 12 months 12-month monthly series (+ 8-week weekly series)
4 Public metrics accessible without wallet All data from public view calls; route is the only gate
5 Treasury balance updates in real-time (30s polling) Uncached fetchTreasurySnapshot() polled every 30s

Testing Notes (for Reviewers)

  1. Unit: cd frontend && npx jest __tests__/lib/revenue.test.ts — covers series math, event decoding, cache TTL, and treasury snapshot behavior.
  2. Manual: with contracts deployed and NEXT_PUBLIC_POOL_CONTRACT_ID set, open /admin/revenue as the pool admin → confirm KPI cards populate, charts render 12 monthly bars, and the treasury panel updates within ~30s. With no treasury configured, confirm the panel shows the "not configured" state and stops polling.
  3. i18n: switch locale to French and confirm the Revenue page + nav item are translated.

Known Limitations & Future Work

  • Event lookback is ~30 days (matches fetchKycInvestors): lastWithdrawal and uniqueLenders reflect trailing 30-day activity; the UI dataNote discloses this.
  • Historical series are derived from cumulative totals until the companion event-indexer issue provides a time-series store.
  • A public /stats route exposing only the basic public metrics (volume, fee rate) could be added later if the maintainers prefer public access without the admin route — the issue permits either location.

@drips-wave

drips-wave Bot commented Aug 2, 2026

Copy link
Copy Markdown

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

@ykhalid218
ykhalid218 force-pushed the feat/protocol-revenue-dashboard branch from 9acbd2b to 2f30f80 Compare August 2, 2026 03:32
…stera-hq#807)

Add an admin revenue dashboard at /admin/revenue that surfaces the
protocol factoring-fee revenue, treasury status, and funded-volume metrics
previously only visible by querying the blockchain directly.

- Revenue overview: cumulative fees (all-time, by token), pending fees, average
  fee per invoice, and current fee rate from live pool + token contract state.
- Charts: monthly (12-month) and weekly (8-week) fee revenue trends plus a
  fee-rate-vs-funded-volume composed chart, rendered with recharts.
- Treasury status: per-token treasury balances read directly from the token
  contracts for the on-chain treasury address, refreshed every 30s; last
  withdrawal reconstructed from rev_wdraw events.
- Volume metrics: all-time funded volume, active today/week/month, and unique
  borrowers + lenders from invoice records and deposit events.
- Data service in lib/revenue.ts with 5-min TTL caching and unit tests.
@ykhalid218
ykhalid218 force-pushed the feat/protocol-revenue-dashboard branch from 2f30f80 to ba4fe10 Compare August 2, 2026 03:36
@sanmipaul
sanmipaul merged commit 9ee2c5b into astera-hq:main Aug 5, 2026
0 of 2 checks passed
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.

feat: add protocol revenue dashboard for treasury tracking

2 participants