Skip to content
Open
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
70 changes: 70 additions & 0 deletions docs/issuer-assumptions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# Issuer Asset Registration — Assumptions & Notes

## Overview

The `/register-asset` page provides an issuer-facing UI to submit metadata for a new Real-World Asset (RWA) and prepare a signed Soroban registration transaction.

---

## Issuer Assumptions

1. **Wallet connected** — The issuer must have the Freighter browser extension installed and their Stellar account connected before the registration form is accessible.

2. **Issuer responsibility** — Aegis does **not** verify the legal status, ownership, or regulatory compliance of the underlying asset. Issuers are solely responsible for ensuring their submissions comply with applicable laws and regulations in their jurisdiction.

3. **Document URI** — The `documentUri` field must reference a publicly accessible document (either via IPFS or HTTPS) that describes the asset (e.g., a prospectus, deed, or offering memorandum). The protocol stores only the URI on-chain; the content is not validated.

4. **Ticker uniqueness** — The ticker symbol must be unique across the protocol. The UI performs an async availability check against the contract before allowing submission.

5. **Jurisdiction code** — The `jurisdiction` field accepts [ISO-3166-1 alpha-2](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) country codes (e.g., `US`, `GB`, `DE`). This is used for informational display only and does not trigger jurisdiction-specific compliance logic on the contract side in the current version.

6. **Total supply is fixed at registration** — Once a supply is submitted and anchored on-chain, it cannot be changed through this UI. Administrative re-issuance flows are handled separately by protocol admins.

---

## Multi-Step Flow

| Step | Description |
|---|---|
| **1. Input** | Issuer fills in asset metadata fields with inline validation. |
| **2. Review** | Read-only summary displayed before any signing occurs. |
| **3. Receipt** | Success (tx hash + contract ID) or error state with retry option. |

---

## SDK Integration

All contract interactions are centralised in `src/lib/aegisSdk.ts`. The `registerAsset()` function:

1. Builds the `register_asset` Soroban invocation XDR.
2. Requests a signature from Freighter via `signTransaction()`.
3. Submits the signed XDR to the Stellar Soroban RPC.
4. Polls for ledger inclusion and returns the transaction hash and contract ID.

> **Note:** The `@aegis/sdk` package is not yet published. `aegisSdk.ts` currently contains a mock implementation. Replace the mock bodies with real SDK calls once the package is available.

---

## Validation Rules (client-side)

| Field | Rule |
|---|---|
| `name` | Required; max 80 characters |
| `ticker` | Required; 2–12 uppercase letters, digits, or hyphens (`[A-Z0-9\-]{2,12}`) |
| `assetType` | Required; must be one of the predefined options |
| `totalSupply` | Required; positive whole number |
| `documentUri` | Required; must start with `ipfs://`, `http://`, or `https://` |
| `jurisdiction` | Required; 2-letter ISO-3166-1 alpha-2 code |
| `description` | Optional; max 500 characters |

---

## Development Fixtures

Pre-filled test data is available in `src/fixtures/assetFixtures.ts`. Import `FIXTURE_REAL_ESTATE` or `FIXTURE_TREASURY` during local development to skip manual form entry.

---

## Legal Disclaimer

> Submitting an asset registration through this UI does **not** constitute legal verification, regulatory approval, or endorsement of the underlying asset by Aegis or its contributors. All submitted metadata is stored as-is on the Stellar blockchain.
5 changes: 5 additions & 0 deletions next-env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />

// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.
314 changes: 314 additions & 0 deletions src/components/AssetRegistrationForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,314 @@
/**
* AssetRegistrationForm – multi-step form for issuer asset registration.
*
* Steps:
* 1. Input – issuer fills out metadata fields
* 2. Review – read-only summary before signing
* 3. Receipt – success or error result
*
* The form is self-contained so it can be embedded anywhere or used as the
* sole content of the /register-asset page.
*/

