# Spec 14 — Reference UI (PWA wallet & webapp)

**Status:** draft  
**Related:** [`11-leaf-host.md`](11-leaf-host.md), [`12-rpc.md`](12-rpc.md), [`03-transactions.md`](03-transactions.md), [`04-proofs.md`](04-proofs.md), [`16-sponsored-registration.md`](16-sponsored-registration.md), [`00-overview.md`](00-overview.md), whitepaper §1.6 / §3 / §5 / §6  
**GIP:** [`../gips/gip-5.md`](../gips/gip-5.md)

This document is the **normative UI coverage map**: every whitepaper / L0 user capability, how the reference client exposes it, and which node API it consumes. Protocol validity stays in specs 02–04 / 07 / 16; this spec only defines screens, flows, and API wiring.

---

## 1. Goals

- Ship a **reference client** without prioritizing native mobile apps first.  
- **Primary wallet UX:** installable **PWA** — register, send, balance, activity, account management, **groups**, **threshold cosign**.  
- **Target journey** (whitepaper §1.6): hear about Guld → guld.io or install software → name + fee → sponsor → PWA with on-device keys → **browser extension** for site login → many gapps, one identity.  
- **Optional external signing** for users who refuse in-browser keys.  
- **Webapp** also hosts explorer, whitepaper, specs, docs browser, software catalog.

The **browser extension** is **ecosystem software** (not required to validate the chain) but is **in scope for the target user story** as the bridge for “log into this website as my Guld name.”

### 1.1 Implementation status (surfaces)

| Surface | Normative (spec) | Shipped today |
|---------|------------------|---------------|
| **guld.io PWA** | AES-256-GCM keyring + full §8–§10 flows | **Partial** — register (individual + group), send/subs/memo, UpdateMaster/RotateKeys + **cosign workstation**; Transfer cosign shipped |
| **Browser extension** | Same keyring schema as PWA; site-login; hosts `guld-web-ui`; pairing §10.2 | Full-tab `guld-web-ui`; compact popup subset; site-login; `guld_pairOffer` |
| `guld-wallet` (Dioxus desktop) | Encrypted file keyring or OS keychain | Deprecated for default path; legacy claim still supported |
| Mobile native | Later | PWA covers cross-platform first |

Living feature matrix: **§3**.

---

## 2. Reference surfaces & stack

| Surface | Path / package | Role |
|---------|----------------|------|
| **Landing** | `/` (`index.html`) | Brand + entry to wallet / register / explorer |
| **Dashboard** | `/dashboard/` | Logged-in home: profile peek, balances, gapp status |
| **Profile** | `/profile/#/<name>` | Human public/owner view (bio/remotes); not explorer |
| **Wallet PWA** | `/wallet/` + `src/guld-web-ui/` | Daily: lookup, send, activity, account mgmt |
| **Contacts** | `/contacts/` | Address book, import, private invite compose ([GIP-31](../gips/gip-31.md)) |
| **Register** | `/register/` | Individual (and later group) onboarding wizard |
| **Login / settings** | `/login/`, `/settings/` | Unlock keyring; API base; OTC desk prefs; extension pair; link to contacts |
| **Key manager** | `/keys/` | Key table + `#/<name>` details; pubkey mapping; import / generate / export; change passphrase |
| **Gateway** | `/gateway/` | Optional paid registrar desk (spec 16) |
| **Claim** | `/claim/` | `ClaimLegacy` for 1.0 holders |
| **Explorer** | `/explorer/` | Blocks, confirmed txs, **mempool**, accounts (hash routes) |
| **Docs / specs / whitepaper** | `/docs/`, `/specs/`, `/whitepaper/` | Markdown browsers |
| **Software** | `/software/` | Package catalog + clone URLs (protocol **and** reference gapps) |
| **Developers** | `/developers/` | Core + gapp walkthroughs |
| **Reference gapps (hub mounts)** | `/apps/`, `/ttt/`, `/dex/`, `/chat/` | Optional sidecars; same-origin under guld.io (legacy `*.guld.io` may still exist). Core gapps share site chrome with explorer/wallet where mounted; suite session via `guld_suite_session` when configured. |
| **guld-web-ui** | `src/guld-web-ui/` | Shared wallet UI leaf ([GIP-32](../gips/gip-32.md)) |
| **Browser extension** | `src/guld-extension/` | Full-tab UI host + gapp site-login + pairing |
| **Desktop (optional)** | `src/guld-wallet/` | File keyring, PGP claim, external signer |
| **Node** | `guld-node --http` | Canonical `/api/v1/…`; MUST NOT store keys |

**Reference stack (PWA):** repo-root static tree + local or remote `guld-node --http` (optionally `--http-static .`). **Web stack:** framework-less JS — web components, ES modules, CSS tokens (no React/Vue/bundler). No `guld-api`. No Node.js build for the site.

**API preference:** wallets and gapps SHOULD use **HTTP** `/api/v1/…`. **JSON-RPC** (`guld_*` methods on `--rpc` / same-origin `POST /rpc`) is a **transitional adapter** with the same logical ops ([`12-rpc.md`](12-rpc.md)). The explorer today reads via RPC; new wallet code SHOULD prefer HTTP and MAY fall back to RPC when only `--rpc` is available.

Clients MUST NOT embed consensus validation. The former Python `guld-api` façade was **removed**.

### 2.1 Optional paid registrar

Any funded peer MAY enable **paid registrations** by connecting a supported third-party payment service and fulfilling spec 16 as sponsor. guld.io may ship with this **on**; other clones default **off**. GIP: [`../gips/gip-8.md`](../gips/gip-8.md).

The wallet MUST still support **friend sponsor** (paste/QR registration JSON) without any payment gateway.

### 2.2 Testnet vs mainnet (durable)

Peers expose `mode` (`testnet` | `mainnet`), `network`, and optional `faucet` on `GET /api/v1/chain/status`. The reference UI MUST:

