UX Surfaces
This document walks every primary route the wallet exposes. All routes live in @xchain-wallet/core (packages/core/src/shared/routes/) and render identically in the web SPA, the extension popup, the extension full-screen view, and the desktop renderer.
Route map
| Route | Component | Purpose |
|---|---|---|
| Onboarding | Onboarding.jsx |
First-run create / import / migrate / restore landing |
| Create wallet | CreateWallet.jsx |
Generate BIP39 mnemonic, set password, optional 25th-word passphrase |
| Import wallet | ImportWallet.jsx |
Restore from BIP39 or single WIF |
| Migrate to BIP39 | MigrateToBip39.jsx |
One-way migration from Counterwallet legacy mnemonic |
| Locked | Locked.jsx |
Password unlock screen |
| Loading | Loading.jsx |
Vault decryption progress |
| Home | Home.jsx |
Multi-chain balance dashboard, primary CTA hub |
| Send | Send.jsx |
Single-asset send with review + sign |
| Receive | Receive.jsx |
Address QR + label + chain picker |
| History | History.jsx |
Per-address action history |
| Address list | AddressList.jsx |
Standalone address browser with multisig badging + filter; a green “Active” pill marks each chain’s active address, “Use” sets it, and “Add address” opens AddAddressModal (batch-generate 1-25, Coin + Type) |
| Contacts | ContactsList.jsx |
Saved address book with edit / remove |
| Actions menu | ActionsMenu.jsx |
Indexed launcher for every supported action |
| Token wizard | TokenWizard.jsx |
Guided issue + mint + distribute flow for new token issuers |
| Issue | IssueTokenForm.jsx |
Raw ISSUE form with all parameters |
| Mint | MintForm.jsx |
MINT form |
| Destroy | DestroyForm.jsx |
DESTROY form |
| Token admin | TokenAdminForm.jsx |
Owner-only token settings (lock supply, transfer ownership, etc.) |
| Dispenser | DispenserForm.jsx |
Create dispenser flow |
| Dispensers list | DispensersList.jsx |
Browse open dispensers |
| Dispenser detail | DispenserDetail.jsx |
Buy / inspect / close a single dispenser |
| Dispenser explorer | DispenserExplorer.jsx |
Cross-token dispenser discovery surface |
| Dividend | DividendForm.jsx |
DIVIDEND form |
| Airdrop | AirdropForm.jsx |
Multi-recipient airdrop with parsed-recipients preview |
| Markets list | MarketsList.jsx |
DEX market discovery |
| Market view | MarketView.jsx |
Per-market view: price chart, place order, orderbook, recent trades, open orders, trade history |
| Compose message | ComposeMessage.jsx |
Encrypted-message compose (ECIES / ECDH / AES) |
| Messaging inbox | MessagingInbox.jsx |
Decrypted message list with reply |
| Multisig create | MultisigCreate.jsx |
n-of-m configuration setup |
| Multisig signing session | MultisigSigningSession.jsx |
Active session view: round labels, paste inbox, partials, finalize |
| Pair signer | PairSignerForm.jsx |
Pair a hardware or remote signer |
| Cross-chain swap | CrossChainSwapForm.jsx |
SWAP coordinator view across chains |
| Cross-chain templates | CrossChainTemplates.jsx |
Parallel composer presets (e.g. issue+seed dispenser on two chains atomically) |
| Parallel composer | ParallelComposer.jsx |
Custom multi-chain action sequence |
| Stake | StakeForm.jsx |
BTC STAKE form |
| Staking action | StakingActionForm.jsx |
UNSTAKE / COLLECT form |
| Staking dashboard | StakingDashboard.jsx |
Current stake + rewards + epoch view |
| Delegation | DelegationActionForm.jsx |
DELEGATE (rotate + revoke) form |
| Operator dashboard | OperatorDashboard.jsx |
Validator / operator view of delegations + uptime |
| Deploy contract | DeployContractForm.jsx |
DEPLOY form with source review + gas estimate |
| Execute contract | ExecuteContractForm.jsx |
EXECUTE method invocation |
| Contract funds | ContractFundsForm.jsx |
DEPOSIT / WITHDRAW form |
| Contracts list | ContractsList.jsx |
Per-chain deployed contracts |
| Contract detail | ContractDetail.jsx |
Bytecode + state + recent calls |
| Coinpay | CoinpayForm.jsx |
Native-coin payment leg of XChain action |
| Sweep | (in flow) | Send entire balance + dust to one destination |
| Link | LinkForm.jsx |
LINK action: cross-chain anchor |
| Broadcast | BroadcastForm.jsx |
BROADCAST action |
| Advanced action | AdvancedActionsForm.jsx |
Catch-all power-user form for any registered action descriptor |
| Swap | SwapForm.jsx |
SWAP action |
| View private key | ViewPrivateKey.jsx |
Per-address WIF export; gated by a warning in an unlocked session (no password re-entry), requires unlock when the wallet is locked |
| Alerts | AlertsRoute.jsx |
Notification tray: lists info / warning / critical alerts with optional action button per item |
| Attach content | AttachContentForm.jsx |
Attach a file (with optional title) to a token as on-chain content; polls for confirmation |
| Contract staked positions | ContractStakedPositions.jsx |
Lists the wallet’s active stakes against deployed contracts |
| Stake on contract | ContractStakeForm.jsx |
STAKE-to-contract form; BTC-only at launch; scoped to a specific contract by action index |
| Bind controller | ControllerBindForm.jsx |
CONTROLLERBIND action form; sets per-class policy rules (transfer, trade, burn, mint, stake) on a token |
| Manage token | ManageToken.jsx |
Owner hub for a token: metadata, holders panel, supply, and links to admin sub-forms |
| Market activity | MarketActivity.jsx |
Live market feed; opens on the XCHAIN token by default; tap the token header to switch markets |
| Menu | MenuRoute.jsx |
Full-screen pancake menu opened from the shared app header; links to all top-level sections |
| My tokens | MyTokens.jsx |
Tokens issued by or owned by the active account; links to issue-new flow |
| Sign PSBT | PsbtSignForm.jsx |
Paste-in PSBT signing surface; accepts hex or base64; routes hardware signing via signer bridge |
| Project roster | ProjectRosterForm.jsx |
Manage official tokens linked to a project (LINK action); polls for roster updates |
| Receive picker | ReceivePicker.jsx |
Token-picker wrapper scoped to receive; pre-filters to assets the wallet can receive |
| Rename account | RenameAccountForm.jsx |
Inline form to rename an HD account; header carries Back + Save |
| Rename wallet | RenameWalletForm.jsx |
Inline form to rename a wallet; header carries Back + Save |
| Scan | ScanRoute.jsx |
Dedicated full-screen QR scanner; classifies each frame via detectQrContent and routes to the matching flow; does not import WIF / mnemonic directly |
| Sell token name | SellOwnershipForm.jsx |
Form to list a token name for sale via the SELL action |
| Send picker | SendPicker.jsx |
Token-picker wrapper scoped to send; pre-filters to assets the wallet can send |
| Settings | Settings.jsx |
Tabbed settings host covering security, chains, accounts, signers, connected sites, multisig, display, developer, and about |
| Select signer | SignerSelectForm.jsx |
Inline picker for choosing or adding a signer when initiating a signing round |
| Sign message | SignMessageForm.jsx |
Sign an arbitrary message with a wallet address; returns a copyable signature |
| Token detail | TokenDetail.jsx |
Full token info view: metadata, balance, price, recent activity, holder snapshot |
| Verify signature | VerifySignatureForm.jsx |
Verify a message signature against an address without needing a wallet password |
| Wallet details | WalletDetails.jsx |
View and manage a single wallet: rename, migrate to BIP39, export backup, delete |
| Wallet picker | WalletPicker.jsx |
List all wallets with an Add Wallet affordance; reached from the gear / account menu |
| Account picker | AccountPicker.jsx |
Switch between HD accounts under a wallet; add account or rename from this view |
| Add account | AddAccountForm.jsx |
Derive a new HD account under the active wallet |
Onboarding
The first-run experience is one of:
- Create new: generate a fresh 24-word BIP39 mnemonic; user is required to acknowledge the phrase has been saved before the vault persists. Closing the tab at this stage leaves no persisted state.
- Import existing: paste a 12 / 15 / 18 / 21 / 24-word BIP39 mnemonic, with word-count + dictionary validation; or import a single WIF private key.
- Restore from backup file: re-wrap an exported vault under a fresh device password.
- Counterwallet migration: accept the legacy 12-word format, then optionally migrate to BIP39 from Settings later.
Optional 25th-word passphrase is offered on both Create and Import. The passphrase materially changes derived addresses; the wallet shows a pre-derivation preview to let the user confirm they’ve entered the correct passphrase.
Lock / unlock / auto-lock
- Manual lock: accessible from the global menu and via keyboard shortcut. Zeroes the in-memory master key + session key.
- Unlock: password entry; on success, master key is derived (Argon2id) and the vault decrypts.
- Foreground auto-lock: configurable timeout in Settings. Default: 5 minutes idle. Lock fires on tab/window blur for the popup; on idle-detection for the desktop and full-screen views.
- Browser-close lock: extension session-key namespace clears automatically. Web shell drops in-memory keys when the tab unloads.
- OS keychain auto-unlock: desktop only, opt-in. Stores the session master key in the OS keychain (Keychain on macOS, Credential Manager on Windows, Secret Service on Linux). Disabled by default; enabled via Settings → Security.
Home
The home dashboard renders:
- A per-chain balance summary (
ChainBalanceCard) with native-coin + token totals, scoped to each chain’s active address (not an across-all-addresses total) - A primary CTA row: Send / Receive / Buy / Stake (chain-conditional)
- A secondary action grid surfacing the most-used actions (Issue / Dispenser / Order / Compose / Stake / Multisig)
- A history strip with the last 5 actions across the wallet
- A messaging-inbox unread-count badge linking to the inbox
The same Home renders in the popup (compact), the full-screen / desktop (expanded with charts), and the web app (responsive between popup-like and full-screen-like layouts).
Send
The Send route is the canonical example of the sign-screen safety rail (see Security & Threat Model; Sign-screen safety rails):
- Form: from-address (defaults to the chain’s active address, with an advanced override picker), destination, amount, asset (native or token), optional memo, optional fee tier
- Review: plain-English summary rendered alongside the encoded action string and the encoder’s PSBT
- Sign: signer-specific confirmation (in-vault password re-entry for software, device confirm for Trezor / Ledger)
- Broadcast: wallet broadcasts via SDK; status surface streams encoder accept → mempool → confirmed → indexed
- Done: txid + indexed action, with a link to History
stateDiagram-v2
state "encoder accept" as EncoderAccept
state "mempool" as Mempool
state "confirmed" as Confirmed
state "indexed" as Indexed
state "failed" as Failed
[*] --> Form
Form --> Review
Review --> Sign
Sign --> Broadcast
Broadcast --> EncoderAccept
EncoderAccept --> Mempool
Mempool --> Confirmed
Confirmed --> Indexed
Confirmed --> Failed
Indexed --> Done
Done --> [*]
Failed --> [*]
Identical structure for every action form; only the form fields and review template differ.
Receive
- Address QR for the active chain + address-type
- Per-address label
- Chain picker (
ChainBadge+ chain-icon-only buttons witharia-label) - One-click copy for the address string (
CopyButton) - Optional BIP21 URI request (amount + label) for compatible payers
History
Per-address history view. Each row renders the decoded action plus its on-chain status:
- Type; SEND / ISSUE / MINT / ORDER / DISPENSER / DIVIDEND / etc.
- Counterparty, destination or source address (with contact-list lookup)
- Amount + asset
- Fee
- Status, pending / mempool / confirmed / indexed / failed
- Block height + timestamp once indexed
History reads from xchain-explorer via the SDK. The wallet never re-derives state from raw blockchain data.
Sign screens
Every action’s sign screen renders three parallel views:
| Pane | Source | Trust |
|---|---|---|
| Form values | The user’s own form input | Canonical |
| Decoded action summary | Re-decoded by the wallet from the action string | Canonical (deterministic decode) |
| Encoder PSBT | Returned from the encoder | Untrusted (encoder is a remote service) |
A discrepancy between the form pane and the decoded pane never happens unless the action descriptor or the decoder is buggy, both are tested by the same smoke suite. A discrepancy between either of those and the PSBT pane is what the user is being asked to catch by eye, and what the next-iteration byte-level cross-check will catch automatically.
Multisig session view
MultisigSigningSession.jsx renders an active n-of-m or MuSig2 session:
- Round label (explicit “Round 1 of 3) commit”, “Round 2 of 3 (reveal”, “Round 3 of 3) sign” so the user knows where they are in the protocol
- Paste inbox: accepts cosigner partial PSBTs by paste or QR scan
- Cosigner list: green check / pending dot per cosigner
- Camera scanner:
QrScanner.jsxreads chunked PSBT-QR viacore/src/uri/psbtQr.js - Animated frames:
AnimatedQrFrames.jsxpaints multi-frame QRs at 3 fps for the next cosigner; honorsprefers-reduced-motion: reducewith manual prev / next stepping
See Multisig for the full session state machine.
Contacts
- Saved address book with edit / remove
- Removed-row buttons announce “Remove address N” rather than the
×codepoint - Contact lookup is integrated into Send (autocomplete) and History (counterparty resolution)
QR scanner
QrScanner.jsx is the device-camera surface used by:
- Send (paste address from another device’s QR)
- Multisig signing session (scan a cosigner’s PSBT-QR frame stream)
- Pair signer (scan a remote-signer pair code)
The scanner runs through getUserMedia. The Chrome extension surfaces the runtime camera prompt only when the user explicitly clicks “Scan”. No implicit camera grant.
Command palette + keyboard shortcuts
Power users get:
- Command palette: Cmd/Ctrl + K opens an indexed launcher over every action route, settings panel, and help topic
- Keyboard shortcuts: Cmd/Ctrl + L (lock), Cmd/Ctrl + N (new send), Cmd/Ctrl + R (refresh balances), Esc (back / close), Tab/Shift+Tab focus order on every form
Settings
Settings are organized under sections:
- Security: auto-lock timeout, OS-keychain auto-unlock (desktop), view private key, change password, export backup
- Chains: per-chain endpoint URLs (encoder / explorer / hub), default fee strategy, address-type preference
- Accounts: manage HD accounts under each wallet
- Signers: registered hardware + remote signers, pair-new, remove, firmware version
- Connected sites: per-origin dApp grants with revoke
- Multisig: registered n-of-m configs
- Display: locale (i18n), theme, reduced-motion override
- Developer: developer-mode toggle, network override, custom RPC, diagnostic dump
- About:
package.json.version, build hash, license, third-party notices
Automatic donation system (ADS)
Optional opt-in. When enabled, a configurable percentage of every action fee is routed to a user-chosen donation address (default: the platform’s open-development fund). Off by default; surfaced in Settings → Display.
Micro-UX polish
AddressText.jsxtruncates long addresses with a hover-revealed full string and a one-click copyCopyButton.jsxflashes a confirmation toast on copyChainBadge.jsxcolor-codes each chain consistently across all surfacesMultisigBadge.jsxmarks multisig addresses with a chip +aria-labelDispenserBadge.jsxmarks dispenser-source addressesMnemonicGrid.jsxrenders the recovery phrase as a confirm-by-position grid on CreateDerivationPathCrossCheck.jsxshows the derivation path next to each derived address so users can verify correct derivation across wallets
Copyright © 2026 Dankest, LLC
Licensed under the GNU Affero General Public License v3.0 (AGPL-3.0-or-later). See LICENSE and NOTICE for full terms.