feat(ui): receive: address, QR, checkphrase and copy; accounts named in place
ci / gate (push) Waiting to run

The receive screen shows one account's address as text, as a QR (drawn
in the webview by qrcode-generator, since an address is public), and as
the five-word checkphrase Rust derives, with a copy button through
tauri-plugin-clipboard-manager granted only write-text, with its reason.
Accounts can be named in place; the labels live in settings under
account_names, keyed by address, and show on the receive screen too.

Checked against the dev node: the checkphrase for crystal_alice reads
Toast-Terrific-Kitten-Warrior-Churn, the same as `quantus wallet list`;
a `quantus send` of 7 DEV from crystal_bob raised the accounts row from
99,957.786510548 to 99,964.786510548 within the next head; Copy flips
to Copied; a rename is written to settings.json and survives a lock.

Closes #27

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014ftBXYuba8ARhQeF74oUgW
This commit is contained in:
2026-09-15 23:36:46 +03:00
co-authored by Claude Fable 5.1
parent 7101ead194
commit e4cde9a3db
17 changed files with 559 additions and 3 deletions
Generated
+310 -1
View File
@@ -100,6 +100,27 @@ version = "1.0.104"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
[[package]]
name = "arboard"
version = "3.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0348a1c054491f4bfe6ab86a7b6ab1e44e45d899005de92f58b3df180b36ddaf"
dependencies = [
"clipboard-win",
"image",
"log",
"objc2",
"objc2-app-kit",
"objc2-core-foundation",
"objc2-core-graphics",
"objc2-foundation",
"parking_lot",
"percent-encoding",
"windows-sys 0.60.2",
"wl-clipboard-rs",
"x11rb",
]
[[package]]
name = "argon2"
version = "0.5.3"
@@ -520,6 +541,12 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "byteorder-lite"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
[[package]]
name = "bytes"
version = "1.12.1"
@@ -672,6 +699,15 @@ dependencies = [
"inout",
]
[[package]]
name = "clipboard-win"
version = "5.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4"
dependencies = [
"error-code",
]
[[package]]
name = "combine"
version = "4.6.8"
@@ -1335,6 +1371,12 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "error-code"
version = "3.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b5343afd4a8365a643ac588dab4cf234a190c7f6c88c9f6dd6ffe00837661b7"
[[package]]
name = "event-listener"
version = "5.4.2"
@@ -1361,6 +1403,12 @@ version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223"
[[package]]
name = "fax"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "caf1079563223d5d59d83c85886a56e586cfd5c1a26292e971a0fa266531ac5a"
[[package]]
name = "fdeflate"
version = "0.3.7"
@@ -1404,6 +1452,12 @@ dependencies = [
"static_assertions",
]
[[package]]
name = "fixedbitset"
version = "0.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99"
[[package]]
name = "flate2"
version = "1.1.10"
@@ -1733,6 +1787,16 @@ dependencies = [
"version_check",
]
[[package]]
name = "gethostname"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8"
dependencies = [
"rustix",
"windows-link 0.2.1",
]
[[package]]
name = "getrandom"
version = "0.2.17"
@@ -1955,6 +2019,17 @@ dependencies = [
"tracing",
]
[[package]]
name = "half"
version = "2.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b"
dependencies = [
"cfg-if",
"crunchy",
"zerocopy",
]
[[package]]
name = "hashbrown"
version = "0.12.3"
@@ -2326,6 +2401,20 @@ dependencies = [
"icu_properties",
]
[[package]]
name = "image"
version = "0.25.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6506c6c10786659413faa717ceebcb8f70731c0a60cbae39795fdf114519c1a"
dependencies = [
"bytemuck",
"byteorder-lite",
"moxcms",
"num-traits",
"png 0.18.1",
"tiff",
]
[[package]]
name = "impl-codec"
version = "0.7.1"
@@ -2880,6 +2969,16 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "moxcms"
version = "0.7.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac9557c559cd6fc9867e122e20d2cbefc9ca29d80d027a8e39310920ed2f0a97"
dependencies = [
"num-traits",
"pxfm",
]
[[package]]
name = "muda"
version = "0.19.3"
@@ -3065,6 +3164,7 @@ dependencies = [
"block2",
"objc2",
"objc2-core-foundation",
"objc2-core-graphics",
"objc2-foundation",
]
@@ -3302,6 +3402,16 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
[[package]]
name = "os_pipe"
version = "1.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967"
dependencies = [
"libc",
"windows-sys 0.61.2",
]
[[package]]
name = "pango"
version = "0.18.3"
@@ -3411,6 +3521,17 @@ version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
[[package]]
name = "petgraph"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455"
dependencies = [
"fixedbitset",
"hashbrown 0.15.5",
"indexmap 2.14.2",
]
[[package]]
name = "phf"
version = "0.13.1"
@@ -3515,7 +3636,7 @@ checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07"
dependencies = [
"base64 0.22.1",
"indexmap 2.14.2",
"quick-xml",
"quick-xml 0.38.4",
"serde",
"time",
]
@@ -3710,6 +3831,12 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "pxfm"
version = "0.1.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d55d956fa96f5ec02be2e13af0e20391a5aa83d6a074e3ad368959d0fab299ea"
[[package]]
name = "qp-human-checkphrase"
version = "2.2.1"
@@ -3758,6 +3885,12 @@ dependencies = [
"zeroize",
]
[[package]]
name = "quick-error"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
[[package]]
name = "quick-xml"
version = "0.38.4"
@@ -3767,6 +3900,15 @@ dependencies = [
"memchr",
]
[[package]]
name = "quick-xml"
version = "0.41.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1"
dependencies = [
"memchr",
]
[[package]]
name = "quote"
version = "1.0.47"
@@ -5406,6 +5548,21 @@ dependencies = [
"walkdir",
]
[[package]]
name = "tauri-plugin-clipboard-manager"
version = "2.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4136fb69d967753d000423d7e5f863f89bf949efbdfbecb43a580426a01a0194"
dependencies = [
"arboard",
"log",
"serde",
"serde_json",
"tauri",
"tauri-plugin",
"thiserror 2.0.18",
]
[[package]]
name = "tauri-plugin-dialog"
version = "2.7.3"
@@ -5631,6 +5788,20 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "tiff"
version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af9605de7fee8d9551863fd692cce7637f548dbd9db9180fcc07ccc6d26c336f"
dependencies = [
"fax",
"flate2",
"half",
"quick-error",
"weezl",
"zune-jpeg",
]
[[package]]
name = "time"
version = "0.3.45"
@@ -6007,6 +6178,17 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "tree_magic_mini"
version = "3.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8765b90061cba6c22b5831f675da109ae5561588290f9fa2317adab2714d5a6"
dependencies = [
"memchr",
"nom",
"petgraph",
]
[[package]]
name = "try-lock"
version = "0.2.5"
@@ -6271,6 +6453,7 @@ dependencies = [
"serde_json",
"tauri",
"tauri-build",
"tauri-plugin-clipboard-manager",
"tauri-plugin-dialog",
"toml 0.8.2",
"tracing",
@@ -6478,6 +6661,76 @@ dependencies = [
"bitflags 2.13.2",
]
[[package]]
name = "wayland-backend"
version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38a91b4eaddff87b1cd1074985e3713da4af2c49742d1b356b2c01670a67a078"
dependencies = [
"cc",
"downcast-rs",
"rustix",
"smallvec",
"wayland-sys",
]
[[package]]
name = "wayland-client"
version = "0.31.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3c36a0f861ad76d0901f2800b46321410d9f73f2ea88aac0650d86c32688073"
dependencies = [
"bitflags 2.13.2",
"rustix",
"wayland-backend",
"wayland-scanner",
]
[[package]]
name = "wayland-protocols"
version = "0.32.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "563a85523cade2429938e790815fd7319062103b9f4a2dc806e9b53b95982d8f"
dependencies = [
"bitflags 2.13.2",
"wayland-backend",
"wayland-client",
"wayland-scanner",
]
[[package]]
name = "wayland-protocols-wlr"
version = "0.3.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb04e52f7836d7c7976c78ca0250d61e33873c34156a2a1fc9474828ec268234"
dependencies = [
"bitflags 2.13.2",
"wayland-backend",
"wayland-client",
"wayland-protocols",
"wayland-scanner",
]
[[package]]
name = "wayland-scanner"
version = "0.31.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "338e30461b3a2b67d70eb30a6d89f8e0c93a833e07d2ae89085cd070c4a00ac0"
dependencies = [
"proc-macro2",
"quick-xml 0.41.0",
"quote",
]
[[package]]
name = "wayland-sys"
version = "0.31.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8eab23fefc9e41f8e841df4a9c707e8a8c4ed26e944ef69297184de2785e3be"
dependencies = [
"pkg-config",
]
[[package]]
name = "web-sys"
version = "0.3.105"
@@ -6608,6 +6861,12 @@ dependencies = [
"windows-core 0.61.2",
]
[[package]]
name = "weezl"
version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88"
[[package]]
name = "winapi"
version = "0.3.9"
@@ -7093,6 +7352,24 @@ version = "0.46.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59"
[[package]]
name = "wl-clipboard-rs"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e9651471a32e87d96ef3a127715382b2d11cc7c8bb9822ded8a7cc94072eb0a3"
dependencies = [
"libc",
"log",
"os_pipe",
"rustix",
"thiserror 2.0.18",
"tree_magic_mini",
"wayland-backend",
"wayland-client",
"wayland-protocols",
"wayland-protocols-wlr",
]
[[package]]
name = "writeable"
version = "0.6.4"
@@ -7173,6 +7450,23 @@ dependencies = [
"pkg-config",
]
[[package]]
name = "x11rb"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414"
dependencies = [
"gethostname",
"rustix",
"x11rb-protocol",
]
[[package]]
name = "x11rb-protocol"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd"
[[package]]
name = "x25519-dalek"
version = "2.0.1"
@@ -7319,3 +7613,18 @@ name = "zmij"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
[[package]]
name = "zune-core"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a"
[[package]]
name = "zune-jpeg"
version = "0.4.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29ce2c8a9384ad323cf564b67da86e21d3cfdff87908bc1223ed5c99bc792713"
dependencies = [
"zune-core",
]
+2
View File
@@ -63,6 +63,8 @@ tauri-build = { version = "2", features = ["isolation"] }
# Native file dialogs, for importing a quantus-cli wallet file. The only
# plugin so far; each one is a capability entry with a reason.
tauri-plugin-dialog = "2"
# Copying an address to the clipboard from the receive screen. Write only.
tauri-plugin-clipboard-manager = "2"
[profile.release]
lto = "thin"
+1
View File
@@ -23,6 +23,7 @@ wallet-core.workspace = true
wallet-data.workspace = true
tauri.workspace = true
tauri-plugin-dialog.workspace = true
tauri-plugin-clipboard-manager.workspace = true
serde.workspace = true
serde_json.workspace = true
tracing.workspace = true
@@ -13,4 +13,6 @@ permissions = [
"core:default",
# The native "open file" dialog, so a person can pick a quantus-cli wallet file to import. Only open, never save.
"dialog:allow-open",
# Copy an address from the receive screen. Write only: the app never reads what is on the clipboard.
"clipboard-manager:allow-write-text",
]
+1
View File
@@ -14,6 +14,7 @@ pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_clipboard_manager::init())
.manage(state)
.setup(|app| {
// The idle rule is applied on every command too; this ticker is
+1
View File
@@ -82,6 +82,7 @@ mod tests {
network: ChainId("planck".into()),
idle_lock_seconds: 120,
show_testnets: true,
account_names: [("qz1".to_string(), "savings".to_string())].into(),
};
store.save(&changed).unwrap();
assert_eq!(store.load().unwrap(), changed);
+5
View File
@@ -13,6 +13,7 @@
//! Amounts are the exception and travel as strings.
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use ts_rs::TS;
/// Identifies a chain the wallet knows a profile for, e.g. `quantus`,
@@ -429,6 +430,9 @@ pub struct Settings {
pub idle_lock_seconds: u64,
/// Whether testnet profiles are offered in the switcher.
pub show_testnets: bool,
/// Labels people give their accounts, by address. Cosmetic: an
/// address without one is shown as itself.
pub account_names: BTreeMap<String, String>,
}
impl Default for Settings {
@@ -437,6 +441,7 @@ impl Default for Settings {
network: ChainId("quantus".into()),
idle_lock_seconds: 10 * 60,
show_testnets: false,
account_names: BTreeMap::new(),
}
}
}
+2
View File
@@ -16,7 +16,9 @@
"dependencies": {
"@tanstack/react-query": "^5.102.8",
"@tauri-apps/api": "^2.9.0",
"@tauri-apps/plugin-clipboard-manager": "^2.3.3",
"@tauri-apps/plugin-dialog": "^2.7.3",
"qrcode-generator": "^2.0.4",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"react-router-dom": "^7.8.0"
+18
View File
@@ -14,9 +14,15 @@ importers:
'@tauri-apps/api':
specifier: ^2.9.0
version: 2.11.1
'@tauri-apps/plugin-clipboard-manager':
specifier: ^2.3.3
version: 2.3.3
'@tauri-apps/plugin-dialog':
specifier: ^2.7.3
version: 2.7.3
qrcode-generator:
specifier: ^2.0.4
version: 2.0.4
react:
specifier: ^19.1.1
version: 19.3.0
@@ -535,6 +541,9 @@ packages:
'@tauri-apps/api@2.11.1':
resolution: {integrity: sha512-M2FPuYND2m+wh5hfW9ZpSdxMPdEJovPBWwoHJmwUpysTYNHaOkVFN419m/K0LIgjb/7KU2vBgsUepJWugQCvAA==}
'@tauri-apps/plugin-clipboard-manager@2.3.3':
resolution: {integrity: sha512-KnyoTs9gj1yEgDkSPUNjOIOHjJTr5wk8IWcYMOWxYTIJCip6QwlyPW8u2X+6bd6kHM4fAdZNpxoal0gy/TwJbg==}
'@tauri-apps/plugin-dialog@2.7.3':
resolution: {integrity: sha512-CRgE+7TP4tvq9MjBU6f04NLTFIqVMLKHk3hAqlhil00ngK9ACTrXPH3oHpKMProxILodd3YjBoKbMwSI4IEcfA==}
@@ -936,6 +945,9 @@ packages:
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
engines: {node: '>=6'}
qrcode-generator@2.0.4:
resolution: {integrity: sha512-mZSiP6RnbHl4xL2Ap5HfkjLnmxfKcPWpWe/c+5XxCuetEenqmNFf1FH/ftXPCtFG5/TDobjsjz6sSNL0Sr8Z9g==}
react-dom@19.3.0:
resolution: {integrity: sha512-JDk8dgif51OjFoDE70+OT9ICyYr+69HlmihNwp1+Nsfbna3t5sIiCa9ZJktDmQ4/1b/rn26hIAR2uYXDMr5r0Q==}
peerDependencies:
@@ -1378,6 +1390,10 @@ snapshots:
'@tauri-apps/api@2.11.1': {}
'@tauri-apps/plugin-clipboard-manager@2.3.3':
dependencies:
'@tauri-apps/api': 2.11.1
'@tauri-apps/plugin-dialog@2.7.3':
dependencies:
'@tauri-apps/api': 2.11.1
@@ -1805,6 +1821,8 @@ snapshots:
punycode@2.3.1: {}
qrcode-generator@2.0.4: {}
react-dom@19.3.0(react@19.3.0):
dependencies:
react: 19.3.0
+2 -1
View File
@@ -6,6 +6,7 @@ import { StatusBar } from './components/StatusBar'
import { Accounts } from './routes/Accounts'
import { Create, ImportFile, OnboardingIndex, Restore } from './routes/Onboarding'
import { Placeholder } from './routes/Placeholder'
import { Receive } from './routes/Receive'
import { SettingsRoute } from './routes/Settings'
// The webview renders; it never holds a key, decodes a transaction, or does
@@ -46,7 +47,7 @@ function Gate() {
<Routes>
<Route path="/" element={<Accounts />} />
<Route path="/send" element={<Placeholder title="Send" issue={28} />} />
<Route path="/receive" element={<Placeholder title="Receive" issue={27} />} />
<Route path="/receive" element={<Receive />} />
<Route path="/history" element={<Placeholder title="History" issue={30} />} />
<Route path="/settings" element={<SettingsRoute />} />
<Route path="/onboarding/*" element={<OnboardingRoutes />} />
+8
View File
@@ -0,0 +1,8 @@
// Write-only clipboard access, for copying an address. The app never reads
// the clipboard; the capability grants only write-text.
import { writeText } from '@tauri-apps/plugin-clipboard-manager'
export function copyText(text: string): Promise<void> {
return writeText(text)
}
+6 -1
View File
@@ -16,4 +16,9 @@ idle_lock_seconds: number,
/**
* Whether testnet profiles are offered in the switcher.
*/
show_testnets: boolean, };
show_testnets: boolean,
/**
* Labels people give their accounts, by address. Cosmetic: an
* address without one is shown as itself.
*/
account_names: { [key in string]?: string }, };
+51
View File
@@ -0,0 +1,51 @@
import { useState } from 'react'
import { useSetSettings, useSettings } from '../api/hooks'
/** An account's label from settings, editable in place. Cosmetic only. */
export function AccountName({ address }: { address: string }) {
const settings = useSettings()
const set = useSetSettings()
const [editing, setEditing] = useState(false)
const [draft, setDraft] = useState('')
const current = settings.data?.account_names[address] ?? ''
function save() {
if (!settings.data) return
const names = { ...settings.data.account_names }
const trimmed = draft.trim()
if (trimmed) names[address] = trimmed
else delete names[address]
set.mutate({ ...settings.data, account_names: names })
setEditing(false)
}
if (editing) {
return (
<input
className="input"
autoFocus
value={draft}
placeholder="name this account"
onChange={(e) => setDraft(e.target.value)}
onBlur={save}
onKeyDown={(e) => {
if (e.key === 'Enter') save()
if (e.key === 'Escape') setEditing(false)
}}
/>
)
}
return (
<button
type="button"
className="linkish"
title="Rename"
onClick={() => {
setDraft(current)
setEditing(true)
}}
>
{current || <span className="muted">unnamed</span>}
</button>
)
}
+36
View File
@@ -317,3 +317,39 @@ select {
textarea.input {
resize: vertical;
}
/* ---- receive ------------------------------------------------------------- */
.receive {
display: flex;
gap: 24px;
align-items: flex-start;
flex-wrap: wrap;
}
.qr {
width: 180px;
height: 180px;
padding: 8px;
background: #fff;
border-radius: 6px;
flex: none;
}
.qr svg {
width: 100%;
height: 100%;
display: block;
}
.address {
word-break: break-all;
}
.linkish {
background: none;
border: none;
padding: 0;
font: inherit;
color: inherit;
cursor: pointer;
text-align: left;
}
.linkish:hover {
text-decoration: underline;
}
+10
View File
@@ -0,0 +1,10 @@
// A QR code as an SVG string. The library runs in the webview because what
// it encodes is an address, which is public; nothing secret passes here.
import qrcode from 'qrcode-generator'
export function qrSvg(text: string, cellSize = 4): string {
const qr = qrcode(0, 'M')
qr.addData(text)
qr.make()
return qr.createSvgTag({ cellSize, margin: 2, scalable: true })
}
+5
View File
@@ -1,4 +1,5 @@
import { useBalances, useSession, useSettings } from '../api/hooks'
import { AccountName } from '../components/AccountName'
import { ChainCaveat } from '../components/ChainCaveat'
import { formatAmount } from '../lib/format'
@@ -40,6 +41,7 @@ export function Accounts() {
<table className="table">
<thead>
<tr>
<th>Name</th>
<th>Address</th>
<th>Scheme</th>
<th className="num">Spendable</th>
@@ -50,6 +52,9 @@ export function Accounts() {
<tbody>
{balances.data.map((b, i) => (
<tr key={b.address}>
<td>
<AccountName address={b.address} />
</td>
<td className="mono">{b.address}</td>
<td>{session.data?.accounts[i]?.scheme ?? ''}</td>
<td className="num numeral">
+99
View File
@@ -0,0 +1,99 @@
import { useEffect, useState } from 'react'
import { useQuery } from '@tanstack/react-query'
import { copyText } from '../api/clipboard'
import { useSession, useSettings } from '../api/hooks'
import { checkphrase } from '../api/wallet'
import { ChainCaveat } from '../components/ChainCaveat'
import { qrSvg } from '../lib/qr'
/**
* One account's address to hand to a sender: as text, as a QR, and as the
* checkphrase they can read back over the phone. The address is the same
* on every Quantus-family chain; the caveat says which one is selected.
*/
export function Receive() {
const session = useSession()
const settings = useSettings()
const accounts = session.data?.accounts ?? []
const [address, setAddress] = useState('')
const selected = address || accounts[0]?.address || ''
const phrase = useQuery({
queryKey: ['checkphrase', selected],
queryFn: () => checkphrase(selected),
enabled: !!selected,
staleTime: Infinity,
})
const [copied, setCopied] = useState(false)
useEffect(() => {
if (!copied) return
const t = setTimeout(() => setCopied(false), 1500)
return () => clearTimeout(t)
}, [copied])
if (!selected) return null
const name = settings.data?.account_names[selected]
return (
<>
<ChainCaveat />
<section className="panel">
<div className="panel-head">
<h2 className="panel-title">Receive</h2>
</div>
<div className="panel-body">
{accounts.length > 1 && (
<div className="field">
<label htmlFor="account">Account</label>
<select
id="account"
className="select"
value={selected}
onChange={(e) => setAddress(e.target.value)}
>
{accounts.map((a) => (
<option key={a.address} value={a.address}>
{settings.data?.account_names[a.address] ?? a.address}
</option>
))}
</select>
</div>
)}
<div className="receive">
<div
className="qr"
aria-label={`QR code for ${selected}`}
dangerouslySetInnerHTML={{ __html: qrSvg(selected) }}
/>
<div>
{name && <div className="eyebrow">{name}</div>}
<div className="field">
<label>Address</label>
<div className="mono address" data-testid="address">
{selected}
</div>
</div>
<div className="field">
<label>Checkphrase</label>
<div className="mono" data-testid="checkphrase">
{phrase.data ?? (phrase.isError ? phrase.error.message : '…')}
</div>
<span className="muted" style={{ fontSize: 12 }}>
Five words derived from the address. A sender who sees the same five words on
their side is sending to this account.
</span>
</div>
<div className="row">
<button
className="button primary"
type="button"
onClick={() => copyText(selected).then(() => setCopied(true))}
>
{copied ? 'Copied' : 'Copy address'}
</button>
</div>
</div>
</div>
</div>
</section>
</>
)
}