- Show a site-wide **footer network switcher/indicator** derived from that peer (not a hard-coded “beta forever” string). Do not use a top-of-page testnet banner.
- Display the native currency as **`tGULD`** on testnet and **`GULD`** on mainnet (display ticker only; protocol amounts remain GULD quanta).
- Keep **settings presets** for both testnet and mainnet API bases after mainnet launch.
- On testnet when `faucet.ready`, offer **faucet register** (`POST /api/v1/faucet/register`) and **10 GULD drip** (`POST /api/v1/faucet/drip`) on register / wallet. Mainnet peers MUST NOT enable faucet routes.

Ops: [`../../deploy/SIMBA.md`](../../deploy/SIMBA.md).

---

## 3. Coverage matrix (protocol → API → UI)

Status: **shipped** | **partial** | **missing** | **out of UI** (node/miner/ops only) | **later** (roadmap, not blocking groups/cosign).

### 3.1 Transaction types ([`03-transactions.md`](03-transactions.md))

| Tx type | User need | Node API | Reference UI | Status |
|---------|-----------|----------|--------------|--------|
| `RegisterUsername` | Claim individual name | Fee estimate + `POST …/transactions` (or `guld_sendTransaction`) | `/register/` + friend JSON + gateway + **testnet faucet** | **shipped** |
| `RegisterGroup` | Multi-key group name; fee `F_group(L,n)` | Same + `kind=group`, `nKeys` | Group create wizard (§8.6) | **shipped** |
| `RegisterSubaccount` | `parent.label` device wallets (max 8) | Fee `kind=subaccount` + broadcast | Wallet account → create sub | **shipped** |
| `Transfer` | Send GULD name→name (1:1 or multi-recipient `outputs[]` after [GIP-43](../gips/gip-43.md)); optional memo ≤64 B | Broadcast | Wallet Send | **shipped** (1:1 + multi-out + threshold cosign; gated by `multi_leg_transfer=v1`) |
| `JointTransfer` | Multi-account n-of-n settle ([GIP-43](../gips/gip-43.md) Phase B) | Broadcast + per-input auth | Cosign workstation (per-input) | **partial** (digest/cosign legs UI; full multi-account assemble TBD) |
| `UpdateMaster` | Advance home tip under threshold | Broadcast + `threshold_cosign_v1` | Account → UpdateMaster + cosign workstation | **shipped** |
| `RotateKeys` | Change keys/threshold (hygiene, not resale) | Broadcast + dual auth | Account → RotateKeys + cosign workstation | **shipped** |
| `SettleRegistration` | Pay-or-release yearly name | Permissionless; miners enqueue | Wallet “Keep name funded” / renew hint (§8.9) | **partial** (expiry display TBD; no user settle button required — miners settle) |
| `ClaimLegacy` | Unlock 1.0 balance with PGP / attestation | Broadcast | `/claim/` | **shipped** |
| `ConvertAccountKind` | Flip individual ↔ group without releasing name | Broadcast + dual auth | Settings / account tools | **shipped** |
| `UnregisterAccount` | Voluntary early name release ([GIP-33](../gips/gip-33.md)) | Broadcast + threshold | Sub close / advanced release | **missing** (activate [055](../tasks/open/055-simba-single-rule-bundle.md)) |
| `CreateLock` | Escrow lock + optional SHA-256 HTLC ([GIP-40](../gips/gip-40.md)) | Broadcast + threshold | Wallet Escrow / Lock | **partial** (1-of-1 create/claim/refund; needs activation + threshold cosign) |
| `ClaimLock` | Claim locked GULD (sig and/or preimage) | Broadcast + claim witness | Wallet Escrow claim | **partial** |
| `RefundLock` | Timeout refund to `refund_to` | Broadcast (+ optional permissionless) | Wallet Escrow refund | **partial** |
| `UpdateBio` | Set/clear ≤128 B name profile ([GIP-34](../gips/gip-34.md)) | Broadcast | Account tools | **missing** (activate [055](../tasks/open/055-simba-single-rule-bundle.md)) |

### 3.2 Identity & proofs (whitepaper §3 / specs 02, 04)

| Capability | Node API | Reference UI | Status |
|------------|----------|--------------|--------|
| Name lookup, balance, keys, threshold, master | `GET …/accounts/{name}` / `guld_getAccount` | Wallet + explorer account | **shipped** |
| Activity feed | `…/activity` / `guld_getAccountActivity` | Wallet Activity; explorer links | **shipped** |
| Exists / fee estimate | `…/exists`; `guld_estimateRegistrationFee` | Register + Send hints | **shipped** |
| Threshold policy display | account JSON `threshold`, `keys[]` | Explorer + wallet | **shipped** |
| **Cosign workstation** (collect ≥`threshold` sigs) | Client-side; broadcast when complete | Shared partial-cosign import/export (§9) | **shipped** |
| Find accounts by pubkey | `guld_findAccountsByPubkey` / `GET /chain/accounts?pubkey=` | Key manager + explorer `#/key/<0x…>` | **shipped** |
| Prefix name search | `guld_searchAccounts` | Explorer / wallet Send typeahead | **shipped** |
| Local contacts / recent / favorites | — (local storage) | Send typeahead + **`/contacts/`** ([GIP-31](../gips/gip-31.md) Accepted) | **partial** — v1 shipped; address book + private invite [038](../tasks/open/038-contacts-private-invite.md) |
| Account bio display | account JSON `bio` | Explorer + wallet + contacts | **missing** ([GIP-34](../gips/gip-34.md)) |
| Cross-chain / bridge dapps (no L0 reserved names — A11) | Spec 13 informative | N/A for Simba wallet | **later** (dapp layer) |
| Leaf / CAS put-get in wallet | `guld_putObject` / `getObject` | Not required for L0 wallet; leaf host | **later** / ops |

### 3.3 Chain / explorer / ops

