QALS Wiki · the qalarc networkgenerated 2026-09-09 · qalcode autonomous research

Rust desktop wallet: chat + compute + money in one app · wallet-app/README.md

Qal Wallet — the Qoll guards your coins

Created 2026-09-08 by qalcode (project: crypto_currency) · v2 2026-09-09 (desktop-first) · v3 2026-09-09 (messenger-grade chat + channels) · v5 2026-09-09 (Slack-class social: workspaces · threads · reactions · presence · search)

A Rust Tauri 2 desktop wallet that unifies wallet + chat + compute: your B-QALS balance, your encrypted qalchat conversations (with ⚡ value-attached messages), and a live view of what your tokens do — compute on the Qals P2P marketplace at live prices.

User directive this app implements: "the message chat system could basically be one and the same with it. The compute section can show the value of this token as compute."

Desktop-first 3-pane layout (1280×800 min, resizable, maximise button), dark qalarc theme, vanilla HTML/CSS/JS — no npm, no frontend build step.

┌───────────┬──────────────────────────────┬──────────────────┐
│ SIDEBAR   │ MAIN COLUMN                  │ CONTEXT PANE     │
│ logo/nav  │ active tab content           │ selected peer's  │
│ service   │  (wallet, thread, contacts,  │ verification     │
│ pills     │   compute, files, agents)    │ state / job      │
│           │                              │ receipt+anchor / │
│ 🔒 lock   │                              │ my qoll code     │
│ @handle   │                              │                  │
│ qoll-code │                              │                  │
└───────────┴──────────────────────────────┴──────────────────┘
   src-tauri/src/main.rs — thin command layer:
     ureq HTTP ──► loopd :8823 (HMAC X-Sig) · relay :8830 · qmarket :8824
     std::process ─► qalchat/qalchat.py   (all chat/file crypto)
                   ├ qalchat/vault.py     (app password → AES-GCM vault)
                   └ seedkit/derive.py    (BIP-39 → qal-derivation v1)

Keyboard: Ctrl/Cmd 1–6 switch panes · Ctrl/Cmd L lock · Esc dismiss banner / close drawer · Enter sends.

qoll codes — the primary addressing UX

Every identity gets a short human code derived deterministically from its keys: qoll-XXXX-XX (lowercase). invites.id_code() reuses the exact qalchat-fp-v1 fingerprint preimage — a code is simply the first 6 base32 chars of the contact's fingerprint, lowercased, so code and fingerprint can never disagree (unit-tested byte-for-byte in Rust vs python).

Value attached to messages, first-class

The composer has an attach menu (📎): 📷 Image · 📄 File · ⚡ Attach Qals · 🔒 Locked content. Attaching value is now exactly like attaching a picture: pick ⚡, set an amount (live doof equivalent shown — 1 QALS = 1,000,000 doof), hit Confirm & attach — a removable gold chip stages on the composer and the loopd transfer fires with your next message. In the thread it renders as the permanent gold value badge (⚡ 0.01 QALS) with the settle id — hover shows the doof equivalent. Plain ⚡ Pay from the same row sends payment-only, as before.

v3 — the Chat tab is a real messenger (Telegram/Signal-grade)

The chat tab is now a two-column shell inside the desktop 3-pane layout: a conversation rail + the open thread. Still vanilla JS, no npm.

Bubbles & thread

Attach menu (the Signal/Telegram model — value is just an attachment)

Option What it does
📷 Image path (or a received file) → encrypted send → inline image bubble (click to zoom)
📄 File encrypted send → file card with size + sha256 (+ anchor tx when anchored)
⚡ Attach Qals amount → doof preview → confirmation → gold staged chip → fires with the message
🔒 Locked content price + optional preview + file → sealed; renders as a blurred locked card with a price badge

Locked content (qalpub protocol)

Media rendering

Received files (qalchat.py recv-files) hydrate into their message bubbles: images inline (bounded 4 MB reads, img-src data: CSP already allowed), everything else as sha256 file cards. Outbound attachments are remembered so your own sends render the same way.

Find / mute / unread

Channels (Discord-model, under Chat)

qalpub protocol wiring — real vs mock

The full contract is wired for real; a --mock dev toggle keeps the UI alive when the qalpub server is down:

Command Contract Wallet command
qalpay.py listings --json --as H [{listing_id,title,price_cents,preview_*,unlocks[]}] qalpay_listings
qalpay.py buy-unlock <id> --as H --out DIR ✔ UNLOCKED <id> → <path> qalpay_buy_unlock
qalpay.py send-locked <to> <file> --price-cents N [--title][--preview][--public] --as H listing + LOCKED-MANIFEST qalpay_send_locked
qalchannels.py list --json {channels:[{channel_id,name,topic,posts_count,last_digest}]} qalch_list
qalchannels.py read <ch> --json {posts:[{author,body,ts,sig_ok,…}]} qalch_read
qalchannels.py post <ch> <text> --as H ok qalch_post
qalchannels.py digest <ch> anchored digest (tx parsed for the badge) qalch_digest

