mirror of
https://github.com/mimblewimble/grin.git
synced 2025-02-01 08:51:08 +03:00
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:
parent
a6dc48deae
commit
4a5a41fb30
17 changed files with 699 additions and 126 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -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)",
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
128
src/bin/grin.rs
128
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<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 {
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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
150
wallet/src/db_migrate.rs
Normal 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", "");
|
||||
}
|
|
@ -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!(
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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};
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -213,6 +213,7 @@ where
|
|||
height: output.height,
|
||||
lock_height: output.lock_height,
|
||||
is_coinbase: output.is_coinbase,
|
||||
tx_log_entry: None,
|
||||
});
|
||||
} else {
|
||||
warn!(
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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()?;
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue