From ae25dedee4175008d4b6490acbbfbb423f60b16a Mon Sep 17 00:00:00 2001 From: FroVolod Date: Mon, 18 Mar 2024 21:47:43 +0200 Subject: [PATCH] feat: Added the ability to output a signed transaction (serialized as base64) to a file (#313) Resolves #308 --------- Co-authored-by: FroVolod --- .../display/mod.rs | 57 +++++ src/transaction_signature_options/mod.rs | 206 ++---------------- .../save_to_file/mod.rs | 99 +++++++++ src/transaction_signature_options/send/mod.rs | 103 +++++++++ .../sign_later/display.rs | 31 +++ .../sign_later/mod.rs | 54 +++-- .../sign_later/save_to_file.rs | 54 +++++ 7 files changed, 393 insertions(+), 211 deletions(-) create mode 100644 src/transaction_signature_options/display/mod.rs create mode 100644 src/transaction_signature_options/save_to_file/mod.rs create mode 100644 src/transaction_signature_options/send/mod.rs create mode 100644 src/transaction_signature_options/sign_later/display.rs create mode 100644 src/transaction_signature_options/sign_later/save_to_file.rs diff --git a/src/transaction_signature_options/display/mod.rs b/src/transaction_signature_options/display/mod.rs new file mode 100644 index 000000000..76d83c8f0 --- /dev/null +++ b/src/transaction_signature_options/display/mod.rs @@ -0,0 +1,57 @@ +#[derive(Debug, Clone, interactive_clap_derive::InteractiveClap)] +#[interactive_clap(input_context = super::SubmitContext)] +#[interactive_clap(output_context = DisplayContext)] +pub struct Display; + +#[derive(Debug, Clone)] +pub struct DisplayContext; + +impl DisplayContext { + pub fn from_previous_context( + previous_context: super::SubmitContext, + _scope: &::InteractiveClapContextScope, + ) -> color_eyre::eyre::Result { + let mut storage_message = String::new(); + + match previous_context.signed_transaction_or_signed_delegate_action { + super::SignedTransactionOrSignedDelegateAction::SignedTransaction( + signed_transaction, + ) => { + (previous_context.on_before_sending_transaction_callback)( + &signed_transaction, + &previous_context.network_config, + &mut storage_message, + ) + .map_err(color_eyre::Report::msg)?; + + eprintln!( + "\nSigned transaction (serialized as base64):\n{}\n", + crate::types::signed_transaction::SignedTransactionAsBase64::from( + signed_transaction + ) + ); + eprintln!( + "This base64-encoded signed transaction is ready to be sent to the network. You can call RPC server directly, or use a helper command on near CLI:\n$ {} transaction send-signed-transaction\n", + crate::common::get_near_exec_path() + ); + eprintln!("{storage_message}"); + } + super::SignedTransactionOrSignedDelegateAction::SignedDelegateAction( + signed_delegate_action, + ) => { + eprintln!( + "\nSigned delegate action (serialized as base64):\n{}\n", + crate::types::signed_delegate_action::SignedDelegateActionAsBase64::from( + signed_delegate_action + ) + ); + eprintln!( + "This base64-encoded signed delegate action is ready to be sent to the meta-transaction relayer. There is a helper command on near CLI that can do that:\n$ {} transaction send-meta-transaction\n", + crate::common::get_near_exec_path() + ); + eprintln!("{storage_message}"); + } + } + Ok(Self) + } +} diff --git a/src/transaction_signature_options/mod.rs b/src/transaction_signature_options/mod.rs index 311a961c1..4eed75875 100644 --- a/src/transaction_signature_options/mod.rs +++ b/src/transaction_signature_options/mod.rs @@ -1,8 +1,9 @@ use serde::Deserialize; use strum::{EnumDiscriminants, EnumIter, EnumMessage}; -use crate::common::JsonRpcClientExt; - +pub mod display; +pub mod save_to_file; +pub mod send; pub mod sign_later; pub mod sign_with_access_key_file; pub mod sign_with_keychain; @@ -54,206 +55,29 @@ pub enum SignWith { message = "sign-later - Prepare an unsigned transaction to sign it later" ))] /// Prepare unsigned transaction to sign it later - SignLater(self::sign_later::Display), + SignLater(self::sign_later::SignLater), } #[derive(Debug, EnumDiscriminants, Clone, interactive_clap::InteractiveClap)] #[interactive_clap(context = SubmitContext)] #[strum_discriminants(derive(EnumMessage, EnumIter))] -#[interactive_clap(skip_default_from_cli)] /// How would you like to proceed? pub enum Submit { - #[strum_discriminants(strum(message = "send - Send the transaction to the network"))] + #[strum_discriminants(strum( + message = "send - Send the transaction to the network" + ))] /// Send the transaction to the network - Send, + Send(self::send::Send), #[strum_discriminants(strum( - message = "display - Print the signed transaction to terminal (if you want to send it later)" + message = "save-to-file - Save the signed transaction to file (if you want to send it later)" + ))] + /// Save the signed transaction to file (if you want to send it later) + SaveToFile(self::save_to_file::SaveToFile), + #[strum_discriminants(strum( + message = "display - Print the signed transaction to terminal (if you want to send it later)" ))] /// Print the signed transaction to terminal (if you want to send it later) - Display, -} - -impl interactive_clap::FromCli for Submit { - type FromCliContext = SubmitContext; - type FromCliError = color_eyre::eyre::Error; - - fn from_cli( - mut optional_clap_variant: Option<::CliVariant>, - context: Self::FromCliContext, - ) -> interactive_clap::ResultFromCli< - ::CliVariant, - Self::FromCliError, - > - where - Self: Sized + interactive_clap::ToCli, - { - let mut storage_message = String::new(); - - if optional_clap_variant.is_none() { - match Self::choose_variant(context.clone()) { - interactive_clap::ResultFromCli::Ok(cli_submit) => { - optional_clap_variant = Some(cli_submit) - } - result => return result, - } - } - - match optional_clap_variant { - Some(CliSubmit::Send) => match context.signed_transaction_or_signed_delegate_action { - SignedTransactionOrSignedDelegateAction::SignedTransaction(signed_transaction) => { - if let Err(report) = (context.on_before_sending_transaction_callback)( - &signed_transaction, - &context.network_config, - &mut storage_message, - ) { - return interactive_clap::ResultFromCli::Err( - optional_clap_variant, - color_eyre::Report::msg(report), - ); - }; - - eprintln!("Transaction sent ..."); - let transaction_info = loop { - let transaction_info_result = context.network_config.json_rpc_client() - .blocking_call( - near_jsonrpc_client::methods::broadcast_tx_commit::RpcBroadcastTxCommitRequest{ - signed_transaction: signed_transaction.clone() - } - ); - match transaction_info_result { - Ok(response) => { - break response; - } - Err(err) => match crate::common::rpc_transaction_error(err) { - Ok(_) => std::thread::sleep(std::time::Duration::from_millis(100)), - Err(report) => { - return interactive_clap::ResultFromCli::Err( - optional_clap_variant, - color_eyre::Report::msg(report), - ) - } - }, - }; - }; - if let Err(report) = crate::common::print_transaction_status( - &transaction_info, - &context.network_config, - ) { - return interactive_clap::ResultFromCli::Err( - optional_clap_variant, - color_eyre::Report::msg(report), - ); - }; - if let Err(report) = (context.on_after_sending_transaction_callback)( - &transaction_info, - &context.network_config, - ) { - return interactive_clap::ResultFromCli::Err( - optional_clap_variant, - color_eyre::Report::msg(report), - ); - }; - eprintln!("{storage_message}"); - interactive_clap::ResultFromCli::Ok(CliSubmit::Send) - } - SignedTransactionOrSignedDelegateAction::SignedDelegateAction( - signed_delegate_action, - ) => { - let client = reqwest::blocking::Client::new(); - let json_payload = serde_json::json!({ - "signed_delegate_action": crate::types::signed_delegate_action::SignedDelegateActionAsBase64::from( - signed_delegate_action - ).to_string() - }); - match client - .post( - context - .network_config - .meta_transaction_relayer_url - .expect("Internal error: Meta-transaction relayer URL must be Some() at this point"), - ) - .json(&json_payload) - .send() - { - Ok(relayer_response) => { - if relayer_response.status().is_success() { - let response_text = match relayer_response.text() { - Ok(text) => text, - Err(report) => { - return interactive_clap::ResultFromCli::Err( - optional_clap_variant, - color_eyre::Report::msg(report), - ) - } - }; - println!("Relayer Response text: {}", response_text); - } else { - println!( - "Request failed with status code: {}", - relayer_response.status() - ); - } - } - Err(report) => { - return interactive_clap::ResultFromCli::Err( - optional_clap_variant, - color_eyre::Report::msg(report), - ) - } - } - eprintln!("{storage_message}"); - interactive_clap::ResultFromCli::Ok(CliSubmit::Send) - } - }, - Some(CliSubmit::Display) => { - match context.signed_transaction_or_signed_delegate_action { - SignedTransactionOrSignedDelegateAction::SignedTransaction( - signed_transaction, - ) => { - if let Err(report) = (context.on_before_sending_transaction_callback)( - &signed_transaction, - &context.network_config, - &mut storage_message, - ) { - return interactive_clap::ResultFromCli::Err( - optional_clap_variant, - color_eyre::Report::msg(report), - ); - }; - eprintln!( - "\nSigned transaction (serialized as base64):\n{}\n", - crate::types::signed_transaction::SignedTransactionAsBase64::from( - signed_transaction - ) - ); - eprintln!( - "This base64-encoded signed transaction is ready to be sent to the network. You can call RPC server directly, or use a helper command on near CLI:\n$ {} transaction send-signed-transaction\n", - crate::common::get_near_exec_path() - ); - eprintln!("{storage_message}"); - interactive_clap::ResultFromCli::Ok(CliSubmit::Display) - } - SignedTransactionOrSignedDelegateAction::SignedDelegateAction( - signed_delegate_action, - ) => { - eprintln!( - "\nSigned delegate action (serialized as base64):\n{}\n", - crate::types::signed_delegate_action::SignedDelegateActionAsBase64::from( - signed_delegate_action - ) - ); - eprintln!( - "This base64-encoded signed delegate action is ready to be sent to the meta-transaction relayer. There is a helper command on near CLI that can do that:\n$ {} transaction send-meta-transaction\n", - crate::common::get_near_exec_path() - ); - eprintln!("{storage_message}"); - interactive_clap::ResultFromCli::Ok(CliSubmit::Display) - } - } - } - None => unreachable!("Unexpected error"), - } - } + Display(self::display::Display), } #[derive(Debug, Deserialize)] diff --git a/src/transaction_signature_options/save_to_file/mod.rs b/src/transaction_signature_options/save_to_file/mod.rs new file mode 100644 index 000000000..afef10411 --- /dev/null +++ b/src/transaction_signature_options/save_to_file/mod.rs @@ -0,0 +1,99 @@ +use std::io::Write; + +use color_eyre::eyre::Context; +use inquire::CustomType; + +#[derive(Debug, Clone, interactive_clap_derive::InteractiveClap)] +#[interactive_clap(input_context = super::SubmitContext)] +#[interactive_clap(output_context = SaveToFileContext)] +pub struct SaveToFile { + #[interactive_clap(skip_default_input_arg)] + /// What is the location of the file to save the transaction information (path/to/signed-transaction-info.json)? + file_path: crate::types::path_buf::PathBuf, +} + +#[derive(Debug, Clone)] +pub struct SaveToFileContext; + +impl SaveToFileContext { + pub fn from_previous_context( + previous_context: super::SubmitContext, + scope: &::InteractiveClapContextScope, + ) -> color_eyre::eyre::Result { + let mut storage_message = String::new(); + let file_path: std::path::PathBuf = scope.file_path.clone().into(); + + match previous_context.signed_transaction_or_signed_delegate_action { + super::SignedTransactionOrSignedDelegateAction::SignedTransaction( + signed_transaction, + ) => { + (previous_context.on_before_sending_transaction_callback)( + &signed_transaction, + &previous_context.network_config, + &mut storage_message, + ) + .map_err(color_eyre::Report::msg)?; + + let signed_transaction_as_base64 = + crate::types::signed_transaction::SignedTransactionAsBase64::from( + signed_transaction, + ) + .to_string(); + + let data_signed_transaction = serde_json::json!( + {"Signed transaction (serialized as base64)": signed_transaction_as_base64}); + + std::fs::File::create(&file_path) + .wrap_err_with(|| format!("Failed to create file: {:?}", &file_path))? + .write(&serde_json::to_vec(&data_signed_transaction)?) + .wrap_err_with(|| format!("Failed to write to file: {:?}", &file_path))?; + eprintln!("\nThe file {:?} was created successfully. It has a signed transaction (serialized as base64).", &file_path); + + eprintln!( + "This base64-encoded signed transaction is ready to be sent to the network. You can call RPC server directly, or use a helper command on near CLI:\n$ {} transaction send-signed-transaction\n", + crate::common::get_near_exec_path() + ); + eprintln!("{storage_message}"); + } + super::SignedTransactionOrSignedDelegateAction::SignedDelegateAction( + signed_delegate_action, + ) => { + let signed_delegate_action_as_base64 = + crate::types::signed_delegate_action::SignedDelegateActionAsBase64::from( + signed_delegate_action, + ) + .to_string(); + + let data_signed_delegate_action = serde_json::json!( + {"Signed delegate action (serialized as base64)": signed_delegate_action_as_base64}); + + std::fs::File::create(&file_path) + .wrap_err_with(|| format!("Failed to create file: {:?}", &file_path))? + .write(&serde_json::to_vec(&data_signed_delegate_action)?) + .wrap_err_with(|| format!("Failed to write to file: {:?}", &file_path))?; + eprintln!("\nThe file {:?} was created successfully. It has a signed delegate action (serialized as base64).", &file_path); + + eprintln!( + "This base64-encoded signed delegate action is ready to be sent to the meta-transaction relayer. There is a helper command on near CLI that can do that:\n$ {} transaction send-meta-transaction\n", + crate::common::get_near_exec_path() + ); + eprintln!("{storage_message}"); + } + } + Ok(Self) + } +} + +impl SaveToFile { + fn input_file_path( + _context: &super::SubmitContext, + ) -> color_eyre::eyre::Result> { + Ok(Some( + CustomType::new( + "What is the location of the file to save the transaction information?", + ) + .with_starting_input("signed-transaction-info.json") + .prompt()?, + )) + } +} diff --git a/src/transaction_signature_options/send/mod.rs b/src/transaction_signature_options/send/mod.rs new file mode 100644 index 000000000..44ecbe9a4 --- /dev/null +++ b/src/transaction_signature_options/send/mod.rs @@ -0,0 +1,103 @@ +use crate::common::JsonRpcClientExt; + +#[derive(Debug, Clone, interactive_clap_derive::InteractiveClap)] +#[interactive_clap(input_context = super::SubmitContext)] +#[interactive_clap(output_context = SendContext)] +pub struct Send; + +#[derive(Debug, Clone)] +pub struct SendContext; + +impl SendContext { + pub fn from_previous_context( + previous_context: super::SubmitContext, + _scope: &::InteractiveClapContextScope, + ) -> color_eyre::eyre::Result { + let mut storage_message = String::new(); + + match previous_context.signed_transaction_or_signed_delegate_action { + super::SignedTransactionOrSignedDelegateAction::SignedTransaction( + signed_transaction, + ) => { + (previous_context.on_before_sending_transaction_callback)( + &signed_transaction, + &previous_context.network_config, + &mut storage_message, + ) + .map_err(color_eyre::Report::msg)?; + + eprintln!("Transaction sent ..."); + let transaction_info = loop { + let transaction_info_result = previous_context.network_config.json_rpc_client() + .blocking_call( + near_jsonrpc_client::methods::broadcast_tx_commit::RpcBroadcastTxCommitRequest{ + signed_transaction: signed_transaction.clone() + } + ); + match transaction_info_result { + Ok(response) => { + break response; + } + Err(err) => match crate::common::rpc_transaction_error(err) { + Ok(_) => std::thread::sleep(std::time::Duration::from_millis(100)), + Err(report) => return Err(color_eyre::Report::msg(report)), + }, + }; + }; + + crate::common::print_transaction_status( + &transaction_info, + &previous_context.network_config, + )?; + + (previous_context.on_after_sending_transaction_callback)( + &transaction_info, + &previous_context.network_config, + ) + .map_err(color_eyre::Report::msg)?; + + eprintln!("{storage_message}"); + } + super::SignedTransactionOrSignedDelegateAction::SignedDelegateAction( + signed_delegate_action, + ) => { + let client = reqwest::blocking::Client::new(); + let json_payload = serde_json::json!({ + "signed_delegate_action": crate::types::signed_delegate_action::SignedDelegateActionAsBase64::from( + signed_delegate_action + ).to_string() + }); + match client + .post( + previous_context + .network_config + .meta_transaction_relayer_url + .expect("Internal error: Meta-transaction relayer URL must be Some() at this point"), + ) + .json(&json_payload) + .send() + { + Ok(relayer_response) => { + if relayer_response.status().is_success() { + let response_text = relayer_response.text() + .map_err(color_eyre::Report::msg)?; + println!("Relayer Response text: {}", response_text); + } else { + println!( + "Request failed with status code: {}", + relayer_response.status() + ); + } + } + Err(report) => { + return Err( + color_eyre::Report::msg(report), + ) + } + } + eprintln!("{storage_message}"); + } + } + Ok(Self) + } +} diff --git a/src/transaction_signature_options/sign_later/display.rs b/src/transaction_signature_options/sign_later/display.rs new file mode 100644 index 000000000..e701cab1d --- /dev/null +++ b/src/transaction_signature_options/sign_later/display.rs @@ -0,0 +1,31 @@ +#[derive(Debug, Clone, interactive_clap_derive::InteractiveClap)] +#[interactive_clap(input_context = super::SignLaterContext)] +#[interactive_clap(output_context = DisplayContext)] +pub struct Display; + +#[derive(Debug, Clone)] +pub struct DisplayContext; + +impl DisplayContext { + pub fn from_previous_context( + previous_context: super::SignLaterContext, + _scope: &::InteractiveClapContextScope, + ) -> color_eyre::eyre::Result { + eprintln!( + "\nTransaction hash to sign:\n{}", + hex::encode(previous_context.unsigned_transaction.get_hash_and_size().0) + ); + + eprintln!( + "\nUnsigned transaction (serialized as base64):\n{}\n", + crate::types::transaction::TransactionAsBase64::from( + previous_context.unsigned_transaction + ) + ); + eprintln!( + "This base64-encoded transaction can be signed and sent later. There is a helper command on near CLI that can do that:\n$ {} transaction sign-transaction\n", + crate::common::get_near_exec_path() + ); + Ok(Self) + } +} diff --git a/src/transaction_signature_options/sign_later/mod.rs b/src/transaction_signature_options/sign_later/mod.rs index 43697f2a2..5f1e545cb 100644 --- a/src/transaction_signature_options/sign_later/mod.rs +++ b/src/transaction_signature_options/sign_later/mod.rs @@ -1,7 +1,12 @@ +use strum::{EnumDiscriminants, EnumIter, EnumMessage}; + +mod display; +mod save_to_file; + #[derive(Debug, Clone, interactive_clap::InteractiveClap)] #[interactive_clap(input_context = crate::commands::TransactionContext)] -#[interactive_clap(output_context = DisplayContext)] -pub struct Display { +#[interactive_clap(output_context = SignLaterContext)] +pub struct SignLater { #[interactive_clap(long)] /// Enter sender (signer) public key: signer_public_key: crate::types::public_key::PublicKey, @@ -11,15 +16,19 @@ pub struct Display { #[interactive_clap(long)] /// Enter recent block hash: block_hash: crate::types::crypto_hash::CryptoHash, + #[interactive_clap(subcommand)] + output: Output, } #[derive(Debug, Clone)] -pub struct DisplayContext; +pub struct SignLaterContext { + unsigned_transaction: near_primitives::transaction::Transaction, +} -impl DisplayContext { +impl SignLaterContext { pub fn from_previous_context( previous_context: crate::commands::TransactionContext, - scope: &::InteractiveClapContextScope, + scope: &::InteractiveClapContextScope, ) -> color_eyre::eyre::Result { let unsigned_transaction = near_primitives::transaction::Transaction { signer_id: previous_context.prepopulated_transaction.signer_id, @@ -29,20 +38,25 @@ impl DisplayContext { block_hash: scope.block_hash.into(), actions: previous_context.prepopulated_transaction.actions, }; - - eprintln!( - "\nTransaction hash to sign:\n{}", - hex::encode(unsigned_transaction.get_hash_and_size().0) - ); - - eprintln!( - "\nUnsigned transaction (serialized as base64):\n{}\n", - crate::types::transaction::TransactionAsBase64::from(unsigned_transaction) - ); - eprintln!( - "This base64-encoded transaction can be signed and sent later. There is a helper command on near CLI that can do that:\n$ {} transaction sign-transaction\n", - crate::common::get_near_exec_path() - ); - Ok(Self) + Ok(Self { + unsigned_transaction, + }) } } + +#[derive(Debug, EnumDiscriminants, Clone, interactive_clap::InteractiveClap)] +#[interactive_clap(context = SignLaterContext)] +#[strum_discriminants(derive(EnumMessage, EnumIter))] +/// How would you like to proceed? +pub enum Output { + #[strum_discriminants(strum( + message = "save-to-file - Save the unsigned transaction to file" + ))] + /// Save the unsigned transaction to file + SaveToFile(self::save_to_file::SaveToFile), + #[strum_discriminants(strum( + message = "display - Print the unsigned transaction to terminal" + ))] + /// Print the unsigned transaction to terminal + Display(self::display::Display), +} diff --git a/src/transaction_signature_options/sign_later/save_to_file.rs b/src/transaction_signature_options/sign_later/save_to_file.rs new file mode 100644 index 000000000..b0bb26615 --- /dev/null +++ b/src/transaction_signature_options/sign_later/save_to_file.rs @@ -0,0 +1,54 @@ +use std::io::Write; + +use color_eyre::eyre::Context; +use inquire::CustomType; + +#[derive(Debug, Clone, interactive_clap_derive::InteractiveClap)] +#[interactive_clap(input_context = super::SignLaterContext)] +#[interactive_clap(output_context = SaveToFileContext)] +pub struct SaveToFile { + #[interactive_clap(skip_default_input_arg)] + /// What is the location of the file to save the unsigned transaction (path/to/signed-transaction-info.json)? + file_path: crate::types::path_buf::PathBuf, +} + +#[derive(Debug, Clone)] +pub struct SaveToFileContext; + +impl SaveToFileContext { + pub fn from_previous_context( + previous_context: super::SignLaterContext, + scope: &::InteractiveClapContextScope, + ) -> color_eyre::eyre::Result { + let file_path: std::path::PathBuf = scope.file_path.clone().into(); + + let data_unsigned_transaction = serde_json::json!({ + "Transaction hash to sign": hex::encode(previous_context.unsigned_transaction.get_hash_and_size().0).to_string(), + "Unsigned transaction (serialized as base64)": crate::types::transaction::TransactionAsBase64::from(previous_context.unsigned_transaction).to_string(), + }); + + std::fs::File::create(&file_path) + .wrap_err_with(|| format!("Failed to create file: {:?}", &file_path))? + .write(&serde_json::to_vec_pretty(&data_unsigned_transaction)?) + .wrap_err_with(|| format!("Failed to write to file: {:?}", &file_path))?; + eprintln!("\nThe file {:?} was created successfully. It has a unsigned transaction (serialized as base64).", &file_path); + + eprintln!( + "This base64-encoded transaction can be signed and sent later. There is a helper command on near CLI that can do that:\n$ {} transaction sign-transaction\n", + crate::common::get_near_exec_path() + ); + Ok(Self) + } +} + +impl SaveToFile { + fn input_file_path( + _context: &super::SignLaterContext, + ) -> color_eyre::eyre::Result> { + Ok(Some( + CustomType::new("What is the location of the file to save the unsigned transaction?") + .with_starting_input("unsigned-transaction-info.json") + .prompt()?, + )) + } +}