1use {
2 crate::{
3 cli::{CliCommand, CliCommandInfo, CliConfig, CliError, ProcessResult},
4 spend_utils::{resolve_spend_tx_and_check_account_balance, SpendAmount},
5 },
6 bincode::{deserialize, serialized_size},
7 clap::{App, AppSettings, Arg, ArgMatches, SubCommand},
8 miraland_account_decoder::validator_info::{
9 self, ValidatorInfo, MAX_LONG_FIELD_LENGTH, MAX_SHORT_FIELD_LENGTH,
10 },
11 miraland_clap_utils::{
12 hidden_unless_forced,
13 input_parsers::pubkey_of,
14 input_validators::{is_pubkey, is_url},
15 keypair::DefaultSigner,
16 },
17 miraland_cli_output::{CliValidatorInfo, CliValidatorInfoVec},
18 miraland_remote_wallet::remote_wallet::RemoteWalletManager,
19 miraland_rpc_client::rpc_client::RpcClient,
20 reqwest::blocking::Client,
21 serde_json::{Map, Value},
22 miraland_config_program::{config_instruction, get_config_data, ConfigKeys, ConfigState},
23 miraland_sdk::{
24 account::Account,
25 message::Message,
26 pubkey::Pubkey,
27 signature::{Keypair, Signer},
28 transaction::Transaction,
29 },
30 std::{error, rc::Rc},
31};
32
33pub fn check_details_length(string: String) -> Result<(), String> {
35 if string.len() > MAX_LONG_FIELD_LENGTH {
36 Err(format!(
37 "validator details longer than {MAX_LONG_FIELD_LENGTH:?}-byte limit"
38 ))
39 } else {
40 Ok(())
41 }
42}
43
44pub fn check_total_length(info: &ValidatorInfo) -> Result<(), String> {
45 let size = serialized_size(&info).unwrap();
46 let limit = ValidatorInfo::max_space();
47
48 if size > limit {
49 Err(format!(
50 "Total size {size:?} exceeds limit of {limit:?} bytes"
51 ))
52 } else {
53 Ok(())
54 }
55}
56
57pub fn check_url(string: String) -> Result<(), String> {
59 is_url(string.clone())?;
60 if string.len() > MAX_SHORT_FIELD_LENGTH {
61 Err(format!(
62 "url longer than {MAX_SHORT_FIELD_LENGTH:?}-byte limit"
63 ))
64 } else {
65 Ok(())
66 }
67}
68
69pub fn is_short_field(string: String) -> Result<(), String> {
71 if string.len() > MAX_SHORT_FIELD_LENGTH {
72 Err(format!(
73 "validator field longer than {MAX_SHORT_FIELD_LENGTH:?}-byte limit"
74 ))
75 } else {
76 Ok(())
77 }
78}
79
80fn verify_keybase(
81 validator_pubkey: &Pubkey,
82 keybase_username: &Value,
83) -> Result<(), Box<dyn error::Error>> {
84 if let Some(keybase_username) = keybase_username.as_str() {
85 let url = format!(
86 "https://keybase.pub/{keybase_username}/miraland/validator-{validator_pubkey:?}"
87 );
88 let client = Client::new();
89 if client.head(&url).send()?.status().is_success() {
90 Ok(())
91 } else {
92 Err(format!("keybase_username could not be confirmed at: {url}. Please add this pubkey file to your keybase profile to connect").into())
93 }
94 } else {
95 Err(format!("keybase_username could not be parsed as String: {keybase_username}").into())
96 }
97}
98
99fn parse_args(matches: &ArgMatches<'_>) -> Value {
100 let mut map = Map::new();
101 map.insert(
102 "name".to_string(),
103 Value::String(matches.value_of("name").unwrap().to_string()),
104 );
105 if let Some(url) = matches.value_of("website") {
106 map.insert("website".to_string(), Value::String(url.to_string()));
107 }
108
109 if let Some(icon_url) = matches.value_of("icon_url") {
110 map.insert("iconUrl".to_string(), Value::String(icon_url.to_string()));
111 }
112 if let Some(details) = matches.value_of("details") {
113 map.insert("details".to_string(), Value::String(details.to_string()));
114 }
115 if let Some(keybase_username) = matches.value_of("keybase_username") {
116 map.insert(
117 "keybaseUsername".to_string(),
118 Value::String(keybase_username.to_string()),
119 );
120 }
121 Value::Object(map)
122}
123
124fn parse_validator_info(
125 pubkey: &Pubkey,
126 account: &Account,
127) -> Result<(Pubkey, Map<String, serde_json::value::Value>), Box<dyn error::Error>> {
128 if account.owner != miraland_config_program::id() {
129 return Err(format!("{pubkey} is not a validator info account").into());
130 }
131 let key_list: ConfigKeys = deserialize(&account.data)?;
132 if !key_list.keys.is_empty() {
133 let (validator_pubkey, _) = key_list.keys[1];
134 let validator_info_string: String = deserialize(get_config_data(&account.data)?)?;
135 let validator_info: Map<_, _> = serde_json::from_str(&validator_info_string)?;
136 Ok((validator_pubkey, validator_info))
137 } else {
138 Err(format!("{pubkey} could not be parsed as a validator info account").into())
139 }
140}
141
142pub trait ValidatorInfoSubCommands {
143 fn validator_info_subcommands(self) -> Self;
144}
145
146impl ValidatorInfoSubCommands for App<'_, '_> {
147 fn validator_info_subcommands(self) -> Self {
148 self.subcommand(
149 SubCommand::with_name("validator-info")
150 .about("Publish/get Validator info on Miraland")
151 .setting(AppSettings::SubcommandRequiredElseHelp)
152 .subcommand(
153 SubCommand::with_name("publish")
154 .about("Publish Validator info on Miraland")
155 .arg(
156 Arg::with_name("info_pubkey")
157 .short("p")
158 .long("info-pubkey")
159 .value_name("PUBKEY")
160 .takes_value(true)
161 .validator(is_pubkey)
162 .help("The pubkey of the Validator info account to update"),
163 )
164 .arg(
165 Arg::with_name("name")
166 .index(1)
167 .value_name("NAME")
168 .takes_value(true)
169 .required(true)
170 .validator(is_short_field)
171 .help("Validator name"),
172 )
173 .arg(
174 Arg::with_name("website")
175 .short("w")
176 .long("website")
177 .value_name("URL")
178 .takes_value(true)
179 .validator(check_url)
180 .help("Validator website url"),
181 )
182 .arg(
183 Arg::with_name("icon_url")
184 .short("i")
185 .long("icon-url")
186 .value_name("URL")
187 .takes_value(true)
188 .validator(check_url)
189 .help("Validator icon URL"),
190 )
191 .arg(
192 Arg::with_name("keybase_username")
193 .short("n")
194 .long("keybase")
195 .value_name("USERNAME")
196 .takes_value(true)
197 .validator(is_short_field)
198 .hidden(hidden_unless_forced()) .help("Validator Keybase username"),
200 )
201 .arg(
202 Arg::with_name("details")
203 .short("d")
204 .long("details")
205 .value_name("DETAILS")
206 .takes_value(true)
207 .validator(check_details_length)
208 .help("Validator description")
209 )
210 .arg(
211 Arg::with_name("force")
212 .long("force")
213 .takes_value(false)
214 .hidden(hidden_unless_forced()) .help("Override keybase username validity check"),
216 ),
217 )
218 .subcommand(
219 SubCommand::with_name("get")
220 .about("Get and parse Miraland Validator info")
221 .arg(
222 Arg::with_name("info_pubkey")
223 .index(1)
224 .value_name("PUBKEY")
225 .takes_value(true)
226 .validator(is_pubkey)
227 .help("The pubkey of the Validator info account; without this argument, returns all"),
228 ),
229 )
230 )
231 }
232}
233
234pub fn parse_validator_info_command(
235 matches: &ArgMatches<'_>,
236 default_signer: &DefaultSigner,
237 wallet_manager: &mut Option<Rc<RemoteWalletManager>>,
238) -> Result<CliCommandInfo, CliError> {
239 let info_pubkey = pubkey_of(matches, "info_pubkey");
240 let validator_info = parse_args(matches);
242 Ok(CliCommandInfo {
243 command: CliCommand::SetValidatorInfo {
244 validator_info,
245 force_keybase: matches.is_present("force"),
246 info_pubkey,
247 },
248 signers: vec![default_signer.signer_from_path(matches, wallet_manager)?],
249 })
250}
251
252pub fn parse_get_validator_info_command(
253 matches: &ArgMatches<'_>,
254) -> Result<CliCommandInfo, CliError> {
255 let info_pubkey = pubkey_of(matches, "info_pubkey");
256 Ok(CliCommandInfo {
257 command: CliCommand::GetValidatorInfo(info_pubkey),
258 signers: vec![],
259 })
260}
261
262pub fn process_set_validator_info(
263 rpc_client: &RpcClient,
264 config: &CliConfig,
265 validator_info: &Value,
266 force_keybase: bool,
267 info_pubkey: Option<Pubkey>,
268) -> ProcessResult {
269 if let Some(string) = validator_info.get("keybaseUsername") {
271 if force_keybase {
272 println!("--force supplied, skipping Keybase verification");
273 } else {
274 let result = verify_keybase(&config.signers[0].pubkey(), string);
275 if result.is_err() {
276 result.map_err(|err| {
277 CliError::BadParameter(format!("Invalid validator keybase username: {err}"))
278 })?;
279 }
280 }
281 }
282 let validator_string = serde_json::to_string(&validator_info).unwrap();
283 let validator_info = ValidatorInfo {
284 info: validator_string,
285 };
286
287 let result = check_total_length(&validator_info);
288 if result.is_err() {
289 result.map_err(|err| {
290 CliError::BadParameter(format!("Maximum size for validator info: {err}"))
291 })?;
292 }
293
294 let all_config = rpc_client.get_program_accounts(&miraland_config_program::id())?;
296 let existing_account = all_config
297 .iter()
298 .filter(
299 |(_, account)| match deserialize::<ConfigKeys>(&account.data) {
300 Ok(key_list) => key_list.keys.contains(&(validator_info::id(), false)),
301 Err(_) => false,
302 },
303 )
304 .find(|(pubkey, account)| {
305 let (validator_pubkey, _) = parse_validator_info(pubkey, account).unwrap();
306 validator_pubkey == config.signers[0].pubkey()
307 });
308
309 let info_keypair = Keypair::new();
311 let mut info_pubkey = if let Some(pubkey) = info_pubkey {
312 pubkey
313 } else if let Some(validator_info) = existing_account {
314 validator_info.0
315 } else {
316 info_keypair.pubkey()
317 };
318
319 let balance = rpc_client.get_balance(&info_pubkey).unwrap_or(0);
321
322 let keys = vec![
323 (validator_info::id(), false),
324 (config.signers[0].pubkey(), true),
325 ];
326 let data_len = ValidatorInfo::max_space() + ConfigKeys::serialized_size(keys.clone());
327 let lamports = rpc_client.get_minimum_balance_for_rent_exemption(data_len as usize)?;
328
329 let signers = if balance == 0 {
330 if info_pubkey != info_keypair.pubkey() {
331 println!("Account {info_pubkey:?} does not exist. Generating new keypair...");
332 info_pubkey = info_keypair.pubkey();
333 }
334 vec![config.signers[0], &info_keypair]
335 } else {
336 vec![config.signers[0]]
337 };
338
339 let build_message = |lamports| {
340 let keys = keys.clone();
341 if balance == 0 {
342 println!(
343 "Publishing info for Validator {:?}",
344 config.signers[0].pubkey()
345 );
346 let mut instructions = config_instruction::create_account::<ValidatorInfo>(
347 &config.signers[0].pubkey(),
348 &info_pubkey,
349 lamports,
350 keys.clone(),
351 );
352 instructions.extend_from_slice(&[config_instruction::store(
353 &info_pubkey,
354 true,
355 keys,
356 &validator_info,
357 )]);
358 Message::new(&instructions, Some(&config.signers[0].pubkey()))
359 } else {
360 println!(
361 "Updating Validator {:?} info at: {:?}",
362 config.signers[0].pubkey(),
363 info_pubkey
364 );
365 let instructions = vec![config_instruction::store(
366 &info_pubkey,
367 false,
368 keys,
369 &validator_info,
370 )];
371 Message::new(&instructions, Some(&config.signers[0].pubkey()))
372 }
373 };
374
375 let latest_blockhash = rpc_client.get_latest_blockhash()?;
377 let (message, _) = resolve_spend_tx_and_check_account_balance(
378 rpc_client,
379 false,
380 SpendAmount::Some(lamports),
381 &latest_blockhash,
382 &config.signers[0].pubkey(),
383 build_message,
384 config.commitment,
385 )?;
386 let mut tx = Transaction::new_unsigned(message);
387 tx.try_sign(&signers, latest_blockhash)?;
388 let signature_str = rpc_client.send_and_confirm_transaction_with_spinner(&tx)?;
389
390 println!("Success! Validator info published at: {info_pubkey:?}");
391 println!("{signature_str}");
392 Ok("".to_string())
393}
394
395pub fn process_get_validator_info(
396 rpc_client: &RpcClient,
397 config: &CliConfig,
398 pubkey: Option<Pubkey>,
399) -> ProcessResult {
400 let validator_info: Vec<(Pubkey, Account)> = if let Some(validator_info_pubkey) = pubkey {
401 vec![(
402 validator_info_pubkey,
403 rpc_client.get_account(&validator_info_pubkey)?,
404 )]
405 } else {
406 let all_config = rpc_client.get_program_accounts(&miraland_config_program::id())?;
407 all_config
408 .into_iter()
409 .filter(|(_, validator_info_account)| {
410 match deserialize::<ConfigKeys>(&validator_info_account.data) {
411 Ok(key_list) => key_list.keys.contains(&(validator_info::id(), false)),
412 Err(_) => false,
413 }
414 })
415 .collect()
416 };
417
418 let mut validator_info_list: Vec<CliValidatorInfo> = vec![];
419 if validator_info.is_empty() {
420 println!("No validator info accounts found");
421 }
422 for (validator_info_pubkey, validator_info_account) in validator_info.iter() {
423 let (validator_pubkey, validator_info) =
424 parse_validator_info(validator_info_pubkey, validator_info_account)?;
425 validator_info_list.push(CliValidatorInfo {
426 identity_pubkey: validator_pubkey.to_string(),
427 info_pubkey: validator_info_pubkey.to_string(),
428 info: validator_info,
429 });
430 }
431 Ok(config
432 .output_format
433 .formatted_string(&CliValidatorInfoVec::new(validator_info_list)))
434}
435
436#[cfg(test)]
437mod tests {
438 use {
439 super::*,
440 crate::clap_app::get_clap_app,
441 bincode::{serialize, serialized_size},
442 serde_json::json,
443 };
444
445 #[test]
446 fn test_check_details_length() {
447 let short_details = (0..MAX_LONG_FIELD_LENGTH).map(|_| "X").collect::<String>();
448 assert_eq!(check_details_length(short_details), Ok(()));
449
450 let long_details = (0..MAX_LONG_FIELD_LENGTH + 1)
451 .map(|_| "X")
452 .collect::<String>();
453 assert_eq!(
454 check_details_length(long_details),
455 Err(format!(
456 "validator details longer than {MAX_LONG_FIELD_LENGTH:?}-byte limit"
457 ))
458 );
459 }
460
461 #[test]
462 fn test_check_url() {
463 let url = "http://test.com";
464 assert_eq!(check_url(url.to_string()), Ok(()));
465 let long_url = "http://7cLvFwLCbyHuXQ1RGzhCMobAWYPMSZ3VbUml1CMobAWYPMSZ3VbUml1qWi1nkc3FD7zj9hzTZzMvYJ.com";
466 assert!(check_url(long_url.to_string()).is_err());
467 let non_url = "not parseable";
468 assert!(check_url(non_url.to_string()).is_err());
469 }
470
471 #[test]
472 fn test_is_short_field() {
473 let name = "Alice Validator";
474 assert_eq!(is_short_field(name.to_string()), Ok(()));
475 let long_name = "Alice 7cLvFwLCbyHuXQ1RGzhCMobAWYPMSZ3VbUml1qWi1nkc3FD7zj9hzTZzMvYJt6rY9j9hzTZzMvYJt6rY9";
476 assert!(is_short_field(long_name.to_string()).is_err());
477 }
478
479 #[test]
480 fn test_verify_keybase_username_not_string() {
481 let pubkey = miraland_sdk::pubkey::new_rand();
482 let value = Value::Bool(true);
483
484 assert_eq!(
485 verify_keybase(&pubkey, &value).unwrap_err().to_string(),
486 "keybase_username could not be parsed as String: true".to_string()
487 )
488 }
489
490 #[test]
491 fn test_parse_args() {
492 let matches = get_clap_app("test", "desc", "version").get_matches_from(vec![
493 "test",
494 "validator-info",
495 "publish",
496 "Alice",
497 "-n",
498 "alice_keybase",
499 "-i",
500 "https://test.com/icon.png",
501 ]);
502 let subcommand_matches = matches.subcommand();
503 assert_eq!(subcommand_matches.0, "validator-info");
504 assert!(subcommand_matches.1.is_some());
505 let subcommand_matches = subcommand_matches.1.unwrap().subcommand();
506 assert_eq!(subcommand_matches.0, "publish");
507 assert!(subcommand_matches.1.is_some());
508 let matches = subcommand_matches.1.unwrap();
509 let expected = json!({
510 "name": "Alice",
511 "keybaseUsername": "alice_keybase",
512 "iconUrl": "https://test.com/icon.png",
513 });
514 assert_eq!(parse_args(matches), expected);
515 }
516
517 #[test]
518 fn test_validator_info_serde() {
519 let mut info = Map::new();
520 info.insert("name".to_string(), Value::String("Alice".to_string()));
521 let info_string = serde_json::to_string(&Value::Object(info)).unwrap();
522
523 let validator_info = ValidatorInfo {
524 info: info_string.clone(),
525 };
526
527 assert_eq!(serialized_size(&validator_info).unwrap(), 24);
528 assert_eq!(
529 serialize(&validator_info).unwrap(),
530 vec![
531 16, 0, 0, 0, 0, 0, 0, 0, 123, 34, 110, 97, 109, 101, 34, 58, 34, 65, 108, 105, 99,
532 101, 34, 125
533 ]
534 );
535
536 let deserialized: ValidatorInfo = deserialize(&[
537 16, 0, 0, 0, 0, 0, 0, 0, 123, 34, 110, 97, 109, 101, 34, 58, 34, 65, 108, 105, 99, 101,
538 34, 125,
539 ])
540 .unwrap();
541 assert_eq!(deserialized.info, info_string);
542 }
543
544 #[test]
545 fn test_parse_validator_info() {
546 let pubkey = miraland_sdk::pubkey::new_rand();
547 let keys = vec![(validator_info::id(), false), (pubkey, true)];
548 let config = ConfigKeys { keys };
549
550 let mut info = Map::new();
551 info.insert("name".to_string(), Value::String("Alice".to_string()));
552 let info_string = serde_json::to_string(&Value::Object(info.clone())).unwrap();
553 let validator_info = ValidatorInfo { info: info_string };
554 let data = serialize(&(config, validator_info)).unwrap();
555
556 assert_eq!(
557 parse_validator_info(
558 &Pubkey::default(),
559 &Account {
560 owner: miraland_config_program::id(),
561 data,
562 ..Account::default()
563 }
564 )
565 .unwrap(),
566 (pubkey, info)
567 );
568 }
569
570 #[test]
571 fn test_parse_validator_info_not_validator_info_account() {
572 assert!(parse_validator_info(
573 &Pubkey::default(),
574 &Account {
575 owner: miraland_sdk::pubkey::new_rand(),
576 ..Account::default()
577 }
578 )
579 .unwrap_err()
580 .to_string()
581 .contains("is not a validator info account"));
582 }
583
584 #[test]
585 fn test_parse_validator_info_empty_key_list() {
586 let config = ConfigKeys { keys: vec![] };
587 let validator_info = ValidatorInfo {
588 info: String::new(),
589 };
590 let data = serialize(&(config, validator_info)).unwrap();
591
592 assert!(parse_validator_info(
593 &Pubkey::default(),
594 &Account {
595 owner: miraland_config_program::id(),
596 data,
597 ..Account::default()
598 },
599 )
600 .unwrap_err()
601 .to_string()
602 .contains("could not be parsed as a validator info account"));
603 }
604
605 #[test]
606 fn test_validator_info_max_space() {
607 let max_short_string =
609 "Max Length String KWpP299aFCBWvWg1MHpSuaoTsud7cv8zMJsh99aAtP8X1s26yrR1".to_string();
610 let max_long_string = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut libero quam, volutpat et aliquet eu, varius in mi. Aenean vestibulum ex in tristique faucibus. Maecenas in imperdiet turpis. Nullam feugiat aliquet erat. Morbi malesuada turpis sed dui pulvinar lobortis. Pellentesque a lectus eu leo nullam.".to_string();
612 let mut info = Map::new();
613 info.insert("name".to_string(), Value::String(max_short_string.clone()));
614 info.insert(
615 "website".to_string(),
616 Value::String(max_short_string.clone()),
617 );
618 info.insert(
619 "keybaseUsername".to_string(),
620 Value::String(max_short_string),
621 );
622 info.insert("details".to_string(), Value::String(max_long_string));
623 let info_string = serde_json::to_string(&Value::Object(info)).unwrap();
624
625 let validator_info = ValidatorInfo { info: info_string };
626
627 assert_eq!(
628 serialized_size(&validator_info).unwrap(),
629 ValidatorInfo::max_space()
630 );
631 }
632}