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