fix(ipc): lazily initialize the NIP-46 client signer handle

App startup defaults signer_mode to Nip46Client but only SignerModeSet
builds the handle, so a fresh backend answered every nip46_* request
('Show QR' included) with 'not initialized' until the user re-saved the
mode. All nip46_* handlers now ensure the handle exists first.
This commit is contained in:
Avi 2026-09-12 05:09:14 -05:00
commit dc58f3889f

View file

@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize};
use serde_json::json; use serde_json::json;
use tokio::sync::Mutex; use tokio::sync::Mutex;
use crate::app::App; use crate::app::{App, Nip46ClientSignerHandle};
use crate::errors::AppError; use crate::errors::AppError;
use crate::feed; use crate::feed;
use crate::profiles; use crate::profiles;
@ -337,6 +337,22 @@ fn error_code(err: &AppError) -> String {
.unwrap_or_else(|_| "error".to_string()) .unwrap_or_else(|_| "error".to_string())
} }
/// Lazily ensure the NIP-46 client signer handle exists and return it.
///
/// App startup defaults to `signer_mode = Nip46Client` but leaves the handle
/// `None` (only SignerModeSet builds one), so a fresh backend answers
/// nip46_* requests with "not initialized" until the user re-saves the mode.
/// Any nip46_* request initializes the handle when the mode is the NIP-46
/// client mode, matching what SignerModeSet would do. The guard is dropped
/// before returning so callers can await on the handle without deadlocking.
async fn ensure_nip46_signer(app: &Arc<Mutex<App>>) -> Option<Nip46ClientSignerHandle> {
let mut guard = app.lock().await;
if guard.nip46_signer.is_none() && matches!(guard.signer_mode, SignerMode::Nip46Client) {
guard.nip46_signer = Some(Arc::new(Nip46ClientSigner::new(app.clone())));
}
guard.nip46_signer.clone()
}
/// Main request dispatcher. /// Main request dispatcher.
async fn run(app: &Arc<Mutex<App>>, request: Request) -> Result<serde_json::Value, AppError> { async fn run(app: &Arc<Mutex<App>>, request: Request) -> Result<serde_json::Value, AppError> {
match request { match request {
@ -402,15 +418,11 @@ async fn run(app: &Arc<Mutex<App>>, request: Request) -> Result<serde_json::Valu
// NIP-46 client signer // NIP-46 client signer
Request::Nip46Connect { uri, label } => { Request::Nip46Connect { uri, label } => {
// Take the signer handle under the lock, then drop the guard // Take the signer handle (initializing it if needed), then drop
// before awaiting: connect() re-locks the App internally (to // the guard before awaiting: connect() re-locks the App
// persist the connection and resolve its secret), so holding the // internally (to persist the connection and resolve its secret),
// guard across the await would deadlock. // so holding the guard across the await would deadlock.
let signer = { let Some(signer) = ensure_nip46_signer(app).await else {
let guard = app.lock().await;
guard.nip46_signer.clone()
};
let Some(signer) = signer else {
return Err(AppError::config( return Err(AppError::config(
"NIP-46 signer not initialized. Set signer mode to nip46 first.", "NIP-46 signer not initialized. Set signer mode to nip46 first.",
)); ));
@ -422,11 +434,7 @@ async fn run(app: &Arc<Mutex<App>>, request: Request) -> Result<serde_json::Valu
// Same lock discipline as Nip46Connect: pairing persists to the // Same lock discipline as Nip46Connect: pairing persists to the
// vault from its background task, so the guard must not be held // vault from its background task, so the guard must not be held
// across the await. // across the await.
let signer = { let Some(signer) = ensure_nip46_signer(app).await else {
let guard = app.lock().await;
guard.nip46_signer.clone()
};
let Some(signer) = signer else {
return Err(AppError::config( return Err(AppError::config(
"NIP-46 signer not initialized. Set signer mode to nip46 first.", "NIP-46 signer not initialized. Set signer mode to nip46 first.",
)); ));
@ -435,33 +443,27 @@ async fn run(app: &Arc<Mutex<App>>, request: Request) -> Result<serde_json::Valu
Ok(json!(status)) Ok(json!(status))
} }
Request::Nip46Disconnect => { Request::Nip46Disconnect => {
let guard = app.lock().await; let Some(signer) = ensure_nip46_signer(app).await else {
if let Some(signer) = &guard.nip46_signer { return Err(AppError::config("NIP-46 signer not initialized"));
signer.disconnect().await?; };
let status = signer.status().await; signer.disconnect().await?;
Ok(json!(status)) let status = signer.status().await;
} else { Ok(json!(status))
Err(AppError::config("NIP-46 signer not initialized"))
}
} }
Request::Nip46Status => { Request::Nip46Status => {
let guard = app.lock().await; let Some(signer) = ensure_nip46_signer(app).await else {
if let Some(signer) = &guard.nip46_signer { return Ok(json!({ "connected": false, "error": "Not initialized" }));
let status = signer.status().await; };
Ok(json!(status)) let status = signer.status().await;
} else { Ok(json!(status))
Ok(json!({ "connected": false, "error": "Not initialized" }))
}
} }
Request::Nip46Approve { id, approved } => { Request::Nip46Approve { id, approved } => {
let guard = app.lock().await; let Some(signer) = ensure_nip46_signer(app).await else {
if let Some(signer) = &guard.nip46_signer { return Err(AppError::config("NIP-46 signer not initialized"));
signer.respond_to_approval(&id, approved).await?; };
let status = signer.status().await; signer.respond_to_approval(&id, approved).await?;
Ok(json!(status)) let status = signer.status().await;
} else { Ok(json!(status))
Err(AppError::config("NIP-46 signer not initialized"))
}
} }
// Legacy NIP-46 bunker (server mode) // Legacy NIP-46 bunker (server mode)