diff --git a/api/src/owner.rs b/api/src/owner.rs index c180934..ffcdf0b 100644 --- a/api/src/owner.rs +++ b/api/src/owner.rs @@ -262,7 +262,7 @@ where } /// Returns a list of accounts stored in the wallet (i.e. mappings between - /// user-specified labels and BIP32 derivation paths. + /// user-specified labels and BIP32 derivation paths). /// # Arguments /// * `keychain_mask` - Wallet secret mask to XOR against the stored wallet seed before using, if /// being used. @@ -296,11 +296,22 @@ where &self, keychain_mask: Option<&SecretKey>, ) -> Result, Error> { - let mut w_lock = self.wallet_inst.lock(); - let w = w_lock.lc_provider()?.wallet_inst()?; + wallet_lock!(self.wallet_inst, w); // Test keychain mask, to keep API consistent let _ = w.keychain(keychain_mask)?; - owner::accounts(w) + owner::accounts(w, None) + } + + /// Returns accounts with balances from local wallet data, without refreshing from the node. + /// `minimum_confirmations` is the minimum confirmations required for spendable outputs. + pub fn accounts_info( + &self, + keychain_mask: Option<&SecretKey>, + minimum_confirmations: u64, + ) -> Result, Error> { + wallet_lock!(self.wallet_inst, w); + let _ = w.keychain(keychain_mask)?; + owner::accounts(w, Some(minimum_confirmations)) } /// Creates a new 'account', which is a mapping of a user-specified @@ -310,7 +321,7 @@ where /// /// * `keychain_mask` - Wallet secret mask to XOR against the stored wallet seed before using, if /// being used. - /// * `label` - A human readable label to which to map the new BIP32 Path + /// * `label` - A human-readable label to which to map the new BIP32 Path /// /// # Returns /// * Result Containing: @@ -319,10 +330,10 @@ where /// /// # Remarks /// - /// * Wallets should be initialised with the 'default' path mapped to `m/0/0` + /// * Wallets should be initialized with the 'default' path mapped to `m/0/0` /// * Each call to this function will increment the first element of the path /// so the first call will create an account at `m/1/0` and the second at - /// `m/2/0` etc. . . + /// `m/2/0` etc. /// * The account path is used throughout as the parent key for most key-derivation /// operations. See [`set_active_account`](struct.Owner.html#method.set_active_account) for /// further details. @@ -354,12 +365,13 @@ where } /// Sets the wallet's currently active account. This sets the - /// BIP32 parent path used for most key-derivation operations. + /// BIP32 parent path used for most key-derivation operations + /// persistently between wallet restarts. /// /// # Arguments /// * `keychain_mask` - Wallet secret mask to XOR against the stored wallet seed before using, if /// being used. - /// * `label` - The human readable label for the account. Accounts can be retrieved via + /// * `label` - The human-readable label for the account. Accounts can be retrieved via /// the [`account`](struct.Owner.html#method.accounts) method /// /// # Returns @@ -370,7 +382,7 @@ where /// # Remarks /// /// * Wallet parent paths are 2 path elements long, e.g. `m/0/0` is the path - /// labelled 'default'. Keys derived from this parent path are 3 elements long, + /// labeled 'default'. Keys derived from this parent path are 3 elements long, /// e.g. the secret keys derived from the `m/0/0` path will be at paths `m/0/0/0`, /// `m/0/0/1` etc... /// @@ -400,7 +412,7 @@ where let w = w_lock.lc_provider()?.wallet_inst()?; // Test keychain mask, to keep API consistent let _ = w.keychain(keychain_mask)?; - owner::set_active_account(w, label) + owner::set_active_account(w, keychain_mask, label) } /// Returns a list of outputs from the active account in the wallet. @@ -710,9 +722,9 @@ where /// is confirmed. This method also returns a function that will perform that locking, and it is /// up to the caller to decide the best time to call the lock function /// (via the [`tx_lock_outputs`](struct.Owner.html#method.tx_lock_outputs) method). - /// If the exchange method is intended to be synchronous (such as via a direct http call) + /// If the exchange method is intended to be synchronous (such as via a direct http call), /// then the lock call can wait until the response is confirmed. If it is asynchronous, (such - /// as via file transfer) the lock call should happen immediately (before the file is sent + /// as via file transfer), the lock call should happen immediately (before the file is sent /// to the recipient). /// /// If the `send_args` [`InitTxSendArgs`](../grin_wallet_libwallet/types/struct.InitTxSendArgs.html), @@ -1221,10 +1233,10 @@ where /// Cancels a transaction. This entails: /// * Setting the transaction status to either `TxSentCancelled` or `TxReceivedCancelled` /// * Deleting all change outputs or recipient outputs associated with the transaction - /// * Setting the status of all assocatied inputs from `Locked` to `Spent` so they can be + /// * Setting the status of all associated inputs from `Locked` to `Spent` so they can be /// used in new transactions. /// - /// Transactions can be cancelled by transaction log id or slate id (call with either set to + /// Transactions can be canceled by transaction log id or slate id (call with either set to /// Some, not both) /// /// # Arguments @@ -1306,7 +1318,7 @@ where /// /// # Returns /// * Ok(Some([Slate](../grin_wallet_libwallet/slate/struct.Slate.html)) containing the stored - /// transaction, if successful. Note that this Slate will not contain all of the fields used by + /// transaction, if successful. Note that this Slate will not contain all the fields used by /// the original Slate that resulted in the transaction. /// * Ok(None) if the stored Transaction isn't found. /// * [`libwallet::Error`](../grin_wallet_libwallet/struct.Error.html) if an error is encountered. @@ -1344,7 +1356,7 @@ where } /// 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. + /// The rewind hash when shared, help third-party to retrieve information (outputs, balance, ...) that belongs to this wallet. /// /// # Arguments /// @@ -1379,7 +1391,7 @@ where /// 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 function can be used to retrieve outputs information (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 @@ -1429,7 +1441,7 @@ where /// update the wallet state to be consistent with what's currently in the UTXO set. /// /// This function can be used to repair wallet state, particularly by restoring outputs that may - /// be missing if the wallet owner has cancelled transactions locally that were then successfully + /// be missing if the wallet owner has canceled transactions locally that were then successfully /// posted to the chain. /// /// This operation scans the entire chain, and is expected to be time intensive. It is imperative @@ -1571,7 +1583,7 @@ where /// * None /// /// # Returns - /// * Ok with a String value representing the full path to the top level wallet dierctory + /// * Ok with a String value representing the full path to the top level wallet directory /// * or [`libwallet::Error`](../grin_wallet_libwallet/struct.Error.html) if an error is encountered. /// /// # Example @@ -1605,7 +1617,7 @@ where /// description of the top level directory and default paths. /// /// The wallet must be closed and the updater stopped before changing this directory. - /// Open the wallet again afterwards to load its database from the new location. + /// Open the wallet again afterward to load its database from the new location. /// /// # Arguments /// @@ -1803,7 +1815,7 @@ where /// `Opens` a wallet, populating the internal keychain with the encrypted seed, and optionally /// returning a `keychain_mask` token to the caller to provide in all future calls. /// If using a mask, the seed will be stored in-memory XORed against the `keychain_mask`, and - /// will not be useable if the mask is not provided. + /// will not be usable if the mask is not provided. /// /// # Arguments /// @@ -1840,7 +1852,7 @@ where /// // Create configuration /// let result = api_owner.create_config(&ChainTypes::Mainnet, None, None, None); /// - /// // create new wallet wirh random seed + /// // create new wallet with random seed /// let pw = ZeroingString::from("my_password"); /// let _ = api_owner.create_wallet(None, None, 0, pw.clone()); /// @@ -1916,7 +1928,7 @@ where /// * `password`: The password used to encrypt the seed file. /// /// # Returns - /// * Ok(BIP-39 mneminc) if successful + /// * Ok(BIP-39 mnemonic) if successful /// * or [`libwallet::Error`](../grin_wallet_libwallet/struct.Error.html) if an error is encountered. /// /// # Example @@ -2409,14 +2421,14 @@ where } /// Decode an armored slatepack, returning a Slatepack object that can be - /// viewed, manipulated, output as json, etc. The resulting slatepack will be + /// viewed, manipulated, output as JSON, etc. The resulting slatepack will be /// decrypted by this wallet if possible /// /// # Arguments /// /// * `keychain_mask` - Wallet secret mask to XOR against the stored wallet seed before using /// * `slatepack` - A string representing an armored slatepack - /// * `secret_indices` - Indices along this wallet's deriviation path with which to attempt + /// * `secret_indices` - Indices along this wallet's derivation path with which to attempt /// decryption. If this wallet can't decrypt this slatepack, the payload of the returned /// Slatepack will remain encrypted. /// @@ -2601,7 +2613,7 @@ where // MWIXNET - /// Creates an mwixnet request [SwapReq](../grin_wallet_libwallet/api_impl/types/struct.SwapReq.html) + /// Creates a mwixnet request [SwapReq](../grin_wallet_libwallet/api_impl/types/struct.SwapReq.html) /// from a given output commitment under this wallet's control. /// /// # Arguments diff --git a/api/src/owner_rpc.rs b/api/src/owner_rpc.rs index 92511b0..51d3168 100644 --- a/api/src/owner_rpc.rs +++ b/api/src/owner_rpc.rs @@ -48,7 +48,7 @@ use std::time::Duration; #[easy_jsonrpc_mw::rpc] pub trait OwnerRpc { /** - Networked version of [Owner::accounts](struct.Owner.html#method.accounts). + Networked version of [Owner::accounts](struct.Owner.html#method.accounts). Returns account mappings without balances. # Json rpc example @@ -72,7 +72,9 @@ pub trait OwnerRpc { "Ok": [ { "label": "default", - "path": "0200000000000000000000000000000000" + "path": "0200000000000000000000000000000000", + "info": null, + "current": true } ] }, @@ -84,6 +86,60 @@ pub trait OwnerRpc { */ fn accounts(&self, token: Token) -> Result, Error>; + /** + Networked version of [Owner::accounts_info](struct.Owner.html#method.accounts_info). Returns balances from locally stored state without refreshing from the node. + + # Json rpc example + + ``` + # grin_wallet_api::doctest_helper_json_rpc_owner_assert_response!( + # r#" + { + "jsonrpc": "2.0", + "method": "accounts_info", + "params": { + "token": "d202964900000000d302964900000000d402964900000000d502964900000000", + "minimum_confirmations": 1 + }, + "id": 1 + } + # "# + # , + # r#" + { + "jsonrpc": "2.0", + "result": { + "Ok": [ + { + "label": "default", + "path": "0200000000000000000000000000000000", + "info": { + "amount_awaiting_confirmation": "0", + "amount_awaiting_finalization": "0", + "amount_currently_spendable": "60000000000", + "amount_immature": "180000000000", + "amount_locked": "0", + "amount_reverted": "0", + "last_confirmed_height": "4", + "minimum_confirmations": "1", + "total": "240000000000" + }, + "current": true + } + ] + }, + "id": 1 + } + # "# + # , 4, false, false, false, false); + ``` + */ + fn accounts_info( + &self, + token: Token, + minimum_confirmations: u64, + ) -> Result, Error>; + /** Networked version of [Owner::create_account_path](struct.Owner.html#method.create_account_path). @@ -119,6 +175,7 @@ pub trait OwnerRpc { /** Networked version of [Owner::set_active_account](struct.Owner.html#method.set_active_account). + The account selection is saved across wallet restarts # Json rpc example @@ -2089,6 +2146,14 @@ where Owner::accounts(self, (&token.keychain_mask).as_ref()) } + fn accounts_info( + &self, + token: Token, + minimum_confirmations: u64, + ) -> Result, Error> { + Owner::accounts_info(self, (&token.keychain_mask).as_ref(), minimum_confirmations) + } + fn create_account_path(&self, token: Token, label: &String) -> Result { Owner::create_account_path(self, (&token.keychain_mask).as_ref(), label) } diff --git a/controller/src/command.rs b/controller/src/command.rs index 93b02fc..b3e3146 100644 --- a/controller/src/command.rs +++ b/controller/src/command.rs @@ -31,6 +31,7 @@ use crate::util::secp::key::SecretKey; use crate::util::{Mutex, ZeroingString}; use crate::{controller, display}; +use grin_wallet_libwallet::wallet_lock; use qr_code::QrCode; use serde_json as json; use std::fs::File; @@ -52,7 +53,7 @@ fn show_recovery_phrase(phrase: ZeroingString) { /// Arguments common to all wallet commands #[derive(Clone)] pub struct GlobalArgs { - pub account: String, + pub account: Option, pub api_secret: Option, pub node_api_secret: Option, pub show_spent: bool, @@ -263,6 +264,8 @@ where /// Arguments for account command pub struct AccountArgs { pub create: Option, + pub minimum_confirmations: u64, + pub active: Option, } pub fn account( @@ -275,20 +278,7 @@ where C: NodeClient + 'static, K: keychain::Keychain + 'static, { - if args.create.is_none() { - let res = { - let acct_mappings = owner_api.accounts(keychain_mask)?; - // give logging thread a moment to catch up - thread::sleep(Duration::from_millis(200)); - display::accounts(acct_mappings); - Ok(()) - }; - if let Err(e) = res { - error!("Error listing accounts: {}", e); - return Err(Error::LibWallet(e)); - } - } else { - let label = args.create.unwrap(); + if let Some(label) = args.create { let res = { owner_api.create_account_path(keychain_mask, &label)?; thread::sleep(Duration::from_millis(200)); @@ -300,6 +290,31 @@ where error!("Error creating account '{}': {}", label, e); return Err(Error::LibWallet(e)); } + } else if let Some(label) = args.active { + let res = { + owner_api.set_active_account(keychain_mask, &label)?; + thread::sleep(Duration::from_millis(200)); + info!("Account: '{}' set as active!", label); + Ok(()) + }; + if let Err(e) = res { + thread::sleep(Duration::from_millis(200)); + error!("Error setting account '{}' as active: {}", label, e); + return Err(Error::LibWallet(e)); + } + } else { + let res = { + let acct_mappings = + owner_api.accounts_info(keychain_mask, args.minimum_confirmations)?; + // give logging thread a moment to catch up + thread::sleep(Duration::from_millis(200)); + display::accounts(acct_mappings); + Ok(()) + }; + if let Err(e) = res { + error!("Error listing accounts: {}", e); + return Err(Error::LibWallet(e)); + } } Ok(()) } @@ -702,7 +717,6 @@ pub struct ReceiveArgs { pub fn receive( owner_api: &mut Owner, keychain_mask: Option<&SecretKey>, - g_args: &GlobalArgs, args: ReceiveArgs, mut tor_config: TorConfig, test_mode: bool, @@ -752,7 +766,7 @@ where owner_api.config_path(), km, |api| { - slate = api.receive_tx(&slate, Some(&g_args.account), None)?; + slate = api.receive_tx(&slate, None, None)?; Ok(()) }, )?; @@ -1174,7 +1188,6 @@ pub struct InfoArgs { pub fn info( owner_api: &mut Owner, keychain_mask: Option<&SecretKey>, - g_args: &GlobalArgs, args: InfoArgs, dark_scheme: bool, ) -> Result<(), Error> @@ -1186,8 +1199,9 @@ where let updater_running = owner_api.updater_running.load(Ordering::Relaxed); let (validated, wallet_info) = owner_api.retrieve_summary_info(keychain_mask, true, args.minimum_confirmations)?; + let account = account_label(owner_api)?; display::info( - &g_args.account, + &account, &wallet_info, validated || updater_running, dark_scheme, @@ -1210,8 +1224,9 @@ where let res = owner_api.node_height(keychain_mask)?; let (validated, outputs) = owner_api.retrieve_outputs(keychain_mask, g_args.show_spent, true, None)?; + let account = account_label(owner_api)?; display::outputs( - &g_args.account, + &account, res.height, validated || updater_running, outputs, @@ -1230,7 +1245,6 @@ pub struct TxsArgs { pub fn txs( owner_api: &mut Owner, keychain_mask: Option<&SecretKey>, - g_args: &GlobalArgs, args: TxsArgs, dark_scheme: bool, ) -> Result<(), Error> @@ -1249,8 +1263,9 @@ where let first_tx = args .count .map_or(0, |c| txs.len().saturating_sub(c as usize)); + let account = account_label(owner_api)?; display::txs( - &g_args.account, + &account, res.height, validated || updater_running, &txs[first_tx..], @@ -1276,7 +1291,7 @@ where if id.is_some() { let (_, outputs) = owner_api.retrieve_outputs(keychain_mask, true, false, id)?; display::outputs( - &g_args.account, + &account, res.height, validated || updater_running, outputs, @@ -1459,7 +1474,6 @@ where /// Payment Proof Address pub fn address( owner_api: &mut Owner, - g_args: &GlobalArgs, keychain_mask: Option<&SecretKey>, ) -> Result<(), Error> where @@ -1469,14 +1483,26 @@ where { // Just address at derivation index 0 for now let address = owner_api.get_slatepack_address(keychain_mask, 0)?; + let account = account_label(owner_api)?; println!(); - println!("Address for account - {}", g_args.account); + println!("Address for account - {}", account); println!("-------------------------------------"); println!("{}", address); println!(); Ok(()) } +/// Get current account label. +fn account_label(owner_api: &mut Owner) -> Result +where + L: WalletLCProvider<'static, C, K> + 'static, + C: NodeClient + 'static, + K: keychain::Keychain + 'static, +{ + wallet_lock!(owner_api.wallet_inst, w); + Ok(w.active_account().label) +} + /// Proof Export Args pub struct ProofExportArgs { pub output_file: String, diff --git a/controller/src/display.rs b/controller/src/display.rs index 54077fc..9ed991f 100644 --- a/controller/src/display.rs +++ b/controller/src/display.rs @@ -540,13 +540,20 @@ pub fn accounts(acct_mappings: Vec) { mMG->"Name", bMG->"Parent Output Key", bMG->"Slatepack Address (Index 0)", + bMG->"Spendable balance", ]); for m in acct_mappings { let slatepack_path = address::address_derivation_path(&m.path, 0).to_bip_32_string(); + let spendable = if let Some(info) = m.info { + amount_to_hr_string(info.amount_currently_spendable, true) + } else { + "-".to_string() + }; table.add_row(row![ bFC->m.label, bGC->m.path.to_bip_32_string(), bGC->slatepack_path, + bGC->spendable, ]); } table.set_format( @@ -562,6 +569,7 @@ pub fn accounts(acct_mappings: Vec) { let width = table.to_string().find('\n').unwrap_or(0); println!("{:^1$}", "BIP-32 Derivation Path", width); table.printstd(); + println!("Balances use local data and may be out of date."); println!(); } diff --git a/controller/tests/accounts.rs b/controller/tests/accounts.rs index fd19c28..f1dcbe1 100644 --- a/controller/tests/accounts.rs +++ b/controller/tests/accounts.rs @@ -81,7 +81,7 @@ fn accounts_test_impl(test_dir: &'static str) -> Result<(), libwallet::Error> { // test default accounts exist { - let accounts = api1.accounts(mask1)?; + let accounts = api1.accounts_info(mask1, 2)?; assert_eq!(accounts[0].label, "default"); assert_eq!(accounts[0].path, ExtKeychain::derive_key_id(2, 0, 0, 0, 0)); } @@ -97,6 +97,53 @@ fn accounts_test_impl(test_dir: &'static str) -> Result<(), libwallet::Error> { // trying to add same label again should fail let res = api1.create_account_path(mask1, "account1"); assert!(res.is_err()); + // Check missing flags and temporary account changes + for saved in [None, Some(true)] { + { + wallet_inst!(wallet1, w); + let mut account = w.get_acct_path("default".to_owned())?.unwrap(); + account.current = saved; + let mut batch = w.batch(mask1)?; + batch.save_acct_path(account)?; + batch.commit()?; + } + for active in ["default", "account1"] { + { + wallet_inst!(wallet1, w); + w.set_account_by_name(active)?; + } + for accounts in [api1.accounts(mask1)?, api1.accounts_info(mask1, 2)?] { + assert_eq!(accounts.len(), 4); + assert_eq!(accounts[0].label, active); + for account in accounts { + assert_eq!(account.current, Some(account.label == active)); + } + } + // Keep the saved selection unchanged + wallet_inst!(wallet1, w); + for account in w.acct_path_iter()? { + let expected = if account.label == "default" { + saved + } else { + None + }; + assert_eq!(account.current, expected); + } + } + } + // Restore the saved account on reopen + api1.set_active_account(mask1, "account1")?; + { + wallet_inst!(wallet1, w); + w.set_account_by_name("default")?; + assert_eq!(w.active_account().label, "default"); + } + api1.close_wallet(None)?; + api1.open_wallet(None, "".into(), false)?; + { + wallet_inst!(wallet1, w); + assert_eq!(w.active_account().label, "account1"); + } } // add account to wallet 2 @@ -108,20 +155,20 @@ fn accounts_test_impl(test_dir: &'static str) -> Result<(), libwallet::Error> { // Default wallet 2 to listen on that account { wallet_inst!(wallet2, w); - w.set_parent_key_id_by_name("listener_account")?; + w.set_account_by_name("listener_account")?; } // Mine into two different accounts in the same wallet { wallet_inst!(wallet1, w); - w.set_parent_key_id_by_name("account1")?; + w.set_account_by_name("account1")?; assert_eq!(w.parent_key_id(), ExtKeychain::derive_key_id(2, 1, 0, 0, 0)); } let _ = test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, 7, false); { wallet_inst!(wallet1, w); - w.set_parent_key_id_by_name("account2")?; + w.set_account_by_name("account2")?; assert_eq!(w.parent_key_id(), ExtKeychain::derive_key_id(2, 2, 0, 0, 0)); } let _ = test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, 5, false); @@ -141,12 +188,28 @@ fn accounts_test_impl(test_dir: &'static str) -> Result<(), libwallet::Error> { // now check second account { wallet_inst!(wallet1, w); - w.set_parent_key_id_by_name("account1")?; + w.set_account_by_name("account1")?; } { - let labels: Vec<_> = api1.accounts(mask1)?.into_iter().map(|a| a.label).collect(); + let labels: Vec<_> = api1 + .accounts_info(mask1, 2)? + .into_iter() + .map(|a| a.label) + .collect(); assert_eq!(labels, ["account1", "default", "account2", "account3"]); + assert!(api1.accounts(mask1)?.iter().all(|a| a.info.is_none())); + for confirmations in [0, 1, 2, 10] { + let accounts = api1.accounts_info(mask1, confirmations)?; + assert_eq!(accounts[0].info.as_ref().unwrap().last_confirmed_height, 0); + assert_eq!(accounts[2].info.as_ref().unwrap().last_confirmed_height, 12); + for account in accounts { + api1.set_active_account(mask1, &account.label)?; + let (_, summary) = api1.retrieve_summary_info(mask1, false, confirmations)?; + assert_eq!(account.info.unwrap(), summary); + } + api1.set_active_account(mask1, "account1")?; + } // check last confirmed height on this account is different from above (should be 0) let (_, wallet1_info) = api1.retrieve_summary_info(mask1, false, 1)?; assert_eq!(wallet1_info.last_confirmed_height, 0); @@ -159,10 +222,11 @@ fn accounts_test_impl(test_dir: &'static str) -> Result<(), libwallet::Error> { let (_, txs) = api1.retrieve_txs(mask1, true, None, None, None)?; assert_eq!(txs.len(), 7); } + // should be nothing in default account { wallet_inst!(wallet1, w); - w.set_parent_key_id_by_name("default")?; + w.set_account_by_name("default")?; } { let (_, wallet1_info) = api1.retrieve_summary_info(mask1, false, 1)?; @@ -180,7 +244,7 @@ fn accounts_test_impl(test_dir: &'static str) -> Result<(), libwallet::Error> { // Send a tx to another wallet { wallet_inst!(wallet1, w); - w.set_parent_key_id_by_name("account1")?; + w.set_account_by_name("account1")?; } { let args = InitTxArgs { @@ -218,7 +282,7 @@ fn accounts_test_impl(test_dir: &'static str) -> Result<(), libwallet::Error> { // other account should be untouched { wallet_inst!(wallet1, w); - w.set_parent_key_id_by_name("account2")?; + w.set_account_by_name("account2")?; } { let (_, wallet1_info) = api1.retrieve_summary_info(mask1, false, 1)?; @@ -241,7 +305,7 @@ fn accounts_test_impl(test_dir: &'static str) -> Result<(), libwallet::Error> { // Default account on wallet 2 should be untouched { wallet_inst!(wallet2, w); - w.set_parent_key_id_by_name("default")?; + w.set_account_by_name("default")?; } { let (_, wallet2_info) = api2.retrieve_summary_info(mask2, false, 1)?; diff --git a/controller/tests/file.rs b/controller/tests/file.rs index a4e1fc4..b768080 100644 --- a/controller/tests/file.rs +++ b/controller/tests/file.rs @@ -88,7 +88,7 @@ fn file_exchange_test_impl(test_dir: &'static str, use_bin: bool) -> Result<(), // Get some mining done { wallet_inst!(wallet1, w); - w.set_parent_key_id_by_name("mining")?; + w.set_account_by_name("mining")?; } let mut bh = 10u64; let _ = @@ -130,7 +130,7 @@ fn file_exchange_test_impl(test_dir: &'static str, use_bin: bool) -> Result<(), // Get some mining done { wallet_inst!(wallet2, w); - w.set_parent_key_id_by_name("account1")?; + w.set_account_by_name("account1")?; } let mut slate = PathToSlate((&send_file).into()).get_tx()?.0; diff --git a/controller/tests/invoice.rs b/controller/tests/invoice.rs index 8b76998..4671db0 100644 --- a/controller/tests/invoice.rs +++ b/controller/tests/invoice.rs @@ -80,7 +80,7 @@ fn invoice_tx_impl(test_dir: &'static str) -> Result<(), libwallet::Error> { // Get some mining done { wallet_inst!(wallet1, w); - w.set_parent_key_id_by_name("mining")?; + w.set_account_by_name("mining")?; } let mut _bh = 10u64; let _ = diff --git a/controller/tests/late_lock.rs b/controller/tests/late_lock.rs index 8783610..5624976 100644 --- a/controller/tests/late_lock.rs +++ b/controller/tests/late_lock.rs @@ -77,11 +77,11 @@ fn late_lock_test_impl(test_dir: &'static str) -> Result<(), libwallet::Error> { // Get some mining done { wallet_inst!(wallet1, w); - w.set_parent_key_id_by_name("mining")?; + w.set_account_by_name("mining")?; } { wallet_inst!(wallet2, w); - w.set_parent_key_id_by_name("account1")?; + w.set_account_by_name("account1")?; } test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), mask1, 10, false)?; diff --git a/controller/tests/mwixnet.rs b/controller/tests/mwixnet.rs index 5ae886b..04e920c 100644 --- a/controller/tests/mwixnet.rs +++ b/controller/tests/mwixnet.rs @@ -72,7 +72,7 @@ fn mwixnet_test_impl(test_dir: &'static str) -> Result<(), libwallet::Error> { // Get some mining done { wallet_inst!(wallet1, w); - w.set_parent_key_id_by_name("mining")?; + w.set_account_by_name("mining")?; } let mut bh = 10u64; let _ = @@ -121,7 +121,7 @@ fn mwixnet_test_impl(test_dir: &'static str) -> Result<(), libwallet::Error> { // Check total in 'listener' account { wallet_inst!(wallet1, w); - w.set_parent_key_id_by_name("listener")?; + w.set_account_by_name("listener")?; } let (wallet1_refreshed, wallet1_info) = api1.retrieve_summary_info(mask1, true, 1)?; assert!(wallet1_refreshed); diff --git a/controller/tests/repost.rs b/controller/tests/repost.rs index 7f3a39b..2eccce9 100644 --- a/controller/tests/repost.rs +++ b/controller/tests/repost.rs @@ -86,7 +86,7 @@ fn file_repost_test_impl(test_dir: &'static str) -> Result<(), libwallet::Error> // Get some mining done { wallet_inst!(wallet1, w); - w.set_parent_key_id_by_name("mining")?; + w.set_account_by_name("mining")?; } let mut bh = 10u64; let _ = @@ -120,7 +120,7 @@ fn file_repost_test_impl(test_dir: &'static str) -> Result<(), libwallet::Error> // wallet 1 receives file to different account, completes { wallet_inst!(wallet1, w); - w.set_parent_key_id_by_name("listener")?; + w.set_account_by_name("listener")?; } wallet::controller::foreign_single_use( @@ -138,7 +138,7 @@ fn file_repost_test_impl(test_dir: &'static str) -> Result<(), libwallet::Error> // wallet 1 receives file to different account, completes { wallet_inst!(wallet1, w); - w.set_parent_key_id_by_name("mining")?; + w.set_account_by_name("mining")?; } // wallet 1 finalize @@ -164,7 +164,7 @@ fn file_repost_test_impl(test_dir: &'static str) -> Result<(), libwallet::Error> { wallet_inst!(wallet1, w); - w.set_parent_key_id_by_name("listener")?; + w.set_account_by_name("listener")?; } let (wallet1_refreshed, wallet1_info) = api1.retrieve_summary_info(mask1, true, 1)?; @@ -175,11 +175,11 @@ fn file_repost_test_impl(test_dir: &'static str) -> Result<(), libwallet::Error> // as above, but synchronously { wallet_inst!(wallet1, w); - w.set_parent_key_id_by_name("mining")?; + w.set_account_by_name("mining")?; } { wallet_inst!(wallet2, w); - w.set_parent_key_id_by_name("account1")?; + w.set_account_by_name("account1")?; } let amount = 60_000_000_000; diff --git a/controller/tests/self_send.rs b/controller/tests/self_send.rs index c6ca437..424d31a 100644 --- a/controller/tests/self_send.rs +++ b/controller/tests/self_send.rs @@ -70,7 +70,7 @@ fn self_send_test_impl(test_dir: &'static str) -> Result<(), libwallet::Error> { // Get some mining done { wallet_inst!(wallet1, w); - w.set_parent_key_id_by_name("mining")?; + w.set_account_by_name("mining")?; } let mut bh = 10u64; let _ = @@ -119,7 +119,7 @@ fn self_send_test_impl(test_dir: &'static str) -> Result<(), libwallet::Error> { // Check total in 'listener' account { wallet_inst!(wallet1, w); - w.set_parent_key_id_by_name("listener")?; + w.set_account_by_name("listener")?; } let (wallet1_refreshed, wallet1_info) = api1.retrieve_summary_info(mask1, true, 1)?; assert!(wallet1_refreshed); diff --git a/controller/tests/slatepack.rs b/controller/tests/slatepack.rs index d53f1c4..21023f4 100644 --- a/controller/tests/slatepack.rs +++ b/controller/tests/slatepack.rs @@ -137,7 +137,7 @@ fn slatepack_exchange_test_impl( // Get some mining done { wallet_inst!(wallet1, w); - w.set_parent_key_id_by_name("mining")?; + w.set_account_by_name("mining")?; } let mut bh = 10u64; let _ = @@ -213,7 +213,7 @@ fn slatepack_exchange_test_impl( // Get some mining done { wallet_inst!(wallet2, w); - w.set_parent_key_id_by_name("account1")?; + w.set_account_by_name("account1")?; } let (mut slatepack, mut slate) = diff --git a/controller/tests/updater_thread.rs b/controller/tests/updater_thread.rs index 8e33355..cb339b0 100644 --- a/controller/tests/updater_thread.rs +++ b/controller/tests/updater_thread.rs @@ -80,7 +80,7 @@ fn updater_thread_test_impl(test_dir: &'static str) -> Result<(), libwallet::Err // Get some mining done { wallet_inst!(wallet1, w); - w.set_parent_key_id_by_name("mining")?; + w.set_account_by_name("mining")?; } let bh = 10u64; let _ = diff --git a/libwallet/src/api_impl/owner.rs b/libwallet/src/api_impl/owner.rs index 58bfbd2..e6fb0d0 100644 --- a/libwallet/src/api_impl/owner.rs +++ b/libwallet/src/api_impl/owner.rs @@ -52,19 +52,30 @@ use std::sync::mpsc::Sender; use std::sync::Arc; /// List of accounts -pub fn accounts(w: &mut WalletBackend) -> Result, Error> +pub fn accounts( + w: &mut WalletBackend, + minimum_confirmations: Option, +) -> Result, Error> where C: NodeClient, K: Keychain, { - let mut accounts = keys::accounts(w)?; + let mut accounts: Vec = if let Some(mc) = minimum_confirmations { + updater::retrieve_accounts_info(w, mc)? + } else { + keys::accounts(w)? + }; + let active = w.parent_key_id(); + for account in accounts.iter_mut() { + account.current = Some(account.path == active); + } accounts.sort_by(|a, b| a.path.cmp(&b.path)); // Put active account on top. - accounts.sort_by_key(|k| k.path != w.parent_key_id()); + accounts.sort_by_key(|k| k.path != active); Ok(accounts) } -/// new account path +/// New account path pub fn create_account_path( w: &mut WalletBackend, keychain_mask: Option<&SecretKey>, @@ -77,13 +88,17 @@ where keys::new_acct_path(w, keychain_mask, label) } -/// set active account -pub fn set_active_account(w: &mut WalletBackend, label: &str) -> Result<(), Error> +/// Set active account +pub fn set_active_account( + w: &mut WalletBackend, + keychain_mask: Option<&SecretKey>, + label: &str, +) -> Result<(), Error> where C: NodeClient, K: Keychain, { - w.set_parent_key_id_by_name(label) + w.set_active_account(keychain_mask, label) } /// Hash of the wallet root public key diff --git a/libwallet/src/backend.rs b/libwallet/src/backend.rs index 1fdd310..97324c0 100644 --- a/libwallet/src/backend.rs +++ b/libwallet/src/backend.rs @@ -108,8 +108,8 @@ where pub keychain: Option, /// Check value for XORed keychain seed pub master_checksum: Box>, - /// Parent path to use by default for output operations - parent_key_id: Identifier, + /// BIP32 account path to use by default for output operations + active_account: AcctPathMapping, /// wallet to node client w2n_client: C, } @@ -138,30 +138,48 @@ where // Make sure default wallet derivation path always exists // as well as path (so it can be retrieved by batches to know where to store - // completed transactions, for reference + // completed transactions, for reference) let default_account = AcctPathMapping { - label: "default".to_owned(), + label: "default".to_string(), path: WalletBackend::::default_path(), + info: None, + current: Some(true), }; - { let mut batch = store.batch()?; - batch.put_ser( + let saved_default = batch.get_ser::( Some(ACCOUNT_PATH_MAPPING_PREFIX), default_account.label.as_bytes(), - &default_account, + None, )?; - batch.commit()?; + if saved_default.is_none() { + batch.put_ser( + Some(ACCOUNT_PATH_MAPPING_PREFIX), + default_account.label.as_bytes(), + &default_account, + )?; + batch.commit()?; + } } - let res = WalletBackend { + let mut res = WalletBackend { db: store, data_file_dir: data_file_dir.to_owned(), keychain: None, master_checksum: Box::new(None), - parent_key_id: WalletBackend::::default_path(), + active_account: default_account, w2n_client: n_client, }; + + // Set current active account + let active = res + .acct_path_iter()? + .filter(|a| a.current.unwrap_or(false)) + .collect::>(); + if let Some(a) = active.get(0) { + res.active_account = a.clone(); + } + Ok(res) } @@ -275,27 +293,55 @@ where /*}*/ } - /// Set parent key id by stored account name. - pub fn set_parent_key_id_by_name(&mut self, label: &str) -> Result<(), Error> { + /// Set current active account by stored account name. + pub fn set_active_account( + &mut self, + keychain_mask: Option<&SecretKey>, + label: &str, + ) -> Result<(), Error> { + let res = self.acct_path_iter()?.find(|l| l.label == label); + match res { + None => Err(Error::UnknownAccountLabel(label.to_string())), + Some(mut a) => { + let mut batch = self.batch(keychain_mask)?; + let current = batch + .acct_path_iter()? + .filter(|a| a.current.unwrap_or(false)) + .collect::>(); + for mut a in current { + a.current = None; + batch.save_acct_path(a.clone())?; + } + a.current = Some(true); + batch.save_acct_path(a.clone())?; + batch.commit()?; + + self.active_account = a; + Ok(()) + } + } + } + + /// Set account by stored account name. + pub fn set_account_by_name(&mut self, label: &str) -> Result<(), Error> { let label = label.to_owned(); let res = self.acct_path_iter()?.find(|l| l.label == label); if let Some(a) = res { - self.set_parent_key_id(a.path); + self.active_account = a; Ok(()) } else { Err(Error::UnknownAccountLabel(label)) } } - /// The BIP32 path of the parent path to use for all output-related - /// functions, essentially 'accounts' within a wallet. - pub fn set_parent_key_id(&mut self, id: Identifier) { - self.parent_key_id = id; + /// Get active account. + pub fn active_account(&self) -> AcctPathMapping { + self.active_account.clone() } /// Get the parent path. pub fn parent_key_id(&mut self) -> Identifier { - self.parent_key_id.clone() + self.active_account.path.clone() } /// Get output data by id. @@ -465,14 +511,14 @@ where /// Next child ID when we want to create a new output, based on current parent. pub fn next_child(&mut self, keychain_mask: Option<&SecretKey>) -> Result { - let parent_key_id = self.parent_key_id.clone(); + let parent_key_id = self.active_account.path.clone(); let mut deriv_idx = { let batch = self.db.batch()?; batch - .get_ser(Some(DERIV_PREFIX), &self.parent_key_id.to_bytes(), None)? + .get_ser(Some(DERIV_PREFIX), &parent_key_id.to_bytes(), None)? .unwrap_or_else(|| 0) }; - let mut return_path = self.parent_key_id.to_path(); + let mut return_path = parent_key_id.to_path(); return_path.depth += 1; return_path.path[return_path.depth as usize - 1] = ChildNumber::from(deriv_idx); deriv_idx += 1; @@ -484,8 +530,8 @@ where /// Last verified height of outputs directly descending from the current parent key. pub fn last_confirmed_height(&mut self) -> Result { - let parent_key_id = self.parent_key_id.clone(); - self.last_confirmed_height_for_parent(&parent_key_id) + let parent_key_id = self.active_account.clone(); + self.last_confirmed_height_for_parent(&parent_key_id.path) } /// Last verified height of outputs directly descending from the given parent key. @@ -693,7 +739,8 @@ where } /// Save an account label -> path mapping. - pub fn save_acct_path(&mut self, mapping: AcctPathMapping) -> Result<(), Error> { + pub fn save_acct_path(&mut self, mut mapping: AcctPathMapping) -> Result<(), Error> { + mapping.info = None; self.db.put_ser( Some(ACCOUNT_PATH_MAPPING_PREFIX), mapping.label.as_bytes(), diff --git a/libwallet/src/internal/keys.rs b/libwallet/src/internal/keys.rs index 21c937a..7b223bd 100644 --- a/libwallet/src/internal/keys.rs +++ b/libwallet/src/internal/keys.rs @@ -68,7 +68,7 @@ where K: Keychain, { let label = label.to_owned(); - if wallet.acct_path_iter()?.any(|l| l.label == label) { + if wallet.acct_path_iter()?.any(|a| a.label == label) { return Err(Error::AccountLabelAlreadyExists(label)); } @@ -93,6 +93,8 @@ where let save_path = AcctPathMapping { label, path: return_id.clone(), + info: None, + current: None, }; let mut batch = wallet.batch(keychain_mask)?; @@ -116,6 +118,8 @@ where let save_path = AcctPathMapping { label, path: path.clone(), + info: None, + current: None, }; let mut batch = wallet.batch(keychain_mask)?; diff --git a/libwallet/src/internal/updater.rs b/libwallet/src/internal/updater.rs index 47134e5..024559c 100644 --- a/libwallet/src/internal/updater.rs +++ b/libwallet/src/internal/updater.rs @@ -25,12 +25,12 @@ use crate::grin_core::global; use crate::grin_core::libtx::proof::ProofBuilder; use crate::grin_core::libtx::reward; use crate::grin_keychain::{Identifier, Keychain, SwitchCommitmentType}; -use crate::grin_util as util; use crate::grin_util::secp::key::SecretKey; use crate::grin_util::secp::pedersen; use crate::grin_util::static_secp_instance; use crate::internal::keys; use crate::types::{NodeClient, OutputData, OutputStatus, TxLogEntry, TxLogEntryType, WalletInfo}; +use crate::{grin_util as util, AcctPathMapping}; use crate::{ BlockFees, CbData, OutputCommitMapping, RetrieveTxQueryArgs, RetrieveTxQuerySortField, RetrieveTxQuerySortOrder, WalletBackend, @@ -793,22 +793,12 @@ where Ok(()) } -/// Retrieve summary info about the wallet -/// caller should refresh first if desired -pub fn retrieve_info( - wallet: &mut WalletBackend, - parent_key_id: &Identifier, +/// Return summary info about the wallet account from provided outputs. +fn account_outputs_info>( + outputs: impl IntoIterator, + current_height: u64, minimum_confirmations: u64, -) -> Result -where - C: NodeClient, - K: Keychain, -{ - let current_height = wallet.last_confirmed_height_for_parent(parent_key_id)?; - let outputs = wallet - .iter()? - .filter(|out| out.root_key_id == *parent_key_id); - +) -> Result { let mut unspent_total = 0; let mut immature_total = 0; let mut awaiting_finalization_total = 0; @@ -817,6 +807,7 @@ where let mut reverted_total = 0; for out in outputs { + let out = out.borrow(); match out.status { OutputStatus::Unspent => { if out.is_coinbase && out.lock_height > current_height { @@ -859,6 +850,46 @@ where }) } +/// Retrieve summary info about the wallet accounts +/// caller should refresh first if desired +pub fn retrieve_accounts_info( + wallet: &mut WalletBackend, + minimum_confirmations: u64, +) -> Result, Error> +where + C: NodeClient, + K: Keychain, +{ + let outputs: Vec = wallet.iter()?.collect(); + let mut accounts = keys::accounts(wallet)?; + for a in accounts.iter_mut() { + let os = outputs.iter().filter(|out| out.root_key_id == a.path); + let current_height = wallet.last_confirmed_height_for_parent(&a.path)?; + let info = account_outputs_info(os, current_height, minimum_confirmations)?; + a.info = Some(info); + } + Ok(accounts) +} + +/// Retrieve summary info about the wallet +/// caller should refresh first if desired +pub fn retrieve_info( + wallet: &mut WalletBackend, + parent_key_id: &Identifier, + minimum_confirmations: u64, +) -> Result +where + C: NodeClient, + K: Keychain, +{ + let outputs = wallet + .iter()? + .filter(|out| out.root_key_id == *parent_key_id); + let current_height = wallet.last_confirmed_height_for_parent(&parent_key_id)?; + let info = account_outputs_info(outputs, current_height, minimum_confirmations)?; + Ok(info) +} + /// Build a coinbase output and insert into wallet pub fn build_coinbase( wallet: &mut WalletBackend, diff --git a/libwallet/src/types.rs b/libwallet/src/types.rs index 0aec740..76d7189 100644 --- a/libwallet/src/types.rs +++ b/libwallet/src/types.rs @@ -768,10 +768,14 @@ impl ser::Readable for StoredProofInfo { /// Map of named accounts to BIP32 paths #[derive(Clone, Debug, Serialize, Deserialize)] pub struct AcctPathMapping { - /// label used by user + /// Label used by user. pub label: String, - /// Corresponding parent BIP32 derivation path + /// Corresponding parent BIP32 derivation path. pub path: Identifier, + /// Wallet information for this account, can be loaded after deserialization. + pub info: Option, + /// Flag to check if this is current active account. + pub current: Option, } impl ser::Writeable for AcctPathMapping { diff --git a/src/bin/grin-wallet.yml b/src/bin/grin-wallet.yml index 93a4342..2980a87 100644 --- a/src/bin/grin-wallet.yml +++ b/src/bin/grin-wallet.yml @@ -21,7 +21,6 @@ args: short: a long: account takes_value: true - default_value: default - top_level_dir: help: Top directory in which wallet files are stored (location of 'grin-wallet.toml') short: t @@ -41,13 +40,24 @@ subcommands: - cli: about: Start the wallet in interactive CLI mode (EXPERIMENTAL and UNDER DEVELOPMENT) - account: - about: List wallet accounts or create a new account + about: List accounts, create an account or select the active account args: - create: help: Create a new wallet account with provided name short: c long: create takes_value: true + - minimum_confirmations: + help: Minimum number of confirmations required for an output to be spendable + short: m + long: min_conf + default_value: "10" + takes_value: true + - active: + help: Save the active account, used whenever --account is not given + short: a + long: active + takes_value: true - rewind_hash: about: Return the hash of the wallet root public key. - scan_rewind_hash: diff --git a/src/cli/cli.rs b/src/cli/cli.rs index d410785..2df64e3 100644 --- a/src/cli/cli.rs +++ b/src/cli/cli.rs @@ -186,7 +186,7 @@ where keychain_mask = match args.subcommand() { ("open", Some(_)) => { let mut wallet_lock = owner_api.wallet_inst.lock(); - let lc = wallet_lock.lc_provider().unwrap(); + let lc = wallet_lock.lc_provider()?; let mask = match lc.open_wallet( None, wallet_args::prompt_password(&global_wallet_args.password)?, @@ -202,17 +202,20 @@ where None } }; - if let Some(account) = args.value_of("account") { + if let Some(account) = args + .value_of("account") + .or(global_wallet_args.account.as_deref()) + { if wallet_opened { let wallet_inst = lc.wallet_inst()?; - wallet_inst.set_parent_key_id_by_name(account)?; + wallet_inst.set_account_by_name(account)?; } } mask } ("close", Some(_)) => { let mut wallet_lock = owner_api.wallet_inst.lock(); - let lc = wallet_lock.lc_provider().unwrap(); + let lc = wallet_lock.lc_provider()?; lc.close_wallet(None)?; None } diff --git a/src/cmd/wallet_args.rs b/src/cmd/wallet_args.rs index 35cf7b6..55297b2 100644 --- a/src/cmd/wallet_args.rs +++ b/src/cmd/wallet_args.rs @@ -308,7 +308,11 @@ pub fn parse_global_args( config: &WalletConfig, args: &ArgMatches, ) -> Result { - let account = parse_required(args, "account")?; + let account = if args.subcommand_matches("account").is_some() { + None + } else { + parse_optional(args, "account")? + }; let mut show_spent = false; if args.is_present("show_spent") { show_spent = true; @@ -335,7 +339,7 @@ pub fn parse_global_args( }; Ok(command::GlobalArgs { - account: account.to_owned(), + account, show_spent, api_secret, node_api_secret, @@ -431,7 +435,26 @@ pub fn parse_account_args(account_args: &ArgMatches) -> Result None, Some(s) => Some(s.to_owned()), }; - Ok(command::AccountArgs { create }) + + let active = match account_args.value_of("active") { + None => None, + Some(s) => Some(s.to_owned()), + }; + + if create.is_some() && active.is_some() { + let msg = "create and active cannot both be present".to_string(); + return Err(ParseError::ArgumentError(msg)); + } + + // minimum_confirmations + let min_c = parse_required(account_args, "minimum_confirmations")?; + let min_c = parse_u64(min_c, "minimum_confirmations")?; + + Ok(command::AccountArgs { + create, + minimum_confirmations: min_c, + active, + }) } pub fn parse_send_args(args: &ArgMatches) -> Result { @@ -1143,7 +1166,7 @@ where )?; if let Some(account) = wallet_args.value_of("account") { let wallet_inst = lc.wallet_inst()?; - wallet_inst.set_parent_key_id_by_name(account)?; + wallet_inst.set_account_by_name(account)?; } Ok(mask) } @@ -1252,7 +1275,7 @@ where } ("receive", Some(args)) => { let a = arg_parse!(parse_receive_args(&args)); - command::receive(owner_api, km, &global_wallet_args, a, tor_config, test_mode) + command::receive(owner_api, km, a, tor_config, test_mode) } ("unpack", Some(args)) => { let a = arg_parse!(parse_unpack_args(&args)); @@ -1288,7 +1311,6 @@ where command::info( owner_api, km, - global_wallet_args, a, wallet_config.dark_background_color_scheme.unwrap_or(true), ) @@ -1304,7 +1326,6 @@ where command::txs( owner_api, km, - &global_wallet_args, a, wallet_config.dark_background_color_scheme.unwrap_or(true), ) @@ -1329,7 +1350,7 @@ where let a = arg_parse!(parse_verify_proof_args(&args)); command::proof_verify(owner_api, km, a) } - ("address", Some(_)) => command::address(owner_api, &global_wallet_args, km), + ("address", Some(_)) => command::address(owner_api, km), ("scan", Some(args)) => { let a = arg_parse!(parse_check_args(&args)); command::scan(owner_api, km, a) diff --git a/tests/cmd_line_basic.rs b/tests/cmd_line_basic.rs index f54987c..632c362 100644 --- a/tests/cmd_line_basic.rs +++ b/tests/cmd_line_basic.rs @@ -21,6 +21,8 @@ extern crate log; extern crate grin_wallet; use grin_wallet_impls::test_framework::{self, LocalWalletClient, WalletProxy}; +use std::io::Write; +use std::process::{Command, Stdio}; use clap::App; use std::sync::mpsc; @@ -153,6 +155,36 @@ fn command_line_test_impl(test_dir: &str) -> Result<(), grin_wallet_controller:: ]; execute_command(&app, test_dir, "wallet2", &client2, arg_vec)?; + // Check account selection through the CLI loop + let mut cli = Command::new(env!("CARGO_BIN_EXE_grin-wallet")) + .args([ + "-t", + &format!("{}/wallet2", test_dir), + "-p", + "password2", + "-a", + "account_1", + "-r", + "http://127.0.0.1:1", + "cli", + ]) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .unwrap(); + cli.stdin.take().unwrap().write_all( + b"open\naddress\nclose\n-a default open\naddress\naccount -a account_1\naddress\nexit\n", + ).unwrap(); + let output = cli.wait_with_output().unwrap(); + assert!(output.status.success()); + let output = String::from_utf8(output.stdout).unwrap(); + let accounts: Vec<_> = output + .lines() + .filter_map(|line| line.strip_prefix("Address for account - ")) + .collect(); + assert_eq!(accounts, ["account_1", "default", "account_1"]); + // let's see those accounts let arg_vec = vec!["grin-wallet", "-p", "password1", "account"]; execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?; @@ -213,7 +245,35 @@ fn command_line_test_impl(test_dir: &str) -> Result<(), grin_wallet_controller:: "-i", &file_name, ]; - execute_command(&app, test_dir, "wallet2", &client2, arg_vec.clone())?; + // Reuse startup args as in interactive mode + { + let start = app + .clone() + .get_matches_from(vec!["grin-wallet", "-a", "default", "cli"]); + let global_args = + grin_wallet::cmd::wallet_args::parse_global_args(&wallet_config2, &start).unwrap(); + let mut api = Owner::new(wallet2.clone(), None, config2.config_file_path.clone()); + for command in [ + vec!["grin-wallet", "account", "-a", "account_1"], + vec!["grin-wallet", "address"], + vec!["grin-wallet", "receive", "-i", &file_name], + ] { + let args = app.clone().get_matches_from(command); + grin_wallet::cmd::wallet_args::parse_and_execute( + &mut api, + mask2_i.clone(), + &wallet_config2, + config2.tor_config(), + &global_args, + &args, + true, + true, + )?; + } + let (_, txs) = api.retrieve_txs(mask2_i.as_ref(), false, None, None, None)?; + assert_eq!(txs.len(), 1); + api.set_active_account(mask2_i.as_ref(), "default")?; + } // shouldn't be allowed to receive twice assert!(execute_command(&app, test_dir, "wallet2", &client2, arg_vec).is_err()); @@ -527,6 +587,10 @@ fn command_line_test_impl(test_dir: &str) -> Result<(), grin_wallet_controller:: ]; execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?; + // set default account for wallet2 + let arg_vec = vec!["grin-wallet", "-p", "password2", "account", "-a", "default"]; + execute_command(&app, test_dir, "wallet2", &client2, arg_vec)?; + // issue an invoice tx, wallet 2 let arg_vec = vec!["grin-wallet", "-p", "password2", "invoice", "65"]; execute_command(&app, test_dir, "wallet2", &client2, arg_vec)?; diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 85cbe74..2f4d5b5 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -241,7 +241,7 @@ pub fn instantiate_wallet( grin_wallet_controller::Error, > { wallet_config.chain_type = None; - let mut wallet = Box::new(DefaultWalletImpl::::new(node_client).unwrap()) + let mut wallet = Box::new(DefaultWalletImpl::::new(node_client)?) as Box< dyn WalletInst< DefaultLCProvider, @@ -249,7 +249,7 @@ pub fn instantiate_wallet( ExtKeychain, >, >; - let lc = wallet.lc_provider().unwrap(); + let lc = wallet.lc_provider()?; // legacy hack to avoid the need for changes in existing grin-wallet.toml files // remove `wallet_data` from end of path as // new lifecycle provider assumes grin_wallet.toml is in root of data directory @@ -259,11 +259,9 @@ pub fn instantiate_wallet( wallet_config.data_file_dir = top_level_wallet_dir.to_str().unwrap().into(); } let _ = lc.set_top_level_directory(&wallet_config.data_file_dir); - let keychain_mask = lc - .open_wallet(None, ZeroingString::from(passphrase), true, false) - .unwrap(); + let keychain_mask = lc.open_wallet(None, ZeroingString::from(passphrase), true, false)?; let wallet_inst = lc.wallet_inst()?; - wallet_inst.set_parent_key_id_by_name(account)?; + wallet_inst.set_account_by_name(account)?; Ok((Arc::new(Mutex::new(wallet)), keychain_mask)) }