🎮 Live Demo
Pick a page on the left: its spec appears here and the interactive demo on the right. The demo is entirely mock state — no real RPC calls.
① Product pages (S01~S06) are the user-facing screen specs based on the cns-experience.html implementation. ② In the admin console, the owner connects a wallet to sign & execute onlyOwner actions directly in the browser (cns-admin.html). ③ The Admin Console page embeds the real cns-admin.html — connect the owner wallet there to enable on-chain actions. ④ Press ⌘K / Ctrl+K anywhere to search names, reports, and pages and jump. ⑤ Full written spec (PRD, contract, ops, legal, etc.) lives in Notion — Core Docs 7, via the sidebar "Full spec" link.
Admin console (owner-only): cns-admin.html ↗ — actions unlock when the owner wallet is connected.
| Group | Pages | Source documents |
|---|---|---|
| Product pages | S01~S06 | docs/fe-page-spec-ko.md (based on the 1.0.0 implementation) |
| Admin console | embedded | cns-admin.html — owner wallet connect executes onlyOwner actions directly (Emergency pause targets 1.1.0) |
🔍 S01 · Register/Search
Root (/) screen. Search a .ctc name → check availability → start registration.
1Spec & policy
/ · ?name={label} (fills input) · #/{label}.ctc/register hash alias → normalized to / · /{label}.ctc/register → replace-redirect to /CNS | Creditcoin Name Service, with a label {label}.ctc | CNSInput normalization (clean) — input is never blocked; it is normalized and then judged:
- lowercase → strip
.ctcsuffix → remove chars outside[a-z0-9-]→ trim leading/trailing hyphens → truncate to 63 chars - placeholder
Type a name· fixed.ctcsuffix display · maxlength 63 · Enter = CTA click - size classes by input length: >10 chars compact · >24 chars long
On-chain availability check: available(label) eth_call after a 220ms debounce. Sequence token prevents races (stale results are discarded when input changes). Runs only for 4+ chars + non-reserved + not owned + contract address present.
FE reserved list (first-pass filter; the contract is the final judge) — 90+ labels: brand/chain (gluwa, creditcoin, ctc, wctc, cns…), tokens & stablecoins (space, usdg, wlfi, usd1…), products (penguin, penguinbase, spacerouter…), key people (impersonation), and generic/phishing terms (airdrop, faucet, official, api, bridge…). Operators can reserve more on-chain via setReserved.
Content moderation — offensive-word filter: a built-in dictionary (leetspeak-normalized, e.g. sh1t → shit) blocks abusive/offensive labels in-app — verdict Not allowed, CTA disabled, and the registration confirm flow re-checks so it can’t be bypassed. Anything beyond the dictionary is reserved on-chain via setReserved.
Testnet CTC funding: registration needs 50 CTC (5-char base) + gas — request testnet CTC from the internal team faucet/operator. See the onboarding doc for the exact faucet URL.
2Screen states — 9 availability verdicts (priority order)
First match from the top applies. Status text, tone, CTA, and helper copy follow this table:
| # | Condition | Status text | Tone | CTA | Helper copy |
|---|---|---|---|---|---|
| 1 | Empty input | (none) | - | Type a name to start (disabled) | - |
| 2 | 1~3 chars | Restricted | warning | Short claim only (disabled) | 1-3 character names require an assigned claim. |
| 3 | Owned by me | You own this | success | Open My names (enabled → goes to names + row highlight) | You already own this name. |
| 4 | FE reserved list | Reserved | warning | Reserved name (disabled) | - |
| 5 | Offensive / blocked (content filter) | Not allowed | danger | Name not allowed (disabled) | This name isn’t allowed. |
| 6 | On-chain check in flight | (loading dots) | - | loading dots (disabled) | - |
| 7 | Already registered | Registered | danger | Already registered (disabled) | This name is already registered. |
| 8 | Check failed | Check failed | warning | Try again (disabled) | Try again in a moment. |
| 9 | Available | Available | success | Review registration (enabled) | - |
🏷 Card title variants Variant
Register .ctc → while typing Register {label}.ctc → owned Already in My names → registered Name already registered → restricted Restricted .ctc name⏸ During 1.1.0 register pause Planned
💸 CTA button state transitions Variant
Review registration (enabled) → during the registration transaction Registering... (loading · disabled) → right after completion Registered (disabled). Otherwise the action text from the verdict table.3Screen components
DOM structure (top → bottom):
- Intro headline
Your CTC Chain name - Card: title (varies by state) · status line · input (+ fixed
.ctcsuffix) · helper copy · CTA - Price display: when the verdict is Available, shows annual price × years (thousands separators, en-US)
Shared header: logo (CNS) | My names button (connected only) | network badge (Testnet) | wallet button — see S06.
4Data sources
| Data | Source | Tag | Notes |
|---|---|---|---|
| Availability | available(label) eth_call | ON-CHAIN | 220ms debounce + sequence token |
| Pricing | FE local computation (same policy as the contract) | ON-CHAIN | external integrators use GET /v1/pricing/{label}?years=N |
| Reserved / blocklist | FE list (90+ labels) + built-in offensive-word filter | EDITABLE | on-chain reserved(labelHash) is the final judge; operators add more via setReserved |
Reads call the public RPC directly (eth_call); writes go through the wallet provider — global rule §1.6.
5Mobile / responsive
Single-column card layout is kept. Long-name display shrink rules (>16/>26/>42 chars) apply (QA §12-9). Input compact/long classes adjust font size. CTA full-width.
6QA cases
- TC-S01-01 · P0 Given each of the 9 states in §2 When the matching input is entered Then the exact status text, tone, CTA, and helper copy are shown.
- TC-S01-02 · P0 Given rapid consecutive input When an earlier eth_call response arrives late Then it is discarded via the sequence token — only the verdict for the latest input is shown (no race).
- TC-S01-03 · P0 Given entry via
?name=MyName.CTCWhen loaded Then normalized tomyname, input filled, verdict performed. - TC-S01-04 · P1 Given an RPC error When the availability check fails Then
Check failed+Try again in a moment.— no full-screen block. - TC-S01-05 · P1 Given a 43-char name When displayed Then the long class applies · no layout break.
- TC-S01-06 · P1 Given a name I own is entered When
Open My namesis clicked Then navigates to /names + highlights the row for 2.6s + scrolls to it.
+Interaction map — what each press does (incl. navigation)
| Element | Press | Action / navigation |
|---|---|---|
| Name input | Typing | Normalize (lowercase · [a-z0-9-] · 63 chars) → 220ms debounce → re-evaluate → update status line/helper/CTA/card title (§2, 8 states) |
| Input Enter key | Key press | Same as clicking the CTA |
CTA Review registration | Click (only active when available) | Opens review modal M-2 → URL /{label}.ctc/register/review |
CTA Open My names | Click (name you own) | Go to /names + 2.6s row highlight + scroll |
| CTA disabled (5 kinds) | — | Type a name to start / Short claim only / Reserved name / Already registered / Try again — not clickable, no navigation |
📝 S02 · Registration Flow
Review modal M-2 (/{label}.ctc/register/review) → success modal M-3 (…/registered). Includes the metadata prepare (signature auth) step.
1Spec & policy — review modal M-2
Review registration / Confirm the name and fee before opening your wallet.Set as primary — ON by defaultExpires YYYY-MM-DD · Primary Yes/No · Registration fee {N} CTC · Due now {N} CTC + gasreplaceState (avoids history pollution) · applying a route closes all modals and opens only the target oneConfirm button states:
| Condition | Button text | Enabled |
|---|---|---|
| Wallet not connected | Connect wallet | enabled (wallet modal → auto-resume after connect) |
| Availability check in flight | Checking availability | disabled |
| Not registrable | Registration unavailable | disabled |
| Registrable | Confirm registration | enabled |
| In progress | Waiting for wallet... | disabled — cancel/close also disabled |
2Confirm flow — 9 steps (each step aborts with a status message on failure)
| # | Step | Copy on failure |
|---|---|---|
| 1 | Re-judge availability | helper or This name cannot be registered. |
| 2 | Not connected → resume flag + wallet modal | Connect a wallet, then you can confirm this registration. |
| 3 | Ensure network (switch/add chain) | abort on failure — S06 network-switch copy |
| 4 | Re-check on-chain availability | front-run: This name was just registered by another wallet. · failure: Could not check this name. Try again. |
| 5 | Preparing registration. → metadata support probe (eth_call) | NFT metadata registration is not available. Try again after the contract/API check passes. |
| 6 | Preparing NFT metadata. Sign the request in your wallet. → personal_sign → POST /v1/metadata/prepare | 404: Metadata route is not deployed. Restart the API server. · 429: Metadata preparation is rate limited. Try again in {N}s. · other: server message or Could not prepare NFT metadata. |
| 7 | Your wallet is opening. Confirm the transaction there. → balance check → preflight eth_call → send registerWithMetadata | insufficient balance: Not enough CTC. Need {N} CTC plus gas. · contract errors translated via the §10 table · rejection (4001): Transaction rejected |
| 8 | Button Confirming · Transaction submitted. Waiting for Creditcoin confirmation. → receipt polling 1.5s × 30 | failure: Transaction failed. · timeout: Transaction submitted. Check your wallet activity. |
| 9 | Success: close modal after 700ms, show success modal M-3 · optimistic local list update + background reload | - |
3Screen components — success modal M-3
- Badge
Registered(Renewedfor renewals) · titleYour name is live(renewal:Name renewed) - Name +
Expirespill (YYYY-MM-DD) - Buttons ①
View transaction(when a tx hash exists — otherwiseView on explorer: token instance / token / base URL fallback order) ②View all names→ goes to names + row highlight + toastOpened name list - Close: from register, clears the input and goes to
/; from renew, goes to/names
4Data sources
| Step | Call | Tag | Limit/notes |
|---|---|---|---|
| metadata prepare | POST /v1/metadata/prepare (Pinata/IPFS upload) | API | 20/min · signature required · replay rejected |
| registration tx | registerWithMetadata(label, years, primary, metadataURI) | ON-CHAIN | value = annual price × years |
| receipt | eth_getTransactionReceipt polling | ON-CHAIN | 1.5s × 30 = max 45s |
5Mobile / responsive
Modal becomes a full-width sheet. The 4 duration presets form a 2×2 grid. Close-blocking while in progress is unchanged.
6QA cases
- TC-S02-01 · P0 Given the happy path When Confirm registration Then the 9 steps run in order · success modal · optimistic list update.
- TC-S02-02 · P0 Given wallet signature rejection (4001) When at step 6 or 7 Then
Transaction rejected-family copy · modal stays open · retry possible. - TC-S02-03 · P0 Given prepare 429 When at step 6 Then
Metadata preparation is rate limited. Try again in {N}s.· no tx is sent. - TC-S02-04 · P0 Given another wallet front-runs When step 4 re-checks Then
This name was just registered by another wallet. - TC-S02-05 · P0 Given insufficient balance When at step 7 Then
Not enough CTC. Need {N} CTC plus gas.· no tx is sent. - TC-S02-06 · P1 Given Confirm while disconnected When the wallet connects Then registration confirm auto-resumes via the resume flag.
- TC-S02-07 · P1 Given receipt polling exceeds 30 tries When it times out Then
Transaction submitted. Check your wallet activity.— never assert failure.
+Interaction map — what each press does (incl. navigation)
| Element | Press | Action / navigation |
|---|---|---|
Duration presets 1·2·5·10 years | Click | Mark selection + recompute summary (Expires YYYY-MM-DD · Registration fee · Due now) |
Years direct input | Input | Clamp 1–100 → recompute summary |
Set as primary toggle | Click | Default ON ↔ OFF → summary Primary Yes/No |
Confirm button Connect wallet | Click (disconnected) | Opens wallet modal M-1 → auto-resumes registration on success |
Confirm button Confirm registration | Click | Starts the §2 9-step confirm flow (stops with the step copy on failure) |
| X close / backdrop | Click | replaceState back to parent route — blocked while in progress (Waiting for wallet...) |
M-3 View transaction | Click | Explorer tx page, new tab (no tx hash → View on explorer: token instance→token→base fallback) |
M-3 View all names | Click | Go to /names + row highlight + toast Opened name list |
| M-3 close | Click | From register: reset input + / · from renew: /names |
📇 S03 · My names
/names — manage the list of owned names (entry point for record editing / renewal / setting primary).
1Spec & policy — table
/names · /names/connect-wallet · /names/{label}.ctc/renew|records|renewedMy names | CNS- Sorting: clicking the same column toggles asc↔desc. The primary name is always pinned to the top regardless of sort. Ties break by creation date (desc) → name.
- Name cell: avatar (or placeholder) + name + record-edit icon (pencil) +
Created YYYY-MM-DD(when present). - Expiry cell:
YYYY-MM-DD+ renew button (+). Urgent styling when expiry is within 3 months (accent color). - Primary cell:
Primarysuccess pill if primary, otherwise aMake primarybutton (loading dots + disabled during the transaction). - Avatar:
ipfs://→https://ipfs.io/ipfs/gateway conversion · https only · failed sources are not retried within the session.
Interaction map — everything clickable on this screen:
| Element | Press | Behavior |
|---|---|---|
Name/Expiry column headers | Click | Toggle sort asc↔desc (primary always pinned to top; ties by creation date desc → name) |
| Pencil icon in Name cell | Click | Edit-records modal M-5 (/names/{label}.ctc/records) |
+ button in Expiry cell | Click | Renew modal M-4 (/names/{label}.ctc/renew) |
Make primary button | Click | If forward address ≠ my address: toast Update the address record first and stop · otherwise sends setPrimary, button shows loading dots + disabled · success: toast {name}.ctc is primary + wallet button text refresh · rejected Transaction rejected / failure Primary transaction failed |
Empty state Connect wallet | Click | Wallet connect modal M-1 |
Empty list Register name | Click | Go to register screen (/) |
| Row highlight | — | 2.6s highlight + scroll when arriving via success modal View all names or S01 Open My names |
2Screen states — display by state
| State | Title | Body | Action |
|---|---|---|---|
| Wallet not connected | Connect wallet | Connect your wallet to load the names you own. | Connect wallet button → wallet modal |
| Loading (no cache) | Loading names | Reading CNS NFTs from Creditcoin Testnet. | - |
| Empty list | No names found | Register a name first, then manage it here. | Register name button → register screen |
| Load failed | (keeps current display) | toast Could not load names | - |
🟢 active / 🟠 expiring / 🔴 expired row variants Variant
NameExpired) — attempting renewal shows This name has expired.; with no grace period anyone can re-register immediately. Steering users to re-register is the policy.⚡ Cache-first display Loading
CNS_NAMES_{chainId}_{address} localStorage cache is rendered first, then reloaded in the background. If the same address was already loaded, the reload is skipped (ignored when forced).3Screen components
- Page title + table (Name / Expiry / Primary)
- Row actions: pencil (records modal M-5) ·
+(renewal modal M-4) ·Make primary - Arriving from Register/Search via
Open My nameshighlights the row for 2.6s + scrolls to it
4Data sources — 3-tier fallback loading
| Order | Source | Tag | Notes |
|---|---|---|---|
| ① | resolver API GET /v1/names/{address} | INDEXER-READY | expensive route — mind the 10/min/IP limit |
| ② | Blockscout GET /api/v2/addresses/{address}/nft | INDEXER | max 5 pages · CNS contract filter · each label re-verified via resolveRecords |
| ③ | RPC eth_getLogs Transfer topic scan | ON-CHAIN | from startBlock 4,810,000 in 20,000-block chunks · in/out token set → rebuilt via ownerOf/labelOf/resolveRecords |
Finally, primaryOf(address) syncs the primary. Results are stored in the localStorage cache.
5Mobile / responsive
Table reflows into a card list (name/expiry/primary stacked vertically). Long-name truncation rules unchanged. Row actions become a button row at the card bottom.
6QA cases
- TC-S03-01 · P0 Given ① API fails When loading Then falls back to ② Blockscout, and to ③ log scan if ② fails — user sees a single loading state.
- TC-S03-02 · P0 Given a primary name is owned When any sort is applied Then the primary row stays pinned to the top.
- TC-S03-03 · P0 Given a name 80 days from expiry When rendered Then Expiry shows the urgent accent.
- TC-S03-04 · P1 Given a cache exists + revisit When connecting Then cache renders immediately, then background refresh — no flicker.
- TC-S03-05 · P1 Given all loads fail When a cache exists Then keep the current display + toast
Could not load names. - TC-S03-06 · P2 Given every date When displayed Then fixed
YYYY-MM-DD(never expose ISO timestamps).
🗂 S04 · Records (M-5)
/names/{label}.ctc/records — edit the address + 10 optional records. All records are public on-chain data.
1Spec & policy
Edit records / Set the address and optional records for this name.setRecords transaction overwrites all 10 fieldsFields + validation rules (empty values are always allowed — except Address):
| Field | placeholder | Validation |
|---|---|---|
| Address | 0x... | EVM address required (when connected). Default: existing record or my address |
| Avatar | ipfs:// or https:// | ipfs:// or https:// |
| Profile URL | https:// | https:// only |
| Contenthash | ipfs:// or ipns:// | ipfs/ipns/bzz scheme or 0x hex |
| Bio | Short public bio | max 280 chars (textarea) |
| X | @handle or https://x.com/handle | handle (≤40 chars) or x.com/twitter.com URL |
| GitHub | handle or https://github.com/handle | handle or github.com URL |
| Discord | username or invite URL | username (2~32 chars) or discord.gg/discord.com |
| Telegram | @handle or https://t.me/handle | handle or t.me/telegram.me |
name@example.com | email format |
2Screen states
📄 Default (editable) Success
Save records. On success: local update + cache save + close modal + toast Records saved.⚠️ Live validation failure Error
Enter records in the correct format. + save disabled.🔒 RecordsLocked Variant
RecordsLocked/MetadataLocked have no dedicated user copy and surface as the generic Records transaction failed/Transaction failed (dedicated copy is a 1.1.0 candidate). The UI has no lock toggle today (contract feature, P1 extension). Lock state is read via lockStatus / the resolver response's locks.records.👛 Not connected / name not owned Variant
Connect wallet (starts the connect flow on click). Name not owned: Name not found + This name is not loaded in this wallet.⏳ Saving Loading
Saving (loading) · close blocked while in progress. Rejected: Transaction rejected / failed: Records transaction failed or Transaction failed.3Screen components
- Basic area: Address · Avatar · Profile URL · Bio
- Advanced collapsible: Contenthash · X · GitHub · Discord · Telegram · Email
- Status line:
role="status"+aria-live="polite"(modal common §11)
4Data sources
| Data | Source | Tag |
|---|---|---|
| Current records | resolveRecords(label) | ON-CHAIN |
| Save | setRecords(...) — overwrites all 10 fields | ON-CHAIN |
| Lock state | lockStatus / resolver locks | ON-CHAIN |
5Mobile / responsive
Modal becomes a full-screen sheet. Fields stack in 1 column. Advanced collapsible behavior unchanged. The Bio textarea is fixed at 3 rows.
6QA cases
- TC-S04-01 · P0 Given invalid input in each of the 10 fields When typing Then invalid marker shows immediately + save disabled (§12-5).
- TC-S04-02 · P0 Given Address cleared When save is attempted Then blocked — Address may not be empty (when connected).
- TC-S04-03 · P0 Given a RecordsLocked name When saving Then revert detected in preflight → shown as the generic
Records transaction failed/Transaction failed(no dedicated copy — adding it is a 1.1.0 candidate), no gas wasted on the tx. - TC-S04-04 · P1 Given only some fields edited When saving Then all 10 fields are overwritten — verify untouched field values are preserved.
- TC-S04-05 · P1 Given a Bio containing
<script>When displayed Then escapeHtml applied — no XSS (§12-10).
+Interaction map — what each press does (incl. navigation)
| Element | Press | Action / navigation |
|---|---|---|
Advanced disclosure | Click | Open/close optional fields (Contenthash, socials) — closed by default |
| Each field input | Input | Live validation (§1 table) — invalid highlights field + status line Enter records in the correct format. + disables save |
Save Connect wallet | Click (disconnected) | Starts wallet connect flow |
Save Save records | Click | Ensure network → send setRecords (overwrites all 10 fields) → success: local update+cache+close+toast Records saved · rejected Transaction rejected / failure Records transaction failed |
Save Name not found | — | Disabled + This name is not loaded in this wallet. |
| X close / backdrop | Click | Back to /names — blocked while saving (Saving) |
🔄 S05 · Renewal (M-4)
/names/{label}.ctc/renew — add years without changing records/primary. No renewal after expiry.
1Spec & policy
Renew name / Add years without changing records or primary name.Current expiry → After renewal date comparison / year presets (1·2·5·10) + manual input (1~100) / Due now {N} CTC + gas2-step confirm button: disconnected Connect wallet → Review renewal (1st click) → Confirm renewal (2nd click actually proceeds) → in progress Renewing.... Changing the years resets the confirm step.
Owner only: only the name (NFT) owner can renew — for any other wallet the contract reverts with NotNameOwner.
2Screen states
📄 Default (2-step confirm) Success
NFT metadata renewal is not available.) → prepare (expiry = current expiry + years) → send renewWithMetadata → receipt → on success, success modal (Renewed) after 700ms.🔴 Expired name Error
NameExpired in preflight → This name has expired. Steer to re-registration instead of renewal.🟠 Expiring soon Variant
+ button is the main path.❓ Name not in the list Empty
Name not found.⏳ In progress Loading
Renewing... · close blocked · receipt polling 1.5s×30 — same shared tx UX (§10).3Screen components
- Name header · Current expiry → After renewal comparison block
- 4 year-preset buttons + number input (clamped 1~100)
- Summary:
Due now {N} CTC + gas(annual price × years, comma-formatted) - Reuses success modal M-3 (badge
Renewed· titleName renewed· close →/names)
4Data sources
| Data | Source | Tag |
|---|---|---|
| Current expiry | expiresAt(labelHash) / local list | ON-CHAIN |
| metadata prepare | POST /v1/metadata/prepare (image/JSON reflecting the new expiry) | API |
| renewal tx | renewWithMetadata(label, years, metadataURI) | ON-CHAIN |
5Mobile / responsive
Modal becomes a full-screen sheet. Presets 2×2. Date comparison block stacks vertically.
6QA cases
- TC-S05-01 · P0 Given after the 1st
Review renewalclick When the years change Then the confirm step resets — nothing is sent immediately (§12-4). - TC-S05-02 · P0 Given an expired name When renewal is attempted Then
This name has expired.· no tx sent. - TC-S05-03 · P0 Given a normal renewal When it succeeds Then After renewal = current expiry + years · success modal Renewed · list expiry updated.
- TC-S05-04 · P1 Given prepare fails When renewing Then
NFT metadata renewal is not available.· no tx sent. - TC-S05-05 · P2 Given 101 entered When typing manually Then clamped to 100 · the
DurationOutOfRangecopy isChoose between 1 and 100 years.
+Interaction map — what each press does (incl. navigation)
| Element | Press | Action / navigation |
|---|---|---|
Year presets 1·2·5·10 / direct input | Click/Input | Clamp 1–100 → recompute After renewal date and Due now + reset confirm step |
Button Connect wallet | Click (disconnected) | Wallet modal M-1 |
Button Review renewal (1st) | Click | Switch to confirm step — button becomes Confirm renewal |
Button Confirm renewal (2nd) | Click | Ensure network → metadata probe → prepare (expiry=current+years) → renewWithMetadata → receipt → success modal (Renewed) after 700ms |
| X close / backdrop | Click | Back to /names — blocked while in progress (Renewing...) |
| When name not in list | — | Modal closes + toast Name not found |
👛 S06 · Wallet Connect (M-1) + Set Primary
/connect-wallet-family modals + Make primary on My names rows (no modal).
1Spec & policy — modal M-1
Connect wallet (after connecting Wallet connected)Select a wallet. We will switch it to {chainName} if needed. (after connecting {MetaMask|WalletConnect|Preview wallet} is ready on {chainName}.)Browser wallet) / WalletConnect (Mobile wallets and QR) — both options hidden once connectedConnection error copy: not installed MetaMask is not installed. · no account No account selected. · rejected (4001) Connection rejected. · other Wallet connection failed.
Network switch (ensureCreditcoinNetwork): check eth_chainId → if different wallet_switchEthereumChain → on 4902 (chain missing) wallet_addEthereumChain then retry. Status: Checking {chainName}... → Switch to {chainName} in your wallet. → (if needed) Add {chainName} in your wallet. → success {chainName} ready. Failure: rejected (4001) Network request rejected. / unsupported (4200, -32601) This wallet cannot add or switch networks automatically. / other Could not switch to {chainName}. {message}
Global behavior: auto-restore (eth_accounts — skipped if the manual disconnect record CNS_WALLET_MANUAL_DISCONNECT_{chainId} exists). accountsChanged with an empty array → disconnect + toast Wallet disconnected. chainChanged to another chain → toast Switching to {label} then auto-switch attempt.
Wallet button text states:
| State | Text |
|---|---|
| Not connected | Connect |
| Connected + has primary | primary name (middle ... ellipsis over 18 chars) |
| Connected + preview mode | Preview |
| Connected (otherwise) | 0x1234...abcd (first 6 + last 4) |
2Screen states + primary-setting rules
📄 Connect success Success
{MetaMask|WalletConnect} ready → close modal. If arriving from registration review (resume flag), registration confirm auto-resumes.⏳ Connecting Loading
Opening MetaMask.../Opening WalletConnect... · close blocked while connecting (except force close).📱 WalletConnect variant Variant
WalletConnect Project ID required. On disconnect, provider.disconnect() is attempted.⭐ Set primary (no modal) Feature
Update the address record first. Flow: ensure network → send setPrimary → row button loading dots → receipt → success toast {name}.ctc is primary + wallet button text refresh. Rejected: Transaction rejected / failed: Primary transaction failed. Contract-side rejection is ForwardRecordMismatch → The address record must point to this wallet before setting primary.🔌 Dropdown menu Variant
Copy→✓ for 2s, on failure toast Could not copy address) ② Explorer link (CNS token inventory URL preferred) ③ Disconnect. Closes on outside click/ESC.3Screen components
- Modal M-1: title · note · 2 wallet option buttons · status line (
role="status") - Header wallet button + dropdown (Copy address / Explorer / Disconnect)
- My names Primary cell:
Primarypill orMake primarybutton
4Data sources
| Data | Source | Tag |
|---|---|---|
| Account restore | eth_accounts | WALLET |
| Network | eth_chainId · wallet_switchEthereumChain · wallet_addEthereumChain | WALLET |
| primary lookup | primaryOf(address) | ON-CHAIN |
| set primary | setPrimary(label) | ON-CHAIN |
| manual disconnect memory | CNS_WALLET_MANUAL_DISCONNECT_{chainId} localStorage | LOCAL |
Test hooks (localhost only): ?mockWallet=1 · ?mockNetworkReject=1 · ?wcProjectId=.
5Mobile / responsive
On mobile, WalletConnect is the main path (deep link instead of QR). The dropdown menu becomes a bottom sheet. Button-text truncation rules unchanged.
6QA cases
- TC-S06-01 · P0 Given a wallet without the Creditcoin chain When connecting Then proceeds automatically add chain → switch (§12-7).
- TC-S06-02 · P0 Given a refresh after manual Disconnect When loading Then no auto-reconnect — the record is cleared on successful reconnect (§12-8).
- TC-S06-03 · P0 Given forward address ≠ my address When Make primary Then toast
Update the address record first· no tx sent (§12-6). - TC-S06-04 · P1 Given
accountsChangedwith an empty array When it fires Then disconnect handling + toastWallet disconnected. - TC-S06-05 · P1 Given network switch rejected (4001) When connecting Then
Network request rejected.· stays not-connected. - TC-S06-06 · P2 Given a 19-char primary name When shown on the wallet button Then middle
...ellipsis.
+Interaction map — what each press does (incl. navigation)
| Element | Press | Action / navigation |
|---|---|---|
MetaMask option | Click | Status Opening MetaMask... → success: ensure network → toast MetaMask ready → close (auto-resume if from review) · not installed MetaMask is not installed. / rejected Connection rejected. / no account No account selected. |
WalletConnect option | Click | Dynamic import provider → show QR modal · no Project ID → notice + toast WalletConnect Project ID required |
| While connecting | — | Both options disabled+loading · close blocked |
| After connect | — | Options hidden · title Wallet connected · note {wallet} is ready on {chainName}. |
Make primary (My names row) | Click | See S03 interaction map — setPrimary flow |
🛠 Admin Console
Wallet-connected owner console — the real thing is embedded on the right.
Owner connects MetaMask and signs onlyOwner actions directly: setReserved, setShortNameClaim, setMetadataURI, withdraw, transferOwnership / acceptOwnership / cancelOwnershipTransfer. Includes Lookup (name / address / price) and an NFT · metadata inspector. IPFS upload stays server-side (PINATA_JWT never in the browser). Emergency pause is a 1.1.0 item (no pause function in the contract yet). On mainnet the owner may be a Safe multisig — actions are then proposed via the wallet, not executed instantly. Access is enforced on-chain (onlyOwner); the console only enables the buttons when the connected wallet is the owner. In 1.1.0 the owner can also authorize delegated operators via setOperator, who may run setReserved / setShortNameClaim / setMetadataURI, while treasury (withdraw), ownership transfer, upgrade, and setOperator itself stay owner-only. Content moderation: reserve offensive/impersonation labels via setReserved; the app also ships a built-in offensive-word filter.