lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README | LICENSE

client.rs (5871B)


      1 use crate::error::RadrootsNostrConnectError;
      2 use crate::message::{
      3     RADROOTS_NOSTR_CONNECT_RPC_KIND, RadrootsNostrConnectRequest,
      4     RadrootsNostrConnectRequestMessage, RadrootsNostrConnectResponse,
      5     RadrootsNostrConnectResponseEnvelope,
      6 };
      7 use crate::method::RadrootsNostrConnectMethod;
      8 use nostr::nips::nip44::{self, Version};
      9 use nostr::{Event, EventBuilder, Keys, Kind, PublicKey, RelayUrl, Tag};
     10 use std::future::Future;
     11 use std::pin::Pin;
     12 
     13 pub type RadrootsNostrConnectClientTransportFuture<'a, T> =
     14     Pin<Box<dyn Future<Output = Result<T, RadrootsNostrConnectError>> + Send + 'a>>;
     15 
     16 #[derive(Debug, Clone, PartialEq, Eq)]
     17 pub struct RadrootsNostrConnectClientTarget {
     18     pub remote_signer_public_key: PublicKey,
     19     pub relays: Vec<RelayUrl>,
     20 }
     21 
     22 impl RadrootsNostrConnectClientTarget {
     23     pub fn new(remote_signer_public_key: PublicKey, relays: Vec<RelayUrl>) -> Self {
     24         Self {
     25             remote_signer_public_key,
     26             relays,
     27         }
     28     }
     29 }
     30 
     31 #[derive(Debug, Clone, PartialEq, Eq)]
     32 pub struct RadrootsNostrConnectClientRequest {
     33     pub request_id: String,
     34     pub request: RadrootsNostrConnectRequest,
     35 }
     36 
     37 impl RadrootsNostrConnectClientRequest {
     38     pub fn new(request_id: impl Into<String>, request: RadrootsNostrConnectRequest) -> Self {
     39         Self {
     40             request_id: request_id.into(),
     41             request,
     42         }
     43     }
     44 
     45     pub fn method(&self) -> RadrootsNostrConnectMethod {
     46         self.request.method()
     47     }
     48 
     49     pub fn into_message(self) -> RadrootsNostrConnectRequestMessage {
     50         RadrootsNostrConnectRequestMessage::new(self.request_id, self.request)
     51     }
     52 }
     53 
     54 #[derive(Debug, Clone, PartialEq, Eq)]
     55 pub enum RadrootsNostrConnectClientProgress {
     56     AuthChallenge { url: String },
     57 }
     58 
     59 #[derive(Debug, Clone, PartialEq, Eq)]
     60 pub enum RadrootsNostrConnectClientEventOutcome {
     61     Ignore,
     62     Progress(RadrootsNostrConnectClientProgress),
     63     Response(RadrootsNostrConnectResponse),
     64 }
     65 
     66 pub trait RadrootsNostrConnectClientTransport {
     67     fn publish_request_event<'a>(
     68         &'a mut self,
     69         event: Event,
     70     ) -> RadrootsNostrConnectClientTransportFuture<'a, ()>;
     71 
     72     fn next_response_event<'a>(
     73         &'a mut self,
     74     ) -> RadrootsNostrConnectClientTransportFuture<'a, Event>;
     75 }
     76 
     77 pub fn build_request_event(
     78     client_keys: &Keys,
     79     target: &RadrootsNostrConnectClientTarget,
     80     message: RadrootsNostrConnectRequestMessage,
     81 ) -> Result<Event, RadrootsNostrConnectError> {
     82     let payload = serde_json::to_string(&message).map_err(RadrootsNostrConnectError::from)?;
     83     let ciphertext = nip44::encrypt(
     84         client_keys.secret_key(),
     85         &target.remote_signer_public_key,
     86         payload,
     87         Version::V2,
     88     )
     89     .map_err(|error| RadrootsNostrConnectError::Encrypt {
     90         reason: error.to_string(),
     91     })?;
     92 
     93     EventBuilder::new(Kind::Custom(RADROOTS_NOSTR_CONNECT_RPC_KIND), ciphertext)
     94         .tag(Tag::public_key(target.remote_signer_public_key))
     95         .sign_with_keys(client_keys)
     96         .map_err(|error| RadrootsNostrConnectError::Sign {
     97             reason: error.to_string(),
     98         })
     99 }
    100 
    101 pub fn parse_response_event(
    102     client_keys: &Keys,
    103     target: &RadrootsNostrConnectClientTarget,
    104     request_id: &str,
    105     method: &RadrootsNostrConnectMethod,
    106     event: &Event,
    107 ) -> Result<RadrootsNostrConnectClientEventOutcome, RadrootsNostrConnectError> {
    108     if event.kind != Kind::Custom(RADROOTS_NOSTR_CONNECT_RPC_KIND) {
    109         return Ok(RadrootsNostrConnectClientEventOutcome::Ignore);
    110     }
    111 
    112     if event.pubkey != target.remote_signer_public_key {
    113         return Ok(RadrootsNostrConnectClientEventOutcome::Ignore);
    114     }
    115 
    116     let client_public_key = client_keys.public_key();
    117     if !event
    118         .tags
    119         .public_keys()
    120         .any(|public_key| *public_key == client_public_key)
    121     {
    122         return Ok(RadrootsNostrConnectClientEventOutcome::Ignore);
    123     }
    124 
    125     let decrypted = nip44::decrypt(
    126         client_keys.secret_key(),
    127         &target.remote_signer_public_key,
    128         &event.content,
    129     )
    130     .map_err(|error| RadrootsNostrConnectError::Decrypt {
    131         reason: error.to_string(),
    132     })?;
    133 
    134     let envelope: RadrootsNostrConnectResponseEnvelope =
    135         serde_json::from_str(&decrypted).map_err(RadrootsNostrConnectError::from)?;
    136     if envelope.id != request_id {
    137         return Ok(RadrootsNostrConnectClientEventOutcome::Ignore);
    138     }
    139 
    140     let response = RadrootsNostrConnectResponse::from_envelope(method, envelope)?;
    141     Ok(match response {
    142         RadrootsNostrConnectResponse::AuthUrl(url) => {
    143             RadrootsNostrConnectClientEventOutcome::Progress(
    144                 RadrootsNostrConnectClientProgress::AuthChallenge { url },
    145             )
    146         }
    147         response => RadrootsNostrConnectClientEventOutcome::Response(response),
    148     })
    149 }
    150 
    151 pub async fn execute_request_with_transport<T, F>(
    152     client_keys: &Keys,
    153     target: &RadrootsNostrConnectClientTarget,
    154     request: RadrootsNostrConnectClientRequest,
    155     transport: &mut T,
    156     mut on_progress: F,
    157 ) -> Result<RadrootsNostrConnectResponse, RadrootsNostrConnectError>
    158 where
    159     T: RadrootsNostrConnectClientTransport,
    160     F: FnMut(RadrootsNostrConnectClientProgress) -> Result<(), RadrootsNostrConnectError>,
    161 {
    162     let method = request.method();
    163     let request_id = request.request_id.clone();
    164     let event = build_request_event(client_keys, target, request.into_message())?;
    165     transport.publish_request_event(event).await?;
    166 
    167     loop {
    168         let event = transport.next_response_event().await?;
    169         match parse_response_event(client_keys, target, &request_id, &method, &event)? {
    170             RadrootsNostrConnectClientEventOutcome::Ignore => {}
    171             RadrootsNostrConnectClientEventOutcome::Progress(progress) => on_progress(progress)?,
    172             RadrootsNostrConnectClientEventOutcome::Response(response) => return Ok(response),
    173         }
    174     }
    175 }