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 @@ -43,6 +43,7 @@ Key resources for contributors:
- [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
- [Compliance Status Panel](docs/compliance-status-panel.md) — Address-level compliance status for investor and admin views (Issue #175)
- [Environment Mismatch Blocking Screen](docs/environment-mismatch-blocking.md) — Full-page blocking screen when the wallet network does not match the dashboard target network
- [Investor Onboarding Eligibility](docs/investor-onboarding-eligibility.md) — Investor onboarding eligibility page, evaluation precedence, and SDK mapping
- [Performance Budget Review](docs/performance-budget-review.md) — Typed budget threshold evaluation, edge cases, and reviewer checklist
Expand Down
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ Reference material for contributors implementing new functionality.
| [audit-log.md](audit-log.md) | Audit log data model, filtering, safe CSV/JSON export, redaction |
| [compliance-reviewer-workflow.md](compliance-reviewer-workflow.md) | Compliance operator workflow for reviewing investor eligibility |
| [compliance-safe-wording.md](compliance-safe-wording.md) | Compliance-safe wording guidance, shared disclaimer helper, and reviewer checklist |
| [compliance-status-panel.md](compliance-status-panel.md) | Address-level compliance status panel for investor/admin views (Issue #175) |
| [performance-budget-review.md](performance-budget-review.md) | Typed budget threshold evaluation, edge cases, and reviewer checklist |
| [feature-flags.md](feature-flags.md) | Feature flag hook, panel location, flag naming conventions |
| [form-idempotency.md](form-idempotency.md) | Content-derived idempotency key, double-submit guard, TTL |
Expand Down
6 changes: 5 additions & 1 deletion docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,8 @@ The UI is strictly separated into pages and domain-specific features:
- `src/features/auth/` encapsulates:
- dashboard role model and route access config
- SDK-backed role resolution (mocked)
- route guard hook and fixtures for access states
- route guard hook and fixtures for access states
- `src/features/compliance/` encapsulates:
- address-level compliance status panel
- SDK raw-record mapping and safe explanatory copy
- fixtures covering approved / blocked / pending / revoked / unknown / unavailable
1 change: 1 addition & 0 deletions docs/compliance-safe-wording.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ withDisclaimer("") // → "Protocol-level compliance information. Not legal, reg
## Related

- [Bulk Compliance Review](bulk-compliance-review.md) — Technical implementation of the compliance review table, including the `ComplianceUpdateModal` review modal (Issue #27)
- [Compliance Status Panel](compliance-status-panel.md) — Address-level status panel for investor and admin views (Issue #175)
- [Compliance Update Modal](bulk-compliance-review.md#compliance-update-review-modal) — The confirmation modal that wraps `TransactionReview` and `TransactionReceipt` with the disclaimer
- [Compliance Reviewer Workflow](compliance-reviewer-workflow.md) — Operator-facing workflow documentation
- [SDK Error Recovery](sdk-error-recovery.md) — Recovery plans that also include compliance-safe wording
Expand Down
80 changes: 80 additions & 0 deletions docs/compliance-status-panel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# Compliance Status Panel

Issue: [#175](https://github.com/Axionvera/aegis-dashboard/issues/175)

## Goal

Show investors and admins a clear, protocol-level compliance status for a wallet address.

Supported states:

- `approved`
- `blocked`
- `pending`
- `revoked`
- `unknown`
- `unavailable`

## Where It Appears

- Investor portfolio (`/portfolio`) — connected wallet status
- Admin dashboard (`/admin`) — connected wallet status plus address lookup

## Architecture

| Layer | Path | Responsibility |
|---|---|---|
| SDK contract | `src/lib/sdk/IAegisProvider.ts` | `getAddressCompliance(address)` |
| Mock / live providers | `src/lib/sdk/MockAegisProvider.ts`, `LiveAegisProvider.ts` | Centralised SDK access |
| Client stub | `src/lib/aegis/client.ts` | Live provider stub implementation |
| Hook | `src/hooks/useAegis.ts` | UI loading wrapper around provider |
| Mapper | `src/features/compliance/statusMap.ts` | Raw SDK record → panel model |
| Panel | `src/features/compliance/components/ComplianceStatusPanel.tsx` | Investor / admin UI |

SDK calls are centralised through `getAegisProvider().getAddressCompliance(...)`. Components never call Freighter or contract RPCs directly for this panel.

## Separation From Related Surfaces

| Surface | Vocabulary | Purpose |
|---|---|---|
| This panel | `approved` / `blocked` / `pending` / `revoked` / `unknown` / `unavailable` | Address-level registry status |
| Asset badge (`lib/aegis/types`) | `compliant` / `restricted` / `pending_review` | Per-holding portfolio badge |
| Bulk review (`lib/complianceReview`) | `pending` / `approved` / `rejected` / `review` | Admin KYC queue actions |
| Static legend (`types/compliance`) | Same six public states as this panel | Reference copy only |

Do not reuse transfer/onboarding eligibility (`evaluateEligibility`) for this panel.

## Status Meaning

| State | Meaning |
|---|---|
| Approved | Address is currently marked approved in the protocol registry |
| Blocked | Address is blocked from protocol actions |
| Pending | Review is still in progress |
| Revoked | Prior approval was revoked |
| Unknown | No clear record was returned |
| Unavailable | Registry / SDK data could not be retrieved |

## Copy Assumptions

- All explanations are **protocol-level only**.
- The panel always shows `COMPLIANCE_DISCLAIMER` from `src/lib/complianceReview.ts`.
- The panel explicitly states it does **not** perform real-world KYC.
- Copy must not claim legal approval, regulatory clearance, or financial advice.

See also: [Compliance-Safe Wording Guidance](compliance-safe-wording.md).

## Fixtures And Tests

- Fixtures: `src/features/compliance/fixtures.ts`
- Mapper tests: `src/features/compliance/statusMap.test.ts`
- Panel tests: `src/features/compliance/components/ComplianceStatusPanel.test.tsx`

## Mock Address Examples

- Approved: `GCFXCOMPAPPROVED0000000000000000000000000000000000000`
- Blocked: `GCFXCOMPBLOCKED00000000000000000000000000000000000000`
- Pending: `GCFXCOMPPENDING00000000000000000000000000000000000000`
- Revoked: `GCFXCOMPREVOKED000000000000000000000000000000000000000`
- Unknown: `GCFXCOMPUNKNOWN00000000000000000000000000000000000000`
- Unavailable: `GCFXCOMPUNAVAILABLE000000000000000000000000000000000`
19 changes: 19 additions & 0 deletions src/__tests__/sdk/provider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,25 @@ describe('MockAegisProvider', () => {
expect(await provider.checkWhitelist('GABC')).toBe(false);
});

it('getAddressCompliance returns fixture status for known addresses', async () => {
const record = await provider.getAddressCompliance(
'GCFXCOMPREVOKED000000000000000000000000000000000000000',
);
expect(record.status).toBe('revoked');
expect(record.address).toContain('REVOKED');
});

it('getAddressCompliance falls back to approved for long G-addresses', async () => {
const address = 'G' + 'A'.repeat(54);
const record = await provider.getAddressCompliance(address);
expect(record.status).toBe('approved');
});

it('getAddressCompliance returns unknown for unsupported addresses', async () => {
const record = await provider.getAddressCompliance('INVALID');
expect(record.status).toBe('unknown');
});

it('transfer returns SUCCESS for a standard amount', async () => {
const result = await provider.transfer('GCFXTEST', 100);
expect(result.status).toBe('SUCCESS');
Expand Down
14 changes: 7 additions & 7 deletions src/features/admin/components/ComplianceInfo.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,18 @@ describe('ComplianceInfo', () => {
const text = container.textContent || '';

expect(text).toContain('Approved');
expect(text).toContain('Not Approved');
expect(text).toContain('Revoked');
expect(text).toContain('Pending');
expect(text).toContain('Blocked');
expect(text).toContain('Unknown');
expect(text).toContain('Unavailable');

// Each status has an explanation
expect(text).toContain('fully approved');
expect(text).toContain('denied approval');
expect(text).toContain('under compliance review');
expect(text).toContain('regulatory restrictions');
expect(text).toContain('not yet determined');
expect(text).toContain('temporarily unavailable');
expect(text).toContain('marked approved in the protocol compliance registry');
expect(text).toContain('has been revoked');
expect(text).toContain('under protocol compliance review');
expect(text).toContain('blocked from protocol actions');
expect(text).toContain('No clear protocol compliance record');
expect(text).toContain('could not be retrieved right now');
});
});
3 changes: 1 addition & 2 deletions src/features/admin/components/ComplianceInfo.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { ComplianceStatus, COMPLIANCE_CONFIG } from '@/types/compliance';
import ComplianceBadge from '@/features/assets/components/ComplianceBadge';

const ALL_STATUSES: ComplianceStatus[] = [
'approved',
'not-approved',
'revoked',
'pending',
'blocked',
'unknown',
Expand Down
93 changes: 93 additions & 0 deletions src/features/compliance/components/ComplianceStatusPanel.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import ComplianceStatusPanel from '@/features/compliance/components/ComplianceStatusPanel';
import { COMPLIANCE_DISCLAIMER } from '@/lib/complianceReview';

const mockUseComplianceStatus = vi.fn();

vi.mock('@/features/compliance/hooks/useComplianceStatus', () => ({
useComplianceStatus: (...args: unknown[]) => mockUseComplianceStatus(...args),
}));

describe('ComplianceStatusPanel', () => {
beforeEach(() => {
mockUseComplianceStatus.mockReset();
});

it('prompts to connect when no address is provided', () => {
mockUseComplianceStatus.mockReturnValue({
status: 'idle',
record: null,
error: null,
refetch: vi.fn(),
});

render(<ComplianceStatusPanel address={null} />);

expect(screen.getByText(/connect a wallet/i)).toBeInTheDocument();
expect(screen.getByText(COMPLIANCE_DISCLAIMER)).toBeInTheDocument();
});

it('renders approved status with safe disclaimer copy', () => {
mockUseComplianceStatus.mockReturnValue({
status: 'ready',
record: {
address: 'GCFXCOMPAPPROVED0000000000000000000000000000000000000',
state: 'approved',
label: 'Approved',
explanation:
'This address is currently marked approved in the protocol compliance registry for dashboard actions.',
reasonCode: 'REGISTRY_APPROVED',
source: 'sdk',
},
error: null,
refetch: vi.fn(),
});

render(<ComplianceStatusPanel address="GCFXCOMPAPPROVED0000000000000000000000000000000000000" />);

expect(screen.getByText('Approved')).toBeInTheDocument();
expect(screen.getByText(/protocol compliance registry/i)).toBeInTheDocument();
expect(screen.getByText(COMPLIANCE_DISCLAIMER)).toBeInTheDocument();
expect(screen.getByText(/does not perform real-world KYC/i)).toBeInTheDocument();
});

it('renders revoked and unavailable labels distinctly', () => {
mockUseComplianceStatus.mockReturnValue({
status: 'ready',
record: {
address: 'GCFXCOMPREVOKED000000000000000000000000000000000000000',
state: 'revoked',
label: 'Revoked',
explanation: 'Prior protocol approval for this address has been revoked.',
source: 'fixture',
},
error: null,
refetch: vi.fn(),
});

const { rerender } = render(
<ComplianceStatusPanel address="GCFXCOMPREVOKED000000000000000000000000000000000000000" />,
);
expect(screen.getByText('Revoked')).toBeInTheDocument();

mockUseComplianceStatus.mockReturnValue({
status: 'ready',
record: {
address: 'GCFXCOMPUNAVAILABLE000000000000000000000000000000000',
state: 'unavailable',
label: 'Unavailable',
explanation: 'Compliance data could not be retrieved.',
source: 'fallback',
},
error: null,
refetch: vi.fn(),
});

rerender(
<ComplianceStatusPanel address="GCFXCOMPUNAVAILABLE000000000000000000000000000000000" />,
);
expect(screen.getByText('Unavailable')).toBeInTheDocument();
});
});
Loading
Loading