1use {
2 crate::keypair::{parse_signer_source, SignerSourceKind, ASK_KEYWORD},
3 chrono::DateTime,
4 solana_clock::{Epoch, Slot},
5 solana_hash::Hash,
6 solana_keypair::read_keypair_file,
7 solana_pubkey::{Pubkey, MAX_SEED_LEN},
8 solana_signature::Signature,
9 std::{fmt::Display, ops::RangeBounds, str::FromStr},
10};
11
12fn is_parsable_generic<U, T>(string: T) -> Result<(), String>
13where
14 T: AsRef<str> + Display,
15 U: FromStr,
16 U::Err: Display,
17{
18 string
19 .as_ref()
20 .parse::<U>()
21 .map(|_| ())
22 .map_err(|err| format!("error parsing '{string}': {err}"))
23}
24
25pub fn is_parsable<T>(string: String) -> Result<(), String>
28where
29 T: FromStr,
30 T::Err: Display,
31{
32 is_parsable_generic::<T, String>(string)
33}
34
35pub fn is_within_range<T, R>(string: String, range: R) -> Result<(), String>
38where
39 T: FromStr + Copy + std::fmt::Debug + PartialOrd + std::ops::Add<Output = T> + From<usize>,
40 T::Err: Display,
41 R: RangeBounds<T> + std::fmt::Debug,
42{
43 match string.parse::<T>() {
44 Ok(input) => {
45 if !range.contains(&input) {
46 Err(format!("input '{input:?}' out of range {range:?}"))
47 } else {
48 Ok(())
49 }
50 }
51 Err(err) => Err(format!("error parsing '{string}': {err}")),
52 }
53}
54
55pub fn is_pubkey<T>(string: T) -> Result<(), String>
57where
58 T: AsRef<str> + Display,
59{
60 is_parsable_generic::<Pubkey, _>(string)
61}
62
63pub fn is_hash<T>(string: T) -> Result<(), String>
65where
66 T: AsRef<str> + Display,
67{
68 is_parsable_generic::<Hash, _>(string)
69}
70
71pub fn is_keypair<T>(string: T) -> Result<(), String>
73where
74 T: AsRef<str> + Display,
75{
76 read_keypair_file(string.as_ref())
77 .map(|_| ())
78 .map_err(|err| format!("{err}"))
79}
80
81pub fn is_keypair_or_ask_keyword<T>(string: T) -> Result<(), String>
83where
84 T: AsRef<str> + Display,
85{
86 if string.as_ref() == ASK_KEYWORD {
87 return Ok(());
88 }
89 read_keypair_file(string.as_ref())
90 .map(|_| ())
91 .map_err(|err| format!("{err}"))
92}
93
94pub fn is_prompt_signer_source<T>(string: T) -> Result<(), String>
96where
97 T: AsRef<str> + Display,
98{
99 if string.as_ref() == ASK_KEYWORD {
100 return Ok(());
101 }
102 match parse_signer_source(string.as_ref())
103 .map_err(|err| format!("{err}"))?
104 .kind
105 {
106 SignerSourceKind::Prompt => Ok(()),
107 _ => Err(format!(
108 "Unable to parse input as `prompt:` URI scheme or `ASK` keyword: {string}"
109 )),
110 }
111}
112
113pub fn is_pubkey_or_keypair<T>(string: T) -> Result<(), String>
115where
116 T: AsRef<str> + Display,
117{
118 is_pubkey(string.as_ref()).or_else(|_| is_keypair(string))
119}
120
121pub fn is_valid_pubkey<T>(string: T) -> Result<(), String>
124where
125 T: AsRef<str> + Display,
126{
127 match parse_signer_source(string.as_ref())
128 .map_err(|err| format!("{err}"))?
129 .kind
130 {
131 SignerSourceKind::Filepath(path) => is_keypair(path),
132 _ => Ok(()),
133 }
134}
135
136pub fn is_valid_signer<T>(string: T) -> Result<(), String>
145where
146 T: AsRef<str> + Display,
147{
148 is_valid_pubkey(string)
149}
150
151pub fn is_pubkey_sig<T>(string: T) -> Result<(), String>
153where
154 T: AsRef<str> + Display,
155{
156 let mut signer = string.as_ref().split('=');
157 match Pubkey::from_str(
158 signer
159 .next()
160 .ok_or_else(|| "Malformed signer string".to_string())?,
161 ) {
162 Ok(_) => {
163 match Signature::from_str(
164 signer
165 .next()
166 .ok_or_else(|| "Malformed signer string".to_string())?,
167 ) {
168 Ok(_) => Ok(()),
169 Err(err) => Err(format!("{err}")),
170 }
171 }
172 Err(err) => Err(format!("{err}")),
173 }
174}
175
176pub fn is_url<T>(string: T) -> Result<(), String>
178where
179 T: AsRef<str> + Display,
180{
181 match url::Url::parse(string.as_ref()) {
182 Ok(url) => {
183 if url.has_host() {
184 Ok(())
185 } else {
186 Err("no host provided".to_string())
187 }
188 }
189 Err(err) => Err(format!("{err}")),
190 }
191}
192
193pub fn is_url_or_moniker<T>(string: T) -> Result<(), String>
194where
195 T: AsRef<str> + Display,
196{
197 match url::Url::parse(&normalize_to_url_if_moniker(string.as_ref())) {
198 Ok(url) => {
199 if url.has_host() {
200 Ok(())
201 } else {
202 Err("no host provided".to_string())
203 }
204 }
205 Err(err) => Err(format!("{err}")),
206 }
207}
208
209pub fn normalize_to_url_if_moniker<T: AsRef<str>>(url_or_moniker: T) -> String {
210 match url_or_moniker.as_ref() {
211 "m" | "mainnet-beta" => "https://api.mainnet-beta.solana.com",
212 "t" | "testnet" => "https://api.testnet.solana.com",
213 "d" | "devnet" => "https://api.devnet.solana.com",
214 "l" | "localhost" => "http://localhost:8899",
215 url => url,
216 }
217 .to_string()
218}
219
220pub fn is_epoch<T>(epoch: T) -> Result<(), String>
221where
222 T: AsRef<str> + Display,
223{
224 is_parsable_generic::<Epoch, _>(epoch)
225}
226
227pub fn is_slot<T>(slot: T) -> Result<(), String>
228where
229 T: AsRef<str> + Display,
230{
231 is_parsable_generic::<Slot, _>(slot)
232}
233
234pub fn is_pow2<T>(bins: T) -> Result<(), String>
235where
236 T: AsRef<str> + Display,
237{
238 bins.as_ref()
239 .parse::<usize>()
240 .map_err(|e| format!("Unable to parse, provided: {bins}, err: {e}"))
241 .and_then(|v| {
242 if !v.is_power_of_two() {
243 Err(format!("Must be a power of 2: {v}"))
244 } else {
245 Ok(())
246 }
247 })
248}
249
250pub fn is_port<T>(port: T) -> Result<(), String>
251where
252 T: AsRef<str> + Display,
253{
254 is_parsable_generic::<u16, _>(port)
255}
256
257pub fn is_valid_percentage<T>(percentage: T) -> Result<(), String>
258where
259 T: AsRef<str> + Display,
260{
261 percentage
262 .as_ref()
263 .parse::<u8>()
264 .map_err(|e| format!("Unable to parse input percentage, provided: {percentage}, err: {e}"))
265 .and_then(|v| {
266 if v > 100 {
267 Err(format!(
268 "Percentage must be in range of 0 to 100, provided: {v}"
269 ))
270 } else {
271 Ok(())
272 }
273 })
274}
275
276pub fn is_amount<T>(amount: T) -> Result<(), String>
277where
278 T: AsRef<str> + Display,
279{
280 if amount.as_ref().parse::<u64>().is_ok() || amount.as_ref().parse::<f64>().is_ok() {
281 Ok(())
282 } else {
283 Err(format!(
284 "Unable to parse input amount as integer or float, provided: {amount}"
285 ))
286 }
287}
288
289pub fn is_amount_or_all<T>(amount: T) -> Result<(), String>
290where
291 T: AsRef<str> + Display,
292{
293 if amount.as_ref().parse::<u64>().is_ok()
294 || amount.as_ref().parse::<f64>().is_ok()
295 || amount.as_ref() == "ALL"
296 {
297 Ok(())
298 } else {
299 Err(format!(
300 "Unable to parse input amount as integer or float, provided: {amount}"
301 ))
302 }
303}
304
305pub fn is_rfc3339_datetime<T>(value: T) -> Result<(), String>
306where
307 T: AsRef<str> + Display,
308{
309 DateTime::parse_from_rfc3339(value.as_ref())
310 .map(|_| ())
311 .map_err(|e| format!("{e}"))
312}
313
314pub fn is_derivation<T>(value: T) -> Result<(), String>
315where
316 T: AsRef<str> + Display,
317{
318 let value = value.as_ref().replace('\'', "");
319 let mut parts = value.split('/');
320 let account = parts.next().unwrap();
321 account
322 .parse::<u32>()
323 .map_err(|e| format!("Unable to parse derivation, provided: {account}, err: {e}"))
324 .and_then(|_| {
325 if let Some(change) = parts.next() {
326 change.parse::<u32>().map_err(|e| {
327 format!("Unable to parse derivation, provided: {change}, err: {e}")
328 })
329 } else {
330 Ok(0)
331 }
332 })
333 .map(|_| ())
334}
335
336pub fn is_structured_seed<T>(value: T) -> Result<(), String>
337where
338 T: AsRef<str> + Display,
339{
340 let (prefix, value) = value
341 .as_ref()
342 .split_once(':')
343 .ok_or("Seed must contain ':' as delimiter")
344 .unwrap();
345 if prefix.is_empty() || value.is_empty() {
346 Err(String::from("Seed prefix or value is empty"))
347 } else {
348 match prefix {
349 "string" | "pubkey" | "hex" | "u8" => Ok(()),
350 _ => {
351 let len = prefix.len();
352 if len != 5 && len != 6 {
353 Err(format!("Wrong prefix length {len} {prefix}:{value}"))
354 } else {
355 let sign = &prefix[0..1];
356 let type_size = &prefix[1..len.saturating_sub(2)];
357 let byte_order = &prefix[len.saturating_sub(2)..len];
358 if sign != "u" && sign != "i" {
359 Err(format!("Wrong prefix sign {sign} {prefix}:{value}"))
360 } else if type_size != "16"
361 && type_size != "32"
362 && type_size != "64"
363 && type_size != "128"
364 {
365 Err(format!(
366 "Wrong prefix type size {type_size} {prefix}:{value}"
367 ))
368 } else if byte_order != "le" && byte_order != "be" {
369 Err(format!(
370 "Wrong prefix byte order {byte_order} {prefix}:{value}"
371 ))
372 } else {
373 Ok(())
374 }
375 }
376 }
377 }
378 }
379}
380
381pub fn is_derived_address_seed<T>(value: T) -> Result<(), String>
382where
383 T: AsRef<str> + Display,
384{
385 let value = value.as_ref();
386 if value.len() > MAX_SEED_LEN {
387 Err(format!(
388 "Address seed must not be longer than {MAX_SEED_LEN} bytes"
389 ))
390 } else {
391 Ok(())
392 }
393}
394
395pub fn validate_maximum_full_snapshot_archives_to_retain<T>(value: T) -> Result<(), String>
396where
397 T: AsRef<str> + Display,
398{
399 let value = value.as_ref();
400 if value.eq("0") {
401 Err(String::from(
402 "--maximum-full-snapshot-archives-to-retain cannot be zero",
403 ))
404 } else {
405 Ok(())
406 }
407}
408
409pub fn validate_maximum_incremental_snapshot_archives_to_retain<T>(value: T) -> Result<(), String>
410where
411 T: AsRef<str> + Display,
412{
413 let value = value.as_ref();
414 if value.eq("0") {
415 Err(String::from(
416 "--maximum-incremental-snapshot-archives-to-retain cannot be zero",
417 ))
418 } else {
419 Ok(())
420 }
421}
422
423#[cfg(test)]
424mod tests {
425 use super::*;
426
427 #[test]
428 fn test_is_derivation() {
429 assert_eq!(is_derivation("2"), Ok(()));
430 assert_eq!(is_derivation("0"), Ok(()));
431 assert_eq!(is_derivation("65537"), Ok(()));
432 assert_eq!(is_derivation("0/2"), Ok(()));
433 assert_eq!(is_derivation("0'/2'"), Ok(()));
434 assert!(is_derivation("a").is_err());
435 assert!(is_derivation("4294967296").is_err());
436 assert!(is_derivation("a/b").is_err());
437 assert!(is_derivation("0/4294967296").is_err());
438 }
439}