Store additional wallet detail and WalletInfo cleanup (#1167)

* adding wallet detail file, clean up wallet info output

* rustfmt

* ensure change outputs aren't written early

* rustfmt

* travis problems AGAIN

* file wallet explicit types
This commit is contained in:
Yeastplume 2018-06-14 17:02:05 +01:00 committed by GitHub
parent a30ee88236
commit 9e0b3b6862
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 177 additions and 106 deletions

View file

@ -27,15 +27,16 @@ extern crate grin_wallet as wallet;
mod framework;
use std::{thread, time};
use std::sync::{Arc, Mutex};
use framework::{LocalServerContainer, LocalServerContainerConfig};
use std::sync::{Arc, Mutex};
use std::{thread, time};
use util::LOGGER;
/// Start 1 node mining, 1 non mining node and two wallets.
/// Then send a transaction from one wallet to another and propagate it a stem transaction
/// but without stem relay and check if the transaction is still broadcasted.
/// Then send a transaction from one wallet to another and propagate it a stem
/// transaction but without stem relay and check if the transaction is still
/// broadcasted.
#[test]
#[ignore]
fn test_dandelion_timeout() {
@ -155,9 +156,7 @@ fn test_dandelion_timeout() {
// The transaction should be waiting in the node stempool thus cannot be mined.
println!("Recipient wallet info: {:?}", recipient_info);
assert!(
recipient_info.data_confirmed && recipient_info.amount_awaiting_confirmation == 50000000000
);
assert!(recipient_info.amount_awaiting_confirmation == 50000000000);
// Wait for stem timeout
thread::sleep(time::Duration::from_millis(35000));

View file

@ -303,7 +303,8 @@ impl LocalServerContainer {
let mut wallet = FileWallet::new(config.clone(), "")
.unwrap_or_else(|e| panic!("Error creating wallet: {:?} Config: {:?}", e, config));
wallet.keychain = Some(keychain);
wallet::libwallet::internal::updater::retrieve_info(&mut wallet, true).unwrap()
let _ = wallet::libwallet::internal::updater::refresh_outputs(&mut wallet);
wallet::libwallet::internal::updater::retrieve_info(&mut wallet).unwrap()
}
pub fn send_amount_to(

View file

@ -114,7 +114,7 @@ fn basic_wallet_transactions() {
1,
"not_all",
"http://127.0.0.1:20002",
true,
false,
);
//Wait for a confirmation
@ -125,9 +125,7 @@ fn basic_wallet_transactions() {
let recipient_info = LocalServerContainer::get_wallet_info(&recp_wallet_config, &recp_seed);
println!("Recipient wallet info: {:?}", recipient_info);
assert!(
recipient_info.data_confirmed && recipient_info.amount_currently_spendable == 50000000000
);
assert!(recipient_info.amount_currently_spendable == 50000000000);
warn!(
LOGGER,
@ -140,7 +138,7 @@ fn basic_wallet_transactions() {
1,
"not_all",
"http://127.0.0.1:20002",
true,
false,
);
}
@ -151,9 +149,7 @@ fn basic_wallet_transactions() {
recipient_info
);
assert!(
recipient_info.data_confirmed && recipient_info.amount_currently_spendable == 60000000000
);
assert!(recipient_info.amount_currently_spendable == 60000000000);
//send some cash right back
LocalServerContainer::send_amount_to(
&recp_wallet_config,
@ -161,7 +157,7 @@ fn basic_wallet_transactions() {
1,
"all",
"http://127.0.0.1:10002",
true,
false,
);
thread::sleep(time::Duration::from_millis(5000));

View file

@ -681,13 +681,14 @@ fn wallet_command(wallet_args: &ArgMatches, global_config: GlobalConfig) {
Ok(())
}
("info", Some(_)) => {
let _res = wallet::display::info(&api.retrieve_summary_info(true)?)
.unwrap_or_else(|e| {
let (validated, wallet_info) =
api.retrieve_summary_info(true).unwrap_or_else(|e| {
panic!(
"Error getting wallet info: {:?} Config: {:?}",
e, wallet_config
)
});
wallet::display::info(&wallet_info, validated);
Ok(())
}
("outputs", Some(_)) => {

View file

@ -74,15 +74,15 @@ pub fn outputs(cur_height: u64, validated: bool, outputs: Vec<OutputData>) -> Re
}
/// Display summary info in a pretty way
pub fn info(wallet_info: &WalletInfo) -> Result<(), Error> {
pub fn info(wallet_info: &WalletInfo, validated: bool) {
println!(
"\n____ Wallet Summary Info at {} ({}) ____\n",
wallet_info.current_height, wallet_info.data_confirmed_from
"\n____ Wallet Summary Info as of {} ____\n",
wallet_info.last_confirmed_height
);
let mut table = table!(
[bFG->"Total", FG->amount_to_hr_string(wallet_info.total)],
[bFY->"Awaiting Confirmation", FY->amount_to_hr_string(wallet_info.amount_awaiting_confirmation)],
[bFY->"Confirmed but Still Locked", FY->amount_to_hr_string(wallet_info.amount_confirmed_but_locked)],
[bFY->"Immature Coinbase", FY->amount_to_hr_string(wallet_info.amount_immature)],
[bFG->"Currently Spendable", FG->amount_to_hr_string(wallet_info.amount_currently_spendable)],
[Fw->"---------", Fw->"---------"],
[Fr->"(Locked by previous transaction)", Fr->amount_to_hr_string(wallet_info.amount_locked)]
@ -90,13 +90,11 @@ pub fn info(wallet_info: &WalletInfo) -> Result<(), Error> {
table.set_format(*prettytable::format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR);
table.printstd();
println!();
if !wallet_info.data_confirmed {
if !validated {
println!(
"\nWARNING: Failed to verify wallet contents with grin server. \
Above info is maybe not fully updated or invalid! \
Check that your `grin server` is OK, or see `wallet help restore`"
"\nWARNING: Wallet failed to verify data against a live chain. \
The above is from local cache and only valid up to the given height! \
(is your `grin server` offline or broken?)"
);
};
Ok(())
}
}

View file

@ -37,8 +37,10 @@ use client;
use libtx::slate::Slate;
use libwallet;
use libwallet::types::{BlockFees, BlockIdentifier, CbData, MerkleProofWrapper, OutputData,
TxWrapper, WalletBackend, WalletClient};
TxWrapper, WalletBackend, WalletClient, WalletDetails};
const DETAIL_FILE: &'static str = "wallet.det";
const DET_BCK_FILE: &'static str = "wallet.detbck";
const DAT_FILE: &'static str = "wallet.dat";
const BCK_FILE: &'static str = "wallet.bck";
const LOCK_FILE: &'static str = "wallet.lock";
@ -176,12 +178,18 @@ pub struct FileWallet<K> {
passphrase: String,
/// List of outputs
pub outputs: HashMap<String, OutputData>,
/// Details
pub details: WalletDetails,
/// Data file path
pub data_file_path: String,
/// Backup file path
pub backup_file_path: String,
/// lock file path
pub lock_file_path: String,
/// details file path
pub details_file_path: String,
/// Details backup file path
pub details_bak_path: String,
}
impl<K> WalletBackend<K> for FileWallet<K>
@ -274,10 +282,10 @@ where
// We successfully acquired the lock - so do what needs to be done.
self.read_or_create_paths()
.context(libwallet::ErrorKind::CallbackImpl("Lock Error"))?;
self.write(&self.backup_file_path)
self.write(&self.backup_file_path, &self.details_bak_path)
.context(libwallet::ErrorKind::CallbackImpl("Write Error"))?;
let res = f(self);
self.write(&self.data_file_path)
self.write(&self.data_file_path, &self.details_file_path)
.context(libwallet::ErrorKind::CallbackImpl("Write Error"))?;
// delete the lock file
@ -318,14 +326,20 @@ where
}
/// Next child index when we want to create a new output.
fn next_child(&self, root_key_id: keychain::Identifier) -> u32 {
fn next_child(&mut self, root_key_id: keychain::Identifier) -> u32 {
let mut max_n = 0;
for out in self.outputs.values() {
if max_n < out.n_child && out.root_key_id == root_key_id {
max_n = out.n_child;
}
}
max_n + 1
if self.details.last_child_index <= max_n {
self.details.last_child_index = max_n + 1;
} else {
self.details.last_child_index += 1;
}
self.details.last_child_index
}
/// Select spendable coins from the wallet.
@ -394,6 +408,11 @@ where
eligible.iter().take(max_outputs).cloned().collect()
}
/// Return current metadata
fn details(&mut self) -> &mut WalletDetails {
&mut self.details
}
/// Restore wallet contents
fn restore(&mut self) -> Result<(), libwallet::Error> {
libwallet::internal::restore::restore(self).context(libwallet::ErrorKind::Restore)?;
@ -480,8 +499,8 @@ impl<K> WalletClient for FileWallet<K> {
/// retrieve merkle proof for a commit from a node
fn get_merkle_proof_for_commit(
&self,
addr: &str,
commit: &str,
_addr: &str,
_commit: &str,
) -> Result<MerkleProofWrapper, libwallet::Error> {
Err(libwallet::ErrorKind::GenericError("Not Implemented"))?
}
@ -498,9 +517,12 @@ where
config: config.clone(),
passphrase: String::from(passphrase),
outputs: HashMap::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),
lock_file_path: format!("{}{}{}", config.data_file_dir, MAIN_SEPARATOR, LOCK_FILE),
details_file_path: format!("{}{}{}", config.data_file_dir, MAIN_SEPARATOR, DETAIL_FILE),
details_bak_path: format!("{}{}{}", config.data_file_dir, MAIN_SEPARATOR, DET_BCK_FILE),
};
match retval.read_or_create_paths() {
Ok(_) => Ok(retval),
@ -519,9 +541,21 @@ where
if Path::new(&self.data_file_path.clone()).exists() {
self.read()?;
}
if Path::new(&self.details_file_path.clone()).exists() {
self.details = self.read_details()?;
}
Ok(())
}
/// Read details file from disk
fn read_details(&self) -> Result<WalletDetails, Error> {
let details_file = File::open(self.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())
}
/// Read output_data vec from disk.
fn read_outputs(&self) -> Result<Vec<OutputData>, Error> {
let data_file = File::open(self.data_file_path.clone())
@ -541,8 +575,8 @@ where
Ok(())
}
/// Write the wallet data to disk.
fn write(&self, data_file_path: &str) -> Result<(), Error> {
/// Write the wallet and details data to disk.
fn write(&self, data_file_path: &str, details_file_path: &str) -> Result<(), Error> {
let mut data_file =
File::create(data_file_path).context(ErrorKind::FileWallet(&"Could not create "))?;
let mut outputs = self.outputs.values().collect::<Vec<_>>();
@ -551,7 +585,16 @@ where
.context(ErrorKind::FileWallet("Error serializing wallet data"))?;
data_file
.write_all(res_json.as_slice())
.context(ErrorKind::FileWallet(&"Error writing wallet file"))
.context(ErrorKind::FileWallet(&"Error writing wallet file"))?;
// write details file
let mut details_file =
File::create(details_file_path).context(ErrorKind::FileWallet(&"Could not create "))?;
let res_json = serde_json::to_string_pretty(&self.details).context(ErrorKind::FileWallet(
"Error serializing wallet details file",
))?;
details_file
.write_all(res_json.into_bytes().as_slice())
.context(ErrorKind::FileWallet(&"Error writing wallet details file"))
.map_err(|e| e.into())
}

View file

@ -72,12 +72,16 @@ where
}
/// Retrieve summary info for wallet
pub fn retrieve_summary_info(&mut self, refresh_from_node: bool) -> Result<WalletInfo, Error> {
pub fn retrieve_summary_info(
&mut self,
refresh_from_node: bool,
) -> Result<(bool, WalletInfo), Error> {
let mut validated = false;
if refresh_from_node {
validated = self.update_outputs();
}
updater::retrieve_info(self.wallet, validated)
let wallet_info = updater::retrieve_info(self.wallet)?;
Ok((validated, wallet_info))
}
/// Issues a send transaction and sends to recipient

View file

@ -164,7 +164,7 @@ where
&self,
req: &mut Request,
api: &mut APIOwner<T, K>,
) -> Result<WalletInfo, Error> {
) -> Result<(bool, WalletInfo), Error> {
let mut update_from_node = false;
if let Ok(params) = req.get_ref::<UrlEncodedQuery>() {
if let Some(_) = params.get("refresh") {

View file

@ -46,7 +46,7 @@ where
T: WalletBackend<K>,
K: Keychain,
{
let (elems, inputs, change_id, amount, fee) = select_send_tx(
let (elems, inputs, change, change_derivation, amount, fee) = select_send_tx(
wallet,
amount,
current_height,
@ -78,8 +78,9 @@ where
}
// Store change output
if change_id.is_some() {
context.add_output(&change_id.unwrap());
if change_derivation.is_some() {
let change_id = keychain.derive_key_id(change_derivation.unwrap()).unwrap();
context.add_output(&change_id);
}
let lock_inputs = context.get_inputs().clone();
@ -89,10 +90,28 @@ where
// so we avoid accidental double spend attempt.
let update_sender_wallet_fn = move |wallet: &mut T| {
wallet.with_wallet(|wallet_data| {
// Lock the inputs we've selected
for id in lock_inputs {
let coin = wallet_data.get_output(&id).unwrap().clone();
wallet_data.lock_output(&coin);
}
if let Some(d) = change_derivation {
// Add our change output to the wallet
let root_key_id = keychain.root_key_id();
let change_id = keychain.derive_key_id(change_derivation.unwrap()).unwrap();
wallet_data.add_output(OutputData {
root_key_id: root_key_id.clone(),
key_id: change_id,
n_child: d,
value: change as u64,
status: OutputStatus::Unconfirmed,
height: current_height,
lock_height: 0,
is_coinbase: false,
block: None,
merkle_proof: None,
});
}
})
};
@ -176,7 +195,8 @@ pub fn select_send_tx<T, K>(
(
Vec<Box<build::Append<K>>>,
Vec<OutputData>,
Option<Identifier>,
u64, //change
Option<u32>, //change derivation
u64, // amount
u64, // fee
),
@ -264,13 +284,14 @@ where
}
// build transaction skeleton with inputs and change
let (mut parts, change_key) = inputs_and_change(&coins, wallet, current_height, amount, fee)?;
let (mut parts, change, change_derivation) =
inputs_and_change(&coins, wallet, current_height, amount, fee)?;
// This is more proof of concept than anything but here we set lock_height
// on tx being sent (based on current chain height via api).
parts.push(build::with_lock_height(lock_height));
Ok((parts, coins, change_key, amount, fee))
Ok((parts, coins, change, change_derivation, amount, fee))
}
/// coins proof count
@ -285,7 +306,7 @@ pub fn inputs_and_change<T, K>(
height: u64,
amount: u64,
fee: u64,
) -> Result<(Vec<Box<build::Append<K>>>, Option<Identifier>), Error>
) -> Result<(Vec<Box<build::Append<K>>>, u64, Option<u32>), Error>
where
T: WalletBackend<K>,
K: Keychain,
@ -321,34 +342,20 @@ where
}
}
let change_key;
let mut change_derivation = None;
if change != 0 {
// track the output representing our change
change_key = wallet.with_wallet(|wallet_data| {
let keychain = wallet_data.keychain().clone();
let root_key_id = keychain.root_key_id();
let change_derivation = wallet_data.next_child(root_key_id.clone());
let change_key = keychain.derive_key_id(change_derivation).unwrap();
wallet_data.add_output(OutputData {
root_key_id: root_key_id.clone(),
key_id: change_key.clone(),
n_child: change_derivation,
value: change as u64,
status: OutputStatus::Unconfirmed,
height: height,
lock_height: 0,
is_coinbase: false,
block: None,
merkle_proof: None,
});
let cd = wallet_data.next_child(root_key_id.clone());
let change_key = keychain.derive_key_id(cd).unwrap();
change_derivation = Some(cd);
Some(change_key)
})?;
parts.push(build::output(change, change_key.clone().unwrap()));
} else {
change_key = None
}
Ok((parts, change_key))
Ok((parts, change, change_derivation))
}

View file

@ -165,7 +165,10 @@ where
debug!(LOGGER, "selected some coins - {}", coins.len());
let fee = tx_fee(coins.len(), 2, selection::coins_proof_count(&coins), None);
let (mut parts, _) = selection::inputs_and_change(&coins, wallet, current_height, amount, fee)?;
let (mut parts, _, _) =
selection::inputs_and_change(&coins, wallet, current_height, amount, fee)?;
//TODO: If we end up using this, create change output here
// add burn output and fees
parts.push(build::output(amount - fee, Identifier::zero()));

View file

@ -24,6 +24,7 @@ use core::core::{Output, TxKernel};
use core::{global, ser};
use keychain::{Identifier, Keychain};
use libtx::reward;
use libwallet;
use libwallet::error::{Error, ErrorKind};
use libwallet::internal::keys;
use libwallet::types::{BlockFees, CbData, OutputData, OutputStatus, WalletBackend, WalletClient,
@ -181,7 +182,8 @@ pub fn apply_api_outputs<T, K>(
wallet: &mut T,
wallet_outputs: &HashMap<pedersen::Commitment, Identifier>,
api_outputs: &HashMap<pedersen::Commitment, String>,
) -> Result<(), Error>
height: u64,
) -> Result<(), libwallet::Error>
where
T: WalletBackend<K>,
K: Keychain,
@ -199,7 +201,10 @@ where
};
}
}
})
let details = wallet_data.details();
details.last_confirmed_height = height;
})?;
Ok(())
}
/// Builds a single api query to retrieve the latest output data from the node.
@ -218,7 +223,7 @@ where
let wallet_output_keys = wallet_outputs.keys().map(|commit| commit.clone()).collect();
let api_outputs = wallet.get_outputs_from_node(wallet.node_url(), wallet_output_keys)?;
apply_api_outputs(wallet, &wallet_outputs, &api_outputs)?;
apply_api_outputs(wallet, &wallet_outputs, &api_outputs, height)?;
clean_old_unconfirmed(wallet, height)?;
Ok(())
}
@ -241,23 +246,15 @@ where
/// Retrieve summary info about the wallet
/// caller should refresh first if desired
pub fn retrieve_info<T, K>(wallet: &mut T, refreshed: bool) -> Result<WalletInfo, Error>
pub fn retrieve_info<T, K>(wallet: &mut T) -> Result<WalletInfo, Error>
where
T: WalletBackend<K> + WalletClient,
K: Keychain,
{
let height_res = wallet.get_chain_height(&wallet.node_url());
let ret_val = wallet.read_wallet(|wallet_data| {
let (current_height, from) = match height_res {
Ok(height) => (height, "from server node"),
Err(_) => match wallet_data.outputs().values().map(|out| out.height).max() {
Some(height) => (height, "from wallet"),
None => (0, "node/wallet unavailable"),
},
};
let current_height = wallet_data.details().last_confirmed_height;
let mut unspent_total = 0;
let mut unspent_but_locked_total = 0;
let mut immature_total = 0;
let mut unconfirmed_total = 0;
let mut locked_total = 0;
for out in wallet_data
@ -266,11 +263,11 @@ where
.values()
.filter(|out| out.root_key_id == wallet_data.keychain().root_key_id())
{
if out.status == OutputStatus::Unspent {
if out.status == OutputStatus::Unspent && out.lock_height <= current_height {
unspent_total += out.value;
if out.lock_height > current_height {
unspent_but_locked_total += out.value;
}
if out.status == OutputStatus::Unspent && out.lock_height > current_height {
immature_total += out.value;
}
if out.status == OutputStatus::Unconfirmed && !out.is_coinbase {
unconfirmed_total += out.value;
@ -281,14 +278,12 @@ where
}
Ok(WalletInfo {
current_height: current_height,
total: unspent_total + unconfirmed_total,
last_confirmed_height: wallet_data.details().last_confirmed_height,
total: unspent_total + unconfirmed_total + immature_total,
amount_awaiting_confirmation: unconfirmed_total,
amount_confirmed_but_locked: unspent_but_locked_total,
amount_currently_spendable: unspent_total - unspent_but_locked_total,
amount_immature: immature_total,
amount_locked: locked_total,
data_confirmed: refreshed,
data_confirmed_from: String::from(from),
amount_currently_spendable: unspent_total,
})
});
ret_val

View file

@ -76,7 +76,11 @@ where
fn get_output(&self, key_id: &Identifier) -> Option<&OutputData>;
/// Next child ID when we want to create a new output
fn next_child(&self, root_key_id: Identifier) -> u32;
/// Should also increment index
fn next_child(&mut self, root_key_id: Identifier) -> u32;
/// Return current details
fn details(&mut self) -> &mut WalletDetails;
/// Select spendable coins from the wallet
fn select_coins(
@ -399,21 +403,40 @@ pub struct CbData {
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct WalletInfo {
/// height from which info was taken
pub current_height: u64,
pub last_confirmed_height: u64,
/// total amount in the wallet
pub total: u64,
/// amount awaiting confirmation
pub amount_awaiting_confirmation: u64,
/// confirmed but locked
pub amount_confirmed_but_locked: u64,
/// coinbases waiting for lock height
pub amount_immature: u64,
/// amount currently spendable
pub amount_currently_spendable: u64,
/// amount locked by previous transactions
/// amount locked via previous transactions
pub amount_locked: u64,
/// whether the data was confirmed against a live node
pub data_confirmed: bool,
/// node confirming the data
pub data_confirmed_from: String,
}
/// Separate data for a wallet, containing fields
/// that are needed but not necessarily represented
/// via simple rows of OutputData
/// If a wallet is restored from seed this is obvious
/// lost and re-populated as well as possible
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct WalletDetails {
/// The last block height at which the wallet data
/// was confirmed against a node
pub last_confirmed_height: u64,
/// The last child index used
pub last_child_index: u32,
}
impl Default for WalletDetails {
fn default() -> WalletDetails {
WalletDetails {
last_confirmed_height: 0,
last_child_index: 0,
}
}
}
/// Dummy wrapper for the hex-encoded serialized transaction.

View file

@ -61,7 +61,8 @@ where
None => {}
}
}
updater::apply_api_outputs(wallet, &wallet_outputs, &api_outputs)?;
let height = chain.head().unwrap().height;
updater::apply_api_outputs(wallet, &wallet_outputs, &api_outputs, height)?;
Ok(())
}