lib

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

util.rs (7102B)


      1 #[cfg(not(feature = "std"))]
      2 use alloc::{
      3     string::{String, ToString},
      4     vec,
      5     vec::Vec,
      6 };
      7 
      8 use radroots_events::{
      9     job::{JobFeedbackStatus, JobInputType},
     10     job_request::{RadrootsJobInput, RadrootsJobParam},
     11 };
     12 
     13 use crate::job::error::JobParseError;
     14 
     15 fn looks_like_hex_id(s: &str) -> bool {
     16     let n = s.len();
     17     (n == 32 || n == 64) && s.as_bytes().iter().all(|c| c.is_ascii_hexdigit())
     18 }
     19 
     20 fn starts_with_ignore_ascii_case(s: &str, prefix: &str) -> bool {
     21     s.get(..prefix.len())
     22         .map(|head| head.eq_ignore_ascii_case(prefix))
     23         .unwrap_or(false)
     24 }
     25 
     26 fn looks_like_url_or_nostr(s: &str) -> bool {
     27     starts_with_ignore_ascii_case(s, "http://")
     28         || starts_with_ignore_ascii_case(s, "https://")
     29         || starts_with_ignore_ascii_case(s, "nostr:")
     30         || starts_with_ignore_ascii_case(s, "note")
     31         || starts_with_ignore_ascii_case(s, "nevent")
     32         || starts_with_ignore_ascii_case(s, "naddr")
     33         || looks_like_hex_id(s)
     34 }
     35 
     36 fn looks_like_ws_relay(s: &str) -> bool {
     37     starts_with_ignore_ascii_case(s, "ws://") || starts_with_ignore_ascii_case(s, "wss://")
     38 }
     39 
     40 pub fn parse_bool_encrypted(tags: &[Vec<String>]) -> bool {
     41     tags.iter()
     42         .any(|t| t.first().map(|s| s.as_str()) == Some("encrypted"))
     43 }
     44 
     45 #[inline]
     46 pub fn job_input_type_tag(t: JobInputType) -> &'static str {
     47     match t {
     48         JobInputType::Url => "url",
     49         JobInputType::Event => "event",
     50         JobInputType::Job => "job",
     51         JobInputType::Text => "text",
     52     }
     53 }
     54 
     55 #[inline]
     56 pub fn job_input_type_from_tag(s: &str) -> Option<JobInputType> {
     57     match s {
     58         "url" => Some(JobInputType::Url),
     59         "event" => Some(JobInputType::Event),
     60         "job" => Some(JobInputType::Job),
     61         "text" => Some(JobInputType::Text),
     62         _ => None,
     63     }
     64 }
     65 
     66 #[inline]
     67 pub fn feedback_status_tag(s: JobFeedbackStatus) -> &'static str {
     68     match s {
     69         JobFeedbackStatus::PaymentRequired => "payment-required",
     70         JobFeedbackStatus::Processing => "processing",
     71         JobFeedbackStatus::Error => "error",
     72         JobFeedbackStatus::Success => "success",
     73         JobFeedbackStatus::Partial => "partial",
     74     }
     75 }
     76 
     77 #[inline]
     78 pub fn feedback_status_from_tag(s: &str) -> Option<JobFeedbackStatus> {
     79     match s {
     80         "payment-required" => Some(JobFeedbackStatus::PaymentRequired),
     81         "processing" => Some(JobFeedbackStatus::Processing),
     82         "error" => Some(JobFeedbackStatus::Error),
     83         "success" => Some(JobFeedbackStatus::Success),
     84         "partial" => Some(JobFeedbackStatus::Partial),
     85         _ => None,
     86     }
     87 }
     88 
     89 pub fn parse_i_tags(tags: &[Vec<String>]) -> Vec<RadrootsJobInput> {
     90     let mut out = Vec::new();
     91     for t in tags
     92         .iter()
     93         .filter(|t| t.first().map(|s| s.as_str()) == Some("i"))
     94     {
     95         if t.len() < 2 {
     96             continue;
     97         }
     98 
     99         let mut data = String::new();
    100         let mut input_type = JobInputType::Text;
    101         let mut relay: Option<String> = None;
    102         let mut marker: Option<String> = None;
    103 
    104         match t.len() {
    105             2 => {
    106                 let v = &t[1];
    107                 if looks_like_url_or_nostr(v) {
    108                     data = v.clone();
    109                     let is_url = starts_with_ignore_ascii_case(v, "http://")
    110                         || starts_with_ignore_ascii_case(v, "https://");
    111                     input_type = if is_url {
    112                         JobInputType::Url
    113                     } else {
    114                         JobInputType::Event
    115                     };
    116                 } else {
    117                     marker = Some(v.clone());
    118                 }
    119             }
    120             3 => {
    121                 data = t[1].clone();
    122                 let v = t[2].as_str();
    123                 if let Some(it) = job_input_type_from_tag(v) {
    124                     input_type = it;
    125                 } else {
    126                     marker = Some(t[2].clone());
    127                 }
    128             }
    129             4 => {
    130                 data = t[1].clone();
    131                 input_type = job_input_type_from_tag(t[2].as_str()).unwrap_or(JobInputType::Text);
    132                 let v = &t[3];
    133                 if looks_like_ws_relay(v) {
    134                     relay = Some(v.clone());
    135                 } else {
    136                     marker = Some(v.clone());
    137                 }
    138             }
    139             _ => {
    140                 data = t[1].clone();
    141                 input_type = job_input_type_from_tag(t[2].as_str()).unwrap_or(JobInputType::Text);
    142                 let relay_or_marker = &t[3];
    143                 if looks_like_ws_relay(relay_or_marker) {
    144                     relay = Some(relay_or_marker.clone());
    145                     marker = Some(t[4].clone());
    146                 } else {
    147                     marker = Some(relay_or_marker.clone());
    148                 }
    149             }
    150         }
    151 
    152         out.push(RadrootsJobInput {
    153             data,
    154             input_type,
    155             relay,
    156             marker,
    157         });
    158     }
    159     out
    160 }
    161 
    162 pub fn parse_params(tags: &[Vec<String>]) -> Vec<RadrootsJobParam> {
    163     let mut params = Vec::new();
    164     for t in tags
    165         .iter()
    166         .filter(|t| t.first().map(|s| s.as_str()) == Some("param"))
    167     {
    168         if t.len() >= 3 {
    169             params.push(RadrootsJobParam {
    170                 key: t[1].clone(),
    171                 value: t[2].clone(),
    172             });
    173         }
    174     }
    175     params
    176 }
    177 
    178 pub fn parse_amount_tag_sat(
    179     tags: &[Vec<String>],
    180 ) -> Result<Option<(u32, Option<String>)>, JobParseError> {
    181     let amt = match tags
    182         .iter()
    183         .find(|t| t.first().map(|s| s.as_str()) == Some("amount"))
    184     {
    185         Some(a) => a,
    186         None => return Ok(None),
    187     };
    188     let msat_s = amt.get(1).ok_or(JobParseError::InvalidTag("amount"))?;
    189     let msat_u64: u64 = msat_s
    190         .parse()
    191         .map_err(|e| JobParseError::InvalidNumber("amount", e))?;
    192     if !msat_u64.is_multiple_of(1000) {
    193         return Err(JobParseError::NonWholeSats("amount"));
    194     }
    195     let sat_u64 = msat_u64 / 1000;
    196     if sat_u64 > (u32::MAX as u64) {
    197         return Err(JobParseError::AmountOverflow("amount"));
    198     }
    199     let bolt11 = amt.get(2).cloned();
    200     Ok(Some((sat_u64 as u32, bolt11)))
    201 }
    202 
    203 pub fn push_amount_tag_msat(tags: &mut Vec<Vec<String>>, sat: u32, bolt11: Option<String>) {
    204     let msat = (sat as u64) * 1000;
    205     let mut v = vec!["amount".into(), msat.to_string()];
    206     if let Some(b) = bolt11 {
    207         v.push(b);
    208     }
    209     tags.push(v);
    210 }
    211 
    212 pub fn parse_bid_tag_sat(tags: &[Vec<String>]) -> Result<Option<u32>, JobParseError> {
    213     let bid = match tags
    214         .iter()
    215         .find(|t| t.first().map(|s| s.as_str()) == Some("bid"))
    216     {
    217         Some(b) => b,
    218         None => return Ok(None),
    219     };
    220     let sat_s = bid.get(1).ok_or(JobParseError::InvalidTag("bid"))?;
    221     let sat_u64: u64 = sat_s
    222         .parse()
    223         .map_err(|e| JobParseError::InvalidNumber("bid", e))?;
    224     if sat_u64 > (u32::MAX as u64) {
    225         return Err(JobParseError::AmountOverflow("bid"));
    226     }
    227     Ok(Some(sat_u64 as u32))
    228 }
    229 
    230 pub fn push_bid_tag_sat(tags: &mut Vec<Vec<String>>, bid_sat: u32) {
    231     tags.push(vec!["bid".into(), bid_sat.to_string()]);
    232 }