diff --git a/client-cli/src/command.rs b/client-cli/src/command.rs index d0f2992aa..89075e113 100644 --- a/client-cli/src/command.rs +++ b/client-cli/src/command.rs @@ -381,7 +381,30 @@ impl Command { let passphrase = ask_passphrase(None)?; let balance = wallet_client.balance(name, &passphrase)?; - success(&format!("Wallet balance: {}", balance)); + let rows = vec![ + Row::new(vec![ + Cell::new("Total", Default::default()), + Cell::new(format!("{}", balance.total).as_str(), Default::default()), + ]), + Row::new(vec![ + Cell::new("Pending", Default::default()), + Cell::new(format!("{}", balance.pending).as_str(), Default::default()), + ]), + Row::new(vec![ + Cell::new("Available", Default::default()), + Cell::new( + format!("{}", balance.available).as_str(), + Default::default(), + ), + ]), + ]; + + let table = Table::new(rows, Default::default()); + table + .print_stdout() + .chain(|| (ErrorKind::IoError, "Unable to print table"))?; + + success(&format!("Wallet balance: \n {:?}", balance)); Ok(()) } diff --git a/client-cli/src/command/transaction_command.rs b/client-cli/src/command/transaction_command.rs index 813a1bb56..b9eb128c5 100644 --- a/client-cli/src/command/transaction_command.rs +++ b/client-cli/src/command/transaction_command.rs @@ -11,6 +11,7 @@ use chain_core::tx::data::input::TxoPointer; use chain_core::tx::data::output::TxOut; use chain_core::tx::TxAux; use client_common::{Error, ErrorKind, PublicKey, Result, ResultExt}; +use client_core::types::TransactionPending; use client_core::WalletClient; use client_network::NetworkOpsClient; use hex::decode; @@ -87,21 +88,45 @@ fn new_transaction( ) -> Result<()> { let passphrase = ask_passphrase(None)?; - let transaction = match transaction_type { - TransactionType::Transfer => new_transfer_transaction(wallet_client, name, &passphrase), - TransactionType::Deposit => new_deposit_transaction(network_ops_client, name, &passphrase), - TransactionType::Unbond => new_unbond_transaction(network_ops_client, name, &passphrase), + match transaction_type { + TransactionType::Transfer => { + let (tx_aux, tx_pending) = new_transfer_transaction(wallet_client, name, &passphrase)?; + wallet_client.broadcast_transaction(&tx_aux)?; + wallet_client.update_tx_pending_state( + &name, + &passphrase, + tx_aux.tx_id(), + tx_pending, + )?; + } + TransactionType::Deposit => { + let tx_aux = new_deposit_transaction(network_ops_client, name, &passphrase)?; + wallet_client.broadcast_transaction(&tx_aux)?; + } + TransactionType::Unbond => { + let tx_aux = new_unbond_transaction(network_ops_client, name, &passphrase)?; + wallet_client.broadcast_transaction(&tx_aux)?; + } TransactionType::Withdraw => { - new_withdraw_transaction(wallet_client, network_ops_client, name, &passphrase) + let (tx_aux, tx_pending) = + new_withdraw_transaction(wallet_client, network_ops_client, name, &passphrase)?; + wallet_client.broadcast_transaction(&tx_aux)?; + wallet_client.update_tx_pending_state( + &name, + &passphrase, + tx_aux.tx_id(), + tx_pending, + )?; + } + TransactionType::Unjail => { + let tx_aux = new_unjail_transaction(network_ops_client, name, &passphrase)?; + wallet_client.broadcast_transaction(&tx_aux)?; } - TransactionType::Unjail => new_unjail_transaction(network_ops_client, name, &passphrase), TransactionType::NodeJoin => { - new_node_join_transaction(network_ops_client, name, &passphrase) + let tx_aux = new_node_join_transaction(network_ops_client, name, &passphrase)?; + wallet_client.broadcast_transaction(&tx_aux)?; } - }?; - - wallet_client.broadcast_transaction(&transaction)?; - + }; Ok(()) } @@ -110,7 +135,7 @@ fn new_withdraw_transaction( network_ops_client: &N, name: &str, passphrase: &SecUtf8, -) -> Result { +) -> Result<(TxAux, TransactionPending)> { let from_address = ask_staking_address()?; let to_address = ask_transfer_address()?; let view_keys = ask_view_keys()?; @@ -172,7 +197,7 @@ fn new_transfer_transaction( wallet_client: &T, name: &str, passphrase: &SecUtf8, -) -> Result { +) -> Result<(TxAux, TransactionPending)> { let outputs = ask_outputs()?; let view_keys = ask_view_keys()?; @@ -194,7 +219,20 @@ fn new_transfer_transaction( let return_address = wallet_client.new_transfer_address(name, &passphrase)?; - wallet_client.create_transaction(name, &passphrase, outputs, attributes, None, return_address) + let (transaction, used_inputs, return_amount) = wallet_client.create_transaction( + name, + &passphrase, + outputs, + attributes, + None, + return_address, + )?; + let tx_pending = TransactionPending { + block_height: wallet_client.get_current_block_height()?, + used_inputs, + return_amount, + }; + Ok((transaction, tx_pending)) } fn new_unjail_transaction( diff --git a/client-core/src/service/wallet_state_service.rs b/client-core/src/service/wallet_state_service.rs index 746b8665c..0dcc977c0 100644 --- a/client-core/src/service/wallet_state_service.rs +++ b/client-core/src/service/wallet_state_service.rs @@ -1,18 +1,18 @@ -use std::collections::BTreeMap; - use parity_scale_codec::{Decode, Encode}; use secstr::SecUtf8; +use std::collections::BTreeMap; -use chain_core::{ - init::coin::Coin, - tx::data::{input::TxoPointer, output::TxOut, TxId}, -}; +use chain_core::tx::data::{input::TxoPointer, output::TxOut, TxId}; use client_common::{Error, ErrorKind, Result, ResultExt, SecureStorage, Storage}; -use crate::types::TransactionChange; +use crate::types::{TransactionChange, TransactionPending, WalletBalance}; +use chain_core::init::coin::{sum_coins, CoinError}; /// key space of wallet state const KEYSPACE: &str = "core_wallet_state"; +/// If we can't find the transaction in the latest BLOCK_HEIGHT_ENSURE after it broadcasted, +/// we'll rollback the pending transactions +pub const BLOCK_HEIGHT_ENSURE: u64 = 50; /// Maintains mapping `wallet-name -> wallet-state` #[derive(Debug, Default, Clone)] @@ -60,8 +60,8 @@ where name: &str, passphrase: &SecUtf8, ) -> Result> { - self.get_wallet_state(name, passphrase) - .map(|wallet_state| wallet_state.unspent_transactions) + let wallet_state = self.get_wallet_state(name, passphrase)?; + Ok(wallet_state.get_available_transactions()) } /// Returns currently stored transaction history for given wallet @@ -107,9 +107,12 @@ where } /// Returns currently stored balance for given wallet - #[inline] - pub fn get_balance(&self, name: &str, passphrase: &SecUtf8) -> Result { - Ok(self.get_wallet_state(name, passphrase)?.balance) + pub fn get_balance(&self, name: &str, passphrase: &SecUtf8) -> Result { + let wallet_state = self.get_wallet_state(name, passphrase)?; + let balance = wallet_state + .get_balance() + .chain(|| (ErrorKind::StorageError, "Calculate balance error"))?; + Ok(balance) } fn modify_state(&self, name: &str, passphrase: &SecUtf8, f: F) -> Result<()> @@ -219,12 +222,12 @@ pub fn delete_wallet_state(storage: &S, name: &str) -> Result<()> { pub struct WalletState { /// UTxO pub unspent_transactions: BTreeMap, + /// Transaction pending information indexed by txid + pub pending_transactions: BTreeMap, /// Transaction history indexed by txid pub transaction_history: BTreeMap, /// Transaction ids ordered by insert order. pub transaction_log: Vec, - /// Balance - pub balance: Coin, } impl Default for WalletState { @@ -232,14 +235,76 @@ impl Default for WalletState { fn default() -> WalletState { WalletState { unspent_transactions: Default::default(), + pending_transactions: Default::default(), transaction_history: Default::default(), transaction_log: vec![], - balance: Coin::zero(), } } } impl WalletState { + /// if the txid can not be found in the latest `BLOCK_HEIGHT_ENSURE` blocks after it broadcast + /// we need to rollback + pub fn get_rollback_pending_tx(&self, current_block_height: u64) -> Vec { + self.pending_transactions + .iter() + .filter_map(|(key, value)| { + if value.block_height + BLOCK_HEIGHT_ENSURE < current_block_height { + Some(*key) + } else { + None + } + }) + .collect() + } + + fn get_pending_inputs(&self) -> Vec { + self.pending_transactions + .values() + .map(|value| value.used_inputs.clone()) + .flatten() + .collect() + } + /// get transactions which in unspent_ransactions and not in pending_transactions + pub fn get_available_transactions(&self) -> BTreeMap { + let pending_inputs = self.get_pending_inputs(); + let mut result = BTreeMap::new(); + let _ = self + .unspent_transactions + .iter() + .filter(|(key, _value)| !pending_inputs.contains(key)) + .map(|(key, value)| result.insert(key.clone(), value.clone())) + .collect::>(); + result + } + /// get the balance info + pub fn get_balance(&self) -> std::result::Result { + // pending amount + let pending_coins = self + .pending_transactions + .values() + .map(|value| value.return_amount); + let amount_pending = sum_coins(pending_coins)?; + + // unavailable amount + let pending_inputs = self.get_pending_inputs(); + let available_coins = self + .unspent_transactions + .iter() + .filter(|(key, _value)| !pending_inputs.contains(key)) + .map(|(_key, value)| value.value); + let amount_available = sum_coins(available_coins)?; + + // total amount + let amount_total = (amount_pending + amount_available)?; + + let wallet_balances = WalletBalance { + total: amount_total, + available: amount_available, + pending: amount_pending, + }; + Ok(wallet_balances) + } /// Applies memento to wallet state pub fn apply_memento(&mut self, memento: &WalletStateMemento) -> Result<()> { for operation in memento.0.iter() { @@ -258,7 +323,6 @@ impl WalletState { match memento_operation { MementoOperation::AddTransactionChange(ref transaction_id, ref transaction_change) => { if !self.transaction_history.contains_key(transaction_id) { - self.balance = (self.balance + transaction_change.balance_change)?; self.add_transaction_change(transaction_id.clone(), transaction_change.clone()); } } @@ -269,6 +333,16 @@ impl WalletState { MementoOperation::RemoveUnspentTransaction(ref input) => { self.unspent_transactions.remove(input); } + MementoOperation::AddPendingTransaction(ref transaction_id, ref pending_info) => { + if !self.pending_transactions.contains_key(transaction_id) { + let _ = self + .pending_transactions + .insert(*transaction_id, pending_info.clone()); + } + } + MementoOperation::RemovePendingTransaction(ref transaction_id) => { + self.pending_transactions.remove(transaction_id); + } } Ok(()) } @@ -303,6 +377,8 @@ pub struct WalletStateMemento(Vec); enum MementoOperation { AddTransactionChange(TxId, TransactionChange), AddUnspentTransaction(TxoPointer, TxOut), + AddPendingTransaction(TxId, TransactionPending), + RemovePendingTransaction(TxId), RemoveUnspentTransaction(TxoPointer), } @@ -316,6 +392,13 @@ impl WalletStateMemento { )) } + /// Adds transaction pending info to memento + #[inline] + pub fn add_pending_transaction(&mut self, tx_id: TxId, tx_pending: TransactionPending) { + self.0 + .push(MementoOperation::AddPendingTransaction(tx_id, tx_pending)) + } + /// Adds unspent transaction to memento #[inline] pub fn add_unspent_transaction(&mut self, input: TxoPointer, output: TxOut) { @@ -329,6 +412,13 @@ impl WalletStateMemento { self.0 .push(MementoOperation::RemoveUnspentTransaction(input)) } + + /// Removes pending transaction from memento + #[inline] + pub fn remove_pending_transaction(&mut self, tx_id: TxId) { + self.0 + .push(MementoOperation::RemovePendingTransaction(tx_id)) + } } #[cfg(test)] @@ -341,6 +431,7 @@ mod tests { use client_common::tendermint::types::Time; use crate::types::{BalanceChange, TransactionType}; + use chain_core::init::coin::Coin; #[test] fn check_wallet_state_service_flow() { @@ -374,14 +465,13 @@ mod tests { .is_none()); assert_eq!( - Coin::zero(), + WalletBalance::default(), wallet_state_service.get_balance(name, passphrase).unwrap() ); // Add an unspent transaction and check if it is added let mut memento = WalletStateMemento::default(); - memento.add_unspent_transaction( TxoPointer::new([0; 32], 0), TxOut::new(ExtendedAddr::OrTree([0; 32]), Coin::zero()), @@ -400,11 +490,8 @@ mod tests { ); // Remove previously added unspent transaction and check if it is removed - let mut memento = WalletStateMemento::default(); - memento.remove_unspent_transaction(TxoPointer::new([0; 32], 0)); - assert!(wallet_state_service .apply_memento(name, passphrase, &memento) .is_ok()); @@ -417,6 +504,31 @@ mod tests { .len() ); + // Add a pending transaction + + let mut memento = WalletStateMemento::default(); + memento.add_pending_transaction( + [0; 32], + TransactionPending { + used_inputs: vec![], + block_height: 0, + return_amount: Coin::unit(), + }, + ); + assert!(wallet_state_service + .apply_memento(name, passphrase, &memento) + .is_ok()); + // remove the previous added pending transaction + let mut memento = WalletStateMemento::default(); + memento.remove_pending_transaction([0; 32]); + assert!(wallet_state_service + .apply_memento(name, passphrase, &memento) + .is_ok()); + let wallet_state = wallet_state_service + .get_wallet_state(name, passphrase) + .unwrap(); + assert_eq!(0, wallet_state.pending_transactions.len()); + // Add a transaction change (with incoming balance) and check if it is added and also new wallet balance let mut memento = WalletStateMemento::default(); @@ -437,11 +549,6 @@ mod tests { .apply_memento(name, passphrase, &memento) .is_ok()); - assert_eq!( - Coin::new(50).unwrap(), - wallet_state_service.get_balance(name, passphrase).unwrap() - ); - assert_eq!( 1, wallet_state_service @@ -481,11 +588,6 @@ mod tests { .apply_memento(name, passphrase, &memento) .is_ok()); - assert_eq!( - Coin::zero(), - wallet_state_service.get_balance(name, passphrase).unwrap() - ); - assert_eq!( 2, wallet_state_service @@ -499,4 +601,123 @@ mod tests { .unwrap() .is_some()); } + + fn prepare_wallet_storage(name: &str, passphrase: &SecUtf8) -> MemoryStorage { + let storage = MemoryStorage::default(); + let wallet_state_service = WalletStateService::new(storage.clone()); + + let mut memento = WalletStateMemento::default(); + let tx_pointer = |n: u8, i: usize| TxoPointer::new([n; 32], i); + let output = + |n: u8, m: u64| TxOut::new(ExtendedAddr::OrTree([n; 32]), Coin::new(m).unwrap()); + // Add two unspent transaction + memento.add_unspent_transaction(tx_pointer(0, 0), output(0, 100)); + memento.add_unspent_transaction(tx_pointer(0, 1), output(0, 100)); + wallet_state_service + .apply_memento(name, passphrase, &memento) + .unwrap(); + assert_eq!( + wallet_state_service.get_balance(name, passphrase).unwrap(), + WalletBalance { + total: Coin::new(200).unwrap(), + available: Coin::new(200).unwrap(), + pending: Coin::zero(), + } + ); + + // spent the first utxo and return 50 coin + let mut memento = WalletStateMemento::default(); + memento.add_pending_transaction( + [1; 32], + TransactionPending { + used_inputs: vec![tx_pointer(0, 0)], + block_height: 1, + return_amount: Coin::new(50).unwrap(), + }, + ); + wallet_state_service + .apply_memento(name, passphrase, &memento) + .unwrap(); + + assert_eq!( + wallet_state_service.get_balance(name, passphrase).unwrap(), + WalletBalance { + total: Coin::new(150).unwrap(), + available: Coin::new(100).unwrap(), + pending: Coin::new(50).unwrap(), + } + ); + + // now the available utxo is only the second one + let unspent_tx = wallet_state_service + .get_unspent_transactions(name, passphrase) + .unwrap(); + let mut target = BTreeMap::new(); + target.insert(tx_pointer(0, 1), output(0, 100)); + assert_eq!(unspent_tx, target); + storage + } + + #[test] + fn test_sync_and_get_balance() { + let name = "name"; + let passphrase = &SecUtf8::from("passphrase"); + let storage = prepare_wallet_storage(name, passphrase); + let wallet_state_service = WalletStateService::new(storage); + let tx_pointer = |n: u8, i: usize| TxoPointer::new([n; 32], i); + let output = + |n: u8, m: u64| TxOut::new(ExtendedAddr::OrTree([n; 32]), Coin::new(m).unwrap()); + let mut memento = WalletStateMemento::default(); + // if the broadcast transaction success, then we should remove the pending transaction and the unspent transaction + memento.remove_pending_transaction([1; 32]); + memento.remove_unspent_transaction(tx_pointer(0, 0)); + // and add the returned utxo + memento.add_unspent_transaction(tx_pointer(1, 0), output(0, 50)); + wallet_state_service + .apply_memento(name, passphrase, &memento) + .unwrap(); + // now, we can get the balance + assert_eq!( + wallet_state_service.get_balance(name, passphrase).unwrap(), + WalletBalance { + total: Coin::new(150).unwrap(), + available: Coin::new(150).unwrap(), + pending: Coin::zero(), + } + ); + let unspent_tx = wallet_state_service + .get_unspent_transactions(name, passphrase) + .unwrap(); + assert_eq!(unspent_tx.len(), 2); + } + + #[test] + fn test_rollback_and_get_balance() { + let name = "name"; + let passphrase = &SecUtf8::from("passphrase"); + let storage = prepare_wallet_storage(name, passphrase); + let wallet_state_service = WalletStateService::new(storage); + // assume that broadcat failed, then we should rollback + let current_height = 2 + BLOCK_HEIGHT_ENSURE; + let wallet_state = wallet_state_service + .get_wallet_state(name, passphrase) + .unwrap(); + let rollback_txids = wallet_state.get_rollback_pending_tx(current_height); + assert_eq!(rollback_txids, vec![[1; 32]]); + let mut memento = WalletStateMemento::default(); + for txid in rollback_txids { + memento.remove_pending_transaction(txid); + } + wallet_state_service + .apply_memento(name, passphrase, &memento) + .unwrap(); + assert_eq!( + wallet_state_service.get_balance(name, passphrase).unwrap(), + WalletBalance { + total: Coin::new(200).unwrap(), + available: Coin::new(200).unwrap(), + pending: Coin::new(0).unwrap(), + } + ); + } } diff --git a/client-core/src/transaction_builder.rs b/client-core/src/transaction_builder.rs index 1a2d9f256..5f83ddb60 100644 --- a/client-core/src/transaction_builder.rs +++ b/client-core/src/transaction_builder.rs @@ -9,8 +9,10 @@ pub use unauthorized_wallet_transaction_builder::UnauthorizedWalletTransactionBu use secstr::SecUtf8; +use chain_core::init::coin::Coin; use chain_core::tx::data::address::ExtendedAddr; use chain_core::tx::data::attribute::TxAttributes; +use chain_core::tx::data::input::TxoPointer; use chain_core::tx::data::output::TxOut; use chain_core::tx::TxAux; use client_common::{Result, SignedTransaction}; @@ -30,6 +32,11 @@ pub trait WalletTransactionBuilder: Send + Sync { /// - `outputs`: Transaction outputs /// - `return_address`: Address to which change amount will get returned /// - `attributes`: Transaction attributes, + /// + /// # return + /// - `TxAux`: obfuscated transaction + /// - `Vec`: the selected inputs + /// - `Coin`: the return amount of Coin fn build_transfer_tx( &self, name: &str, @@ -38,7 +45,7 @@ pub trait WalletTransactionBuilder: Send + Sync { outputs: Vec, return_address: ExtendedAddr, attributes: TxAttributes, - ) -> Result; + ) -> Result<(TxAux, Vec, Coin)>; /// Obfuscates given signed transaction fn obfuscate(&self, signed_transaction: SignedTransaction) -> Result; diff --git a/client-core/src/transaction_builder/default_wallet_transaction_builder.rs b/client-core/src/transaction_builder/default_wallet_transaction_builder.rs index 54b2fdb72..7574e48c9 100644 --- a/client-core/src/transaction_builder/default_wallet_transaction_builder.rs +++ b/client-core/src/transaction_builder/default_wallet_transaction_builder.rs @@ -3,6 +3,7 @@ use secstr::SecUtf8; use chain_core::init::coin::{sum_coins, Coin}; use chain_core::tx::data::address::ExtendedAddr; use chain_core::tx::data::attribute::TxAttributes; +use chain_core::tx::data::input::TxoPointer; use chain_core::tx::data::output::TxOut; use chain_core::tx::fee::FeeAlgorithm; use chain_core::tx::TxAux; @@ -54,21 +55,31 @@ where outputs: Vec, return_address: ExtendedAddr, attributes: TxAttributes, - ) -> Result { + ) -> Result<(TxAux, Vec, Coin)> { let mut raw_builder = self.select_and_build( &unspent_transactions, outputs, - return_address, + return_address.clone(), attributes.clone(), )?; + let selected_inputs: Vec = raw_builder + .iter_inputs() + .map(|witness_utxo| witness_utxo.prev_txo_pointer.clone()) + .collect(); + let return_amount = raw_builder + .iter_outputs() + .find(|&m| m.address == return_address) + .map(|output| output.value) + .unwrap_or_default(); + let signer = self.signer_manager.create_signer(name, passphrase); raw_builder.sign_all(signer)?; let tx_aux = raw_builder.to_tx_aux(self.transaction_obfuscation.clone())?; - Ok(tx_aux) + Ok((tx_aux, selected_inputs, return_amount)) } #[inline] @@ -121,7 +132,6 @@ where "Sum of output values and fee exceeds maximum allowed amount", ) })?)?; - let raw_tx_builder = self.build_raw_transaction( &selected_unspent_txs, &outputs, @@ -303,7 +313,7 @@ mod default_wallet_transaction_builder_tests { )]; let attributes = TxAttributes::new(171); - let tx_aux = transaction_builder + let (tx_aux, _selected_inputs, _return_amount) = transaction_builder .build_transfer_tx( name, passphrase, diff --git a/client-core/src/transaction_builder/unauthorized_wallet_transaction_builder.rs b/client-core/src/transaction_builder/unauthorized_wallet_transaction_builder.rs index d211a9696..b33d6ff77 100644 --- a/client-core/src/transaction_builder/unauthorized_wallet_transaction_builder.rs +++ b/client-core/src/transaction_builder/unauthorized_wallet_transaction_builder.rs @@ -1,7 +1,9 @@ use secstr::SecUtf8; +use chain_core::init::coin::Coin; use chain_core::tx::data::address::ExtendedAddr; use chain_core::tx::data::attribute::TxAttributes; +use chain_core::tx::data::input::TxoPointer; use chain_core::tx::data::output::TxOut; use chain_core::tx::TxAux; use client_common::{ErrorKind, Result, SignedTransaction}; @@ -22,7 +24,7 @@ impl WalletTransactionBuilder for UnauthorizedWalletTransactionBuilder { _: Vec, _: ExtendedAddr, _: TxAttributes, - ) -> Result { + ) -> Result<(TxAux, Vec, Coin)> { Err(ErrorKind::PermissionDenied.into()) } diff --git a/client-core/src/types.rs b/client-core/src/types.rs index adc379041..1f3026ec5 100644 --- a/client-core/src/types.rs +++ b/client-core/src/types.rs @@ -7,6 +7,7 @@ pub mod transaction_change; pub use self::address_type::AddressType; #[doc(inline)] pub use self::transaction_change::{ - BalanceChange, TransactionChange, TransactionInput, TransactionType, + BalanceChange, TransactionChange, TransactionInput, TransactionPending, TransactionType, + WalletBalance, }; pub use self::wallet_type::WalletKind; diff --git a/client-core/src/types/transaction_change.rs b/client-core/src/types/transaction_change.rs index 81a2b2c16..0b46d92cb 100644 --- a/client-core/src/types/transaction_change.rs +++ b/client-core/src/types/transaction_change.rs @@ -12,6 +12,28 @@ use chain_core::{ use client_common::tendermint::types::Time; use client_common::{ErrorKind, Result, ResultExt, Transaction}; +/// Wallet balance info +#[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize, Encode, Decode)] +pub struct WalletBalance { + /// The total amount balance + pub total: Coin, + /// The available amount balance that can be currently used + pub available: Coin, + /// The pending amount balance + pub pending: Coin, +} + +/// Transaction pending infomation +#[derive(Debug, Clone, Serialize, Deserialize, Encode, Decode)] +pub struct TransactionPending { + /// The selected inputs of the transaction + pub used_inputs: Vec, + /// The block height when broadcast the transaction + pub block_height: u64, + /// the return amount of the transaction + pub return_amount: Coin, +} + /// Transaction data with attached metadata #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct TransactionChange { diff --git a/client-core/src/wallet.rs b/client-core/src/wallet.rs index e02a47625..73a38685b 100644 --- a/client-core/src/wallet.rs +++ b/client-core/src/wallet.rs @@ -19,13 +19,13 @@ use chain_core::tx::data::address::ExtendedAddr; use chain_core::tx::data::attribute::TxAttributes; use chain_core::tx::data::input::TxoPointer; use chain_core::tx::data::output::TxOut; -use chain_core::tx::data::Tx; +use chain_core::tx::data::{Tx, TxId}; use chain_core::tx::witness::tree::RawPubkey; use chain_core::tx::TxAux; use client_common::tendermint::types::BroadcastTxResponse; use client_common::{PrivateKey, PublicKey, Result}; -use crate::types::{AddressType, TransactionChange, WalletKind}; +use crate::types::{AddressType, TransactionChange, TransactionPending, WalletBalance, WalletKind}; use crate::{InputSelectionStrategy, Mnemonic, UnspentTransactions}; /// Interface for a generic wallet @@ -170,7 +170,7 @@ pub trait WalletClient: Send + Sync { ) -> Result; /// Retrieves current balance of wallet - fn balance(&self, name: &str, passphrase: &SecUtf8) -> Result; + fn balance(&self, name: &str, passphrase: &SecUtf8) -> Result; /// Retrieves transaction history of wallet fn history( @@ -215,10 +215,22 @@ pub trait WalletClient: Send + Sync { attributes: TxAttributes, input_selection_strategy: Option, return_address: ExtendedAddr, - ) -> Result; + ) -> Result<(TxAux, Vec, Coin)>; /// Broadcasts a transaction to Crypto.com Chain fn broadcast_transaction(&self, tx_aux: &TxAux) -> Result; + + /// Get the current block height + fn get_current_block_height(&self) -> Result; + + /// Update the wallet state + fn update_tx_pending_state( + &self, + name: &str, + passphrase: &SecUtf8, + tx_id: TxId, + tx_pending: TransactionPending, + ) -> Result<()>; } /// Interface for a generic wallet for multi-signature transactions diff --git a/client-core/src/wallet/default_wallet_client.rs b/client-core/src/wallet/default_wallet_client.rs index 781f58224..cefd670a0 100644 --- a/client-core/src/wallet/default_wallet_client.rs +++ b/client-core/src/wallet/default_wallet_client.rs @@ -12,7 +12,7 @@ use chain_core::tx::data::address::ExtendedAddr; use chain_core::tx::data::attribute::TxAttributes; use chain_core::tx::data::input::TxoPointer; use chain_core::tx::data::output::TxOut; -use chain_core::tx::data::Tx; +use chain_core::tx::data::{Tx, TxId}; use chain_core::tx::witness::tree::RawPubkey; use chain_core::tx::witness::{TxInWitness, TxWitness}; use chain_core::tx::TxAux; @@ -24,8 +24,8 @@ use client_common::{ use crate::service::*; use crate::transaction_builder::UnauthorizedWalletTransactionBuilder; -use crate::types::WalletKind; -use crate::types::{AddressType, BalanceChange, TransactionChange}; +use crate::types::{AddressType, BalanceChange, TransactionChange, TransactionPending}; +use crate::types::{WalletBalance, WalletKind}; use crate::{ InputSelectionStrategy, Mnemonic, MultiSigWalletClient, UnspentTransactions, WalletClient, WalletTransactionBuilder, @@ -407,7 +407,7 @@ where } #[inline] - fn balance(&self, name: &str, passphrase: &SecUtf8) -> Result { + fn balance(&self, name: &str, passphrase: &SecUtf8) -> Result { // Check if wallet exists self.wallet_service.view_key(name, passphrase)?; self.wallet_state_service.get_balance(name, passphrase) @@ -490,7 +490,7 @@ where attributes: TxAttributes, input_selection_strategy: Option, return_address: ExtendedAddr, - ) -> Result { + ) -> Result<(TxAux, Vec, Coin)> { let mut unspent_transactions = self.unspent_transactions(name, passphrase)?; unspent_transactions.apply_all(input_selection_strategy.unwrap_or_default().as_ref()); @@ -509,6 +509,26 @@ where self.tendermint_client .broadcast_transaction(&tx_aux.encode()) } + #[inline] + fn get_current_block_height(&self) -> Result { + let status = self.tendermint_client.status()?; + let current_block_height = status.sync_info.latest_block_height.value(); + Ok(current_block_height) + } + + #[inline] + fn update_tx_pending_state( + &self, + name: &str, + passphrase: &SecUtf8, + tx_id: TxId, + tx_pending: TransactionPending, + ) -> Result<()> { + let mut wallet_state_memento = WalletStateMemento::default(); + wallet_state_memento.add_pending_transaction(tx_id, tx_pending); + self.wallet_state_service + .apply_memento(name, passphrase, &wallet_state_memento) + } } impl MultiSigWalletClient for DefaultWalletClient diff --git a/client-core/src/wallet/syncer.rs b/client-core/src/wallet/syncer.rs index 44c184cbf..1a1f98d96 100644 --- a/client-core/src/wallet/syncer.rs +++ b/client-core/src/wallet/syncer.rs @@ -370,8 +370,19 @@ impl<'a, S: SecureStorage, C: Client, D: TxDecryptor> WalletSyncerImpl<'a, S, C, self.handle_batch(non_empty_batch)?; } } + // rollback the pending transaction + self.rollback_pending_tx(current_block_height) + } - Ok(()) + fn rollback_pending_tx(&mut self, current_block_height: u64) -> Result<()> { + let mut memento = WalletStateMemento::default(); + let state = + service::load_wallet_state(&self.env.storage, &self.env.name, &self.env.passphrase)? + .chain(|| (ErrorKind::StorageError, "get wallet state failed"))?; + for tx_id in state.get_rollback_pending_tx(current_block_height) { + memento.remove_pending_transaction(tx_id); + } + self.save(&memento) } /// Fast forwards state to given status if app hashes match diff --git a/client-core/src/wallet/syncer_logic.rs b/client-core/src/wallet/syncer_logic.rs index 4376669b3..9eadec1cb 100644 --- a/client-core/src/wallet/syncer_logic.rs +++ b/client-core/src/wallet/syncer_logic.rs @@ -126,6 +126,7 @@ fn on_transaction_change( } } + memento.remove_pending_transaction(transaction_change.transaction_id); memento.add_transaction_change(transaction_change); } @@ -367,7 +368,6 @@ mod tests { .expect("handle block for wallet2"); states[1].apply_memento(&memento).expect("apply memento2"); } - assert_eq!(states[0].balance, Coin::new(100).unwrap()); assert_eq!(states[0].transaction_history.len(), 1); assert_eq!(states[0].unspent_transactions.len(), 1); @@ -386,11 +386,17 @@ mod tests { states[1].apply_memento(&memento).expect("apply memento2"); } - assert_eq!(states[0].balance, Coin::new(0).unwrap()); + assert_eq!( + states[0].get_balance().unwrap().total, + Coin::new(0).unwrap() + ); assert_eq!(states[0].transaction_history.len(), 2); assert_eq!(states[0].unspent_transactions.len(), 0); - assert_eq!(states[1].balance, Coin::new(100).unwrap()); + assert_eq!( + states[1].get_balance().unwrap().total, + Coin::new(100).unwrap() + ); assert_eq!(states[1].transaction_history.len(), 1); assert_eq!(states[1].unspent_transactions.len(), 1); } diff --git a/client-network/src/network_ops.rs b/client-network/src/network_ops.rs index d769171bc..adcdc6667 100644 --- a/client-network/src/network_ops.rs +++ b/client-network/src/network_ops.rs @@ -15,6 +15,7 @@ use chain_core::tx::data::input::TxoPointer; use chain_core::tx::data::output::TxOut; use chain_core::tx::TxAux; use client_common::Result; +use client_core::types::TransactionPending; /// Interface for performing network operations on Crypto.com Chain pub trait NetworkOpsClient: Send + Sync { @@ -46,7 +47,7 @@ pub trait NetworkOpsClient: Send + Sync { from_address: &StakedStateAddress, outputs: Vec, attributes: TxAttributes, - ) -> Result; + ) -> Result<(TxAux, TransactionPending)>; /// Creates a new transaction for withdrawing all unbonded stake from an account fn create_withdraw_all_unbonded_stake_transaction( @@ -56,7 +57,7 @@ pub trait NetworkOpsClient: Send + Sync { from_address: &StakedStateAddress, to_address: ExtendedAddr, attributes: TxAttributes, - ) -> Result; + ) -> Result<(TxAux, TransactionPending)>; /// Creates a new transaction for un-jailing a previously jailed account fn create_unjail_transaction( diff --git a/client-network/src/network_ops/default_network_ops_client.rs b/client-network/src/network_ops/default_network_ops_client.rs index 8c5b3fdbb..50f45b811 100644 --- a/client-network/src/network_ops/default_network_ops_client.rs +++ b/client-network/src/network_ops/default_network_ops_client.rs @@ -20,6 +20,7 @@ use client_common::tendermint::types::AbciQueryExt; use client_common::tendermint::Client; use client_common::{Error, ErrorKind, Result, ResultExt, SignedTransaction, Storage}; use client_core::signer::{DummySigner, Signer, WalletSignerManager}; +use client_core::types::TransactionPending; use client_core::{TransactionObfuscation, UnspentTransactions, WalletClient}; use tendermint::Time; @@ -245,7 +246,7 @@ where from_address: &StakedStateAddress, outputs: Vec, attributes: TxAttributes, - ) -> Result { + ) -> Result<(TxAux, TransactionPending)> { let last_block_time = self.get_last_block_time()?; let staked_state = self.get_staked_state(name, passphrase, from_address)?; @@ -308,8 +309,17 @@ where signature, ); let tx_aux = self.transaction_cipher.encrypt(signed_transaction)?; - - Ok(tx_aux) + let block_height = match self.wallet_client.get_current_block_height() { + Ok(h) => h, + Err(e) if e.kind() == ErrorKind::PermissionDenied => 0, // to make unit test pass + Err(e) => return Err(e), + }; + let pending_transaction = TransactionPending { + block_height, + used_inputs: vec![], + return_amount: output_value, + }; + Ok((tx_aux, pending_transaction)) } fn create_unjail_transaction( @@ -371,7 +381,7 @@ where from_address: &StakedStateAddress, to_address: ExtendedAddr, attributes: TxAttributes, - ) -> Result { + ) -> Result<(TxAux, TransactionPending)> { let staked_state = self.get_staked_state(name, passphrase, from_address)?; verify_unjailed(&staked_state).map_err(|e| { @@ -827,7 +837,7 @@ mod tests { .new_staking_address(name, passphrase) .unwrap(); - let transaction = network_ops_client + let (transaction, _pending_tx) = network_ops_client .create_withdraw_unbonded_stake_transaction( name, passphrase, @@ -886,7 +896,7 @@ mod tests { .unwrap(); let to_address = ExtendedAddr::OrTree([0; 32]); - let transaction = network_ops_client + let (transaction, tx_pending) = network_ops_client .create_withdraw_all_unbonded_stake_transaction( name, passphrase, @@ -896,6 +906,8 @@ mod tests { ) .unwrap(); + assert!(tx_pending.is_some()); + match transaction { TxAux::EnclaveTx(TxEnclaveAux::WithdrawUnbondedStakeTx { witness, diff --git a/client-rpc/src/rpc/staking_rpc.rs b/client-rpc/src/rpc/staking_rpc.rs index 613950c4a..1a3d9c7cf 100644 --- a/client-rpc/src/rpc/staking_rpc.rs +++ b/client-rpc/src/rpc/staking_rpc.rs @@ -208,7 +208,7 @@ where let attributes = TxAttributes::new_with_access(self.network_id, access_policies); - let transaction = self + let (transaction, tx_pending) = self .ops_client .create_withdraw_all_unbonded_stake_transaction( &request.name, @@ -222,7 +222,15 @@ where self.client .broadcast_transaction(&transaction) .map_err(to_rpc_error)?; - + // update the wallet pending transaction state + self.client + .update_tx_pending_state( + &request.name, + &request.passphrase, + transaction.tx_id(), + tx_pending, + ) + .map_err(to_rpc_error)?; Ok(hex::encode(transaction.tx_id())) } diff --git a/client-rpc/src/rpc/wallet_rpc.rs b/client-rpc/src/rpc/wallet_rpc.rs index 8fba1e8ed..6c20cd53f 100644 --- a/client-rpc/src/rpc/wallet_rpc.rs +++ b/client-rpc/src/rpc/wallet_rpc.rs @@ -15,14 +15,14 @@ use chain_core::tx::{TxAux, TxEnclaveAux}; use client_common::{PrivateKey, PublicKey, Result as CommonResult}; use client_core::types::WalletKind; use client_core::types::{AddressType, TransactionChange}; +use client_core::types::{TransactionPending, WalletBalance}; use client_core::{Mnemonic, MultiSigWalletClient, UnspentTransactions, WalletClient}; - use crate::server::{rpc_error_from_string, to_rpc_error, WalletRequest}; #[rpc] pub trait WalletRpc: Send + Sync { #[rpc(name = "wallet_balance")] - fn balance(&self, request: WalletRequest) -> Result; + fn balance(&self, request: WalletRequest) -> Result; #[rpc(name = "wallet_create")] fn create(&self, request: WalletRequest, walletkind: WalletKind) -> Result; @@ -114,11 +114,10 @@ impl WalletRpc for WalletRpcImpl where T: WalletClient + MultiSigWalletClient + 'static, { - fn balance(&self, request: WalletRequest) -> Result { - match self.client.balance(&request.name, &request.passphrase) { - Ok(balance) => Ok(balance), - Err(e) => Err(to_rpc_error(e)), - } + fn balance(&self, request: WalletRequest) -> Result { + self.client + .balance(&request.name, &request.passphrase) + .map_err(to_rpc_error) } fn create(&self, request: WalletRequest, kind: WalletKind) -> Result { @@ -276,6 +275,11 @@ where amount: Coin, view_keys: Vec, ) -> Result { + let current_block_height = self + .client + .get_current_block_height() + .map_err(to_rpc_error)?; + let address = to_address .parse::() .map_err(|err| rpc_error_from_string(format!("{}", err)))?; @@ -311,7 +315,7 @@ where .new_transfer_address(&request.name, &request.passphrase) .map_err(to_rpc_error)?; - let transaction = self + let (transaction, selected_inputs, return_amount) = self .client .create_transaction( &request.name, @@ -326,6 +330,21 @@ where self.client .broadcast_transaction(&transaction) .map_err(to_rpc_error)?; + //update the wallet state + let tx_pending = TransactionPending { + used_inputs: selected_inputs, + block_height: current_block_height, + return_amount, + }; + + self.client + .update_tx_pending_state( + &request.name, + &request.passphrase, + transaction.tx_id(), + tx_pending, + ) + .map_err(to_rpc_error)?; if let TxAux::EnclaveTx(TxEnclaveAux::TransferTx { payload: TxObfuscated { txid, .. }, @@ -572,7 +591,7 @@ pub mod tests { ) .unwrap(); assert_eq!( - Coin::zero(), + WalletBalance::default(), wallet_rpc .balance(create_wallet_request("Default", "123456")) .unwrap()