Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions docs/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -2184,6 +2184,56 @@ soroban contract invoke \

---

## Subscription Integrity Diagnostics

Administrative utilities for detecting and repairing corrupted subscription records after migrations or contract upgrades.

### `validate_subscription`

Read-only integrity check for a subscriber address.

```
validate_subscription(env: Env, user: Address) -> SubscriptionValidationReport
```

**Returns `SubscriptionValidationReport`**

| Field | Type | Description |
| --- | --- | --- |
| `is_valid` | `bool` | `true` when no inconsistencies are detected |
| `violations` | `Vec<String>` | General integrity violations |
| `missing_records` | `Vec<String>` | Missing auxiliary records (history, metadata, etc.) |
| `invalid_state_transitions` | `Vec<String>` | Illegal active/paused/cancelled state combinations |
| `corrupted_references` | `Vec<String>` | Broken merchant/token/referrer references |

**Auth:** None (read-only simulation).

**Frontend:** Exposed in the Admin Dashboard → Subscription Repair panel.

---

### `repair_subscription`

Repairs detected subscription inconsistencies for a user.

```
repair_subscription(env: Env, user: Address) -> u32
```

**Auth:** Contract admin only (`require_admin`).

**Returns:** Count of fixed inconsistencies (also emitted in the `subscription_repaired` event).

**Event emitted**

| Event name | Topic | Data |
| --- | --- | --- |
| `subscription_repaired` | `("subscription_repaired", user_address)` | `fixed_inconsistencies: u32` |

**Frontend authorization:** The repair button is enabled only when the connected Freighter wallet matches the on-chain admin returned by `get_admin`.

---

## Units & Conversions

