1use std::collections::VecDeque;
2use std::convert::{TryFrom, TryInto};
3use std::io::Write;
4use std::str::FromStr;
5
6use color_eyre::eyre::{ContextCompat, WrapErr};
7use futures::{StreamExt, TryStreamExt};
8use prettytable::Table;
9
10use unc_primitives::{hash::CryptoHash, types::BlockReference, views::AccessKeyPermissionView};
11
12pub type CliResult = color_eyre::eyre::Result<()>;
13
14use inquire::{Select, Text};
15use strum::IntoEnumIterator;
16
17use rand::Rng;
18use rsa::pkcs8::{EncodePrivateKey, EncodePublicKey};
19use rsa::{RsaPrivateKey, RsaPublicKey};
20
21pub fn get_unc_exec_path() -> String {
22 std::env::args()
23 .next()
24 .unwrap_or_else(|| "./unc".to_owned())
25}
26
27#[derive(
28 Debug,
29 Clone,
30 strum_macros::IntoStaticStr,
31 strum_macros::EnumString,
32 strum_macros::EnumVariantNames,
33 smart_default::SmartDefault,
34)]
35#[strum(serialize_all = "snake_case")]
36pub enum OutputFormat {
37 #[default]
38 Plaintext,
39 Json,
40}
41
42impl std::fmt::Display for OutputFormat {
43 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44 match self {
45 OutputFormat::Plaintext => write!(f, "plaintext"),
46 OutputFormat::Json => write!(f, "json"),
47 }
48 }
49}
50
51#[derive(Debug, Clone)]
52pub struct BlockHashAsBase58 {
53 pub inner: unc_primitives::hash::CryptoHash,
54}
55
56impl std::str::FromStr for BlockHashAsBase58 {
57 type Err = String;
58 fn from_str(s: &str) -> Result<Self, Self::Err> {
59 Ok(Self {
60 inner: bs58::decode(s)
61 .into_vec()
62 .map_err(|err| format!("base58 block hash sequence is invalid: {}", err))?
63 .as_slice()
64 .try_into()
65 .map_err(|err| format!("block hash could not be collected: {}", err))?,
66 })
67 }
68}
69
70impl std::fmt::Display for BlockHashAsBase58 {
71 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
72 write!(f, "BlockHash {}", self.inner)
73 }
74}
75
76pub use unc_gas::UncGas;
77
78#[derive(Debug, Clone, Default, PartialEq, Eq, PartialOrd)]
79pub struct TransferAmount {
80 amount: unc_token::UncToken,
81}
82
83impl interactive_clap::ToCli for TransferAmount {
84 type CliVariant = unc_token::UncToken;
85}
86
87impl std::fmt::Display for TransferAmount {
88 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
89 write!(f, "{}", self.amount)
90 }
91}
92
93impl TransferAmount {
94 pub fn from(
95 amount: unc_token::UncToken,
96 account_transfer_allowance: &AccountTransferAllowance,
97 ) -> color_eyre::eyre::Result<Self> {
98 if amount <= account_transfer_allowance.transfer_allowance() {
99 Ok(Self { amount })
100 } else {
101 Err(color_eyre::Report::msg(
102 "the amount exceeds the transfer allowance",
103 ))
104 }
105 }
106
107 pub fn from_unchecked(amount: unc_token::UncToken) -> Self {
108 Self { amount }
109 }
110
111 pub fn as_attounc(&self) -> u128 {
112 self.amount.as_attounc()
113 }
114}
115
116impl From<TransferAmount> for unc_token::UncToken {
117 fn from(item: TransferAmount) -> Self {
118 item.amount
119 }
120}
121
122#[derive(Debug)]
123pub struct AccountTransferAllowance {
124 account_id: unc_primitives::types::AccountId,
125 account_liquid_balance: unc_token::UncToken,
126 account_locked_balance: unc_token::UncToken,
127 storage_pledge: unc_token::UncToken,
128 pessimistic_transaction_fee: unc_token::UncToken,
129}
130
131impl std::fmt::Display for AccountTransferAllowance {
132 fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
133 write!(fmt,
134 "\n{} account has {} available for transfer (the total balance is {}, but {} is locked for storage and the transfer transaction fee is ~{})",
135 self.account_id,
136 self.transfer_allowance(),
137 self.account_liquid_balance,
138 self.liquid_storage_pledge(),
139 self.pessimistic_transaction_fee
140 )
141 }
142}
143
144impl AccountTransferAllowance {
145 pub fn liquid_storage_pledge(&self) -> unc_token::UncToken {
146 self.storage_pledge
147 .saturating_sub(self.account_locked_balance)
148 }
149
150 pub fn transfer_allowance(&self) -> unc_token::UncToken {
151 self.account_liquid_balance
152 .saturating_sub(self.liquid_storage_pledge())
153 .saturating_sub(self.pessimistic_transaction_fee)
154 }
155}
156
157pub fn get_account_transfer_allowance(
158 network_config: crate::config::NetworkConfig,
159 account_id: unc_primitives::types::AccountId,
160 block_reference: BlockReference,
161) -> color_eyre::eyre::Result<AccountTransferAllowance> {
162 let account_view = if let Ok(account_view) =
163 get_account_state(network_config.clone(), account_id.clone(), block_reference)
164 {
165 account_view
166 } else if !account_id.get_account_type().is_implicit() {
167 return color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!(
168 "Account <{}> does not exist on network <{}>.",
169 account_id,
170 network_config.network_name
171 ));
172 } else {
173 return Ok(AccountTransferAllowance {
174 account_id,
175 account_liquid_balance: unc_token::UncToken::from_unc(0),
176 account_locked_balance: unc_token::UncToken::from_unc(0),
177 storage_pledge: unc_token::UncToken::from_unc(0),
178 pessimistic_transaction_fee: unc_token::UncToken::from_unc(0),
179 });
180 };
181 let storage_amount_per_byte = tokio::runtime::Runtime::new()
182 .unwrap()
183 .block_on(network_config.json_rpc_client().call(
184 unc_jsonrpc_client::methods::EXPERIMENTAL_protocol_config::RpcProtocolConfigRequest {
185 block_reference: unc_primitives::types::Finality::Final.into(),
186 },
187 ))
188 .wrap_err("RpcError")?
189 .runtime_config
190 .storage_amount_per_byte;
191
192 Ok(AccountTransferAllowance {
193 account_id,
194 account_liquid_balance: unc_token::UncToken::from_attounc(account_view.amount),
195 account_locked_balance: unc_token::UncToken::from_attounc(account_view.pledging),
196 storage_pledge: unc_token::UncToken::from_attounc(
197 u128::from(account_view.storage_usage) * storage_amount_per_byte,
198 ),
199 pessimistic_transaction_fee: unc_token::UncToken::from_milliunc(1),
203 })
204}
205
206pub fn verify_account_access_key(
207 account_id: unc_primitives::types::AccountId,
208 public_key: unc_crypto::PublicKey,
209 network_config: crate::config::NetworkConfig,
210) -> color_eyre::eyre::Result<
211 unc_primitives::views::AccessKeyView,
212 unc_jsonrpc_client::errors::JsonRpcError<unc_jsonrpc_primitives::types::query::RpcQueryError>,
213> {
214 loop {
215 match network_config
216 .json_rpc_client()
217 .blocking_call_view_access_key(
218 &account_id,
219 &public_key,
220 unc_primitives::types::BlockReference::latest(),
221 ) {
222 Ok(rpc_query_response) => {
223 if let unc_jsonrpc_primitives::types::query::QueryResponseKind::AccessKey(result) =
224 rpc_query_response.kind
225 {
226 return Ok(result);
227 } else {
228 return Err(unc_jsonrpc_client::errors::JsonRpcError::TransportError(unc_jsonrpc_client::errors::RpcTransportError::RecvError(
229 unc_jsonrpc_client::errors::JsonRpcTransportRecvError::UnexpectedServerResponse(
230 unc_jsonrpc_primitives::message::Message::error(unc_jsonrpc_primitives::errors::RpcError::parse_error("Transport error: unexpected server response".to_string()))
231 ),
232 )));
233 }
234 }
235 Err(
236 err @ unc_jsonrpc_client::errors::JsonRpcError::ServerError(
237 unc_jsonrpc_client::errors::JsonRpcServerError::HandlerError(
238 unc_jsonrpc_primitives::types::query::RpcQueryError::UnknownAccessKey {
239 ..
240 },
241 ),
242 ),
243 ) => {
244 return Err(err);
245 }
246 Err(unc_jsonrpc_client::errors::JsonRpcError::TransportError(err)) => {
247 eprintln!("\nAccount information ({}) cannot be fetched on <{}> network due to connectivity issue.",
248 account_id, network_config.network_name
249 );
250 if !need_check_account() {
251 return Err(unc_jsonrpc_client::errors::JsonRpcError::TransportError(
252 err,
253 ));
254 }
255 }
256 Err(unc_jsonrpc_client::errors::JsonRpcError::ServerError(err)) => {
257 eprintln!("\nAccount information ({}) cannot be fetched on <{}> network due to server error.",
258 account_id, network_config.network_name
259 );
260 if !need_check_account() {
261 return Err(unc_jsonrpc_client::errors::JsonRpcError::ServerError(err));
262 }
263 }
264 }
265 }
266}
267
268pub fn is_account_exist(
269 networks: &linked_hash_map::LinkedHashMap<String, crate::config::NetworkConfig>,
270 account_id: unc_primitives::types::AccountId,
271) -> bool {
272 for (_, network_config) in networks {
273 if get_account_state(
274 network_config.clone(),
275 account_id.clone(),
276 unc_primitives::types::Finality::Final.into(),
277 )
278 .is_ok()
279 {
280 return true;
281 }
282 }
283 false
284}
285
286pub fn find_network_where_account_exist(
287 context: &crate::GlobalContext,
288 new_account_id: unc_primitives::types::AccountId,
289) -> Option<crate::config::NetworkConfig> {
290 for (_, network_config) in context.config.network_connection.iter() {
291 if get_account_state(
292 network_config.clone(),
293 new_account_id.clone(),
294 unc_primitives::types::BlockReference::latest(),
295 )
296 .is_ok()
297 {
298 return Some(network_config.clone());
299 }
300 }
301 None
302}
303
304pub fn ask_if_different_account_id_wanted() -> color_eyre::eyre::Result<bool> {
305 #[derive(strum_macros::Display, PartialEq)]
306 enum ConfirmOptions {
307 #[strum(to_string = "Yes, I want to enter a new name for account ID.")]
308 Yes,
309 #[strum(to_string = "No, I want to keep using this name for account ID.")]
310 No,
311 }
312 let select_choose_input = Select::new(
313 "Do you want to enter a different name for the new account ID?",
314 vec![ConfirmOptions::Yes, ConfirmOptions::No],
315 )
316 .prompt()?;
317 Ok(select_choose_input == ConfirmOptions::Yes)
318}
319
320pub fn get_account_state(
321 network_config: crate::config::NetworkConfig,
322 account_id: unc_primitives::types::AccountId,
323 block_reference: BlockReference,
324) -> color_eyre::eyre::Result<
325 unc_primitives::views::AccountView,
326 unc_jsonrpc_client::errors::JsonRpcError<unc_jsonrpc_primitives::types::query::RpcQueryError>,
327> {
328 loop {
329 let query_view_method_response = network_config
330 .json_rpc_client()
331 .blocking_call_view_account(&account_id.clone(), block_reference.clone());
332 match query_view_method_response {
333 Ok(rpc_query_response) => {
334 if let unc_jsonrpc_primitives::types::query::QueryResponseKind::ViewAccount(
335 account_view,
336 ) = rpc_query_response.kind
337 {
338 return Ok(account_view);
339 } else {
340 return Err(unc_jsonrpc_client::errors::JsonRpcError::TransportError(unc_jsonrpc_client::errors::RpcTransportError::RecvError(
341 unc_jsonrpc_client::errors::JsonRpcTransportRecvError::UnexpectedServerResponse(
342 unc_jsonrpc_primitives::message::Message::error(unc_jsonrpc_primitives::errors::RpcError::parse_error("Transport error: unexpected server response".to_string()))
343 ),
344 )));
345 }
346 }
347 Err(
348 err @ unc_jsonrpc_client::errors::JsonRpcError::ServerError(
349 unc_jsonrpc_client::errors::JsonRpcServerError::HandlerError(
350 unc_jsonrpc_primitives::types::query::RpcQueryError::UnknownAccount {
351 ..
352 },
353 ),
354 ),
355 ) => {
356 return Err(err);
357 }
358 Err(unc_jsonrpc_client::errors::JsonRpcError::TransportError(err)) => {
359 eprintln!("\nAccount information ({}) cannot be fetched on <{}> network due to connectivity issue.",
360 account_id, network_config.network_name
361 );
362 if !need_check_account() {
363 return Err(unc_jsonrpc_client::errors::JsonRpcError::TransportError(
364 err,
365 ));
366 }
367 }
368 Err(unc_jsonrpc_client::errors::JsonRpcError::ServerError(err)) => {
369 eprintln!("\nAccount information ({}) cannot be fetched on <{}> network due to server error.",
370 account_id, network_config.network_name
371 );
372 if !need_check_account() {
373 return Err(unc_jsonrpc_client::errors::JsonRpcError::ServerError(err));
374 }
375 }
376 }
377 }
378}
379
380fn need_check_account() -> bool {
381 #[derive(strum_macros::Display, PartialEq)]
382 enum ConfirmOptions {
383 #[strum(to_string = "Yes, I want to check the account again.")]
384 Yes,
385 #[strum(to_string = "No, I want to skip the check and use the specified account ID.")]
386 No,
387 }
388 let select_choose_input = Select::new(
389 "Do you want to try again?",
390 vec![ConfirmOptions::Yes, ConfirmOptions::No],
391 )
392 .prompt()
393 .unwrap_or(ConfirmOptions::Yes);
394 select_choose_input == ConfirmOptions::Yes
395}
396
397#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
398pub struct KeyPairProperties {
399 pub seed_phrase_hd_path: crate::types::slip10::BIP32Path,
400 pub master_seed_phrase: String,
401 pub implicit_account_id: unc_primitives::types::AccountId,
402 #[serde(rename = "public_key")]
403 pub public_key_str: String,
404 #[serde(rename = "private_key")]
405 pub secret_keypair_str: String,
406}
407
408pub fn get_key_pair_properties_from_seed_phrase(
409 seed_phrase_hd_path: crate::types::slip10::BIP32Path,
410 master_seed_phrase: String,
411) -> color_eyre::eyre::Result<KeyPairProperties> {
412 let master_seed = bip39::Mnemonic::parse(&master_seed_phrase)?.to_seed("");
413 let derived_private_key = slip10::derive_key_from_path(
414 &master_seed,
415 slip10::Curve::Ed25519,
416 &seed_phrase_hd_path.clone().into(),
417 )
418 .map_err(|err| {
419 color_eyre::Report::msg(format!(
420 "Failed to derive a key from the master key: {}",
421 err
422 ))
423 })?;
424
425 let secret_keypair = {
426 let secret = ed25519_dalek::SecretKey::from_bytes(&derived_private_key.key)?;
427 let public = ed25519_dalek::PublicKey::from(&secret);
428 ed25519_dalek::Keypair { secret, public }
429 };
430
431 let implicit_account_id =
432 unc_primitives::types::AccountId::try_from(hex::encode(secret_keypair.public))?;
433 let public_key_str = format!(
434 "ed25519:{}",
435 bs58::encode(&secret_keypair.public).into_string()
436 );
437 let secret_keypair_str = format!(
438 "ed25519:{}",
439 bs58::encode(secret_keypair.to_bytes()).into_string()
440 );
441 let key_pair_properties: KeyPairProperties = KeyPairProperties {
442 seed_phrase_hd_path,
443 master_seed_phrase,
444 implicit_account_id,
445 public_key_str,
446 secret_keypair_str,
447 };
448 Ok(key_pair_properties)
449}
450
451pub fn get_public_key_from_seed_phrase(
452 seed_phrase_hd_path: slip10::BIP32Path,
453 master_seed_phrase: &str,
454) -> color_eyre::eyre::Result<unc_crypto::PublicKey> {
455 let master_seed = bip39::Mnemonic::parse(master_seed_phrase)?.to_seed("");
456 let derived_private_key =
457 slip10::derive_key_from_path(&master_seed, slip10::Curve::Ed25519, &seed_phrase_hd_path)
458 .map_err(|err| {
459 color_eyre::Report::msg(format!(
460 "Failed to derive a key from the master key: {}",
461 err
462 ))
463 })?;
464 let secret_keypair = {
465 let secret = ed25519_dalek::SecretKey::from_bytes(&derived_private_key.key)?;
466 let public = ed25519_dalek::PublicKey::from(&secret);
467 ed25519_dalek::Keypair { secret, public }
468 };
469 let public_key_str = format!(
470 "ed25519:{}",
471 bs58::encode(&secret_keypair.public).into_string()
472 );
473 Ok(unc_crypto::PublicKey::from_str(&public_key_str)?)
474}
475
476pub fn generate_ed25519_keypair() -> color_eyre::eyre::Result<KeyPairProperties> {
477 let generate_keypair: crate::utils_command::generate_keypair_subcommand::CliGenerateKeypair =
478 crate::utils_command::generate_keypair_subcommand::CliGenerateKeypair::default();
479 let (master_seed_phrase, master_seed) =
480 if let Some(master_seed_phrase) = generate_keypair.master_seed_phrase.as_deref() {
481 (
482 master_seed_phrase.to_owned(),
483 bip39::Mnemonic::parse(master_seed_phrase)?.to_seed(""),
484 )
485 } else {
486 let mnemonic =
487 bip39::Mnemonic::generate(generate_keypair.new_master_seed_phrase_words_count)?;
488 let master_seed_phrase = mnemonic.word_iter().collect::<Vec<&str>>().join(" ");
489 (master_seed_phrase, mnemonic.to_seed(""))
490 };
491
492 let derived_private_key = slip10::derive_key_from_path(
493 &master_seed,
494 slip10::Curve::Ed25519,
495 &generate_keypair.seed_phrase_hd_path.clone().into(),
496 )
497 .map_err(|err| {
498 color_eyre::Report::msg(format!(
499 "Failed to derive a key from the master key: {}",
500 err
501 ))
502 })?;
503
504 let secret_keypair = {
505 let secret = ed25519_dalek::SecretKey::from_bytes(&derived_private_key.key)?;
506 let public = ed25519_dalek::PublicKey::from(&secret);
507 ed25519_dalek::Keypair { secret, public }
508 };
509
510 let implicit_account_id =
511 unc_primitives::types::AccountId::try_from(hex::encode(secret_keypair.public))?;
512 let public_key_str = format!(
513 "ed25519:{}",
514 bs58::encode(&secret_keypair.public).into_string()
515 );
516 let secret_keypair_str = format!(
517 "ed25519:{}",
518 bs58::encode(secret_keypair.to_bytes()).into_string()
519 );
520 let key_pair_properties: KeyPairProperties = KeyPairProperties {
521 seed_phrase_hd_path: generate_keypair.seed_phrase_hd_path,
522 master_seed_phrase,
523 implicit_account_id,
524 public_key_str,
525 secret_keypair_str,
526 };
527 Ok(key_pair_properties)
528}
529
530
531pub const RAW_PUBLIC_KEY_RSA_2048_LENGTH: usize = 294;
533
534pub fn generate_rsa2048_keypair() -> color_eyre::eyre::Result<KeyPairProperties> {
536 let generate_keypair: crate::utils_command::generate_keypair_subcommand::CliGenerateKeypair =
537 crate::utils_command::generate_keypair_subcommand::CliGenerateKeypair::default();
538 let (master_seed_phrase, _master_seed) =
539 if let Some(master_seed_phrase) = generate_keypair.master_seed_phrase.as_deref() {
540 (
541 master_seed_phrase.to_owned(),
542 bip39::Mnemonic::parse(master_seed_phrase)?.to_seed(""),
543 )
544 } else {
545 let mnemonic =
546 bip39::Mnemonic::generate(generate_keypair.new_master_seed_phrase_words_count)?;
547 let master_seed_phrase = mnemonic.word_iter().collect::<Vec<&str>>().join(" ");
548 (master_seed_phrase, mnemonic.to_seed(""))
549 };
550
551 let mut rng = rand::thread_rng();
552 let bits = 2048;
553 let priv_key = RsaPrivateKey::new(&mut rng, bits)?;
554 let pub_key = RsaPublicKey::from(&priv_key);
555
556 let implicit_account_id =
557 unc_primitives::types::AccountId::try_from(format!("test{}", rng.gen_range(0..10000)))?;
558
559 let der_pk_encoded = pub_key.to_public_key_der().unwrap();
560 let public_key_str = format!(
561 "rsa2048:{}",
562 bs58::encode(&der_pk_encoded.as_bytes()).into_string()
563 );
564
565 let der_sk_encoded = priv_key.to_pkcs8_der().unwrap().to_bytes();
566 let secret_keypair_str = format!(
567 "rsa2048:{}",
568 bs58::encode(der_sk_encoded.as_slice()).into_string()
569 );
570 let key_pair_properties: KeyPairProperties = KeyPairProperties {
571 seed_phrase_hd_path: generate_keypair.seed_phrase_hd_path,
572 master_seed_phrase,
573 implicit_account_id,
574 public_key_str,
575 secret_keypair_str,
576 };
577 Ok(key_pair_properties)
578}
579
580pub fn print_full_signed_transaction(transaction: unc_primitives::transaction::SignedTransaction) {
581 eprintln!("{:<25} {}\n", "signature:", transaction.signature);
582 crate::common::print_full_unsigned_transaction(transaction.transaction);
583}
584
585pub fn print_full_unsigned_transaction(transaction: unc_primitives::transaction::Transaction) {
586 eprintln!(
587 "Unsigned transaction hash (Base58-encoded SHA-256 hash): {}\n\n",
588 transaction.get_hash_and_size().0
589 );
590
591 eprintln!("{:<13} {}", "public_key:", &transaction.public_key);
592 eprintln!("{:<13} {}", "nonce:", &transaction.nonce);
593 eprintln!("{:<13} {}", "block_hash:", &transaction.block_hash);
594
595 let prepopulated = crate::commands::PrepopulatedTransaction::from(transaction);
596 print_unsigned_transaction(&prepopulated);
597}
598
599pub fn print_unsigned_transaction(transaction: &crate::commands::PrepopulatedTransaction) {
600 eprintln!("{:<13} {}", "signer_id:", &transaction.signer_id);
601 eprintln!("{:<13} {}", "receiver_id:", &transaction.receiver_id);
602 if transaction
603 .actions
604 .iter()
605 .any(|action| matches!(action, unc_primitives::transaction::Action::Delegate(_)))
606 {
607 eprintln!("signed delegate action:");
608 } else {
609 eprintln!("actions:");
610 };
611
612 for action in &transaction.actions {
613 match action {
614 unc_primitives::transaction::Action::CreateAccount(_) => {
615 eprintln!(
616 "{:>5} {:<20} {}",
617 "--", "create account:", &transaction.receiver_id
618 )
619 }
620 unc_primitives::transaction::Action::DeployContract(_) => {
621 eprintln!("{:>5} {:<20}", "--", "deploy contract")
622 }
623 unc_primitives::transaction::Action::FunctionCall(function_call_action) => {
624 eprintln!("{:>5} {:<20}", "--", "function call:");
625 eprintln!(
626 "{:>18} {:<13} {}",
627 "", "method name:", &function_call_action.method_name
628 );
629 eprintln!(
630 "{:>18} {:<13} {}",
631 "",
632 "args:",
633 match serde_json::from_slice::<serde_json::Value>(&function_call_action.args) {
634 Ok(parsed_args) => {
635 serde_json::to_string_pretty(&parsed_args)
636 .unwrap_or_else(|_| "".to_string())
637 .replace('\n', "\n ")
638 }
639 Err(_) => {
640 if let Ok(args) = String::from_utf8(function_call_action.args.clone()) {
641 args
642 } else {
643 format!(
644 "<non-printable data ({})>",
645 bytesize::ByteSize(function_call_action.args.len() as u64)
646 )
647 }
648 }
649 }
650 );
651 eprintln!(
652 "{:>18} {:<13} {}",
653 "",
654 "gas:",
655 crate::common::UncGas::from_gas(function_call_action.gas)
656 );
657 eprintln!(
658 "{:>18} {:<13} {}",
659 "",
660 "deposit:",
661 crate::types::unc_token::UncToken::from_attounc(
662 function_call_action.deposit
663 )
664 );
665 }
666 unc_primitives::transaction::Action::Transfer(transfer_action) => {
667 eprintln!(
668 "{:>5} {:<20} {}",
669 "--",
670 "transfer deposit:",
671 crate::types::unc_token::UncToken::from_attounc(transfer_action.deposit)
672 );
673 }
674 unc_primitives::transaction::Action::Pledge(pledge_action) => {
675 eprintln!("{:>5} {:<20}", "--", "pledge:");
676 eprintln!(
677 "{:>18} {:<13} {}",
678 "", "public key:", &pledge_action.public_key
679 );
680 eprintln!(
681 "{:>18} {:<13} {}",
682 "",
683 "pledge:",
684 crate::types::unc_token::UncToken::from_attounc(pledge_action.pledge)
685 );
686 }
687 unc_primitives::transaction::Action::AddKey(add_key_action) => {
688 eprintln!("{:>5} {:<20}", "--", "add access key:");
689 eprintln!(
690 "{:>18} {:<13} {}",
691 "", "public key:", &add_key_action.public_key
692 );
693 eprintln!(
694 "{:>18} {:<13} {}",
695 "", "nonce:", &add_key_action.access_key.nonce
696 );
697 eprintln!(
698 "{:>18} {:<13} {:?}",
699 "", "permission:", &add_key_action.access_key.permission
700 );
701 }
702 unc_primitives::transaction::Action::DeleteKey(delete_key_action) => {
703 eprintln!("{:>5} {:<20}", "--", "delete access key:");
704 eprintln!(
705 "{:>18} {:<13} {}",
706 "", "public key:", &delete_key_action.public_key
707 );
708 }
709 unc_primitives::transaction::Action::DeleteAccount(delete_account_action) => {
710 eprintln!(
711 "{:>5} {:<20} {}",
712 "--", "delete account:", &transaction.receiver_id
713 );
714 eprintln!(
715 "{:>5} {:<20} {}",
716 "", "beneficiary id:", &delete_account_action.beneficiary_id
717 );
718 }
719 unc_primitives::transaction::Action::Delegate(signed_delegate_action) => {
720 let prepopulated_transaction = crate::commands::PrepopulatedTransaction {
721 signer_id: signed_delegate_action.delegate_action.sender_id.clone(),
722 receiver_id: signed_delegate_action.delegate_action.receiver_id.clone(),
723 actions: signed_delegate_action.delegate_action.get_actions(),
724 };
725 print_unsigned_transaction(&prepopulated_transaction);
726 }
727 unc_primitives::transaction::Action::RegisterRsa2048Keys(register_rsa2048_action) => {
728 eprintln!("{:>5} {:<20}", "--", "register rsa2048 key:");
729 eprintln!(
730 "{:>18} {:<13} {}",
731 "", "public key:", ®ister_rsa2048_action.public_key
732 );
733 eprintln!(
734 "{:>18} {:<13} {}",
735 "", "op type:", ®ister_rsa2048_action.operation_type
736 );
737 },
738 unc_primitives::transaction::Action::CreateRsa2048Challenge(create_rsa2048keys_challenge_action) => {
739 eprintln!(
740 "{:>18} {:<13} {}",
741 "", "public key:", &create_rsa2048keys_challenge_action.public_key
742 );
743 eprintln!(
744 "{:>18} {:<13} {}",
745 "",
746 "args:",
747 match serde_json::from_slice::<serde_json::Value>(&create_rsa2048keys_challenge_action.args) {
748 Ok(parsed_args) => {
749 serde_json::to_string_pretty(&parsed_args)
750 .unwrap_or_else(|_| "".to_string())
751 .replace('\n', "\n ")
752 }
753 Err(_) => {
754 format!(
755 "<non-printable data ({})>",
756 bytesize::ByteSize(create_rsa2048keys_challenge_action.args.len() as u64)
757 )
758 }
759 }
760 );
761 },
762 }
763 }
764}
765
766fn print_value_successful_transaction(
767 transaction_info: unc_primitives::views::FinalExecutionOutcomeView,
768) {
769 for action in transaction_info.transaction.actions {
770 match action {
771 unc_primitives::views::ActionView::CreateAccount => {
772 eprintln!(
773 "New account <{}> has been successfully created.",
774 transaction_info.transaction.receiver_id,
775 );
776 }
777 unc_primitives::views::ActionView::DeployContract { code: _ } => {
778 eprintln!("Contract code has been successfully deployed.",);
779 }
780 unc_primitives::views::ActionView::FunctionCall {
781 method_name,
782 args: _,
783 gas: _,
784 deposit: _,
785 } => {
786 eprintln!(
787 "The \"{}\" call to <{}> on behalf of <{}> succeeded.",
788 method_name,
789 transaction_info.transaction.receiver_id,
790 transaction_info.transaction.signer_id,
791 );
792 }
793 unc_primitives::views::ActionView::Transfer { deposit } => {
794 eprintln!(
795 "<{}> has transferred {} to <{}> successfully.",
796 transaction_info.transaction.signer_id,
797 crate::types::unc_token::UncToken::from_attounc(deposit),
798 transaction_info.transaction.receiver_id,
799 );
800 }
801 unc_primitives::views::ActionView::Pledge {
802 pledge,
803 public_key: _,
804 } => {
805 if pledge == 0 {
806 eprintln!(
807 "Validator <{}> successfully unpledged.",
808 transaction_info.transaction.signer_id,
809 );
810 } else {
811 eprintln!(
812 "Validator <{}> has successfully pledged {}.",
813 transaction_info.transaction.signer_id,
814 crate::types::unc_token::UncToken::from_attounc(pledge),
815 );
816 }
817 }
818 unc_primitives::views::ActionView::AddKey {
819 public_key,
820 access_key: _,
821 } => {
822 eprintln!(
823 "Added access key = {} to {}.",
824 public_key, transaction_info.transaction.receiver_id,
825 );
826 }
827 unc_primitives::views::ActionView::DeleteKey { public_key } => {
828 eprintln!(
829 "Access key <{}> for account <{}> has been successfully deleted.",
830 public_key, transaction_info.transaction.signer_id,
831 );
832 }
833 unc_primitives::views::ActionView::DeleteAccount { beneficiary_id: _ } => {
834 eprintln!(
835 "Account <{}> has been successfully deleted.",
836 transaction_info.transaction.signer_id,
837 );
838 }
839 unc_primitives::views::ActionView::Delegate {
840 delegate_action,
841 signature: _,
842 } => {
843 eprintln!(
844 "Actions delegated for <{}> completed successfully.",
845 delegate_action.sender_id,
846 );
847 }
848 unc_primitives::views::ActionView::RegisterRsa2048Keys { public_key, operation_type, args: _, } => {
849 eprintln!(
850 "Rsa2048 key <{}>, op_type <{}> for account <{}> has been successfully registered.",
851 public_key, operation_type, transaction_info.transaction.signer_id,
852 );
853 },
854 unc_primitives::views::ActionView::CreateRsa2048Challenge { public_key, challenge_key, args: _, } => {
855 eprintln!(
856 "Rsa2048 <{}> with ChallengeKey <{}> for account <{}> has been successfully challenge created.",
857 public_key, challenge_key, transaction_info.transaction.signer_id,
858 );
859 },
860 }
861 }
862}
863
864pub fn rpc_transaction_error(
865 err: unc_jsonrpc_client::errors::JsonRpcError<
866 unc_jsonrpc_client::methods::broadcast_tx_commit::RpcTransactionError,
867 >,
868) -> CliResult {
869 match &err {
870 unc_jsonrpc_client::errors::JsonRpcError::TransportError(_rpc_transport_error) => {
871 eprintln!("Transport error transaction.\nPlease wait. The next try to send this transaction is happening right now ...");
872 }
873 unc_jsonrpc_client::errors::JsonRpcError::ServerError(rpc_server_error) => match rpc_server_error {
874 unc_jsonrpc_client::errors::JsonRpcServerError::HandlerError(rpc_transaction_error) => match rpc_transaction_error {
875 unc_jsonrpc_client::methods::broadcast_tx_commit::RpcTransactionError::TimeoutError => {
876 eprintln!("Timeout error transaction.\nPlease wait. The next try to send this transaction is happening right now ...");
877 }
878 unc_jsonrpc_client::methods::broadcast_tx_commit::RpcTransactionError::InvalidTransaction { context } => {
879 return handler_invalid_tx_error(context);
880 }
881 unc_jsonrpc_client::methods::broadcast_tx_commit::RpcTransactionError::DoesNotTrackShard => {
882 return color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!("RPC Server Error: {}", err));
883 }
884 unc_jsonrpc_client::methods::broadcast_tx_commit::RpcTransactionError::RequestRouted{transaction_hash} => {
885 return color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!("RPC Server Error for transaction with hash {}\n{}", transaction_hash, err));
886 }
887 unc_jsonrpc_client::methods::broadcast_tx_commit::RpcTransactionError::UnknownTransaction{requested_transaction_hash} => {
888 return color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!("RPC Server Error for transaction with hash {}\n{}", requested_transaction_hash, err));
889 }
890 unc_jsonrpc_client::methods::broadcast_tx_commit::RpcTransactionError::InternalError{debug_info} => {
891 return color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!("RPC Server Error: {}", debug_info));
892 }
893 }
894 unc_jsonrpc_client::errors::JsonRpcServerError::RequestValidationError(rpc_request_validation_error) => {
895 return color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!("Incompatible request with the server: {:#?}", rpc_request_validation_error));
896 }
897 unc_jsonrpc_client::errors::JsonRpcServerError::InternalError{ info } => {
898 eprintln!("Internal server error: {}.\nPlease wait. The next try to send this transaction is happening right now ...", info.clone().unwrap_or_default());
899 }
900 unc_jsonrpc_client::errors::JsonRpcServerError::NonContextualError(rpc_error) => {
901 return color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!("Unexpected response: {}", rpc_error));
902 }
903 unc_jsonrpc_client::errors::JsonRpcServerError::ResponseStatusError(json_rpc_server_response_status_error) => match json_rpc_server_response_status_error {
904 unc_jsonrpc_client::errors::JsonRpcServerResponseStatusError::Unauthorized => {
905 return color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!("JSON RPC server requires authentication. Please, authenticate unc CLI with the JSON RPC server you use."));
906 }
907 unc_jsonrpc_client::errors::JsonRpcServerResponseStatusError::TooManyRequests => {
908 eprintln!("JSON RPC server is currently busy.\nPlease wait. The next try to send this transaction is happening right now ...");
909 }
910 unc_jsonrpc_client::errors::JsonRpcServerResponseStatusError::Unexpected{status} => {
911 return color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!("JSON RPC server responded with an unexpected status code: {}", status));
912 }
913 }
914 }
915 }
916 Ok(())
917}
918
919pub fn rpc_async_transaction_error(
920 _err: unc_jsonrpc_client::errors::JsonRpcError<
921 unc_jsonrpc_client::methods::broadcast_tx_async::RpcBroadcastTxAsyncError,
922 >,
923) -> CliResult {
924 Ok(())
925}
926
927pub fn print_action_error(action_error: &unc_primitives::errors::ActionError) -> crate::CliResult {
928 match &action_error.kind {
929 unc_primitives::errors::ActionErrorKind::AccountAlreadyExists { account_id } => {
930 color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!("Error: Create Account action tries to create an account with account ID <{}> which already exists in the storage.", account_id))
931 }
932 unc_primitives::errors::ActionErrorKind::AccountDoesNotExist { account_id } => {
933 color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!(
934 "Error: TX receiver ID <{}> doesn't exist (but action is not \"Create Account\").",
935 account_id
936 ))
937 }
938 unc_primitives::errors::ActionErrorKind::CreateAccountOnlyByRegistrar {
939 account_id: _,
940 registrar_account_id: _,
941 predecessor_id: _,
942 } => {
943 color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!("Error: A top-level account ID can only be created by registrar."))
944 }
945 unc_primitives::errors::ActionErrorKind::CreateAccountNotAllowed {
946 account_id,
947 predecessor_id,
948 } => {
949 color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!("Error: A newly created account <{}> must be under a namespace of the creator account <{}>.", account_id, predecessor_id))
950 }
951 unc_primitives::errors::ActionErrorKind::ActorNoPermission {
952 account_id: _,
953 actor_id: _,
954 } => {
955 color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!("Error: Administrative actions can be proceed only if sender=receiver or the first TX action is a \"Create Account\" action."))
956 }
957 unc_primitives::errors::ActionErrorKind::DeleteKeyDoesNotExist {
958 account_id,
959 public_key,
960 } => {
961 color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!(
962 "Error: Account <{}> tries to remove an access key <{}> that doesn't exist.",
963 account_id, public_key
964 ))
965 }
966 unc_primitives::errors::ActionErrorKind::AddKeyAlreadyExists {
967 account_id,
968 public_key,
969 } => {
970 color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!(
971 "Error: Public key <{}> is already used for an existing account ID <{}>.",
972 public_key, account_id
973 ))
974 }
975 unc_primitives::errors::ActionErrorKind::DeleteAccountPledging { account_id } => {
976 color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!(
977 "Error: Account <{}> is pledging and can not be deleted",
978 account_id
979 ))
980 }
981 unc_primitives::errors::ActionErrorKind::LackBalanceForState { account_id, amount } => {
982 color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!("Error: Receipt action can't be completed, because the remaining balance will not be enough to cover storage.\nAn account which needs balance: <{}>\nBalance required to complete the action: <{}>",
983 account_id,
984 crate::types::unc_token::UncToken::from_attounc(*amount)
985 ))
986 }
987 unc_primitives::errors::ActionErrorKind::TriesToUnpledge { account_id } => {
988 color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!(
989 "Error: Account <{}> is not yet pledged, but tries to unpledge.",
990 account_id
991 ))
992 }
993 unc_primitives::errors::ActionErrorKind::TriesToPledge {
994 account_id,
995 pledge,
996 pledging: _,
997 balance,
998 } => {
999 color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!(
1000 "Error: Account <{}> doesn't have enough balance ({}) to increase the pledge ({}).",
1001 account_id,
1002 crate::types::unc_token::UncToken::from_attounc(*balance),
1003 crate::types::unc_token::UncToken::from_attounc(*pledge)
1004 ))
1005 }
1006 unc_primitives::errors::ActionErrorKind::InsufficientPledge {
1007 account_id: _,
1008 pledge,
1009 minimum_pledge,
1010 } => {
1011 color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!(
1012 "Error: Insufficient pledge {}.\nThe minimum rate must be {}.",
1013 crate::types::unc_token::UncToken::from_attounc(*pledge),
1014 crate::types::unc_token::UncToken::from_attounc(*minimum_pledge)
1015 ))
1016 }
1017 unc_primitives::errors::ActionErrorKind::FunctionCallError(function_call_error_ser) => {
1018 color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!("Error: An error occurred during a `FunctionCall` Action, parameter is debug message.\n{:?}", function_call_error_ser))
1019 }
1020 unc_primitives::errors::ActionErrorKind::NewReceiptValidationError(
1021 receipt_validation_error,
1022 ) => {
1023 color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!("Error: Error occurs when a new `ActionReceipt` created by the `FunctionCall` action fails.\n{:?}", receipt_validation_error))
1024 }
1025 unc_primitives::errors::ActionErrorKind::OnlyImplicitAccountCreationAllowed {
1026 account_id: _,
1027 } => {
1028 color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!("Error: `CreateAccount` action is called on hex-characters account of length 64.\nSee implicit account creation NEP: https://github.com/unc/NEPs/pull/71"))
1029 }
1030 unc_primitives::errors::ActionErrorKind::DeleteAccountWithLargeState { account_id } => {
1031 color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!(
1032 "Error: Delete account <{}> whose state is large is temporarily banned.",
1033 account_id
1034 ))
1035 }
1036 unc_primitives::errors::ActionErrorKind::DelegateActionInvalidSignature => {
1037 color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!("Error: Invalid Signature on DelegateAction"))
1038 }
1039 unc_primitives::errors::ActionErrorKind::DelegateActionSenderDoesNotMatchTxReceiver {
1040 sender_id,
1041 receiver_id,
1042 } => {
1043 color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!("Error: Delegate Action sender {sender_id} does not match transaction receiver {receiver_id}"))
1044 }
1045 unc_primitives::errors::ActionErrorKind::DelegateActionExpired => {
1046 color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!("Error: DelegateAction Expired"))
1047 }
1048 unc_primitives::errors::ActionErrorKind::DelegateActionAccessKeyError(_) => {
1049 color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!("Error: The given public key doesn't exist for the sender"))
1050 }
1051 unc_primitives::errors::ActionErrorKind::DelegateActionInvalidNonce {
1052 delegate_nonce,
1053 ak_nonce,
1054 } => {
1055 color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!("Error: DelegateAction Invalid Delegate Nonce: {delegate_nonce} ak_nonce: {ak_nonce}"))
1056 }
1057 unc_primitives::errors::ActionErrorKind::DelegateActionNonceTooLarge {
1058 delegate_nonce,
1059 upper_bound,
1060 } => {
1061 color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!("Error: DelegateAction Invalid Delegate Nonce: {delegate_nonce} upper bound: {upper_bound}"))
1062 }
1063 unc_primitives::errors::ActionErrorKind::RsaKeysNotFound { account_id, public_key } => {
1064 color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!("Error: RSA key not found for account <{}> and public key <{}>.", account_id, public_key))
1065 },
1066 }
1067}
1068
1069pub fn handler_invalid_tx_error(
1070 invalid_tx_error: &unc_primitives::errors::InvalidTxError,
1071) -> crate::CliResult {
1072 match invalid_tx_error {
1073 unc_primitives::errors::InvalidTxError::InvalidAccessKeyError(invalid_access_key_error) => {
1074 match invalid_access_key_error {
1075 unc_primitives::errors::InvalidAccessKeyError::AccessKeyNotFound{account_id, public_key} => {
1076 color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!("Error: Public key {} doesn't exist for the account <{}>.", public_key, account_id))
1077 },
1078 unc_primitives::errors::InvalidAccessKeyError::ReceiverMismatch{tx_receiver, ak_receiver} => {
1079 color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!("Error: Transaction for <{}> doesn't match the access key for <{}>.", tx_receiver, ak_receiver))
1080 },
1081 unc_primitives::errors::InvalidAccessKeyError::MethodNameMismatch{method_name} => {
1082 color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!("Error: Transaction method name <{}> isn't allowed by the access key.", method_name))
1083 },
1084 unc_primitives::errors::InvalidAccessKeyError::RequiresFullAccess => {
1085 color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!("Error: Transaction requires a full permission access key."))
1086 },
1087 unc_primitives::errors::InvalidAccessKeyError::NotEnoughAllowance{account_id, public_key, allowance, cost} => {
1088 color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!("Error: Access Key <{}> for account <{}> does not have enough allowance ({}) to cover transaction cost ({}).",
1089 public_key,
1090 account_id,
1091 crate::types::unc_token::UncToken::from_attounc(*allowance),
1092 crate::types::unc_token::UncToken::from_attounc(*cost)
1093 ))
1094 },
1095 unc_primitives::errors::InvalidAccessKeyError::DepositWithFunctionCall => {
1096 color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!("Error: Having a deposit with a function call action is not allowed with a function call access key."))
1097 }
1098 }
1099 },
1100 unc_primitives::errors::InvalidTxError::InvalidSignerId { signer_id } => {
1101 color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!("Error: TX signer ID <{}> is not in a valid format or does not satisfy requirements\nSee \"unc_runtime_utils::utils::is_valid_account_id\".", signer_id))
1102 },
1103 unc_primitives::errors::InvalidTxError::SignerDoesNotExist { signer_id } => {
1104 color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!("Error: TX signer ID <{}> is not found in the storage.", signer_id))
1105 },
1106 unc_primitives::errors::InvalidTxError::InvalidNonce { tx_nonce, ak_nonce } => {
1107 color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!("Error: Transaction nonce ({}) must be account[access_key].nonce ({}) + 1.", tx_nonce, ak_nonce))
1108 },
1109 unc_primitives::errors::InvalidTxError::NonceTooLarge { tx_nonce, upper_bound } => {
1110 color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!("Error: Transaction nonce ({}) is larger than the upper bound ({}) given by the block height.", tx_nonce, upper_bound))
1111 },
1112 unc_primitives::errors::InvalidTxError::InvalidReceiverId { receiver_id } => {
1113 color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!("Error: TX receiver ID ({}) is not in a valid format or does not satisfy requirements\nSee \"unc_runtime_utils::is_valid_account_id\".", receiver_id))
1114 },
1115 unc_primitives::errors::InvalidTxError::InvalidSignature => {
1116 color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!("Error: TX signature is not valid"))
1117 },
1118 unc_primitives::errors::InvalidTxError::NotEnoughBalance {signer_id, balance, cost} => {
1119 color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!("Error: Account <{}> does not have enough balance ({}) to cover TX cost ({}).",
1120 signer_id,
1121 crate::types::unc_token::UncToken::from_attounc(*balance),
1122 crate::types::unc_token::UncToken::from_attounc(*cost)
1123 ))
1124 },
1125 unc_primitives::errors::InvalidTxError::LackBalanceForState {signer_id, amount} => {
1126 color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!("Error: Signer account <{}> doesn't have enough balance ({}) after transaction.",
1127 signer_id,
1128 crate::types::unc_token::UncToken::from_attounc(*amount)
1129 ))
1130 },
1131 unc_primitives::errors::InvalidTxError::CostOverflow => {
1132 color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!("Error: An integer overflow occurred during transaction cost estimation."))
1133 },
1134 unc_primitives::errors::InvalidTxError::InvalidChain => {
1135 color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!("Error: Transaction parent block hash doesn't belong to the current chain."))
1136 },
1137 unc_primitives::errors::InvalidTxError::Expired => {
1138 color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!("Error: Transaction has expired."))
1139 },
1140 unc_primitives::errors::InvalidTxError::ActionsValidation(actions_validation_error) => {
1141 match actions_validation_error {
1142 unc_primitives::errors::ActionsValidationError::DeleteActionMustBeFinal => {
1143 color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!("Error: The delete action must be the final action in transaction."))
1144 },
1145 unc_primitives::errors::ActionsValidationError::TotalPrepaidGasExceeded {total_prepaid_gas, limit} => {
1146 color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!("Error: The total prepaid gas ({}) for all given actions exceeded the limit ({}).",
1147 total_prepaid_gas,
1148 limit
1149 ))
1150 },
1151 unc_primitives::errors::ActionsValidationError::TotalNumberOfActionsExceeded {total_number_of_actions, limit} => {
1152 color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!("Error: The number of actions ({}) exceeded the given limit ({}).", total_number_of_actions, limit))
1153 },
1154 unc_primitives::errors::ActionsValidationError::AddKeyMethodNamesNumberOfBytesExceeded {total_number_of_bytes, limit} => {
1155 color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!("Error: The total number of bytes ({}) of the method names exceeded the limit ({}) in a Add Key action.", total_number_of_bytes, limit))
1156 },
1157 unc_primitives::errors::ActionsValidationError::AddKeyMethodNameLengthExceeded {length, limit} => {
1158 color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!("Error: The length ({}) of some method name exceeded the limit ({}) in a Add Key action.", length, limit))
1159 },
1160 unc_primitives::errors::ActionsValidationError::IntegerOverflow => {
1161 color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!("Error: Integer overflow."))
1162 },
1163 unc_primitives::errors::ActionsValidationError::InvalidAccountId {account_id} => {
1164 color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!("Error: Invalid account ID <{}>.", account_id))
1165 },
1166 unc_primitives::errors::ActionsValidationError::ContractSizeExceeded {size, limit} => {
1167 color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!("Error: The size ({}) of the contract code exceeded the limit ({}) in a DeployContract action.", size, limit))
1168 },
1169 unc_primitives::errors::ActionsValidationError::FunctionCallMethodNameLengthExceeded {length, limit} => {
1170 color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!("Error: The length ({}) of the method name exceeded the limit ({}) in a Function Call action.", length, limit))
1171 },
1172 unc_primitives::errors::ActionsValidationError::FunctionCallArgumentsLengthExceeded {length, limit} => {
1173 color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!("Error: The length ({}) of the arguments exceeded the limit ({}) in a Function Call action.", length, limit))
1174 },
1175 unc_primitives::errors::ActionsValidationError::UnsuitablePledgingKey {public_key} => {
1176 color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!("Error: An attempt to pledge with a public key <{}> that is not convertible to ristretto.", public_key))
1177 },
1178 unc_primitives::errors::ActionsValidationError::FunctionCallZeroAttachedGas => {
1179 color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!("Error: The attached amount of gas in a FunctionCall action has to be a positive number."))
1180 }
1181 unc_primitives::errors::ActionsValidationError::DelegateActionMustBeOnlyOne => {
1182 color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!("Error: DelegateActionMustBeOnlyOne"))
1183 }
1184 unc_primitives::errors::ActionsValidationError::UnsupportedProtocolFeature { protocol_feature, version } => {
1185 color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!("Error: Protocol Feature {} is unsupported in version {}", protocol_feature, version))
1186 }
1187 }
1188 },
1189 unc_primitives::errors::InvalidTxError::TransactionSizeExceeded { size, limit } => {
1190 color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!("Error: The size ({}) of serialized transaction exceeded the limit ({}).", size, limit))
1191 }
1192 }
1193}
1194
1195pub fn print_transaction_error(
1196 tx_execution_error: &unc_primitives::errors::TxExecutionError,
1197) -> crate::CliResult {
1198 eprintln!("Failed transaction");
1199 match tx_execution_error {
1200 unc_primitives::errors::TxExecutionError::ActionError(action_error) => {
1201 print_action_error(action_error)
1202 }
1203 unc_primitives::errors::TxExecutionError::InvalidTxError(invalid_tx_error) => {
1204 handler_invalid_tx_error(invalid_tx_error)
1205 }
1206 }
1207}
1208
1209pub fn print_transaction_status(
1210 transaction_info: &unc_primitives::views::FinalExecutionOutcomeView,
1211 network_config: &crate::config::NetworkConfig,
1212) -> crate::CliResult {
1213 eprintln!("--- Logs ---------------------------");
1214 for receipt in transaction_info.receipts_outcome.iter() {
1215 if receipt.outcome.logs.is_empty() {
1216 eprintln!("Logs [{}]: No logs", receipt.outcome.executor_id);
1217 } else {
1218 eprintln!("Logs [{}]:", receipt.outcome.executor_id);
1219 eprintln!(" {}", receipt.outcome.logs.join("\n "));
1220 };
1221 }
1222 match &transaction_info.status {
1223 unc_primitives::views::FinalExecutionStatus::NotStarted
1224 | unc_primitives::views::FinalExecutionStatus::Started => unreachable!(),
1225 unc_primitives::views::FinalExecutionStatus::Failure(tx_execution_error) => {
1226 return print_transaction_error(tx_execution_error);
1227 }
1228 unc_primitives::views::FinalExecutionStatus::SuccessValue(bytes_result) => {
1229 eprintln!("--- Result -------------------------");
1230 if bytes_result.is_empty() {
1231 eprintln!("Empty result");
1232 } else if let Ok(json_result) =
1233 serde_json::from_slice::<serde_json::Value>(bytes_result)
1234 {
1235 println!("{}", serde_json::to_string_pretty(&json_result)?);
1236 } else if let Ok(string_result) = String::from_utf8(bytes_result.clone()) {
1237 println!("{string_result}");
1238 } else {
1239 eprintln!("The returned value is not printable (binary data)");
1240 }
1241 eprintln!("------------------------------------\n");
1242 print_value_successful_transaction(transaction_info.clone())
1243 }
1244 };
1245 eprintln!("Transaction ID: {id}\nTo see the transaction in the transaction explorer, please open this url in your browser:\n{path}{id}\n",
1246 id=transaction_info.transaction_outcome.id,
1247 path=network_config.explorer_transaction_url
1248 );
1249 Ok(())
1250}
1251
1252pub fn print_async_transaction_status(
1253 tx_hash: &CryptoHash,
1254 network_config: &crate::config::NetworkConfig,
1255) -> crate::CliResult {
1256 eprintln!("--- Logs ---------------------------");
1257 eprintln!("Transaction ID: {id}\nTo see the transaction in the transaction explorer, please open this url in your browser:\n{path}{id}\n",
1258 id=tx_hash,
1259 path=network_config.explorer_transaction_url
1260 );
1261 Ok(())
1262}
1263
1264pub fn save_access_key_to_keychain(
1265 network_config: crate::config::NetworkConfig,
1266 key_pair_properties_buf: &str,
1267 public_key_str: &str,
1268 account_id: &str,
1269) -> color_eyre::eyre::Result<String> {
1270 let service_name = std::borrow::Cow::Owned(format!(
1271 "unc-{}-{}",
1272 network_config.network_name, account_id
1273 ));
1274
1275 keyring::Entry::new(&service_name, &format!("{}:{}", account_id, public_key_str))
1276 .wrap_err("Failed to open keychain")?
1277 .set_password(key_pair_properties_buf)
1278 .wrap_err("Failed to save password to keychain")?;
1279
1280 Ok("The data for the access key is saved in the keychain".to_string())
1281}
1282
1283pub fn save_access_key_to_legacy_keychain(
1284 network_config: crate::config::NetworkConfig,
1285 credentials_home_dir: std::path::PathBuf,
1286 key_pair_properties_buf: &str,
1287 public_key_str: &str,
1288 account_id: &str,
1289) -> color_eyre::eyre::Result<String> {
1290 let dir_name = network_config.network_name.as_str();
1291 let file_with_key_name: std::path::PathBuf =
1292 format!("{}.json", public_key_str.replace(':', "_")).into();
1293 let mut path_with_key_name = std::path::PathBuf::from(&credentials_home_dir);
1294 path_with_key_name.push(dir_name);
1295 path_with_key_name.push(account_id);
1296 std::fs::create_dir_all(&path_with_key_name)?;
1297 path_with_key_name.push(file_with_key_name);
1298 let message_1 = if path_with_key_name.exists() {
1299 format!(
1300 "The file: {} already exists! Therefore it was not overwritten.",
1301 &path_with_key_name.display()
1302 )
1303 } else {
1304 std::fs::File::create(&path_with_key_name)
1305 .wrap_err_with(|| format!("Failed to create file: {:?}", path_with_key_name))?
1306 .write(key_pair_properties_buf.as_bytes())
1307 .wrap_err_with(|| format!("Failed to write to file: {:?}", path_with_key_name))?;
1308 format!(
1309 "The data for the access key is saved in a file {}",
1310 &path_with_key_name.display()
1311 )
1312 };
1313
1314 let file_with_account_name: std::path::PathBuf = format!("{}.json", account_id).into();
1315 let mut path_with_account_name = std::path::PathBuf::from(&credentials_home_dir);
1316 path_with_account_name.push(dir_name);
1317 path_with_account_name.push(file_with_account_name);
1318 if path_with_account_name.exists() {
1319 Ok(format!(
1320 "{}\nThe file: {} already exists! Therefore it was not overwritten.",
1321 message_1,
1322 &path_with_account_name.display()
1323 ))
1324 } else {
1325 std::fs::File::create(&path_with_account_name)
1326 .wrap_err_with(|| format!("Failed to create file: {:?}", path_with_account_name))?
1327 .write(key_pair_properties_buf.as_bytes())
1328 .wrap_err_with(|| format!("Failed to write to file: {:?}", path_with_account_name))?;
1329 Ok(format!(
1330 "{}\nThe data for the access key is saved in a file {}",
1331 message_1,
1332 &path_with_account_name.display()
1333 ))
1334 }
1335}
1336
1337pub fn get_config_toml() -> color_eyre::eyre::Result<crate::config::Config> {
1338 if let Some(mut path_config_toml) = dirs::config_dir() {
1339 path_config_toml.extend(&["unc-cli", "config.toml"]);
1340
1341 if !path_config_toml.is_file() {
1342 write_config_toml(crate::config::Config::default())?;
1343 };
1344 let config_toml = std::fs::read_to_string(&path_config_toml)?;
1345 toml::from_str(&config_toml).or_else(|err| {
1346 eprintln!("Warning: `unc` CLI configuration file stored at {path_config_toml:?} could not be parsed due to: {err}");
1347 eprintln!("Note: The default configuration printed below will be used instead:\n");
1348 let default_config = crate::config::Config::default();
1349 eprintln!("{}", toml::to_string(&default_config)?);
1350 Ok(default_config)
1351 })
1352 } else {
1353 Ok(crate::config::Config::default())
1354 }
1355}
1356pub fn write_config_toml(config: crate::config::Config) -> CliResult {
1357 let config_toml = toml::to_string(&config)?;
1358 let mut path_config_toml = dirs::config_dir().wrap_err("Impossible to get your config dir!")?;
1359 path_config_toml.push("unc-cli");
1360 std::fs::create_dir_all(&path_config_toml)?;
1361 path_config_toml.push("config.toml");
1362 std::fs::File::create(&path_config_toml)
1363 .wrap_err_with(|| format!("Failed to create file: {path_config_toml:?}"))?
1364 .write(config_toml.as_bytes())
1365 .wrap_err_with(|| format!("Failed to write to file: {path_config_toml:?}"))?;
1366 eprintln!("Note: `unc` CLI configuration is stored in {path_config_toml:?}");
1367 Ok(())
1368}
1369
1370pub fn try_external_subcommand_execution(error: clap::Error) -> CliResult {
1371 let (subcommand, args) = {
1372 let mut args = std::env::args().skip(1);
1373 let subcommand = args
1374 .next()
1375 .ok_or_else(|| color_eyre::eyre::eyre!("subcommand is not provided"))?;
1376 (subcommand, args.collect::<Vec<String>>())
1377 };
1378 let is_top_level_command_known = crate::commands::TopLevelCommandDiscriminants::iter()
1379 .map(|x| format!("{:?}", &x).to_lowercase())
1380 .any(|x| x == subcommand);
1381 if is_top_level_command_known {
1382 error.exit()
1383 }
1384 let subcommand_exe = format!("unc-{}{}", subcommand, std::env::consts::EXE_SUFFIX);
1385
1386 let path = path_directories()
1387 .iter()
1388 .map(|dir| dir.join(&subcommand_exe))
1389 .find(|file| is_executable(file));
1390
1391 let command = path.ok_or_else(|| {
1392 color_eyre::eyre::eyre!(
1393 "{} command or {} extension does not exist",
1394 subcommand,
1395 subcommand_exe
1396 )
1397 })?;
1398
1399 let err = match cargo_util::ProcessBuilder::new(command)
1400 .args(&args)
1401 .exec_replace()
1402 {
1403 Ok(()) => return Ok(()),
1404 Err(e) => e,
1405 };
1406
1407 if let Some(perr) = err.downcast_ref::<cargo_util::ProcessError>() {
1408 if let Some(code) = perr.code {
1409 return Err(color_eyre::eyre::eyre!("perror occurred, code: {}", code));
1410 }
1411 }
1412 Err(color_eyre::eyre::eyre!(err))
1413}
1414
1415fn is_executable<P: AsRef<std::path::Path>>(path: P) -> bool {
1416 #[cfg(target_family = "unix")]
1417 {
1418 use std::os::unix::prelude::*;
1419 std::fs::metadata(path)
1420 .map(|metadata| metadata.is_file() && metadata.permissions().mode() & 0o111 != 0)
1421 .unwrap_or(false)
1422 }
1423 #[cfg(target_family = "windows")]
1424 path.as_ref().is_file()
1425}
1426
1427fn path_directories() -> Vec<std::path::PathBuf> {
1428 if let Some(val) = std::env::var_os("PATH") {
1429 std::env::split_paths(&val).collect()
1430 } else {
1431 Vec::new()
1432 }
1433}
1434
1435pub fn get_delegated_validator_list_from_mainnet(
1436 network_connection: &linked_hash_map::LinkedHashMap<String, crate::config::NetworkConfig>,
1437) -> color_eyre::eyre::Result<std::collections::BTreeSet<unc_primitives::types::AccountId>> {
1438 let network_config = network_connection
1439 .get("mainnet")
1440 .wrap_err("There is no 'mainnet' network in your configuration.")?;
1441
1442 let epoch_validator_info = network_config
1443 .json_rpc_client()
1444 .blocking_call(
1445 &unc_jsonrpc_client::methods::validators::RpcValidatorRequest {
1446 epoch_reference: unc_primitives::types::EpochReference::Latest,
1447 },
1448 )
1449 .wrap_err("Failed to get epoch validators information request.")?;
1450
1451 Ok(epoch_validator_info
1452 .current_pledge_proposals
1453 .into_iter()
1454 .map(|current_proposal| current_proposal.take_account_id())
1455 .chain(
1456 epoch_validator_info
1457 .current_validators
1458 .into_iter()
1459 .map(|current_validator| current_validator.account_id),
1460 )
1461 .chain(
1462 epoch_validator_info
1463 .next_validators
1464 .into_iter()
1465 .map(|next_validator| next_validator.account_id),
1466 )
1467 .collect())
1468}
1469
1470pub fn get_used_delegated_validator_list(
1471 config: &crate::config::Config,
1472) -> color_eyre::eyre::Result<VecDeque<unc_primitives::types::AccountId>> {
1473 let used_account_list: VecDeque<UsedAccount> =
1474 get_used_account_list(&config.credentials_home_dir);
1475 let mut delegated_validator_list =
1476 get_delegated_validator_list_from_mainnet(&config.network_connection)?;
1477 let mut used_delegated_validator_list: VecDeque<unc_primitives::types::AccountId> =
1478 VecDeque::new();
1479
1480 for used_account in used_account_list {
1481 if delegated_validator_list.remove(&used_account.account_id) {
1482 used_delegated_validator_list.push_back(used_account.account_id);
1483 }
1484 }
1485
1486 used_delegated_validator_list.extend(delegated_validator_list);
1487 Ok(used_delegated_validator_list)
1488}
1489
1490pub fn input_pledging_pool_validator_account_id(
1491 config: &crate::config::Config,
1492) -> color_eyre::eyre::Result<Option<crate::types::account_id::AccountId>> {
1493 let used_delegated_validator_list = get_used_delegated_validator_list(config)?
1494 .into_iter()
1495 .map(String::from)
1496 .collect::<Vec<_>>();
1497 let validator_account_id_str = match Text::new("What is delegated validator account ID?")
1498 .with_autocomplete(move |val: &str| {
1499 Ok(used_delegated_validator_list
1500 .iter()
1501 .filter(|s| s.contains(val))
1502 .cloned()
1503 .collect())
1504 })
1505 .with_validator(|account_id_str: &str| {
1506 match unc_primitives::types::AccountId::validate(account_id_str) {
1507 Ok(_) => Ok(inquire::validator::Validation::Valid),
1508 Err(err) => Ok(inquire::validator::Validation::Invalid(
1509 inquire::validator::ErrorMessage::Custom(format!("Invalid account ID: {err}")),
1510 )),
1511 }
1512 })
1513 .prompt()
1514 {
1515 Ok(value) => value,
1516 Err(
1517 inquire::error::InquireError::OperationCanceled
1518 | inquire::error::InquireError::OperationInterrupted,
1519 ) => return Ok(None),
1520 Err(err) => return Err(err.into()),
1521 };
1522 let validator_account_id =
1523 crate::types::account_id::AccountId::from_str(&validator_account_id_str)?;
1524 update_used_account_list_as_non_signer(
1525 &config.credentials_home_dir,
1526 validator_account_id.as_ref(),
1527 );
1528 Ok(Some(validator_account_id))
1529}
1530
1531#[derive(Debug, Clone, PartialEq, Eq)]
1532pub struct PledgingPoolInfo {
1533 pub validator_id: unc_primitives::types::AccountId,
1534 pub fee: Option<RewardFeeFraction>,
1535 pub delegators: Option<u64>,
1536 pub pledge: unc_primitives::types::Balance,
1537}
1538
1539#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize)]
1540pub struct RewardFeeFraction {
1541 pub numerator: u32,
1542 pub denominator: u32,
1543}
1544
1545pub fn get_validator_list(
1546 network_config: &crate::config::NetworkConfig,
1547) -> color_eyre::eyre::Result<Vec<PledgingPoolInfo>> {
1548 let json_rpc_client = network_config.json_rpc_client();
1549
1550 let validators_pledge = get_validators_pledge(&json_rpc_client)?;
1551
1552 let runtime = tokio::runtime::Builder::new_multi_thread()
1553 .enable_all()
1554 .build()?;
1555 let concurrency = 10;
1556
1557 let mut validator_list = runtime.block_on(
1558 futures::stream::iter(validators_pledge.iter())
1559 .map(|(validator_account_id, pledge)| async {
1560 get_pledging_pool_info(
1561 &json_rpc_client.clone(),
1562 validator_account_id.clone(),
1563 *pledge,
1564 )
1565 .await
1566 })
1567 .buffer_unordered(concurrency)
1568 .try_collect::<Vec<_>>(),
1569 )?;
1570 validator_list.sort_by(|a, b| b.pledge.cmp(&a.pledge));
1571 Ok(validator_list)
1572}
1573
1574pub fn get_validators_pledge(
1575 json_rpc_client: &unc_jsonrpc_client::JsonRpcClient,
1576) -> color_eyre::eyre::Result<
1577 std::collections::HashMap<unc_primitives::types::AccountId, unc_primitives::types::Balance>,
1578> {
1579 let epoch_validator_info = json_rpc_client
1580 .blocking_call(
1581 &unc_jsonrpc_client::methods::validators::RpcValidatorRequest {
1582 epoch_reference: unc_primitives::types::EpochReference::Latest,
1583 },
1584 )
1585 .wrap_err("Failed to get epoch validators information request.")?;
1586
1587 Ok(epoch_validator_info
1588 .current_pledge_proposals
1589 .into_iter()
1590 .map(|validator_pledge_view| {
1591 let validator_pledge = validator_pledge_view.into_validator_pledge();
1592 validator_pledge.account_and_pledge()
1593 })
1594 .chain(epoch_validator_info.current_validators.into_iter().map(
1595 |current_epoch_validator_info| {
1596 (
1597 current_epoch_validator_info.account_id,
1598 current_epoch_validator_info.pledge,
1599 )
1600 },
1601 ))
1602 .chain(
1603 epoch_validator_info
1604 .next_validators
1605 .into_iter()
1606 .map(|next_epoch_validator_info| {
1607 (
1608 next_epoch_validator_info.account_id,
1609 next_epoch_validator_info.pledge,
1610 )
1611 }),
1612 )
1613 .collect())
1614}
1615
1616async fn get_pledging_pool_info(
1617 json_rpc_client: &unc_jsonrpc_client::JsonRpcClient,
1618 validator_account_id: unc_primitives::types::AccountId,
1619 pledge: u128,
1620) -> color_eyre::Result<PledgingPoolInfo> {
1621 let fee = match json_rpc_client
1622 .call(unc_jsonrpc_client::methods::query::RpcQueryRequest {
1623 block_reference: unc_primitives::types::Finality::Final.into(),
1624 request: unc_primitives::views::QueryRequest::CallFunction {
1625 account_id: validator_account_id.clone(),
1626 method_name: "get_reward_fee_fraction".to_string(),
1627 args: unc_primitives::types::FunctionArgs::from(vec![]),
1628 },
1629 })
1630 .await
1631 {
1632 Ok(response) => Some(
1633 response
1634 .call_result()?
1635 .parse_result_from_json::<RewardFeeFraction>()
1636 .wrap_err(
1637 "Failed to parse return value of view function call for RewardFeeFraction.",
1638 )?,
1639 ),
1640 Err(unc_jsonrpc_client::errors::JsonRpcError::ServerError(
1641 unc_jsonrpc_client::errors::JsonRpcServerError::HandlerError(
1642 unc_jsonrpc_client::methods::query::RpcQueryError::NoContractCode { .. }
1643 | unc_jsonrpc_client::methods::query::RpcQueryError::ContractExecutionError {
1644 ..
1645 },
1646 ),
1647 )) => None,
1648 Err(err) => return Err(err.into()),
1649 };
1650
1651 let delegators = match json_rpc_client
1652 .call(unc_jsonrpc_client::methods::query::RpcQueryRequest {
1653 block_reference: unc_primitives::types::Finality::Final.into(),
1654 request: unc_primitives::views::QueryRequest::CallFunction {
1655 account_id: validator_account_id.clone(),
1656 method_name: "get_number_of_accounts".to_string(),
1657 args: unc_primitives::types::FunctionArgs::from(vec![]),
1658 },
1659 })
1660 .await
1661 {
1662 Ok(response) => Some(
1663 response
1664 .call_result()?
1665 .parse_result_from_json::<u64>()
1666 .wrap_err("Failed to parse return value of view function call for u64.")?,
1667 ),
1668 Err(unc_jsonrpc_client::errors::JsonRpcError::ServerError(
1669 unc_jsonrpc_client::errors::JsonRpcServerError::HandlerError(
1670 unc_jsonrpc_client::methods::query::RpcQueryError::NoContractCode { .. }
1671 | unc_jsonrpc_client::methods::query::RpcQueryError::ContractExecutionError {
1672 ..
1673 },
1674 ),
1675 )) => None,
1676 Err(err) => return Err(err.into()),
1677 };
1678
1679 Ok(PledgingPoolInfo {
1680 validator_id: validator_account_id.clone(),
1681 fee,
1682 delegators,
1683 pledge,
1684 })
1685}
1686
1687pub fn display_account_info(
1688 viewed_at_block_hash: &CryptoHash,
1689 viewed_at_block_height: &unc_primitives::types::BlockHeight,
1690 account_id: &unc_primitives::types::AccountId,
1691 delegated_pledge: &std::collections::BTreeMap<
1692 unc_primitives::types::AccountId,
1693 unc_token::UncToken,
1694 >,
1695 account_view: &unc_primitives::views::AccountView,
1696 access_keys: &[unc_primitives::views::AccessKeyInfoView],
1697) {
1698 let mut table: Table = Table::new();
1699 table.set_format(*prettytable::format::consts::FORMAT_NO_COLSEP);
1700
1701 profile_table(
1702 viewed_at_block_hash,
1703 viewed_at_block_height,
1704 account_id,
1705 &mut table,
1706 );
1707
1708 table.add_row(prettytable::row![
1709 Fg->"Native account balance",
1710 Fy->unc_token::UncToken::from_attounc(account_view.amount)
1711 ]);
1712 table.add_row(prettytable::row![
1713 Fg->"Validator pledge",
1714 Fy->unc_token::UncToken::from_attounc(account_view.pledging)
1715 ]);
1716
1717 for (validator_id, pledge) in delegated_pledge {
1718 table.add_row(prettytable::row![
1719 Fg->format!("Delegated pledge with <{validator_id}>"),
1720 Fy->pledge
1721 ]);
1722 }
1723
1724 table.add_row(prettytable::row![
1725 Fg->"Storage used by the account",
1726 Fy->bytesize::ByteSize(account_view.storage_usage),
1727 ]);
1728
1729 let contract_status = if account_view.code_hash == CryptoHash::default() {
1730 "No contract code".to_string()
1731 } else {
1732 hex::encode(account_view.code_hash.as_ref())
1733 };
1734 table.add_row(prettytable::row![
1735 Fg->"Contract (SHA-256 checksum hex)",
1736 Fy->contract_status
1737 ]);
1738
1739 let access_keys_summary = if access_keys.is_empty() {
1740 "Account is locked (no access keys)".to_string()
1741 } else {
1742 let full_access_keys_count = access_keys
1743 .iter()
1744 .filter(|access_key| {
1745 matches!(
1746 access_key.access_key.permission,
1747 unc_primitives::views::AccessKeyPermissionView::FullAccess
1748 )
1749 })
1750 .count();
1751 format!(
1752 "{} full access keys and {} function-call-only access keys",
1753 full_access_keys_count,
1754 access_keys.len() - full_access_keys_count
1755 )
1756 };
1757 table.add_row(prettytable::row![
1758 Fg->"Access keys",
1759 Fy->access_keys_summary
1760 ]);
1761 table.printstd();
1762}
1763
1764pub fn display_account_profile(
1765 viewed_at_block_hash: &CryptoHash,
1766 viewed_at_block_height: &unc_primitives::types::BlockHeight,
1767 account_id: &unc_primitives::types::AccountId,
1768) {
1769 let mut table = Table::new();
1770 table.set_format(*prettytable::format::consts::FORMAT_NO_COLSEP);
1771 profile_table(
1772 viewed_at_block_hash,
1773 viewed_at_block_height,
1774 account_id,
1775 &mut table,
1776 );
1777 table.printstd();
1778}
1779
1780fn profile_table(
1781 viewed_at_block_hash: &CryptoHash,
1782 viewed_at_block_height: &unc_primitives::types::BlockHeight,
1783 account_id: &unc_primitives::types::AccountId,
1784 table: &mut Table,
1785) {
1786 table.add_row(prettytable::row![
1787 Fy->account_id,
1788 format!("At block #{}\n({})", viewed_at_block_height, viewed_at_block_hash)
1789 ]);
1790}
1791
1792pub fn display_access_key_list(access_keys: &[unc_primitives::views::AccessKeyInfoView]) {
1793 let mut table = Table::new();
1794 table.set_titles(prettytable::row![Fg=>"#", "Public Key", "Nonce", "Permissions"]);
1795
1796 for (index, access_key) in access_keys.iter().enumerate() {
1797 let permissions_message = match &access_key.access_key.permission {
1798 AccessKeyPermissionView::FullAccess => "full access".to_owned(),
1799 AccessKeyPermissionView::FunctionCall {
1800 allowance,
1801 receiver_id,
1802 method_names,
1803 } => {
1804 let allowance_message = match allowance {
1805 Some(amount) => format!(
1806 "with an allowance of {}",
1807 unc_token::UncToken::from_attounc(*amount)
1808 ),
1809 None => "with no limit".to_string(),
1810 };
1811 if method_names.is_empty() {
1812 format!(
1813 "do any function calls on {} {}",
1814 receiver_id, allowance_message
1815 )
1816 } else {
1817 format!(
1818 "only do {:?} function calls on {} {}",
1819 method_names, receiver_id, allowance_message
1820 )
1821 }
1822 }
1823 };
1824
1825 table.add_row(prettytable::row![
1826 Fg->index + 1,
1827 access_key.public_key,
1828 access_key.access_key.nonce,
1829 permissions_message
1830 ]);
1831 }
1832
1833 table.set_format(*prettytable::format::consts::FORMAT_NO_LINESEP_WITH_TITLE);
1834 table.printstd();
1835}
1836
1837pub fn input_network_name(
1842 config: &crate::config::Config,
1843 account_ids: &[unc_primitives::types::AccountId],
1844) -> color_eyre::eyre::Result<Option<String>> {
1845 if config.network_connection.len() == 1 {
1846 return Ok(config.network_names().pop());
1847 }
1848 let variants = if !account_ids.is_empty() {
1849 let (mut matches, non_matches): (Vec<_>, Vec<_>) = config
1850 .network_connection
1851 .iter()
1852 .partition(|(_, network_config)| {
1853 network_config
1857 .linkdrop_account_id
1858 .as_ref()
1859 .map_or(false, |linkdrop_account_id| {
1860 account_ids.iter().any(|account_id| {
1861 account_id.as_str().ends_with(linkdrop_account_id.as_str())
1862 })
1863 })
1864 });
1865 let variants = if matches.is_empty() {
1866 non_matches
1867 } else {
1868 matches.extend(non_matches);
1869 matches
1870 };
1871 variants.into_iter().map(|(k, _)| k).collect()
1872 } else {
1873 config.network_connection.keys().collect()
1874 };
1875
1876 let select_submit = Select::new("What is the name of the network?", variants).prompt();
1877 match select_submit {
1878 Ok(value) => Ok(Some(value.clone())),
1879 Err(
1880 inquire::error::InquireError::OperationCanceled
1881 | inquire::error::InquireError::OperationInterrupted,
1882 ) => Ok(None),
1883 Err(err) => Err(err.into()),
1884 }
1885}
1886
1887#[easy_ext::ext(JsonRpcClientExt)]
1888pub impl unc_jsonrpc_client::JsonRpcClient {
1889 fn blocking_call<M>(
1890 &self,
1891 method: M,
1892 ) -> unc_jsonrpc_client::MethodCallResult<M::Response, M::Error>
1893 where
1894 M: unc_jsonrpc_client::methods::RpcMethod,
1895 {
1896 tokio::runtime::Runtime::new()
1897 .unwrap()
1898 .block_on(self.call(method))
1899 }
1900
1901 fn blocking_call_view_function(
1904 &self,
1905 account_id: &unc_primitives::types::AccountId,
1906 method_name: &str,
1907 args: Vec<u8>,
1908 block_reference: unc_primitives::types::BlockReference,
1909 ) -> Result<unc_primitives::views::CallResult, color_eyre::eyre::Error> {
1910 let query_view_method_response = self
1911 .blocking_call(unc_jsonrpc_client::methods::query::RpcQueryRequest {
1912 block_reference,
1913 request: unc_primitives::views::QueryRequest::CallFunction {
1914 account_id: account_id.clone(),
1915 method_name: method_name.to_owned(),
1916 args: unc_primitives::types::FunctionArgs::from(args),
1917 },
1918 })
1919 .wrap_err("Failed to make a view-function call")?;
1920 query_view_method_response.call_result()
1921 }
1922
1923 fn blocking_call_view_access_key(
1924 &self,
1925 account_id: &unc_primitives::types::AccountId,
1926 public_key: &unc_crypto::PublicKey,
1927 block_reference: unc_primitives::types::BlockReference,
1928 ) -> Result<
1929 unc_jsonrpc_primitives::types::query::RpcQueryResponse,
1930 unc_jsonrpc_client::errors::JsonRpcError<
1931 unc_jsonrpc_primitives::types::query::RpcQueryError,
1932 >,
1933 > {
1934 self.blocking_call(unc_jsonrpc_client::methods::query::RpcQueryRequest {
1935 block_reference,
1936 request: unc_primitives::views::QueryRequest::ViewAccessKey {
1937 account_id: account_id.clone(),
1938 public_key: public_key.clone(),
1939 },
1940 })
1941 }
1942
1943 fn blocking_call_view_access_key_list(
1944 &self,
1945 account_id: &unc_primitives::types::AccountId,
1946 block_reference: unc_primitives::types::BlockReference,
1947 ) -> Result<
1948 unc_jsonrpc_primitives::types::query::RpcQueryResponse,
1949 unc_jsonrpc_client::errors::JsonRpcError<
1950 unc_jsonrpc_primitives::types::query::RpcQueryError,
1951 >,
1952 > {
1953 self.blocking_call(unc_jsonrpc_client::methods::query::RpcQueryRequest {
1954 block_reference,
1955 request: unc_primitives::views::QueryRequest::ViewAccessKeyList {
1956 account_id: account_id.clone(),
1957 },
1958 })
1959 }
1960
1961 fn blocking_call_view_account(
1962 &self,
1963 account_id: &unc_primitives::types::AccountId,
1964 block_reference: unc_primitives::types::BlockReference,
1965 ) -> Result<
1966 unc_jsonrpc_primitives::types::query::RpcQueryResponse,
1967 unc_jsonrpc_client::errors::JsonRpcError<
1968 unc_jsonrpc_primitives::types::query::RpcQueryError,
1969 >,
1970 > {
1971 self.blocking_call(unc_jsonrpc_client::methods::query::RpcQueryRequest {
1972 block_reference,
1973 request: unc_primitives::views::QueryRequest::ViewAccount {
1974 account_id: account_id.clone(),
1975 },
1976 })
1977 }
1978}
1979
1980#[easy_ext::ext(RpcQueryResponseExt)]
1981pub impl unc_jsonrpc_primitives::types::query::RpcQueryResponse {
1982 fn access_key_view(&self) -> color_eyre::eyre::Result<unc_primitives::views::AccessKeyView> {
1983 if let unc_jsonrpc_primitives::types::query::QueryResponseKind::AccessKey(
1984 access_key_view,
1985 ) = &self.kind
1986 {
1987 Ok(access_key_view.clone())
1988 } else {
1989 color_eyre::eyre::bail!(
1990 "Internal error: Received unexpected query kind in response to a View Access Key query call",
1991 );
1992 }
1993 }
1994
1995 fn access_key_list_view(
1996 &self,
1997 ) -> color_eyre::eyre::Result<unc_primitives::views::AccessKeyList> {
1998 if let unc_jsonrpc_primitives::types::query::QueryResponseKind::AccessKeyList(
1999 access_key_list,
2000 ) = &self.kind
2001 {
2002 Ok(access_key_list.clone())
2003 } else {
2004 color_eyre::eyre::bail!(
2005 "Internal error: Received unexpected query kind in response to a View Access Key List query call",
2006 );
2007 }
2008 }
2009
2010 fn account_view(&self) -> color_eyre::eyre::Result<unc_primitives::views::AccountView> {
2011 if let unc_jsonrpc_primitives::types::query::QueryResponseKind::ViewAccount(account_view) =
2012 &self.kind
2013 {
2014 Ok(account_view.clone())
2015 } else {
2016 color_eyre::eyre::bail!(
2017 "Internal error: Received unexpected query kind in response to a View Account query call",
2018 );
2019 }
2020 }
2021
2022 fn call_result(&self) -> color_eyre::eyre::Result<unc_primitives::views::CallResult> {
2023 if let unc_jsonrpc_primitives::types::query::QueryResponseKind::CallResult(result) =
2024 &self.kind
2025 {
2026 Ok(result.clone())
2027 } else {
2028 color_eyre::eyre::bail!(
2029 "Internal error: Received unexpected query kind in response to a view-function query call",
2030 );
2031 }
2032 }
2033}
2034
2035#[easy_ext::ext(CallResultExt)]
2036pub impl unc_primitives::views::CallResult {
2037 fn parse_result_from_json<T>(&self) -> Result<T, color_eyre::eyre::Error>
2038 where
2039 T: for<'de> serde::Deserialize<'de>,
2040 {
2041 serde_json::from_slice(&self.result).wrap_err_with(|| {
2042 format!(
2043 "Failed to parse view-function call return value: {}",
2044 String::from_utf8_lossy(&self.result)
2045 )
2046 })
2047 }
2048
2049 fn print_logs(&self) {
2050 eprintln!("--------------");
2051 if self.logs.is_empty() {
2052 eprintln!("No logs")
2053 } else {
2054 eprintln!("Logs:");
2055 eprintln!(" {}", self.logs.join("\n "));
2056 }
2057 eprintln!("--------------");
2058 }
2059}
2060
2061#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
2062pub struct UsedAccount {
2063 pub account_id: unc_primitives::types::AccountId,
2064 pub used_as_signer: bool,
2065}
2066
2067fn get_used_account_list_path(credentials_home_dir: &std::path::Path) -> std::path::PathBuf {
2068 credentials_home_dir.join("accounts.json")
2069}
2070
2071pub fn create_used_account_list_from_keychain(
2072 credentials_home_dir: &std::path::Path,
2073) -> color_eyre::eyre::Result<()> {
2074 let mut used_account_list: std::collections::BTreeSet<unc_primitives::types::AccountId> =
2075 std::collections::BTreeSet::new();
2076 let read_dir =
2077 |dir: &std::path::Path| dir.read_dir().map(Iterator::flatten).into_iter().flatten();
2078 for network_connection_dir in read_dir(credentials_home_dir) {
2079 for entry in read_dir(&network_connection_dir.path()) {
2080 match (entry.path().file_stem(), entry.path().extension()) {
2081 (Some(file_stem), Some(extension)) if extension == "json" => {
2082 if let Ok(account_id) = file_stem.to_string_lossy().parse() {
2083 used_account_list.insert(account_id);
2084 }
2085 }
2086 _ if entry.path().is_dir() => {
2087 if let Ok(account_id) = entry.file_name().to_string_lossy().parse() {
2088 used_account_list.insert(account_id);
2089 }
2090 }
2091 _ => {}
2092 }
2093 }
2094 }
2095
2096 if !used_account_list.is_empty() {
2097 let used_account_list_path = get_used_account_list_path(credentials_home_dir);
2098 let used_account_list_buf = serde_json::to_string(
2099 &used_account_list
2100 .into_iter()
2101 .map(|account_id| UsedAccount {
2102 account_id,
2103 used_as_signer: true,
2104 })
2105 .collect::<Vec<_>>(),
2106 )?;
2107 std::fs::write(&used_account_list_path, used_account_list_buf).wrap_err_with(|| {
2108 format!(
2109 "Failed to write to file: {}",
2110 used_account_list_path.display()
2111 )
2112 })?;
2113 }
2114 Ok(())
2115}
2116
2117pub fn update_used_account_list_as_signer(
2118 credentials_home_dir: &std::path::Path,
2119 account_id: &unc_primitives::types::AccountId,
2120) {
2121 let account_is_signer = true;
2122 update_used_account_list(credentials_home_dir, account_id, account_is_signer);
2123}
2124
2125pub fn update_used_account_list_as_non_signer(
2126 credentials_home_dir: &std::path::Path,
2127 account_id: &unc_primitives::types::AccountId,
2128) {
2129 let account_is_signer = false;
2130 update_used_account_list(credentials_home_dir, account_id, account_is_signer);
2131}
2132
2133fn update_used_account_list(
2134 credentials_home_dir: &std::path::Path,
2135 account_id: &unc_primitives::types::AccountId,
2136 account_is_signer: bool,
2137) {
2138 let mut used_account_list = get_used_account_list(credentials_home_dir);
2139
2140 let used_account = if let Some(mut used_account) = used_account_list
2141 .iter()
2142 .position(|used_account| &used_account.account_id == account_id)
2143 .and_then(|position| used_account_list.remove(position))
2144 {
2145 used_account.used_as_signer |= account_is_signer;
2146 used_account
2147 } else {
2148 UsedAccount {
2149 account_id: account_id.clone(),
2150 used_as_signer: account_is_signer,
2151 }
2152 };
2153 used_account_list.push_front(used_account);
2154
2155 let used_account_list_path = get_used_account_list_path(credentials_home_dir);
2156 if let Ok(used_account_list_buf) = serde_json::to_string(&used_account_list) {
2157 let _ = std::fs::write(used_account_list_path, used_account_list_buf);
2158 }
2159}
2160
2161pub fn get_used_account_list(credentials_home_dir: &std::path::Path) -> VecDeque<UsedAccount> {
2162 let used_account_list_path = get_used_account_list_path(credentials_home_dir);
2163 serde_json::from_str(
2164 std::fs::read_to_string(used_account_list_path)
2165 .as_deref()
2166 .unwrap_or("[]"),
2167 )
2168 .unwrap_or_default()
2169}
2170
2171pub fn is_used_account_list_exist(credentials_home_dir: &std::path::Path) -> bool {
2172 get_used_account_list_path(credentials_home_dir).exists()
2173}
2174
2175pub fn input_signer_account_id_from_used_account_list(
2176 credentials_home_dir: &std::path::Path,
2177 message: &str,
2178) -> color_eyre::eyre::Result<Option<crate::types::account_id::AccountId>> {
2179 let account_is_signer = true;
2180 input_account_id_from_used_account_list(credentials_home_dir, message, account_is_signer)
2181}
2182
2183pub fn input_non_signer_account_id_from_used_account_list(
2184 credentials_home_dir: &std::path::Path,
2185 message: &str,
2186) -> color_eyre::eyre::Result<Option<crate::types::account_id::AccountId>> {
2187 let account_is_signer = false;
2188 input_account_id_from_used_account_list(credentials_home_dir, message, account_is_signer)
2189}
2190
2191fn input_account_id_from_used_account_list(
2192 credentials_home_dir: &std::path::Path,
2193 message: &str,
2194 account_is_signer: bool,
2195) -> color_eyre::eyre::Result<Option<crate::types::account_id::AccountId>> {
2196 let used_account_list = get_used_account_list(credentials_home_dir)
2197 .into_iter()
2198 .filter(|account| !account_is_signer || account.used_as_signer)
2199 .map(|account| account.account_id.to_string())
2200 .collect::<Vec<_>>();
2201 let account_id_str = match Text::new(message)
2202 .with_autocomplete(move |val: &str| {
2203 Ok(used_account_list
2204 .iter()
2205 .filter(|s| s.contains(val))
2206 .cloned()
2207 .collect())
2208 })
2209 .with_validator(|account_id_str: &str| {
2210 match unc_primitives::types::AccountId::validate(account_id_str) {
2211 Ok(_) => Ok(inquire::validator::Validation::Valid),
2212 Err(err) => Ok(inquire::validator::Validation::Invalid(
2213 inquire::validator::ErrorMessage::Custom(format!("Invalid account ID: {err}")),
2214 )),
2215 }
2216 })
2217 .prompt()
2218 {
2219 Ok(value) => value,
2220 Err(
2221 inquire::error::InquireError::OperationCanceled
2222 | inquire::error::InquireError::OperationInterrupted,
2223 ) => return Ok(None),
2224 Err(err) => return Err(err.into()),
2225 };
2226 let account_id = crate::types::account_id::AccountId::from_str(&account_id_str)?;
2227 update_used_account_list(credentials_home_dir, account_id.as_ref(), account_is_signer);
2228 Ok(Some(account_id))
2229}