| Capability | Node API | Reference UI | Status |
|------------|----------|--------------|--------|
| Tip, chain id, ready | `GET /chain/status` / RPC | Explorer home; wallet status | **shipped** |
| Block by height | `guld_getBlockByNumber` | `#/block/<h>` | **shipped** |
| Block by hash | `guld_getBlockByHash` | Deep link / search | **partial** (RPC shipped; explorer UI next) |
| Tx by id | `guld_getTransaction` | Explorer search + `#/tx/<0x…>` (confirmed or mempool) | **shipped** |
| Tx by height:index | — | Legacy `#/tx/<h>/<i>` redirects to TxId | **deprecated** |
| Mempool snapshot | `GET /chain/mempool` / `guld_getMempool` | Explorer home + `#/mempool` | **shipped** |
| Live tip + mempool | `GET /chain/events` (SSE) | Explorer home + `#/mempool` | **shipped** (GIP-19) |
| Mempool fee hints / weight | `guld_getMempoolFeeHints`, `guld_estimateWeight` | Send fee defaults | **partial** (RPC shipped; fee UI next) |
| Rules hash / schedule | `guld_getGuldRulesHash`, `guld_getRulesSchedule` | Ops / about (optional) | **later** |
| Peer count / node info | `guld_peerCount`, `guld_nodeInfo` | Optional status chip | **partial** |
| Force mine (dev) | `guld_mineBlock` | **out of UI** | — |
| Escrow lock by id | `guld_getLock` / `GET …/locks/{id}` | Explorer `#/lock/<0x…>` | **shipped** |
| Account locks | `guld_getAccountLocks` / `GET …/accounts/{name}/locks` | Explorer account + wallet Escrow | **shipped** |

### 3.4 Site (non-consensus)

| Surface | Status |
|---------|--------|
| Docs / specs / whitepaper browsers | **shipped** |
| Software catalog + tree/blob | **shipped** (MVP) — includes reference gapps |
| Developers walkthroughs | **shipped** — `/developers/` |
| Gapp directory link | **shipped** — `/apps/` |
| Leaf standards docs | **shipped** — `/docs/leaves/` |
| Account remotes UI | **shipped** — wallet UpdateRemotes + explorer display ([GIP-36](../gips/gip-36.md)) |
| PWA install polish | **partial** |
| Extension site-login | **shipped** (Phase 3) — §10.1; `/demo/login/` |

---

## 4. API consumption map

Logical ops: [`12-rpc.md`](12-rpc.md). Prefer HTTP where a route exists.

| Screen / flow | Prefer (HTTP) | RPC equivalent | Notes |
|---------------|---------------|----------------|-------|
| Status strip | `GET /chain/status` | `guld_blockNumber`, `guld_chainId`, `guld_ready` | Includes `mode` / `network` / `faucet` |
| Account card | `GET /chain/accounts/{name}` | `guld_getAccount` | |
| Activity | `GET …/activity` | `guld_getAccountActivity` | Use `tx_id` for explorer deep links (`#/tx/<0x…>`) |
| Exists while typing | `GET …/exists` | `guld_accountExists` | Debounce |
| Registration fee | `GET /chain/fees/registration?…` (when present) | `guld_estimateRegistrationFee` | Pass `kind`, `nKeys` for groups |
| Broadcast signed tx | `POST /chain/transactions` | `guld_sendTransaction` | **PWA write path** |
| Paid desk | `GET /registrar`, gateway orders | — | Spec 16; out of consensus |
| Testnet faucet | `GET/POST /faucet…` | — | Drip + free register; testnet only |
| Explorer blocks | — (today) | `guld_getBlockByNumber` | MAY add HTTP later |
| Explorer mempool | `GET /chain/mempool` | `guld_getMempool` | Pending txs until mined |
| Explorer account | — or HTTP | `guld_getAccount` + activity | |
| CAS (tools) | — | `guld_putObject` / `guld_getObject` | Not default wallet chrome |
| Pubkey reverse lookup | `GET /chain/accounts?pubkey=` | `guld_findAccountsByPubkey` | Key manager + explorer `#/key/<0x…>` |

**Signing:** all fee-paying txs are built and signed **in the client** (JS/WASM aligned with `guld-client` message tags). The node MUST NOT receive private keys.

**Memo:** optional UTF-8 ≤64 bytes on fee-paying txs ([`03-transactions.md`](03-transactions.md) §2.1); Transfer UI ships a memo field; other builders SHOULD expose the same optional field where useful (order ids).

---

## 5. Security requirements (reference wallet)

Normative crypto: [`01-cryptography.md`](01-cryptography.md) §3.1.

| Rule | Requirement |
|------|-------------|
| Key storage (browser) | **AES-256-GCM** encrypted keyring; **PBKDF2-SHA256** (≥310k iter) from user passphrase; MUST NOT persist `privHex` in plaintext |
| Unlock / lock | One **keyring passphrase** (not per-key) unlocks all keys into memory; **sessionStorage** keeps the tab unlocked across MPA navigations until Lock / clear / tab close. Signing and security actions re-confirm the passphrase. Clients MUST offer **change passphrase** (re-wrap all ciphertext under a new passphrase; fresh salt/IV per record). |
| Multi-key accounts | Keyring MAY hold several named key slots; UI MUST show which `key_index` the device holds for a group |
| External signing | Unsigned payloads exportable; signed payloads / cosign fragments importable; no key required in browser |
| Confirmations | User confirms sends, registrations, rotations, and broadcasts of completed cosign sets |
| Node trust | Label operator-hosted API as light-client / custodial risk; power users run own node |
| Leaf content | Wallet MUST NOT implement leaf/CAS encryption — opaque bytes only |

---

## 6. Non-goals (reference UI)

- Replacing full node or leaf-host runtimes  
- Browser extension as required protocol surface  
- Native mobile-first roadmap blocking PWA reference  
- QR codes for **routine** sends (names are typed); QR MAY be used for sponsor handoff and contact cards  
- On-chain friend graphs or social feeds  
- Interpreting leaf politics (“why we signed”) — only collect and verify threshold proofs  
- User-facing `SettleRegistration` broadcast as the primary renew UX (miners settle; wallet keeps balance funded)  
- Full foreign-chain light-client UX in Phase 1–2 of this spec  

---

## 7. Information architecture

### 7.1 Primary nav (marketing + app)

| Entry | Lands on |
|-------|----------|
| Wallet | `/wallet/` — requires unlock for writes |
| Get a name | `/register/` |
| Explorer | `/explorer/` |
| Docs / Specs / Whitepaper / Software | respective trees |

