app

Local-first trade for farms and co-ops
git clone https://radroots.dev/git/app.git
Log | Files | Refs | README | LICENSE

commit 27f0e7a4e7b5a3fdb0db746572ff51ac6bb352c9
parent 867de416133a3fc1ca39ef6752549bcaaeaffe9a
Author: triesap <tyson@radroots.org>
Date:   Sat, 28 Mar 2026 20:00:55 +0000

app: keep remote signer polling single-owner

Diffstat:
Mcrates/android/src/remote_signer.rs | 20++++++++++++++------
Mcrates/core/src/lib.rs | 11++++++++++-
Mcrates/desktop/src/remote_signer.rs | 20++++++++++++++------
Mcrates/ios/src/remote_signer.rs | 20++++++++++++++------
Mcrates/remote-signer/src/controller.rs | 82++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------
Mcrates/remote-signer/src/lib.rs | 5++++-
6 files changed, 127 insertions(+), 31 deletions(-)

diff --git a/crates/android/src/remote_signer.rs b/crates/android/src/remote_signer.rs @@ -7,8 +7,8 @@ use radroots_app_core::{ use radroots_app_remote_signer::{ RADROOTS_APP_REMOTE_SIGNER_SECRET_NAMESPACE, RadrootsAppRemoteSignerController, RadrootsAppRemoteSignerControllerHooks, RadrootsAppRemoteSignerPendingSession, - RadrootsAppRemoteSignerSessionRecord, RadrootsAppRemoteSignerSessionStoreState, - radroots_app_remote_signer_clear_pending_session, + RadrootsAppRemoteSignerPendingState, RadrootsAppRemoteSignerSessionRecord, + RadrootsAppRemoteSignerSessionStoreState, radroots_app_remote_signer_clear_pending_session, radroots_app_remote_signer_disconnect_selected, radroots_app_remote_signer_preview, radroots_app_remote_signer_purge_all_custody_state, radroots_app_remote_signer_reconcile_startup, @@ -118,10 +118,18 @@ impl AndroidRemoteSigner { } if pending_connection()?.is_some() { - return Ok(SetupActionState { - label: "Remote Signer Waiting for Approval".to_owned(), - enabled: false, - pending: false, + return Ok(match self.controller.pending_state() { + RadrootsAppRemoteSignerPendingState::TransportFailure { .. } => SetupActionState { + label: "Remote Signer Approval Check Retrying".to_owned(), + enabled: false, + pending: false, + }, + RadrootsAppRemoteSignerPendingState::Idle + | RadrootsAppRemoteSignerPendingState::WaitingApproval => SetupActionState { + label: "Remote Signer Waiting for Approval".to_owned(), + enabled: false, + pending: false, + }, }); } diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs @@ -920,7 +920,16 @@ impl RadrootsApp { }); } RemoteSignerEntryState::WaitingApproval(pending) => { - ui.label("Remote signer connection is waiting for signer approval."); + ui.label(action.label.as_str()); + if action.label == "Remote Signer Approval Check Retrying" { + ui.add_space(8.0); + ui.label( + "The app is retrying approval checks after a relay or network failure.", + ); + } else { + ui.add_space(8.0); + ui.label("Remote signer connection is waiting for signer approval."); + } ui.add_space(8.0); ui.monospace(format!("signer: {}", pending.signer_npub)); if pending.relays.is_empty() { diff --git a/crates/desktop/src/remote_signer.rs b/crates/desktop/src/remote_signer.rs @@ -7,8 +7,8 @@ use radroots_app_core::{ use radroots_app_remote_signer::{ RADROOTS_APP_REMOTE_SIGNER_SECRET_NAMESPACE, RadrootsAppRemoteSignerController, RadrootsAppRemoteSignerControllerHooks, RadrootsAppRemoteSignerPendingSession, - RadrootsAppRemoteSignerSessionRecord, RadrootsAppRemoteSignerSessionStoreState, - radroots_app_remote_signer_clear_pending_session, + RadrootsAppRemoteSignerPendingState, RadrootsAppRemoteSignerSessionRecord, + RadrootsAppRemoteSignerSessionStoreState, radroots_app_remote_signer_clear_pending_session, radroots_app_remote_signer_disconnect_selected, radroots_app_remote_signer_preview, radroots_app_remote_signer_purge_all_custody_state, radroots_app_remote_signer_reconcile_startup, @@ -117,10 +117,18 @@ impl DesktopRemoteSigner { } if pending_connection()?.is_some() { - return Ok(SetupActionState { - label: "Remote Signer Waiting for Approval".to_owned(), - enabled: false, - pending: false, + return Ok(match self.controller.pending_state() { + RadrootsAppRemoteSignerPendingState::TransportFailure { .. } => SetupActionState { + label: "Remote Signer Approval Check Retrying".to_owned(), + enabled: false, + pending: false, + }, + RadrootsAppRemoteSignerPendingState::Idle + | RadrootsAppRemoteSignerPendingState::WaitingApproval => SetupActionState { + label: "Remote Signer Waiting for Approval".to_owned(), + enabled: false, + pending: false, + }, }); } diff --git a/crates/ios/src/remote_signer.rs b/crates/ios/src/remote_signer.rs @@ -7,8 +7,8 @@ use radroots_app_core::{ use radroots_app_remote_signer::{ RADROOTS_APP_REMOTE_SIGNER_SECRET_NAMESPACE, RadrootsAppRemoteSignerController, RadrootsAppRemoteSignerControllerHooks, RadrootsAppRemoteSignerPendingSession, - RadrootsAppRemoteSignerSessionRecord, RadrootsAppRemoteSignerSessionStoreState, - radroots_app_remote_signer_clear_pending_session, + RadrootsAppRemoteSignerPendingState, RadrootsAppRemoteSignerSessionRecord, + RadrootsAppRemoteSignerSessionStoreState, radroots_app_remote_signer_clear_pending_session, radroots_app_remote_signer_disconnect_selected, radroots_app_remote_signer_preview, radroots_app_remote_signer_purge_all_custody_state, radroots_app_remote_signer_reconcile_startup, @@ -118,10 +118,18 @@ impl IosRemoteSigner { } if pending_connection()?.is_some() { - return Ok(SetupActionState { - label: "Remote Signer Waiting for Approval".to_owned(), - enabled: false, - pending: false, + return Ok(match self.controller.pending_state() { + RadrootsAppRemoteSignerPendingState::TransportFailure { .. } => SetupActionState { + label: "Remote Signer Approval Check Retrying".to_owned(), + enabled: false, + pending: false, + }, + RadrootsAppRemoteSignerPendingState::Idle + | RadrootsAppRemoteSignerPendingState::WaitingApproval => SetupActionState { + label: "Remote Signer Waiting for Approval".to_owned(), + enabled: false, + pending: false, + }, }); } diff --git a/crates/remote-signer/src/controller.rs b/crates/remote-signer/src/controller.rs @@ -8,6 +8,13 @@ use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex}; use std::time::Duration; +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum RadrootsAppRemoteSignerPendingState { + Idle, + WaitingApproval, + TransportFailure { message: String }, +} + pub trait RadrootsAppRemoteSignerControllerHooks: Clone + Send + Sync + 'static { type ReadyState: Send + 'static; @@ -45,6 +52,7 @@ where changed: Arc<AtomicBool>, connecting: Arc<AtomicBool>, polling: Arc<AtomicBool>, + pending_state: Arc<Mutex<RadrootsAppRemoteSignerPendingState>>, _ready_state: PhantomData<H::ReadyState>, } @@ -59,6 +67,7 @@ where changed: Arc::clone(&self.changed), connecting: Arc::clone(&self.connecting), polling: Arc::clone(&self.polling), + pending_state: Arc::clone(&self.pending_state), _ready_state: PhantomData, } } @@ -75,6 +84,7 @@ where changed: Arc::new(AtomicBool::new(false)), connecting: Arc::new(AtomicBool::new(false)), polling: Arc::new(AtomicBool::new(false)), + pending_state: Arc::new(Mutex::new(RadrootsAppRemoteSignerPendingState::Idle)), _ready_state: PhantomData, }; if let Err(error) = controller.reconcile_startup_state() { @@ -97,6 +107,13 @@ where self.connecting.load(Ordering::Acquire) } + pub fn pending_state(&self) -> RadrootsAppRemoteSignerPendingState { + self.pending_state + .lock() + .map(|state| state.clone()) + .unwrap_or(RadrootsAppRemoteSignerPendingState::Idle) + } + pub fn begin_connect(&self, input: &str) -> Result<(), String> { if self.connecting.swap(true, Ordering::AcqRel) { return Err("remote signer connection is already starting".to_owned()); @@ -110,6 +127,7 @@ where if let Ok(mut slot) = self.update.lock() { *slot = None; } + self.set_pending_state(RadrootsAppRemoteSignerPendingState::Idle); let tracker = self.clone(); let input = input.to_owned(); @@ -117,10 +135,8 @@ where let outcome = (|| -> Result<(), String> { let pending = radroots_app_remote_signer_connect_pending(input.as_str()) .map_err(|error| error.to_string())?; - let client_account_id = pending.record.client_account_id().to_owned(); - let client_secret_key_hex = pending.client_secret_key_hex.clone(); tracker.hooks.store_pending_session(&pending)?; - tracker.start_polling(client_account_id, client_secret_key_hex); + tracker.start_polling(); Ok(()) })(); @@ -147,14 +163,13 @@ where let Some(record) = self.pending_session_record()? else { return Ok(()); }; - let client_secret_key_hex = self - .hooks + self.hooks .load_pending_client_secret(record.client_account_id())?; - self.start_polling(record.client_account_id().to_owned(), client_secret_key_hex); + self.start_polling(); Ok(()) } - fn start_polling(&self, client_account_id: String, client_secret_key_hex: String) { + fn start_polling(&self) { if self.polling.swap(true, Ordering::AcqRel) { return; } @@ -163,12 +178,26 @@ where std::thread::spawn(move || { loop { let pending_record = match tracker.hooks.pending_session_record() { - Ok(Some(record)) if record.client_account_id() == client_account_id => record, - Ok(Some(_)) | Ok(None) => { + Ok(Some(record)) => record, + Ok(None) => { + tracker.set_pending_state(RadrootsAppRemoteSignerPendingState::Idle); tracker.polling.store(false, Ordering::Release); return; } Err(error) => { + tracker.set_pending_state(RadrootsAppRemoteSignerPendingState::Idle); + tracker.push_update(Err(error)); + tracker.polling.store(false, Ordering::Release); + return; + } + }; + let client_secret_key_hex = match tracker + .hooks + .load_pending_client_secret(pending_record.client_account_id()) + { + Ok(secret) => secret, + Err(error) => { + tracker.set_pending_state(RadrootsAppRemoteSignerPendingState::Idle); tracker.push_update(Err(error)); tracker.polling.store(false, Ordering::Release); return; @@ -181,17 +210,35 @@ where ) .map_err(|error| error.to_string()) { - Ok(RadrootsAppRemoteSignerPendingPollOutcome::PendingApproval) - | Ok(RadrootsAppRemoteSignerPendingPollOutcome::TransportFailure { .. }) => { + Ok(RadrootsAppRemoteSignerPendingPollOutcome::PendingApproval) => { + tracker.set_pending_state( + RadrootsAppRemoteSignerPendingState::WaitingApproval, + ); + std::thread::sleep(Duration::from_secs(1)); + } + Ok(RadrootsAppRemoteSignerPendingPollOutcome::TransportFailure { message }) => { + let changed = tracker.set_pending_state( + RadrootsAppRemoteSignerPendingState::TransportFailure { + message: message.clone(), + }, + ); + if changed { + tracker.push_update(Err(format!( + "remote signer approval check failed: {message}" + ))); + } std::thread::sleep(Duration::from_secs(1)); } Ok(RadrootsAppRemoteSignerPendingPollOutcome::Approved(user_identity)) => { + tracker.set_pending_state(RadrootsAppRemoteSignerPendingState::Idle); let ready_state = match tracker.hooks.activate_pending_session( pending_record.client_account_id(), user_identity, ) { Ok(state) => state, Err(error) => { + tracker + .set_pending_state(RadrootsAppRemoteSignerPendingState::Idle); tracker.push_update(Err(error)); tracker.polling.store(false, Ordering::Release); return; @@ -203,6 +250,7 @@ where } Ok(RadrootsAppRemoteSignerPendingPollOutcome::Rejected { message }) | Ok(RadrootsAppRemoteSignerPendingPollOutcome::FatalError { message }) => { + tracker.set_pending_state(RadrootsAppRemoteSignerPendingState::Idle); let outcome = tracker .hooks .clear_pending_session() @@ -219,6 +267,7 @@ where return; } Err(error) => { + tracker.set_pending_state(RadrootsAppRemoteSignerPendingState::Idle); tracker.push_update(Err(error)); tracker.polling.store(false, Ordering::Release); return; @@ -234,4 +283,15 @@ where self.changed.store(true, Ordering::Release); } } + + fn set_pending_state(&self, next: RadrootsAppRemoteSignerPendingState) -> bool { + if let Ok(mut state) = self.pending_state.lock() { + if *state == next { + return false; + } + *state = next; + return true; + } + false + } } diff --git a/crates/remote-signer/src/lib.rs b/crates/remote-signer/src/lib.rs @@ -9,7 +9,10 @@ mod session; pub const RADROOTS_APP_REMOTE_SIGNER_SECRET_NAMESPACE: &str = "remote-signer"; -pub use controller::{RadrootsAppRemoteSignerController, RadrootsAppRemoteSignerControllerHooks}; +pub use controller::{ + RadrootsAppRemoteSignerController, RadrootsAppRemoteSignerControllerHooks, + RadrootsAppRemoteSignerPendingState, +}; pub use custody::{ radroots_app_remote_signer_clear_pending_session, radroots_app_remote_signer_disconnect_selected,