LMDB Wallet Transaction Log + LMDB Migration for all (#1268)

* beginnings of transaction log for db

* add migrate utility for file-wallet to db wallet

* rustfmt + missing file

* update transaction log entry status on confirmed txs, add basic tests for transaction log
This commit is contained in:
Yeastplume 2018-07-19 10:35:36 +01:00 committed by GitHub
parent a6dc48deae
commit 4a5a41fb30
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 699 additions and 126 deletions

2
Cargo.lock generated
View file

@ -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)",

View file

@ -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

View file

@ -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<WalletInst<HTTPWalletClient, keychain::ExtKeychain>> {
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<HTTPWalletClient, keychain::ExtKeychain> =
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<HTTPWalletClient, keychain::ExtKeychain> =
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 {

View file

@ -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<T: ser::Readable>(&self, from: &[u8]) -> Result<SerIterator<T>, 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<T: ser::Readable>(&self, key: &[u8]) -> Result<Option<T>, Error> {

View file

@ -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" }

150
wallet/src/db_migrate.rs Normal file
View file

@ -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<Vec<OutputData>, 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<WalletDetails, Error> {
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", "");
}

View file

@ -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<OutputData>) -> 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<OutputData>) -> 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<OutputData>) -> 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<OutputData>) -> Re
Ok(())
}
/// Display transaction log in a pretty way
pub fn txs(cur_height: u64, validated: bool, txs: Vec<TxLogEntry>) -> 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!(

View file

@ -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<u32, libwallet::Error> {
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<Iterator<Item = OutputData>> {
unimplemented!()
}
fn tx_log_iter(&self) -> Box<Iterator<Item = TxLogEntry>> {
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<C, K> {
passphrase: String,
/// List of outputs
pub outputs: HashMap<String, OutputData>,
/// Tx log
pub tx_log: Vec<TxLogEntry>,
/// 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<Option<TxLogEntry>, libwallet::Error> {
Ok(None)
}
fn tx_log_iter<'a>(&'a self) -> Box<Iterator<Item = TxLogEntry> + 'a> {
Box::new(self.tx_log.iter().cloned())
}
fn get(&self, id: &Identifier) -> Result<OutputData, libwallet::Error> {
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<WalletDetails, libwallet::Error> {
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),

View file

@ -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};

View file

@ -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<TxLogEntry>), 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))
}

View file

@ -213,6 +213,7 @@ where
height: output.height,
lock_height: output.lock_height,
is_coinbase: output.is_coinbase,
tx_log_entry: None,
});
} else {
warn!(

View file

@ -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,

View file

@ -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<T: ?Sized, C, K>(wallet: &mut T) -> Result<Vec<TxLogEntry>, Error>
where
T: WalletBackend<C, K>,
C: WalletClient,
K: Keychain,
{
// just read the wallet here, no need for a write lock
let mut txs = wallet.tx_log_iter().collect::<Vec<_>>();
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<T: ?Sized, C, K>(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()?;
}

View file

@ -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<OutputData, Error>;
/// Get an (Optional) tx log entry by uuid
fn get_tx_log_entry(&self, uuid: &Uuid) -> Result<Option<TxLogEntry>, Error>;
/// Iterate over all output data stored by the backend
fn tx_log_iter<'a>(&'a self) -> Box<Iterator<Item = TxLogEntry> + 'a>;
/// Create a new write batch to update or remove output data
fn batch<'a>(&'a mut self) -> Result<Box<WalletOutputBatch + 'a>, 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<OutputData, Error>;
/// Iterate over all output data stored by the backend
fn iter(&self) -> Box<Iterator<Item = OutputData>>;
/// 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<u32, Error>;
/// Iterate over all output data stored by the backend
fn tx_log_iter(&self) -> Box<Iterator<Item = TxLogEntry>>;
/// 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<u32>,
}
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<Uuid>,
/// Transaction type (as above)
pub tx_type: TxLogEntryType,
/// Time this tx entry was created
/// #[serde(with = "tx_date_format")]
pub creation_ts: DateTime<Utc>,
/// Time this tx was confirmed (by this wallet)
/// #[serde(default, with = "opt_tx_date_format")]
pub confirmation_ts: Option<DateTime<Utc>>,
/// 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<W: ser::Writer>(&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<TxLogEntry, ser::Error> {
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 {

View file

@ -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<store::Error> 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<Option<TxLogEntry>, 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<Iterator<Item = TxLogEntry> + 'a> {
Box::new(self.db.iter(&[TX_LOG_ENTRY_PREFIX]).unwrap())
}
fn batch<'a>(&'a mut self) -> Result<Box<WalletOutputBatch + 'a>, Error> {
Ok(Box::new(Batch {
_store: self,
@ -194,12 +206,49 @@ where
).map_err(|e| e.into())
}
fn iter(&self) -> Box<Iterator<Item = OutputData>> {
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<u32, Error> {
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<Iterator<Item = TxLogEntry>> {
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())

View file

@ -77,7 +77,7 @@ impl WalletSeed {
WalletSeed(seed)
}
fn from_hex(hex: &str) -> Result<WalletSeed, Error> {
pub fn from_hex(hex: &str) -> Result<WalletSeed, Error> {
let bytes = util::from_hex(hex.to_string())
.context(ErrorKind::GenericError("Invalid hex".to_owned()))?;
Ok(WalletSeed::from_bytes(&bytes))

View file

@ -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<LocalWalletClient, ExtKeychain> = 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);
}
}