### 7.2 Wallet shell (after unlock)

| Tab / panel | Purpose |
|-------------|---------|
| **Home / Account** | Balance, kind, threshold, expiry hint, keys summary, deep links |
| **Send** | Transfer + memo; contacts combobox; multi-recipient rows ([GIP-43](../gips/gip-43.md) after activation) |
| **Joint pay** | `JointTransfer` assemble + cosign (`op=joint_transfer`) |
| **Escrow** | CreateLock / ClaimLock / RefundLock (GIP-40; 1-of-1 MVP) |
| **Activity** | Local view of `guld_getAccountActivity` |
| **Contacts** | Link to `/contacts/` — address book, import, **Invite friend** (private share) |
| **Account tools** | UpdateMaster, RotateKeys, subaccounts, **Login key** / **Signing key** (`.auth` / `.sign` — [GIP-47](../gips/gip-47.md)), **Cosign**, **Create group** (or link to `/register/?kind=group`) |
| **Settings** | API base URL, OTC desk prefs, link to contacts / key manager, sponsor a name, advanced / claim |
| **Key manager** (`/keys/`) | Table of local keys; `#/<name>` details (mapping, export, remove); generate / import; change passphrase |

Profile menu: account switcher + links to dashboard / profile / key manager / wallet / contacts / settings (import/export live on `/keys/`; bio/remotes on `/profile/`).

Wallet secondary CTA **Invite** MUST open `/contacts/` (compose or list), not an OTC-only URL alone.

Sponsored friend path remains available from Register, Contacts invite (sponsor offer), and Settings / Wallet Advanced (“Sponsor a name” → paste registration JSON).

### 7.3 Explorer hash routes

| Route | Source | Shows |
|-------|--------|--------|
| `#/` | tip + recent blocks/txs + **mempool strip** | home |
| `#/mempool` | `guld_getMempool` | full unconfirmed list |
| `#/tx/<0x…>` | `guld_getTransaction` | tx detail (confirmed or mempool); `#/tx/unconfirmed/<id>` and `#/tx/pending/<id>` redirect here |
| `#/block/<height>` | `guld_getBlockByNumber` | header, miner, tx list (by TxId) |
| `#/tx/<height>/<index>` | block body → TxId | **legacy** — redirects to `#/tx/<0x…>` |
| `#/key/<0x…>` | `guld_findAccountsByPubkey` | accounts that include this pubkey (reuse / cosigner role) |
| `#/account/<name>` | account + activity + open locks | balance, **kind**, **keys** (link to `#/key/…`), **threshold**, expiry, escrow locks, activity |
| `#/lock/<0x…>` | `guld_getLock` | amount, status, creator, refund_to, claim, heights, preimage_hash |
| Search box | parse query | name → account; digits → block; `0x…` → tx, then pubkey hits, then block |

Wherever a username, block height, TxId, or pubkey is shown, link to the matching route. Do not use `height:index` as a durable locator (reorgs can change it). Prefix browse requires `guld_searchAccounts` — not Phase 1 of explorer.

---

## 8. Wallet flows (normative screens)

### 8.1 First-run wizard (PWA)

Empty keyring opens setup:

| Path | Who | Steps |
|------|-----|--------|
| **New individual name (default)** | Most users | Passphrase → pick name → fee estimate → keygen (encrypted) **or** export unsigned → sponsor (friend JSON/QR **or** paid desk) → poll until registered |
| **New group name** | Orgs / multisig | See §8.6 |
| **Import key** | Returning device | Passphrase + scan export QR (or paste secret) → encrypt per spec 01 §3.1 |
| **Legacy 1.0 claim** | Import holders | `/claim/` (PGP or attestation) |
| **External signing only** | Paranoid / hardware | Never store key; build requests in browser; sign elsewhere; paste signed JSON / cosign fragments |

Pending registration name + request JSON MUST persist locally so users can leave and return.

### 8.2 Main shell — Send & Activity

**Send:** from unlocked account → `to` name → amount → optional memo → confirm → sign (`threshold==1` spend sig today; n-of-m via §9) → broadcast → append `to` to `recent_recipients`.

**Activity:** list from node; link each item to explorer `#/tx/<0x…>` when `tx_id` is present.

### 8.3 Contacts & Send UX

Local only — no on-chain friend graph. GIP: [`../gips/gip-20.md`](../gips/gip-20.md) (lookup + typeahead); address book + private invite: [`../gips/gip-31.md`](../gips/gip-31.md), design [`../design/contacts-and-private-invite.md`](../design/contacts-and-private-invite.md).

| Feature | Storage | Chain |
|---------|---------|--------|
| **Recent recipients** | Local `recent_recipients[]` | Names only; backfilled from activity counterparties |
| **Address book** | Local `contacts[]` (`guld.contacts.v2`) | Optional `guldName` link |
| **Favorites** | `contacts[].favorite` | — |
| **Display / labels** | `displayName`, `labels`, off-chain emails/phones/socials | Display only; tx uses `guldName` |
| **Send typeahead** | UI | Local favorites → recent → contacts **with** `guldName`, then `guld_searchAccounts` / `GET /chain/accounts?prefix=` |
| **Exists hint** | API | Debounced lookup |
| **Private invite** | Compose on `/contacts/` | Non-binding register URL hints (§8.3.2) |

**v1 shape (shipped today, `guld.contacts.v1`):**

```json
{
  "contacts": [
    { "name": "bob", "alias": "Bob (meetup)", "favorite": true }
  ],
  "recent_recipients": [
    { "name": "carol", "last_sent_at": "2026-09-23T20:00:00Z" }
  ]
}
```

**v2 shape (GIP-31 — migrate on first load):**

```json
{
  "v": 2,
  "contacts": [
    {
      "id": "c_1",
      "displayName": "Bob",
      "guldName": "bob",
      "emails": ["bob@example.com"],
      "phones": [],
      "socials": [],
      "labels": ["friends"],
      "notes": "",
      "favorite": true
    }
  ],
  "recent_recipients": [
    { "name": "carol", "last_sent_at": "2026-09-23T20:00:00Z" }
  ]
}
```

