near_cli_rs/transaction_signature_options/sign_with_legacy_keychain/
mod.rs

1extern crate dirs;
2
3use std::str::FromStr;
4
5use color_eyre::eyre::{ContextCompat, WrapErr};
6use inquire::{CustomType, Select};
7use near_primitives::transaction::Transaction;
8use near_primitives::transaction::TransactionV0;
9
10use crate::common::JsonRpcClientExt;
11use crate::common::RpcQueryResponseExt;
12
13#[derive(Debug, Clone, interactive_clap::InteractiveClap)]
14#[interactive_clap(input_context = crate::commands::TransactionContext)]
15#[interactive_clap(output_context = SignLegacyKeychainContext)]
16pub struct SignLegacyKeychain {
17    #[interactive_clap(long)]
18    #[interactive_clap(skip_default_input_arg)]
19    signer_public_key: Option<crate::types::public_key::PublicKey>,
20    #[interactive_clap(long)]
21    #[interactive_clap(skip_default_input_arg)]
22    nonce: Option<u64>,
23    #[interactive_clap(long)]
24    #[interactive_clap(skip_default_input_arg)]
25    pub block_hash: Option<crate::types::crypto_hash::CryptoHash>,
26    #[interactive_clap(long)]
27    #[interactive_clap(skip_default_input_arg)]
28    pub block_height: Option<near_primitives::types::BlockHeight>,
29    #[interactive_clap(long)]
30    #[interactive_clap(skip_interactive_input)]
31    meta_transaction_valid_for: Option<u64>,
32    #[interactive_clap(subcommand)]
33    submit: super::Submit,
34}
35
36#[derive(Clone)]
37pub struct SignLegacyKeychainContext {
38    pub(crate) network_config: crate::config::NetworkConfig,
39    pub(crate) global_context: crate::GlobalContext,
40    pub(crate) signed_transaction_or_signed_delegate_action:
41        super::SignedTransactionOrSignedDelegateAction,
42    pub(crate) on_before_sending_transaction_callback:
43        crate::transaction_signature_options::OnBeforeSendingTransactionCallback,
44    pub(crate) on_after_sending_transaction_callback:
45        crate::transaction_signature_options::OnAfterSendingTransactionCallback,
46}
47
48impl SignLegacyKeychainContext {
49    #[tracing::instrument(
50        name = "Signing the transaction with a key saved in legacy keychain ...",
51        skip_all
52    )]
53    pub fn from_previous_context(
54        previous_context: crate::commands::TransactionContext,
55        scope: &<SignLegacyKeychain as interactive_clap::ToInteractiveClapContextScope>::InteractiveClapContextScope,
56    ) -> color_eyre::eyre::Result<Self> {
57        let network_config = previous_context.network_config.clone();
58
59        let keychain_folder = previous_context
60            .global_context
61            .config
62            .credentials_home_dir
63            .join(&network_config.network_name);
64        let signer_keychain_folder =
65            keychain_folder.join(previous_context.prepopulated_transaction.signer_id.as_str());
66        let signer_access_key_file_path: std::path::PathBuf = {
67            if previous_context.global_context.offline {
68                signer_keychain_folder.join(format!(
69                    "{}.json",
70                    scope
71                        .signer_public_key
72                        .as_ref()
73                        .wrap_err(
74                            "Signer public key is required to sign a transaction in offline mode"
75                        )?
76                        .to_string()
77                        .replace(':', "_")
78                ))
79            } else if signer_keychain_folder.exists() {
80                let full_access_key_filenames = network_config
81                    .json_rpc_client()
82                    .blocking_call_view_access_key_list(
83                        &previous_context.prepopulated_transaction.signer_id,
84                        near_primitives::types::Finality::Final.into(),
85                    )
86                    .wrap_err_with(|| {
87                        format!(
88                            "Failed to fetch access KeyList for {}",
89                            previous_context.prepopulated_transaction.signer_id
90                        )
91                    })?
92                    .access_key_list_view()?
93                    .keys
94                    .iter()
95                    .filter(
96                        |access_key_info| match access_key_info.access_key.permission {
97                            near_primitives::views::AccessKeyPermissionView::FullAccess => true,
98                            near_primitives::views::AccessKeyPermissionView::FunctionCall {
99                                ..
100                            } => false,
101                        },
102                    )
103                    .map(|access_key_info| {
104                        format!(
105                            "{}.json",
106                            access_key_info.public_key.to_string().replace(":", "_")
107                        )
108                        .into()
109                    })
110                    .collect::<std::collections::HashSet<std::ffi::OsString>>();
111
112                signer_keychain_folder
113                    .read_dir()
114                    .wrap_err("There are no access keys found in the keychain for the signer account. Import an access key for an account before signing transactions with keychain.")?
115                    .filter_map(Result::ok)
116                    .find(|entry| full_access_key_filenames.contains(&entry.file_name()))
117                    .map(|signer_access_key| signer_access_key.path())
118                    .unwrap_or_else(|| keychain_folder.join(format!(
119                        "{}.json",
120                        previous_context.prepopulated_transaction.signer_id
121                    )))
122            } else {
123                keychain_folder.join(format!(
124                    "{}.json",
125                    previous_context.prepopulated_transaction.signer_id
126                ))
127            }
128        };
129        let signer_access_key_json =
130            std::fs::read(&signer_access_key_file_path).wrap_err_with(|| {
131                format!(
132                    "Access key file for account <{}> on network <{}> not found! \nSearch location: {:?}",
133                    previous_context.prepopulated_transaction.signer_id,
134                    network_config.network_name, signer_access_key_file_path
135                )
136            })?;
137        let signer_access_key: super::AccountKeyPair =
138            serde_json::from_slice(&signer_access_key_json).wrap_err_with(|| {
139                format!(
140                    "Error reading data from file: {:?}",
141                    &signer_access_key_file_path
142                )
143            })?;
144
145        let (nonce, block_hash, block_height) = if previous_context.global_context.offline {
146            (
147                scope
148                    .nonce
149                    .wrap_err("Nonce is required to sign a transaction in offline mode")?,
150                scope
151                    .block_hash
152                    .wrap_err("Block Hash is required to sign a transaction in offline mode")?
153                    .0,
154                scope
155                    .block_height
156                    .wrap_err("Block Height is required to sign a transaction in offline mode")?,
157            )
158        } else {
159            let rpc_query_response = network_config
160                .json_rpc_client()
161                .blocking_call_view_access_key(
162                    &previous_context.prepopulated_transaction.signer_id,
163                    &signer_access_key.public_key,
164                    near_primitives::types::BlockReference::latest()
165                )
166                .wrap_err(
167                    "Cannot sign a transaction due to an error while fetching the most recent nonce value",
168                )?;
169            (
170                rpc_query_response
171                    .access_key_view()
172                    .wrap_err("Error current_nonce")?
173                    .nonce
174                    + 1,
175                rpc_query_response.block_hash,
176                rpc_query_response.block_height,
177            )
178        };
179
180        let mut unsigned_transaction = TransactionV0 {
181            public_key: signer_access_key.public_key.clone(),
182            block_hash,
183            nonce,
184            signer_id: previous_context.prepopulated_transaction.signer_id,
185            receiver_id: previous_context.prepopulated_transaction.receiver_id,
186            actions: previous_context.prepopulated_transaction.actions,
187        };
188
189        (previous_context.on_before_signing_callback)(&mut unsigned_transaction, &network_config)?;
190
191        let unsigned_transaction = Transaction::V0(unsigned_transaction);
192
193        if network_config.meta_transaction_relayer_url.is_some() {
194            let max_block_height = block_height
195                + scope
196                    .meta_transaction_valid_for
197                    .unwrap_or(super::META_TRANSACTION_VALID_FOR_DEFAULT);
198
199            let signed_delegate_action = super::get_signed_delegate_action(
200                unsigned_transaction,
201                &signer_access_key.public_key,
202                signer_access_key.private_key,
203                max_block_height,
204            );
205
206            return Ok(Self {
207                network_config: previous_context.network_config,
208                global_context: previous_context.global_context,
209                signed_transaction_or_signed_delegate_action: signed_delegate_action.into(),
210                on_before_sending_transaction_callback: previous_context
211                    .on_before_sending_transaction_callback,
212                on_after_sending_transaction_callback: previous_context
213                    .on_after_sending_transaction_callback,
214            });
215        }
216
217        let signature = signer_access_key
218            .private_key
219            .sign(unsigned_transaction.get_hash_and_size().0.as_ref());
220
221        let signed_transaction = near_primitives::transaction::SignedTransaction::new(
222            signature.clone(),
223            unsigned_transaction,
224        );
225
226        tracing::info!(
227            parent: &tracing::Span::none(),
228            "Your transaction was signed successfully.{}",
229            crate::common::indent_payload(&format!(
230                "\nPublic key: {}\nSignature:  {}\n",
231                signer_access_key.public_key,
232                signature
233            ))
234        );
235
236        Ok(Self {
237            network_config: previous_context.network_config,
238            global_context: previous_context.global_context,
239            signed_transaction_or_signed_delegate_action: signed_transaction.into(),
240            on_before_sending_transaction_callback: previous_context
241                .on_before_sending_transaction_callback,
242            on_after_sending_transaction_callback: previous_context
243                .on_after_sending_transaction_callback,
244        })
245    }
246}
247
248impl From<SignLegacyKeychainContext> for super::SubmitContext {
249    fn from(item: SignLegacyKeychainContext) -> Self {
250        Self {
251            network_config: item.network_config,
252            global_context: item.global_context,
253            signed_transaction_or_signed_delegate_action: item
254                .signed_transaction_or_signed_delegate_action,
255            on_before_sending_transaction_callback: item.on_before_sending_transaction_callback,
256            on_after_sending_transaction_callback: item.on_after_sending_transaction_callback,
257        }
258    }
259}
260
261impl SignLegacyKeychain {
262    fn input_signer_public_key(
263        context: &crate::commands::TransactionContext,
264    ) -> color_eyre::eyre::Result<Option<crate::types::public_key::PublicKey>> {
265        if context.global_context.offline {
266            let network_config = context.network_config.clone();
267
268            let mut path =
269                std::path::PathBuf::from(&context.global_context.config.credentials_home_dir);
270
271            let dir_name = network_config.network_name;
272            path.push(&dir_name);
273
274            path.push(context.prepopulated_transaction.signer_id.to_string());
275
276            let signer_dir = path.read_dir()?;
277
278            let key_list = signer_dir
279                .filter_map(|entry| entry.ok())
280                .filter_map(|entry| entry.file_name().into_string().ok())
281                .filter(|file_name_str| file_name_str.starts_with("ed25519_"))
282                .map(|file_name_str| file_name_str.replace(".json", "").replace('_', ":"))
283                .collect::<Vec<_>>();
284
285            let selected_input = Select::new("Choose public_key:", key_list).prompt()?;
286
287            return Ok(Some(crate::types::public_key::PublicKey::from_str(
288                &selected_input,
289            )?));
290        }
291        Ok(None)
292    }
293
294    fn input_nonce(
295        context: &crate::commands::TransactionContext,
296    ) -> color_eyre::eyre::Result<Option<u64>> {
297        if context.global_context.offline {
298            return Ok(Some(
299                CustomType::<u64>::new("Enter a nonce for the access key:").prompt()?,
300            ));
301        }
302        Ok(None)
303    }
304
305    fn input_block_hash(
306        context: &crate::commands::TransactionContext,
307    ) -> color_eyre::eyre::Result<Option<crate::types::crypto_hash::CryptoHash>> {
308        if context.global_context.offline {
309            return Ok(Some(
310                CustomType::<crate::types::crypto_hash::CryptoHash>::new(
311                    "Enter recent block hash:",
312                )
313                .prompt()?,
314            ));
315        }
316        Ok(None)
317    }
318
319    fn input_block_height(
320        context: &crate::commands::TransactionContext,
321    ) -> color_eyre::eyre::Result<Option<near_primitives::types::BlockHeight>> {
322        if context.global_context.offline {
323            return Ok(Some(
324                CustomType::<near_primitives::types::BlockHeight>::new(
325                    "Enter recent block height:",
326                )
327                .prompt()?,
328            ));
329        }
330        Ok(None)
331    }
332}