feat(ui): receive: address, QR, checkphrase and copy; accounts named in place
ci / gate (push) Waiting to run
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:
Generated
+310
-1
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
Generated
+18
@@ -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
@@ -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 />} />
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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 }, };
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user