Migration: each v1 `{ name, alias?, favorite? }` → v2 contact with `guldName: name`, `displayName: alias || name`. Storage key MAY remain `guld.contacts.v1` with `"v": 2` inside, or move to `guld.contacts.v2` — implementation MUST read both during transition.

Primary UI: **`/contacts/`** (list, detail, import, invite compose). Settings: link to Contacts + OTC desk prefs; MAY keep a short favorites summary.

#### 8.3.1 Contact card QR (`guld1contact:`)

In-person exchange of a **Guld name** only — **not** for routine payment and **not** a full address-book export. Import merges into local contacts; no chain tx.

**Prefix:** `guld1contact:`  
**Body:** compact JSON (no whitespace required):

```json
{ "v": 1, "name": "bob", "pub": "0x…", "alias": "Bob" }
```

| Field | Required | Notes |
|-------|----------|--------|
| `v` | yes | Schema version; currently `1` |
| `name` | yes | Canonical lowercase account name |
| `pub` | no | Optional Ed25519 pubkey hex (`0x…`) for display / recovery hint |
| `alias` | no | Local display nickname on import |

Reference UI: Contacts / Settings shows “my contact card” QR when logged in; paste/import field accepts the payload.

#### 8.3.2 Private registration invite (non-binding)

**Purpose:** Help a user invite a friend over a **private** channel (SMS, email, chat, Web Share) with clear copy and a register link. This is **off-consensus UX**.

**MUST NOT:**

- Reserve or suggest a claimable name in a public growth URL  
- Mint invite codes, hold names, or bind fees/keys  
- Authorize sponsorship by itself (sponsorship remains Spec 16 dual-sig when the inviter later signs)

**MAY** prefill register wizard **hints** via query string on the **current peer origin**:

| Param | Role |
|-------|------|
| `from` | Inviter Guld name (display / “return the request to …”) |
| `name` | Optional username hint — prefills register step 1; not reserved |
| `offer` | `1` = emphasize friend-sponsor path after keygen; `0` = prefer pay desk |
| `pay` | Optional desk payment link when inviter OTC is configured |
| `sponsor` | Desk registrar **name** on OTC invites (GIP-8) — not the friend-offer flag |
| `peer` | Optional absolute origin hint |

**Invite compose** (`/contacts/`):

1. Optional checkbox **Offer to sponsor their registration**.  
2. Editable message template + absolute register URL.  
3. Web Share and/or copy message / copy link.

| Mode | Default steering |
|------|------------------|
| `offer=1` | Invitee picks name + keys, then sends portable registration request back to `from` (Spec 16 friend sponsor) |
| `offer=0` | Invitee uses pay desk on this peer when available (`pay` / peer desk); else bootstrap desk |

Default English templates live in the design doc; UI MUST allow edit before share.

Register wizard MUST treat missing/unknown params as absent (no error). Name availability and fees remain live checks at register time.

### 8.4 UpdateMaster (tip advance)

| Step | UI |
|------|-----|
| 1 | Show current `master_hash`, `nonce`, `threshold`, key list |
| 2 | User pastes or computes `new_master_hash` (leaf tooling / hex) |
| 3 | If this device holds enough keys alone (`threshold==1` or multiple local slots), sign and broadcast |
| 4 | Else open **Cosign workstation** (§9) with message `guld/cosign/v1` over `account_id ‖ prev ‖ new ‖ nonce ‖ chain_id` ([`04-proofs.md`](04-proofs.md)) |
| 5 | On race failure (stale nonce/head), refresh account and restart |

**Shipped today:** step 3 for `key_index: 0` only. **Required next:** steps 4–5 for groups.

### 8.5 RotateKeys

| Step | UI |
|------|-----|
| 1 | Propose `new_keys[]` and `new_threshold` (paste pubkeys or generate) |
| 2 | If **group** and `n_new > n_old`, show **expansion fee** ≈ `F_group(L,n_new) − F_group(L,n_old)` (in addition to inclusion). Shrinking: inclusion only. |
| 3 | Collect **old** threshold cosignatures + `new_keys[0]` intent signature ([`03-transactions.md`](03-transactions.md) §3.3) |
| 4 | Confirm; broadcast; if this device’s key is removed, warn to export/backup first |

Wallet MUST preview the expansion fee before broadcast for groups.

### 8.6 RegisterGroup (create group)

Group registration is first-class in the whitepaper and `RegisterGroup` tx. The reference UI MUST provide a wizard; it MUST NOT force groups through the individual register path with silent `threshold: 1`.

**Entry:** `/register/?kind=group` or Wallet → **Create group**.

| Step | Behavior |
|------|----------|
| 1. Name | Same namespace rules as individuals (no `.`); live exists check |
| 2. Signers | Add `n ≥ 1` pubkeys (generate local key + paste others); show fee preview via `guld_estimateRegistrationFee(name, "group", n)` → `F_group(L,n) = F_user(L)×(2+n)` |
| 3. Threshold | Integer `1…n`; explain “need this many signatures to move tip / spend / rotate” |
| 4. Initial master | Default empty/zero tip or paste; optional |
| 5. Endowment | Optional GULD to fund the group account at create |
| 6. Auth | Build sponsored `RegisterGroup` dual-sig intent (registrant `keys[0]` + payer) like username ([`16-sponsored-registration.md`](16-sponsored-registration.md)) |
| 7. Sponsor | Friend paste/QR **or** paid desk (desk MUST accept `kind=group` and `nKeys`) |
| 8. Done | Poll until account exists; offer to save this device’s key slot as member of `name` |

**Constraints (UI MUST surface):**

- Groups **cannot** open subaccounts (only `individual` parents).  
- Larger `n` raises registration fee **and** ongoing tip weight (`W_sig`) — show a short warning.  
- After create, daily spend/tip for `threshold > 1` goes through §9.

### 8.7 RegisterSubaccount

Shipped under individual account tools: pick `label`, generate/import key, parent pays `F_sub`, dual-sig, create `parent.label`. Cap **8** live; UI MUST disable create when at cap and explain.

