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
46 changes: 46 additions & 0 deletions docs/asset-registration-review.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Asset Registration Review Step

The RWA asset creation wizard (`src/features/asset-creation/components/AssetCreationWizard.tsx`)
requires a review-before-submit step so issuers confirm what they are sending
for compliance review. This documents the review flow and its warning states.

## Flow

```
form -> review -> success
```

1. **Form** — the issuer enters asset name, ticker, asset class, initial
requested supply, and jurisdiction. Field-level validation runs on
"Review request".
2. **Review** — a read-only summary of the request. The issuer must
explicitly confirm before anything is submitted.
3. **Success** — an admin action receipt confirms the request was created in
`pending` status.

## Review step contents

The review screen shows:

- **Validation summary** — a checklist confirming required fields, ticker
format/uniqueness, and supply bounds passed.
- **Request details** — asset name, ticker, asset class, issuer (the
submitting wallet, truncated), initial requested supply, jurisdiction, and
the target **network** (from `NEXT_PUBLIC_NETWORK_PASSPHRASE`, resolved via
`formatNetworkLabel`).
- **Warning states** — non-blocking amber warnings surfaced when:
- requested supply exceeds 50% of the soft cap
(`DEFAULT_ASSET_CREATION_MAX_AMOUNT`), or
- the jurisdiction is outside the supported list.

Warnings are informational only — they do not block submission, but they flag
requests that compliance review should scrutinise more closely.

## Notes

- The issuer address is truncated for display (`ABCDEF…7890`) and is never
used to make a legal or regulatory determination.
- The review is a protocol-level compliance check only and is not legal or
financial advice.
- Submitting creates an `IssuanceRequest` in `pending` status; the asset
becomes mintable only after a separate compliance approval step.
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,25 @@ describe('AssetCreationWizard', () => {
expect(screen.getByText('Request submitted')).toBeInTheDocument();
});

it('shows review details including issuer, network, and warnings', () => {
render(<AssetCreationWizard onCreate={vi.fn()} requestedBy="ABCDEF1234567890" />);

fireEvent.change(screen.getByLabelText(/asset name/i), {
target: { value: 'Frankfurt Logistics Fund' },
});
fireEvent.change(screen.getByLabelText(/ticker/i), { target: { value: 'FR-LOG2' } });
fireEvent.change(screen.getByLabelText(/initial requested supply/i), {
target: { value: '600000000' },
});

fireEvent.click(screen.getByText('Review request'));

expect(screen.getByText('ABCDEF…7890')).toBeInTheDocument();
expect(screen.getByText(/Network/)).toBeInTheDocument();
expect(screen.getByText(/Validation summary/)).toBeInTheDocument();
expect(screen.getByRole('alert')).toHaveTextContent(/exceeds 50% of the soft cap/i);
});

