radrootsd

JSON-RPC bridge for Radroots event publishing
git clone https://radroots.dev/git/radrootsd.git
Log | Files | Refs | README | LICENSE

client.rs (5665B)


      1 #![forbid(unsafe_code)]
      2 
      3 use std::time::Duration;
      4 
      5 use crate::core::nip46::session::Nip46Session;
      6 use crate::transport::jsonrpc::{RpcError, params::DEFAULT_TIMEOUT_SECS};
      7 use nostr::JsonUtil;
      8 use nostr::UnsignedEvent;
      9 use nostr::nips::{
     10     nip44,
     11     nip46::{NostrConnectMessage, NostrConnectMethod, NostrConnectRequest, ResponseResult},
     12 };
     13 use radroots_nostr::prelude::{
     14     RadrootsNostrEventBuilder, RadrootsNostrFilter, RadrootsNostrKind,
     15     RadrootsNostrRelayPoolNotification, RadrootsNostrSubscriptionId, RadrootsNostrTimestamp,
     16     radroots_nostr_filter_tag,
     17 };
     18 use tokio::sync::broadcast;
     19 use tokio::time::sleep;
     20 
     21 pub async fn sign_event(
     22     session: &Nip46Session,
     23     unsigned: UnsignedEvent,
     24     label: &str,
     25 ) -> Result<nostr::Event, RpcError> {
     26     let req = NostrConnectRequest::SignEvent(unsigned);
     27     let response = request(session, req, label).await?;
     28     let response = response
     29         .to_response(NostrConnectMethod::SignEvent)
     30         .map_err(|e| RpcError::Other(format!("nip46 {label} failed: {e}")))?;
     31 
     32     if let Some(error) = response.error {
     33         return Err(RpcError::Other(format!("nip46 {label} error: {error}")));
     34     }
     35 
     36     let event = match response.result {
     37         Some(ResponseResult::SignEvent(event)) => *event,
     38         Some(_) => {
     39             return Err(RpcError::Other(format!(
     40                 "nip46 {label} unexpected response"
     41             )));
     42         }
     43         None => return Err(RpcError::Other(format!("nip46 {label} missing response"))),
     44     };
     45 
     46     event
     47         .verify()
     48         .map_err(|e| RpcError::Other(format!("nip46 {label} invalid event: {e}")))?;
     49 
     50     Ok(event)
     51 }
     52 
     53 pub async fn request(
     54     session: &Nip46Session,
     55     request: NostrConnectRequest,
     56     label: &str,
     57 ) -> Result<NostrConnectMessage, RpcError> {
     58     session.client.connect().await;
     59     session
     60         .client
     61         .wait_for_connection(Duration::from_secs(DEFAULT_TIMEOUT_SECS))
     62         .await;
     63 
     64     let message = NostrConnectMessage::request(&request);
     65     let request_id = message.id().to_string();
     66     let filter = response_filter(session, RadrootsNostrTimestamp::now(), label)?;
     67     let notifications = session.client.notifications();
     68     let subscription = session
     69         .client
     70         .subscribe(filter, None)
     71         .await
     72         .map_err(|e| RpcError::Other(format!("nip46 {label} failed: {e}")))?;
     73     let event = RadrootsNostrEventBuilder::nostr_connect(
     74         &session.client_keys,
     75         session.remote_signer_pubkey.clone(),
     76         message,
     77     )
     78     .map_err(|e| RpcError::Other(format!("nip46 {label} failed: {e}")))?;
     79 
     80     if let Err(error) = session
     81         .client
     82         .send_event_builder(event)
     83         .await
     84         .map_err(|e| RpcError::Other(format!("nip46 {label} failed: {e}")))
     85     {
     86         session.client.unsubscribe(&subscription.val).await;
     87         return Err(error);
     88     }
     89 
     90     wait_for_response(
     91         session,
     92         &request_id,
     93         label,
     94         notifications,
     95         &subscription.val,
     96     )
     97     .await
     98 }
     99 
    100 fn response_filter(
    101     session: &Nip46Session,
    102     since: RadrootsNostrTimestamp,
    103     label: &str,
    104 ) -> Result<RadrootsNostrFilter, RpcError> {
    105     let filter = RadrootsNostrFilter::new()
    106         .kind(RadrootsNostrKind::NostrConnect)
    107         .author(session.remote_signer_pubkey.clone())
    108         .since(since);
    109     radroots_nostr_filter_tag(filter, "p", vec![session.client_pubkey.to_hex()])
    110         .map_err(|e| RpcError::Other(format!("nip46 {label} failed: {e}")))
    111 }
    112 
    113 async fn wait_for_response(
    114     session: &Nip46Session,
    115     request_id: &str,
    116     label: &str,
    117     mut notifications: broadcast::Receiver<RadrootsNostrRelayPoolNotification>,
    118     subscription_id: &RadrootsNostrSubscriptionId,
    119 ) -> Result<NostrConnectMessage, RpcError> {
    120     let timeout = sleep(Duration::from_secs(DEFAULT_TIMEOUT_SECS));
    121     tokio::pin!(timeout);
    122 
    123     loop {
    124         tokio::select! {
    125             _ = &mut timeout => {
    126                 session.client.unsubscribe(subscription_id).await;
    127                 return Err(RpcError::Other(format!("nip46 {label} response not found")));
    128             }
    129             msg = notifications.recv() => {
    130                 let notification = match msg {
    131                     Ok(notification) => notification,
    132                     Err(broadcast::error::RecvError::Lagged(_)) => continue,
    133                     Err(broadcast::error::RecvError::Closed) => {
    134                         session.client.unsubscribe(subscription_id).await;
    135                         return Err(RpcError::Other(format!("nip46 {label} notification closed")));
    136                     }
    137                 };
    138                 let RadrootsNostrRelayPoolNotification::Event { event, .. } = notification else {
    139                     continue;
    140                 };
    141                 let event = (*event).clone();
    142                 if event.kind != RadrootsNostrKind::NostrConnect
    143                     || event.pubkey != session.remote_signer_pubkey
    144                 {
    145                     continue;
    146                 }
    147                 let decrypted = nip44::decrypt(
    148                     session.client_keys.secret_key(),
    149                     &session.remote_signer_pubkey,
    150                     &event.content,
    151                 )
    152                 .map_err(|e| RpcError::Other(format!("nip46 {label} failed: {e}")))?;
    153                 let message = NostrConnectMessage::from_json(&decrypted)
    154                     .map_err(|e| RpcError::Other(format!("nip46 {label} failed: {e}")))?;
    155                 if message.is_response() && message.id() == request_id {
    156                     session.client.unsubscribe(subscription_id).await;
    157                     return Ok(message);
    158                 }
    159             }
    160         }
    161     }
    162 }