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:
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,