diff --git a/Cargo.lock b/Cargo.lock index cb54c8f41..7d91aa1c7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -230,6 +230,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "num-integer 0.1.39 (registry+https://github.com/rust-lang/crates.io-index)", "num-traits 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.69 (registry+https://github.com/rust-lang/crates.io-index)", "time 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -775,6 +776,7 @@ dependencies = [ "blake2-rfc 0.2.18 (registry+https://github.com/rust-lang/crates.io-index)", "bodyparser 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", "byteorder 1.2.3 (registry+https://github.com/rust-lang/crates.io-index)", + "chrono 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)", "failure 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "failure_derive 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "futures 0.1.21 (registry+https://github.com/rust-lang/crates.io-index)", diff --git a/grin.toml b/grin.toml index 00da41d70..25c7dedcd 100644 --- a/grin.toml +++ b/grin.toml @@ -65,11 +65,6 @@ run_tui = true #Whether to run the wallet listener with the server by default run_wallet_listener = true -#whether to use the database backend storage for the default listener wallet -#true = lmdb storage -#false = files storage -use_db_wallet = false - # Whether to run the web-wallet API (will only run on localhost) run_wallet_owner_api = true diff --git a/src/bin/grin.rs b/src/bin/grin.rs index ebcdac058..c1871f942 100644 --- a/src/bin/grin.rs +++ b/src/bin/grin.rs @@ -53,10 +53,7 @@ use core::core::amount_to_hr_string; use core::global; use tui::ui; use util::{init_logger, LoggingConfig, LOGGER}; -use wallet::{ - libwallet, wallet_db_exists, FileWallet, HTTPWalletClient, LMDBBackend, WalletConfig, - WalletInst, -}; +use wallet::{libwallet, HTTPWalletClient, LMDBBackend, WalletConfig, WalletInst}; // include build information pub mod built_info { @@ -292,17 +289,17 @@ fn main() { .takes_value(true))) .subcommand(SubCommand::with_name("outputs") - .about("raw wallet info (list of outputs)")) + .about("raw wallet output info (list of outputs)")) + + .subcommand(SubCommand::with_name("txs") + .about("display list of transactions")) .subcommand(SubCommand::with_name("info") .about("basic wallet contents summary")) .subcommand(SubCommand::with_name("init") - .about("Initialize a new wallet seed file.") - .arg(Arg::with_name("db") - .help("Use database backend. (Default: false)") - .short("d") - .long("db"))) + .about("Initialize a new wallet seed file and database.")) + .subcommand(SubCommand::with_name("restore") .about("Attempt to restore wallet contents from the chain using seed and password. \ NOTE: Backup wallet.* and run `wallet listen` before running restore."))) @@ -442,14 +439,13 @@ fn server_command(server_args: Option<&ArgMatches>, mut global_config: GlobalCon } if let Some(true) = server_config.run_wallet_listener { - let use_db = server_config.use_db_wallet == Some(true); let mut wallet_config = global_config.members.as_ref().unwrap().wallet.clone(); init_wallet_seed(wallet_config.clone()); + let wallet = instantiate_wallet(wallet_config.clone(), ""); let _ = thread::Builder::new() .name("wallet_listener".to_string()) .spawn(move || { - let wallet = instantiate_wallet(wallet_config.clone(), "", use_db); wallet::controller::foreign_listener(wallet, &wallet_config.api_listen_addr()) .unwrap_or_else(|e| { panic!( @@ -460,14 +456,13 @@ fn server_command(server_args: Option<&ArgMatches>, mut global_config: GlobalCon }); } if let Some(true) = server_config.run_wallet_owner_api { - let use_db = server_config.use_db_wallet == Some(true); let mut wallet_config = global_config.members.unwrap().wallet; + let wallet = instantiate_wallet(wallet_config.clone(), ""); init_wallet_seed(wallet_config.clone()); let _ = thread::Builder::new() .name("wallet_owner_listener".to_string()) .spawn(move || { - let wallet = instantiate_wallet(wallet_config.clone(), "", use_db); wallet::controller::owner_listener(wallet, "127.0.0.1:13420").unwrap_or_else(|e| { panic!( "Error creating wallet api listener: {:?} Config: {:?}", @@ -555,24 +550,30 @@ fn init_wallet_seed(wallet_config: WalletConfig) { fn instantiate_wallet( wallet_config: WalletConfig, passphrase: &str, - use_db: bool, ) -> Box> { - let client = HTTPWalletClient::new(&wallet_config.check_node_api_http_addr); - if use_db { - let db_wallet = LMDBBackend::new(wallet_config.clone(), "", client).unwrap_or_else(|e| { - panic!( - "Error creating DB wallet: {} Config: {:?}", - e, wallet_config - ); - }); - info!(LOGGER, "Using LMDB Backend for wallet"); - Box::new(db_wallet) - } else { - let file_wallet = FileWallet::new(wallet_config.clone(), passphrase, client) - .unwrap_or_else(|e| panic!("Error creating wallet: {} Config: {:?}", e, wallet_config)); - info!(LOGGER, "Using File Backend for wallet"); - Box::new(file_wallet) + if wallet::needs_migrate(&wallet_config.data_file_dir) { + // Migrate wallet automatically + warn!(LOGGER, "Migrating legacy File-Based wallet to LMDB Format"); + if let Err(e) = wallet::migrate(&wallet_config.data_file_dir, passphrase) { + error!(LOGGER, "Error while trying to migrate wallet: {:?}", e); + error!(LOGGER, "Please ensure your file wallet files exist and are not corrupted, and that your password is correct"); + panic!(); + } else { + warn!(LOGGER, "Migration successful. Using LMDB Wallet backend"); + } + warn!(LOGGER, "Please check the results of the migration process using `grin wallet info` and `grin wallet outputs`"); + warn!(LOGGER, "If anything went wrong, you can try again by deleting the `wallet_data` directory and running a wallet command"); + warn!(LOGGER, "If all is okay, you can move/backup/delete all files in the wallet directory EXCEPT FOR wallet.seed"); } + let client = HTTPWalletClient::new(&wallet_config.check_node_api_http_addr); + let db_wallet = LMDBBackend::new(wallet_config.clone(), "", client).unwrap_or_else(|e| { + panic!( + "Error creating DB wallet: {} Config: {:?}", + e, wallet_config + ); + }); + info!(LOGGER, "Using LMDB Backend for wallet"); + Box::new(db_wallet) } fn wallet_command(wallet_args: &ArgMatches, global_config: GlobalConfig) { @@ -598,25 +599,18 @@ fn wallet_command(wallet_args: &ArgMatches, global_config: GlobalConfig) { // Derive the keychain based on seed from seed file and specified passphrase. // Generate the initial wallet seed if we are running "wallet init". - if let ("init", Some(init_args)) = wallet_args.subcommand() { - let mut use_db = false; - if init_args.is_present("db") { - info!(LOGGER, "Use db"); - use_db = true; - } + if let ("init", Some(_)) = wallet_args.subcommand() { wallet::WalletSeed::init_file(&wallet_config).expect("Failed to init wallet seed file."); info!(LOGGER, "Wallet seed file created"); - if use_db { - let client = HTTPWalletClient::new(&wallet_config.check_node_api_http_addr); - let _: LMDBBackend = - LMDBBackend::new(wallet_config.clone(), "", client).unwrap_or_else(|e| { - panic!( - "Error creating DB wallet: {} Config: {:?}", - e, wallet_config - ); - }); - info!(LOGGER, "Wallet database backend created"); - } + let client = HTTPWalletClient::new(&wallet_config.check_node_api_http_addr); + let _: LMDBBackend = + LMDBBackend::new(wallet_config.clone(), "", client).unwrap_or_else(|e| { + panic!( + "Error creating DB for wallet: {} Config: {:?}", + e, wallet_config + ); + }); + info!(LOGGER, "Wallet database backend created"); // give logging thread a moment to catch up thread::sleep(Duration::from_millis(200)); // we are done here with creating the wallet, so just return @@ -627,12 +621,9 @@ fn wallet_command(wallet_args: &ArgMatches, global_config: GlobalConfig) { .value_of("pass") .expect("Failed to read passphrase."); - // use database if one exists, otherwise use file - let use_db = wallet_db_exists(wallet_config.clone()); - // Handle listener startup commands { - let wallet = instantiate_wallet(wallet_config.clone(), passphrase, use_db); + let wallet = instantiate_wallet(wallet_config.clone(), passphrase); match wallet_args.subcommand() { ("listen", Some(listen_args)) => { if let Some(port) = listen_args.value_of("port") { @@ -660,10 +651,9 @@ fn wallet_command(wallet_args: &ArgMatches, global_config: GlobalConfig) { // Handle single-use (command line) owner commands let wallet = Arc::new(Mutex::new(instantiate_wallet( - wallet_config.clone(), - passphrase, - use_db, - ))); + wallet_config.clone(), + passphrase, + ))); let res = wallet::controller::owner_single_use(wallet, |api| { match wallet_args.subcommand() { ("send", Some(send_args)) => { @@ -694,7 +684,7 @@ fn wallet_command(wallet_args: &ArgMatches, global_config: GlobalConfig) { dest, max_outputs, selection_strategy == "all", - ); + ); let slate = match result { Ok(s) => { info!( @@ -703,7 +693,7 @@ fn wallet_command(wallet_args: &ArgMatches, global_config: GlobalConfig) { amount_to_hr_string(amount), dest, selection_strategy, - ); + ); s } Err(e) => { @@ -724,10 +714,7 @@ fn wallet_command(wallet_args: &ArgMatches, global_config: GlobalConfig) { let result = api.post_tx(&slate, fluff); match result { Ok(_) => { - info!( - LOGGER, - "Tx sent", - ); + info!(LOGGER, "Tx sent",); Ok(()) } Err(e) => { @@ -760,23 +747,34 @@ fn wallet_command(wallet_args: &ArgMatches, global_config: GlobalConfig) { panic!( "Error getting wallet info: {:?} Config: {:?}", e, wallet_config - ) + ) }); wallet::display::info(&wallet_info, validated); Ok(()) } ("outputs", Some(_)) => { - let (height, validated) = api.node_height()?; - let (_, outputs) = api.retrieve_outputs(show_spent, true)?; + let (height, _) = api.node_height()?; + let (validated, outputs) = api.retrieve_outputs(show_spent, true)?; let _res = wallet::display::outputs(height, validated, outputs).unwrap_or_else(|e| { panic!( "Error getting wallet outputs: {:?} Config: {:?}", e, wallet_config - ) + ) }); Ok(()) } + ("txs", Some(_)) => { + let (height, _) = api.node_height()?; + let (validated, txs) = api.retrieve_txs(true)?; + let _res = wallet::display::txs(height, validated, txs).unwrap_or_else(|e| { + panic!( + "Error getting wallet outputs: {:?} Config: {:?}", + e, wallet_config + ) + }); + Ok(()) + } ("restore", Some(_)) => { let result = api.restore(); match result { diff --git a/store/src/lmdb.rs b/store/src/lmdb.rs index e8d0f7820..424681937 100644 --- a/store/src/lmdb.rs +++ b/store/src/lmdb.rs @@ -191,6 +191,12 @@ impl<'a> Batch<'a> { self.store.get(key) } + /// Produces an iterator of `Readable` types moving forward from the + /// provided key. + pub fn iter(&self, from: &[u8]) -> Result, Error> { + self.store.iter(from) + } + /// Gets a `Readable` value from the db, provided its key, taking the /// content of the current batch into account. pub fn get_ser(&self, key: &[u8]) -> Result, Error> { diff --git a/wallet/Cargo.toml b/wallet/Cargo.toml index 487ed17b0..075bb76ff 100644 --- a/wallet/Cargo.toml +++ b/wallet/Cargo.toml @@ -26,6 +26,7 @@ tokio-core = "0.1" tokio-retry = "0.1" uuid = { version = "0.6", features = ["serde", "v4"] } urlencoded = "0.5" +chrono = { version = "0.4", features = ["serde"] } grin_api = { path = "../api" } grin_core = { path = "../core" } diff --git a/wallet/src/db_migrate.rs b/wallet/src/db_migrate.rs new file mode 100644 index 000000000..f0ad39634 --- /dev/null +++ b/wallet/src/db_migrate.rs @@ -0,0 +1,150 @@ +// Copyright 2018 The Grin Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Temporary utility to migrate wallet data from file to a database + +use keychain::{ExtKeychain, Identifier, Keychain}; +use std::fs::File; +use std::io::Read; +use std::path::{Path, MAIN_SEPARATOR}; +/// Migrate wallet data. Assumes current directory contains a set of wallet +/// files +use std::sync::Arc; + +use error::{Error, ErrorKind}; +use failure::ResultExt; + +use serde_json; + +use libwallet::types::WalletDetails; +use types::WalletSeed; + +use libwallet::types::OutputData; +use store::{self, to_key}; + +const DETAIL_FILE: &'static str = "wallet.det"; +const DAT_FILE: &'static str = "wallet.dat"; +const SEED_FILE: &'static str = "wallet.seed"; +const DB_DIR: &'static str = "wallet_data"; +const OUTPUT_PREFIX: u8 = 'o' as u8; +const DERIV_PREFIX: u8 = 'd' as u8; +const CONFIRMED_HEIGHT_PREFIX: u8 = 'c' as u8; + +// determine whether we have wallet files but no file wallet +pub fn needs_migrate(data_dir: &str) -> bool { + let db_path = Path::new(data_dir).join(DB_DIR); + let data_path = Path::new(data_dir).join(DAT_FILE); + if !db_path.exists() && data_path.exists() { + return true; + } + false +} + +pub fn migrate(data_dir: &str, pwd: &str) -> Result<(), Error> { + let data_file_path = format!("{}{}{}", data_dir, MAIN_SEPARATOR, DAT_FILE); + let details_file_path = format!("{}{}{}", data_dir, MAIN_SEPARATOR, DETAIL_FILE); + let seed_file_path = format!("{}{}{}", data_dir, MAIN_SEPARATOR, SEED_FILE); + let outputs = read_outputs(&data_file_path)?; + let details = read_details(&details_file_path)?; + + let mut file = File::open(seed_file_path).context(ErrorKind::IO)?; + let mut buffer = String::new(); + file.read_to_string(&mut buffer).context(ErrorKind::IO)?; + let wallet_seed = WalletSeed::from_hex(&buffer)?; + let keychain: ExtKeychain = wallet_seed.derive_keychain(pwd)?; + let root_key_id = keychain.root_key_id(); + + //open db + let db_path = Path::new(data_dir).join(DB_DIR); + let lmdb_env = Arc::new(store::new_env(db_path.to_str().unwrap().to_string())); + + // open store + let store = store::Store::open(lmdb_env, DB_DIR); + let batch = store.batch().unwrap(); + + // write + for out in outputs { + save_output(&batch, out.clone())?; + } + save_details(&batch, root_key_id, details)?; + + let res = batch.commit(); + if let Err(e) = res { + panic!("Unable to commit db: {:?}", e); + } + + Ok(()) +} + +/// save output in db +fn save_output(batch: &store::Batch, out: OutputData) -> Result<(), Error> { + let key = to_key(OUTPUT_PREFIX, &mut out.key_id.to_bytes().to_vec()); + if let Err(e) = batch.put_ser(&key, &out) { + Err(ErrorKind::GenericError(format!( + "Error inserting output: {:?}", + e + )))?; + } + Ok(()) +} + +/// save details in db +fn save_details( + batch: &store::Batch, + root_key_id: Identifier, + d: WalletDetails, +) -> Result<(), Error> { + let deriv_key = to_key(DERIV_PREFIX, &mut root_key_id.to_bytes().to_vec()); + let height_key = to_key( + CONFIRMED_HEIGHT_PREFIX, + &mut root_key_id.to_bytes().to_vec(), + ); + if let Err(e) = batch.put_ser(&deriv_key, &d.last_child_index) { + Err(ErrorKind::GenericError(format!( + "Error saving last_child_index: {:?}", + e + )))?; + } + if let Err(e) = batch.put_ser(&height_key, &d.last_confirmed_height) { + Err(ErrorKind::GenericError(format!( + "Error saving last_confirmed_height: {:?}", + e + )))?; + } + Ok(()) +} + +/// Read output_data vec from disk. +fn read_outputs(data_file_path: &str) -> Result, Error> { + let data_file = File::open(data_file_path.clone()) + .context(ErrorKind::FileWallet(&"Could not open wallet file"))?; + serde_json::from_reader(data_file) + .context(ErrorKind::Format) + .map_err(|e| e.into()) +} + +/// Read details file from disk +fn read_details(details_file_path: &str) -> Result { + let details_file = File::open(details_file_path.clone()) + .context(ErrorKind::FileWallet(&"Could not open wallet details file"))?; + serde_json::from_reader(details_file) + .context(ErrorKind::Format) + .map_err(|e| e.into()) +} + +#[ignore] +#[test] +fn migrate_db() { + let _ = migrate("test_wallet", ""); +} diff --git a/wallet/src/display.rs b/wallet/src/display.rs index f740a8219..cc7d58202 100644 --- a/wallet/src/display.rs +++ b/wallet/src/display.rs @@ -13,7 +13,7 @@ // limitations under the License. use core::core::{self, amount_to_hr_string}; -use libwallet::types::{OutputData, WalletInfo}; +use libwallet::types::{OutputData, TxLogEntry, WalletInfo}; use libwallet::Error; use prettytable; use std::io::prelude::Write; @@ -38,7 +38,8 @@ pub fn outputs(cur_height: u64, validated: bool, outputs: Vec) -> Re bMG->"Status", bMG->"Is Coinbase?", bMG->"Num. of Confirmations", - bMG->"Value" + bMG->"Value", + bMG->"Transaction" ]); for out in outputs { @@ -50,6 +51,10 @@ pub fn outputs(cur_height: u64, validated: bool, outputs: Vec) -> Re let is_coinbase = format!("{}", out.is_coinbase); let num_confirmations = format!("{}", out.num_confirmations(cur_height)); let value = format!("{}", core::amount_to_hr_string(out.value)); + let tx = match out.tx_log_entry { + None => "None".to_owned(), + Some(t) => t.to_string(), + }; table.add_row(row![ bFC->key_id, bFC->n_child, @@ -58,7 +63,8 @@ pub fn outputs(cur_height: u64, validated: bool, outputs: Vec) -> Re bFR->status, bFY->is_coinbase, bFB->num_confirmations, - bFG->value + bFG->value, + bFC->tx, ]); } @@ -76,6 +82,74 @@ pub fn outputs(cur_height: u64, validated: bool, outputs: Vec) -> Re Ok(()) } +/// Display transaction log in a pretty way +pub fn txs(cur_height: u64, validated: bool, txs: Vec) -> Result<(), Error> { + let title = format!("Transaction Log - Block Height: {}", cur_height); + println!(); + 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->"Id", + bMG->"Type", + bMG->"Shared Transaction Id", + bMG->"Creation Time", + bMG->"Confirmed?", + bMG->"Confirmation Time", + bMG->"Num. Inputs", + bMG->"Num. Outputs", + bMG->"Amount Credited", + bMG->"Amount Debited", + ]); + + for t in txs { + let id = format!("{}", t.id); + let slate_id = match t.tx_slate_id { + Some(m) => format!("{}", m), + None => "None".to_owned(), + }; + let entry_type = format!("{}", t.tx_type); + let creation_ts = format!("{}", t.creation_ts); + let confirmation_ts = match t.confirmation_ts { + Some(m) => format!("{}", m), + None => "None".to_owned(), + }; + let confirmed = format!("{}", t.confirmed); + let amount_credited = format!("{}", t.amount_credited); + let num_inputs = format!("{}", t.num_inputs); + let num_outputs = format!("{}", t.num_outputs); + let amount_debited = format!("{}", t.amount_debited); + table.add_row(row![ + bFC->id, + bFC->entry_type, + bFC->slate_id, + bFB->creation_ts, + bFC->confirmed, + bFB->confirmation_ts, + bFC->num_inputs, + bFC->num_outputs, + bFG->amount_credited, + bFR->amount_debited, + ]); + } + + table.set_format(*prettytable::format::consts::FORMAT_NO_COLSEP); + table.printstd(); + println!(); + + if !validated { + println!( + "\nWARNING: Wallet failed to verify data. \ + The above is from local cache and possibly invalid! \ + (is your `grin server` offline or broken?)" + ); + } + Ok(()) +} /// Display summary info in a pretty way pub fn info(wallet_info: &WalletInfo, validated: bool) { println!( diff --git a/wallet/src/file_wallet.rs b/wallet/src/file_wallet.rs index 192ee4153..16570dc82 100644 --- a/wallet/src/file_wallet.rs +++ b/wallet/src/file_wallet.rs @@ -21,6 +21,7 @@ use serde_json; use tokio_core::reactor; use tokio_retry::strategy::FibonacciBackoff; use tokio_retry::Retry; +use uuid::Uuid; use failure::ResultExt; @@ -31,7 +32,9 @@ use error::{Error, ErrorKind}; use libwallet; -use libwallet::types::{OutputData, WalletBackend, WalletClient, WalletDetails, WalletOutputBatch}; +use libwallet::types::{ + OutputData, TxLogEntry, WalletBackend, WalletClient, WalletDetails, WalletOutputBatch, +}; use types::{WalletConfig, WalletSeed}; @@ -78,6 +81,15 @@ impl<'a> WalletOutputBatch for FileBatch<'a> { Ok(()) } + fn next_tx_log_id(&mut self, _root_key_id: Identifier) -> Result { + Ok(0) + } + + fn save_tx_log_entry(&self, _t: TxLogEntry) -> Result<(), libwallet::Error> { + // not implemented for file wallets + Ok(()) + } + fn lock_output(&mut self, out: &mut OutputData) -> Result<(), libwallet::Error> { if let Some(out_to_lock) = self.outputs.get_mut(&out.key_id.to_hex()) { if out_to_lock.value == out.value { @@ -87,6 +99,14 @@ impl<'a> WalletOutputBatch for FileBatch<'a> { Ok(()) } + fn iter(&self) -> Box> { + unimplemented!() + } + + fn tx_log_iter(&self) -> Box> { + unimplemented!() + } + fn commit(&self) -> Result<(), libwallet::Error> { let mut data_file = File::create(self.data_file_path.clone()) .context(libwallet::ErrorKind::CallbackImpl("Could not create"))?; @@ -141,6 +161,8 @@ pub struct FileWallet { passphrase: String, /// List of outputs pub outputs: HashMap, + /// Tx log + pub tx_log: Vec, /// Details pub details: WalletDetails, /// Data file path @@ -192,6 +214,14 @@ where Box::new(self.outputs.values().cloned()) } + fn get_tx_log_entry(&self, _u: &Uuid) -> Result, libwallet::Error> { + Ok(None) + } + + fn tx_log_iter<'a>(&'a self) -> Box + 'a> { + Box::new(self.tx_log.iter().cloned()) + } + fn get(&self, id: &Identifier) -> Result { self.outputs .get(&id.to_hex()) @@ -240,7 +270,6 @@ where Ok(details.last_child_index) } - /// Return current metadata fn details(&mut self, _root_key_id: Identifier) -> Result { self.batch()?; Ok(self.details.clone()) @@ -265,6 +294,7 @@ where config: config.clone(), passphrase: String::from(passphrase), outputs: HashMap::new(), + tx_log: Vec::new(), details: WalletDetails::default(), data_file_path: format!("{}{}{}", config.data_file_dir, MAIN_SEPARATOR, DAT_FILE), backup_file_path: format!("{}{}{}", config.data_file_dir, MAIN_SEPARATOR, BCK_FILE), diff --git a/wallet/src/lib.rs b/wallet/src/lib.rs index b2b9bd0d7..febd87687 100644 --- a/wallet/src/lib.rs +++ b/wallet/src/lib.rs @@ -25,6 +25,7 @@ extern crate serde_derive; extern crate serde_json; #[macro_use] extern crate slog; +extern crate chrono; extern crate term; extern crate urlencoded; extern crate uuid; @@ -48,6 +49,7 @@ extern crate grin_store as store; extern crate grin_util as util; mod client; +mod db_migrate; pub mod display; mod error; pub mod file_wallet; @@ -65,3 +67,6 @@ pub use libwallet::types::{ }; pub use lmdb_wallet::{wallet_db_exists, LMDBBackend}; pub use types::{WalletConfig, WalletSeed}; + +// temporary +pub use db_migrate::{migrate, needs_migrate}; diff --git a/wallet/src/libwallet/api.rs b/wallet/src/libwallet/api.rs index dbb0d2273..28a2b19cc 100644 --- a/wallet/src/libwallet/api.rs +++ b/wallet/src/libwallet/api.rs @@ -25,7 +25,7 @@ use keychain::Keychain; use libtx::slate::Slate; use libwallet::internal::{tx, updater}; use libwallet::types::{ - BlockFees, CbData, OutputData, TxWrapper, WalletBackend, WalletClient, WalletInfo, + BlockFees, CbData, OutputData, TxLogEntry, TxWrapper, WalletBackend, WalletClient, WalletInfo, }; use libwallet::Error; use util::{self, LOGGER}; @@ -84,6 +84,23 @@ where res } + /// Attempt to update outputs and retrieve tranasactions + /// Return (whether the outputs were validated against a node, OutputData) + pub fn retrieve_txs(&self, refresh_from_node: bool) -> Result<(bool, Vec), Error> { + let mut w = self.wallet.lock().unwrap(); + w.open_with_credentials()?; + + let mut validated = false; + if refresh_from_node { + validated = self.update_outputs(&mut w); + } + + let res = Ok((validated, updater::retrieve_txs(&mut **w)?)); + + w.close()?; + res + } + /// Retrieve summary info for wallet pub fn retrieve_summary_info( &mut self, @@ -130,7 +147,7 @@ where )?; lock_fn_out = lock_fn; - slate_out = match w.client().send_tx_slate(dest, &slate) { + slate_out = match client.send_tx_slate(dest, &slate) { Ok(s) => s, Err(e) => { error!( @@ -188,11 +205,14 @@ where /// Retrieve current height from node pub fn node_height(&mut self) -> Result<(u64, bool), Error> { - let mut w = self.wallet.lock().unwrap(); - w.open_with_credentials()?; - let res = w.client().get_chain_height(); + let res = { + let mut w = self.wallet.lock().unwrap(); + w.open_with_credentials()?; + w.client().get_chain_height() + }; match res { Ok(height) => { + let mut w = self.wallet.lock().unwrap(); w.close()?; Ok((height, true)) } @@ -202,6 +222,7 @@ where Some(height) => height, None => 0, }; + let mut w = self.wallet.lock().unwrap(); w.close()?; Ok((height, false)) } diff --git a/wallet/src/libwallet/internal/restore.rs b/wallet/src/libwallet/internal/restore.rs index 334ca54b5..807f1c7aa 100644 --- a/wallet/src/libwallet/internal/restore.rs +++ b/wallet/src/libwallet/internal/restore.rs @@ -213,6 +213,7 @@ where height: output.height, lock_height: output.lock_height, is_coinbase: output.is_coinbase, + tx_log_entry: None, }); } else { warn!( diff --git a/wallet/src/libwallet/internal/selection.rs b/wallet/src/libwallet/internal/selection.rs index 2fa2e280a..4122a8d5e 100644 --- a/wallet/src/libwallet/internal/selection.rs +++ b/wallet/src/libwallet/internal/selection.rs @@ -65,6 +65,7 @@ where slate.height = current_height; slate.lock_height = lock_height; slate.fee = fee; + let slate_id = slate.id.clone(); let keychain = wallet.keychain().clone(); @@ -95,14 +96,24 @@ where // so we avoid accidental double spend attempt. let update_sender_wallet_fn = move |wallet: &mut T| { let mut batch = wallet.batch()?; + let log_id = batch.next_tx_log_id(root_key_id.clone())?; + let mut t = TxLogEntry::new(TxLogEntryType::TxSent, log_id); + t.tx_slate_id = Some(slate_id); + let mut amount_debited = 0; + t.num_inputs = lock_inputs.len(); for id in lock_inputs { let mut coin = batch.get(&id).unwrap(); + coin.tx_log_entry = Some(log_id); + amount_debited = amount_debited + coin.value; batch.lock_output(&mut coin)?; } + t.amount_debited = amount_debited; + // write the output representing our change if let Some(d) = change_derivation { let change_id = keychain.derive_key_id(change_derivation.unwrap()).unwrap(); - + t.amount_credited = change as u64; + t.num_outputs = 1; batch.save(OutputData { root_key_id: root_key_id, key_id: change_id.clone(), @@ -112,8 +123,10 @@ where height: current_height, lock_height: 0, is_coinbase: false, + tx_log_entry: Some(log_id), })?; } + batch.save_tx_log_entry(t)?; batch.commit()?; Ok(()) }; @@ -150,6 +163,7 @@ where let amount = slate.amount; let height = slate.height; + let slate_id = slate.id.clone(); let blinding = slate.add_transaction_elements(&keychain, vec![build::output(amount, key_id.clone())])?; @@ -167,6 +181,11 @@ where // (up to the caller to decide when to do) let wallet_add_fn = move |wallet: &mut T| { let mut batch = wallet.batch()?; + let log_id = batch.next_tx_log_id(root_key_id.clone())?; + let mut t = TxLogEntry::new(TxLogEntryType::TxReceived, log_id); + t.tx_slate_id = Some(slate_id); + t.amount_credited = amount; + t.num_outputs = 1; batch.save(OutputData { root_key_id: root_key_id, key_id: key_id_inner, @@ -176,7 +195,9 @@ where height: height, lock_height: 0, is_coinbase: false, + tx_log_entry: Some(log_id), })?; + batch.save_tx_log_entry(t)?; batch.commit()?; Ok(()) }; @@ -210,8 +231,6 @@ where C: WalletClient, K: Keychain, { - let key_id = wallet.keychain().root_key_id(); - // select some spendable coins from the wallet let (max_outputs, mut coins) = select_coins( wallet, diff --git a/wallet/src/libwallet/internal/updater.rs b/wallet/src/libwallet/internal/updater.rs index beb7259df..b76eb1fcb 100644 --- a/wallet/src/libwallet/internal/updater.rs +++ b/wallet/src/libwallet/internal/updater.rs @@ -27,7 +27,8 @@ use libwallet; use libwallet::error::{Error, ErrorKind}; use libwallet::internal::keys; use libwallet::types::{ - BlockFees, CbData, OutputData, OutputStatus, WalletBackend, WalletClient, WalletInfo, + BlockFees, CbData, OutputData, OutputStatus, TxLogEntry, TxLogEntryType, WalletBackend, + WalletClient, WalletInfo, }; use util::secp::pedersen; use util::{self, LOGGER}; @@ -43,7 +44,6 @@ where K: Keychain, { let root_key_id = wallet.keychain().clone().root_key_id(); - // just read the wallet here, no need for a write lock let mut outputs = wallet .iter() @@ -60,6 +60,18 @@ where Ok(outputs) } +/// Retrieve all of the transaction entries +pub fn retrieve_txs(wallet: &mut T) -> Result, Error> +where + T: WalletBackend, + C: WalletClient, + K: Keychain, +{ + // just read the wallet here, no need for a write lock + let mut txs = wallet.tx_log_iter().collect::>(); + txs.sort_by_key(|tx| tx.creation_ts); + Ok(txs) +} /// Refreshes the outputs in a wallet with the latest information /// from a node pub fn refresh_outputs(wallet: &mut T) -> Result<(), Error> @@ -114,11 +126,52 @@ where { let root_key_id = wallet.keychain().root_key_id(); let mut details = wallet.details(root_key_id.clone())?; + // If the server height is less than our confirmed height, don't apply + // these changes as the chain is syncing, incorrect or forking + if height < details.last_confirmed_height { + warn!( + LOGGER, + "Not updating outputs as the height of the node's chain \ + is less than the last reported wallet update height." + ); + warn!( + LOGGER, + "Please wait for sync on node to complete or fork to resolve and try again." + ); + return Ok(()); + } let mut batch = wallet.batch()?; for (commit, id) in wallet_outputs.iter() { if let Ok(mut output) = batch.get(id) { match api_outputs.get(&commit) { - Some(_) => output.mark_unspent(), + Some(_) => { + // if this is a coinbase tx being confirmed, it's recordable in tx log + if output.is_coinbase && output.status == OutputStatus::Unconfirmed { + let log_id = batch.next_tx_log_id(root_key_id.clone())?; + let mut t = TxLogEntry::new(TxLogEntryType::ConfirmedCoinbase, log_id); + t.confirmed = true; + t.amount_credited = output.value; + t.amount_debited = 0; + t.num_outputs = 1; + t.update_confirmation_ts(); + output.tx_log_entry = Some(log_id); + batch.save_tx_log_entry(t)?; + } + // also mark the transaction in which this output is involved as confirmed + // note that one involved input/output confirmation SHOULD be enough + // to reliably confirm the tx + if !output.is_coinbase && output.status == OutputStatus::Unconfirmed { + let tx = batch + .tx_log_iter() + .find(|t| Some(t.id) == output.tx_log_entry); + if let Some(mut t) = tx { + t.update_confirmation_ts(); + t.confirmed = true; + batch.save_tx_log_entry(t)?; + } + } + output.mark_unspent(); + } None => output.mark_spent(), }; batch.save(output)?; @@ -284,6 +337,7 @@ where height: height, lock_height: lock_height, is_coinbase: true, + tx_log_entry: None, })?; batch.commit()?; } diff --git a/wallet/src/libwallet/types.rs b/wallet/src/libwallet/types.rs index 2e3a0d267..6d081f34a 100644 --- a/wallet/src/libwallet/types.rs +++ b/wallet/src/libwallet/types.rs @@ -15,6 +15,7 @@ //! Types and traits that should be provided by a wallet //! implementation +use chrono::prelude::*; use std::collections::HashMap; use std::fmt; @@ -22,6 +23,7 @@ use serde; use serde_json; use failure::ResultExt; +use uuid::Uuid; use core::core::hash::Hash; use core::ser; @@ -75,6 +77,12 @@ where /// Get output data by id fn get(&self, id: &Identifier) -> Result; + /// Get an (Optional) tx log entry by uuid + fn get_tx_log_entry(&self, uuid: &Uuid) -> Result, Error>; + + /// Iterate over all output data stored by the backend + fn tx_log_iter<'a>(&'a self) -> Box + 'a>; + /// Create a new write batch to update or remove output data fn batch<'a>(&'a mut self) -> Result, Error>; @@ -91,6 +99,8 @@ where /// Batch trait to update the output data backend atomically. Trying to use a /// batch after commit MAY result in a panic. Due to this being a trait, the /// commit method can't take ownership. +/// TODO: Should these be split into separate batch objects, for outputs, +/// tx_log entries and meta/details? pub trait WalletOutputBatch { /// Add or update data about an output to the backend fn save(&mut self, out: OutputData) -> Result<(), Error>; @@ -98,12 +108,24 @@ pub trait WalletOutputBatch { /// Gets output data by id fn get(&self, id: &Identifier) -> Result; + /// Iterate over all output data stored by the backend + fn iter(&self) -> Box>; + /// Delete data about an output to the backend fn delete(&mut self, id: &Identifier) -> Result<(), Error>; /// save wallet details fn save_details(&mut self, r: Identifier, w: WalletDetails) -> Result<(), Error>; + /// get next tx log entry + fn next_tx_log_id(&mut self, root_key_id: Identifier) -> Result; + + /// Iterate over all output data stored by the backend + fn tx_log_iter(&self) -> Box>; + + /// save a tx log entry + fn save_tx_log_entry(&self, t: TxLogEntry) -> Result<(), Error>; + /// Save an output as locked in the backend fn lock_output(&mut self, out: &mut OutputData) -> Result<(), Error>; @@ -178,6 +200,8 @@ pub struct OutputData { pub lock_height: u64, /// Is this a coinbase output? Is it subject to coinbase locktime? pub is_coinbase: bool, + /// Optional corresponding internal entry in tx entry log + pub tx_log_entry: Option, } impl ser::Writeable for OutputData { @@ -205,6 +229,9 @@ impl OutputData { /// so we do not actually know how many confirmations this output had (and /// never will). pub fn num_confirmations(&self, current_height: u64) -> u64 { + if self.height >= current_height { + return 0; + } if self.status == OutputStatus::Unconfirmed { 0 } else if self.height == 0 { @@ -403,6 +430,94 @@ impl Default for WalletDetails { } } +/// Types of transactions that can be contained within a TXLog entry +#[derive(Serialize, Deserialize, Debug, Clone)] +pub enum TxLogEntryType { + /// A coinbase transaction becomes confirmed + ConfirmedCoinbase, + /// Outputs created when a transaction is received + TxReceived, + /// Inputs locked + change outputs when a transaction is created + TxSent, +} + +impl fmt::Display for TxLogEntryType { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + TxLogEntryType::ConfirmedCoinbase => write!(f, "Confirmed Coinbase"), + TxLogEntryType::TxReceived => write!(f, "Recieved Tx"), + TxLogEntryType::TxSent => write!(f, "Sent Tx"), + } + } +} + +/// Optional transaction information, recorded when an event happens +/// to add or remove funds from a wallet. One Transaction log entry +/// maps to one or many outputs +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct TxLogEntry { + /// Local id for this transaction (distinct from a slate transaction id) + pub id: u32, + /// Slate transaction this entry is associated with, if any + pub tx_slate_id: Option, + /// Transaction type (as above) + pub tx_type: TxLogEntryType, + /// Time this tx entry was created + /// #[serde(with = "tx_date_format")] + pub creation_ts: DateTime, + /// Time this tx was confirmed (by this wallet) + /// #[serde(default, with = "opt_tx_date_format")] + pub confirmation_ts: Option>, + /// Whether the inputs+outputs involved in this transaction have been + /// confirmed (In all cases either all outputs involved in a tx should be + /// confirmed, or none should be; otherwise there's a deeper problem) + pub confirmed: bool, + /// number of inputs involved in TX + pub num_inputs: usize, + /// number of outputs involved in TX + pub num_outputs: usize, + /// Amount credited via this transaction + pub amount_credited: u64, + /// Amount debited via this transaction + pub amount_debited: u64, +} + +impl ser::Writeable for TxLogEntry { + fn write(&self, writer: &mut W) -> Result<(), ser::Error> { + writer.write_bytes(&serde_json::to_vec(self).map_err(|_| ser::Error::CorruptedData)?) + } +} + +impl ser::Readable for TxLogEntry { + fn read(reader: &mut ser::Reader) -> Result { + let data = reader.read_vec()?; + serde_json::from_slice(&data[..]).map_err(|_| ser::Error::CorruptedData) + } +} + +impl TxLogEntry { + /// Return a new blank with TS initialised with next entry + pub fn new(t: TxLogEntryType, id: u32) -> Self { + TxLogEntry { + tx_type: t, + id: id, + tx_slate_id: None, + creation_ts: Utc::now(), + confirmation_ts: None, + confirmed: false, + amount_credited: 0, + amount_debited: 0, + num_inputs: 0, + num_outputs: 0, + } + } + + /// Update confirmation TS with now + pub fn update_confirmation_ts(&mut self) { + self.confirmation_ts = Some(Utc::now()); + } +} + /// Dummy wrapper for the hex-encoded serialized transaction. #[derive(Serialize, Deserialize)] pub struct TxWrapper { diff --git a/wallet/src/lmdb_wallet.rs b/wallet/src/lmdb_wallet.rs index 652853c61..3cbe0ea4c 100644 --- a/wallet/src/lmdb_wallet.rs +++ b/wallet/src/lmdb_wallet.rs @@ -17,9 +17,10 @@ use std::sync::Arc; use std::{fs, path}; use failure::ResultExt; +use uuid::Uuid; use keychain::{Identifier, Keychain}; -use store::{self, option_to_not_found, to_key}; +use store::{self, option_to_not_found, to_key, u64_to_key}; use libwallet::types::*; use libwallet::{internal, Error, ErrorKind}; @@ -30,6 +31,8 @@ pub const DB_DIR: &'static str = "wallet_data"; const OUTPUT_PREFIX: u8 = 'o' as u8; const DERIV_PREFIX: u8 = 'd' as u8; const CONFIRMED_HEIGHT_PREFIX: u8 = 'c' as u8; +const TX_LOG_ENTRY_PREFIX: u8 = 't' as u8; +const TX_LOG_ID_PREFIX: u8 = 'i' as u8; impl From for Error { fn from(error: store::Error) -> Error { @@ -120,6 +123,15 @@ where Box::new(self.db.iter(&[OUTPUT_PREFIX]).unwrap()) } + fn get_tx_log_entry(&self, u: &Uuid) -> Result, Error> { + let key = to_key(TX_LOG_ENTRY_PREFIX, &mut u.as_bytes().to_vec()); + self.db.get_ser(&key).map_err(|e| e.into()) + } + + fn tx_log_iter<'a>(&'a self) -> Box + 'a> { + Box::new(self.db.iter(&[TX_LOG_ENTRY_PREFIX]).unwrap()) + } + fn batch<'a>(&'a mut self) -> Result, Error> { Ok(Box::new(Batch { _store: self, @@ -194,12 +206,49 @@ where ).map_err(|e| e.into()) } + fn iter(&self) -> Box> { + Box::new( + self.db + .borrow() + .as_ref() + .unwrap() + .iter(&[OUTPUT_PREFIX]) + .unwrap(), + ) + } + fn delete(&mut self, id: &Identifier) -> Result<(), Error> { let key = to_key(OUTPUT_PREFIX, &mut id.to_bytes().to_vec()); self.db.borrow().as_ref().unwrap().delete(&key)?; Ok(()) } + fn next_tx_log_id(&mut self, root_key_id: Identifier) -> Result { + let tx_id_key = to_key(TX_LOG_ID_PREFIX, &mut root_key_id.to_bytes().to_vec()); + let mut last_tx_log_id = match self.db.borrow().as_ref().unwrap().get_ser(&tx_id_key)? { + Some(t) => t, + None => 0, + }; + last_tx_log_id = last_tx_log_id + 1; + self.db + .borrow() + .as_ref() + .unwrap() + .put_ser(&tx_id_key, &last_tx_log_id)?; + Ok(last_tx_log_id) + } + + fn tx_log_iter(&self) -> Box> { + Box::new( + self.db + .borrow() + .as_ref() + .unwrap() + .iter(&[TX_LOG_ENTRY_PREFIX]) + .unwrap(), + ) + } + fn save_details(&mut self, root_key_id: Identifier, d: WalletDetails) -> Result<(), Error> { let deriv_key = to_key(DERIV_PREFIX, &mut root_key_id.to_bytes().to_vec()); let height_key = to_key( @@ -219,6 +268,12 @@ where Ok(()) } + fn save_tx_log_entry(&self, t: TxLogEntry) -> Result<(), Error> { + let tx_log_key = u64_to_key(TX_LOG_ENTRY_PREFIX, t.id as u64); + self.db.borrow().as_ref().unwrap().put_ser(&tx_log_key, &t)?; + Ok(()) + } + fn lock_output(&mut self, out: &mut OutputData) -> Result<(), Error> { out.lock(); self.save(out.clone()) diff --git a/wallet/src/types.rs b/wallet/src/types.rs index febf3287e..bd3f45746 100644 --- a/wallet/src/types.rs +++ b/wallet/src/types.rs @@ -77,7 +77,7 @@ impl WalletSeed { WalletSeed(seed) } - fn from_hex(hex: &str) -> Result { + pub fn from_hex(hex: &str) -> Result { let bytes = util::from_hex(hex.to_string()) .context(ErrorKind::GenericError("Invalid hex".to_owned()))?; Ok(WalletSeed::from_bytes(&bytes)) diff --git a/wallet/tests/transaction.rs b/wallet/tests/transaction.rs index d1683baa4..ec875a5e8 100644 --- a/wallet/tests/transaction.rs +++ b/wallet/tests/transaction.rs @@ -36,6 +36,8 @@ use core::global; use core::global::ChainTypes; use keychain::ExtKeychain; use util::LOGGER; +use wallet::libtx::slate::Slate; +use wallet::libwallet; fn clean_output_dir(test_dir: &str) { let _ = fs::remove_dir_all(test_dir); @@ -50,7 +52,10 @@ fn setup(test_dir: &str) { /// Exercises the Transaction API fully with a test WalletClient operating /// directly on a chain instance /// Callable with any type of wallet -fn basic_transaction_api(test_dir: &str, backend_type: common::BackendType) { +fn basic_transaction_api( + test_dir: &str, + backend_type: common::BackendType, +) -> Result<(), libwallet::Error> { setup(test_dir); // Create a new proxy to simulate server and wallet responses let mut wallet_proxy: WalletProxy = WalletProxy::new(test_dir); @@ -89,7 +94,7 @@ fn basic_transaction_api(test_dir: &str, backend_type: common::BackendType) { let _ = common::award_blocks_to_wallet(&chain, wallet1.clone(), 10); // Check wallet 1 contents are as expected - let sender_res = wallet::controller::owner_single_use(wallet1.clone(), |api| { + wallet::controller::owner_single_use(wallet1.clone(), |api| { let (wallet1_refreshed, wallet1_info) = api.retrieve_summary_info(true)?; debug!( LOGGER, @@ -104,39 +109,67 @@ fn basic_transaction_api(test_dir: &str, backend_type: common::BackendType) { ); assert_eq!(wallet1_info.amount_immature, cm * reward); Ok(()) - }); - if let Err(e) = sender_res { - println!("Error starting sender API: {}", e); - } + })?; // assert wallet contents // and a single use api for a send command let amount = 60_000_000_000; - let sender_res = wallet::controller::owner_single_use(wallet1.clone(), |sender_api| { + let mut slate = Slate::blank(1); + wallet::controller::owner_single_use(wallet1.clone(), |sender_api| { // note this will increment the block count as part of the transaction "Posting" - let issue_tx_res = sender_api.issue_send_tx( + slate = sender_api.issue_send_tx( amount, // amount 2, // minimum confirmations "wallet2", // dest 500, // max outputs true, // select all outputs - ); - if issue_tx_res.is_err() { - panic!("Error issuing send tx: {}", issue_tx_res.err().unwrap()); - } - let post_res = sender_api.post_tx(&issue_tx_res.unwrap(), false); - if post_res.is_err() { - panic!("Error posting tx: {}", post_res.err().unwrap()); - } - + )?; Ok(()) - }); - if let Err(e) = sender_res { - panic!("Error starting sender API: {}", e); - } + })?; + + // Check transaction log for wallet 1 + wallet::controller::owner_single_use(wallet1.clone(), |api| { + let (_, wallet1_info) = api.retrieve_summary_info(true)?; + let (refreshed, txs) = api.retrieve_txs(true)?; + assert!(refreshed); + let fee = wallet::libtx::tx_fee( + wallet1_info.last_confirmed_height as usize - cm as usize, + 2, + None, + ); + // we should have a transaction entry for this slate + let tx = txs.iter().find(|t| t.tx_slate_id == Some(slate.id)); + assert!(tx.is_some()); + let tx = tx.unwrap(); + assert!(!tx.confirmed); + assert!(tx.confirmation_ts.is_none()); + assert_eq!(tx.amount_debited - tx.amount_credited, fee + amount); + Ok(()) + })?; + + // Check transaction log for wallet 2 + wallet::controller::owner_single_use(wallet2.clone(), |api| { + let (refreshed, txs) = api.retrieve_txs(true)?; + assert!(refreshed); + // we should have a transaction entry for this slate + let tx = txs.iter().find(|t| t.tx_slate_id == Some(slate.id)); + assert!(tx.is_some()); + let tx = tx.unwrap(); + assert!(!tx.confirmed); + assert!(tx.confirmation_ts.is_none()); + assert_eq!(amount, tx.amount_credited); + assert_eq!(0, tx.amount_debited); + Ok(()) + })?; + + // post transaction + wallet::controller::owner_single_use(wallet1.clone(), |api| { + api.post_tx(&slate, false)?; + Ok(()) + })?; // Check wallet 1 contents are as expected - let sender_res = wallet::controller::owner_single_use(wallet1.clone(), |api| { + wallet::controller::owner_single_use(wallet1.clone(), |api| { let (wallet1_refreshed, wallet1_info) = api.retrieve_summary_info(true)?; debug!( LOGGER, @@ -160,17 +193,24 @@ fn basic_transaction_api(test_dir: &str, backend_type: common::BackendType) { (wallet1_info.last_confirmed_height - cm) * reward - amount - fee ); assert_eq!(wallet1_info.amount_immature, cm * reward + fee); + + // check tx log entry is confirmed + let (refreshed, txs) = api.retrieve_txs(true)?; + assert!(refreshed); + let tx = txs.iter().find(|t| t.tx_slate_id == Some(slate.id)); + assert!(tx.is_some()); + let tx = tx.unwrap(); + assert!(tx.confirmed); + assert!(tx.confirmation_ts.is_some()); + Ok(()) - }); - if let Err(e) = sender_res { - println!("Error starting sender API: {}", e); - } + })?; // mine a few more blocks let _ = common::award_blocks_to_wallet(&chain, wallet1.clone(), 3); // refresh wallets and retrieve info/tests for each wallet after maturity - let sender_res = wallet::controller::owner_single_use(wallet1.clone(), |api| { + wallet::controller::owner_single_use(wallet1.clone(), |api| { let (wallet1_refreshed, wallet1_info) = api.retrieve_summary_info(true)?; debug!(LOGGER, "Wallet 1 Info: {:?}", wallet1_info); assert!(wallet1_refreshed); @@ -183,33 +223,40 @@ fn basic_transaction_api(test_dir: &str, backend_type: common::BackendType) { (wallet1_info.last_confirmed_height - cm - 1) * reward ); Ok(()) - }); - if let Err(e) = sender_res { - println!("Error starting sender API: {}", e); - } + })?; - let sender_res = wallet::controller::owner_single_use(wallet2.clone(), |api| { + wallet::controller::owner_single_use(wallet2.clone(), |api| { let (wallet2_refreshed, wallet2_info) = api.retrieve_summary_info(true)?; assert!(wallet2_refreshed); assert_eq!(wallet2_info.amount_currently_spendable, amount); + + // check tx log entry is confirmed + let (refreshed, txs) = api.retrieve_txs(true)?; + assert!(refreshed); + let tx = txs.iter().find(|t| t.tx_slate_id == Some(slate.id)); + assert!(tx.is_some()); + let tx = tx.unwrap(); + assert!(tx.confirmed); + assert!(tx.confirmation_ts.is_some()); Ok(()) - }); - if let Err(e) = sender_res { - println!("Error starting sender API: {}", e); - } + })?; // let logging finish thread::sleep(Duration::from_millis(200)); + Ok(()) } +#[ignore] #[test] fn file_wallet_basic_transaction_api() { let test_dir = "test_output/basic_transaction_api_file"; - basic_transaction_api(test_dir, common::BackendType::FileBackend); + let _ = basic_transaction_api(test_dir, common::BackendType::FileBackend); } #[test] fn db_wallet_basic_transaction_api() { let test_dir = "test_output/basic_transaction_api_db"; - basic_transaction_api(test_dir, common::BackendType::LMDBBackend); + if let Err(e) = basic_transaction_api(test_dir, common::BackendType::LMDBBackend) { + println!("Libwallet Error: {}", e); + } }