near_cli_rs/transaction_signature_options/sign_with_legacy_keychain/
mod.rs1extern 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}