### 8.8 ClaimLegacy

`/claim/`: prove 1.0 control (PGP cleartext or `isysd` attestation per spec 15), set new keys/threshold, broadcast `ClaimLegacy`. After claim, normal yearly funding rules apply.

### 8.9 Name funding & expiry

Pay-or-release ([`../gips/gip-11.md`](../gips/gip-11.md)): miners include `SettleRegistration` when overdue. Reference UI SHOULD:

- Show `expires_at_height` (and approximate time) on the account card when the field is present in account JSON.  
- Warn when tip height approaches expiry and balance `<` next `F_*`.  
- Explain: keep the account funded; miners renew or release — **no** required user “Renew” button.  
- Optional advanced: “Request settle” that broadcasts permissionless `SettleRegistration` for an overdue name (power users / tools) — **MAY**.

### 8.10 Sponsor a name (friend path)

Funded user pastes registration request JSON (individual or group), reviews fee + name + keys, confirms, signs as `payer`, broadcasts. MUST remain available even when paid gateway is enabled.

### 8.11 External-signing-only

Settings / first-run opt-in: build unsigned tx or cosign challenge → download/copy → sign offline → paste signature fragment or full signed tx → broadcast. No private key in browser storage.

---

## 9. Cosign workstation

Threshold accounts (`threshold > 1`, or any account where this device holds fewer than `threshold` keys) need a **first-class UI** to assemble `threshold_cosign_v1` (UpdateMaster, RotateKeys, Transfer, and **JointTransfer** input auth) without inventing leaf politics.

### 9.1 Goals

- Export a **cosign request** (opaque to the chain): account name, message tag, binding fields (`prev_master_hash`, `new_master_hash`, `nonce`, `chain_id`, or rotate commit), and which `key_index` values are still needed.  
- Import **cosign responses**: `{ key_index, signature }` fragments from other devices/members.  
- Show progress: `signed / threshold` with distinct indices.  
- When `signatures.len() >= threshold`, offer **Broadcast** (builds final tx).  
- On stale nonce/head after delay, invalidate the session and force refresh from the node.

### 9.2 Transport (out of consensus)

| Mode | Norm |
|------|------|
| **Copy/paste JSON** | MUST — default, works offline air-gap style |
| **QR** | SHOULD — for in-person cosign of compact requests |
| **File download** | MAY |
| **In-band leaf / chat** | Out of scope for reference UI (leaf concern) |

### 9.2.1 Cosign JSON schema (**frozen** — ecosystem, not consensus)

Version field: **`v: 1`**. Types: `guld1cosignreq` / `guld1cosignres`. Private keys MUST NOT appear. Amounts and nonces are **decimal strings**. Hashes/keys/signatures are `0x` + lowercase hex.

**Request** (`guld1cosignreq`) — initiator builds this; co-signers re-derive the message and sign:

```json
{
  "v": 1,
  "type": "guld1cosignreq",
  "op": "update_master",
  "name": "treasury",
  "account_id": "0x…",
  "nonce": "0",
  "chain_id": 1,
  "threshold": 2,
  "keys": ["0x…", "0x…"],
  "needed": [0, 1],
  "inclusion_fee": "10000",
  "prev_master_hash": "0x…",
  "new_master_hash": "0x…"
}
```

| Field | Required | Notes |
|-------|----------|-------|
| `op` | yes | `update_master` \| `rotate_keys` \| `transfer` \| `joint_transfer` |
| `name`, `account_id`, `nonce`, `chain_id` | yes | Bind to live account; stale nonce ⇒ discard |
| `threshold`, `keys` | yes | Current on-chain policy (snapshot at request time) |
| `needed` | yes | Distinct `key_index` values still unsigned; subset of `0..keys.len()` |
| `inclusion_fee` | yes | Quanta string; covered by signed message where applicable |
| `prev_master_hash`, `new_master_hash` | if `op=update_master` | Exact tip advance |
| `new_keys`, `new_threshold` | if `op=rotate_keys` | Proposed policy |
| `to`, `amount` | if `op=transfer` and single-recipient | Recipient name; amount in quanta (decimal string) |
| `outputs` | if `op=transfer` multi-recipient ([GIP-43](../gips/gip-43.md)) | `[{ "to", "amount" }, …]` — binds `guld/transfer/v2` |
| `inputs`, `outputs`, `input_index` | if `op=joint_transfer` | Full joint legs + which input this signer covers; binds `guld/joint_transfer/v1` |
| `memo` | no | UTF-8 string ≤64 bytes if present |

**Response** (`guld1cosignres`) — one signature fragment; MUST echo binding fields so the collector can verify without trusting the peer:

```json
{
  "v": 1,
  "type": "guld1cosignres",
  "op": "update_master",
  "name": "treasury",
  "account_id": "0x…",
  "nonce": "0",
  "chain_id": 1,
  "inclusion_fee": "10000",
  "prev_master_hash": "0x…",
  "new_master_hash": "0x…",
  "key_index": 1,
  "signature": "0x…"
}
```

| Rule | Norm |
|------|------|
| Verify | Collector re-derives message bytes from echoed fields + `op`, verifies Ed25519 under `keys[key_index]` from the **live** account (or from request `keys` if still matching) |
| Merge | Distinct `key_index` only; duplicates overwritten by latest verified sig |
| Ready | `verified_indices.length >= threshold` ⇒ MAY build tx and broadcast |
| Stale | If node `nonce` / `master_hash` / `account_id` diverge from request → invalidate session |

**Registration request** (spec 16) for groups uses the same portable blob with `"type": "register_group"` (individual remains `"register_username"`). See [`16-sponsored-registration.md`](16-sponsored-registration.md) §4.

### 9.3 Where it appears

| Action | Opens workstation when |
|--------|------------------------|
| UpdateMaster | Local keys `< threshold` |
| Transfer (group spend) | Account `threshold > 1` (or proof required) |
| RotateKeys | Old policy needs multi-sig |
| RegisterGroup setup | Optional: collect co-founder pubkeys only (registration still dual-sig sponsor model); post-create tips use workstation |

