app

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

commit c0ecbbd484ea636ea068a05e86a0b4fe2961295c
parent 73c6e71560a3f3ece67546960a1ffb82d80466ee
Author: triesap <tyson@radroots.org>
Date:   Wed, 24 Jun 2026 01:13:39 +0000

app: share NIP-46 signer execution

- route app remote signer requests through shared NIP-46 transport primitives
- make startup signer connect and polling await the async signer API
- keep app SDK local-key writes on explicit-signer enqueue methods
- refresh signer dependency features, lock state, and source guard ownership

Diffstat:
MCargo.lock | 2++
Mcrates/desktop/src/source_guards.rs | 6+++---
Mcrates/desktop/src/window.rs | 2++
Mcrates/runtime/src/sdk.rs | 35++++++++++++++++++++++++++++-------
Mcrates/signer/Cargo.toml | 4++--
Mcrates/signer/src/protocol.rs | 393++++++++++++++++++++++++++++++++++++++++++-------------------------------------
6 files changed, 245 insertions(+), 197 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -5475,6 +5475,8 @@ dependencies = [ "radroots_events_codec", "radroots_identity", "radroots_nostr", + "radroots_nostr_connect", + "radroots_nostr_signer", "radroots_outbox", "radroots_relay_transport", "radroots_trade", diff --git a/crates/desktop/src/source_guards.rs b/crates/desktop/src/source_guards.rs @@ -1258,9 +1258,9 @@ const LEGACY_SDK_BOUNDARY_ALLOWLIST: &[LegacySdkBoundaryAllowlistEntry] = &[ LegacySdkBoundaryAllowlistEntry { path: "crates/signer/src/protocol.rs", pattern: "RadrootsIdentity::from_secret_key_str", - owner: "rpv1-app-sdk-hardening.04", - reason: "remote signer protocol connection still materializes client identity from local pending-session custody", - removal_condition: "remove when remote signer protocol sessions are mediated by SDK signer adapters and protected store APIs", + owner: "rpv1-sdksign.5", + reason: "remote signer startup custody still reloads the NIP-46 client identity before shared protocol transport execution", + removal_condition: "remove when startup remote signer custody stores client identities through protected signer-session APIs", }, LegacySdkBoundaryAllowlistEntry { path: "crates/store/src/lib.rs", diff --git a/crates/desktop/src/window.rs b/crates/desktop/src/window.rs @@ -12562,6 +12562,7 @@ async fn run_startup_signer_connect( source_input: String, ) -> Result<RadrootsAppRemoteSignerPendingSession, String> { radroots_app_remote_signer_connect_pending(source_input.as_str()) + .await .map_err(|error| error.to_string()) } @@ -12595,6 +12596,7 @@ async fn run_startup_signer_pending_poll( } => auth_challenge_url = Some(url), }, ) + .await .map_err(|error| error.to_string()); StartupSignerPollCycleResult { diff --git a/crates/runtime/src/sdk.rs b/crates/runtime/src/sdk.rs @@ -1384,7 +1384,10 @@ fn enqueue_farm_publish_with_sdk( .map_err(|error| AppSdkRuntimeIssue::from_sdk_error(&error))?; } let receipt = runtime - .block_on(sdk.farms().enqueue_publish(enqueue, &signer)) + .block_on( + sdk.farms() + .enqueue_publish_with_explicit_signer(enqueue, &signer), + ) .map_err(|error| AppSdkRuntimeIssue::from_sdk_error(&error))?; Ok(app_sdk_farm_receipt(receipt, request.actor_pubkey)) } @@ -1408,7 +1411,10 @@ fn enqueue_listing_publish_with_sdk( .map_err(|error| AppSdkRuntimeIssue::from_sdk_error(&error))?; } let receipt = runtime - .block_on(sdk.listings().enqueue_publish(enqueue, &signer)) + .block_on( + sdk.listings() + .enqueue_publish_with_explicit_signer(enqueue, &signer), + ) .map_err(|error| AppSdkRuntimeIssue::from_sdk_error(&error))?; Ok(app_sdk_listing_receipt(receipt, request.actor_pubkey)) } @@ -1433,7 +1439,10 @@ fn enqueue_order_submit_with_sdk( .map_err(|error| AppSdkRuntimeIssue::from_sdk_error(&error))?; } let receipt = runtime - .block_on(sdk.orders().enqueue_submit(enqueue, &signer)) + .block_on( + sdk.orders() + .enqueue_submit_with_explicit_signer(enqueue, &signer), + ) .map_err(|error| AppSdkRuntimeIssue::from_sdk_error(&error))?; Ok(app_sdk_order_submit_ack(receipt, request.actor_pubkey)) } @@ -1470,7 +1479,10 @@ fn enqueue_order_decision_with_sdk( .map_err(|error| AppSdkRuntimeIssue::from_sdk_error(&error))?; } let receipt = runtime - .block_on(sdk.orders().enqueue_decision(enqueue, &signer)) + .block_on( + sdk.orders() + .enqueue_decision_with_explicit_signer(enqueue, &signer), + ) .map_err(|error| AppSdkRuntimeIssue::from_sdk_error(&error))?; Ok(app_sdk_order_decision_receipt( receipt, @@ -1504,7 +1516,10 @@ fn enqueue_order_revision_proposal_with_sdk( .map_err(|error| AppSdkRuntimeIssue::from_sdk_error(&error))?; } let receipt = runtime - .block_on(sdk.orders().enqueue_revision_proposal(enqueue, &signer)) + .block_on( + sdk.orders() + .enqueue_revision_proposal_with_explicit_signer(enqueue, &signer), + ) .map_err(|error| AppSdkRuntimeIssue::from_sdk_error(&error))?; Ok(app_sdk_order_revision_proposal_receipt( receipt, @@ -1538,7 +1553,10 @@ fn enqueue_order_revision_decision_with_sdk( .map_err(|error| AppSdkRuntimeIssue::from_sdk_error(&error))?; } let receipt = runtime - .block_on(sdk.orders().enqueue_revision_decision(enqueue, &signer)) + .block_on( + sdk.orders() + .enqueue_revision_decision_with_explicit_signer(enqueue, &signer), + ) .map_err(|error| AppSdkRuntimeIssue::from_sdk_error(&error))?; Ok(app_sdk_order_revision_decision_receipt( receipt, @@ -1572,7 +1590,10 @@ fn enqueue_order_cancellation_with_sdk( .map_err(|error| AppSdkRuntimeIssue::from_sdk_error(&error))?; } let receipt = runtime - .block_on(sdk.orders().enqueue_cancellation(enqueue, &signer)) + .block_on( + sdk.orders() + .enqueue_cancellation_with_explicit_signer(enqueue, &signer), + ) .map_err(|error| AppSdkRuntimeIssue::from_sdk_error(&error))?; Ok(app_sdk_order_cancellation_receipt( receipt, diff --git a/crates/signer/Cargo.toml b/crates/signer/Cargo.toml @@ -13,13 +13,13 @@ publish = false workspace = true [dependencies] -nostr = { version = "0.44.2", features = ["nip44"] } +nostr = "0.44.2" radroots_identity.workspace = true radroots_nostr.workspace = true radroots_nostr_connect.workspace = true serde.workspace = true serde_json.workspace = true -tokio = { version = "1.48", features = ["rt", "sync", "time"] } +tokio = { version = "1.48", features = ["sync", "time"] } url = "2.5" [dev-dependencies] diff --git a/crates/signer/src/protocol.rs b/crates/signer/src/protocol.rs @@ -2,25 +2,22 @@ use crate::error::RadrootsAppRemoteSignerError; use crate::input::{RadrootsAppRemoteSignerTarget, radroots_app_remote_signer_preview}; use crate::session::RadrootsAppRemoteSignerSessionRecord; use nostr::JsonUtil; -use nostr::nips::nip44; -use nostr::nips::nip44::Version; -use nostr::{EventBuilder, UnsignedEvent}; +use nostr::{EventBuilder, RelayUrl, UnsignedEvent}; use radroots_identity::{RadrootsIdentity, RadrootsIdentityPublic}; use radroots_nostr::prelude::{ - RadrootsNostrClient, RadrootsNostrEvent, RadrootsNostrEventBuilder, RadrootsNostrFilter, - RadrootsNostrKind, RadrootsNostrRelayPoolNotification, RadrootsNostrTag, - RadrootsNostrTimestamp, radroots_nostr_filter_tag, radroots_nostr_kind, + RadrootsNostrClient, RadrootsNostrEvent, RadrootsNostrFilter, RadrootsNostrKind, + RadrootsNostrRelayPoolNotification, RadrootsNostrTimestamp, radroots_nostr_filter_tag, }; -use radroots_nostr_connect::message::RADROOTS_NOSTR_CONNECT_RPC_KIND; use radroots_nostr_connect::prelude::{ - RadrootsNostrConnectMethod, RadrootsNostrConnectPendingConnectionPollOutcome, - RadrootsNostrConnectPermissions, RadrootsNostrConnectRequest, - RadrootsNostrConnectRequestMessage, RadrootsNostrConnectResponse, - RadrootsNostrConnectResponseEnvelope, + RADROOTS_NOSTR_CONNECT_RPC_KIND, RadrootsNostrConnectClientProgress, + RadrootsNostrConnectClientRequest, RadrootsNostrConnectClientTarget, + RadrootsNostrConnectClientTransport, RadrootsNostrConnectClientTransportFuture, + RadrootsNostrConnectError, RadrootsNostrConnectMethod, + RadrootsNostrConnectPendingConnectionPollOutcome, RadrootsNostrConnectPermissions, + RadrootsNostrConnectRequest, RadrootsNostrConnectResponse, execute_request_with_transport, }; use std::sync::atomic::{AtomicU64, Ordering}; use std::time::Duration; -use tokio::runtime::Builder; use tokio::sync::broadcast; use tokio::time::timeout; @@ -69,21 +66,25 @@ pub(crate) struct RadrootsAppRemoteSignerPendingPoller { } struct ConnectedRemoteSignerSessionClient { - runtime: tokio::runtime::Runtime, client_identity: RadrootsIdentity, target: RadrootsAppRemoteSignerTarget, + client_target: RadrootsNostrConnectClientTarget, + transport: ConnectedRemoteSignerTransport, +} + +struct ConnectedRemoteSignerTransport { client: RadrootsNostrClient, notifications: broadcast::Receiver<RadrootsNostrRelayPoolNotification>, } -pub fn radroots_app_remote_signer_connect_pending( +pub async fn radroots_app_remote_signer_connect_pending( input: &str, ) -> Result<RadrootsAppRemoteSignerPendingSession, RadrootsAppRemoteSignerError> { let target = radroots_app_remote_signer_preview(input)?; - connect_pending_session(target) + connect_pending_session(target).await } -pub fn radroots_app_remote_signer_poll_pending_session( +pub async fn radroots_app_remote_signer_poll_pending_session( record: &RadrootsAppRemoteSignerSessionRecord, client_secret_key_hex: &str, ) -> Result<RadrootsAppRemoteSignerPendingPollOutcome, RadrootsAppRemoteSignerError> { @@ -92,9 +93,10 @@ pub fn radroots_app_remote_signer_poll_pending_session( client_secret_key_hex, |_| {}, ) + .await } -pub fn radroots_app_remote_signer_poll_pending_session_with_progress<F>( +pub async fn radroots_app_remote_signer_poll_pending_session_with_progress<F>( record: &RadrootsAppRemoteSignerSessionRecord, client_secret_key_hex: &str, mut progress: F, @@ -102,32 +104,33 @@ pub fn radroots_app_remote_signer_poll_pending_session_with_progress<F>( where F: FnMut(RadrootsAppRemoteSignerProgressUpdate), { - let mut poller = radroots_app_remote_signer_open_pending_poller(record, client_secret_key_hex)?; - radroots_app_remote_signer_poll_pending_poller_with_progress(&mut poller, &mut progress) + let mut poller = + radroots_app_remote_signer_open_pending_poller(record, client_secret_key_hex).await?; + radroots_app_remote_signer_poll_pending_poller_with_progress(&mut poller, &mut progress).await } -pub(crate) fn radroots_app_remote_signer_open_pending_poller( +pub(crate) async fn radroots_app_remote_signer_open_pending_poller( record: &RadrootsAppRemoteSignerSessionRecord, client_secret_key_hex: &str, ) -> Result<RadrootsAppRemoteSignerPendingPoller, RadrootsAppRemoteSignerError> { let client_identity = load_client_identity(client_secret_key_hex)?; let target = target_for_record(record); Ok(RadrootsAppRemoteSignerPendingPoller { - client: ConnectedRemoteSignerSessionClient::connect(client_identity, target)?, + client: ConnectedRemoteSignerSessionClient::connect(client_identity, target).await?, }) } -pub(crate) fn radroots_app_remote_signer_poll_pending_poller_with_progress<F>( +pub(crate) async fn radroots_app_remote_signer_poll_pending_poller_with_progress<F>( poller: &mut RadrootsAppRemoteSignerPendingPoller, progress: &mut F, ) -> Result<RadrootsAppRemoteSignerPendingPollOutcome, RadrootsAppRemoteSignerError> where F: FnMut(RadrootsAppRemoteSignerProgressUpdate), { - poller.poll_with_progress(progress) + poller.poll_with_progress(progress).await } -pub fn radroots_app_remote_signer_sign_kind1_note( +pub async fn radroots_app_remote_signer_sign_kind1_note( record: &RadrootsAppRemoteSignerSessionRecord, client_secret_key_hex: &str, content: &str, @@ -138,9 +141,10 @@ pub fn radroots_app_remote_signer_sign_kind1_note( content, |_| {}, ) + .await } -pub fn radroots_app_remote_signer_sign_kind1_note_with_progress<F>( +pub async fn radroots_app_remote_signer_sign_kind1_note_with_progress<F>( record: &RadrootsAppRemoteSignerSessionRecord, client_secret_key_hex: &str, content: &str, @@ -149,10 +153,10 @@ pub fn radroots_app_remote_signer_sign_kind1_note_with_progress<F>( where F: FnMut(RadrootsAppRemoteSignerProgressUpdate), { - sign_kind1_note(record, client_secret_key_hex, content, &mut progress) + sign_kind1_note(record, client_secret_key_hex, content, &mut progress).await } -pub fn radroots_app_remote_signer_sign_unsigned_event( +pub async fn radroots_app_remote_signer_sign_unsigned_event( record: &RadrootsAppRemoteSignerSessionRecord, client_secret_key_hex: &str, unsigned_event: UnsignedEvent, @@ -163,9 +167,10 @@ pub fn radroots_app_remote_signer_sign_unsigned_event( unsigned_event, |_| {}, ) + .await } -pub fn radroots_app_remote_signer_sign_unsigned_event_with_progress<F>( +pub async fn radroots_app_remote_signer_sign_unsigned_event_with_progress<F>( record: &RadrootsAppRemoteSignerSessionRecord, client_secret_key_hex: &str, unsigned_event: UnsignedEvent, @@ -174,10 +179,10 @@ pub fn radroots_app_remote_signer_sign_unsigned_event_with_progress<F>( where F: FnMut(RadrootsAppRemoteSignerProgressUpdate), { - sign_unsigned_event(record, client_secret_key_hex, unsigned_event, &mut progress) + sign_unsigned_event(record, client_secret_key_hex, unsigned_event, &mut progress).await } -fn connect_pending_session( +async fn connect_pending_session( target: RadrootsAppRemoteSignerTarget, ) -> Result<RadrootsAppRemoteSignerPendingSession, RadrootsAppRemoteSignerError> { let client_identity = RadrootsIdentity::generate(); @@ -188,7 +193,8 @@ fn connect_pending_session( RadrootsNostrConnectMethod::Connect, connect_request, CONNECT_TIMEOUT, - )?; + ) + .await?; match response { RadrootsNostrConnectResponse::ConnectAcknowledged @@ -222,7 +228,7 @@ fn connect_request_for_target( } } -fn sign_kind1_note<F>( +async fn sign_kind1_note<F>( record: &RadrootsAppRemoteSignerSessionRecord, client_secret_key_hex: &str, content: &str, @@ -243,10 +249,10 @@ where })?; let unsigned_event = EventBuilder::text_note(content.trim()) .build(parse_public_key_hex(user_identity.public_key_hex.as_str())?); - sign_unsigned_event(record, client_secret_key_hex, unsigned_event, progress) + sign_unsigned_event(record, client_secret_key_hex, unsigned_event, progress).await } -fn sign_unsigned_event<F>( +async fn sign_unsigned_event<F>( record: &RadrootsAppRemoteSignerSessionRecord, client_secret_key_hex: &str, unsigned_event: UnsignedEvent, @@ -257,14 +263,16 @@ where { let client_identity = load_client_identity(client_secret_key_hex)?; let target = target_for_record(record); - let mut client = ConnectedRemoteSignerSessionClient::connect(client_identity, target)?; - let relays = client.sync_relays_if_allowed(record, progress)?; - let response = client.execute_request_with_progress( - RadrootsNostrConnectMethod::SignEvent, - RadrootsNostrConnectRequest::SignEvent(unsigned_event), - SIGN_EVENT_TIMEOUT, - progress, - )?; + let mut client = ConnectedRemoteSignerSessionClient::connect(client_identity, target).await?; + let relays = client.sync_relays_if_allowed(record, progress).await?; + let response = client + .execute_request_with_progress( + RadrootsNostrConnectMethod::SignEvent, + RadrootsNostrConnectRequest::SignEvent(unsigned_event), + SIGN_EVENT_TIMEOUT, + progress, + ) + .await?; match response { RadrootsNostrConnectResponse::SignedEvent(event) => { @@ -284,7 +292,7 @@ where } } -fn execute_request( +async fn execute_request( client_identity: &RadrootsIdentity, target: &RadrootsAppRemoteSignerTarget, method: RadrootsNostrConnectMethod, @@ -292,24 +300,31 @@ fn execute_request( request_timeout: Duration, ) -> Result<RadrootsNostrConnectResponse, RadrootsAppRemoteSignerError> { let mut client = - ConnectedRemoteSignerSessionClient::connect(client_identity.clone(), target.clone())?; - client.execute_request_with_progress(method, request, request_timeout, &mut |_| {}) + ConnectedRemoteSignerSessionClient::connect(client_identity.clone(), target.clone()) + .await?; + client + .execute_request_with_progress(method, request, request_timeout, &mut |_| {}) + .await } impl RadrootsAppRemoteSignerPendingPoller { - fn poll_with_progress<F>( + async fn poll_with_progress<F>( &mut self, progress: &mut F, ) -> Result<RadrootsAppRemoteSignerPendingPollOutcome, RadrootsAppRemoteSignerError> where F: FnMut(RadrootsAppRemoteSignerProgressUpdate), { - match self.client.execute_request_with_progress( - RadrootsNostrConnectMethod::GetSessionCapability, - RadrootsNostrConnectRequest::GetSessionCapability, - GET_SESSION_CAPABILITY_TIMEOUT, - progress, - ) { + match self + .client + .execute_request_with_progress( + RadrootsNostrConnectMethod::GetSessionCapability, + RadrootsNostrConnectRequest::GetSessionCapability, + GET_SESSION_CAPABILITY_TIMEOUT, + progress, + ) + .await + { Ok(response) => Ok(classify_pending_poll_response(response)), Err(error) => Ok(classify_pending_poll_error(error)), } @@ -317,48 +332,45 @@ impl RadrootsAppRemoteSignerPendingPoller { } impl ConnectedRemoteSignerSessionClient { - fn connect( + async fn connect( client_identity: RadrootsIdentity, target: RadrootsAppRemoteSignerTarget, ) -> Result<Self, RadrootsAppRemoteSignerError> { - let runtime = Builder::new_current_thread() - .enable_all() - .build() - .map_err(|error| RadrootsAppRemoteSignerError::ConnectFailed(error.to_string()))?; + let client_target = client_target_for_app_target(&target)?; let client = RadrootsNostrClient::from_identity(&client_identity); - let notifications = runtime.block_on(async { - for relay in &target.relays { - client.add_relay(relay).await.map_err(|error| { - RadrootsAppRemoteSignerError::ConnectFailed(error.to_string()) - })?; - } - client.connect().await; - let filter = radroots_nostr_filter_tag( - RadrootsNostrFilter::new() - .kind(RadrootsNostrKind::Custom(RADROOTS_NOSTR_CONNECT_RPC_KIND)) - .since(RadrootsNostrTimestamp::now()), - "p", - vec![client_identity.public_key_hex()], - ) - .map_err(|error| RadrootsAppRemoteSignerError::ConnectFailed(error.to_string()))?; - let notifications = client.notifications(); + for relay in &target.relays { client - .subscribe(filter, None) + .add_relay(relay) .await .map_err(|error| RadrootsAppRemoteSignerError::ConnectFailed(error.to_string()))?; - Ok::<_, RadrootsAppRemoteSignerError>(notifications) - })?; + } + client.connect().await; + let filter = radroots_nostr_filter_tag( + RadrootsNostrFilter::new() + .kind(RadrootsNostrKind::Custom(RADROOTS_NOSTR_CONNECT_RPC_KIND)) + .since(RadrootsNostrTimestamp::now()), + "p", + vec![client_identity.public_key_hex()], + ) + .map_err(|error| RadrootsAppRemoteSignerError::ConnectFailed(error.to_string()))?; + let notifications = client.notifications(); + client + .subscribe(filter, None) + .await + .map_err(|error| RadrootsAppRemoteSignerError::ConnectFailed(error.to_string()))?; Ok(Self { - runtime, client_identity, target, - client, - notifications, + client_target, + transport: ConnectedRemoteSignerTransport { + client, + notifications, + }, }) } - fn sync_relays_if_allowed<F>( + async fn sync_relays_if_allowed<F>( &mut self, record: &RadrootsAppRemoteSignerSessionRecord, progress: &mut F, @@ -370,15 +382,21 @@ impl ConnectedRemoteSignerSessionClient { return Ok(self.target.relays.clone()); } - match self.execute_request_with_progress( - RadrootsNostrConnectMethod::SwitchRelays, - RadrootsNostrConnectRequest::SwitchRelays, - SWITCH_RELAYS_TIMEOUT, - progress, - )? { + match self + .execute_request_with_progress( + RadrootsNostrConnectMethod::SwitchRelays, + RadrootsNostrConnectRequest::SwitchRelays, + SWITCH_RELAYS_TIMEOUT, + progress, + ) + .await? + { RadrootsNostrConnectResponse::RelayList(relays) => { - let relays: Vec<String> = - relays.into_iter().map(|relay| relay.to_string()).collect(); + let relays: Vec<String> = relays.iter().map(ToString::to_string).collect(); + self.client_target.relays = relays + .iter() + .map(|relay| parse_relay_url(relay)) + .collect::<Result<Vec<_>, _>>()?; self.target.relays = relays.clone(); Ok(relays) } @@ -395,7 +413,7 @@ impl ConnectedRemoteSignerSessionClient { } } - fn execute_request_with_progress<F>( + async fn execute_request_with_progress<F>( &mut self, method: RadrootsNostrConnectMethod, request: RadrootsNostrConnectRequest, @@ -407,121 +425,126 @@ impl ConnectedRemoteSignerSessionClient { { let request_id = next_request_id(method.to_string().as_str()); let response_method = method.clone(); - self.runtime.block_on(async { - let event_builder = build_request_event( - &self.client_identity, - &self.target.signer_identity, - request_id.as_str(), + let client_keys = self.client_identity.keys().clone(); + let client_target = self.client_target.clone(); + let request = RadrootsNostrConnectClientRequest::new(request_id, request); + let response = timeout( + request_timeout, + execute_request_with_transport( + &client_keys, + &client_target, request, - )?; + &mut self.transport, + |event| { + match event { + RadrootsNostrConnectClientProgress::AuthChallenge { url } => { + progress(RadrootsAppRemoteSignerProgressUpdate::AuthChallenge { url }); + } + } + Ok(()) + }, + ), + ) + .await + .map_err(|_| RadrootsAppRemoteSignerError::RequestTimedOut { + method: response_method.clone(), + })?; + response.map_err(|error| app_error_from_nostr_connect_error(&response_method, error)) + } +} + +impl RadrootsNostrConnectClientTransport for ConnectedRemoteSignerTransport { + fn publish_request_event<'a>( + &'a mut self, + event: RadrootsNostrEvent, + ) -> RadrootsNostrConnectClientTransportFuture<'a, ()> { + Box::pin(async move { self.client - .send_event_builder(event_builder) + .send_event(&event) .await - .map_err(|error| RadrootsAppRemoteSignerError::ConnectFailed(error.to_string()))?; + .map(|_| ()) + .map_err(|error| RadrootsNostrConnectError::Transport { + reason: error.to_string(), + }) + }) + } - timeout(request_timeout, async { - loop { - let notification = match self.notifications.recv().await { - Ok(notification) => notification, - Err(broadcast::error::RecvError::Lagged(_)) => continue, - Err(broadcast::error::RecvError::Closed) => { - return Err(RadrootsAppRemoteSignerError::ConnectFailed( - "remote signer notification stream closed".to_owned(), - )); - } - }; - let RadrootsNostrRelayPoolNotification::Event { event, .. } = notification - else { - continue; - }; - let event = *event; - if event.kind != RadrootsNostrKind::Custom(RADROOTS_NOSTR_CONNECT_RPC_KIND) { - continue; - } - if event.pubkey.to_hex() != self.target.signer_identity.public_key_hex { - continue; - } - match parse_response_event( - &self.client_identity, - &event, - &response_method, - request_id.as_str(), - )? { - Some(RadrootsNostrConnectResponse::AuthUrl(url)) => { - progress(RadrootsAppRemoteSignerProgressUpdate::AuthChallenge { url }); - } - Some(response) => return Ok(response), - None => continue, + fn next_response_event<'a>( + &'a mut self, + ) -> RadrootsNostrConnectClientTransportFuture<'a, RadrootsNostrEvent> { + Box::pin(async move { + loop { + let notification = match self.notifications.recv().await { + Ok(notification) => notification, + Err(broadcast::error::RecvError::Lagged(_)) => continue, + Err(broadcast::error::RecvError::Closed) => { + return Err(RadrootsNostrConnectError::Transport { + reason: "remote signer notification stream closed".to_owned(), + }); } + }; + let RadrootsNostrRelayPoolNotification::Event { event, .. } = notification else { + continue; + }; + let event = *event; + if event.kind != RadrootsNostrKind::Custom(RADROOTS_NOSTR_CONNECT_RPC_KIND) { + continue; } - }) - .await - .map_err(|_| RadrootsAppRemoteSignerError::RequestTimedOut { - method: response_method.clone(), - })? + return Ok(event); + } }) } } -fn build_request_event( - client_identity: &RadrootsIdentity, - signer_identity: &RadrootsIdentityPublic, - request_id: &str, - request: RadrootsNostrConnectRequest, -) -> Result<RadrootsNostrEventBuilder, RadrootsAppRemoteSignerError> { - let payload = serde_json::to_string(&RadrootsNostrConnectRequestMessage::new( - request_id.to_owned(), - request, +fn client_target_for_app_target( + target: &RadrootsAppRemoteSignerTarget, +) -> Result<RadrootsNostrConnectClientTarget, RadrootsAppRemoteSignerError> { + Ok(RadrootsNostrConnectClientTarget::new( + parse_public_key_hex(target.signer_identity.public_key_hex.as_str())?, + target + .relays + .iter() + .map(|relay| parse_relay_url(relay)) + .collect::<Result<Vec<_>, _>>()?, )) - .map_err(|error| RadrootsAppRemoteSignerError::ConnectFailed(error.to_string()))?; - let signer_public_key = parse_public_key_hex(signer_identity.public_key_hex.as_str())?; - let ciphertext = nip44::encrypt( - client_identity.keys().secret_key(), - &signer_public_key, - payload, - Version::V2, - ) - .map_err(|error| RadrootsAppRemoteSignerError::ConnectFailed(error.to_string()))?; - Ok(RadrootsNostrEventBuilder::new( - radroots_nostr_kind(RADROOTS_NOSTR_CONNECT_RPC_KIND), - ciphertext, - ) - .tags(vec![RadrootsNostrTag::public_key(signer_public_key)])) } -fn parse_response_event( - client_identity: &RadrootsIdentity, - event: &RadrootsNostrEvent, +fn parse_relay_url(value: &str) -> Result<RelayUrl, RadrootsAppRemoteSignerError> { + RelayUrl::parse(value).map_err(|error| { + RadrootsAppRemoteSignerError::ConnectFailed(format!( + "invalid remote signer relay `{value}`: {error}" + )) + }) +} + +fn app_error_from_nostr_connect_error( method: &RadrootsNostrConnectMethod, - request_id: &str, -) -> Result<Option<RadrootsNostrConnectResponse>, RadrootsAppRemoteSignerError> { - let decrypted = nip44::decrypt( - client_identity.keys().secret_key(), - &event.pubkey, - &event.content, - ) - .map_err(|error| RadrootsAppRemoteSignerError::UnexpectedResponse { - method: method.clone(), - response: format!("failed to decrypt signer response: {error}"), - })?; - let envelope: RadrootsNostrConnectResponseEnvelope = - serde_json::from_str(&decrypted).map_err(|error| { - RadrootsAppRemoteSignerError::UnexpectedResponse { + error: RadrootsNostrConnectError, +) -> RadrootsAppRemoteSignerError { + match error { + RadrootsNostrConnectError::RequestTimedOut => { + RadrootsAppRemoteSignerError::RequestTimedOut { method: method.clone(), - response: format!("failed to decode signer response envelope: {error}"), } - })?; - if envelope.id != request_id { - return Ok(None); - } - let response = - RadrootsNostrConnectResponse::from_envelope(method, envelope).map_err(|error| { + } + RadrootsNostrConnectError::Transport { reason } + | RadrootsNostrConnectError::Encrypt { reason } + | RadrootsNostrConnectError::Sign { reason } => { + RadrootsAppRemoteSignerError::ConnectFailed(reason) + } + RadrootsNostrConnectError::Decrypt { reason } + | RadrootsNostrConnectError::Json(reason) + | RadrootsNostrConnectError::InvalidResponsePayload { reason, .. } => { RadrootsAppRemoteSignerError::UnexpectedResponse { method: method.clone(), - response: format!("failed to decode signer response payload: {error}"), + response: reason, } - })?; - Ok(Some(response)) + } + other => RadrootsAppRemoteSignerError::UnexpectedResponse { + method: method.clone(), + response: other.to_string(), + }, + } } fn classify_pending_poll_response(