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 }