All amounts are in **stroops** — the smallest unit of a Stellar token.
Expand Down Expand Up @@ -2219,6 +2269,7 @@ For a complete reference of all events with detailed schemas and examples, see [
| `paused` | `("paused", user_address)` | `()` |
| `resumed` | `("resumed", user_address)` | `()` |
| `referred` | `("referred", user_address)` | `referrer_address` |
| `subscription_repaired` | `("subscription_repaired", user_address)` | `fixed_inconsistencies: u32` |

---

Expand Down
21 changes: 21 additions & 0 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,27 @@ Events are the main off-chain integration surface for analytics, indexers, and t

The frontend does not talk to the contract directly. `frontend/src/stellar.ts` builds and simulates Soroban transactions, then Freighter signs them.

```
App.tsx
├── useWallet() — Freighter connection, signing, submission
├── SubscribeForm.tsx — form to create a subscription
├── Dashboard.tsx — view subscription, cancel, pay-per-use
├── MerchantDashboard.tsx — merchant subscriber management
└── AdminDashboard.tsx — admin diagnostics and subscription repair
```

### Admin repair workflow

1. Admin connects Freighter wallet; `useAdmin` compares `publicKey` to on-chain `get_admin`.
2. Operator enters a subscriber address and runs `validate_subscription` via RPC simulation.
3. Violations are mapped to human-readable messages in the UI (missing records, invalid transitions, corrupted references).
4. If failures exist and the wallet is admin, `repair_subscription` is submitted after confirmation.
5. The UI parses the `subscription_repaired` event for the exact fixed-inconsistency count and re-runs validation.

Authorization is enforced both in the UI (repair button disabled for non-admins) and on-chain (`require_admin` in the contract).

All Soroban SDK calls are isolated in `stellar.ts`. Components never import `@stellar/stellar-sdk` directly. This makes it easy to swap the SDK version or mock it in tests.

Typical flows:

- Subscribe: build transaction, simulate, sign, submit.
Expand Down
10 changes: 9 additions & 1 deletion docs/STRUCTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,9 +125,17 @@ Form component for creating a new subscription. Accepts merchant address, XLM am

Displays the user's active subscription. Shows merchant, amount, interval, and next charge date. Provides cancel and pay-per-use actions.

### `frontend/src/pages/AdminDashboard.tsx`

Administrative tools layout. Hosts the Subscription Repair panel for on-chain diagnostics and recovery.

### `frontend/src/components/admin/SubscriptionRepairPanel.tsx`

Admin-only subscription integrity panel. Calls `validate_subscription` (read-only simulation) and `repair_subscription` (admin-auth transaction). Authorization is enforced by comparing the connected wallet to `get_admin`.

### `frontend/src/App.tsx`

Root component. Manages wallet connection state and tab switching between `SubscribeForm` and `Dashboard`.
Root component. Manages wallet connection state and tab switching between `SubscribeForm`, `Dashboard`, `MerchantDashboard`, and `AdminDashboard`.

---

Expand Down
9 changes: 9 additions & 0 deletions docs/TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,3 +171,12 @@ Frontend tests run with Vitest:
cd frontend
npm run test
```

### Admin subscription repair panel

| Test file | Coverage |
| --- | --- |
| `subscriptionValidation.test.ts` | Violation formatting and failure detection |
| `useAdmin.test.tsx` | Admin wallet authorization |
| `SubscriptionRepairPanel.test.tsx` | Validation/repair UI states, event count display, unauthorized repair |
| `AdminDashboard.test.tsx` | Dashboard integration and read-only guidance |
17 changes: 16 additions & 1 deletion frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { useKeyboardShortcuts } from "./hooks/useKeyboardShortcuts";
import { useAnalytics } from "./hooks/useAnalytics";
import SubscribeForm from "./components/SubscribeForm";
import Dashboard from "./components/Dashboard";
import AdminDashboard from "./pages/AdminDashboard";
import SystemHealthCard from "./components/SystemHealthCard";
import TabBar from "./components/TabBar";
import ConnectWallet from "./components/ConnectWallet";
Expand Down Expand Up @@ -120,6 +121,7 @@ export default function App() {
const subscribeErrorBoundaryRef = useRef<ErrorBoundary>(null);
const dashboardErrorBoundaryRef = useRef<ErrorBoundary>(null);
const merchantErrorBoundaryRef = useRef<ErrorBoundary>(null);
const adminErrorBoundaryRef = useRef<ErrorBoundary>(null);

// Keyboard shortcuts
const shortcuts = useKeyboardShortcuts({
Expand Down Expand Up @@ -410,7 +412,20 @@ export default function App() {
</Suspense>
</ErrorBoundary>
) : tab === "admin" ? (
<SystemHealthCard callerKey={publicKey} />
<ErrorBoundary
ref={adminErrorBoundaryRef}
fallback={
<TabErrorFallback
title="Admin Dashboard"
onRetry={() => adminErrorBoundaryRef.current?.reset()}
/>
}
>
<>
<SystemHealthCard callerKey={publicKey} />
<AdminDashboard publicKey={publicKey} onSign={signAndSubmit} />
</>
</ErrorBoundary>
) : (
<ErrorBoundary
ref={dashboardErrorBoundaryRef}
Expand Down
67 changes: 67 additions & 0 deletions frontend/src/__tests__/AdminDashboard.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import React from "react";
import { render, screen, waitFor } from "@testing-library/react";
import { vi, describe, it, expect, beforeEach } from "vitest";

vi.mock("../hooks/useAdmin");
vi.mock("../stellar");
vi.mock("../hooks/useSubscription", () => ({
useSubscription: vi.fn(() => ({
subscription: null,
loading: false,
error: null,
refresh: vi.fn(),
})),
}));
vi.mock("../hooks/useTransaction", () => ({
useTransaction: vi.fn(() => ({
status: "idle",
hash: null,
error: null,
submit: vi.fn(),
})),
}));

import { useAdmin } from "../hooks/useAdmin";
import AdminDashboard from "../pages/AdminDashboard";

describe("AdminDashboard", () => {
beforeEach(() => {
vi.clearAllMocks();
vi.mocked(useAdmin).mockReturnValue({
adminAddress: "GADMIN123",
isAdmin: true,
loading: false,
error: null,
refresh: vi.fn(),
});
});

it("renders the admin dashboard with subscription repair section", async () => {
render(<AdminDashboard publicKey="GADMIN123" onSign={vi.fn()} />);

await waitFor(() => {
expect(screen.getByText("Admin Dashboard")).toBeTruthy();
});

expect(screen.getByText("Subscription Repair")).toBeTruthy();
expect(screen.getByRole("button", { name: /validate subscription/i })).toBeTruthy();
});

it("shows read-only guidance for non-admin wallets", async () => {
vi.mocked(useAdmin).mockReturnValue({
adminAddress: "GADMIN123",
isAdmin: false,
loading: false,
error: null,
refresh: vi.fn(),
});

render(<AdminDashboard publicKey="GUSER456" onSign={vi.fn()} />);

await waitFor(() => {
expect(
screen.getByText(/Diagnostic tools are available in read-only mode/)
).toBeTruthy();
});
});
});
Loading
Loading