diff --git a/Cargo.lock b/Cargo.lock index 1ba19e4a..d5ea0a77 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2305,14 +2305,17 @@ dependencies = [ [[package]] name = "near-ledger" -version = "0.2.0" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1edfdfadf4d772ea67c11d69cdbc8b8f0e0742d88c3baac64bdcf21482f7c950" +checksum = "2f92dbf396469644ca92d9565b4e935d0cb66115af30181fef5fe44a6e50d4e0" dependencies = [ "ed25519-dalek", + "hex 0.4.3", "ledger-apdu", "ledger-transport", "ledger-transport-hid", + "log", + "near-primitives-core", "slip10", ] diff --git a/Cargo.toml b/Cargo.toml index b28090ab..5b373032 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -61,7 +61,7 @@ thiserror = "1" bytesize = "1.1.0" prettytable = "0.10.0" -near-ledger = { version = "0.2.0", optional = true } +near-ledger = { version = "0.3.0", optional = true } near-crypto = "0.17.0" near-primitives = "0.17.0" diff --git a/src/commands/mod.rs b/src/commands/mod.rs index e3adad1b..64090b90 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -68,6 +68,16 @@ pub struct PrepopulatedTransaction { pub actions: Vec, } +impl From for PrepopulatedTransaction { + fn from(value: near_primitives::transaction::Transaction) -> Self { + Self { + signer_id: value.signer_id, + receiver_id: value.receiver_id, + actions: value.actions, + } + } +} + #[derive(Clone)] pub struct ActionContext { pub global_context: crate::GlobalContext, diff --git a/src/commands/transaction/mod.rs b/src/commands/transaction/mod.rs index 9529c623..be2582ed 100644 --- a/src/commands/transaction/mod.rs +++ b/src/commands/transaction/mod.rs @@ -2,6 +2,7 @@ use strum::{EnumDiscriminants, EnumIter, EnumMessage}; pub mod construct_transaction; +mod print_transaction; mod reconstruct_transaction; mod send_meta_transaction; mod send_signed_transaction; @@ -41,6 +42,11 @@ pub enum TransactionActions { ))] /// Sign previously prepared unsigned transaction SignTransaction(self::sign_transaction::SignTransaction), + #[strum_discriminants(strum( + message = "print-transaction - Print all fields of previously prepared transaction without modification" + ))] + /// Print previously prepared unsigned transaction without modification + PrintTransaction(self::print_transaction::PrintTransactionCommands), #[strum_discriminants(strum( message = "send-signed-transaction - Send a signed transaction" ))] diff --git a/src/commands/transaction/print_transaction/mod.rs b/src/commands/transaction/print_transaction/mod.rs new file mode 100644 index 00000000..0f6b3785 --- /dev/null +++ b/src/commands/transaction/print_transaction/mod.rs @@ -0,0 +1,28 @@ +#![allow(clippy::enum_variant_names, clippy::large_enum_variant)] +use strum::{EnumDiscriminants, EnumIter, EnumMessage}; + +mod signed; +mod unsigned; + +#[derive(Debug, Clone, interactive_clap::InteractiveClap)] +#[interactive_clap(context = crate::GlobalContext)] +pub struct PrintTransactionCommands { + #[interactive_clap(subcommand)] + show_transaction_actions: PrintTransactionActions, +} + +#[derive(Debug, EnumDiscriminants, Clone, interactive_clap::InteractiveClap)] +#[interactive_clap(context = crate::GlobalContext)] +#[strum_discriminants(derive(EnumMessage, EnumIter))] +pub enum PrintTransactionActions { + #[strum_discriminants(strum( + message = "unsigned - Print all fields of previously prepared unsigned transaction without modification" + ))] + /// Print previously prepared unsigned transaction without modification + Unsigned(self::unsigned::PrintTransaction), + #[strum_discriminants(strum( + message = "signed - Print all fields of previously prepared signed transaction without modification" + ))] + /// Send a signed transaction + Signed(self::signed::PrintTransaction), +} diff --git a/src/commands/transaction/print_transaction/signed/mod.rs b/src/commands/transaction/print_transaction/signed/mod.rs new file mode 100644 index 00000000..7bd26512 --- /dev/null +++ b/src/commands/transaction/print_transaction/signed/mod.rs @@ -0,0 +1,26 @@ +#[derive(Debug, Clone, interactive_clap::InteractiveClap)] +#[interactive_clap(input_context = crate::GlobalContext)] +#[interactive_clap(output_context = PrintContext)] +pub struct PrintTransaction { + /// Enter the signed transaction encoded in base64: + signed_transaction: crate::types::signed_transaction::SignedTransactionAsBase64, +} + +#[derive(Debug, Clone)] +pub struct PrintContext; + +impl PrintContext { + pub fn from_previous_context( + _previous_context: crate::GlobalContext, + scope: &::InteractiveClapContextScope, + ) -> color_eyre::eyre::Result { + let signed_transaction: near_primitives::transaction::SignedTransaction = + scope.signed_transaction.clone().into(); + + eprintln!("\nSigned transaction (full):\n"); + crate::common::print_full_signed_transaction(signed_transaction); + eprintln!(); + + Ok(Self) + } +} diff --git a/src/commands/transaction/print_transaction/unsigned/mod.rs b/src/commands/transaction/print_transaction/unsigned/mod.rs new file mode 100644 index 00000000..b31673c9 --- /dev/null +++ b/src/commands/transaction/print_transaction/unsigned/mod.rs @@ -0,0 +1,26 @@ +#[derive(Debug, Clone, interactive_clap::InteractiveClap)] +#[interactive_clap(input_context = crate::GlobalContext)] +#[interactive_clap(output_context = PrintContext)] +pub struct PrintTransaction { + /// Enter the unsigned transaction encoded in base64: + unsigned_transaction: crate::types::transaction::TransactionAsBase64, +} + +#[derive(Debug, Clone)] +pub struct PrintContext; + +impl PrintContext { + pub fn from_previous_context( + _previous_context: crate::GlobalContext, + scope: &::InteractiveClapContextScope, + ) -> color_eyre::eyre::Result { + let unsigned_transaction: near_primitives::transaction::Transaction = + scope.unsigned_transaction.clone().into(); + + eprintln!("\nUnsigned transaction (full):\n"); + crate::common::print_full_unsigned_transaction(unsigned_transaction); + eprintln!(); + + Ok(Self) + } +} diff --git a/src/commands/transaction/sign_transaction/mod.rs b/src/commands/transaction/sign_transaction/mod.rs index 5feba731..a838d7bb 100644 --- a/src/commands/transaction/sign_transaction/mod.rs +++ b/src/commands/transaction/sign_transaction/mod.rs @@ -23,11 +23,9 @@ impl SignTransactionContext { scope.unsigned_transaction.clone().into(); move |_network_config| { - Ok(crate::commands::PrepopulatedTransaction { - signer_id: unsigned_transaction.signer_id.clone(), - receiver_id: unsigned_transaction.receiver_id.clone(), - actions: unsigned_transaction.actions.clone(), - }) + Ok(crate::commands::PrepopulatedTransaction::from( + unsigned_transaction.clone(), + )) } }); diff --git a/src/common.rs b/src/common.rs index 3f635e67..3a1b8d3f 100644 --- a/src/common.rs +++ b/src/common.rs @@ -5,6 +5,7 @@ use std::str::FromStr; use color_eyre::eyre::WrapErr; use futures::{StreamExt, TryStreamExt}; +use near_primitives::borsh::BorshSerialize; use prettytable::Table; use near_primitives::{hash::CryptoHash, types::BlockReference, views::AccessKeyPermissionView}; @@ -517,6 +518,28 @@ pub fn generate_keypair() -> color_eyre::eyre::Result { Ok(key_pair_properties) } +pub fn print_full_signed_transaction(transaction: near_primitives::transaction::SignedTransaction) { + eprintln!("{:<25} {}\n", "signature:", transaction.signature); + crate::common::print_full_unsigned_transaction(transaction.transaction); +} + +pub fn print_full_unsigned_transaction(transaction: near_primitives::transaction::Transaction) { + let bytes = transaction + .try_to_vec() + .expect("Transaction is not expected to fail on serialization"); + eprintln!( + "Unsigned transaction hash (Base58-encoded SHA-256 hash): {}\n\n", + CryptoHash::hash_bytes(&bytes) + ); + + eprintln!("{:<13} {}", "public_key:", &transaction.public_key); + eprintln!("{:<13} {}", "nonce:", &transaction.nonce); + eprintln!("{:<13} {}", "block_hash:", &transaction.block_hash); + + let prepopulated = crate::commands::PrepopulatedTransaction::from(transaction); + print_unsigned_transaction(&prepopulated); +} + pub fn print_unsigned_transaction(transaction: &crate::commands::PrepopulatedTransaction) { eprintln!("{:<13} {}", "signer_id:", &transaction.signer_id); eprintln!("{:<13} {}", "receiver_id:", &transaction.receiver_id); diff --git a/src/transaction_signature_options/sign_with_ledger/mod.rs b/src/transaction_signature_options/sign_with_ledger/mod.rs index b14c60ba..cc8bc6ef 100644 --- a/src/transaction_signature_options/sign_with_ledger/mod.rs +++ b/src/transaction_signature_options/sign_with_ledger/mod.rs @@ -1,9 +1,10 @@ use std::str::FromStr; use color_eyre::eyre::{ContextCompat, WrapErr}; -use inquire::{CustomType, Text}; +use inquire::{CustomType, Select, Text}; use near_primitives::borsh::BorshSerialize; +use slip10::BIP32Path; use crate::common::JsonRpcClientExt; use crate::common::RpcQueryResponseExt; @@ -42,8 +43,77 @@ pub struct SignLedgerContext { on_after_sending_transaction_callback: crate::transaction_signature_options::OnAfterSendingTransactionCallback, } +const BLIND_SIGN_MEMO: &str = "Blind signature means that transaction is prepared by CLI, but cannot be reviewed on the Ledger device. \ + In order to be absolutely sure that the transaction you are signing is not forged, take the constructed transaction, \ + verify its content using NEAR CLI on another host or use any other tool capable of displaying unsigned NEAR transactions, \ + and confirm that the SHA256 hash matches the one displayed above and another identical one, that will be displayed on your Ledger device after confirming the prompt. \ + Following helper command on NEAR CLI can be used:"; impl SignLedgerContext { + fn input_blind_agree() -> color_eyre::eyre::Result { + let options: Vec<&str> = vec!["Yes", "No"]; + + Ok( + Select::new("Do you agree to continue with blind signature? ", options) + .prompt() + .map(|selected| selected == "Yes")?, + ) + } + + fn blind_sign_subflow( + hash: near_primitives::hash::CryptoHash, + hd_path: BIP32Path, + unsigned_transaction: near_primitives::transaction::Transaction, + ) -> color_eyre::eyre::Result { + eprintln!("\n\nBuffer overflow on Ledger device occured. Transaction is too large for normal signature."); + eprintln!("\nThe following is Base58-encoded SHA-256 hash of unsigned transaction:"); + eprintln!("{}", hash); + + eprintln!( + "\nUnsigned transaction (serialized as base64):\n{}\n", + crate::types::transaction::TransactionAsBase64::from(unsigned_transaction) + ); + eprintln!("{}", BLIND_SIGN_MEMO); + eprintln!( + "$ {} transaction print-transaction unsigned\n\n", + crate::common::get_near_exec_path() + ); + + eprintln!("Make sure to enable blind sign in NEAR app's settings on Ledger device\n"); + let agree = Self::input_blind_agree()?; + if agree { + eprintln!( + "Confirm transaction blind signing on your Ledger device (HD Path: {})", + hd_path, + ); + let result = near_ledger::blind_sign_transaction(hash, hd_path); + let signature = result.map_err(|err| { + match err { + near_ledger::NEARLedgerError::BlindSignatureDisabled => { + color_eyre::Report::msg("Blind signature is disabled in NEAR app's settings on Ledger device".to_string()) + }, + near_ledger::NEARLedgerError::BlindSignatureNotSupported => { + color_eyre::Report::msg("Blind signature is not supported by the version of NEAR app installed on Ledger device. \ + Version of the app with the feature available is tracked in https://github.com/LedgerHQ/app-near/pull/32".to_string()) + }, + err => { + color_eyre::Report::msg(format!( + "Error occurred while signing the transaction: {:?}", + err + )) + } + } + })?; + let signature = + near_crypto::Signature::from_parts(near_crypto::KeyType::ED25519, &signature) + .expect("Signature is not expected to fail on deserialization"); + + Ok(signature) + } else { + Err(color_eyre::Report::msg("signing with ledger aborted")) + } + } + pub fn from_previous_context( previous_context: crate::commands::TransactionContext, scope: &::InteractiveClapContextScope, @@ -101,12 +171,19 @@ impl SignLedgerContext { unsigned_transaction .try_to_vec() .expect("Transaction is not expected to fail on serialization"), - seed_phrase_hd_path, + seed_phrase_hd_path.clone(), ) { Ok(signature) => { near_crypto::Signature::from_parts(near_crypto::KeyType::ED25519, &signature) .expect("Signature is not expected to fail on deserialization") } + Err(near_ledger::NEARLedgerError::BufferOverflow { transaction_hash }) => { + Self::blind_sign_subflow( + transaction_hash, + seed_phrase_hd_path, + unsigned_transaction.clone(), + )? + } Err(near_ledger_error) => { return Err(color_eyre::Report::msg(format!( "Error occurred while signing the transaction: {:?}", diff --git a/src/types/signed_transaction.rs b/src/types/signed_transaction.rs index 2685da60..18fa7cee 100644 --- a/src/types/signed_transaction.rs +++ b/src/types/signed_transaction.rs @@ -5,6 +5,12 @@ pub struct SignedTransactionAsBase64 { pub inner: near_primitives::transaction::SignedTransaction, } +impl From for near_primitives::transaction::SignedTransaction { + fn from(transaction: SignedTransactionAsBase64) -> Self { + transaction.inner + } +} + impl std::str::FromStr for SignedTransactionAsBase64 { type Err = String; fn from_str(s: &str) -> Result {