### 9.4 Security copy

UI MUST state: cosigners authorize a **specific** statement (tip advance / spend / rotate). The network does not interpret why. A malicious majority of keys can move the account — same as any multisig; mitigation is `RotateKeys` and leaf policy.

---

## 10. Extension & dapp identity

| Phase | Behavior |
|-------|----------|
| Shipped | Shared encrypted keyring schema; register / sponsor / send |
| Shipped (§1.4) | Site-login: dapp presents challenge; extension signs under registered key; dapp verifies via node account lookup (§10.1) |
| Shipped / in progress ([GIP-32](../gips/gip-32.md)) | Shared **`guld-web-ui`** leaf hosted by PWA + extension full tab; confirmed pairing (§10.2); compact popup subset |
| Later | Multiple named accounts in extension confirm; group key_index picker; optional PWA extension-only signing mode |

Extension MUST NOT be required to validate blocks. Site-login is **ecosystem** (not a consensus opcode). Format **frozen** at §10.1 (`v: 1`).

### 10.0 Shared reference UI hosts ([GIP-32](../gips/gip-32.md))

| Host | Role |
|------|------|
| **PWA (umbrella)** | Primary daily client; service worker; installability; marketing chrome; thin route shells under `/dashboard/`, `/profile/`, `/wallet/`, `/register/`, … |
| **guld-extension** | Full-tab host of the same **`guld-web-ui`** leaf; `window.guld` provider; confirm UI; compact popup (unlock / dashboard subset + expand) |

Wallet product modules MUST live in `src/guld-web-ui/` (`@guld/web-ui`). Shared code MUST use a **host runtime** (`kind`: `pwa` \| `extension`) for storage, asset bases, and service-worker registration (no-op on extension). Landing, explorer, docs, and software catalog remain umbrella-only.

Design SoT: [`../design/pwa-extension-shared-ui.md`](../design/pwa-extension-shared-ui.md).

### 10.1 Site-login challenge / response (frozen `v: 1`)

Dapp ↔ extension portable blobs (same spirit as cosign `guld1cosignreq` / `guld1cosignres`).

**Challenge** (`type: "guld1loginreq"`):

| Field | Required | Notes |
|-------|----------|-------|
| `v` | yes | `1` |
| `type` | yes | `"guld1loginreq"` |
| `domain` | yes | `URL.host` of the page (e.g. `guld.io` or `localhost:8080`); MUST equal page origin host |
| `uri` | yes | Full page URL; MUST be same-origin as the requesting page |
| `name` | yes | Lowercase NFC account name |
| `chain_id` | yes | u32; MUST match node `GET /api/v1/chain/status` |
| `nonce` | yes | ≥ 128 bits entropy (hex or base64url) |
| `issued_at` | yes | ISO-8601 UTC string; hashed **byte-identical** (no reformat) |
| `expiration_time` | yes | ISO-8601 UTC; dapp SHOULD use ≤ 10 minutes window |
| `statement` | no | Human string for confirm UI; max **256** UTF-8 bytes |

**Digest** (Ed25519 message = 32-byte tagged hash, same pattern as transfer):

```text
msg = tagged_hash("guld/site_login/v1",
  u32_be(chain_id)
  ‖ utf8(name) ‖ 0x00
  ‖ utf8(domain) ‖ 0x00
  ‖ utf8(uri) ‖ 0x00
  ‖ utf8(nonce) ‖ 0x00
  ‖ utf8(issued_at) ‖ 0x00
  ‖ utf8(expiration_time) ‖ 0x00
  ‖ u16_be(statement_len) ‖ statement_bytes   // len 0 if absent
)
```

**Response** (`type: "guld1login"`): echo challenge fields + `key_index` (u16), `pubkey` (`0x`-hex 32 B), `signature` (`0x`-hex 64 B) over `msg`.

**Auth strength:** any key in `account.keys` MAY prove login (`key_index`); this is **not** a spend threshold. After [GIP-47](../gips/gip-47.md):

- Extension / provider SHOULD prefer an **AUTH**-role key on the requested name, else keys of `{name}.auth` when that sub is in the keyring.  
- Dapps verifying “login as `alice`” MUST accept a proof under `alice.auth` when `alice.auth` exists, `kind = subaccount`, and `parent = alice`, and MUST set the session subject to **`alice`**.  
- Do not treat an arbitrary other sub (`alice.mobile`) as proof of `alice` unless the challenge named that sub.

**Provider:** `guld.request({ method: "guld_login", params: [challenge] })` → response object. Extension MUST show user confirm (origin, name, domain, statement, expiry) and MUST reject if `domain` ≠ origin host or `uri` is not same-origin. `guld_requestAccounts` MUST confirm once per origin (no silent auto-grant).

**Verify (dapp):**

1. Schema + `now ∈ [issued_at, expiration_time]` + `chain_id` matches status.  
2. Recompute `msg`; Ed25519-verify under `pubkey`.  
3. Resolve signer account: challenge `name`, or — when proving parent login via well-known sub — `GET /api/v1/chain/accounts/{name}` where `name` is the **signer** account in the response (MAY be `{subject}.auth`). Confirm `keys[key_index]` equals `pubkey`.  
4. If signer is `{subject}.auth`: confirm `kind = subaccount` and `parent = subject`; session subject = `parent`.  
5. Reject if account missing, index OOB, keys empty, or parent link fails.

Session cookies / JWTs after verify are **out of band**. Reference demo: `/demo/login/`. Capability norms: [leaves/03-site-login](../leaves/03-site-login.md), [GIP-47](../gips/gip-47.md).

**Non-goals:** consensus validation of login; threshold multi-approve login; free-form `signMessage` without SIGN / `.sign` hygiene ([GIP-47](../gips/gip-47.md)); **silent** PWA↔extension key sync without user confirm (see §10.2 for confirmed pairing).

### 10.2 PWA ↔ extension pairing (`v: 1`)

Ecosystem handoff so the reference PWA and extension can share a keyring (and optionally local contacts/settings) after **explicit user confirmation**. Not a consensus opcode. Design: [`../design/pwa-extension-shared-ui.md`](../design/pwa-extension-shared-ui.md).