Mode toggle (Settings drawer → qalpub protocol): auto (default — real CLIs when detected, mock otherwise) / mock (force the in-app mock store: channels, posts and demo listings incl. a real-image unlock flip) / real (require the CLIs; pending states otherwise). Every degraded state is explicit in the UI (live / mock / pending chips) — never silent. QALPUB_HOME env overrides the qalpub dir (default: qalpub/ next to qalchat/).

v4 — the "qal-fly" design system (Firefly-derived UI, 2026-09-09)

User complaint: "UI of all the apps should be improved. some lines get cut and some displays are not good. Review and redo many of the app design features… Look at the repos with Firefly app for reference on design styles."

The whole UI was rebuilt against Firefly (IOTA's official wallet) — its design system lives in repos/firefly/packages/shared (tailwind.config.js, Tile/Box/Pill components, dashboard Sidebar). Extracted values are credited in the token block at the top of ui/styles.css:

Token family Values (Firefly source)
Surfaces bg #0B1524 (Shimmer bg lifted) · rail #0E1A2C · cards #15233B (gray-1000) · raised #1B2D4B (gray-900) · hover #223457 (gray-850)
Accents primary teal #17E1D5 (turquoise-500 / Shimmer highlight) · active-nav blue #108CFF (blue-500)
Status success #61E897 (green-500, confirmed/backed only per brand rule v1) · warning #FFB526 · danger #FF6755 · Qals gold #FFD166 kept
Type Inter + Fira Mono stacks · 8px spacing grid (--q-1..--q-8) · radius 6/10/14/20 · Firefly elevation-1..4 shadows

Layout changes

Cut-line fixes (the explicit complaint)

Audit

bash wallet-app/ui/audit.sh (45 checks): token block present · zero inline styles in index.html · all 218 class references in app.js+index.html defined in styles.css · mono-trunc + mid() wired · anti-clip + tabular-nums rules present. Screenshot of the running app: ui/audit-v3.png. Prior UI files are archived at ui/archive/ (*_v3q_20260909.*).

v5 — Slack-class social layer (workspaces · threads · reactions · presence)

2026-09-09. Chat graduated from "channels" to a full Slack-class interface on the qalpub v2 backend (see qalpub/README.md § C). All new traffic rides three generic IPC bridges (qalpub_get/post/delete, native ureq → http://127.0.0.1:8832, path-locked to the local origin because the webview CSP forbids direct fetch). Signature-required writes (posts + thread replies) still go through the signed-CLI path (qalch_post, now with reply_to).

What the Chat tab does now

Files & tests

v2 — onboarding, vault, verification

Onboarding flow (first screen)

Path Steps
Create account pick handle → seedkit mnemonic (12 words, BIP-39, show-once grid with copy + "I wrote it down" checkbox) → identity derived via qalchat.py init --mnemonic … (qal-derivation v1, loopd agent auto-bound) → set app passwordsuccess screen: your qoll code
Restore from seed 12/24-word entry → same deterministic derivation, keys byte-identical to the originals
Import identity file paste identity JSON (refuses public-only files) or unwrap an encrypted export container

Existing installs (identities but no vault) get a one-click "Set an app password" path. The mnemonic is displayed ONCE and discarded from memory on the very next step — the wallet never writes a seed stub (the CLI's 0600 stub is vaulted too).

Password lock (vault)

Verification UI (Contacts)

Files tab

Encrypted transfer via qalchat (send-file ≤8 MB chunked, recv-files sha256-verified, auto-anchored on receive — the ⚓ tx renders inline per file and in the context pane). The tab probes the CLI at runtime (files_capable) and shows a graceful "files are coming online" stub if the subcommands are absent. Listing reads the canonical files --json index (fallback: download-dir scan).

Tabs

Pane What ships
Wallet B-QALS balance + reserve-backed ✓ badge, G-QALS line, holds (115% pre-auth), Send (HMAC-signed), Topup (paper mint), activity feed — context: my identity + code
Chat messenger rail (search · conversations · channels), thread (3 s poll, recv merged across every relay, bubbles + ticks + day separators + media + locked cards), attach menu (📷 / 📄 / ⚡ Qals / 🔒 locked), find-in-conversation, mute, unread badges — context: peer's pin state or channel digest
Compute live price board, converter, buy (hold → execute → settle → anchor), job history — context: clicked job's receipt + anchor tx/object/sha256
Contacts codes, fingerprints, invites, add-person — context: selected contact's full verification state
Files send to pinned contact, received list with anchor txs — context: selected file's sha256 + anchor
Agents loopd agents, caps, revoke/restore (only while the vault holds the secret)

Settings (sidebar account card → drawer): loopd / multi-relay list / qmarket URLs, identity selector, auto-lock minutes, vault status, export identity.

Command surface (src-tauri → frontend)

svc_status            health of the three services (sidebar pills)
relay_probe           per-URL health for the multi-relay list
loopd_report  loopd_receipts  loopd_account
loopd_transfer        (signs w/ local identity secret — X-Sig over raw body)
loopd_topup           (paper mint, Phase 0)
loopd_revoke          (self-revoke only — needs the local secret)
market_providers  market_jobs  market_earnings  market_buy
relay_envelopes       (ciphertext fetch for a handle)
chat_send chat_recv chat_pay chat_anchor chat_verify   ── delegate to CLI
qalchat_cli           (raw passthrough, e.g. `recover --force`)
list_identities  owned_agent_ids  qalchat_home_info  read_history
── v2 ──
vault_status  vault_setup  vault_unlock  vault_lock     (passwords via STDIN)
seed_mnemonic         (seedkit/derive.py mnemonic — 12 words)
account_create        (qalchat init --mnemonic …; deterministic restore)
import_identity_json  import_identity_container  export_identity
my_id_code            (qoll-XXXX-XX + fingerprint — works while sealed)
list_contacts         (pins + fingerprints + codes, no subprocess)
contact_pin_code      (keyserver resolve → TOFU-warned code pin)
invite_create         (qalchat invite → URI + fingerprint + QR data-URL)
invite_accept         (qalchat accept; signature verified before pinning)
files_capable  file_send  files_recv  files_list  files_reveal
── v3: qalpub protocol + inline media ──
qalpub_capable  qalpay_listings  qalpay_buy_unlock  qalpay_send_locked
qalch_list  qalch_read  qalch_post  qalch_digest
resolve_received_path  file_data_url

Security model — what protects what

Layer Secret Protects Does NOT Where it lives
App password PBKDF2-SHA256 ×200k + per-install salt → AES-256-GCM key material at rest on this device (stolen laptop / copied data/) recover the identity elsewhere; stop a live attacker while unlocked; weak passwords are weak data/vault.bin — identity files hold public fields only while locked
Mnemonic (12 words) BIP-39 seed → qal-derivation v1 → every key, deterministically recovery of everything on any device: identity, chat ratchet (relay replay), loopd binding + balance protect the device at rest; anyone who sees the words owns the identity; no recovery if lost your paper (shown ONCE; never stored by the wallet — the CLI stub is vaulted)
Contact pins invite fingerprints + pinned pubkeys (ed25519-signed invites) relay key substitution / MITM: different key for a pinned handle → exit 4, delivery blocked, red banner encrypt anything; survive a compromised out-of-band channel (verify codes/fingerprints by voice/IRL) data/pinned/<me>/*.json + identity pins (public — never vaulted)

All three are independent and all three matter: the password guards the device, the mnemonic guards against the device dying, the pins guard the wire. Losing any one costs exactly what it protects — nothing more. The qoll code is a presentation of the pin/fingerprint layer, not a fourth secret.

Unchanged from v1: the wallet holds no crypto of its own (all X25519/Ed25519/AES-GCM lives in qalchat.py, delegated via subprocess); chat is ciphertext-blind at the relay; loopd signing reads identity secrets only in-process and never returns them across the IPC boundary; revoke is self-service only.

Run

# dev (uses the static ui/ — no devUrl, no npm):
cd wallet-app/src-tauri && cargo tauri dev

# production debug build:
cd wallet-app/src-tauri && cargo tauri build --debug

# plain build (the acceptance test):
cd wallet-app/src-tauri && cargo build

# full smoke (binary + services + vault round-trip + unit tests
#               + v3 chat UI contract + qalpub protocol probe):
bash wallet-app/smoke.sh

Prereqs: loopd (:8823) + relay (:8830, python3 qalchat/qalchat.py serve) + qmarket (:8824); chain on :9000 only for anchor/verify (via qalpipe). QALCHAT_HOME locates the identity store; vault.py follows the same QALCHAT_DIR env var as qalchat.py.

Tests

Phase-0 architecture note (deliberate)

All crypto/chat operations delegate to the Python CLI via std::process::Command — wire compatibility with live identities/relay on day one. Native Rust crypto (x25519-dalek, hkdf, aes-gcm, ed25519-dalek) remains the phase-1 milestone. The Rust layer does native HTTP + HMAC (hmac + sha2) for loopd money rails. The vault is python-side (cryptography AESGCM + hashlib PBKDF2) so CLI and wallet share one implementation; the only Rust "crypto" is the sha256+b32 presentation layer for fingerprints/codes (unit-tested against the python reference).

Security notes