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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ Key resources for contributors:
- [Frontend Developer Guide](docs/frontend-guide.md) — Styling conventions and page creation
- [Compliance Reviewer Workflow](docs/compliance-reviewer-workflow.md) — Guide for compliance operators reviewing investor eligibility
- [Investor Transfer Request Flow](docs/investor-transfer-request-flow.md) — Request-validation edge cases (address, self-transfer, amount, precision) and RPC-failure handling for the transfer modal
- [RWA Asset Minting Workflow](docs/rwa-asset-minting-workflow.md) — Admin mint: asset selector, compliance pre-check, review, Freighter signing, receipt (Issue #6)
- [Compliance-Safe Wording Guidance](docs/compliance-safe-wording.md) — Canonical disclaimer text, typed helper, and reviewer checklist for compliance-facing copy
- [RWA Asset Lifecycle Status](docs/asset-lifecycle-status.md) — Lifecycle state machine, transition validation, and status UI for already-minted RWA assets
- [Bulk Compliance Review](docs/bulk-compliance-review.md) — Bulk compliance review table with action confirmation modal
Expand Down
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ Reference material for contributors implementing new functionality.
| [investor-dashboard.md](investor-dashboard.md) | Portfolio page data flow, mock portfolio shape, SDK assumptions |
| [investor-transfer-eligibility.md](investor-transfer-eligibility.md) | Eligibility checks before transfer submission |
| [investor-transfer-request-flow.md](investor-transfer-request-flow.md) | Request-validation layer: address/amount edge cases, RPC-failure vs. not-whitelisted (Issue #41) |
| [rwa-asset-minting-workflow.md](rwa-asset-minting-workflow.md) | Admin RWA mint workflow: asset selector, compliance pre-check, review, Freighter signing, receipt (Issue #6) |
| [asset-lifecycle-status.md](asset-lifecycle-status.md) | RWA asset lifecycle state machine, transition validation, badge/timeline UI (Issue #30) |
| [investor-onboarding-eligibility.md](investor-onboarding-eligibility.md) | Investor onboarding eligibility page, evaluation precedence, SDK mapping (Issue #28) |
| [admin-role-management-design.md](admin-role-management-design.md) | Admin role resolution, whitelist heuristic, mock admin address |
Expand Down
2 changes: 1 addition & 1 deletion docs/diagnostics.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ When opening a GitHub Issue or requesting support in Discord, click **Copy Repor
"wallet": "GBXY...WXYZ",
"network": "TESTNET",
"flags": {
"newMintFlow": false,
"newMintFlow": true,
"complianceBanner": true,
"darkMode": false
}
Expand Down
6 changes: 5 additions & 1 deletion docs/feature-flags.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,13 @@ Flags are managed by a [zustand](https://github.com/pmndrs/zustand) store at `sr
import { useFeatureFlags } from '@/hooks/useFeatureFlags';

const isNewMintFlowEnabled = useFeatureFlags((s) => s.flags.newMintFlow);

```

`newMintFlow` defaults to **true** and gates the guided RWA mint workflow on
`/admin` (Issue #6). Toggle it off in the feature-flags panel to fall back to
the legacy fixed-amount mint panel. See
[rwa-asset-minting-workflow.md](rwa-asset-minting-workflow.md).

Adding a new flag

1. Add the key to the `FeatureFlagKey` union in `useFeatureFlags.ts`.
Expand Down
8 changes: 5 additions & 3 deletions docs/mock-mode.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,9 +107,11 @@ The `MockAegisProvider` maps the transfer/mint `amount` to a specific outcome:
| `0.03` | Unknown status (exercises the fallback UI path) |
| Any other value | `SUCCESS` |

This lets you exercise every receipt state without touching any code. Open the
Admin page, enter any G-address longer than 50 characters, and mint with one of
these amounts.
This lets you exercise every receipt state without touching any code. On the
Admin page with the guided mint workflow (`newMintFlow`, default on), select an
asset, enter any G-address longer than 50 characters, and mint with one of
these amounts (0.01 / 0.02 / 0.03). See
[rwa-asset-minting-workflow.md](rwa-asset-minting-workflow.md).

---

Expand Down
124 changes: 124 additions & 0 deletions docs/rwa-asset-minting-workflow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
# Admin RWA Asset Minting Workflow

Closes #6. Documents the guided admin minting flow for compliant RWA asset
issuance: asset selection, recipient & amount validation, compliance
pre-check, review, Freighter signing phases, and receipt / recovery states.

## Scope

This document covers the **minting workflow** itself. Related concerns are
documented elsewhere and not duplicated here:

| Concern | Document |
|---|---|
| Shared review / progress / receipt UI | [transaction-components.md](transaction-components.md) |
| Double-submit / idempotency | [form-idempotency.md](form-idempotency.md) |
| Failure / unknown recovery | [sdk-error-recovery.md](sdk-error-recovery.md) |
| Feature flag gating | [feature-flags.md](feature-flags.md) |
| Mock amount → outcome mapping | [mock-mode.md](mock-mode.md) |
| Post-mint lifecycle states | [asset-lifecycle-status.md](asset-lifecycle-status.md) |

## Entry point

- Route: `/admin` (admin role only — see [route-access.md](route-access.md))
- Component: `src/features/minting/components/MintWorkflow.tsx`
- Wired from `src/features/admin/components/AdminPanel.tsx` when the
`newMintFlow` feature flag is enabled (default: **on**)
- Legacy fixed-amount panel remains available when `newMintFlow` is toggled off

## Flow

```
idle (asset + recipient + amount)
→ validateMintRequest
→ checkWhitelist (compliance pre-check)
→ review (TransactionReview)
→ signing / pending (TransactionProgress, Freighter via provider phases)
→ success receipt | failure/unknown recovery (SdkErrorRecovery)
```

1. Admin selects a mintable asset from the catalogue
(`src/features/minting/fixtures.ts`).
2. Admin enters recipient address and amount.
3. On **Review mint**, `validateMintRequest` runs
(`src/lib/mintRequest.ts`).
4. If valid, `useAegis().checkWhitelist(recipient)` runs. RPC failure and
"not whitelisted" are surfaced as distinct errors — neither advances to
review.
5. Review screen shows asset, amount, recipient, signer, and network.
6. **Confirm & Sign** submits through `useIdempotentSubmit({ scope: 'mint' })`
and `useAegis().mint(...)`. Provider phase callbacks drive the progress UI
(`signing` → `pending`); Freighter signing is owned by the provider /
wallet layer, not called directly from the dashboard.
7. Success / pending → `TransactionReceipt`. Failure / unknown →
`SdkErrorRecovery` with a classified plan.

## Data model

### Validation — `src/lib/mintRequest.ts`

Pure module (no React / SDK imports):

- `MintRequestInput` — recipient, amount (string), assetId
- `MintRequestContext` — `maxDecimals`, optional soft `maxAmount`
- `validateMintRequest()` — returns `{ valid, error?, parsedAmount? }`

Reuses `isPlausibleStellarAddress` from `transferRequest.ts` (shape check
only — not full StrKey/CRC16 validation).

### Mintable assets — `src/features/minting/fixtures.ts`

Synthetic catalogue for the selector (`MintableAsset`: id, name, ticker,
decimals, assetClass, description). Replace with an SDK registry read when
the live asset-registry API is available.

## Edge cases

| Case | Behaviour |
|---|---|
| Empty asset / recipient / amount | Blocked before any network call |
| Malformed Stellar address | Blocked client-side (shape check) |
| Zero / negative amount | Blocked client-side |
| Decimal precision beyond asset decimals | Blocked client-side |
| Amount above soft cap | Blocked client-side (`DEFAULT_MINT_MAX_AMOUNT`) |
| Whitelist RPC failure vs not whitelisted | Distinct error messages |
| Double-submit on Confirm | Idempotency guard — single provider call |
| Provider FAILED / unknown / thrown error | Recovery panel; unknown does not offer blind retry |

## Mock mode outcomes

Because amount is user-entered (unlike the legacy fixed `1000`), admins can
exercise non-success paths in mock mode:

| Amount | Mock outcome |
|---|---|
| `0.01` | FAILED |
| `0.02` | PENDING |
| `0.03` | unknown status |
| anything else | SUCCESS |

## Security & compliance assumptions

- The whitelist check is **protocol-level compliance enforcement**, not legal
or financial advice. UI copy must stay consistent with
[compliance-safe-wording.md](compliance-safe-wording.md) and
[sdk-error-recovery.md](sdk-error-recovery.md).
- On-chain / RPC whitelist status is authoritative; the client only decides
whether to *attempt* the mint.
- Soft amount caps and address shape checks are UX guards only — the SDK /
contracts remain the source of truth.

## Testing

| Layer | Location |
|---|---|
| Pure validation | `src/lib/mintRequest.test.ts` |
| Workflow (happy, validation, compliance, idempotency, recovery) | `src/features/minting/components/MintWorkflow.test.tsx` |
| Flag wiring | `src/features/admin/components/AdminPanel.test.tsx` |
| Provider mint outcomes | `src/__tests__/sdk/provider.test.ts` |

## Related

- Issue #6 — Implement RWA asset minting workflow
- Transfer counterpart: [investor-transfer-request-flow.md](investor-transfer-request-flow.md)
- `TransferModal` is the UX template this flow mirrors
2 changes: 1 addition & 1 deletion docs/test-first-contribution-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ Each area below links to an existing test file that shows the pattern to follow.
| **Investor views** | `src/features/investor/InvestorEligibilityPanel.test.tsx` | Render the view under loading, empty, error, and populated states. Assert on screen text, badge labels, and presence/absence of elements. |
| **Compliance screens** | `src/features/admin/components/ComplianceInfo.test.tsx` | Test each verdict (pass, fail, pending, unknown) — never default an unknown verdict to a safe badge. |
| **Asset registration** | `src/lib/eligibility.test.ts` | Test card rendering with valid, malformed, and missing metadata. Assert the UI shows an error state rather than rendering blank or incorrect values. |
| **Minting** | `src/features/investor/components/TransferModal.test.tsx` | Test input validation before submit. Cover invalid address, empty input, provider rejection, and double-submit. |
| **Minting** | `src/features/minting/components/MintWorkflow.test.tsx` | Test input validation before submit. Cover invalid address, empty input, not-whitelisted, provider rejection, and double-submit. |
| **Wallet connection** | `src/lib/route-guard.test.ts` (mock wallet pattern) | Test `connect`, `disconnect`, and `tryAutoReconnect` state transitions. Cover Freighter-not-installed, rejected prompt, and network mismatch. |
| **Diagnostics** | `src/lib/diagnostics/redact.test.ts` | Test redaction rules, env-var fallbacks, and malformed input. Assert the report renders without throwing and secrets remain redacted. |

Expand Down
2 changes: 1 addition & 1 deletion docs/testing-standard.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ And one document that goes deeper on SDK-adjacent logic specifically:
| **Investor views** | `src/features/investor/components/PortfolioList.tsx`, `PortfolioEmptyState.tsx`, `PortfolioErrorState.tsx`, `PortfolioDisclaimer.tsx` | Required — list rendering logic for a given portfolio shape | Required — the view responds correctly to loading/empty/error/populated states from the hook it consumes | Required — empty portfolio, disconnected wallet, load error (see [InvestorEligibilityPanel.test.tsx](../src/features/investor/InvestorEligibilityPanel.test.tsx) for the pattern) | Required — one screenshot per state (empty, error, populated) |
| **Compliance screens** | `src/features/admin/components/ComplianceInfo.tsx`, `BulkComplianceReview.tsx`, `src/features/assets/components/ComplianceBadge.tsx` | Required — status-to-copy/badge mapping | Required if the screen consumes a compliance verdict from a hook or fixture | Required — rejected, review-flagged, and unknown/pending verdicts (see [ComplianceInfo.test.tsx](../src/features/admin/components/ComplianceInfo.test.tsx)) | Required for any change to how a verdict is displayed |
| **Asset registration** | `src/features/assets/components/AssetCard.tsx`, `AssetCardSkeleton.tsx`, `TransferEligibilityBadge.tsx`, RWA metadata parsing (see [SDK standard](sdk-testing-standard.md)) | Required — card rendering for a given asset shape | Required if the card consumes a live/mocked provider read | Required — missing/malformed metadata, zero balance, ineligible transfer | Required for any visual change to the card or badges |
| **Minting** | `AdminPanel.tsx` mint flow; provider call itself is covered by the [SDK standard](sdk-testing-standard.md#minimum-coverage-by-change-type) | Required — input validation before submit | Required — provider called with correct arguments, phase callbacks fire in order | Required — invalid address, empty input, provider rejection, double-submit | Required — before/after for disabled and loading states |
| **Minting** | `src/features/minting/components/MintWorkflow.tsx`; provider call itself is covered by the [SDK standard](sdk-testing-standard.md#minimum-coverage-by-change-type) | Required — input validation before submit (`mintRequest.ts`) | Required — provider called with correct arguments, phase callbacks fire in order; compliance checked before review | Required — invalid address, empty input, not-whitelisted, provider rejection, double-submit | Required — before/after for disabled and loading states |
| **Wallet connection** | `src/hooks/useWallet.ts` | Required — `connect`, `disconnect`, and `tryAutoReconnect` state transitions | Not required unless a component's rendering depends on a specific transition | Required — Freighter not installed, `requestAccess` rejected/throws, `tryAutoReconnect` with no prior grant, network mismatch | Required if the connect/disconnect UI changes — capture the connected, disconnected, and error-banner states |
| **Diagnostics** | `src/features/diagnostics/components/DiagnosticsPanel.tsx`, `StatusCard.tsx`, `src/lib/diagnostics/redact.ts` | Required — `redact.ts` already has coverage ([redact.test.ts](../src/lib/diagnostics/redact.test.ts)); new redaction rules need a case added there | Required if `DiagnosticsPanel` changes what it reads from the wallet/feature-flag stores | Required — missing env var, wallet disconnected, malformed contract ID | Required for any layout change — the "Copy Report" output must still be redacted correctly, screenshot the copied JSON |

Expand Down
13 changes: 9 additions & 4 deletions docs/transaction-components.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,9 +177,14 @@ status as legal or financial advice.
- **`src/features/investor/components/TransferModal.tsx`** — the KYC whitelist check runs first,
then `Review Transfer` opens `TransactionReview`, confirming signs and submits
the transfer, and the receipt replaces the old `alert("Transfer Successful!")`.
- **`src/components/AdminPanel.tsx`** — `Mint Asset` opens the review inline in
the card, then progress, then the receipt.

`Whitelist User` in the admin panel still uses a plain `alert()`: it does not go
- **`src/features/minting/components/MintWorkflow.tsx`** — guided admin mint (Issue #6):
asset selector, amount/recipient validation, compliance pre-check, then
`TransactionReview` → progress → receipt / SDK recovery. Wired from
`AdminPanel` when `newMintFlow` is enabled (default on). See
[rwa-asset-minting-workflow.md](rwa-asset-minting-workflow.md).
- **`src/features/admin/components/AdminPanel.tsx`** — hosts the mint workflow
(or the legacy fixed-amount panel when `newMintFlow` is off).

`Whitelist User` in the legacy admin panel still uses an inline confirmation: it does not go
through `useAegis` and has no contract call or hash behind it yet. It should move
onto these components (`action: 'compliance-update'`) as soon as it does.
14 changes: 7 additions & 7 deletions src/__tests__/hooks/useFeatureFlags.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,15 @@ beforeEach(() => {
describe('useFeatureFlags', () => {
it('has the expected default values', () => {
const { flags } = useFeatureFlags.getState();
expect(flags.newMintFlow).toBe(false);
expect(flags.newMintFlow).toBe(true);
expect(flags.complianceBanner).toBe(true);
expect(flags.darkMode).toBe(false);
});

it('toggles a flag from false to true', () => {
it('toggles a flag from true to false', () => {
const { toggleFlag } = useFeatureFlags.getState();
toggleFlag('newMintFlow');
expect(useFeatureFlags.getState().flags.newMintFlow).toBe(true);
expect(useFeatureFlags.getState().flags.newMintFlow).toBe(false);
});

it('toggles a flag back to its original value', () => {
Expand Down Expand Up @@ -63,16 +63,16 @@ describe('useFeatureFlags', () => {
resetFlags();

const { flags } = useFeatureFlags.getState();
expect(flags.newMintFlow).toBe(false);
expect(flags.newMintFlow).toBe(true);
expect(flags.darkMode).toBe(false);
});

it('isEnabled reflects current flag state', () => {
const { setFlag, isEnabled } = useFeatureFlags.getState();
expect(isEnabled('newMintFlow')).toBe(false);
expect(isEnabled('newMintFlow')).toBe(true);

setFlag('newMintFlow', true);
expect(useFeatureFlags.getState().isEnabled('newMintFlow')).toBe(true);
setFlag('newMintFlow', false);
expect(useFeatureFlags.getState().isEnabled('newMintFlow')).toBe(false);
});

// --- mockMode flag ---
Expand Down
44 changes: 44 additions & 0 deletions src/features/admin/components/AdminPanel.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { render, screen } from '@testing-library/react';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import AdminPanel from '@/features/admin/components/AdminPanel';
import { useFeatureFlags } from '@/hooks/useFeatureFlags';

vi.mock('@/hooks/useAegis', () => ({
useAegis: () => ({
checkWhitelist: vi.fn(async () => true),
mint: vi.fn(),
isLoading: false,
}),
}));

vi.mock('@/hooks/useWallet', () => ({
useWallet: () => ({
address: 'GCEZWKCA5VLDNRLN3RPRJMRZOX3Z6G5CHCGSNFHEYVXM3XOJMDS674JZ',
network: 'TESTNET',
connect: vi.fn(),
}),
}));

beforeEach(() => {
useFeatureFlags.getState().resetFlags();
});

describe('AdminPanel — mint flow flag', () => {
it('renders the guided MintWorkflow when newMintFlow is enabled', () => {
useFeatureFlags.getState().setFlag('newMintFlow', true);
render(<AdminPanel />);

expect(screen.getByRole('heading', { name: /mint rwa asset/i })).toBeInTheDocument();
expect(screen.getByLabelText(/^asset$/i)).toBeInTheDocument();
expect(screen.getByRole('button', { name: /review mint/i })).toBeInTheDocument();
});

it('renders the legacy fixed-amount panel when newMintFlow is disabled', () => {
useFeatureFlags.getState().setFlag('newMintFlow', false);
render(<AdminPanel />);

expect(screen.getByRole('heading', { name: /admin controls/i })).toBeInTheDocument();
expect(screen.getByRole('button', { name: /mint asset/i })).toBeInTheDocument();
expect(screen.getByRole('button', { name: /whitelist user/i })).toBeInTheDocument();
});
});
Loading