import { useState } from 'react';
import { useWallet } from '@/hooks/useWallet';
import {
validateAssetMetadata,
fieldError,
type AssetMetadata,
type ValidationError,
} from '@/lib/validateAssetMetadata';
import {
registerAsset,
type RegisterAssetResult,
type SdkError,
} from '@/lib/aegisSdk';
import ReviewStep from '@/components/ReviewStep';
import { SuccessReceipt, ErrorReceipt } from '@/components/RegistrationReceipt';
import { ASSET_TYPE_OPTIONS } from '@/fixtures/assetFixtures';

// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------

type Step = 'input' | 'review' | 'receipt';

const EMPTY_FORM: AssetMetadata = {
name: '',
ticker: '',
assetType: '',
totalSupply: '',
documentUri: '',
jurisdiction: '',
description: '',
};

// ---------------------------------------------------------------------------
// Component
// ---------------------------------------------------------------------------

export default function AssetRegistrationForm() {
const { address } = useWallet();
const [step, setStep] = useState<Step>('input');
const [form, setForm] = useState<AssetMetadata>(EMPTY_FORM);
const [errors, setErrors] = useState<ValidationError[]>([]);
const [isSubmitting, setIsSubmitting] = useState(false);
const [result, setResult] = useState<RegisterAssetResult | null>(null);
const [sdkError, setSdkError] = useState<string | null>(null);

// -------------------------------------------------------------------------
// Handlers
// -------------------------------------------------------------------------

function handleChange(
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>
) {
const { name, value } = e.target;
setForm((prev) => ({ ...prev, [name]: value }));
// Clear the error for this field on change
setErrors((prev) => prev.filter((err) => err.field !== name));
}

function handleReview(e: React.FormEvent) {
e.preventDefault();
const validation = validateAssetMetadata(form);
if (!validation.valid) {
setErrors(validation.errors);
return;
}
setErrors([]);
setStep('review');
}

async function handleSubmit() {
if (!address) return;
setIsSubmitting(true);
setSdkError(null);
try {
const res = await registerAsset({ metadata: form, issuerAddress: address });
setResult(res);
setStep('receipt');
} catch (err) {
const sdkErr = err as SdkError;
setSdkError(sdkErr.message ?? 'An unexpected error occurred. Please try again.');
setStep('receipt');
} finally {
setIsSubmitting(false);
}
}

function handleRetry() {
setSdkError(null);
setResult(null);
setStep('review');
}

// -------------------------------------------------------------------------
// Render
// -------------------------------------------------------------------------

if (step === 'review') {
return (
<ReviewStep
metadata={form}
issuerAddress={address!}
onConfirm={handleSubmit}
onBack={() => setStep('input')}
isSubmitting={isSubmitting}
/>
);
}

if (step === 'receipt') {
if (result) {
return (
<SuccessReceipt result={result} assetName={form.name} ticker={form.ticker} />
);
}
return <ErrorReceipt message={sdkError ?? 'Unknown error.'} onRetry={handleRetry} />;
}

// ----- Input step -----
return (
<form onSubmit={handleReview} noValidate className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
{/* Asset Name */}
<Field
label="Asset Name"
hint="Full name of the real-world asset"
error={fieldError(errors, 'name')}
required
>
<input
name="name"
type="text"
value={form.name}
onChange={handleChange}
placeholder="Manhattan Commercial Real Estate"
className={inputCls(!!fieldError(errors, 'name'))}
/>
</Field>

{/* Ticker */}
<Field
label="Ticker Symbol"
hint="2-12 uppercase letters/digits/hyphens"
error={fieldError(errors, 'ticker')}
required
>
<input
name="ticker"
type="text"
value={form.ticker}
onChange={handleChange}
placeholder="NY-CRE"
className={inputCls(!!fieldError(errors, 'ticker'))}
style={{ textTransform: 'uppercase' }}
/>
</Field>

{/* Asset Type */}
<Field
label="Asset Type"
error={fieldError(errors, 'assetType')}
required
>
<select
name="assetType"
value={form.assetType}
onChange={handleChange}
className={inputCls(!!fieldError(errors, 'assetType'))}
>
<option value="">Select a type…</option>
{ASSET_TYPE_OPTIONS.map((o) => (
<option key={o.value} value={o.value}>
{o.label}
</option>
))}
</select>
</Field>

{/* Total Supply */}
<Field
label="Total Supply"
hint="Whole number of tokens to issue"
error={fieldError(errors, 'totalSupply')}
required
>
<input
name="totalSupply"
type="number"
min="1"
step="1"
value={form.totalSupply}
onChange={handleChange}
placeholder="10000"
className={inputCls(!!fieldError(errors, 'totalSupply'))}
/>
</Field>

{/* Jurisdiction */}
<Field
label="Jurisdiction"
hint="ISO-3166-1 alpha-2 country code (e.g. US)"
error={fieldError(errors, 'jurisdiction')}
required
>
<input
name="jurisdiction"
type="text"
maxLength={2}
value={form.jurisdiction}
onChange={handleChange}
placeholder="US"
className={inputCls(!!fieldError(errors, 'jurisdiction'))}
style={{ textTransform: 'uppercase' }}
/>
</Field>

{/* Document URI */}
<Field
label="Document URI"
hint="IPFS CID or HTTPS link to the offering document"
error={fieldError(errors, 'documentUri')}
required
>
<input
name="documentUri"
type="text"
value={form.documentUri}
onChange={handleChange}
placeholder="ipfs://bafybei… or https://…"
className={inputCls(!!fieldError(errors, 'documentUri'))}
/>
</Field>
</div>

{/* Description (full width) */}
<Field
label="Description"
hint="Optional — max 500 characters"
error={fieldError(errors, 'description')}
>
<textarea
name="description"
value={form.description}
onChange={handleChange}
rows={3}
placeholder="Briefly describe the asset for prospective investors…"
className={inputCls(!!fieldError(errors, 'description'))}
/>
</Field>

{/* Disclaimer */}
<p className="text-xs text-slate-400 leading-relaxed">
Submitting this form does not constitute legal verification or regulatory approval of the
underlying asset. Ensure all submitted information complies with applicable laws in your
jurisdiction.
</p>

<button
type="submit"
className="w-full bg-aegis-brand hover:bg-blue-600 text-white py-3 rounded-lg font-semibold text-sm transition shadow-sm"
>
Review Registration →
</button>
</form>
);
}