it('lets the user go back from review to fix the form', () => {
render(<AssetCreationWizard onCreate={vi.fn()} />);
fillValidForm();
Expand Down
72 changes: 71 additions & 1 deletion src/features/asset-creation/components/AssetCreationWizard.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
import { useState } from 'react';
import { AlertTriangle, CheckCircle2 } from 'lucide-react';
import {
validateAssetCreationRequest,
ASSET_CREATION_ERROR_MESSAGES,
SUPPORTED_JURISDICTIONS,
ASSET_CLASS_OPTIONS,
DEFAULT_ASSET_CREATION_MAX_AMOUNT,
type AssetCreationErrorCode,
} from '@/lib/assetCreationRequest';
import { useFormErrors, FormFieldError, FormError } from '@/features/forms/validation';
import {
AdminActionReceiptView,
mapAdminActionReceipt,
} from '@/features/admin/receipts';
import { getTargetNetwork, formatNetworkLabel } from '@/lib/environment';
import type { IssuanceRequest } from '@/fixtures/issuer';

type WizardStep = 'form' | 'review' | 'success';
Expand Down Expand Up @@ -170,9 +173,50 @@ export default function AssetCreationWizard({
}

if (step === 'review') {
const networkLabel = formatNetworkLabel(getTargetNetwork());
const parsedAmount = Number(amount);
const isHighSupply = parsedAmount > DEFAULT_ASSET_CREATION_MAX_AMOUNT * 0.5;
const isRestrictedJurisdiction = !SUPPORTED_JURISDICTIONS.includes(
jurisdiction.trim().toUpperCase() as (typeof SUPPORTED_JURISDICTIONS)[number],
);
const warnings: string[] = [];
if (isHighSupply) {
warnings.push(
'Requested supply exceeds 50% of the soft cap. Ensure compliance review can accommodate this volume.',
);
}
if (isRestrictedJurisdiction) {
warnings.push(
'Jurisdiction is outside the supported list. Additional compliance review may be required.',
);
}

return (
<div className="bg-white p-6 rounded-xl shadow-sm border border-slate-200">
<h2 className="text-xl font-bold mb-4">Review issuance request</h2>

{/* Validation summary */}
<div className="mb-6 rounded-lg border border-slate-200 bg-slate-50 p-4">
<h3 className="text-sm font-semibold text-slate-700 mb-2 flex items-center gap-1.5">
<CheckCircle2 size={16} className="text-green-600" aria-hidden="true" />
Validation summary
</h3>
<ul className="space-y-1 text-sm text-slate-600">
<li className="flex items-center gap-1.5">
<CheckCircle2 size={14} className="text-green-600 shrink-0" aria-hidden="true" />
All required fields are present and valid.
</li>
<li className="flex items-center gap-1.5">
<CheckCircle2 size={14} className="text-green-600 shrink-0" aria-hidden="true" />
Ticker format is valid and not a duplicate.
</li>
<li className="flex items-center gap-1.5">
<CheckCircle2 size={14} className="text-green-600 shrink-0" aria-hidden="true" />
Supply is a positive number within the soft cap.
</li>
</ul>
</div>

<dl className="space-y-3 mb-6 text-sm">
<div className="flex justify-between border-b border-slate-100 pb-2">
<dt className="text-slate-500">Asset name</dt>
Expand All @@ -188,18 +232,44 @@ export default function AssetCreationWizard({
<dt className="text-slate-500">Asset class</dt>
<dd className="font-medium text-slate-900">{assetClass}</dd>
</div>
<div className="flex justify-between border-b border-slate-100 pb-2">
<dt className="text-slate-500">Issuer</dt>
<dd className="font-medium text-slate-900 font-mono">
{requestedBy ? `${requestedBy.slice(0, 6)}…${requestedBy.slice(-4)}` : 'Unknown'}
</dd>
</div>
<div className="flex justify-between border-b border-slate-100 pb-2">
<dt className="text-slate-500">Initial requested supply</dt>
<dd className="font-medium text-slate-900">
{Number(amount).toLocaleString('en-US')}
</dd>
</div>
<div className="flex justify-between">
<div className="flex justify-between border-b border-slate-100 pb-2">
<dt className="text-slate-500">Jurisdiction</dt>
<dd className="font-medium text-slate-900">{jurisdiction.trim().toUpperCase()}</dd>
</div>
<div className="flex justify-between">
<dt className="text-slate-500">Network</dt>
<dd className="font-medium text-slate-900">{networkLabel}</dd>
</div>
</dl>

{/* Warning states */}
{warnings.length > 0 && (
<div className="mb-4 space-y-2">
{warnings.map((warning, idx) => (
<div
key={idx}
role="alert"
className="flex items-start gap-2 rounded bg-amber-50 border border-amber-200 p-3 text-sm text-amber-700"
>
<AlertTriangle size={16} className="mt-0.5 shrink-0" aria-hidden="true" />
<span>{warning}</span>
</div>
))}
</div>
)}

<FormError message={formError} />

<p className="text-xs text-slate-500 mb-4">
Expand Down
Loading