1use std::collections::VecDeque;
2use std::convert::{TryFrom, TryInto};
3use std::fs::OpenOptions;
4use std::io::Write;
5use std::str::FromStr;
6
7use color_eyre::eyre::{ContextCompat, WrapErr};
8use color_eyre::owo_colors::OwoColorize;
9use futures::{StreamExt, TryStreamExt};
10use near_primitives::action::{GlobalContractDeployMode, GlobalContractIdentifier};
11use prettytable::Table;
12use rust_decimal::prelude::FromPrimitive;
13use tracing_indicatif::span_ext::IndicatifSpanExt;
14use tracing_indicatif::suspend_tracing_indicatif;
15
16use near_primitives::{hash::CryptoHash, types::BlockReference, views::AccessKeyPermissionView};
17
18pub type CliResult = color_eyre::eyre::Result<()>;
19
20pub type BoxedJsonRpcResult<T, E> = Result<T, Box<near_jsonrpc_client::errors::JsonRpcError<E>>>;
23
24use inquire::{Select, Text};
25use strum::IntoEnumIterator;
26
27use crate::types::partial_protocol_config::get_partial_protocol_config;
28
29const FINAL_COMMAND_FILE_NAME: &str = "near-cli-rs-final-command.log";
30
31pub fn get_near_exec_path() -> String {
32 std::env::args()
33 .next()
34 .unwrap_or_else(|| "./near".to_owned())
35}
36
37#[derive(
38 Debug,
39 Clone,
40 strum_macros::IntoStaticStr,
41 strum_macros::EnumString,
42 strum_macros::EnumVariantNames,
43 smart_default::SmartDefault,
44)]
45#[strum(serialize_all = "snake_case")]
46pub enum OutputFormat {
47 #[default]
48 Plaintext,
49 Json,
50}
51
52impl std::fmt::Display for OutputFormat {
53 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54 match self {
55 OutputFormat::Plaintext => write!(f, "plaintext"),
56 OutputFormat::Json => write!(f, "json"),
57 }
58 }
59}
60
61#[derive(Debug, Clone)]
62pub struct BlockHashAsBase58 {
63 pub inner: near_primitives::hash::CryptoHash,
64}
65
66impl std::str::FromStr for BlockHashAsBase58 {
67 type Err = String;
68 fn from_str(s: &str) -> Result<Self, Self::Err> {
69 Ok(Self {
70 inner: bs58::decode(s)
71 .into_vec()
72 .map_err(|err| format!("base58 block hash sequence is invalid: {err}"))?
73 .as_slice()
74 .try_into()
75 .map_err(|err| format!("block hash could not be collected: {err}"))?,
76 })
77 }
78}
79
80impl std::fmt::Display for BlockHashAsBase58 {
81 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
82 write!(f, "BlockHash {}", self.inner)
83 }
84}
85
86pub use near_gas::NearGas;
87
88#[derive(Debug, Clone, Default, PartialEq, Eq, PartialOrd)]
89pub struct TransferAmount {
90 amount: near_token::NearToken,
91}
92
93impl interactive_clap::ToCli for TransferAmount {
94 type CliVariant = near_token::NearToken;
95}
96
97impl std::fmt::Display for TransferAmount {
98 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
99 write!(f, "{}", self.amount)
100 }
101}
102
103impl TransferAmount {
104 pub fn from(
105 amount: near_token::NearToken,
106 account_transfer_allowance: &AccountTransferAllowance,
107 ) -> color_eyre::eyre::Result<Self> {
108 if amount <= account_transfer_allowance.transfer_allowance() {
109 Ok(Self { amount })
110 } else {
111 Err(color_eyre::Report::msg(
112 "the amount exceeds the transfer allowance",
113 ))
114 }
115 }
116
117 pub fn from_unchecked(amount: near_token::NearToken) -> Self {
118 Self { amount }
119 }
120
121 pub fn as_yoctonear(&self) -> u128 {
122 self.amount.as_yoctonear()
123 }
124}
125
126impl From<TransferAmount> for near_token::NearToken {
127 fn from(item: TransferAmount) -> Self {
128 item.amount
129 }
130}
131
132#[derive(Debug)]
133pub struct AccountTransferAllowance {
134 account_id: near_primitives::types::AccountId,
135 account_liquid_balance: near_token::NearToken,
136 account_locked_balance: near_token::NearToken,
137 storage_stake: near_token::NearToken,
138 pessimistic_transaction_fee: near_token::NearToken,
139}
140
141impl std::fmt::Display for AccountTransferAllowance {
142 fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
143 write!(fmt,
144 "\n{} account has {} available for transfer (the total balance is {}, but {} is locked for storage)",
145 self.account_id,
146 self.transfer_allowance(),
147 self.account_liquid_balance,
148 self.liquid_storage_stake(),
149 )
150 }
151}
152
153impl AccountTransferAllowance {
154 pub fn liquid_storage_stake(&self) -> near_token::NearToken {
155 self.storage_stake
156 .saturating_sub(self.account_locked_balance)
157 }
158
159 pub fn transfer_allowance(&self) -> near_token::NearToken {
160 self.account_liquid_balance
161 .saturating_sub(self.liquid_storage_stake())
162 .saturating_sub(self.pessimistic_transaction_fee)
163 }
164}
165
166#[derive(Debug)]
167pub enum AccountStateError<E> {
168 JsonRpcError(near_jsonrpc_client::errors::JsonRpcError<E>),
169 Cancel,
170}
171
172#[tracing::instrument(name = "Getting the transfer allowance for the account ...", skip_all)]
173pub async fn get_account_transfer_allowance(
174 network_config: &crate::config::NetworkConfig,
175 account_id: near_primitives::types::AccountId,
176 block_reference: BlockReference,
177) -> color_eyre::eyre::Result<AccountTransferAllowance> {
178 let account_state =
179 get_account_state(network_config, &account_id, block_reference.clone()).await;
180 let account_view = match account_state {
181 Ok(account_view) => account_view,
182 Err(AccountStateError::JsonRpcError(
183 near_jsonrpc_client::errors::JsonRpcError::ServerError(
184 near_jsonrpc_client::errors::JsonRpcServerError::HandlerError(
185 near_jsonrpc_primitives::types::query::RpcQueryError::UnknownAccount { .. },
186 ),
187 ),
188 )) if account_id.get_account_type().is_implicit() => {
189 return Ok(AccountTransferAllowance {
190 account_id,
191 account_liquid_balance: near_token::NearToken::from_near(0),
192 account_locked_balance: near_token::NearToken::from_near(0),
193 storage_stake: near_token::NearToken::from_near(0),
194 pessimistic_transaction_fee: near_token::NearToken::from_near(0),
195 });
196 }
197 Err(AccountStateError::JsonRpcError(
198 near_jsonrpc_client::errors::JsonRpcError::TransportError(err),
199 )) => {
200 return color_eyre::eyre::Result::Err(
201 color_eyre::eyre::eyre!("\nAccount information ({account_id}) cannot be fetched on <{}> network due to connectivity issue.\n{err}",
202 network_config.network_name
203 ));
204 }
205 Err(AccountStateError::JsonRpcError(
206 near_jsonrpc_client::errors::JsonRpcError::ServerError(err),
207 )) => {
208 return color_eyre::eyre::Result::Err(
209 color_eyre::eyre::eyre!("\nAccount information ({account_id}) cannot be fetched on <{}> network due to server error.\n{err}",
210 network_config.network_name
211 ));
212 }
213 Err(AccountStateError::Cancel) => {
214 return color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!(
215 "Operation was canceled by the user"
216 ));
217 }
218 };
219 let storage_amount_per_byte =
220 get_partial_protocol_config(&network_config.json_rpc_client(), &block_reference)
221 .await?
222 .runtime_config
223 .storage_amount_per_byte;
224
225 Ok(AccountTransferAllowance {
226 account_id,
227 account_liquid_balance: near_token::NearToken::from_yoctonear(account_view.amount),
228 account_locked_balance: near_token::NearToken::from_yoctonear(account_view.locked),
229 storage_stake: near_token::NearToken::from_yoctonear(
230 u128::from(account_view.storage_usage) * storage_amount_per_byte,
231 ),
232 pessimistic_transaction_fee: near_token::NearToken::from_millinear(1),
236 })
237}
238
239#[allow(clippy::result_large_err)]
240#[tracing::instrument(name = "Account access key verification ...", skip_all)]
241pub fn verify_account_access_key(
242 account_id: near_primitives::types::AccountId,
243 public_key: near_crypto::PublicKey,
244 network_config: crate::config::NetworkConfig,
245) -> color_eyre::eyre::Result<
246 near_primitives::views::AccessKeyView,
247 AccountStateError<near_jsonrpc_primitives::types::query::RpcQueryError>,
248> {
249 loop {
250 match network_config
251 .json_rpc_client()
252 .blocking_call_view_access_key(
253 &account_id,
254 &public_key,
255 near_primitives::types::BlockReference::latest(),
256 )
257 .map_err(|err| *err)
258 {
259 Ok(rpc_query_response) => {
260 if let near_jsonrpc_primitives::types::query::QueryResponseKind::AccessKey(result) =
261 rpc_query_response.kind
262 {
263 return Ok(result);
264 } else {
265 return Err(AccountStateError::JsonRpcError(near_jsonrpc_client::errors::JsonRpcError::TransportError(near_jsonrpc_client::errors::RpcTransportError::RecvError(
266 near_jsonrpc_client::errors::JsonRpcTransportRecvError::UnexpectedServerResponse(
267 near_jsonrpc_primitives::message::Message::error(near_jsonrpc_primitives::errors::RpcError::parse_error("Transport error: unexpected server response".to_string()))
268 ),
269 ))));
270 }
271 }
272 Err(
273 err @ near_jsonrpc_client::errors::JsonRpcError::ServerError(
274 near_jsonrpc_client::errors::JsonRpcServerError::HandlerError(
275 near_jsonrpc_primitives::types::query::RpcQueryError::UnknownAccessKey {
276 ..
277 },
278 ),
279 ),
280 ) => {
281 return Err(AccountStateError::JsonRpcError(err));
282 }
283 Err(near_jsonrpc_client::errors::JsonRpcError::TransportError(err)) => {
284 let need_check_account = need_check_account(format!("\nAccount information ({account_id}) cannot be fetched on <{}> network due to connectivity issue.", network_config.network_name));
285 if need_check_account.is_err() {
286 return Err(AccountStateError::Cancel);
287 }
288 if let Ok(false) = need_check_account {
289 return Err(AccountStateError::JsonRpcError(
290 near_jsonrpc_client::errors::JsonRpcError::TransportError(err),
291 ));
292 }
293 }
294 Err(near_jsonrpc_client::errors::JsonRpcError::ServerError(err)) => {
295 let need_check_account = need_check_account(format!("\nAccount information ({account_id}) cannot be fetched on <{}> network due to server error.", network_config.network_name));
296 if need_check_account.is_err() {
297 return Err(AccountStateError::Cancel);
298 }
299 if let Ok(false) = need_check_account {
300 return Err(AccountStateError::JsonRpcError(
301 near_jsonrpc_client::errors::JsonRpcError::ServerError(err),
302 ));
303 }
304 }
305 }
306 }
307}
308
309#[tracing::instrument(name = "Checking the existence of the account ...", skip_all)]
310pub fn is_account_exist(
311 networks: &linked_hash_map::LinkedHashMap<String, crate::config::NetworkConfig>,
312 account_id: near_primitives::types::AccountId,
313) -> color_eyre::eyre::Result<bool> {
314 for (_, network_config) in networks {
315 let result = tokio::runtime::Runtime::new()
316 .unwrap()
317 .block_on(get_account_state(
318 network_config,
319 &account_id,
320 near_primitives::types::Finality::Final.into(),
321 ));
322
323 if result.is_ok() {
324 return Ok(true);
325 }
326
327 if let Err(AccountStateError::Cancel) = result {
328 return color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!(
329 "Operation was canceled by the user"
330 ));
331 }
332 }
333 Ok(false)
334}
335
336#[tracing::instrument(name = "Searching for a network where an account exists for", skip_all)]
337pub fn find_network_where_account_exist(
338 context: &crate::GlobalContext,
339 new_account_id: near_primitives::types::AccountId,
340) -> color_eyre::eyre::Result<Option<crate::config::NetworkConfig>> {
341 tracing::Span::current().pb_set_message(new_account_id.as_str());
342 for (_, network_config) in context.config.network_connection.iter() {
343 let result = tokio::runtime::Runtime::new()
344 .unwrap()
345 .block_on(get_account_state(
346 network_config,
347 &new_account_id,
348 near_primitives::types::BlockReference::latest(),
349 ));
350
351 if result.is_ok() {
352 return Ok(Some(network_config.clone()));
353 }
354
355 if let Err(AccountStateError::Cancel) = result {
356 return color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!(
357 "Operation was canceled by the user"
358 ));
359 }
360 }
361 Ok(None)
362}
363
364pub fn ask_if_different_account_id_wanted() -> color_eyre::eyre::Result<bool> {
365 #[derive(strum_macros::Display, PartialEq)]
366 enum ConfirmOptions {
367 #[strum(to_string = "Yes, I want to enter a new name for account ID.")]
368 Yes,
369 #[strum(to_string = "No, I want to keep using this name for account ID.")]
370 No,
371 }
372 let select_choose_input = Select::new(
373 "Do you want to enter a different name for the new account ID?",
374 vec![ConfirmOptions::Yes, ConfirmOptions::No],
375 )
376 .prompt()?;
377 Ok(select_choose_input == ConfirmOptions::Yes)
378}
379
380#[tracing::instrument(name = "Getting account status information for", skip_all)]
381pub async fn get_account_state(
382 network_config: &crate::config::NetworkConfig,
383 account_id: &near_primitives::types::AccountId,
384 block_reference: BlockReference,
385) -> color_eyre::eyre::Result<
386 near_primitives::views::AccountView,
387 AccountStateError<near_jsonrpc_primitives::types::query::RpcQueryError>,
388> {
389 loop {
390 tracing::Span::current().pb_set_message(&format!(
391 "<{account_id}> on network <{}> ...",
392 network_config.network_name
393 ));
394 tracing::info!(target: "near_teach_me", "<{account_id}> on network <{}> ...", network_config.network_name);
395
396 let query_view_method_response = view_account(
397 format!("{}", network_config.rpc_url),
398 &network_config.json_rpc_client(),
399 account_id,
400 block_reference.clone(),
401 )
402 .await;
403
404 match query_view_method_response {
405 Ok(rpc_query_response) => {
406 if let near_jsonrpc_primitives::types::query::QueryResponseKind::ViewAccount(
407 account_view,
408 ) = rpc_query_response.kind
409 {
410 return Ok(account_view);
411 } else {
412 return Err(AccountStateError::JsonRpcError(near_jsonrpc_client::errors::JsonRpcError::TransportError(near_jsonrpc_client::errors::RpcTransportError::RecvError(
413 near_jsonrpc_client::errors::JsonRpcTransportRecvError::UnexpectedServerResponse(
414 near_jsonrpc_primitives::message::Message::error(near_jsonrpc_primitives::errors::RpcError::parse_error("Transport error: unexpected server response".to_string()))
415 ),
416 ))));
417 }
418 }
419 Err(
420 err @ near_jsonrpc_client::errors::JsonRpcError::ServerError(
421 near_jsonrpc_client::errors::JsonRpcServerError::HandlerError(
422 near_jsonrpc_primitives::types::query::RpcQueryError::UnknownAccount {
423 ..
424 },
425 ),
426 ),
427 ) => {
428 return Err(AccountStateError::JsonRpcError(err));
429 }
430 Err(near_jsonrpc_client::errors::JsonRpcError::TransportError(err)) => {
431 let need_check_account = suspend_tracing_indicatif::<
432 _,
433 color_eyre::eyre::Result<bool>,
434 >(|| {
435 need_check_account(format!("\nAccount information ({account_id}) cannot be fetched on <{}> network due to connectivity issue.",
436 network_config.network_name))
437 });
438 if need_check_account.is_err() {
439 return Err(AccountStateError::Cancel);
440 }
441 if let Ok(false) = need_check_account {
442 return Err(AccountStateError::JsonRpcError(
443 near_jsonrpc_client::errors::JsonRpcError::TransportError(err),
444 ));
445 }
446 }
447 Err(near_jsonrpc_client::errors::JsonRpcError::ServerError(err)) => {
448 let need_check_account = suspend_tracing_indicatif::<
449 _,
450 color_eyre::eyre::Result<bool>,
451 >(|| {
452 need_check_account(format!("\nAccount information ({account_id}) cannot be fetched on <{}> network due to server error.",
453 network_config.network_name))
454 });
455 if need_check_account.is_err() {
456 return Err(AccountStateError::Cancel);
457 }
458 if let Ok(false) = need_check_account {
459 return Err(AccountStateError::JsonRpcError(
460 near_jsonrpc_client::errors::JsonRpcError::ServerError(err),
461 ));
462 }
463 }
464 }
465 }
466}
467
468#[tracing::instrument(name = "Receiving request via RPC", skip_all)]
469async fn view_account(
470 instrument_message: String,
471 json_rpc_client: &near_jsonrpc_client::JsonRpcClient,
472 account_id: &near_primitives::types::AccountId,
473 block_reference: BlockReference,
474) -> Result<
475 near_jsonrpc_primitives::types::query::RpcQueryResponse,
476 near_jsonrpc_client::errors::JsonRpcError<near_jsonrpc_primitives::types::query::RpcQueryError>,
477> {
478 tracing::Span::current().pb_set_message(&instrument_message);
479
480 let query_view_method_request = near_jsonrpc_client::methods::query::RpcQueryRequest {
481 block_reference,
482 request: near_primitives::views::QueryRequest::ViewAccount {
483 account_id: account_id.clone(),
484 },
485 };
486
487 tracing::info!(
488 target: "near_teach_me",
489 parent: &tracing::Span::none(),
490 "I am making HTTP call to NEAR JSON RPC to query information about `{}` account, learn more https://docs.near.org/api/rpc/contracts#view-account",
491 account_id
492 );
493
494 if let Ok(request_payload) = near_jsonrpc_client::methods::to_json(&query_view_method_request) {
495 tracing::info!(
496 target: "near_teach_me",
497 parent: &tracing::Span::none(),
498 "HTTP POST {}",
499 json_rpc_client.server_addr()
500 );
501 tracing::info!(
502 target: "near_teach_me",
503 parent: &tracing::Span::none(),
504 "JSON Request Body:\n{}",
505 indent_payload(&format!("{request_payload:#}"))
506 );
507 }
508
509 json_rpc_client
510 .call(query_view_method_request)
511 .await
512 .inspect_err(|err| match err {
513 near_jsonrpc_client::errors::JsonRpcError::TransportError(transport_error) => {
514 tracing::info!(
515 target: "near_teach_me",
516 parent: &tracing::Span::none(),
517 "JSON RPC Request failed due to connectivity issue:\n{}",
518 indent_payload(&format!("{transport_error:#?}"))
519 );
520 }
521 near_jsonrpc_client::errors::JsonRpcError::ServerError(
522 near_jsonrpc_client::errors::JsonRpcServerError::HandlerError(handler_error),
523 ) => {
524 tracing::info!(
525 target: "near_teach_me",
526 parent: &tracing::Span::none(),
527 "JSON RPC Request returned a handling error:\n{}",
528 indent_payload(&serde_json::to_string_pretty(handler_error).unwrap_or_else(|_| handler_error.to_string()))
529 );
530 }
531 near_jsonrpc_client::errors::JsonRpcError::ServerError(server_error) => {
532 tracing::info!(
533 target: "near_teach_me",
534 parent: &tracing::Span::none(),
535 "JSON RPC Request returned a generic server error:\n{}",
536 indent_payload(&format!("{server_error:#?}"))
537 );
538 }
539 })
540 .inspect(teach_me_call_response)
541}
542
543fn need_check_account(message: String) -> color_eyre::eyre::Result<bool> {
544 #[derive(strum_macros::Display, PartialEq)]
545 enum ConfirmOptions {
546 #[strum(to_string = "Yes, I want to check the account again.")]
547 Yes,
548 #[strum(to_string = "No, I want to skip the check and use the specified account ID.")]
549 No,
550 }
551 let select_choose_input = Select::new(
552 &format!("{message}\nDo you want to try again?"),
553 vec![ConfirmOptions::Yes, ConfirmOptions::No],
554 )
555 .prompt()?;
556
557 Ok(select_choose_input == ConfirmOptions::Yes)
558}
559
560#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
561pub struct KeyPairProperties {
562 pub seed_phrase_hd_path: crate::types::slip10::BIP32Path,
563 pub master_seed_phrase: String,
564 pub implicit_account_id: near_primitives::types::AccountId,
565 #[serde(rename = "public_key")]
566 pub public_key_str: String,
567 #[serde(rename = "private_key")]
568 pub secret_keypair_str: String,
569}
570
571pub fn get_key_pair_properties_from_seed_phrase(
572 seed_phrase_hd_path: crate::types::slip10::BIP32Path,
573 master_seed_phrase: String,
574) -> color_eyre::eyre::Result<KeyPairProperties> {
575 let master_seed = bip39::Mnemonic::parse(&master_seed_phrase)?.to_seed("");
576 let derived_private_key = slipped10::derive_key_from_path(
577 &master_seed,
578 slipped10::Curve::Ed25519,
579 &seed_phrase_hd_path.clone().into(),
580 )
581 .map_err(|err| {
582 color_eyre::Report::msg(format!("Failed to derive a key from the master key: {err}"))
583 })?;
584
585 let signing_key = ed25519_dalek::SigningKey::from_bytes(&derived_private_key.key);
586
587 let public_key = signing_key.verifying_key();
588 let implicit_account_id = near_primitives::types::AccountId::try_from(hex::encode(public_key))?;
589 let public_key_str = format!("ed25519:{}", bs58::encode(&public_key).into_string());
590 let secret_keypair_str = format!(
591 "ed25519:{}",
592 bs58::encode(signing_key.to_keypair_bytes()).into_string()
593 );
594 let key_pair_properties: KeyPairProperties = KeyPairProperties {
595 seed_phrase_hd_path,
596 master_seed_phrase,
597 implicit_account_id,
598 public_key_str,
599 secret_keypair_str,
600 };
601 Ok(key_pair_properties)
602}
603
604pub fn get_public_key_from_seed_phrase(
605 seed_phrase_hd_path: slipped10::BIP32Path,
606 master_seed_phrase: &str,
607) -> color_eyre::eyre::Result<near_crypto::PublicKey> {
608 let master_seed = bip39::Mnemonic::parse(master_seed_phrase)?.to_seed("");
609 let derived_private_key = slipped10::derive_key_from_path(
610 &master_seed,
611 slipped10::Curve::Ed25519,
612 &seed_phrase_hd_path,
613 )
614 .map_err(|err| {
615 color_eyre::Report::msg(format!("Failed to derive a key from the master key: {err}"))
616 })?;
617 let signing_key = ed25519_dalek::SigningKey::from_bytes(&derived_private_key.key);
618 let public_key_str = format!(
619 "ed25519:{}",
620 bs58::encode(&signing_key.verifying_key()).into_string()
621 );
622 Ok(near_crypto::PublicKey::from_str(&public_key_str)?)
623}
624
625pub fn generate_keypair() -> color_eyre::eyre::Result<KeyPairProperties> {
626 let generate_keypair: crate::utils_command::generate_keypair_subcommand::CliGenerateKeypair =
627 crate::utils_command::generate_keypair_subcommand::CliGenerateKeypair::default();
628 let (master_seed_phrase, master_seed) =
629 if let Some(master_seed_phrase) = generate_keypair.master_seed_phrase.as_deref() {
630 (
631 master_seed_phrase.to_owned(),
632 bip39::Mnemonic::parse(master_seed_phrase)?.to_seed(""),
633 )
634 } else {
635 let mnemonic =
636 bip39::Mnemonic::generate(generate_keypair.new_master_seed_phrase_words_count)?;
637 let master_seed_phrase = mnemonic.words().collect::<Vec<&str>>().join(" ");
638 (master_seed_phrase, mnemonic.to_seed(""))
639 };
640
641 let derived_private_key = slipped10::derive_key_from_path(
642 &master_seed,
643 slipped10::Curve::Ed25519,
644 &generate_keypair.seed_phrase_hd_path.clone().into(),
645 )
646 .map_err(|err| {
647 color_eyre::Report::msg(format!("Failed to derive a key from the master key: {err}"))
648 })?;
649
650 let signing_key = ed25519_dalek::SigningKey::from_bytes(&derived_private_key.key);
651
652 let public = signing_key.verifying_key();
653 let implicit_account_id = near_primitives::types::AccountId::try_from(hex::encode(public))?;
654 let public_key_str = format!("ed25519:{}", bs58::encode(&public).into_string());
655 let secret_keypair_str = format!(
656 "ed25519:{}",
657 bs58::encode(signing_key.to_keypair_bytes()).into_string()
658 );
659 let key_pair_properties: KeyPairProperties = KeyPairProperties {
660 seed_phrase_hd_path: generate_keypair.seed_phrase_hd_path,
661 master_seed_phrase,
662 implicit_account_id,
663 public_key_str,
664 secret_keypair_str,
665 };
666 Ok(key_pair_properties)
667}
668
669pub fn print_full_signed_transaction(
670 transaction: near_primitives::transaction::SignedTransaction,
671) -> String {
672 let mut info_str = format!("\n{:<13} {}", "signature:", transaction.signature);
673 info_str.push_str(&crate::common::print_full_unsigned_transaction(
674 transaction.transaction,
675 ));
676 info_str
677}
678
679pub fn print_full_unsigned_transaction(
680 transaction: near_primitives::transaction::Transaction,
681) -> String {
682 let mut info_str = format!(
683 "\nunsigned transaction hash (Base58-encoded SHA-256 hash): {}",
684 transaction.get_hash_and_size().0
685 );
686
687 info_str.push_str(&format!(
688 "\n{:<13} {}",
689 "public_key:",
690 &transaction.public_key()
691 ));
692 info_str.push_str(&format!("\n{:<13} {}", "nonce:", &transaction.nonce()));
693 info_str.push_str(&format!(
694 "\n{:<13} {}",
695 "block_hash:",
696 &transaction.block_hash()
697 ));
698
699 let prepopulated = crate::commands::PrepopulatedTransaction::from(transaction);
700
701 info_str.push_str(&print_unsigned_transaction(&prepopulated));
702
703 info_str
704}
705
706pub fn print_unsigned_transaction(
707 transaction: &crate::commands::PrepopulatedTransaction,
708) -> String {
709 let mut info_str = String::new();
710 info_str.push_str(&format!("\n{:<13} {}", "signer_id:", transaction.signer_id));
711 info_str.push_str(&format!(
712 "\n{:<13} {}",
713 "receiver_id:", transaction.receiver_id
714 ));
715
716 if transaction
717 .actions
718 .iter()
719 .any(|action| matches!(action, near_primitives::transaction::Action::Delegate(_)))
720 {
721 info_str.push_str("\nsigned delegate action:");
722 } else {
723 info_str.push_str("\nactions:");
724 };
725
726 for action in &transaction.actions {
727 match action {
728 near_primitives::transaction::Action::CreateAccount(_) => {
729 info_str.push_str(&format!(
730 "\n{:>5} {:<20} {}",
731 "--", "create account:", &transaction.receiver_id
732 ));
733 }
734 near_primitives::transaction::Action::DeployContract(code) => {
735 let code_hash = CryptoHash::hash_bytes(&code.code);
736 info_str.push_str(&format!(
737 "\n{:>5} {:<70}",
738 "--",
739 format!(
740 "deploy code <{:?}> to a account <{}>",
741 code_hash, transaction.receiver_id
742 )
743 ))
744 }
745 near_primitives::transaction::Action::FunctionCall(function_call_action) => {
746 info_str.push_str(&format!("\n{:>5} {:<20}", "--", "function call:"));
747 info_str.push_str(&format!(
748 "\n{:>18} {:<13} {}",
749 "", "method name:", &function_call_action.method_name
750 ));
751 info_str.push_str(&format!(
752 "\n{:>18} {:<13} {}",
753 "",
754 "args:",
755 match serde_json::from_slice::<serde_json::Value>(&function_call_action.args) {
756 Ok(parsed_args) => {
757 serde_json::to_string_pretty(&parsed_args)
758 .unwrap_or_else(|_| "".to_string())
759 .replace('\n', "\n ")
760 }
761 Err(_) => {
762 if let Ok(args) = String::from_utf8(function_call_action.args.clone()) {
763 args
764 } else {
765 format!(
766 "<non-printable data ({})>",
767 bytesize::ByteSize(function_call_action.args.len() as u64)
768 )
769 }
770 }
771 }
772 ));
773 info_str.push_str(&format!(
774 "\n{:>18} {:<13} {}",
775 "",
776 "gas:",
777 crate::common::NearGas::from_gas(function_call_action.gas)
778 ));
779 info_str.push_str(&format!(
780 "\n{:>18} {:<13} {}",
781 "",
782 "deposit:",
783 crate::types::near_token::NearToken::from_yoctonear(
784 function_call_action.deposit
785 )
786 ));
787 }
788 near_primitives::transaction::Action::Transfer(transfer_action) => {
789 info_str.push_str(&format!(
790 "\n{:>5} {:<20} {}",
791 "--",
792 "transfer deposit:",
793 crate::types::near_token::NearToken::from_yoctonear(transfer_action.deposit)
794 ));
795 }
796 near_primitives::transaction::Action::Stake(stake_action) => {
797 info_str.push_str(&format!("\n{:>5} {:<20}", "--", "stake:"));
798 info_str.push_str(&format!(
799 "\n{:>18} {:<13} {}",
800 "", "public key:", &stake_action.public_key
801 ));
802 info_str.push_str(&format!(
803 "\n{:>18} {:<13} {}",
804 "",
805 "stake:",
806 crate::types::near_token::NearToken::from_yoctonear(stake_action.stake)
807 ));
808 }
809 near_primitives::transaction::Action::AddKey(add_key_action) => {
810 info_str.push_str(&format!("\n{:>5} {:<20}", "--", "add access key:"));
811 info_str.push_str(&format!(
812 "\n{:>18} {:<13} {}",
813 "", "public key:", &add_key_action.public_key
814 ));
815 info_str.push_str(&format!(
816 "\n{:>18} {:<13} {}",
817 "", "nonce:", &add_key_action.access_key.nonce
818 ));
819 info_str.push_str(&format!(
820 "\n{:>18} {:<13} {:?}",
821 "", "permission:", &add_key_action.access_key.permission
822 ));
823 }
824 near_primitives::transaction::Action::DeleteKey(delete_key_action) => {
825 info_str.push_str(&format!("\n{:>5} {:<20}", "--", "delete access key:"));
826 info_str.push_str(&format!(
827 "\n{:>18} {:<13} {}",
828 "", "public key:", &delete_key_action.public_key
829 ));
830 }
831 near_primitives::transaction::Action::DeleteAccount(delete_account_action) => {
832 info_str.push_str(&format!(
833 "\n{:>5} {:<20} {}",
834 "--", "delete account:", &transaction.receiver_id
835 ));
836 info_str.push_str(&format!(
837 "\n{:>8} {:<17} {}",
838 "", "beneficiary id:", &delete_account_action.beneficiary_id
839 ));
840 }
841 near_primitives::transaction::Action::Delegate(signed_delegate_action) => {
842 let prepopulated_transaction = crate::commands::PrepopulatedTransaction {
843 signer_id: signed_delegate_action.delegate_action.sender_id.clone(),
844 receiver_id: signed_delegate_action.delegate_action.receiver_id.clone(),
845 actions: signed_delegate_action.delegate_action.get_actions(),
846 };
847 info_str.push_str(&print_unsigned_transaction(&prepopulated_transaction));
848 }
849 near_primitives::transaction::Action::DeployGlobalContract(deploy) => {
850 let code_hash = CryptoHash::hash_bytes(&deploy.code);
851 let identifier = match deploy.deploy_mode {
852 GlobalContractDeployMode::CodeHash => {
853 format!("deploy code <{code_hash:?}> as a global hash")
854 }
855 GlobalContractDeployMode::AccountId => {
856 format!(
857 "deploy code <{:?}> to a global account <{}>",
858 code_hash, transaction.receiver_id
859 )
860 }
861 };
862 info_str.push_str(&format!("{:>5} {:<70}", "--", identifier));
863 }
864 near_primitives::transaction::Action::UseGlobalContract(contract_identifier) => {
865 let identifier = match contract_identifier.contract_identifier {
866 GlobalContractIdentifier::CodeHash(hash) => {
867 format!("use global <{hash}> code to deploy from")
868 }
869 GlobalContractIdentifier::AccountId(ref account_id) => {
870 format!("use global <{account_id}> code to deploy from")
871 }
872 };
873 info_str.push_str(&format!("{:>5} {:<70}", "--", identifier));
874 }
875 }
876 }
877 info_str.push('\n');
878 info_str
879}
880
881fn print_value_successful_transaction(
882 transaction_info: near_primitives::views::FinalExecutionOutcomeView,
883) -> String {
884 let mut info_str: String = String::from('\n');
885 for action in transaction_info.transaction.actions {
886 match action {
887 near_primitives::views::ActionView::CreateAccount => {
888 info_str.push_str(&format!(
889 "\nNew account <{}> has been successfully created.",
890 transaction_info.transaction.receiver_id,
891 ));
892 }
893 near_primitives::views::ActionView::DeployContract { code: _ } => {
894 info_str.push_str("Contract code has been successfully deployed.");
895 }
896 near_primitives::views::ActionView::FunctionCall {
897 method_name,
898 args: _,
899 gas: _,
900 deposit: _,
901 } => {
902 info_str.push_str(&format!(
903 "\nThe \"{}\" call to <{}> on behalf of <{}> succeeded.",
904 method_name,
905 transaction_info.transaction.receiver_id,
906 transaction_info.transaction.signer_id,
907 ));
908 }
909 near_primitives::views::ActionView::Transfer { deposit } => {
910 info_str.push_str(&format!(
911 "\n<{}> has transferred {} to <{}> successfully.",
912 transaction_info.transaction.signer_id,
913 crate::types::near_token::NearToken::from_yoctonear(deposit),
914 transaction_info.transaction.receiver_id,
915 ));
916 }
917 near_primitives::views::ActionView::Stake {
918 stake,
919 public_key: _,
920 } => {
921 if stake == 0 {
922 info_str.push_str(&format!(
923 "\nValidator <{}> successfully unstaked.",
924 transaction_info.transaction.signer_id,
925 ));
926 } else {
927 info_str.push_str(&format!(
928 "\nValidator <{}> has successfully staked {}.",
929 transaction_info.transaction.signer_id,
930 crate::types::near_token::NearToken::from_yoctonear(stake),
931 ));
932 }
933 }
934 near_primitives::views::ActionView::AddKey {
935 public_key,
936 access_key: _,
937 } => {
938 info_str.push_str(&format!(
939 "\nAdded access key = {} to {}.",
940 public_key, transaction_info.transaction.receiver_id,
941 ));
942 }
943 near_primitives::views::ActionView::DeleteKey { public_key } => {
944 info_str.push_str(&format!(
945 "\nAccess key <{}> for account <{}> has been successfully deleted.",
946 public_key, transaction_info.transaction.signer_id,
947 ));
948 }
949 near_primitives::views::ActionView::DeleteAccount { beneficiary_id: _ } => {
950 info_str.push_str(&format!(
951 "\nAccount <{}> has been successfully deleted.",
952 transaction_info.transaction.signer_id,
953 ));
954 }
955 near_primitives::views::ActionView::Delegate {
956 delegate_action,
957 signature: _,
958 } => {
959 info_str.push_str(&format!(
960 "Actions delegated for <{}> completed successfully.",
961 delegate_action.sender_id,
962 ));
963 }
964 near_primitives::views::ActionView::DeployGlobalContract { code: _ }
965 | near_primitives::views::ActionView::DeployGlobalContractByAccountId { code: _ } => {
966 info_str.push_str("Global contract has been successfully deployed.");
967 }
968 near_primitives::views::ActionView::UseGlobalContractByAccountId { account_id } => {
969 info_str.push_str(&format!("Contract has been successfully deployed with the code from the global account <{account_id}>."));
970 }
971 near_primitives::views::ActionView::UseGlobalContract { code_hash } => {
972 info_str.push_str(&format!("Contract has been successfully deployed with the code from the global hash <{code_hash}>."));
973 }
974 }
975 }
976 info_str.push('\n');
977 info_str
978}
979
980pub fn rpc_transaction_error(
981 err: &near_jsonrpc_client::errors::JsonRpcError<
982 near_jsonrpc_client::methods::broadcast_tx_commit::RpcTransactionError,
983 >,
984) -> color_eyre::Result<String> {
985 match &err {
986 near_jsonrpc_client::errors::JsonRpcError::TransportError(_rpc_transport_error) => {
987 Ok("Transport error transaction".to_string())
988 }
989 near_jsonrpc_client::errors::JsonRpcError::ServerError(rpc_server_error) => match rpc_server_error {
990 near_jsonrpc_client::errors::JsonRpcServerError::HandlerError(rpc_transaction_error) => match rpc_transaction_error {
991 near_jsonrpc_client::methods::broadcast_tx_commit::RpcTransactionError::TimeoutError => {
992 Ok("Timeout error transaction".to_string())
993 }
994 near_jsonrpc_client::methods::broadcast_tx_commit::RpcTransactionError::InvalidTransaction { context } => {
995 match convert_invalid_tx_error_to_cli_result(context) {
996 Ok(_) => Ok("".to_string()),
997 Err(err) => Err(err)
998 }
999 }
1000 near_jsonrpc_client::methods::broadcast_tx_commit::RpcTransactionError::DoesNotTrackShard => {
1001 color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!("RPC Server Error: {}", err))
1002 }
1003 near_jsonrpc_client::methods::broadcast_tx_commit::RpcTransactionError::RequestRouted{transaction_hash} => {
1004 color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!("RPC Server Error for transaction with hash {}\n{}", transaction_hash, err))
1005 }
1006 near_jsonrpc_client::methods::broadcast_tx_commit::RpcTransactionError::UnknownTransaction{requested_transaction_hash} => {
1007 color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!("RPC Server Error for transaction with hash {}\n{}", requested_transaction_hash, err))
1008 }
1009 near_jsonrpc_client::methods::broadcast_tx_commit::RpcTransactionError::InternalError{debug_info} => {
1010 color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!("RPC Server Error: {}", debug_info))
1011 }
1012 }
1013 near_jsonrpc_client::errors::JsonRpcServerError::RequestValidationError(rpc_request_validation_error) => {
1014 color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!("Incompatible request with the server: {:#?}", rpc_request_validation_error))
1015 }
1016 near_jsonrpc_client::errors::JsonRpcServerError::InternalError{ info } => {
1017 Ok(format!("Internal server error: {}", info.clone().unwrap_or_default()))
1018 }
1019 near_jsonrpc_client::errors::JsonRpcServerError::NonContextualError(rpc_error) => {
1020 color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!("Unexpected response: {}", rpc_error))
1021 }
1022 near_jsonrpc_client::errors::JsonRpcServerError::ResponseStatusError(json_rpc_server_response_status_error) => match json_rpc_server_response_status_error {
1023 near_jsonrpc_client::errors::JsonRpcServerResponseStatusError::Unauthorized => {
1024 color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!("JSON RPC server requires authentication. Please, authenticate near CLI with the JSON RPC server you use."))
1025 }
1026 near_jsonrpc_client::errors::JsonRpcServerResponseStatusError::TooManyRequests => {
1027 Ok("JSON RPC server is currently busy".to_string())
1028 }
1029 near_jsonrpc_client::errors::JsonRpcServerResponseStatusError::Unexpected{status} => {
1030 color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!("JSON RPC server responded with an unexpected status code: {}", status))
1031 }
1032 near_jsonrpc_client::errors::JsonRpcServerResponseStatusError::BadRequest => {
1033 color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!("JSON RPC server responded with a bad request. Please, check your request parameters."))
1034 }
1035 near_jsonrpc_client::errors::JsonRpcServerResponseStatusError::TimeoutError => {
1036 Ok("Timeout error while sending a request to the JSON RPC server".to_string())
1037 }
1038 near_jsonrpc_client::errors::JsonRpcServerResponseStatusError::ServiceUnavailable => {
1039 Ok("JSON RPC server is currently unavailable".to_string())
1040 }
1041 }
1042 }
1043 }
1044}
1045
1046pub fn convert_action_error_to_cli_result(
1047 action_error: &near_primitives::errors::ActionError,
1048) -> crate::CliResult {
1049 match &action_error.kind {
1050 near_primitives::errors::ActionErrorKind::AccountAlreadyExists { account_id } => {
1051 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))
1052 }
1053 near_primitives::errors::ActionErrorKind::AccountDoesNotExist { account_id } => {
1054 color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!(
1055 "Error: TX receiver ID <{}> doesn't exist (but action is not \"Create Account\").",
1056 account_id
1057 ))
1058 }
1059 near_primitives::errors::ActionErrorKind::CreateAccountOnlyByRegistrar {
1060 account_id: _,
1061 registrar_account_id: _,
1062 predecessor_id: _,
1063 } => {
1064 color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!("Error: A top-level account ID can only be created by registrar."))
1065 }
1066 near_primitives::errors::ActionErrorKind::CreateAccountNotAllowed {
1067 account_id,
1068 predecessor_id,
1069 } => {
1070 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))
1071 }
1072 near_primitives::errors::ActionErrorKind::ActorNoPermission {
1073 account_id: _,
1074 actor_id: _,
1075 } => {
1076 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."))
1077 }
1078 near_primitives::errors::ActionErrorKind::DeleteKeyDoesNotExist {
1079 account_id,
1080 public_key,
1081 } => {
1082 color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!(
1083 "Error: Account <{}> tries to remove an access key <{}> that doesn't exist.",
1084 account_id, public_key
1085 ))
1086 }
1087 near_primitives::errors::ActionErrorKind::AddKeyAlreadyExists {
1088 account_id,
1089 public_key,
1090 } => {
1091 color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!(
1092 "Error: Public key <{}> is already used for an existing account ID <{}>.",
1093 public_key, account_id
1094 ))
1095 }
1096 near_primitives::errors::ActionErrorKind::DeleteAccountStaking { account_id } => {
1097 color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!(
1098 "Error: Account <{}> is staking and can not be deleted",
1099 account_id
1100 ))
1101 }
1102 near_primitives::errors::ActionErrorKind::LackBalanceForState { account_id, amount } => {
1103 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: <{}>",
1104 account_id,
1105 crate::types::near_token::NearToken::from_yoctonear(*amount)
1106 ))
1107 }
1108 near_primitives::errors::ActionErrorKind::TriesToUnstake { account_id } => {
1109 color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!(
1110 "Error: Account <{}> is not yet staked, but tries to unstake.",
1111 account_id
1112 ))
1113 }
1114 near_primitives::errors::ActionErrorKind::TriesToStake {
1115 account_id,
1116 stake,
1117 locked: _,
1118 balance,
1119 } => {
1120 color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!(
1121 "Error: Account <{}> doesn't have enough balance ({}) to increase the stake ({}).",
1122 account_id,
1123 crate::types::near_token::NearToken::from_yoctonear(*balance),
1124 crate::types::near_token::NearToken::from_yoctonear(*stake)
1125 ))
1126 }
1127 near_primitives::errors::ActionErrorKind::InsufficientStake {
1128 account_id: _,
1129 stake,
1130 minimum_stake,
1131 } => {
1132 color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!(
1133 "Error: Insufficient stake {}.\nThe minimum rate must be {}.",
1134 crate::types::near_token::NearToken::from_yoctonear(*stake),
1135 crate::types::near_token::NearToken::from_yoctonear(*minimum_stake)
1136 ))
1137 }
1138 near_primitives::errors::ActionErrorKind::FunctionCallError(function_call_error_ser) => {
1139 color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!("Error: An error occurred during a `FunctionCall` action.\n{:?}", function_call_error_ser))
1140 }
1141 near_primitives::errors::ActionErrorKind::NewReceiptValidationError(
1142 receipt_validation_error,
1143 ) => {
1144 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))
1145 }
1146 near_primitives::errors::ActionErrorKind::OnlyImplicitAccountCreationAllowed {
1147 account_id: _,
1148 } => {
1149 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/nearprotocol/NEPs/pull/71"))
1150 }
1151 near_primitives::errors::ActionErrorKind::DeleteAccountWithLargeState { account_id } => {
1152 color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!(
1153 "Error: Delete account <{}> whose state is large is temporarily banned.",
1154 account_id
1155 ))
1156 }
1157 near_primitives::errors::ActionErrorKind::DelegateActionInvalidSignature => {
1158 color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!("Error: Invalid Signature on DelegateAction"))
1159 }
1160 near_primitives::errors::ActionErrorKind::DelegateActionSenderDoesNotMatchTxReceiver {
1161 sender_id,
1162 receiver_id,
1163 } => {
1164 color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!("Error: Delegate Action sender {sender_id} does not match transaction receiver {receiver_id}"))
1165 }
1166 near_primitives::errors::ActionErrorKind::DelegateActionExpired => {
1167 color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!("Error: DelegateAction Expired"))
1168 }
1169 near_primitives::errors::ActionErrorKind::DelegateActionAccessKeyError(_) => {
1170 color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!("Error: The given public key doesn't exist for the sender"))
1171 }
1172 near_primitives::errors::ActionErrorKind::DelegateActionInvalidNonce {
1173 delegate_nonce,
1174 ak_nonce,
1175 } => {
1176 color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!("Error: DelegateAction Invalid Delegate Nonce: {delegate_nonce} ak_nonce: {ak_nonce}"))
1177 }
1178 near_primitives::errors::ActionErrorKind::DelegateActionNonceTooLarge {
1179 delegate_nonce,
1180 upper_bound,
1181 } => {
1182 color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!("Error: DelegateAction Invalid Delegate Nonce: {delegate_nonce} upper bound: {upper_bound}"))
1183 },
1184 near_primitives::errors::ActionErrorKind::GlobalContractDoesNotExist { identifier } => {
1185 let identifier = match identifier {
1186 near_primitives::action::GlobalContractIdentifier::CodeHash(hash) => format!("hash<{hash}>"),
1187 near_primitives::action::GlobalContractIdentifier::AccountId(account_id) => format!("account id<{account_id}>"),
1188 };
1189 color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!("Error: Global contract with identifier {} does not exist.", identifier))
1190 }
1191 }
1192}
1193
1194pub fn convert_invalid_tx_error_to_cli_result(
1195 invalid_tx_error: &near_primitives::errors::InvalidTxError,
1196) -> crate::CliResult {
1197 match invalid_tx_error {
1198 near_primitives::errors::InvalidTxError::InvalidAccessKeyError(invalid_access_key_error) => {
1199 match invalid_access_key_error {
1200 near_primitives::errors::InvalidAccessKeyError::AccessKeyNotFound{account_id, public_key} => {
1201 color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!("Error: Public key {} doesn't exist for the account <{}>.", public_key, account_id))
1202 },
1203 near_primitives::errors::InvalidAccessKeyError::ReceiverMismatch{tx_receiver, ak_receiver} => {
1204 color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!("Error: Transaction for <{}> doesn't match the access key for <{}>.", tx_receiver, ak_receiver))
1205 },
1206 near_primitives::errors::InvalidAccessKeyError::MethodNameMismatch{method_name} => {
1207 color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!("Error: Transaction method name <{}> isn't allowed by the access key.", method_name))
1208 },
1209 near_primitives::errors::InvalidAccessKeyError::RequiresFullAccess => {
1210 color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!("Error: Transaction requires a full permission access key."))
1211 },
1212 near_primitives::errors::InvalidAccessKeyError::NotEnoughAllowance{account_id, public_key, allowance, cost} => {
1213 color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!("Error: Access Key <{}> for account <{}> does not have enough allowance ({}) to cover transaction cost ({}).",
1214 public_key,
1215 account_id,
1216 crate::types::near_token::NearToken::from_yoctonear(*allowance),
1217 crate::types::near_token::NearToken::from_yoctonear(*cost)
1218 ))
1219 },
1220 near_primitives::errors::InvalidAccessKeyError::DepositWithFunctionCall => {
1221 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."))
1222 }
1223 }
1224 },
1225 near_primitives::errors::InvalidTxError::InvalidSignerId { signer_id } => {
1226 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 \"near_runtime_utils::utils::is_valid_account_id\".", signer_id))
1227 },
1228 near_primitives::errors::InvalidTxError::SignerDoesNotExist { signer_id } => {
1229 color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!("Error: TX signer ID <{}> is not found in the storage.", signer_id))
1230 },
1231 near_primitives::errors::InvalidTxError::InvalidNonce { tx_nonce, ak_nonce } => {
1232 color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!("Error: Transaction nonce ({}) must be account[access_key].nonce ({}) + 1.", tx_nonce, ak_nonce))
1233 },
1234 near_primitives::errors::InvalidTxError::NonceTooLarge { tx_nonce, upper_bound } => {
1235 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))
1236 },
1237 near_primitives::errors::InvalidTxError::InvalidReceiverId { receiver_id } => {
1238 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 \"near_runtime_utils::is_valid_account_id\".", receiver_id))
1239 },
1240 near_primitives::errors::InvalidTxError::InvalidSignature => {
1241 color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!("Error: TX signature is not valid"))
1242 },
1243 near_primitives::errors::InvalidTxError::NotEnoughBalance {signer_id, balance, cost} => {
1244 color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!("Error: Account <{}> does not have enough balance ({}) to cover TX cost ({}).",
1245 signer_id,
1246 crate::types::near_token::NearToken::from_yoctonear(*balance),
1247 crate::types::near_token::NearToken::from_yoctonear(*cost)
1248 ))
1249 },
1250 near_primitives::errors::InvalidTxError::LackBalanceForState {signer_id, amount} => {
1251 color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!("Error: Signer account <{}> doesn't have enough balance ({}) after transaction.",
1252 signer_id,
1253 crate::types::near_token::NearToken::from_yoctonear(*amount)
1254 ))
1255 },
1256 near_primitives::errors::InvalidTxError::CostOverflow => {
1257 color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!("Error: An integer overflow occurred during transaction cost estimation."))
1258 },
1259 near_primitives::errors::InvalidTxError::InvalidChain => {
1260 color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!("Error: Transaction parent block hash doesn't belong to the current chain."))
1261 },
1262 near_primitives::errors::InvalidTxError::Expired => {
1263 color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!("Error: Transaction has expired."))
1264 },
1265 near_primitives::errors::InvalidTxError::ActionsValidation(actions_validation_error) => {
1266 match actions_validation_error {
1267 near_primitives::errors::ActionsValidationError::DeleteActionMustBeFinal => {
1268 color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!("Error: The delete action must be the final action in transaction."))
1269 },
1270 near_primitives::errors::ActionsValidationError::TotalPrepaidGasExceeded {total_prepaid_gas, limit} => {
1271 color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!("Error: The total prepaid gas ({}) for all given actions exceeded the limit ({}).",
1272 total_prepaid_gas,
1273 limit
1274 ))
1275 },
1276 near_primitives::errors::ActionsValidationError::TotalNumberOfActionsExceeded {total_number_of_actions, limit} => {
1277 color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!("Error: The number of actions ({}) exceeded the given limit ({}).", total_number_of_actions, limit))
1278 },
1279 near_primitives::errors::ActionsValidationError::AddKeyMethodNamesNumberOfBytesExceeded {total_number_of_bytes, limit} => {
1280 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))
1281 },
1282 near_primitives::errors::ActionsValidationError::AddKeyMethodNameLengthExceeded {length, limit} => {
1283 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))
1284 },
1285 near_primitives::errors::ActionsValidationError::IntegerOverflow => {
1286 color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!("Error: Integer overflow."))
1287 },
1288 near_primitives::errors::ActionsValidationError::InvalidAccountId {account_id} => {
1289 color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!("Error: Invalid account ID <{}>.", account_id))
1290 },
1291 near_primitives::errors::ActionsValidationError::ContractSizeExceeded {size, limit} => {
1292 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))
1293 },
1294 near_primitives::errors::ActionsValidationError::FunctionCallMethodNameLengthExceeded {length, limit} => {
1295 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))
1296 },
1297 near_primitives::errors::ActionsValidationError::FunctionCallArgumentsLengthExceeded {length, limit} => {
1298 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))
1299 },
1300 near_primitives::errors::ActionsValidationError::UnsuitableStakingKey {public_key} => {
1301 color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!("Error: An attempt to stake with a public key <{}> that is not convertible to ristretto.", public_key))
1302 },
1303 near_primitives::errors::ActionsValidationError::FunctionCallZeroAttachedGas => {
1304 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."))
1305 }
1306 near_primitives::errors::ActionsValidationError::DelegateActionMustBeOnlyOne => {
1307 color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!("Error: The transaction contains more than one delegation action"))
1308 }
1309 near_primitives::errors::ActionsValidationError::UnsupportedProtocolFeature { protocol_feature, version } => {
1310 color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!("Error: Protocol Feature {} is unsupported in version {}", protocol_feature, version))
1311 }
1312 }
1313 },
1314 near_primitives::errors::InvalidTxError::TransactionSizeExceeded { size, limit } => {
1315 color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!("Error: The size ({}) of serialized transaction exceeded the limit ({}).", size, limit))
1316 }
1317 near_primitives::errors::InvalidTxError::InvalidTransactionVersion => {
1318 color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!("Error: Invalid transaction version"))
1319 },
1320 near_primitives::errors::InvalidTxError::StorageError(error) => match error {
1321 near_primitives::errors::StorageError::StorageInternalError => color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!("Error: Internal storage error")),
1322 near_primitives::errors::StorageError::MissingTrieValue(missing_trie_value) => color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!("Error: Requested trie value by its hash ({}) which is missing in the storage", missing_trie_value.hash)),
1323 near_primitives::errors::StorageError::UnexpectedTrieValue => color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!("Error: Unexpected trie value")),
1324 near_primitives::errors::StorageError::StorageInconsistentState(message) => color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!("Error: The storage is in the inconsistent state: {}", message)),
1325 near_primitives::errors::StorageError::FlatStorageBlockNotSupported(message) => color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!("Error: The block is not supported by flat storage: {}", message)),
1326 near_primitives::errors::StorageError::MemTrieLoadingError(message) => color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!("Error: The trie is not loaded in memory: {}", message)),
1327 },
1328 near_primitives::errors::InvalidTxError::ShardCongested { shard_id, congestion_level } => color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!("Error: The shard ({shard_id}) is too congested ({congestion_level:.2}/1.00) and can't accept new transaction")),
1329 near_primitives::errors::InvalidTxError::ShardStuck { shard_id, missed_chunks } => color_eyre::eyre::Result::Err(color_eyre::eyre::eyre!("Error: The shard ({shard_id}) is {missed_chunks} blocks behind and can't accept new transaction until it will be in the sync")),
1330 }
1331}
1332
1333fn get_near_usd_exchange_rate(coingecko_url: &url::Url) -> color_eyre::Result<f64> {
1334 #[derive(serde::Deserialize)]
1335 struct CoinGeckoResponse {
1336 near: CoinGeckoNearData,
1337 }
1338
1339 #[derive(serde::Deserialize)]
1340 struct CoinGeckoNearData {
1341 usd: f64,
1342 }
1343
1344 let coingecko_exchange_rate_api_url =
1345 coingecko_url.join("api/v3/simple/price?ids=near&vs_currencies=usd")?;
1346 let mut last_error_message = String::new();
1347
1348 for _ in 0..10 {
1349 match reqwest::blocking::get(coingecko_exchange_rate_api_url.clone()) {
1350 Ok(response) => match response.json::<CoinGeckoResponse>() {
1351 Ok(parsed_body) => return Ok(parsed_body.near.usd),
1352 Err(err) => {
1353 last_error_message =
1354 format!("Failed to parse the response from Coingecko API as JSON: {err}");
1355 }
1356 },
1357 Err(err) => {
1358 last_error_message =
1359 format!("Failed to get the response from Coingecko API: {err}");
1360 }
1361 }
1362
1363 std::thread::sleep(std::time::Duration::from_millis(100));
1364 }
1365
1366 Err(color_eyre::eyre::eyre!(last_error_message))
1367}
1368
1369fn calculate_usd_amount(tokens: u128, price: f64) -> Option<rust_decimal::Decimal> {
1370 let tokens_decimal = rust_decimal::Decimal::from_u128(tokens)?;
1371 let price_decimal = rust_decimal::Decimal::from_f64(price)?;
1372
1373 let divisor = rust_decimal::Decimal::from_u128(10u128.pow(24))?;
1374 let tokens_decimal = tokens_decimal / divisor;
1375
1376 Some(tokens_decimal * price_decimal)
1377}
1378
1379pub fn print_transaction_status(
1380 transaction_info: &near_primitives::views::FinalExecutionOutcomeView,
1381 network_config: &crate::config::NetworkConfig,
1382 verbosity: &crate::Verbosity,
1383) -> crate::CliResult {
1384 let near_usd_exchange_rate: Option<Result<f64, color_eyre::eyre::Error>> = network_config
1385 .coingecko_url
1386 .as_ref()
1387 .map(get_near_usd_exchange_rate);
1388
1389 let mut logs_info = String::new();
1390 let mut total_gas_burnt = transaction_info.transaction_outcome.outcome.gas_burnt;
1391 let mut total_tokens_burnt = transaction_info.transaction_outcome.outcome.tokens_burnt;
1392
1393 for receipt in transaction_info.receipts_outcome.iter() {
1394 total_gas_burnt += receipt.outcome.gas_burnt;
1395 total_tokens_burnt += receipt.outcome.tokens_burnt;
1396
1397 if receipt.outcome.logs.is_empty() {
1398 logs_info.push_str(&format!(
1399 "\nLogs [{}]: No logs",
1400 receipt.outcome.executor_id
1401 ));
1402 } else {
1403 logs_info.push_str(&format!("\nLogs [{}]:", receipt.outcome.executor_id));
1404 logs_info.push_str(&format!("\n {}", receipt.outcome.logs.join("\n ")));
1405 };
1406 }
1407 logs_info.push_str("\n------------------------------------");
1408
1409 tracing::info!(
1410 target: "near_teach_me",
1411 parent: &tracing::Span::none(),
1412 "--- Logs ---------------------------{}",
1413 crate::common::indent_payload(&logs_info)
1414 );
1415
1416 let mut result_info = String::new();
1417 let mut result_output = String::new();
1418
1419 let return_value = match &transaction_info.status {
1420 near_primitives::views::FinalExecutionStatus::NotStarted => {
1421 if let crate::Verbosity::Quiet = verbosity {
1422 return Ok(());
1423 }
1424 tracing::warn!(
1425 parent: &tracing::Span::none(),
1426 "WARNING! The execution has not yet started."
1427 );
1428 Ok(())
1429 }
1430 near_primitives::views::FinalExecutionStatus::Started => {
1431 if let crate::Verbosity::Quiet = verbosity {
1432 return Ok(());
1433 }
1434 tracing::warn!(
1435 parent: &tracing::Span::none(),
1436 "WARNING! The execution has started and still going."
1437 );
1438 Ok(())
1439 }
1440 near_primitives::views::FinalExecutionStatus::Failure(tx_execution_error) => {
1441 match tx_execution_error {
1442 near_primitives::errors::TxExecutionError::ActionError(action_error) => {
1443 convert_action_error_to_cli_result(action_error)
1444 }
1445 near_primitives::errors::TxExecutionError::InvalidTxError(invalid_tx_error) => {
1446 convert_invalid_tx_error_to_cli_result(invalid_tx_error)
1447 }
1448 }
1449 }
1450 near_primitives::views::FinalExecutionStatus::SuccessValue(bytes_result) => {
1451 if let crate::Verbosity::Quiet = verbosity {
1452 std::io::stdout().write_all(bytes_result)?;
1453 return Ok(());
1454 };
1455 let result = if bytes_result.is_empty() {
1456 "Empty result".to_string()
1457 } else if let Ok(json_result) =
1458 serde_json::from_slice::<serde_json::Value>(bytes_result)
1459 {
1460 serde_json::to_string_pretty(&json_result)?
1461 } else if let Ok(string_result) = String::from_utf8(bytes_result.clone()) {
1462 string_result
1463 } else {
1464 "The returned value is not printable (binary data)".to_string()
1465 };
1466 if let crate::Verbosity::Interactive = verbosity {
1467 for action in &transaction_info.transaction.actions {
1468 if let near_primitives::views::ActionView::FunctionCall { .. } = action {
1469 tracing::info!(
1470 parent: &tracing::Span::none(),
1471 "Function execution logs ------------{}",
1472 crate::common::indent_payload(&logs_info)
1473 );
1474 tracing::info!(
1475 parent: &tracing::Span::none(),
1476 "Function execution return value (printed to stdout):"
1477 );
1478 suspend_tracing_indicatif(|| println!("{result}"));
1479 }
1480 }
1481 }
1482 result_info.push_str(&result);
1483 result_info.push_str("\n------------------------------------");
1484
1485 result_output.push_str(&print_value_successful_transaction(
1486 transaction_info.clone(),
1487 ));
1488 tracing::info!(
1489 target: "near_teach_me",
1490 parent: &tracing::Span::none(),
1491 "--- Result -------------------------\n{}",
1492 crate::common::indent_payload(&result_info)
1493 );
1494 Ok(())
1495 }
1496 };
1497
1498 result_output.push_str(&format!(
1499 "\nGas burned: {}",
1500 NearGas::from_gas(total_gas_burnt)
1501 ));
1502
1503 result_output.push_str(&format!(
1504 "\nTransaction fee: {}{}",
1505 crate::types::near_token::NearToken::from_yoctonear(total_tokens_burnt),
1506 match near_usd_exchange_rate {
1507 Some(Ok(exchange_rate)) => calculate_usd_amount(total_tokens_burnt, exchange_rate).map_or_else(
1508 || format!(" (USD equivalent is too big to be displayed, using ${exchange_rate:.2} USD/NEAR exchange rate)"),
1509 |amount| format!(" (approximately ${amount:.8} USD, using ${exchange_rate:.2} USD/NEAR exchange rate)")
1510 ),
1511 Some(Err(err)) => format!(" (USD equivalent is unavailable due to an error: {err})"),
1512 None => String::new(),
1513 }
1514 ));
1515
1516 result_output.push_str(&format!(
1517 "\nTransaction ID: {id}\nTo see the transaction in the transaction explorer, please open this url in your browser:\n{path}{id}\n",
1518 id=transaction_info.transaction_outcome.id,
1519 path=network_config.explorer_transaction_url
1520 ));
1521
1522 if result_info.is_empty() {
1523 tracing::error!(
1524 parent: &tracing::Span::none(),
1525 "Transaction failed{}",
1526 crate::common::indent_payload(&result_output)
1527 );
1528 } else {
1529 tracing::info!(
1530 parent: &tracing::Span::none(),
1531 "{}",
1532 crate::common::indent_payload(&result_output)
1533 );
1534 }
1535 return_value
1536}
1537
1538pub fn save_access_key_to_keychain_or_save_to_legacy_keychain(
1539 network_config: crate::config::NetworkConfig,
1540 credentials_home_dir: std::path::PathBuf,
1541 key_pair_properties_buf: &str,
1542 public_key_str: &str,
1543 account_id: &str,
1544) -> color_eyre::eyre::Result<String> {
1545 match save_access_key_to_keychain(
1546 network_config.clone(),
1547 key_pair_properties_buf,
1548 public_key_str,
1549 account_id,
1550 ) {
1551 Ok(message) => Ok(message),
1552 Err(err) => {
1553 let info_str = format!(
1554 "{}\n{}\n",
1555 format!("Failed to save the access key <{public_key_str}> to the keychain.\n{err}")
1556 .red(),
1557 "The data for the access key will be stored in the legacy keychain.".red()
1558 );
1559 tracing::warn!(
1560 parent: &tracing::Span::none(),
1561 "\n{}",
1562 indent_payload(&info_str)
1563 );
1564 save_access_key_to_legacy_keychain(
1565 network_config.clone(),
1566 credentials_home_dir,
1567 key_pair_properties_buf,
1568 public_key_str,
1569 account_id,
1570 )
1571 }
1572 }
1573}
1574
1575pub fn save_access_key_to_keychain(
1576 network_config: crate::config::NetworkConfig,
1577 key_pair_properties_buf: &str,
1578 public_key_str: &str,
1579 account_id: &str,
1580) -> color_eyre::eyre::Result<String> {
1581 let service_name = std::borrow::Cow::Owned(format!(
1582 "near-{}-{}",
1583 network_config.network_name, account_id
1584 ));
1585
1586 keyring::Entry::new(&service_name, &format!("{account_id}:{public_key_str}"))
1587 .wrap_err("Failed to open keychain")?
1588 .set_password(key_pair_properties_buf)
1589 .wrap_err("Failed to save password to keychain. You may need to install the secure keychain package by following this instruction: https://github.com/jaraco/keyring#using-keyring-on-headless-linux-systems")?;
1590
1591 Ok("The data for the access key is saved in the keychain".to_string())
1592}
1593
1594pub fn save_access_key_to_legacy_keychain(
1595 network_config: crate::config::NetworkConfig,
1596 credentials_home_dir: std::path::PathBuf,
1597 key_pair_properties_buf: &str,
1598 public_key_str: &str,
1599 account_id: &str,
1600) -> color_eyre::eyre::Result<String> {
1601 let dir_name = network_config.network_name.as_str();
1602 let file_with_key_name: std::path::PathBuf =
1603 format!("{}.json", public_key_str.replace(':', "_")).into();
1604 let mut path_with_key_name = std::path::PathBuf::from(&credentials_home_dir);
1605 path_with_key_name.push(dir_name);
1606 path_with_key_name.push(account_id);
1607 std::fs::create_dir_all(&path_with_key_name)?;
1608 path_with_key_name.push(file_with_key_name);
1609 let message_1 = if path_with_key_name.exists() {
1610 format!(
1611 "The file: {} already exists! Therefore it was not overwritten.",
1612 &path_with_key_name.display()
1613 )
1614 } else {
1615 std::fs::File::create(&path_with_key_name)
1616 .wrap_err_with(|| format!("Failed to create file: {path_with_key_name:?}"))?
1617 .write(key_pair_properties_buf.as_bytes())
1618 .wrap_err_with(|| format!("Failed to write to file: {path_with_key_name:?}"))?;
1619 format!(
1620 "The data for the access key is saved in a file {}",
1621 &path_with_key_name.display()
1622 )
1623 };
1624
1625 let file_with_account_name: std::path::PathBuf = format!("{account_id}.json").into();
1626 let mut path_with_account_name = std::path::PathBuf::from(&credentials_home_dir);
1627 path_with_account_name.push(dir_name);
1628 path_with_account_name.push(file_with_account_name);
1629 if path_with_account_name.exists() {
1630 Ok(format!(
1631 "{}\nThe file: {} already exists! Therefore it was not overwritten.",
1632 message_1,
1633 &path_with_account_name.display()
1634 ))
1635 } else {
1636 std::fs::File::create(&path_with_account_name)
1637 .wrap_err_with(|| format!("Failed to create file: {path_with_account_name:?}"))?
1638 .write(key_pair_properties_buf.as_bytes())
1639 .wrap_err_with(|| format!("Failed to write to file: {path_with_account_name:?}"))?;
1640 Ok(format!(
1641 "{}\nThe data for the access key is saved in a file {}",
1642 message_1,
1643 &path_with_account_name.display()
1644 ))
1645 }
1646}
1647
1648pub fn try_external_subcommand_execution(error: clap::Error) -> CliResult {
1649 let (subcommand, args) = {
1650 let mut args = std::env::args().skip(1);
1651 let subcommand = args
1652 .next()
1653 .ok_or_else(|| color_eyre::eyre::eyre!("subcommand is not provided"))?;
1654 (subcommand, args.collect::<Vec<String>>())
1655 };
1656 let is_top_level_command_known = crate::commands::TopLevelCommandDiscriminants::iter()
1657 .map(|x| format!("{:?}", &x).to_lowercase())
1658 .any(|x| x == subcommand);
1659 if is_top_level_command_known {
1660 error.exit()
1661 }
1662 let subcommand_exe = format!("near-{}{}", subcommand, std::env::consts::EXE_SUFFIX);
1663
1664 let path = path_directories()
1665 .iter()
1666 .map(|dir| dir.join(&subcommand_exe))
1667 .find(|file| is_executable(file));
1668
1669 let command = path.ok_or_else(|| {
1670 color_eyre::eyre::eyre!(
1671 "{} command or {} extension does not exist",
1672 subcommand,
1673 subcommand_exe
1674 )
1675 })?;
1676
1677 let err = match cargo_util::ProcessBuilder::new(command)
1678 .args(&args)
1679 .exec_replace()
1680 {
1681 Ok(()) => return Ok(()),
1682 Err(e) => e,
1683 };
1684
1685 if let Some(perr) = err.downcast_ref::<cargo_util::ProcessError>() {
1686 if let Some(code) = perr.code {
1687 return Err(color_eyre::eyre::eyre!("perror occurred, code: {}", code));
1688 }
1689 }
1690 Err(color_eyre::eyre::eyre!(err))
1691}
1692
1693fn is_executable<P: AsRef<std::path::Path>>(path: P) -> bool {
1694 #[cfg(target_family = "unix")]
1695 {
1696 use std::os::unix::prelude::*;
1697 std::fs::metadata(path)
1698 .map(|metadata| metadata.is_file() && metadata.permissions().mode() & 0o111 != 0)
1699 .unwrap_or(false)
1700 }
1701 #[cfg(target_family = "windows")]
1702 path.as_ref().is_file()
1703}
1704
1705fn path_directories() -> Vec<std::path::PathBuf> {
1706 if let Some(val) = std::env::var_os("PATH") {
1707 std::env::split_paths(&val).collect()
1708 } else {
1709 Vec::new()
1710 }
1711}
1712
1713pub fn get_delegated_validator_list_from_mainnet(
1714 network_connection: &linked_hash_map::LinkedHashMap<String, crate::config::NetworkConfig>,
1715) -> color_eyre::eyre::Result<std::collections::BTreeSet<near_primitives::types::AccountId>> {
1716 let network_config = network_connection
1717 .get("mainnet")
1718 .wrap_err("There is no 'mainnet' network in your configuration.")?;
1719
1720 let epoch_validator_info = network_config
1721 .json_rpc_client()
1722 .blocking_call(
1723 &near_jsonrpc_client::methods::validators::RpcValidatorRequest {
1724 epoch_reference: near_primitives::types::EpochReference::Latest,
1725 },
1726 )
1727 .wrap_err("Failed to get epoch validators information request.")?;
1728
1729 Ok(epoch_validator_info
1730 .current_proposals
1731 .into_iter()
1732 .map(|current_proposal| current_proposal.take_account_id())
1733 .chain(
1734 epoch_validator_info
1735 .current_validators
1736 .into_iter()
1737 .map(|current_validator| current_validator.account_id),
1738 )
1739 .chain(
1740 epoch_validator_info
1741 .next_validators
1742 .into_iter()
1743 .map(|next_validator| next_validator.account_id),
1744 )
1745 .collect())
1746}
1747
1748#[tracing::instrument(
1749 name = "Retrieving a list of delegated validators from \"mainnet\" ...",
1750 skip_all
1751)]
1752pub fn get_used_delegated_validator_list(
1753 config: &crate::config::Config,
1754) -> color_eyre::eyre::Result<VecDeque<near_primitives::types::AccountId>> {
1755 let used_account_list: VecDeque<UsedAccount> =
1756 get_used_account_list(&config.credentials_home_dir);
1757 let mut delegated_validator_list =
1758 get_delegated_validator_list_from_mainnet(&config.network_connection)?;
1759 let mut used_delegated_validator_list: VecDeque<near_primitives::types::AccountId> =
1760 VecDeque::new();
1761
1762 for used_account in used_account_list {
1763 if delegated_validator_list.remove(&used_account.account_id) {
1764 used_delegated_validator_list.push_back(used_account.account_id);
1765 }
1766 }
1767
1768 used_delegated_validator_list.extend(delegated_validator_list);
1769 Ok(used_delegated_validator_list)
1770}
1771
1772pub fn input_staking_pool_validator_account_id(
1773 config: &crate::config::Config,
1774) -> color_eyre::eyre::Result<Option<crate::types::account_id::AccountId>> {
1775 let used_delegated_validator_list = get_used_delegated_validator_list(config)?
1776 .into_iter()
1777 .map(String::from)
1778 .collect::<Vec<_>>();
1779 let validator_account_id_str = match Text::new("What is delegated validator account ID?")
1780 .with_autocomplete(move |val: &str| {
1781 Ok(used_delegated_validator_list
1782 .iter()
1783 .filter(|s| s.contains(val))
1784 .cloned()
1785 .collect())
1786 })
1787 .with_validator(|account_id_str: &str| {
1788 match near_primitives::types::AccountId::validate(account_id_str) {
1789 Ok(_) => Ok(inquire::validator::Validation::Valid),
1790 Err(err) => Ok(inquire::validator::Validation::Invalid(
1791 inquire::validator::ErrorMessage::Custom(format!("Invalid account ID: {err}")),
1792 )),
1793 }
1794 })
1795 .prompt()
1796 {
1797 Ok(value) => value,
1798 Err(
1799 inquire::error::InquireError::OperationCanceled
1800 | inquire::error::InquireError::OperationInterrupted,
1801 ) => return Ok(None),
1802 Err(err) => return Err(err.into()),
1803 };
1804 let validator_account_id =
1805 crate::types::account_id::AccountId::from_str(&validator_account_id_str)?;
1806 update_used_account_list_as_non_signer(
1807 &config.credentials_home_dir,
1808 validator_account_id.as_ref(),
1809 );
1810 Ok(Some(validator_account_id))
1811}
1812
1813#[derive(Debug, Clone, PartialEq, Eq)]
1814pub struct StakingPoolInfo {
1815 pub validator_id: near_primitives::types::AccountId,
1816 pub fee: Option<RewardFeeFraction>,
1817 pub delegators: Option<u64>,
1818 pub stake: near_primitives::types::Balance,
1819}
1820
1821#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize)]
1822pub struct RewardFeeFraction {
1823 pub numerator: u32,
1824 pub denominator: u32,
1825}
1826
1827#[tracing::instrument(name = "Getting a list of validators ...", skip_all)]
1828pub fn get_validator_list(
1829 network_config: &crate::config::NetworkConfig,
1830) -> color_eyre::eyre::Result<Vec<StakingPoolInfo>> {
1831 let json_rpc_client = network_config.json_rpc_client();
1832
1833 let validators_stake = get_validators_stake(&json_rpc_client)?;
1834
1835 let runtime = tokio::runtime::Builder::new_multi_thread()
1836 .enable_all()
1837 .build()?;
1838 let concurrency = 10;
1839
1840 let mut validator_list = runtime.block_on(
1841 futures::stream::iter(validators_stake.iter())
1842 .map(|(validator_account_id, stake)| async {
1843 get_staking_pool_info(
1844 &json_rpc_client.clone(),
1845 validator_account_id.clone(),
1846 *stake,
1847 )
1848 .await
1849 })
1850 .buffer_unordered(concurrency)
1851 .try_collect::<Vec<_>>(),
1852 )?;
1853 validator_list.sort_by(|a, b| b.stake.cmp(&a.stake));
1854 Ok(validator_list)
1855}
1856
1857#[derive(Debug, serde::Deserialize)]
1858struct StakingPool {
1859 pool_id: near_primitives::types::AccountId,
1860}
1861
1862#[derive(Debug, serde::Deserialize)]
1863struct StakingResponse {
1864 pools: Vec<StakingPool>,
1865}
1866
1867#[tracing::instrument(name = "Getting historically delegated staking pools ...", skip_all)]
1868pub fn fetch_historically_delegated_staking_pools(
1869 fastnear_url: &url::Url,
1870 account_id: &near_primitives::types::AccountId,
1871) -> color_eyre::Result<std::collections::BTreeSet<near_primitives::types::AccountId>> {
1872 let request =
1873 reqwest::blocking::get(fastnear_url.join(&format!("v1/account/{account_id}/staking"))?)?;
1874 let response: StakingResponse = request.json()?;
1875
1876 Ok(response
1877 .pools
1878 .into_iter()
1879 .map(|pool| pool.pool_id)
1880 .collect())
1881}
1882
1883#[tracing::instrument(name = "Getting currently active staking pools ...", skip_all)]
1884pub fn fetch_currently_active_staking_pools(
1885 json_rpc_client: &near_jsonrpc_client::JsonRpcClient,
1886 staking_pools_factory_account_id: &near_primitives::types::AccountId,
1887) -> color_eyre::Result<std::collections::BTreeSet<near_primitives::types::AccountId>> {
1888 let query_view_method_response = json_rpc_client
1889 .blocking_call(near_jsonrpc_client::methods::query::RpcQueryRequest {
1890 block_reference: near_primitives::types::Finality::Final.into(),
1891 request: near_primitives::views::QueryRequest::ViewState {
1892 account_id: staking_pools_factory_account_id.clone(),
1893 prefix: near_primitives::types::StoreKey::from(b"se".to_vec()),
1894 include_proof: false,
1895 },
1896 })
1897 .map_err(color_eyre::Report::msg)?;
1898 if let near_jsonrpc_primitives::types::query::QueryResponseKind::ViewState(result) =
1899 query_view_method_response.kind
1900 {
1901 Ok(result
1902 .values
1903 .into_iter()
1904 .filter_map(|item| near_primitives::borsh::from_slice(&item.value).ok())
1905 .collect())
1906 } else {
1907 Err(color_eyre::Report::msg("Error call result".to_string()))
1908 }
1909}
1910
1911#[tracing::instrument(name = "Getting a stake of validators ...", skip_all)]
1912pub fn get_validators_stake(
1913 json_rpc_client: &near_jsonrpc_client::JsonRpcClient,
1914) -> color_eyre::eyre::Result<
1915 std::collections::HashMap<near_primitives::types::AccountId, near_primitives::types::Balance>,
1916> {
1917 let epoch_validator_info = json_rpc_client
1918 .blocking_call(
1919 &near_jsonrpc_client::methods::validators::RpcValidatorRequest {
1920 epoch_reference: near_primitives::types::EpochReference::Latest,
1921 },
1922 )
1923 .wrap_err("Failed to get epoch validators information request.")?;
1924
1925 Ok(epoch_validator_info
1926 .current_proposals
1927 .into_iter()
1928 .map(|validator_stake_view| {
1929 let validator_stake = validator_stake_view.into_validator_stake();
1930 validator_stake.account_and_stake()
1931 })
1932 .chain(epoch_validator_info.current_validators.into_iter().map(
1933 |current_epoch_validator_info| {
1934 (
1935 current_epoch_validator_info.account_id,
1936 current_epoch_validator_info.stake,
1937 )
1938 },
1939 ))
1940 .chain(
1941 epoch_validator_info
1942 .next_validators
1943 .into_iter()
1944 .map(|next_epoch_validator_info| {
1945 (
1946 next_epoch_validator_info.account_id,
1947 next_epoch_validator_info.stake,
1948 )
1949 }),
1950 )
1951 .collect())
1952}
1953
1954async fn get_staking_pool_info(
1955 json_rpc_client: &near_jsonrpc_client::JsonRpcClient,
1956 validator_account_id: near_primitives::types::AccountId,
1957 stake: u128,
1958) -> color_eyre::Result<StakingPoolInfo> {
1959 let fee = match json_rpc_client
1960 .call(near_jsonrpc_client::methods::query::RpcQueryRequest {
1961 block_reference: near_primitives::types::Finality::Final.into(),
1962 request: near_primitives::views::QueryRequest::CallFunction {
1963 account_id: validator_account_id.clone(),
1964 method_name: "get_reward_fee_fraction".to_string(),
1965 args: near_primitives::types::FunctionArgs::from(vec![]),
1966 },
1967 })
1968 .await
1969 {
1970 Ok(response) => Some(
1971 response
1972 .call_result()?
1973 .parse_result_from_json::<RewardFeeFraction>()
1974 .wrap_err(
1975 "Failed to parse return value of view function call for RewardFeeFraction.",
1976 )?,
1977 ),
1978 Err(near_jsonrpc_client::errors::JsonRpcError::ServerError(
1979 near_jsonrpc_client::errors::JsonRpcServerError::HandlerError(
1980 near_jsonrpc_client::methods::query::RpcQueryError::NoContractCode { .. }
1981 | near_jsonrpc_client::methods::query::RpcQueryError::ContractExecutionError {
1982 ..
1983 },
1984 ),
1985 )) => None,
1986 Err(err) => return Err(err.into()),
1987 };
1988
1989 let delegators = match json_rpc_client
1990 .call(near_jsonrpc_client::methods::query::RpcQueryRequest {
1991 block_reference: near_primitives::types::Finality::Final.into(),
1992 request: near_primitives::views::QueryRequest::CallFunction {
1993 account_id: validator_account_id.clone(),
1994 method_name: "get_number_of_accounts".to_string(),
1995 args: near_primitives::types::FunctionArgs::from(vec![]),
1996 },
1997 })
1998 .await
1999 {
2000 Ok(response) => Some(
2001 response
2002 .call_result()?
2003 .parse_result_from_json::<u64>()
2004 .wrap_err("Failed to parse return value of view function call for u64.")?,
2005 ),
2006 Err(near_jsonrpc_client::errors::JsonRpcError::ServerError(
2007 near_jsonrpc_client::errors::JsonRpcServerError::HandlerError(
2008 near_jsonrpc_client::methods::query::RpcQueryError::NoContractCode { .. }
2009 | near_jsonrpc_client::methods::query::RpcQueryError::ContractExecutionError {
2010 ..
2011 },
2012 ),
2013 )) => None,
2014 Err(err) => return Err(err.into()),
2015 };
2016
2017 Ok(StakingPoolInfo {
2018 validator_id: validator_account_id.clone(),
2019 fee,
2020 delegators,
2021 stake,
2022 })
2023}
2024
2025pub fn display_account_info(
2026 viewed_at_block_hash: &CryptoHash,
2027 viewed_at_block_height: &near_primitives::types::BlockHeight,
2028 account_id: &near_primitives::types::AccountId,
2029 delegated_stake: color_eyre::Result<
2030 std::collections::BTreeMap<near_primitives::types::AccountId, near_token::NearToken>,
2031 >,
2032 account_view: &near_primitives::views::AccountView,
2033 access_key_list: Option<&near_primitives::views::AccessKeyList>,
2034 optional_account_profile: Option<&near_socialdb_client::types::socialdb_types::AccountProfile>,
2035) {
2036 eprintln!();
2037 let mut table: Table = Table::new();
2038 table.set_format(*prettytable::format::consts::FORMAT_NO_COLSEP);
2039
2040 profile_table(
2041 viewed_at_block_hash,
2042 viewed_at_block_height,
2043 account_id,
2044 optional_account_profile,
2045 &mut table,
2046 );
2047
2048 table.add_row(prettytable::row![
2049 Fg->"Native account balance",
2050 Fy->near_token::NearToken::from_yoctonear(account_view.amount)
2051 ]);
2052 table.add_row(prettytable::row![
2053 Fg->"Validator stake",
2054 Fy->near_token::NearToken::from_yoctonear(account_view.locked)
2055 ]);
2056
2057 match delegated_stake {
2058 Ok(delegated_stake) => {
2059 for (validator_id, stake) in delegated_stake {
2060 table.add_row(prettytable::row![
2061 Fg->format!("Delegated stake with <{validator_id}>"),
2062 Fy->stake
2063 ]);
2064 }
2065 }
2066 Err(err) => {
2067 table.add_row(prettytable::row![
2068 Fg->"Delegated stake",
2069 Fr->err
2070 ]);
2071 }
2072 }
2073
2074 table.add_row(prettytable::row![
2075 Fg->"Storage used by the account",
2076 Fy->bytesize::ByteSize(account_view.storage_usage),
2077 ]);
2078
2079 let (table_code_message, contract_status) = match (
2080 &account_view.code_hash,
2081 &account_view.global_contract_account_id,
2082 &account_view.global_contract_hash,
2083 ) {
2084 (_, Some(global_contract_account_id), None) => (
2085 "Global Contract (by Account Id)",
2086 global_contract_account_id.to_string(),
2087 ),
2088 (_, None, Some(global_contract_hash)) => (
2089 "Global Contract (by Hash: SHA-256 checksum hex)",
2090 hex::encode(global_contract_hash.as_ref()),
2091 ),
2092 (hash, None, None) if *hash == CryptoHash::default() => {
2093 ("Contract", "No contract code".to_string())
2094 }
2095 (code_hash, None, None) => (
2096 "Local Contract (SHA-256 checksum hex)",
2097 hex::encode(code_hash.as_ref()),
2098 ),
2099 (code_hash, global_account_id, global_hash) => (
2100 "Contract",
2101 format!(
2102 "Invalid account contract state. Please contact the developers. code_hash: <{}>, global_account_id: <{:?}>, global_hash: <{:?}>",
2103 hex::encode(code_hash.as_ref()),
2104 global_account_id,
2105 global_hash.as_ref()
2106 )
2107 .red().to_string()
2108 ),
2109 };
2110
2111 table.add_row(prettytable::row![
2112 Fg->table_code_message,
2113 Fy->contract_status
2114 ]);
2115
2116 let access_keys_summary = if let Some(info) = access_key_list {
2117 let keys = &info.keys;
2118 if keys.is_empty() {
2119 "Account is locked (no access keys)".to_string()
2120 } else {
2121 let full_access_keys_count = keys
2122 .iter()
2123 .filter(|access_key| {
2124 matches!(
2125 access_key.access_key.permission,
2126 near_primitives::views::AccessKeyPermissionView::FullAccess
2127 )
2128 })
2129 .count();
2130 format!(
2131 "{} full access keys and {} function-call-only access keys",
2132 full_access_keys_count,
2133 keys.len() - full_access_keys_count
2134 )
2135 }
2136 } else {
2137 "Warning: Failed to retrieve access keys. Retry later."
2138 .red()
2139 .to_string()
2140 };
2141
2142 table.add_row(prettytable::row![
2143 Fg->"Access keys",
2144 Fy->access_keys_summary
2145 ]);
2146 table.printstd();
2147}
2148
2149pub fn display_account_profile(
2150 viewed_at_block_hash: &CryptoHash,
2151 viewed_at_block_height: &near_primitives::types::BlockHeight,
2152 account_id: &near_primitives::types::AccountId,
2153 optional_account_profile: Option<&near_socialdb_client::types::socialdb_types::AccountProfile>,
2154) {
2155 let mut table = Table::new();
2156 table.set_format(*prettytable::format::consts::FORMAT_NO_COLSEP);
2157 profile_table(
2158 viewed_at_block_hash,
2159 viewed_at_block_height,
2160 account_id,
2161 optional_account_profile,
2162 &mut table,
2163 );
2164 table.printstd();
2165}
2166
2167fn profile_table(
2168 viewed_at_block_hash: &CryptoHash,
2169 viewed_at_block_height: &near_primitives::types::BlockHeight,
2170 account_id: &near_primitives::types::AccountId,
2171 optional_account_profile: Option<&near_socialdb_client::types::socialdb_types::AccountProfile>,
2172 table: &mut Table,
2173) {
2174 if let Some(account_profile) = optional_account_profile {
2175 if let Some(name) = &account_profile.profile.name {
2176 table.add_row(prettytable::row![
2177 Fy->format!("{account_id} ({name})"),
2178 format!("At block #{}\n({})", viewed_at_block_height, viewed_at_block_hash)
2179 ]);
2180 } else {
2181 table.add_row(prettytable::row![
2182 Fy->account_id,
2183 format!("At block #{}\n({})", viewed_at_block_height, viewed_at_block_hash)
2184 ]);
2185 }
2186 if let Some(image) = &account_profile.profile.image {
2187 if let Some(url) = &image.url {
2188 table.add_row(prettytable::row![
2189 Fg->"Image (url)",
2190 Fy->url
2191 ]);
2192 }
2193 if let Some(ipfs_cid) = &image.ipfs_cid {
2194 table.add_row(prettytable::row![
2195 Fg->"Image (ipfs_cid)",
2196 Fy->ipfs_cid
2197 ]);
2198 }
2199 }
2200 if let Some(background_image) = &account_profile.profile.background_image {
2201 if let Some(url) = &background_image.url {
2202 table.add_row(prettytable::row![
2203 Fg->"Background image (url)",
2204 Fy->url
2205 ]);
2206 }
2207 if let Some(ipfs_cid) = &background_image.ipfs_cid {
2208 table.add_row(prettytable::row![
2209 Fg->"Background image (ipfs_cid)",
2210 Fy->ipfs_cid
2211 ]);
2212 }
2213 }
2214 if let Some(description) = &account_profile.profile.description {
2215 table.add_row(prettytable::row![
2216 Fg->"Description",
2217 Fy->format!("{}", description)
2218 ]);
2219 }
2220 if let Some(linktree) = &account_profile.profile.linktree {
2221 table.add_row(prettytable::row![
2222 Fg->"Linktree",
2223 Fy->""
2224 ]);
2225 for (key, optional_value) in linktree.iter() {
2226 if let Some(value) = &optional_value {
2227 if key == "github" {
2228 table.add_row(prettytable::row![
2229 Fg->"",
2230 Fy->format!("https://github.com/{value}")
2231 ]);
2232 } else if key == "twitter" {
2233 table.add_row(prettytable::row![
2234 Fg->"",
2235 Fy->format!("https://twitter.com/{value}")
2236 ]);
2237 } else if key == "telegram" {
2238 table.add_row(prettytable::row![
2239 Fg->"",
2240 Fy->format!("https://t.me/{value}")
2241 ]);
2242 }
2243 }
2244 }
2245 }
2246 if let Some(tags) = &account_profile.profile.tags {
2247 let keys = tags.keys().cloned().collect::<Vec<String>>().join(", ");
2248 table.add_row(prettytable::row![
2249 Fg->"Tags",
2250 Fy->keys
2251 ]);
2252 }
2253 } else {
2254 table.add_row(prettytable::row![
2255 Fy->account_id,
2256 format!("At block #{}\n({})", viewed_at_block_height, viewed_at_block_hash)
2257 ]);
2258 table.add_row(prettytable::row![
2259 Fd->"NEAR Social profile unavailable",
2260 Fd->format!("The profile can be edited at {}\nor using the cli command: {}\n(https://github.com/bos-cli-rs/bos-cli-rs)",
2261 "https://near.social".blue(),
2262 "bos social-db manage-profile".blue()
2263 )
2264 ]);
2265 }
2266}
2267
2268pub fn display_access_key_list(access_keys: &[near_primitives::views::AccessKeyInfoView]) {
2269 let mut table = Table::new();
2270 table.set_titles(prettytable::row![Fg=>"#", "Public Key", "Nonce", "Permissions"]);
2271
2272 for (index, access_key) in access_keys.iter().enumerate() {
2273 let permissions_message = match &access_key.access_key.permission {
2274 AccessKeyPermissionView::FullAccess => "full access".to_owned(),
2275 AccessKeyPermissionView::FunctionCall {
2276 allowance,
2277 receiver_id,
2278 method_names,
2279 } => {
2280 let allowance_message = match allowance {
2281 Some(amount) => format!(
2282 "with an allowance of {}",
2283 near_token::NearToken::from_yoctonear(*amount)
2284 ),
2285 None => "with no limit".to_string(),
2286 };
2287 if method_names.is_empty() {
2288 format!("do any function calls on {receiver_id} {allowance_message}")
2289 } else {
2290 format!(
2291 "only do {method_names:?} function calls on {receiver_id} {allowance_message}"
2292 )
2293 }
2294 }
2295 };
2296
2297 table.add_row(prettytable::row![
2298 Fg->index + 1,
2299 access_key.public_key,
2300 access_key.access_key.nonce,
2301 permissions_message
2302 ]);
2303 }
2304
2305 table.set_format(*prettytable::format::consts::FORMAT_NO_LINESEP_WITH_TITLE);
2306 table.printstd();
2307}
2308
2309pub fn input_network_name(
2314 config: &crate::config::Config,
2315 account_ids: &[near_primitives::types::AccountId],
2316) -> color_eyre::eyre::Result<Option<String>> {
2317 if config.network_connection.len() == 1 {
2318 return Ok(config.network_names().pop());
2319 }
2320 let variants = if !account_ids.is_empty() {
2321 let (mut matches, non_matches): (Vec<_>, Vec<_>) = config
2322 .network_connection
2323 .iter()
2324 .partition(|(_, network_config)| {
2325 network_config
2329 .linkdrop_account_id
2330 .as_ref()
2331 .is_some_and(|linkdrop_account_id| {
2332 account_ids.iter().any(|account_id| {
2333 account_id.as_str().ends_with(linkdrop_account_id.as_str())
2334 })
2335 })
2336 });
2337 let variants = if matches.is_empty() {
2338 non_matches
2339 } else {
2340 matches.extend(non_matches);
2341 matches
2342 };
2343 variants.into_iter().map(|(k, _)| k).collect()
2344 } else {
2345 config.network_connection.keys().collect()
2346 };
2347
2348 let select_submit = Select::new("What is the name of the network?", variants).prompt();
2349 match select_submit {
2350 Ok(value) => Ok(Some(value.clone())),
2351 Err(
2352 inquire::error::InquireError::OperationCanceled
2353 | inquire::error::InquireError::OperationInterrupted,
2354 ) => Ok(None),
2355 Err(err) => Err(err.into()),
2356 }
2357}
2358
2359pub trait JsonRpcClientExt {
2360 fn blocking_call<M>(&self, method: M) -> BoxedJsonRpcResult<M::Response, M::Error>
2361 where
2362 M: near_jsonrpc_client::methods::RpcMethod,
2363 M::Error: serde::Serialize + std::fmt::Debug + std::fmt::Display;
2364
2365 fn blocking_call_view_function(
2368 &self,
2369 account_id: &near_primitives::types::AccountId,
2370 method_name: &str,
2371 args: Vec<u8>,
2372 block_reference: near_primitives::types::BlockReference,
2373 ) -> Result<near_primitives::views::CallResult, color_eyre::eyre::Error>;
2374
2375 fn blocking_call_view_access_key(
2376 &self,
2377 account_id: &near_primitives::types::AccountId,
2378 public_key: &near_crypto::PublicKey,
2379 block_reference: near_primitives::types::BlockReference,
2380 ) -> BoxedJsonRpcResult<
2381 near_jsonrpc_primitives::types::query::RpcQueryResponse,
2382 near_jsonrpc_primitives::types::query::RpcQueryError,
2383 >;
2384
2385 fn blocking_call_view_access_key_list(
2386 &self,
2387 account_id: &near_primitives::types::AccountId,
2388 block_reference: near_primitives::types::BlockReference,
2389 ) -> BoxedJsonRpcResult<
2390 near_jsonrpc_primitives::types::query::RpcQueryResponse,
2391 near_jsonrpc_primitives::types::query::RpcQueryError,
2392 >;
2393
2394 fn blocking_call_view_account(
2395 &self,
2396 account_id: &near_primitives::types::AccountId,
2397 block_reference: near_primitives::types::BlockReference,
2398 ) -> BoxedJsonRpcResult<
2399 near_jsonrpc_primitives::types::query::RpcQueryResponse,
2400 near_jsonrpc_primitives::types::query::RpcQueryError,
2401 >;
2402}
2403
2404impl JsonRpcClientExt for near_jsonrpc_client::JsonRpcClient {
2405 fn blocking_call<M>(&self, method: M) -> BoxedJsonRpcResult<M::Response, M::Error>
2406 where
2407 M: near_jsonrpc_client::methods::RpcMethod,
2408 M::Error: serde::Serialize + std::fmt::Debug + std::fmt::Display,
2409 {
2410 if let Ok(request_payload) = near_jsonrpc_client::methods::to_json(&method) {
2411 if tracing::enabled!(target: "near_teach_me", tracing::Level::INFO) {
2412 tracing::info!(
2413 target: "near_teach_me",
2414 parent: &tracing::Span::none(),
2415 "HTTP POST {}",
2416 self.server_addr()
2417 );
2418
2419 let (request_payload, message_about_saving_payload) =
2420 check_request_payload_for_broadcast_tx_commit(request_payload);
2421
2422 tracing::info!(
2423 target: "near_teach_me",
2424 parent: &tracing::Span::none(),
2425 "JSON Request Body:\n{}",
2426 indent_payload(&format!("{request_payload:#}"))
2427 );
2428 match message_about_saving_payload {
2429 Ok(Some(message)) => {
2430 tracing::event!(
2431 target: "near_teach_me",
2432 parent: &tracing::Span::none(),
2433 tracing::Level::INFO,
2434 "{}", message
2435 );
2436 }
2437 Err(message) => {
2438 tracing::event!(
2439 target: "near_teach_me",
2440 parent: &tracing::Span::none(),
2441 tracing::Level::WARN,
2442 "{}", message
2443 );
2444 }
2445 _ => {}
2446 }
2447 }
2448 }
2449
2450 tokio::runtime::Runtime::new()
2451 .unwrap()
2452 .block_on(self.call(method))
2453 .inspect_err(|err| match err {
2454 near_jsonrpc_client::errors::JsonRpcError::TransportError(transport_error) => {
2455 tracing::info!(
2456 target: "near_teach_me",
2457 parent: &tracing::Span::none(),
2458 "JSON RPC Request failed due to connectivity issue:\n{}",
2459 indent_payload(&format!("{transport_error:#?}"))
2460 );
2461 }
2462 near_jsonrpc_client::errors::JsonRpcError::ServerError(
2463 near_jsonrpc_client::errors::JsonRpcServerError::HandlerError(handler_error),
2464 ) => {
2465 tracing::info!(
2466 target: "near_teach_me",
2467 parent: &tracing::Span::none(),
2468 "JSON RPC Request returned a handling error:\n{}",
2469 indent_payload(&serde_json::to_string_pretty(handler_error).unwrap_or_else(|_| handler_error.to_string()))
2470 );
2471 }
2472 near_jsonrpc_client::errors::JsonRpcError::ServerError(server_error) => {
2473 tracing::info!(
2474 target: "near_teach_me",
2475 parent: &tracing::Span::none(),
2476 "JSON RPC Request returned a generic server error:\n{}",
2477 indent_payload(&format!("{server_error:#?}"))
2478 );
2479 }
2480 })
2481 .map_err(Box::new)
2482 }
2483
2484 #[tracing::instrument(name = "Getting the result of executing", skip_all)]
2487 fn blocking_call_view_function(
2488 &self,
2489 account_id: &near_primitives::types::AccountId,
2490 function_name: &str,
2491 args: Vec<u8>,
2492 block_reference: near_primitives::types::BlockReference,
2493 ) -> Result<near_primitives::views::CallResult, color_eyre::eyre::Error> {
2494 tracing::Span::current().pb_set_message(&format!(
2495 "a read-only function '{function_name}' of the <{account_id}> contract ..."
2496 ));
2497 tracing::info!(target: "near_teach_me", "a read-only function '{function_name}' of the <{account_id}> contract ...");
2498
2499 let query_view_method_request = near_jsonrpc_client::methods::query::RpcQueryRequest {
2500 block_reference,
2501 request: near_primitives::views::QueryRequest::CallFunction {
2502 account_id: account_id.clone(),
2503 method_name: function_name.to_owned(),
2504 args: near_primitives::types::FunctionArgs::from(args),
2505 },
2506 };
2507
2508 tracing::info!(
2509 target: "near_teach_me",
2510 parent: &tracing::Span::none(),
2511 "I am making HTTP call to NEAR JSON RPC to call a read-only function `{}` on `{}` account, learn more https://docs.near.org/api/rpc/contracts#call-a-contract-function",
2512 function_name,
2513 account_id
2514 );
2515
2516 let query_view_method_response = self
2517 .blocking_call(query_view_method_request)
2518 .wrap_err("Read-only function execution failed")?;
2519
2520 query_view_method_response.call_result()
2521 .inspect(|call_result| {
2522 tracing::info!(
2523 target: "near_teach_me",
2524 parent: &tracing::Span::none(),
2525 "JSON RPC Response:\n{}",
2526 indent_payload(&format!(
2527 "{{\n \"block_hash\": {}\n \"block_height\": {}\n \"logs\": {:?}\n \"result\": {:?}\n}}",
2528 query_view_method_response.block_hash,
2529 query_view_method_response.block_height,
2530 call_result.logs,
2531 call_result.result
2532 ))
2533 );
2534 tracing::info!(
2535 target: "near_teach_me",
2536 parent: &tracing::Span::none(),
2537 "Decoding the \"result\" array of bytes as UTF-8 string (tip: you can use this Python snippet to do it: `\"\".join([chr(c) for c in result])`):\n{}",
2538 indent_payload(
2539 &String::from_utf8(call_result.result.clone())
2540 .unwrap_or_else(|_| "<decoding failed - the result is not a UTF-8 string>".to_owned())
2541 )
2542 );
2543 })
2544 .inspect_err(|_| {
2545 tracing::info!(
2546 target: "near_teach_me",
2547 parent: &tracing::Span::none(),
2548 "JSON RPC Response:\n{}",
2549 indent_payload("Internal error: Received unexpected query kind in response to a view-function query call")
2550 );
2551 })
2552 }
2553
2554 #[tracing::instrument(name = "Getting access key information:", skip_all)]
2555 fn blocking_call_view_access_key(
2556 &self,
2557 account_id: &near_primitives::types::AccountId,
2558 public_key: &near_crypto::PublicKey,
2559 block_reference: near_primitives::types::BlockReference,
2560 ) -> BoxedJsonRpcResult<
2561 near_jsonrpc_primitives::types::query::RpcQueryResponse,
2562 near_jsonrpc_primitives::types::query::RpcQueryError,
2563 > {
2564 tracing::Span::current().pb_set_message(&format!(
2565 "public key {public_key} on account <{account_id}>..."
2566 ));
2567 tracing::info!(target: "near_teach_me", "public key {public_key} on account <{account_id}>...");
2568
2569 let query_view_method_request = near_jsonrpc_client::methods::query::RpcQueryRequest {
2570 block_reference,
2571 request: near_primitives::views::QueryRequest::ViewAccessKey {
2572 account_id: account_id.clone(),
2573 public_key: public_key.clone(),
2574 },
2575 };
2576
2577 tracing::info!(
2578 target: "near_teach_me",
2579 parent: &tracing::Span::none(),
2580 "I am making HTTP call to NEAR JSON RPC to get an access key details for public key {} on account <{}>, learn more https://docs.near.org/api/rpc/access-keys#view-access-key",
2581 public_key,
2582 account_id
2583 );
2584
2585 self.blocking_call(query_view_method_request)
2586 .inspect(teach_me_call_response)
2587 }
2588
2589 #[tracing::instrument(name = "Getting a list of", skip_all)]
2590 fn blocking_call_view_access_key_list(
2591 &self,
2592 account_id: &near_primitives::types::AccountId,
2593 block_reference: near_primitives::types::BlockReference,
2594 ) -> BoxedJsonRpcResult<
2595 near_jsonrpc_primitives::types::query::RpcQueryResponse,
2596 near_jsonrpc_primitives::types::query::RpcQueryError,
2597 > {
2598 tracing::Span::current()
2599 .pb_set_message(&format!("access keys on account <{account_id}>..."));
2600 tracing::info!(target: "near_teach_me", "access keys on account <{account_id}>...");
2601
2602 let query_view_method_request = near_jsonrpc_client::methods::query::RpcQueryRequest {
2603 block_reference,
2604 request: near_primitives::views::QueryRequest::ViewAccessKeyList {
2605 account_id: account_id.clone(),
2606 },
2607 };
2608
2609 tracing::info!(
2610 target: "near_teach_me",
2611 parent: &tracing::Span::none(),
2612 "I am making HTTP call to NEAR JSON RPC to get a list of keys for account <{}>, learn more https://docs.near.org/api/rpc/access-keys#view-access-key-list",
2613 account_id
2614 );
2615
2616 self.blocking_call(query_view_method_request)
2617 .inspect(teach_me_call_response)
2618 }
2619
2620 #[tracing::instrument(name = "Getting information about", skip_all)]
2621 fn blocking_call_view_account(
2622 &self,
2623 account_id: &near_primitives::types::AccountId,
2624 block_reference: near_primitives::types::BlockReference,
2625 ) -> BoxedJsonRpcResult<
2626 near_jsonrpc_primitives::types::query::RpcQueryResponse,
2627 near_jsonrpc_primitives::types::query::RpcQueryError,
2628 > {
2629 tracing::Span::current().pb_set_message(&format!("account <{account_id}>..."));
2630 tracing::info!(target: "near_teach_me", "account <{account_id}>...");
2631
2632 let query_view_method_request = near_jsonrpc_client::methods::query::RpcQueryRequest {
2633 block_reference,
2634 request: near_primitives::views::QueryRequest::ViewAccount {
2635 account_id: account_id.clone(),
2636 },
2637 };
2638
2639 tracing::info!(
2640 target: "near_teach_me",
2641 parent: &tracing::Span::none(),
2642 "I am making HTTP call to NEAR JSON RPC to query information about account <{}>, learn more https://docs.near.org/api/rpc/contracts#view-account",
2643 account_id
2644 );
2645
2646 self.blocking_call(query_view_method_request)
2647 .inspect(teach_me_call_response)
2648 }
2649}
2650
2651fn check_request_payload_for_broadcast_tx_commit(
2652 mut request_payload: serde_json::Value,
2653) -> (serde_json::Value, Result<Option<String>, String>) {
2654 let mut message_about_saving_payload = Ok(None);
2655 let method = request_payload.get("method").cloned();
2656 let params_value = request_payload.get("params").cloned();
2657 if let Some(method) = method {
2658 if method.to_string().contains("broadcast_tx_commit") {
2659 if let Some(params_value) = params_value {
2660 message_about_saving_payload =
2661 replace_params_with_file(&mut request_payload, params_value);
2662 }
2663 }
2664 }
2665 (request_payload, message_about_saving_payload)
2666}
2667
2668fn replace_params_with_file(
2669 request_payload: &mut serde_json::Value,
2670 params_value: serde_json::Value,
2671) -> Result<Option<String>, String> {
2672 let file_path = std::path::PathBuf::from("broadcast_tx_commit__params_field.json");
2673
2674 let total_params_length = {
2675 match serde_json::to_vec_pretty(¶ms_value) {
2676 Ok(serialized) => serialized.len(),
2677 Err(err) => {
2679 return Err(format!(
2680 "Failed to save payload to `{}`. Serialization error:\n{}",
2681 &file_path.display(),
2682 indent_payload(&format!("{err:#?}"))
2683 ));
2684 }
2685 }
2686 };
2687
2688 if total_params_length > 1000 {
2689 let file_content = {
2690 let mut map = serde_json::Map::new();
2691 map.insert(
2692 "original `params` field of JSON Request Body".into(),
2693 params_value,
2694 );
2695
2696 serde_json::Value::Object(map)
2697 };
2698
2699 let result = match std::fs::File::create(&file_path) {
2700 Ok(mut file) => match serde_json::to_vec_pretty(&file_content) {
2701 Ok(buf) => match file.write(&buf) {
2702 Ok(_) => {
2703 Ok(Some(format!("The file `{}` was created successfully. It has a signed transaction (serialized as base64).", &file_path.display())))
2704 }
2705 Err(err) => Err(format!(
2706 "Failed to save payload to `{}`. Failed to write file:\n{}",
2707 &file_path.display(),
2708 indent_payload(&format!("{err:#?}"))
2709 )),
2710 },
2711 Err(err) => Err(format!(
2712 "Failed to save payload to `{}`. Serialization error:\n{}",
2713 &file_path.display(),
2714 indent_payload(&format!("{err:#?}"))
2715 )),
2716 },
2717 Err(err) => Err(format!(
2718 "Failed to save payload to `{}`. Failed to create file:\n{}",
2719 &file_path.display(),
2720 indent_payload(&format!("{err:#?}"))
2721 )),
2722 };
2723
2724 if result.is_ok() {
2725 request_payload["params"] = serde_json::json!(format!(
2726 "`params` field serialization contains {} characters. Current field will be stored in `{}`",
2727 total_params_length,
2728 &file_path.display()
2729 ));
2730 }
2731 result
2732 } else {
2733 Ok(None)
2734 }
2735}
2736
2737pub(crate) fn teach_me_call_response(response: &impl serde::Serialize) {
2738 if let Ok(response_payload) = serde_json::to_value(response) {
2739 tracing::info!(
2740 target: "near_teach_me",
2741 parent: &tracing::Span::none(),
2742 "JSON RPC Response:\n{}",
2743 indent_payload(&format!("{response_payload:#}"))
2744 );
2745 }
2746}
2747
2748#[cfg(feature = "inspect_contract")]
2749pub(crate) fn teach_me_request_payload(
2750 json_rpc_client: &near_jsonrpc_client::JsonRpcClient,
2751 request: &near_jsonrpc_client::methods::query::RpcQueryRequest,
2752) {
2753 if let Ok(request_payload) = near_jsonrpc_client::methods::to_json(request) {
2754 tracing::info!(
2755 target: "near_teach_me",
2756 parent: &tracing::Span::none(),
2757 "HTTP POST {}",
2758 json_rpc_client.server_addr()
2759 );
2760 tracing::info!(
2761 target: "near_teach_me",
2762 parent: &tracing::Span::none(),
2763 "JSON Request Body:\n{}",
2764 indent_payload(&format!("{request_payload:#}"))
2765 );
2766 }
2767}
2768
2769pub fn indent_payload(s: &str) -> String {
2770 use std::fmt::Write;
2771
2772 let mut indented_string = String::new();
2773 indenter::indented(&mut indented_string)
2774 .with_str(" | ")
2775 .write_str(s)
2776 .ok();
2777 indented_string
2778}
2779
2780#[easy_ext::ext(RpcQueryResponseExt)]
2781pub impl near_jsonrpc_primitives::types::query::RpcQueryResponse {
2782 fn access_key_view(&self) -> color_eyre::eyre::Result<near_primitives::views::AccessKeyView> {
2783 if let near_jsonrpc_primitives::types::query::QueryResponseKind::AccessKey(
2784 access_key_view,
2785 ) = &self.kind
2786 {
2787 Ok(access_key_view.clone())
2788 } else {
2789 color_eyre::eyre::bail!(
2790 "Internal error: Received unexpected query kind in response to a View Access Key query call",
2791 );
2792 }
2793 }
2794
2795 fn access_key_list_view(
2796 &self,
2797 ) -> color_eyre::eyre::Result<near_primitives::views::AccessKeyList> {
2798 if let near_jsonrpc_primitives::types::query::QueryResponseKind::AccessKeyList(
2799 access_key_list,
2800 ) = &self.kind
2801 {
2802 Ok(access_key_list.clone())
2803 } else {
2804 color_eyre::eyre::bail!(
2805 "Internal error: Received unexpected query kind in response to a View Access Key List query call",
2806 );
2807 }
2808 }
2809
2810 fn account_view(&self) -> color_eyre::eyre::Result<near_primitives::views::AccountView> {
2811 if let near_jsonrpc_primitives::types::query::QueryResponseKind::ViewAccount(account_view) =
2812 &self.kind
2813 {
2814 Ok(account_view.clone())
2815 } else {
2816 color_eyre::eyre::bail!(
2817 "Internal error: Received unexpected query kind in response to a View Account query call",
2818 );
2819 }
2820 }
2821
2822 fn call_result(&self) -> color_eyre::eyre::Result<near_primitives::views::CallResult> {
2823 if let near_jsonrpc_primitives::types::query::QueryResponseKind::CallResult(result) =
2824 &self.kind
2825 {
2826 Ok(result.clone())
2827 } else {
2828 color_eyre::eyre::bail!(
2829 "Internal error: Received unexpected query kind in response to a view-function query call",
2830 );
2831 }
2832 }
2833}
2834
2835#[easy_ext::ext(CallResultExt)]
2836pub impl near_primitives::views::CallResult {
2837 fn parse_result_from_json<T>(&self) -> Result<T, color_eyre::eyre::Error>
2838 where
2839 T: for<'de> serde::Deserialize<'de>,
2840 {
2841 serde_json::from_slice(&self.result).wrap_err_with(|| {
2842 format!(
2843 "Failed to parse view-function call return value: {}",
2844 String::from_utf8_lossy(&self.result)
2845 )
2846 })
2847 }
2848
2849 fn print_logs(&self) {
2850 let mut info_str = String::new();
2851 if self.logs.is_empty() {
2852 info_str.push_str("\nNo logs")
2853 } else {
2854 info_str.push_str("\nLogs:");
2855 info_str.push_str(&format!("\n {}", self.logs.join("\n ")));
2856 }
2857 info_str.push_str("\n------------------------------------");
2858 tracing::info!(
2859 target: "near_teach_me",
2860 parent: &tracing::Span::none(),
2861 "--- Logs ---------------------------{}\n",
2862 indent_payload(&info_str)
2863 );
2864 }
2865}
2866
2867#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
2868pub struct UsedAccount {
2869 pub account_id: near_primitives::types::AccountId,
2870 pub used_as_signer: bool,
2871}
2872
2873fn get_used_account_list_path(credentials_home_dir: &std::path::Path) -> std::path::PathBuf {
2874 credentials_home_dir.join("accounts.json")
2875}
2876
2877pub fn create_used_account_list_from_legacy_keychain(
2878 credentials_home_dir: &std::path::Path,
2879) -> color_eyre::eyre::Result<()> {
2880 let mut used_account_list: std::collections::BTreeSet<near_primitives::types::AccountId> =
2881 std::collections::BTreeSet::new();
2882 let read_dir =
2883 |dir: &std::path::Path| dir.read_dir().map(Iterator::flatten).into_iter().flatten();
2884 for network_connection_dir in read_dir(credentials_home_dir) {
2885 for entry in read_dir(&network_connection_dir.path()) {
2886 match (entry.path().file_stem(), entry.path().extension()) {
2887 (Some(file_stem), Some(extension)) if extension == "json" => {
2888 if let Ok(account_id) = file_stem.to_string_lossy().parse() {
2889 used_account_list.insert(account_id);
2890 }
2891 }
2892 _ if entry.path().is_dir() => {
2893 if let Ok(account_id) = entry.file_name().to_string_lossy().parse() {
2894 used_account_list.insert(account_id);
2895 }
2896 }
2897 _ => {}
2898 }
2899 }
2900 }
2901
2902 let used_account_list_path = get_used_account_list_path(credentials_home_dir);
2903 std::fs::create_dir_all(credentials_home_dir)?;
2904 if !used_account_list_path.exists() {
2905 std::fs::File::create(&used_account_list_path)
2906 .wrap_err_with(|| format!("Failed to create file: {:?}", &used_account_list_path))?;
2907 }
2908 if !used_account_list.is_empty() {
2909 let used_account_list_buf = serde_json::to_string(
2910 &used_account_list
2911 .into_iter()
2912 .map(|account_id| UsedAccount {
2913 account_id,
2914 used_as_signer: true,
2915 })
2916 .collect::<Vec<_>>(),
2917 )?;
2918 std::fs::write(&used_account_list_path, used_account_list_buf).wrap_err_with(|| {
2919 format!(
2920 "Failed to write to file: {}",
2921 used_account_list_path.display()
2922 )
2923 })?;
2924 }
2925 Ok(())
2926}
2927
2928pub fn update_used_account_list_as_signer(
2929 credentials_home_dir: &std::path::Path,
2930 account_id: &near_primitives::types::AccountId,
2931) {
2932 let account_is_signer = true;
2933 update_used_account_list(credentials_home_dir, account_id, account_is_signer);
2934}
2935
2936pub fn update_used_account_list_as_non_signer(
2937 credentials_home_dir: &std::path::Path,
2938 account_id: &near_primitives::types::AccountId,
2939) {
2940 let account_is_signer = false;
2941 update_used_account_list(credentials_home_dir, account_id, account_is_signer);
2942}
2943
2944fn update_used_account_list(
2945 credentials_home_dir: &std::path::Path,
2946 account_id: &near_primitives::types::AccountId,
2947 account_is_signer: bool,
2948) {
2949 let mut used_account_list = get_used_account_list(credentials_home_dir);
2950
2951 let used_account = if let Some(mut used_account) = used_account_list
2952 .iter()
2953 .position(|used_account| &used_account.account_id == account_id)
2954 .and_then(|position| used_account_list.remove(position))
2955 {
2956 used_account.used_as_signer |= account_is_signer;
2957 used_account
2958 } else {
2959 UsedAccount {
2960 account_id: account_id.clone(),
2961 used_as_signer: account_is_signer,
2962 }
2963 };
2964 used_account_list.push_front(used_account);
2965
2966 let used_account_list_path = get_used_account_list_path(credentials_home_dir);
2967 if let Ok(used_account_list_buf) = serde_json::to_string(&used_account_list) {
2968 let _ = std::fs::write(used_account_list_path, used_account_list_buf);
2969 }
2970}
2971
2972pub fn get_used_account_list(credentials_home_dir: &std::path::Path) -> VecDeque<UsedAccount> {
2973 let used_account_list_path = get_used_account_list_path(credentials_home_dir);
2974 serde_json::from_str(
2975 std::fs::read_to_string(used_account_list_path)
2976 .as_deref()
2977 .unwrap_or("[]"),
2978 )
2979 .unwrap_or_default()
2980}
2981
2982pub fn is_used_account_list_exist(credentials_home_dir: &std::path::Path) -> bool {
2983 get_used_account_list_path(credentials_home_dir).exists()
2984}
2985
2986pub fn input_signer_account_id_from_used_account_list(
2987 credentials_home_dir: &std::path::Path,
2988 message: &str,
2989) -> color_eyre::eyre::Result<Option<crate::types::account_id::AccountId>> {
2990 let account_is_signer = true;
2991 input_account_id_from_used_account_list(credentials_home_dir, message, account_is_signer)
2992}
2993
2994pub fn input_non_signer_account_id_from_used_account_list(
2995 credentials_home_dir: &std::path::Path,
2996 message: &str,
2997) -> color_eyre::eyre::Result<Option<crate::types::account_id::AccountId>> {
2998 let account_is_signer = false;
2999 input_account_id_from_used_account_list(credentials_home_dir, message, account_is_signer)
3000}
3001
3002fn input_account_id_from_used_account_list(
3003 credentials_home_dir: &std::path::Path,
3004 message: &str,
3005 account_is_signer: bool,
3006) -> color_eyre::eyre::Result<Option<crate::types::account_id::AccountId>> {
3007 let used_account_list = get_used_account_list(credentials_home_dir)
3008 .into_iter()
3009 .filter(|account| !account_is_signer || account.used_as_signer)
3010 .map(|account| account.account_id.to_string())
3011 .collect::<Vec<_>>();
3012 let account_id_str = match Text::new(message)
3013 .with_autocomplete(move |val: &str| {
3014 Ok(used_account_list
3015 .iter()
3016 .filter(|s| s.contains(val))
3017 .cloned()
3018 .collect())
3019 })
3020 .with_validator(|account_id_str: &str| {
3021 match near_primitives::types::AccountId::validate(account_id_str) {
3022 Ok(_) => Ok(inquire::validator::Validation::Valid),
3023 Err(err) => Ok(inquire::validator::Validation::Invalid(
3024 inquire::validator::ErrorMessage::Custom(format!("Invalid account ID: {err}")),
3025 )),
3026 }
3027 })
3028 .prompt()
3029 {
3030 Ok(value) => value,
3031 Err(
3032 inquire::error::InquireError::OperationCanceled
3033 | inquire::error::InquireError::OperationInterrupted,
3034 ) => return Ok(None),
3035 Err(err) => return Err(err.into()),
3036 };
3037 let account_id = crate::types::account_id::AccountId::from_str(&account_id_str)?;
3038 update_used_account_list(credentials_home_dir, account_id.as_ref(), account_is_signer);
3039 Ok(Some(account_id))
3040}
3041
3042pub fn save_cli_command(cli_cmd_str: &str) {
3043 let tmp_file_path = std::env::temp_dir().join(FINAL_COMMAND_FILE_NAME);
3044
3045 let Ok(mut tmp_file) = OpenOptions::new()
3046 .write(true)
3047 .create(true)
3048 .truncate(true)
3049 .open(tmp_file_path)
3050 else {
3051 eprintln!("Failed to open a temporary file to store a cli command");
3052 return;
3053 };
3054
3055 if let Err(err) = writeln!(tmp_file, "{cli_cmd_str}") {
3056 eprintln!("Failed to store a cli command in a temporary file: {err}");
3057 }
3058}