From cb872b05b4cc31161950a9d3c1812cc8af2b7bea Mon Sep 17 00:00:00 2001 From: Boris Oncev Date: Fri, 26 Jan 2024 16:58:49 +0100 Subject: [PATCH] Wallet accept encoded transaction as well as PartaillySigned ones --- Cargo.lock | 1 + wallet/src/account/mod.rs | 96 +++++++++++++++++-- wallet/src/account/output_cache/mod.rs | 2 +- wallet/src/wallet/mod.rs | 9 +- wallet/src/wallet/tests.rs | 25 ++++- wallet/wallet-cli-lib/src/commands/mod.rs | 4 +- .../src/synced_controller.rs | 4 +- wallet/wallet-rpc-lib/Cargo.toml | 1 + wallet/wallet-rpc-lib/src/rpc/mod.rs | 20 +++- wallet/wallet-rpc-lib/src/rpc/types.rs | 6 ++ 10 files changed, 145 insertions(+), 23 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index db7b5b88c3..02e7a677c5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7595,6 +7595,7 @@ dependencies = [ "consensus", "crypto", "futures", + "hex", "jsonrpsee", "logging", "mempool", diff --git a/wallet/src/account/mod.rs b/wallet/src/account/mod.rs index cad7cf58c4..738630a85f 100644 --- a/wallet/src/account/mod.rs +++ b/wallet/src/account/mod.rs @@ -42,7 +42,7 @@ use crate::send_request::{ }; use crate::wallet::WalletPoolsFilter; use crate::wallet_events::{WalletEvents, WalletEventsNoOp}; -use crate::{SendRequest, WalletError, WalletResult}; +use crate::{get_tx_output_destination, SendRequest, WalletError, WalletResult}; use common::address::Address; use common::chain::output_value::OutputValue; use common::chain::signature::inputsig::standard_signature::StandardInputSignature; @@ -90,6 +90,11 @@ pub struct CurrentFeeRate { pub consolidate_fee_rate: FeeRate, } +pub enum TransactionToSign { + Tx(Transaction), + Partial(PartiallySignedTransaction), +} + #[derive(Debug, Eq, PartialEq, Clone, Encode, Decode)] pub struct PartiallySignedTransaction { tx: Transaction, @@ -1135,23 +1140,100 @@ impl Account { PartiallySignedTransaction::new(tx, witnesses, input_utxos, destinations) } + fn tx_to_partially_signed_tx( + &self, + tx: Transaction, + median_time: BlockTimestamp, + ) -> WalletResult { + let current_block_info = BlockInfo { + height: self.account_info.best_block_height(), + timestamp: median_time, + }; + + let (input_utxos, destinations) = tx + .inputs() + .iter() + .map(|tx_inp| match tx_inp { + TxInput::Utxo(outpoint) => { + // find utxo from cache + self.find_unspent_utxo_with_destination(outpoint, current_block_info) + } + TxInput::Account(acc_outpoint) => { + // find delegation destination + match acc_outpoint.account() { + AccountSpending::DelegationBalance(delegation_id, _) => self + .output_cache + .delegation_data(delegation_id) + .map(|data| (None, Some(data.destination.clone()))) + .ok_or(WalletError::DelegationNotFound(*delegation_id)), + } + } + TxInput::AccountCommand(_, cmd) => { + // find authority of the token + match cmd { + AccountCommand::MintTokens(token_id, _) + | AccountCommand::UnmintTokens(token_id) + | AccountCommand::LockTokenSupply(token_id) + | AccountCommand::ChangeTokenAuthority(token_id, _) + | AccountCommand::FreezeToken(token_id, _) + | AccountCommand::UnfreezeToken(token_id) => self + .output_cache + .token_data(token_id) + .map(|data| (None, Some(data.authority.clone()))) + .ok_or(WalletError::UnknownTokenId(*token_id)), + } + } + }) + .collect::>>()? + .into_iter() + .unzip(); + + let num_inputs = tx.inputs().len(); + PartiallySignedTransaction::new(tx, vec![None; num_inputs], input_utxos, destinations) + } + + fn find_unspent_utxo_with_destination( + &self, + outpoint: &UtxoOutPoint, + current_block_info: BlockInfo, + ) -> WalletResult<(Option, Option)> { + let (txo, _) = + self.output_cache.find_unspent_unlocked_utxo(outpoint, current_block_info)?; + + Ok(( + Some(txo.clone()), + Some( + get_tx_output_destination(txo, &|pool_id| { + self.output_cache.pool_data(*pool_id).ok() + }) + .ok_or(WalletError::InputCannotBeSpent(txo.clone()))?, + ), + )) + } + pub fn sign_raw_transaction( &self, - tx: PartiallySignedTransaction, + tx: TransactionToSign, + median_time: BlockTimestamp, db_tx: &impl WalletStorageReadUnlocked, ) -> WalletResult { - let inputs_utxo_refs: Vec<_> = tx.input_utxos().iter().map(|u| u.as_ref()).collect(); + let ptx = match tx { + TransactionToSign::Partial(ptx) => ptx, + TransactionToSign::Tx(tx) => self.tx_to_partially_signed_tx(tx, median_time)?, + }; + + let inputs_utxo_refs: Vec<_> = ptx.input_utxos().iter().map(|u| u.as_ref()).collect(); - let witnesses = tx + let witnesses = ptx .witnesses() .iter() .enumerate() .map(|(i, witness)| match witness { Some(w) => Ok(Some(w.clone())), - None => match tx.destinations().get(i).expect("cannot fail") { + None => match ptx.destinations().get(i).expect("cannot fail") { Some(destination) => { let s = self - .sign_input(tx.tx(), destination, i, &inputs_utxo_refs, db_tx)? + .sign_input(ptx.tx(), destination, i, &inputs_utxo_refs, db_tx)? .ok_or(WalletError::InputCannotBeSigned)?; Ok(Some(s.clone())) } @@ -1160,7 +1242,7 @@ impl Account { }) .collect::, WalletError>>()?; - Ok(tx.new_witnesses(witnesses)) + Ok(ptx.new_witnesses(witnesses)) } pub fn account_index(&self) -> U31 { diff --git a/wallet/src/account/output_cache/mod.rs b/wallet/src/account/output_cache/mod.rs index 8c46ad488b..45e7ad8262 100644 --- a/wallet/src/account/output_cache/mod.rs +++ b/wallet/src/account/output_cache/mod.rs @@ -1034,7 +1034,7 @@ impl OutputCache { }) } - fn find_unspent_unlocked_utxo( + pub fn find_unspent_unlocked_utxo( &self, utxo: &UtxoOutPoint, current_block_info: BlockInfo, diff --git a/wallet/src/wallet/mod.rs b/wallet/src/wallet/mod.rs index 1a7e608915..ff46e222d7 100644 --- a/wallet/src/wallet/mod.rs +++ b/wallet/src/wallet/mod.rs @@ -20,7 +20,7 @@ use std::sync::Arc; use crate::account::transaction_list::TransactionList; use crate::account::{ Currency, CurrentFeeRate, DelegationData, PartiallySignedTransaction, PoolData, - UnconfirmedTokenInfo, UtxoSelectorError, + TransactionToSign, UnconfirmedTokenInfo, UtxoSelectorError, }; use crate::key_chain::{ make_account_path, make_path_to_vrf_key, KeyChainError, MasterKeyChain, LOOKAHEAD_SIZE, @@ -199,6 +199,8 @@ pub enum WalletError { FullySignedTransactionInDecommissionReq, #[error("Input cannot be signed")] InputCannotBeSigned, + #[error("Input cannot be spent {0:?}")] + InputCannotBeSpent(TxOutput), #[error("Failed to convert partially signed tx to signed")] FailedToConvertPartiallySignedTx(PartiallySignedTransaction), #[error("The specified address is not found in this wallet")] @@ -1340,10 +1342,11 @@ impl Wallet { pub fn sign_raw_transaction( &mut self, account_index: U31, - tx: PartiallySignedTransaction, + tx: TransactionToSign, ) -> WalletResult { + let latest_median_time = self.latest_median_time; self.for_account_rw_unlocked(account_index, |account, db_tx, _| { - account.sign_raw_transaction(tx, db_tx) + account.sign_raw_transaction(tx, latest_median_time, db_tx) }) } diff --git a/wallet/src/wallet/tests.rs b/wallet/src/wallet/tests.rs index 32509d3c58..ba22fedcbb 100644 --- a/wallet/src/wallet/tests.rs +++ b/wallet/src/wallet/tests.rs @@ -3866,6 +3866,15 @@ fn sign_decommission_pool_request_between_accounts(#[case] seed: Seed) { }, ) .unwrap(); + + // remove the signatures and try to sign it again + let tx = stake_pool_transaction.transaction().clone(); + let stake_pool_transaction = wallet + .sign_raw_transaction(acc_0_index, TransactionToSign::Tx(tx)) + .unwrap() + .into_signed_tx() + .unwrap(); + let _ = create_block( &chain_config, &mut wallet, @@ -3888,15 +3897,20 @@ fn sign_decommission_pool_request_between_accounts(#[case] seed: Seed) { .unwrap(); // Try to sign decommission request with wrong account - let sign_from_acc0_res = - wallet.sign_raw_transaction(acc_0_index, decommission_partial_tx.clone()); + let sign_from_acc0_res = wallet.sign_raw_transaction( + acc_0_index, + TransactionToSign::Partial(decommission_partial_tx.clone()), + ); assert_eq!( sign_from_acc0_res.unwrap_err(), WalletError::InputCannotBeSigned ); let signed_tx = wallet - .sign_raw_transaction(acc_1_index, decommission_partial_tx) + .sign_raw_transaction( + acc_1_index, + TransactionToSign::Partial(decommission_partial_tx), + ) .unwrap() .into_signed_tx() .unwrap(); @@ -3986,7 +4000,10 @@ fn sign_decommission_pool_request_cold_wallet(#[case] seed: Seed) { // sign the tx with cold wallet let signed_tx = cold_wallet - .sign_raw_transaction(DEFAULT_ACCOUNT_INDEX, decommission_partial_tx) + .sign_raw_transaction( + DEFAULT_ACCOUNT_INDEX, + TransactionToSign::Partial(decommission_partial_tx), + ) .unwrap() .into_signed_tx() .unwrap(); diff --git a/wallet/wallet-cli-lib/src/commands/mod.rs b/wallet/wallet-cli-lib/src/commands/mod.rs index 473e2cecea..e7f65b38f2 100644 --- a/wallet/wallet-cli-lib/src/commands/mod.rs +++ b/wallet/wallet-cli-lib/src/commands/mod.rs @@ -170,8 +170,8 @@ pub enum ColdWalletCommand { /// to the network. #[clap(name = "account-sign-raw-transaction")] SignRawTransaction { - /// Hex encoded transaction. - transaction: HexEncoded, + /// Hex encoded transaction or PartiallySignedTransaction. + transaction: String, }, /// Print command history in the wallet for this execution diff --git a/wallet/wallet-controller/src/synced_controller.rs b/wallet/wallet-controller/src/synced_controller.rs index 30636118f6..8556aa8fff 100644 --- a/wallet/wallet-controller/src/synced_controller.rs +++ b/wallet/wallet-controller/src/synced_controller.rs @@ -36,7 +36,7 @@ use logging::log; use mempool::FeeRate; use node_comm::node_traits::NodeInterface; use wallet::{ - account::{PartiallySignedTransaction, UnconfirmedTokenInfo}, + account::{PartiallySignedTransaction, TransactionToSign, UnconfirmedTokenInfo}, send_request::{ make_address_output, make_address_output_token, make_create_delegation_output, make_data_deposit_output, StakePoolDataArguments, @@ -618,7 +618,7 @@ impl<'a, T: NodeInterface, W: WalletEvents> SyncedController<'a, T, W> { pub fn sign_raw_transaction( &mut self, - tx: PartiallySignedTransaction, + tx: TransactionToSign, ) -> Result> { self.wallet .sign_raw_transaction(self.account_index, tx) diff --git a/wallet/wallet-rpc-lib/Cargo.toml b/wallet/wallet-rpc-lib/Cargo.toml index ac5e1bcd70..10311112bc 100644 --- a/wallet/wallet-rpc-lib/Cargo.toml +++ b/wallet/wallet-rpc-lib/Cargo.toml @@ -31,6 +31,7 @@ serde.workspace = true serde_json.workspace = true thiserror.workspace = true tokio.workspace = true +hex.workspace = true [dev-dependencies] diff --git a/wallet/wallet-rpc-lib/src/rpc/mod.rs b/wallet/wallet-rpc-lib/src/rpc/mod.rs index bad5b867c2..a704d8191b 100644 --- a/wallet/wallet-rpc-lib/src/rpc/mod.rs +++ b/wallet/wallet-rpc-lib/src/rpc/mod.rs @@ -24,11 +24,11 @@ use mempool_types::tx_options::TxOptionsOverrides; use p2p_types::{ bannable_address::BannableAddress, ip_or_socket_address::IpOrSocketAddress, PeerId, }; -use serialization::hex_encoded::HexEncoded; +use serialization::{hex_encoded::HexEncoded, Decode, DecodeAll}; use std::{collections::BTreeMap, fmt::Debug, path::PathBuf, sync::Arc}; use utils::{ensure, shallow_clone::ShallowClone}; use wallet::{ - account::{PartiallySignedTransaction, PoolData}, + account::{PartiallySignedTransaction, PoolData, TransactionToSign}, WalletError, }; @@ -349,16 +349,28 @@ impl WalletRpc { pub async fn sign_raw_transaction( &self, account_index: U31, - tx: HexEncoded, + raw_tx: String, config: ControllerConfig, ) -> WRpcResult { + let hex_bytes = hex::decode(raw_tx).map_err(|_| RpcError::InvalidRawTransaction)?; + let mut bytes = hex_bytes.as_slice(); + let tx = Transaction::decode(&mut bytes).map_err(|_| RpcError::InvalidRawTransaction)?; + let tx_to_sign = if bytes.is_empty() { + TransactionToSign::Tx(tx) + } else { + let mut bytes = hex_bytes.as_slice(); + let ptx = PartiallySignedTransaction::decode_all(&mut bytes) + .map_err(|_| RpcError::InvalidPartialTransaction)?; + TransactionToSign::Partial(ptx) + }; + self.wallet .call_async(move |controller| { Box::pin(async move { controller .synced_controller(account_index, config) .await? - .sign_raw_transaction(tx.take()) + .sign_raw_transaction(tx_to_sign) .map_err(RpcError::Controller) }) }) diff --git a/wallet/wallet-rpc-lib/src/rpc/types.rs b/wallet/wallet-rpc-lib/src/rpc/types.rs index 354ce3047f..dbf8d9831f 100644 --- a/wallet/wallet-rpc-lib/src/rpc/types.rs +++ b/wallet/wallet-rpc-lib/src/rpc/types.rs @@ -82,6 +82,12 @@ pub enum RpcError { #[error("{0}")] SubmitError(#[from] SubmitError), + + #[error("Invalid hex encoded transaction")] + InvalidRawTransaction, + + #[error("Invalid hex encoded partially signed transaction")] + InvalidPartialTransaction, } impl From> for rpc::Error {