radrootsd

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

client.rs (8395B)


      1 #![forbid(unsafe_code)]
      2 
      3 use std::time::Duration;
      4 
      5 use crate::core::nip46::session::Nip46Session;
      6 use crate::host_nostr::{
      7     Filter, Kind, RelayPoolNotification, SubscriptionId, Timestamp, with_filter_tag,
      8 };
      9 use crate::transport::jsonrpc::{RpcError, params::DEFAULT_TIMEOUT_SECS};
     10 use crate::transport::nostr::protocol::sign_nip46_message;
     11 use nostr::JsonUtil;
     12 use nostr::UnsignedEvent;
     13 use nostr::nips::{
     14     nip44,
     15     nip46::{NostrConnectMessage, NostrConnectMethod, NostrConnectRequest, ResponseResult},
     16 };
     17 use tokio::sync::broadcast;
     18 use tokio::time::sleep;
     19 
     20 pub async fn sign_event(
     21     session: &Nip46Session,
     22     mut unsigned: UnsignedEvent,
     23     label: &str,
     24 ) -> Result<nostr::Event, RpcError> {
     25     unsigned.verify_id().map_err(|_| {
     26         RpcError::InvalidParams(format!("nip46 {label} unsigned event ID mismatch"))
     27     })?;
     28     let expected_public_key = unsigned.pubkey;
     29     let expected_event_id = unsigned.id();
     30     let req = NostrConnectRequest::SignEvent(unsigned);
     31     let response = request(session, req, label).await?;
     32     let response = response
     33         .to_response(NostrConnectMethod::SignEvent)
     34         .map_err(|e| RpcError::Other(format!("nip46 {label} failed: {e}")))?;
     35 
     36     if let Some(error) = response.error {
     37         return Err(RpcError::Other(format!("nip46 {label} error: {error}")));
     38     }
     39 
     40     let event = match response.result {
     41         Some(ResponseResult::SignEvent(event)) => *event,
     42         Some(_) => {
     43             return Err(RpcError::Other(format!(
     44                 "nip46 {label} unexpected response"
     45             )));
     46         }
     47         None => return Err(RpcError::Other(format!("nip46 {label} missing response"))),
     48     };
     49 
     50     validate_signed_event_response(expected_public_key, expected_event_id, event, label)
     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, Timestamp::now(), label)?;
     67     let notifications = session.client.clone().into_inner().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 = sign_nip46_message(&session.client_keys, session.remote_signer_pubkey, message)
     74         .map_err(|e| RpcError::Other(format!("nip46 {label} failed: {e}")))?;
     75 
     76     if let Err(error) = session
     77         .client
     78         .send_event(&event)
     79         .await
     80         .map_err(|e| RpcError::Other(format!("nip46 {label} failed: {e}")))
     81     {
     82         session.client.unsubscribe(&subscription.val).await;
     83         return Err(error);
     84     }
     85 
     86     wait_for_response(
     87         session,
     88         &request_id,
     89         label,
     90         notifications,
     91         &subscription.val,
     92     )
     93     .await
     94 }
     95 
     96 fn validate_signed_event_response(
     97     expected_public_key: nostr::PublicKey,
     98     expected_event_id: nostr::EventId,
     99     event: nostr::Event,
    100     label: &str,
    101 ) -> Result<nostr::Event, RpcError> {
    102     if event.pubkey != expected_public_key {
    103         return Err(RpcError::Other(format!(
    104             "nip46 {label} response author mismatch"
    105         )));
    106     }
    107     if event.id != expected_event_id {
    108         return Err(RpcError::Other(format!(
    109             "nip46 {label} response event ID mismatch"
    110         )));
    111     }
    112     event
    113         .verify()
    114         .map_err(|_| RpcError::Other(format!("nip46 {label} response event is invalid")))?;
    115     Ok(event)
    116 }
    117 
    118 fn response_filter(
    119     session: &Nip46Session,
    120     since: Timestamp,
    121     label: &str,
    122 ) -> Result<Filter, RpcError> {
    123     let filter = Filter::new()
    124         .kind(Kind::NostrConnect)
    125         .author(session.remote_signer_pubkey)
    126         .since(since);
    127     with_filter_tag(filter, "p", vec![session.client_pubkey.to_hex()])
    128         .map_err(|e| RpcError::Other(format!("nip46 {label} failed: {e}")))
    129 }
    130 
    131 async fn wait_for_response(
    132     session: &Nip46Session,
    133     request_id: &str,
    134     label: &str,
    135     mut notifications: broadcast::Receiver<RelayPoolNotification>,
    136     subscription_id: &SubscriptionId,
    137 ) -> Result<NostrConnectMessage, RpcError> {
    138     let timeout = sleep(Duration::from_secs(DEFAULT_TIMEOUT_SECS));
    139     tokio::pin!(timeout);
    140 
    141     loop {
    142         tokio::select! {
    143             _ = &mut timeout => {
    144                 session.client.unsubscribe(subscription_id).await;
    145                 return Err(RpcError::Other(format!("nip46 {label} response not found")));
    146             }
    147             msg = notifications.recv() => {
    148                 let notification = match msg {
    149                     Ok(notification) => notification,
    150                     Err(broadcast::error::RecvError::Lagged(_)) => continue,
    151                     Err(broadcast::error::RecvError::Closed) => {
    152                         session.client.unsubscribe(subscription_id).await;
    153                         return Err(RpcError::Other(format!("nip46 {label} notification closed")));
    154                     }
    155                 };
    156                 let RelayPoolNotification::Event { event, .. } = notification else {
    157                     continue;
    158                 };
    159                 let event = (*event).clone();
    160                 if event.kind != Kind::NostrConnect
    161                     || event.pubkey != session.remote_signer_pubkey
    162                 {
    163                     continue;
    164                 }
    165                 let decrypted = nip44::decrypt(
    166                     session.client_keys.secret_key(),
    167                     &session.remote_signer_pubkey,
    168                     &event.content,
    169                 )
    170                 .map_err(|e| RpcError::Other(format!("nip46 {label} failed: {e}")))?;
    171                 let message = NostrConnectMessage::from_json(&decrypted)
    172                     .map_err(|e| RpcError::Other(format!("nip46 {label} failed: {e}")))?;
    173                 if message.is_response() && message.id() == request_id {
    174                     session.client.unsubscribe(subscription_id).await;
    175                     return Ok(message);
    176                 }
    177             }
    178         }
    179     }
    180 }
    181 
    182 #[cfg(test)]
    183 mod tests {
    184     use crate::host_nostr::Keys;
    185     use nostr::{EventBuilder, EventId, Kind, Timestamp};
    186 
    187     use super::validate_signed_event_response;
    188 
    189     fn signed_fixture() -> (nostr::UnsignedEvent, nostr::Event) {
    190         let keys = Keys::generate();
    191         let unsigned = EventBuilder::new(Kind::Custom(30_001), "checked")
    192             .custom_created_at(Timestamp::from_secs(1_784_347_200))
    193             .build(keys.public_key());
    194         let event = unsigned
    195             .clone()
    196             .sign_with_keys(&keys)
    197             .expect("signed fixture");
    198         (unsigned, event)
    199     }
    200 
    201     #[test]
    202     fn sign_event_response_accepts_only_the_exact_valid_event() {
    203         let (mut unsigned, event) = signed_fixture();
    204         let expected_public_key = unsigned.pubkey;
    205         let expected_event_id = unsigned.id();
    206         assert!(
    207             validate_signed_event_response(
    208                 expected_public_key,
    209                 expected_event_id,
    210                 event.clone(),
    211                 "test",
    212             )
    213             .is_ok()
    214         );
    215 
    216         let (_, wrong_author) = signed_fixture();
    217         let error = validate_signed_event_response(
    218             expected_public_key,
    219             expected_event_id,
    220             wrong_author,
    221             "test",
    222         )
    223         .expect_err("wrong author");
    224         assert!(error.to_string().contains("author mismatch"));
    225 
    226         let mut wrong_id = event.clone();
    227         wrong_id.id = EventId::all_zeros();
    228         let error = validate_signed_event_response(
    229             expected_public_key,
    230             expected_event_id,
    231             wrong_id,
    232             "test",
    233         )
    234         .expect_err("wrong event ID");
    235         assert!(error.to_string().contains("event ID mismatch"));
    236 
    237         let mut wrong_signature = event;
    238         wrong_signature.content.push('!');
    239         let error = validate_signed_event_response(
    240             expected_public_key,
    241             expected_event_id,
    242             wrong_signature,
    243             "test",
    244         )
    245         .expect_err("invalid event");
    246         assert!(error.to_string().contains("event is invalid"));
    247     }
    248 }