Add interactive TUI mode for grin-wallet - #771
Conversation
* build: update rust edition 2021 * cargo: format all * build: fix tests and warnings
* scan: save last scanned block info for initial wallet scanning * scan: save start height at scanned block info on outputs collecting * scan: 10k blocks batches * fix: include last height into batch on scan * scan: save hash of last block * scan: add larger window (2880 blocks) to scan outputs when updating wallet state * add test batched scan and resume, add scan doc * rename corrupted restore test --------- Co-authored-by: Joerg <wiesche89@googlemail.com>
* wallet: update lmdb from store crate, migrate single backend from trait to struct, update tests cargo deps and design doc * backend: do not panic on iter and directory creation * return iterator errors * build: update to last node revision * fix: height selection for scan, remove unused mutability for wallet * wallet: remove unused mutability conversion * tx: store slate state at database * build: cargo fmt * fix: do not return error when slate not found on tx slate state update * lmdb: do not collect all tx log records at iterator, show read errors at log * fix: save slate state, return error if slate not found * tx: specify amount of bad txs on slate state update error --------- Co-authored-by: Joerg <wiesche89@googlemail.com>
* build: use node submodule as dependency * ci: checkout submodules * docs: add contributing and build instruction * ci: update release build * git: remove old submodule * git: grin staging submodule * git: add submodule dir
* tor: integrated client for listener, update tokio to last version * tor: send transaction with integrated client * build: code format * tor: optimize bootstrap percent check * tor: handle config build error * tor: do not show slatepack address if tor not launched * tor: handle keystore errors * tor: keep service launched * tor: bootstrap timeout, clean cache before client launch * tor: post request timeout * fix: post timeout * build: update deps after merge * tor: store arti runtime * tor: do not send over tor if `manual` arg presents or `skip_send_attempt` set at config * fix: arg name * tor: handle arti runtime creation error * tor: handle connection error * tor: handle bootstrap error * tor: handle start service name error * tor: handle service config creation error * tor: service proxy errors * tor: handle service key creation emptiness * build: format code * build: update arti to 0.43, hyper to 0.10.1 * api: start foreign listener before tor connection * tor: handle json parse errors * tor: prevent to create several client runtimes, ability to recreate * tor: return bootstrap error * tor: handle host parse error * tor: restart proxy on error * tor: do not use integrated client when config value use_integrated is missing, add logging * build: update lock file
* tor: global client state * tor: optimize init client thread-safety * tor: handle config change on client init * tor: add logging for client reuse or reinit * tor: do not reuse arti client for requests
…timize sending requirement detection
… slate state after tor finalization on receive
# Conflicts: # Cargo.lock # Cargo.toml # api/Cargo.toml # api/src/foreign.rs # api/src/owner.rs # config/Cargo.toml # controller/Cargo.toml # controller/tests/common/mod.rs # impls/Cargo.toml # libwallet/Cargo.toml # util/Cargo.toml
…ter tor sync flow
Merge master to staging
Optimize tor sync flow
Adds a `grin-wallet tui` subcommand: a ratatui-based dashboard (account status, accounts, outputs, transactions, settings, logs) refreshed live from the Owner API, plus an Actions menu covering every wallet subcommand. Submitting an action form builds the equivalent CLI argument vector and runs it through the same wallet_args::parse_and_execute dispatch the plain CLI uses, so existing behavior (password prompts, slatepack exchange, Tor sending, payment proofs, invoices, etc.) works unchanged with no duplicated logic. Closes mimblewimble#751
Most Actions (send/receive/finalize/invoice/scan/etc.) now execute on background threads with structured result modals instead of suspending the alternate screen and shelling out to the CLI. Adds slatepack paste input, QR/output modals, form validation, row context menus, editable settings written back to grin-wallet.toml, help overlay, clipboard copy, and a feature-gated `tui` Cargo feature.
Actions that previously suspended the dashboard (and broke bordered terminal windows) now run in-process: close locks immediately, open and recover use a masked password modal, and the recovery phrase is shown in the existing output modal. The only remaining normal-terminal path is first-run init before the dashboard starts.
Resolve conflicts in grin-wallet.rs (keep TUI logs channel and staging API timeout on HTTPNodeClient) and regenerate Cargo.lock. Adapt the TUI worker to staging's try_slatepack_sync_workflow / Tor send_tor API and DefaultWalletImpl lifetime change.
* build: update grin submodule to last staging * build: update grin submodule * build: remove remove_dir_all dependency * build: update rpassword * build: update semver * build: remove unused deps * build: update ring * build: update lock file deps * build: update rustyline * build: update ed25519-dalek * build: update curve25519-dalek * build: update age * build: update uuid * fix: uuid test * build: update x25519-dalek * fix: x25519-dalek build * build: update arti * build: update grin submodule * build: update hyper * fix: initialize rustls provider for tests * build: update grin submodule * fix: rustls install provider * tor: share getting private key * fix: show an error on node version parse * cli: renamed prompt password method * fix: replace into_boxed_slice with try_from * fix: dalek tests for onion * config: comment expanded secret key * fix: import api body for controller * password: handle prompt error * wallet: parse node error message * tests: do not use random values for sign messages * build: make hook executable * build: cargo format all * rustls: do not install provider at tests and globally * build: update grin submodule * build: fix api channel from grin submodule * args: use read_password for piped prompt * build: remove unused dep * test: fixed signature * preserve piped pw input --------- Co-authored-by: wiesche <wiesche89@googlemail.com>
| crossterm = { version = "0.28", features = ["bracketed-paste"], optional = true } | ||
| qr_code = { version = "1.1.0", optional = true } | ||
| serde_json = { version = "1", optional = true } | ||
| uuid = { version = "0.8", features = ["serde", "v4"], optional = true } |
There was a problem hiding this comment.
This still conflicts with current staging. I also tested the merged sources with a regenerated lockfile: prompt_password now returns a Result, and uuid 0.8 here causes type conflicts with the uuid 1.x types from staging. Could you rebase the PR and update these places as part of it?
There was a problem hiding this comment.
Done in 6f6ae3a.
- Merged current
origin/staginginto this branch and resolved conflicts. prompt_passwordnow uses the stagingResultAPI throughout the TUI open path.- TUI optional
uuidbumped to1.xto match staging (no more 0.8 type conflicts). Owner::new/ listeners /foreign_single_useupdated for the new config-path APIs.
| if test_mode { | ||
| return false; | ||
| } | ||
| tor_config.send_tor(Some(manual)) |
There was a problem hiding this comment.
When Manual is not selected, we pass Some(false) here. send_tor already treats this as an explicit override, so skip_send_attempt = true is ignored and the TUI still tries Tor. Could we pass Some(true) only for Manual and None otherwise?
There was a problem hiding this comment.
Fixed.
can_send_tor now passes Some(true) only when Manual is selected, and None otherwise so a configured skip_send_attempt is still respected. Unit tests cover Manual, config-skip, and test_mode.
| for strategy in ["smallest", "all"] { | ||
| let init_args = InitTxArgs { | ||
| amount, | ||
| amount_includes_fee: Some(p.amount_includes_fee), |
There was a problem hiding this comment.
For max, the CLI automatically includes the fee in the amount. The TUI leaves this disabled by default, so it tries to spend the complete balance plus the fee. Could we use p.amount_includes_fee || use_max in both init paths?
There was a problem hiding this comment.
Fixed.
Send (estimate + real init) now uses amount_includes_fee = p.amount_includes_fee || use_max, matching the CLI spend_max behavior.
| } | ||
| } | ||
|
|
||
| fn switch_account(&mut self, label: String) { |
There was a problem hiding this comment.
Account switching, wallet close and quit are still available while a worker is running. This can close the wallet or terminate the process between transaction creation, output locking and slatepack writing. Could we block these lifecycle actions while busy is set?
There was a problem hiding this comment.
Fixed.
Added refuse_if_busy and block quit (q/Q), account switch, open, close, and recover while a worker is in progress, with a dialog naming the running operation.
| if let Some(b) = bridge { | ||
| tor_config.bridge.bridge_line = Some(b); | ||
| } | ||
| let mask = Arc::new(Mutex::new(ctx.shared.mask.lock().clone())); |
There was a problem hiding this comment.
The listener gets a snapshot of the mask instead of the shared state. After an in-TUI close/open, the old listener keeps running with the old mask and there is no stop path. Could we use the shared mask state and stop the listener cleanly when the wallet is closed?
There was a problem hiding this comment.
Partially fixed.
- Foreign listener now uses the shared
Arc<Mutex<Option<SecretKey>>>mask (same as the TUI open/close path), so re-open updates what the listener sees. - Not done: clean stop of the listener when the wallet is closed. That needs a shutdown/stop path in the controller (listeners currently run until process exit / join). Happy to follow up if we add a stop channel in a later PR.
|
|
||
| /// Word-wraps `text` to `width` columns, hard-breaking words that don't fit | ||
| /// on their own. Empty input produces a single empty line. | ||
| fn wrap_text(text: &str, width: usize) -> Vec<String> { |
There was a problem hiding this comment.
Ratatui's Paragraph already supports Wrap and line_count. This custom wrapper counts characters instead of terminal column widths, so wide Unicode characters can be wrapped incorrectly. Could we use Ratatui's existing reflow implementation and scroll the resulting paragraph to the bottom?
There was a problem hiding this comment.
Fixed (with a small compromise).
Logs use ratatui Paragraph + Wrap instead of the custom character-based wrapper. line_count is private in ratatui 0.29, so bottom-anchoring uses an approximate scroll from source line count.
|
|
||
| fn output_row(o: &OutputCommitMapping) -> Row<'static> { | ||
| Row::new(vec![ | ||
| Cell::from(short_commit(&o.output.commit)), |
There was a problem hiding this comment.
OutputData.commit is only optionally stored, while OutputCommitMapping.commit contains the commitment reconstructed for this response. The existing CLI display also uses the mapping field. Could we format o.commit here so valid outputs do not appear as -?
There was a problem hiding this comment.
Fixed.
Outputs view formats OutputCommitMapping.commit (hex), matching the CLI display path, instead of the optional OutputData.commit.
| } | ||
| lines.push(line( | ||
| "Data Refreshed:", | ||
| if app.view.validated { "Yes" } else { "Pending" }, |
There was a problem hiding this comment.
The refresher calls retrieve_summary_info with refresh_from_node = false, so the returned validated value is always false. Its newly created Owner also does not share the running updater flag, unlike the CLI path which uses validated || updater_running. This status therefore stays at Pending even after a successful sync. Could we derive it from the shared updater or node state?
There was a problem hiding this comment.
Fixed.
Refresher sets validated = validated || shared.updater_running, with updater_running shared from the controller Owner after start_updater, matching the CLI idea of “validated or updater running”.
| mod cli; | ||
| pub mod cmd; | ||
| #[cfg(feature = "tui")] | ||
| pub mod tui; |
There was a problem hiding this comment.
Does the TUI need to be part of the public Rust API? It currently has only one internal caller from wallet_args, so keeping this module private would avoid exposing the controller entry point and committing to its generic API.
There was a problem hiding this comment.
Fixed.
tui is now a private mod tui (only used from wallet_args / the binary), not part of the public crate API.
| Line::from("Esc : Back "), | ||
| Line::from("s : Spent "), | ||
| Line::from("? : Help "), | ||
| Line::from("Q : Quit "), |
There was a problem hiding this comment.
The hint shows uppercase Q, but the key handler only accepts lowercase q. Could we either show q here or handle both characters?
There was a problem hiding this comment.
Fixed.
Menu hint shows q, and the key handler accepts both q and Q. Quit is also blocked while a worker is busy.
* build: update lodash to 4.18.1 * fix: readme * docs: add owner api authentication setup * build: update package-lock.json to version 3 * fix: token result, inline some vars --------- Co-authored-by: ardocrat <ardocrat@gri.mw>
* config: global instance * fix: cli tests * fix: owner api tests * config: save tor config * fix: owner rpc tests * fix: set config file path on init and owner api call * tor: store launched services, get global config at command loop, remove unused listen args, check for empty global config to set default, do not save empty tor config at owner * api: update config in memory after successful file write * arti: restart proxy in loop * fix: comment for tor config save * config: set or init, update without error * config: scoped tor config * build: update lock file * config: ability to store multiple configurations at memory from config path * tor: restart service on config change * fix: owner api test * foreign: pass tor config from args, stop service, error handling * arti: move runtime wrapper to single file * config: read existing config from file at create_config * config: handle error on read * arti: single dir for cache * foreign: register config listener before launch * config: save temp * config: remove unused error type * config: make path and members not optional, return an error on config creation if it exists, pass default tor config to send if not set * arti: non public runtime wrapper * config: migrate comments * fix: rpc tests * config: return from cache if exists * config: save disabled tor config when null provided at owner and tor section was not set * config: update tor with args at listener * config: pass pathbuf directly on creation * config: unique tmp file name, setting permissions and providing config version * fix global config handling * fix config persistence * fix tor service restart * fix config synchronization * require wallet close before changing data directory * unify global config updates --------- Co-authored-by: wiesche <wiesche89@googlemail.com>
* client: use native tls and disable default features for reqwest * build: bring back reqwest default features except tls * client: remove tls selection
Sync the TUI branch with current staging (Owner config path, GlobalWalletConfig, prompt_password Result, foreign/owner listener APIs, uuid 1.x) and apply review feedback from mimblewimble#771: Tor skip_arg handling, max-send fee inclusion, busy lifecycle guards, shared keychain mask for listeners, TLS on Owner API, spawn failure cleanup, staged atomic settings writes, private tui module, stable wallet_command API with TUI-only log entry, typed form validation, recovery phrase clipboard disabled, CLI password not retained after unlock, and unit tests for the fixed paths.
|
Thanks @wiesche89 — replied under each of your inline review threads on Summary
|
Route Settings edits through update_global_config so multi-wallet caches stay coherent and config listeners are notified (PR mimblewimble#769). Display and edit prefill prefer get_global_config over local snapshots. Slatepack send/receive/pay paths read live Tor settings from the global instance.
eccb1ee to
2743dff
Compare
Summary
grin-wallet tuisubcommand: a ratatui-based live dashboard (account status, accounts, outputs, transactions, settings, logs) refreshed from the Owner API.initbefore the dashboard starts.grin-wallet.toml; balance min-confirmations is session-configurable.?), OSC 52 clipboard copy for address/slatepacks, and an optionaltuiCargo feature (on by default).staging(Torsend_tor/ slatepack sync API, HTTP client timeout, grin submodule path deps).Implements #751.
Test plan
cargo build/cargo check -p grin_walletagainst mergedstagingcargo test --lib tui::(form validation unit tests)cargo check --no-default-features(TUI feature-gated off)(locked)without leaving alternate screengrin-wallet.tomland survive restartstaging