// ---------------------------------------------------------------------------
// Sub-components
// ---------------------------------------------------------------------------

interface FieldProps {
label: string;
hint?: string;
error?: string;
required?: boolean;
children: React.ReactNode;
}

function Field({ label, hint, error, required, children }: FieldProps) {
return (
<div className="flex flex-col gap-1">
<label className="text-sm font-medium text-slate-700">
{label}
{required && <span className="text-red-500 ml-0.5">*</span>}
</label>
{hint && <p className="text-xs text-slate-400">{hint}</p>}
{children}
{error && <p className="text-xs text-red-500 mt-0.5">{error}</p>}
</div>
);
}

function inputCls(hasError: boolean) {
return [
'w-full border rounded-lg p-2.5 text-sm outline-none transition',
'focus:ring-2 focus:ring-aegis-brand',
hasError
? 'border-red-400 bg-red-50 focus:ring-red-300'
: 'border-slate-300 bg-white',
].join(' ');
}
1 change: 1 addition & 0 deletions src/components/Navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export default function Navbar() {
</Link>
<div className="hidden md:flex space-x-4 text-sm font-medium text-slate-600">
<Link href="/portfolio" className="hover:text-aegis-brand transition">Portfolio</Link>
<Link href="/register-asset" className="hover:text-aegis-brand transition">Register Asset</Link>
<Link href="/admin" className="hover:text-aegis-brand transition">Admin</Link>
</div>
</div>
Expand Down
Loading