**When to offer (reference UI SHOULD):**

1. After successful registration.  
2. On login / empty-keyring flows when the extension is missing or unpaired.  
3. Settings → Browser extension.

**Detect:** `window.guld` / `guld#initialized` (same pattern as `/demo/login/`).

**Provider methods:**

| Method | Behavior |
|--------|----------|
| `guld_pairStatus` | `{ installed: true, paired: boolean, accounts?: string[] }` (no secrets) |
| `guld_pairOffer` | Params: pair payload (below). Extension MUST show confirm UI listing account names; on accept, merge into extension keyring storage. Returns `{ ok: true, accounts: string[] }` or error. |
| `guld_pairPull` | Optional: after confirm, return encrypted keyring export from extension for PWA import (pull direction). |

**Pair payload** (`type: "guld1pair"`):

| Field | Required | Notes |
|-------|----------|-------|
| `v` | yes | `1` |
| `type` | yes | `"guld1pair"` |
| `keyring` | yes | Same JSON shape as `guld.keyring.v1` (encrypted accounts; no plaintext privkeys) |
| `contacts` | no | `guld.contacts` v1/v2 JSON (local PII) |
| `settings` | no | Subset: `apiBase`, `rpcUrl`, gateway desk prefs |
| `passphrase_hint` | no | Never the passphrase itself — UI only |

The page MUST NOT send plaintext private keys. The user MUST unlock the extension (and PWA keyring as needed) so ciphertext can be re-encrypted under the extension passphrase when schemas differ; if both use the same passphrase, the extension MAY import ciphertext as-is after confirm.

**After pair:** PWA SHOULD prefer `window.guld` for signing when the extension is unlocked and accounts are granted; PWA MUST remain usable offline without the extension. Local pair marker: `guld.extensionPair.v1` (non-secret metadata only).

**Non-goals (v1):** silent/background sync; continuous two-way contacts sync; requiring the extension for PWA use.

---

## 11. Desktop wallet (optional)

`guld-wallet` mirrors PWA flows for users who prefer a file keyring or need **PGP legacy claim**. It MAY serve as the external signer / cosign participant for PWA-exported payloads. Not required for default onboarding.

---

## 12. Implementation order

**Done (dev reference):**

1. Repo-root static PWA, explorer, wallet via `guld-node --http`  
2. Encrypted browser keyring (spec 01 §3.1); individual register/send; letter-based fee estimate  
3. UpdateMaster + RotateKeys (1-of-1) in wallet account view  
4. Subaccounts; optional tx memo on Transfer  
5. Local contacts / recent recipients on send  
6. Paid registrar + friend sponsor (spec 16)  
7. Extension encrypted keyring parity (partial product)  
8. Docs / software browsers  
9. **RegisterGroup** wizard (`/register/?kind=group`) + gateway `register_group`  
10. **Cosign workstation** (`guld1cosignreq` / `guld1cosignres` v1) for UpdateMaster + RotateKeys + Transfer  
11. Account card: kind, threshold, keys, expiry hint; wallet friend-sponsor form  
12. Extension site-login (`guld1loginreq` / `guld1login` §10.1) + `/demo/login/`  
13. Contacts / recent / favorites + Send prefix typeahead (`guld_searchAccounts`) + `guld1contact:` QR  
13b. Contacts address book + private invite (`/contacts/`, GIP-31 Accepted) — task [038](../tasks/open/038-contacts-private-invite.md)  
13c. Shared `guld-web-ui` + extension pair (GIP-32 Accepted) — task [039](../tasks/done/2026-10/039-guld-web-ui-extension-pair.md) **done**  
13d. `UnregisterAccount` + `UpdateBio` (GIP-33/34 Accepted) — Core bundle [055](../tasks/open/055-simba-single-rule-bundle.md) + tasks [041](../tasks/open/041-voluntary-unregister.md)  
13e. Escrow locks — `CreateLock` / `ClaimLock` / `RefundLock` ([GIP-40](../gips/gip-40.md) Accepted) — UI + gate shipped; height-activate tip (`escrow_locks=v1`); threshold CreateLock cosign; deal-key UX for `ClaimThreshold`  
13f. Multi-leg Transfer + JointTransfer ([GIP-43](../gips/gip-43.md) Accepted) — Send multi-recipient; cosign `op=joint_transfer`; explorer multi-leg receipts; rule `multi_leg_transfer=v1`  
14. Optional Paymento registrar path (GIP-8) — any peer + gateway; not consensus 

**Next:**

1. PWA install polish  
2. Explorer polish  
3. External-signing-only path without browser key storage  
4. Mempool visualizer — later  

---

## 13. Open parameters

- Cosign request/response JSON **frozen** at §9.2.1 (`v: 1`); `op=transfer` binds `to` + `amount` or `outputs[]` (+ optional `memo`); `op=joint_transfer` binds full `inputs`/`outputs` + `input_index`  
- Group spend (`Transfer` with `threshold > 1`) — **shipped** via cosignatures (spec 03 §3.6); UI uses workstation when local keys `< threshold`  
- Multi-recipient Transfer v3 + `JointTransfer` — **specified** ([GIP-43](../gips/gip-43.md)); wallet multi-recipient + goldens shipped; JointTransfer assemble + Simba activation pending  
- Extension site-login JSON **frozen** at §10.1 (`v: 1`)  
- Contact card JSON **frozen** at §8.3.1 (`guld1contact:` `v: 1`)  
- JS/WASM signing crate parity with `guld-client` for all message tags  
- Auto-lock timeout duration  
- Hardware key / PQ key UX later  
- Deep links `guld://` for handoff to desktop signer (**TBD**)  
- HTTP resource routes for blocks/CAS parity with RPC (§12)  

---

## 14. Doc maintenance

When a new tx type or logical RPC method is added:

1. Add a row to **§3** (coverage matrix).  
2. Add or extend a flow in **§8–§9**.  
3. Update **§4** API map and **§12** implementation order.  
4. Keep whitepaper §1.6 / §7 in sync at a summary level only — this file remains the UI SoT.
