app

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

input.rs (6028B)


      1 use crate::error::RadrootsAppRemoteSignerError;
      2 use radroots_identity::RadrootsIdentityPublic;
      3 use radroots_nostr_connect::prelude::{
      4     RadrootsNostrConnectMethod, RadrootsNostrConnectPermission, RadrootsNostrConnectPermissions,
      5     RadrootsNostrConnectUri,
      6 };
      7 use radroots_nostr_connect::uri::RADROOTS_NOSTR_CONNECT_BUNKER_URI_SCHEME;
      8 use url::Url;
      9 
     10 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
     11 pub enum RadrootsAppRemoteSignerSource {
     12     BunkerUri,
     13     DiscoveryUrl,
     14 }
     15 
     16 #[derive(Debug, Clone)]
     17 pub struct RadrootsAppRemoteSignerTarget {
     18     pub source: RadrootsAppRemoteSignerSource,
     19     pub signer_identity: RadrootsIdentityPublic,
     20     pub relays: Vec<String>,
     21     pub connect_secret: Option<String>,
     22     pub requested_permissions: RadrootsNostrConnectPermissions,
     23 }
     24 
     25 impl RadrootsAppRemoteSignerTarget {
     26     pub fn source_label(&self) -> &'static str {
     27         match self.source {
     28             RadrootsAppRemoteSignerSource::BunkerUri => "bunker uri",
     29             RadrootsAppRemoteSignerSource::DiscoveryUrl => "discovery url",
     30         }
     31     }
     32 
     33     pub fn requested_permission_labels(&self) -> Vec<String> {
     34         self.requested_permissions
     35             .as_slice()
     36             .iter()
     37             .map(ToString::to_string)
     38             .collect()
     39     }
     40 }
     41 
     42 pub fn radroots_app_remote_signer_requested_permissions() -> RadrootsNostrConnectPermissions {
     43     vec![
     44         RadrootsNostrConnectPermission::with_parameter(
     45             RadrootsNostrConnectMethod::SignEvent,
     46             "kind:1",
     47         ),
     48         RadrootsNostrConnectPermission::new(RadrootsNostrConnectMethod::SwitchRelays),
     49     ]
     50     .into()
     51 }
     52 
     53 pub fn radroots_app_remote_signer_preview(
     54     input: &str,
     55 ) -> Result<RadrootsAppRemoteSignerTarget, RadrootsAppRemoteSignerError> {
     56     let trimmed = input.trim();
     57     if trimmed.is_empty() {
     58         return Err(RadrootsAppRemoteSignerError::EmptyInput);
     59     }
     60 
     61     if trimmed.starts_with(&format!("{RADROOTS_NOSTR_CONNECT_BUNKER_URI_SCHEME}://")) {
     62         return parse_bunker_uri(trimmed, RadrootsAppRemoteSignerSource::BunkerUri);
     63     }
     64 
     65     if trimmed.starts_with("nostrconnect://") {
     66         return Err(RadrootsAppRemoteSignerError::UnsupportedClientUri);
     67     }
     68 
     69     parse_discovery_url(trimmed)
     70 }
     71 
     72 fn parse_discovery_url(
     73     value: &str,
     74 ) -> Result<RadrootsAppRemoteSignerTarget, RadrootsAppRemoteSignerError> {
     75     let url = Url::parse(value)
     76         .map_err(|error| RadrootsAppRemoteSignerError::InvalidDiscoveryUrl(error.to_string()))?;
     77     let Some((_, bunker_uri)) = url.query_pairs().find(|(key, _)| key == "uri") else {
     78         return Err(RadrootsAppRemoteSignerError::MissingDiscoveryUri);
     79     };
     80     parse_bunker_uri(
     81         bunker_uri.as_ref(),
     82         RadrootsAppRemoteSignerSource::DiscoveryUrl,
     83     )
     84 }
     85 
     86 fn parse_bunker_uri(
     87     value: &str,
     88     source: RadrootsAppRemoteSignerSource,
     89 ) -> Result<RadrootsAppRemoteSignerTarget, RadrootsAppRemoteSignerError> {
     90     let uri = RadrootsNostrConnectUri::parse(value)?;
     91     let RadrootsNostrConnectUri::Bunker(bunker_uri) = uri else {
     92         return Err(RadrootsAppRemoteSignerError::UnsupportedClientUri);
     93     };
     94     Ok(RadrootsAppRemoteSignerTarget {
     95         source,
     96         signer_identity: RadrootsIdentityPublic::new(bunker_uri.remote_signer_public_key),
     97         relays: bunker_uri
     98             .relays
     99             .into_iter()
    100             .map(|relay| relay.to_string())
    101             .collect(),
    102         connect_secret: bunker_uri.secret,
    103         requested_permissions: radroots_app_remote_signer_requested_permissions(),
    104     })
    105 }
    106 
    107 #[cfg(test)]
    108 mod tests {
    109     use super::*;
    110     use radroots_identity::RadrootsIdentity;
    111 
    112     const RELAY_PRIMARY_WSS: &str = "wss://relay.example.com";
    113     const SIGNER_SECRET_KEY_HEX: &str =
    114         "1111111111111111111111111111111111111111111111111111111111111111";
    115 
    116     fn signer_identity() -> RadrootsIdentity {
    117         RadrootsIdentity::from_secret_key_str(SIGNER_SECRET_KEY_HEX).expect("identity")
    118     }
    119 
    120     fn bunker_uri() -> String {
    121         format!(
    122             "bunker://{}?relay={}",
    123             signer_identity().public_key_hex(),
    124             urlencoding(RELAY_PRIMARY_WSS)
    125         )
    126     }
    127 
    128     fn discovery_url() -> String {
    129         format!(
    130             "http://localhost/connect?uri={}",
    131             urlencoding(bunker_uri().as_str())
    132         )
    133     }
    134 
    135     fn urlencoding(value: &str) -> String {
    136         url::form_urlencoded::byte_serialize(value.as_bytes()).collect()
    137     }
    138 
    139     #[test]
    140     fn parses_direct_bunker_uri() {
    141         let preview = radroots_app_remote_signer_preview(bunker_uri().as_str()).expect("preview");
    142 
    143         assert_eq!(preview.source, RadrootsAppRemoteSignerSource::BunkerUri);
    144         assert_eq!(
    145             preview.signer_identity.public_key_hex,
    146             signer_identity().public_key_hex()
    147         );
    148         assert_eq!(preview.relays, vec![RELAY_PRIMARY_WSS.to_owned()]);
    149         assert_eq!(preview.connect_secret, None);
    150         assert_eq!(
    151             preview.requested_permission_labels(),
    152             vec!["sign_event:kind:1".to_owned(), "switch_relays".to_owned()]
    153         );
    154     }
    155 
    156     #[test]
    157     fn parses_discovery_url_with_bunker_uri() {
    158         let preview =
    159             radroots_app_remote_signer_preview(discovery_url().as_str()).expect("preview");
    160 
    161         assert_eq!(preview.source, RadrootsAppRemoteSignerSource::DiscoveryUrl);
    162         assert_eq!(
    163             preview.signer_identity.public_key_hex,
    164             signer_identity().public_key_hex()
    165         );
    166         assert_eq!(preview.relays, vec![RELAY_PRIMARY_WSS.to_owned()]);
    167         assert_eq!(
    168             preview.requested_permission_labels(),
    169             vec!["sign_event:kind:1".to_owned(), "switch_relays".to_owned()]
    170         );
    171     }
    172 
    173     #[test]
    174     fn rejects_client_side_nostrconnect_uri_input() {
    175         let err = radroots_app_remote_signer_preview(
    176             "nostrconnect://npub1test?relay=wss%3A%2F%2Frelay.example.com&secret=test",
    177         )
    178         .expect_err("client uri rejected");
    179 
    180         assert_eq!(err, RadrootsAppRemoteSignerError::UnsupportedClientUri);
    181     }
    182 }