near_cli_rs/
common.rs

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
20/// A type alias was introduced to simplify the usage of `Result` with a boxed error type that was
21/// necessary to fix `clippy::result_large_err` warning
22pub 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 = 10^21 - this value is set temporarily
233        // In the future, its value will be calculated by the function: fn tx_cost(...)
234        // https://github.com/near/nearcore/blob/8a377fda0b4ce319385c463f1ae46e4b0b29dcd9/runtime/runtime/src/config.rs#L178-L232
235        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
2309/// Interactive prompt for network name.
2310///
2311/// If account_ids is provided, show the network connections that are more
2312/// relevant at the top of the list.
2313pub 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                // We use `linkdrop_account_id` as a heuristic to determine if
2326                // the accounts are on the same network. In the future, we
2327                // might consider to have a better way to do this.
2328                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    /// A helper function to make a view-funcation call using JSON encoding for the function
2366    /// arguments and function return value.
2367    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    /// A helper function to make a view-funcation call using JSON encoding for the function
2485    /// arguments and function return value.
2486    #[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(&params_value) {
2676            Ok(serialized) => serialized.len(),
2677            // this branch is supposed to be unreachable
2678            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}