diff --git a/api/src/owner.rs b/api/src/owner.rs index bf1710dd..9a5cddb4 100644 --- a/api/src/owner.rs +++ b/api/src/owner.rs @@ -27,8 +27,8 @@ use crate::libwallet::api_impl::owner_updater::{start_updater_log_thread, Status use crate::libwallet::api_impl::{owner, owner_updater}; use crate::libwallet::{ AcctPathMapping, Error, InitTxArgs, IssueInvoiceTxArgs, NodeClient, NodeHeightResult, - OutputCommitMapping, PaymentProof, Slate, Slatepack, SlatepackAddress, TxLogEntry, WalletInfo, - WalletInst, WalletLCProvider, + OutputCommitMapping, PaymentProof, Slate, Slatepack, SlatepackAddress, TxLogEntry, ViewWallet, + WalletInfo, WalletInst, WalletLCProvider, }; use crate::util::logger::LoggingConfig; use crate::util::secp::key::SecretKey; @@ -1181,6 +1181,88 @@ where owner::get_stored_tx(&**w, tx_id, slate_id) } + /// Return the rewind hash of the wallet. + /// The rewind hash when shared, help third-party to retrieve informations (outputs, balance, ...) that belongs to this wallet. + /// + /// # Arguments + /// + /// * `keychain_mask` - Wallet secret mask to XOR against the stored wallet seed before using, if + /// being used. + /// + /// # Returns + /// * `Ok(String)` if successful + /// * or [`libwallet::Error`](../grin_wallet_libwallet/struct.Error.html) if an error is encountered. + + /// # Example + /// Set up as in [`new`](struct.Owner.html#method.new) method above. + /// ``` + /// # grin_wallet_api::doctest_helper_setup_doc_env!(wallet, wallet_config); + /// + /// let mut api_owner = Owner::new(wallet.clone(), None); + /// let result = api_owner.scan( + /// None, + /// Some(20000), + /// false, + /// ); + /// + /// if let Ok(_) = result { + /// // Wallet outputs should be consistent with what's on chain + /// // ... + /// } + /// ``` + + pub fn get_rewind_hash(&self, keychain_mask: Option<&SecretKey>) -> Result { + owner::get_rewind_hash(self.wallet_inst.clone(), keychain_mask) + } + + /// Scans the entire UTXO set from the node, identify which outputs belong to the given rewind hash view wallet. + /// + /// This function can be used to retrieve outputs informations (outputs, balance, ...) from a rewind hash view wallet. + /// + /// This operation scans the entire chain, and is expected to be time intensive. It is imperative + /// that no other processes should be trying to use the wallet at the same time this function is + /// running. + /// + /// # Arguments + /// + /// * `rewind_hash` - Rewind hash of a wallet, used to retrieve the output of a third-party wallet. + /// * `start_height` - If provided, the height of the first block from which to start scanning. + /// The scan will start from block 1 if this is not provided. + /// + /// # Returns + /// * `Ok(ViewWallet)` if successful + /// * or [`libwallet::Error`](../grin_wallet_libwallet/struct.Error.html) if an error is encountered. + + /// # Example + /// Set up as in [`new`](struct.Owner.html#method.new) method above. + /// ``` + /// # grin_wallet_api::doctest_helper_setup_doc_env!(wallet, wallet_config); + /// + /// let mut api_owner = Owner::new(wallet.clone(), None); + /// let result = api_owner.scan( + /// None, + /// Some(20000), + /// false, + /// ); + /// + /// if let Ok(_) = result { + /// // Wallet outputs should be consistent with what's on chain + /// // ... + /// } + /// ``` + + pub fn scan_rewind_hash( + &self, + rewind_hash: String, + start_height: Option, + ) -> Result { + let tx = { + let t = self.status_tx.lock(); + t.clone() + }; + owner::scan_rewind_hash(self.wallet_inst.clone(), rewind_hash, start_height, &tx) + } + /// Scans the entire UTXO set from the node, identify which outputs belong to the given wallet /// update the wallet state to be consistent with what's currently in the UTXO set. /// diff --git a/api/src/owner_rpc.rs b/api/src/owner_rpc.rs index 6e61195e..1b3f45e0 100644 --- a/api/src/owner_rpc.rs +++ b/api/src/owner_rpc.rs @@ -21,7 +21,7 @@ use crate::keychain::{Identifier, Keychain}; use crate::libwallet::{ AcctPathMapping, ErrorKind, InitTxArgs, IssueInvoiceTxArgs, NodeClient, NodeHeightResult, OutputCommitMapping, PaymentProof, Slate, SlateVersion, Slatepack, SlatepackAddress, - StatusMessage, TxLogEntry, VersionedSlate, WalletInfo, WalletLCProvider, + StatusMessage, TxLogEntry, VersionedSlate, ViewWallet, WalletInfo, WalletLCProvider, }; use crate::util::logger::LoggingConfig; use crate::util::secp::key::{PublicKey, SecretKey}; @@ -840,6 +840,115 @@ pub trait OwnerRpc { slate_id: Option, ) -> Result, ErrorKind>; + /** + Networked version of [Owner::get_rewind_hash](struct.Owner.html#method.get_rewind_hash). + ``` + # grin_wallet_api::doctest_helper_json_rpc_owner_assert_response!( + # r#" + { + "jsonrpc": "2.0", + "method": "get_rewind_hash", + "params": { + "token": "d202964900000000d302964900000000d402964900000000d502964900000000" + }, + "id": 1 + } + # "# + # , + # r#" + { + "id":1, + "jsonrpc":"2.0", + "result":{ + "Ok":"c820c52a492b7db511c752035483d0e50e8fd3ec62544f1b99638e220a4682de" + } + } + # "# + # , 0, false, false, false, false); + ``` + */ + fn get_rewind_hash(&self, token: Token) -> Result; + + /** + Networked version of [Owner::scan_rewind_hash](struct.Owner.html#method.scan_rewind_hash). + ``` + # grin_wallet_api::doctest_helper_json_rpc_owner_assert_response!( + # r#" + { + "jsonrpc": "2.0", + "method": "scan_rewind_hash", + "params": { + "rewind_hash": "c820c52a492b7db511c752035483d0e50e8fd3ec62544f1b99638e220a4682de", + "start_height": 1 + }, + "id": 1 + } + # "# + # , + # r#" + { + "id":1, + "jsonrpc":"2.0", + "result":{ + "Ok":{ + "last_pmmr_index":8, + "output_result":[ + { + "commit":"08e1da9e6dc4d6e808a718b2f110a991dd775d65ce5ae408a4e1f002a4961aa9e7", + "height":1, + "is_coinbase":true, + "lock_height":4, + "mmr_index":1, + "value":60000000000 + }, + { + "commit":"087df32304c5d4ae8b2af0bc31e700019d722910ef87dd4eec3197b80b207e3045", + "height":2, + "is_coinbase":true, + "lock_height":5, + "mmr_index":2, + "value":60000000000 + }, + { + "commit":"084219d64014223a205431acfa8f8cc3e8cb8c6d04df80b26713314becf83861c7", + "height":3, + "is_coinbase":true, + "lock_height":6, + "mmr_index":4, + "value":60000000000 + }, + { + "commit":"09c5efc4dab05d7d16fc90168c484c13f15a142ea4e1bf93c3fad12f5e8a402598", + "height":4, + "is_coinbase":true, + "lock_height":7, + "mmr_index":5, + "value":60000000000 + }, + { + "commit":"08fe198e525a5937d0c5d01fa354394d2679be6df5d42064a0f7550c332fce3d9d", + "height":5, + "is_coinbase":true, + "lock_height":8, + "mmr_index":8, + "value":60000000000 + } + ], + "rewind_hash":"c820c52a492b7db511c752035483d0e50e8fd3ec62544f1b99638e220a4682de", + "total_balance":300000000000 + } + } + } + # "# + # , 5, false, false, false, false); + ``` + */ + fn scan_rewind_hash( + &self, + rewind_hash: String, + start_height: Option, + ) -> Result; + /** Networked version of [Owner::scan](struct.Owner.html#method.scan). @@ -1896,6 +2005,18 @@ where .map_err(|e| e.kind()) } + fn get_rewind_hash(&self, token: Token) -> Result { + Owner::get_rewind_hash(self, (&token.keychain_mask).as_ref()).map_err(|e| e.kind()) + } + + fn scan_rewind_hash( + &self, + rewind_hash: String, + start_height: Option, + ) -> Result { + Owner::scan_rewind_hash(self, rewind_hash, start_height).map_err(|e| e.kind()) + } + fn scan( &self, token: Token, diff --git a/controller/src/command.rs b/controller/src/command.rs index 722e344e..410b59b4 100644 --- a/controller/src/command.rs +++ b/controller/src/command.rs @@ -29,6 +29,7 @@ use crate::libwallet::{ use crate::util::secp::key::SecretKey; use crate::util::{Mutex, ZeroingString}; use crate::{controller, display}; +use ::core::time; use serde_json as json; use std::convert::TryFrom; use std::fs::File; @@ -116,6 +117,80 @@ where Ok(()) } +pub fn rewind_hash<'a, L, C, K>( + owner_api: &mut Owner, + keychain_mask: Option<&SecretKey>, +) -> Result<(), Error> +where + L: WalletLCProvider<'static, C, K>, + C: NodeClient + 'static, + K: keychain::Keychain + 'static, +{ + controller::owner_single_use(None, keychain_mask, Some(owner_api), |api, m| { + let rewind_hash = api.get_rewind_hash(m)?; + println!(); + println!("Wallet Rewind Hash"); + println!("-------------------------------------"); + println!("{}", rewind_hash); + println!(); + Ok(()) + })?; + Ok(()) +} + +/// Arguments for rewind hash view wallet scan command +pub struct ViewWalletScanArgs { + pub rewind_hash: String, + pub start_height: Option, + pub backwards_from_tip: Option, +} + +pub fn scan_rewind_hash( + owner_api: &mut Owner, + args: ViewWalletScanArgs, + dark_scheme: bool, +) -> Result<(), Error> +where + L: WalletLCProvider<'static, C, K> + 'static, + C: NodeClient + 'static, + K: keychain::Keychain + 'static, +{ + controller::owner_single_use(None, None, Some(owner_api), |api, m| { + let rewind_hash = args.rewind_hash; + let tip_height = api.node_height(m)?.height; + let start_height = match args.backwards_from_tip { + Some(b) => tip_height.saturating_sub(b), + None => match args.start_height { + Some(s) => s, + None => 1, + }, + }; + warn!( + "Starting view wallet output scan from height {} ...", + start_height + ); + let result = api.scan_rewind_hash(rewind_hash, Some(start_height)); + let deci_sec = time::Duration::from_millis(100); + thread::sleep(deci_sec); + match result { + Ok(res) => { + warn!("View wallet check complete"); + if res.total_balance != 0 { + display::view_wallet_output(res.clone(), tip_height, dark_scheme)?; + } + display::view_wallet_balance(res.clone(), tip_height, dark_scheme); + Ok(()) + } + Err(e) => { + error!("View wallet check failed: {}", e); + error!("Backtrace: {}", e.backtrace().unwrap()); + Err(e) + } + } + })?; + Ok(()) +} + /// Arguments for listen command pub struct ListenArgs {} diff --git a/controller/src/display.rs b/controller/src/display.rs index 557bd04e..5668485b 100644 --- a/controller/src/display.rs +++ b/controller/src/display.rs @@ -16,7 +16,7 @@ use crate::core::core::FeeFields; use crate::core::core::{self, amount_to_hr_string}; use crate::core::global; use crate::libwallet::{ - AcctPathMapping, Error, OutputCommitMapping, OutputStatus, TxLogEntry, WalletInfo, + AcctPathMapping, Error, OutputCommitMapping, OutputStatus, TxLogEntry, ViewWallet, WalletInfo, }; use crate::util::ToHex; use grin_wallet_util::OnionV3Address; @@ -289,6 +289,98 @@ pub fn txs( } Ok(()) } + +pub fn view_wallet_balance(w: ViewWallet, cur_height: u64, dark_background_color_scheme: bool) { + println!( + "\n____ View Wallet Summary Info - Block Height: {} ____\n Rewind Hash - {}\n", + cur_height, w.rewind_hash + ); + let mut table = table!(); + + if dark_background_color_scheme { + table.add_row(row![ + bFG->"Total Balance", + FG->amount_to_hr_string(w.total_balance, false) + ]); + } else { + table.add_row(row![ + bFG->"Total Balance", + FG->amount_to_hr_string(w.total_balance, false) + ]); + }; + table.set_format(*prettytable::format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR); + table.printstd(); + println!(); +} + +pub fn view_wallet_output( + view_wallet: ViewWallet, + cur_height: u64, + dark_background_color_scheme: bool, +) -> Result<(), Error> { + println!(); + let title = format!("View Wallet Outputs - Block Height: {}", cur_height); + + if term::stdout().is_none() { + println!("Could not open terminal"); + return Ok(()); + } + + let mut t = term::stdout().unwrap(); + t.fg(term::color::MAGENTA).unwrap(); + writeln!(t, "{}", title).unwrap(); + t.reset().unwrap(); + + let mut table = table!(); + + table.set_titles(row![ + bMG->"Output Commitment", + bMG->"MMR Index", + bMG->"Block Height", + bMG->"Locked Until", + bMG->"Coinbase?", + bMG->"# Confirms", + bMG->"Value", + ]); + + for m in view_wallet.output_result { + let commit = format!("{}", m.commit); + let index = m.mmr_index; + let height = format!("{}", m.height); + let lock_height = format!("{}", m.lock_height); + let is_coinbase = format!("{}", m.is_coinbase); + let num_confirmations = format!("{}", m.num_confirmations(cur_height)); + let value = format!("{}", core::amount_to_hr_string(m.value, false)); + + if dark_background_color_scheme { + table.add_row(row![ + bFC->commit, + bFB->index, + bFB->height, + bFB->lock_height, + bFY->is_coinbase, + bFB->num_confirmations, + bFG->value, + ]); + } else { + table.add_row(row![ + bFD->commit, + bFB->index, + bFB->height, + bFB->lock_height, + bFD->is_coinbase, + bFB->num_confirmations, + bFG->value, + ]); + } + } + + table.set_format(*prettytable::format::consts::FORMAT_NO_COLSEP); + table.printstd(); + println!(); + Ok(()) +} + /// Display summary info in a pretty way pub fn info( account: &str, diff --git a/libwallet/src/api_impl/owner.rs b/libwallet/src/api_impl/owner.rs index 67092d39..e0a1ae0b 100644 --- a/libwallet/src/api_impl/owner.rs +++ b/libwallet/src/api_impl/owner.rs @@ -18,8 +18,10 @@ use uuid::Uuid; use crate::grin_core::core::hash::Hashed; use crate::grin_core::core::Transaction; +use crate::grin_keychain::ViewKey; use crate::grin_util::secp::key::SecretKey; use crate::grin_util::Mutex; +use crate::grin_util::ToHex; use crate::util::{OnionV3Address, OnionV3AddressError}; use crate::api_impl::owner_updater::StatusMessage; @@ -30,7 +32,7 @@ use crate::types::{AcctPathMapping, NodeClient, TxLogEntry, WalletBackend, Walle use crate::{ address, wallet_lock, InitTxArgs, IssueInvoiceTxArgs, NodeHeightResult, OutputCommitMapping, PaymentProof, ScannedBlockInfo, Slatepack, SlatepackAddress, Slatepacker, SlatepackerArgs, - TxLogEntryType, WalletInitStatus, WalletInst, WalletLCProvider, + TxLogEntryType, ViewWallet, WalletInitStatus, WalletInst, WalletLCProvider, }; use crate::{Error, ErrorKind}; use ed25519_dalek::PublicKey as DalekPublicKey; @@ -75,6 +77,23 @@ where w.set_parent_key_id_by_name(label) } +/// Hash of the wallet root public key +pub fn get_rewind_hash<'a, L, C, K>( + wallet_inst: Arc>>>, + keychain_mask: Option<&SecretKey>, +) -> Result +where + L: WalletLCProvider<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + wallet_lock!(wallet_inst, w); + let keychain = w.keychain(keychain_mask)?; + let root_public_key = keychain.public_root_key(); + let rewind_hash = ViewKey::rewind_hash(keychain.secp(), root_public_key).to_hex(); + Ok(rewind_hash) +} + /// Retrieve the slatepack address for the current parent key at /// the given index pub fn get_slatepack_address<'a, L, C, K>( @@ -936,6 +955,46 @@ where } } +/// Scan outputs with the rewind hash of a third-party wallet. +/// Help to retrieve outputs information that belongs it +pub fn scan_rewind_hash<'a, L, C, K>( + wallet_inst: Arc>>>, + rewind_hash: String, + start_height: Option, + status_send_channel: &Option>, +) -> Result +where + L: WalletLCProvider<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + let is_hex = rewind_hash.chars().all(|c| c.is_ascii_hexdigit()); + let rewind_hash = rewind_hash.to_lowercase(); + if !(is_hex && rewind_hash.len() == 64) { + let msg = format!("Invalid Rewind Hash"); + return Err(ErrorKind::RewindHash(msg).into()); + } + + let tip = { + wallet_lock!(wallet_inst, w); + w.w2n_client().get_chain_tip()? + }; + + let start_height = match start_height { + Some(h) => h, + None => 1, + }; + + let info = scan::scan_rewind_hash( + wallet_inst, + rewind_hash, + start_height, + tip.0, + status_send_channel, + )?; + Ok(info) +} + /// check repair /// Accepts a wallet inst instead of a raw wallet so it can /// lock as little as possible diff --git a/libwallet/src/error.rs b/libwallet/src/error.rs index 4e068ec8..3f1b548c 100644 --- a/libwallet/src/error.rs +++ b/libwallet/src/error.rs @@ -298,6 +298,14 @@ pub enum ErrorKind { #[fail(display = "Age error: {}", _0)] Age(String), + /// Rewind Hash parsing error + #[fail(display = "Rewind Hash error: {}", _0)] + RewindHash(String), + + /// Nonce creation error + #[fail(display = "Nonce error: {}", _0)] + Nonce(String), + /// Slatepack address parsing error #[fail(display = "SlatepackAddress error: {}", _0)] SlatepackAddress(String), diff --git a/libwallet/src/internal/scan.rs b/libwallet/src/internal/scan.rs index d8cb3496..a2a4a3b3 100644 --- a/libwallet/src/internal/scan.rs +++ b/libwallet/src/internal/scan.rs @@ -21,10 +21,13 @@ use crate::grin_core::libtx::proof; use crate::grin_keychain::{Identifier, Keychain, SwitchCommitmentType}; use crate::grin_util::secp::key::SecretKey; use crate::grin_util::secp::pedersen; +use crate::grin_util::secp::{ContextFlag, Secp256k1}; use crate::grin_util::Mutex; +use crate::grin_util::{from_hex, ToHex}; use crate::internal::{keys, updater}; -use crate::types::*; +use crate::{types::*, ErrorKind}; use crate::{wallet_lock, Error, OutputCommitMapping}; +use blake2_rfc::blake2b::blake2b; use std::cmp; use std::collections::HashMap; use std::sync::mpsc::Sender; @@ -142,6 +145,88 @@ where Ok(wallet_outputs) } +fn collect_chain_outputs_rewind_hash<'a, C>( + client: C, + rewind_hash: String, + start_index: u64, + end_index: Option, + status_send_channel: &Option>, +) -> Result +where + C: NodeClient + 'a, +{ + let batch_size = 1000; + let start_index_stat = start_index; + let mut start_index = start_index; + let mut vw = ViewWallet { + rewind_hash: rewind_hash, + output_result: vec![], + total_balance: 0, + last_pmmr_index: 0, + }; + let secp = Secp256k1::with_caps(ContextFlag::VerifyOnly); + + loop { + let (highest_index, last_retrieved_index, outputs) = + client.get_outputs_by_pmmr_index(start_index, end_index, batch_size)?; + + let range = highest_index as f64 - start_index_stat as f64; + let progress = last_retrieved_index as f64 - start_index_stat as f64; + let percentage_complete = cmp::min(((progress / range) * 100.0) as u8, 99); + + let msg = format!( + "Checking {} outputs, up to index {}. (Highest index: {})", + outputs.len(), + highest_index, + last_retrieved_index, + ); + if let Some(ref s) = status_send_channel { + let _ = s.send(StatusMessage::Scanning(msg, percentage_complete)); + } + + // Scanning outputs + for output in outputs.iter() { + let (commit, proof, is_coinbase, height, mmr_index) = output; + let rewind_hash = from_hex(vw.rewind_hash.as_str()).map_err(|e| { + ErrorKind::RewindHash(format!("Unable to decode rewind hash: {}", e)) + })?; + let rewind_nonce = blake2b(32, &commit.0, &rewind_hash); + let nonce = SecretKey::from_slice(&secp, rewind_nonce.as_bytes()) + .map_err(|e| ErrorKind::Nonce(format!("Unable to create nonce: {}", e)))?; + let info = secp.rewind_bullet_proof(*commit, nonce.clone(), None, *proof); + + if info.is_err() { + continue; + } + + let info = info.unwrap(); + vw.total_balance += info.value; + let lock_height = if *is_coinbase { + *height + global::coinbase_maturity() + } else { + *height + }; + + let output_info = ViewWalletOutputResult { + commit: commit.to_hex(), + value: info.value, + height: *height, + mmr_index: *mmr_index, + is_coinbase: *is_coinbase, + lock_height: lock_height, + }; + + vw.output_result.push(output_info); + } + if highest_index <= last_retrieved_index { + vw.last_pmmr_index = last_retrieved_index; + break; + } + start_index = last_retrieved_index + 1; + } + Ok(vw) +} + fn collect_chain_outputs<'a, C, K>( keychain: &K, client: C, @@ -319,6 +404,53 @@ where Ok(()) } +/// Scan outputs with a given rewind hash view wallet. +/// Retrieve all outputs information that belongs to it. +pub fn scan_rewind_hash<'a, L, C, K>( + wallet_inst: Arc>>>, + rewind_hash: String, + start_height: u64, + end_height: u64, + status_send_channel: &Option>, +) -> Result +where + L: WalletLCProvider<'a, C, K>, + C: NodeClient + 'a, + K: Keychain + 'a, +{ + if let Some(ref s) = status_send_channel { + let _ = s.send(StatusMessage::Scanning("Starting UTXO scan".to_owned(), 0)); + } + let client = { + wallet_lock!(wallet_inst, w); + w.w2n_client().clone() + }; + // Retrieve the actual PMMR index range we're looking for + let pmmr_range = client.height_range_to_pmmr_indices(start_height, Some(end_height))?; + + let chain_outs = collect_chain_outputs_rewind_hash( + client, + rewind_hash, + pmmr_range.0, + Some(pmmr_range.1), + status_send_channel, + )?; + + let msg = format!( + "Identified {} wallet_outputs as belonging to this wallet", + chain_outs.output_result.len(), + ); + if let Some(ref s) = status_send_channel { + let _ = s.send(StatusMessage::Scanning(msg, 99)); + } + if let Some(ref s) = status_send_channel { + let _ = s.send(StatusMessage::ScanningComplete( + "Scanning Complete".to_owned(), + )); + } + Ok(chain_outs) +} + /// Check / repair wallet contents by scanning against chain /// assume wallet contents have been freshly updated with contents /// of latest block diff --git a/libwallet/src/lib.rs b/libwallet/src/lib.rs index 2ae08f06..dde3797b 100644 --- a/libwallet/src/lib.rs +++ b/libwallet/src/lib.rs @@ -75,7 +75,8 @@ pub use slate_versions::ser as dalek_ser; pub use types::{ AcctPathMapping, BlockIdentifier, CbData, Context, NodeClient, NodeVersionInfo, OutputData, OutputStatus, ScannedBlockInfo, StoredProofInfo, TxLogEntry, TxLogEntryType, TxWrapper, - WalletBackend, WalletInfo, WalletInitStatus, WalletInst, WalletLCProvider, WalletOutputBatch, + ViewWallet, WalletBackend, WalletInfo, WalletInitStatus, WalletInst, WalletLCProvider, + WalletOutputBatch, }; /// Helper for taking a lock on the wallet instance diff --git a/libwallet/src/types.rs b/libwallet/src/types.rs index 6f1ee585..4ea401c3 100644 --- a/libwallet/src/types.rs +++ b/libwallet/src/types.rs @@ -1019,6 +1019,45 @@ impl ser::Readable for WalletInitStatus { } } +/// Utility struct for return values from below +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ViewWallet { + /// Rewind Hash used to retrieve the outputs + pub rewind_hash: String, + /// All outputs information that belongs to the rewind hash + pub output_result: Vec, + /// total balance + pub total_balance: u64, + /// last pmmr index + pub last_pmmr_index: u64, +} +/// Utility struct for return values from below +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ViewWalletOutputResult { + /// + pub commit: String, + /// + pub value: u64, + /// + pub height: u64, + /// + pub mmr_index: u64, + /// + pub is_coinbase: bool, + /// + pub lock_height: u64, +} + +impl ViewWalletOutputResult { + pub fn num_confirmations(&self, tip_height: u64) -> u64 { + if self.height > tip_height { + return 0; + } else { + 1 + (tip_height - self.height) + } + } +} + /// Serializes an Option to and from a string pub mod option_duration_as_secs { use serde::de::Error; diff --git a/src/bin/grin-wallet.yml b/src/bin/grin-wallet.yml index 59b227b7..88b27624 100644 --- a/src/bin/grin-wallet.yml +++ b/src/bin/grin-wallet.yml @@ -48,6 +48,24 @@ subcommands: short: c long: create takes_value: true + - rewind_hash: + about: Return the hash of the wallet root public key. + - scan_rewind_hash: + about: Scan the UTXO set and return the outputs and the total of grin owned by a view wallet rewind hash. + args: + - rewind_hash: + help: Rewind hash of the wallet to be scanned in order to retrieve all the outputs and balance. + index: 1 + - start_height: + help: If given, the first block from which to start the scan (default 1) + short: h + long: start_height + takes_value: true + - backwards_from_tip: + help: If given, start scan b blocks back from the tip + short: b + long: backwards_from_tip, + takes_value: true - listen: about: Runs the wallet in listening mode waiting for transactions args: diff --git a/src/cmd/wallet_args.rs b/src/cmd/wallet_args.rs index 70d542cd..e9d8039c 100644 --- a/src/cmd/wallet_args.rs +++ b/src/cmd/wallet_args.rs @@ -413,6 +413,23 @@ pub fn parse_owner_api_args( Ok(()) } +pub fn parse_scan_rewind_hash_args( + args: &ArgMatches, +) -> Result { + let rewind_hash = parse_required(args, "rewind_hash")?; + let start_height = parse_u64_or_none(args.value_of("start_height")); + let backwards_from_tip = parse_u64_or_none(args.value_of("backwards_from_tip")); + if backwards_from_tip.is_some() && start_height.is_some() { + let msg = format!("backwards_from tip and start_height cannot both be present"); + return Err(ParseError::ArgumentError(msg)); + } + Ok(command::ViewWalletScanArgs { + rewind_hash: rewind_hash.into(), + start_height, + backwards_from_tip, + }) +} + pub fn parse_account_args(account_args: &ArgMatches) -> Result { let create = match account_args.value_of("create") { None => None, @@ -1110,6 +1127,15 @@ where global_wallet_args, test_mode, ), + ("rewind_hash", Some(_)) => command::rewind_hash(owner_api, km), + ("scan_rewind_hash", Some(args)) => { + let a = arg_parse!(parse_scan_rewind_hash_args(&args)); + command::scan_rewind_hash( + owner_api, + a, + wallet_config.dark_background_color_scheme.unwrap_or(true), + ) + } ("account", Some(args)) => { let a = arg_parse!(parse_account_args(&args)); command::account(owner_api, km, a)