From 670aa11e5a44b90d38fc31c90074c253e157ce1e Mon Sep 17 00:00:00 2001 From: AntiochP <30642645+antiochp@users.noreply.github.com> Date: Sun, 24 Sep 2017 00:40:31 -0400 Subject: [PATCH] expose "wallet info" on CLI (#132) * expose "wallet info" on CLI * add sleep and retry logic when obtaining wallet.lock * fix pool test for immature coinbase --- api/src/endpoints.rs | 5 +- api/src/types.rs | 6 +-- core/src/consensus.rs | 2 +- pool/src/pool.rs | 4 +- src/bin/grin.rs | 112 ++++++++++++++++++++++------------------- wallet/src/checker.rs | 11 ++-- wallet/src/info.rs | 44 ++++++++++++++++ wallet/src/lib.rs | 2 + wallet/src/receiver.rs | 11 ++-- wallet/src/sender.rs | 4 +- wallet/src/types.rs | 82 +++++++++++++++++++++--------- 11 files changed, 193 insertions(+), 90 deletions(-) create mode 100644 wallet/src/info.rs diff --git a/api/src/endpoints.rs b/api/src/endpoints.rs index 684cee7b5..5d89e4011 100644 --- a/api/src/endpoints.rs +++ b/api/src/endpoints.rs @@ -132,7 +132,10 @@ impl ApiEndpoint for PoolApi .write() .unwrap() .add_to_memory_pool(source, tx) - .map_err(|e| Error::Internal(format!("Addition to transaction pool failed: {:?}", e)))?; + .map_err(|e| { + Error::Internal(format!("Addition to transaction pool failed: {:?}", e)) + })?; + Ok(()) } } diff --git a/api/src/types.rs b/api/src/types.rs index 8a1dbb010..240d5c2f8 100644 --- a/api/src/types.rs +++ b/api/src/types.rs @@ -58,9 +58,9 @@ pub struct Output { impl Output { pub fn from_output(output: &core::Output, block_header: &core::BlockHeader) -> Output { - let (output_type, maturity) = match output.features { + let (output_type, lock_height) = match output.features { x if x.contains(core::transaction::COINBASE_OUTPUT) => { - (OutputType::Coinbase, consensus::COINBASE_MATURITY) + (OutputType::Coinbase, block_header.height + consensus::COINBASE_MATURITY) }, _ => (OutputType::Transaction, 0), }; @@ -69,7 +69,7 @@ impl Output { commit: output.commit, proof: output.proof, height: block_header.height, - lock_height: block_header.height + maturity, + lock_height: lock_height, } } } diff --git a/core/src/consensus.rs b/core/src/consensus.rs index cac9996f1..b0772d405 100644 --- a/core/src/consensus.rs +++ b/core/src/consensus.rs @@ -29,7 +29,7 @@ pub const REWARD: u64 = 1_000_000_000; /// Number of blocks before a coinbase matures and can be spent /// TODO - reduced this for testing - need to investigate if we can lower this in test env // pub const COINBASE_MATURITY: u64 = 1_000; -pub const COINBASE_MATURITY: u64 = 10; +pub const COINBASE_MATURITY: u64 = 3; /// Block interval, in seconds, the network will tune its next_target for. Note /// that we may reduce this value in the future as we get more data on mining diff --git a/pool/src/pool.rs b/pool/src/pool.rs index 254d74770..6c73d22eb 100644 --- a/pool/src/pool.rs +++ b/pool/src/pool.rs @@ -671,7 +671,7 @@ mod tests { _ => panic!("expected ImmatureCoinbase error here"), }; - let head_header = block::BlockHeader {height: 11, ..block::BlockHeader::default()}; + let head_header = block::BlockHeader {height: 4, ..block::BlockHeader::default()}; chain_ref.store_head_header(&head_header); let txn = test_transaction(vec![15], vec![10, 4]); @@ -683,7 +683,7 @@ mod tests { _ => panic!("expected ImmatureCoinbase error here"), }; - let head_header = block::BlockHeader {height: 12, ..block::BlockHeader::default()}; + let head_header = block::BlockHeader {height: 5, ..block::BlockHeader::default()}; chain_ref.store_head_header(&head_header); let txn = test_transaction(vec![15], vec![10, 4]); diff --git a/src/bin/grin.rs b/src/bin/grin.rs index 28d4b856a..4ddae6c40 100644 --- a/src/bin/grin.rs +++ b/src/bin/grin.rs @@ -107,9 +107,9 @@ fn main() { } let args = App::new("Grin") - .version("0.1") - .author("The Grin Team") - .about("Lightweight implementation of the MimbleWimble protocol.") + .version("0.1") + .author("The Grin Team") + .about("Lightweight implementation of the MimbleWimble protocol.") // specification of all the server commands and options .subcommand(SubCommand::with_name("server") @@ -152,54 +152,59 @@ fn main() { .subcommand(SubCommand::with_name("status") .about("current status of the Grin chain"))) - // specification of the wallet commands and options - .subcommand(SubCommand::with_name("wallet") - .about("Wallet software for Grin") - .arg(Arg::with_name("pass") - .short("p") - .long("pass") - .help("Wallet passphrase used to generate the private key seed") - .takes_value(true)) - .arg(Arg::with_name("data_dir") - .short("dd") - .long("data_dir") - .help("Directory in which to store wallet files (defaults to current \ - directory)") - .takes_value(true)) - .arg(Arg::with_name("port") - .short("r") - .long("port") - .help("Port on which to run the wallet receiver when in receiver mode") - .takes_value(true)) - .arg(Arg::with_name("api_server_address") - .short("a") - .long("api_server_address") - .help("The api address of a running node on which to check inputs and \ - post transactions") - .takes_value(true)) - .subcommand(SubCommand::with_name("receive") - .about("Run the wallet in receiving mode. If an input file is \ - provided, will process it, otherwise runs in server mode waiting \ - for send requests.") - .arg(Arg::with_name("input") - .help("Partial transaction to receive, expects as a JSON file.") - .short("i") - .long("input") - .takes_value(true))) - .subcommand(SubCommand::with_name("send") - .about("Builds a transaction to send someone some coins. By default, \ - the transaction will just be printed to stdout. If a destination is \ - provided, the command will attempt to contact the receiver at that \ - address and send the transaction directly.") - .arg(Arg::with_name("amount") - .help("Amount to send in the smallest denomination") - .index(1)) - .arg(Arg::with_name("dest") - .help("Send the transaction to the provided server") - .short("d") - .long("dest") - .takes_value(true)))) - .get_matches(); + // specification of the wallet commands and options + .subcommand(SubCommand::with_name("wallet") + .about("Wallet software for Grin") + .arg(Arg::with_name("pass") + .short("p") + .long("pass") + .help("Wallet passphrase used to generate the private key seed") + .takes_value(true)) + .arg(Arg::with_name("data_dir") + .short("dd") + .long("data_dir") + .help("Directory in which to store wallet files (defaults to current \ + directory)") + .takes_value(true)) + .arg(Arg::with_name("port") + .short("r") + .long("port") + .help("Port on which to run the wallet receiver when in receiver mode") + .takes_value(true)) + .arg(Arg::with_name("api_server_address") + .short("a") + .long("api_server_address") + .help("Api address of running node on which to check inputs and post transactions") + .takes_value(true)) + + .subcommand(SubCommand::with_name("receive") + .about("Run the wallet in receiving mode. If an input file is \ + provided, will process it, otherwise runs in server mode waiting \ + for send requests.") + .arg(Arg::with_name("input") + .help("Partial transaction to receive, expects as a JSON file.") + .short("i") + .long("input") + .takes_value(true))) + + .subcommand(SubCommand::with_name("send") + .about("Builds a transaction to send someone some coins. By default, \ + the transaction will just be printed to stdout. If a destination is \ + provided, the command will attempt to contact the receiver at that \ + address and send the transaction directly.") + .arg(Arg::with_name("amount") + .help("Amount to send in the smallest denomination") + .index(1)) + .arg(Arg::with_name("dest") + .help("Send the transaction to the provided server") + .short("d") + .long("dest") + .takes_value(true))) + + .subcommand(SubCommand::with_name("info") + .about("basic wallet info (outputs)"))) + + .get_matches(); match args.subcommand() { // server commands and options @@ -369,7 +374,10 @@ fn wallet_command(wallet_args: &ArgMatches) { dest = d; } wallet::issue_send_tx(&wallet_config, &key, amount, dest.to_string()).unwrap(); - } + }, + ("info", Some(_)) => { + wallet::show_info(&wallet_config, &key); + }, _ => panic!("Unknown wallet command, use 'grin help wallet' for details"), } } diff --git a/wallet/src/checker.rs b/wallet/src/checker.rs index f97c419fe..7740dd63c 100644 --- a/wallet/src/checker.rs +++ b/wallet/src/checker.rs @@ -31,12 +31,17 @@ fn refresh_output( out.height = api_out.height; out.lock_height = api_out.lock_height; - if api_out.lock_height > tip.height { + if out.status == OutputStatus::Locked { + // leave it Locked locally for now + } else if api_out.lock_height >= tip.height { out.status = OutputStatus::Immature; } else { out.status = OutputStatus::Unspent; } - } else if out.status == OutputStatus::Unspent { + } else if vec![ + OutputStatus::Unspent, + OutputStatus::Locked + ].contains(&out.status) { out.status = OutputStatus::Spent; } } @@ -70,7 +75,7 @@ pub fn refresh_outputs(config: &WalletConfig, ext_key: &ExtendedKey) -> Result<( } fn get_tip(config: &WalletConfig) -> Result { - let url = format!("{}/v1/chain", config.check_node_api_http_addr); + let url = format!("{}/v1/chain/1", config.check_node_api_http_addr); api::client::get::(url.as_str()) .map_err(|e| Error::Node(e)) } diff --git a/wallet/src/info.rs b/wallet/src/info.rs new file mode 100644 index 000000000..80364c271 --- /dev/null +++ b/wallet/src/info.rs @@ -0,0 +1,44 @@ +// Copyright 2016 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. + +use secp; +use checker; +use extkey::ExtendedKey; +use types::{WalletConfig, WalletData}; + +pub fn show_info(config: &WalletConfig, ext_key: &ExtendedKey) { + let _ = checker::refresh_outputs(&config, ext_key); + let secp = secp::Secp256k1::with_caps(secp::ContextFlag::Commit); + + // operate within a lock on wallet data + let _ = WalletData::with_wallet(&config.data_file_dir, |wallet_data| { + + println!("Outputs - "); + println!("fingerprint, n_child, height, lock_height, status, value"); + println!("----------------------------------"); + for out in &mut wallet_data.outputs { + let key = ext_key.derive(&secp, out.n_child).unwrap(); + + println!( + "{}, {}, {}, {}, {:?}, {}", + key.identifier().fingerprint(), + out.n_child, + out.height, + out.lock_height, + out.status, + out.value + ); + } + }); +} diff --git a/wallet/src/lib.rs b/wallet/src/lib.rs index eed63d086..b405cc2fa 100644 --- a/wallet/src/lib.rs +++ b/wallet/src/lib.rs @@ -31,11 +31,13 @@ extern crate secp256k1zkp as secp; mod checker; mod extkey; +mod info; mod receiver; mod sender; mod types; pub use extkey::ExtendedKey; +pub use info::show_info; pub use receiver::{WalletReceiver, receive_json_tx}; pub use sender::issue_send_tx; pub use types::{WalletConfig, WalletReceiveRequest, CbAmount, CbData}; diff --git a/wallet/src/receiver.rs b/wallet/src/receiver.rs index c57348a67..2308e2fa4 100644 --- a/wallet/src/receiver.rs +++ b/wallet/src/receiver.rs @@ -69,14 +69,19 @@ struct TxWrapper { /// Receive an already well formed JSON transaction issuance and finalize the /// transaction, adding our receiving output, to broadcast to the rest of the /// network. -pub fn receive_json_tx(config: &WalletConfig, ext_key: &ExtendedKey, partial_tx_str: &str) -> Result<(), Error> { - +pub fn receive_json_tx( + config: &WalletConfig, + ext_key: &ExtendedKey, + partial_tx_str: &str +) -> Result<(), Error> { let (amount, blinding, partial_tx) = partial_tx_from_json(partial_tx_str)?; let final_tx = receive_transaction(&config, ext_key, amount, blinding, partial_tx)?; let tx_hex = util::to_hex(ser::ser_vec(&final_tx).unwrap()); let url = format!("{}/v1/pool/push", config.check_node_api_http_addr.as_str()); - let _: TxWrapper = api::client::post(url.as_str(), &TxWrapper { tx_hex: tx_hex })?; + let _: () = api::client::post(url.as_str(), &TxWrapper { tx_hex: tx_hex }).map_err(|e| { + Error::Node(e) + })?; Ok(()) } diff --git a/wallet/src/sender.rs b/wallet/src/sender.rs index 473378178..da9ad6b17 100644 --- a/wallet/src/sender.rs +++ b/wallet/src/sender.rs @@ -85,8 +85,8 @@ fn build_send_tx(config: &WalletConfig, ext_key: &ExtendedKey, amount: u64) -> R height: 0, lock_height: 0, }); - for mut coin in coins { - coin.lock(); + for coin in coins { + wallet_data.lock_output(&coin); } build::transaction(parts).map_err(&From::from) diff --git a/wallet/src/types.rs b/wallet/src/types.rs index ba2430bfd..cee81a5ae 100644 --- a/wallet/src/types.rs +++ b/wallet/src/types.rs @@ -12,13 +12,14 @@ // See the License for the specific language governing permissions and // limitations under the License. +use std::{num, thread, time}; use std::convert::From; use std::fs::{self, File, OpenOptions}; use std::io::Write; -use std::num; use std::path::Path; use std::path::MAIN_SEPARATOR; + use serde_json; use secp; @@ -164,16 +165,36 @@ impl WalletData { //create directory if it doesn't exist fs::create_dir_all(data_file_dir).unwrap_or_else(|why| { info!("! {:?}", why.kind()); - }); + }); let data_file_path = &format!("{}{}{}", data_file_dir, MAIN_SEPARATOR, DAT_FILE); let lock_file_path = &format!("{}{}{}", data_file_dir, MAIN_SEPARATOR, LOCK_FILE); // create the lock files, if it already exists, will produce an error - OpenOptions::new().write(true).create_new(true).open(lock_file_path).map_err(|_| { - Error::WalletData(format!("Could not create wallet lock file. Either \ - some other process is using the wallet or there's a write access issue.")) - })?; + // sleep and retry a few times if we cannot get it the first time + let mut retries = 0; + loop { + let result = OpenOptions::new() + .write(true) + .create_new(true) + .open(lock_file_path) + .map_err(|_| { + Error::WalletData(format!("Could not create wallet lock file. Either \ + some other process is using the wallet or there's a write access issue.")) + }); + match result { + Ok(_) => { break; }, + Err(e) => { + if retries >= 3 { + return Err(e); + } + debug!("failed to obtain wallet.lock, retries - {}, sleeping", retries); + retries += 1; + thread::sleep(time::Duration::from_millis(500)); + } + } + } + // do what needs to be done let mut wdat = WalletData::read_or_create(data_file_path)?; @@ -182,9 +203,10 @@ impl WalletData { // delete the lock file fs::remove_file(lock_file_path).map_err(|_| { - Error::WalletData(format!("Could not remove wallet lock file. Maybe insufficient \ - rights?")) - })?; + Error::WalletData( + format!("Could not remove wallet lock file. Maybe insufficient rights?") + ) + })?; Ok(res) } @@ -201,20 +223,25 @@ impl WalletData { /// Read the wallet data from disk. fn read(data_file_path:&str) -> Result { - let data_file = File::open(data_file_path) - .map_err(|e| Error::WalletData(format!("Could not open {}: {}", data_file_path, e)))?; - serde_json::from_reader(data_file) - .map_err(|e| Error::WalletData(format!("Error reading {}: {}", data_file_path, e))) + let data_file = File::open(data_file_path).map_err(|e| { + Error::WalletData(format!("Could not open {}: {}", data_file_path, e)) + })?; + serde_json::from_reader(data_file).map_err(|e| { + Error::WalletData(format!("Error reading {}: {}", data_file_path, e)) + }) } /// Write the wallet data to disk. fn write(&self, data_file_path:&str) -> Result<(), Error> { - let mut data_file = File::create(data_file_path) - .map_err(|e| Error::WalletData(format!("Could not create {}: {}", data_file_path, e)))?; - let res_json = serde_json::to_vec_pretty(self) - .map_err(|_| Error::WalletData(format!("Error serializing wallet data.")))?; - data_file.write_all(res_json.as_slice()) - .map_err(|e| Error::WalletData(format!("Error writing {}: {}", data_file_path, e))) + let mut data_file = File::create(data_file_path).map_err(|e| { + Error::WalletData(format!("Could not create {}: {}", data_file_path, e)) + })?; + let res_json = serde_json::to_vec_pretty(self).map_err(|_| { + Error::WalletData(format!("Error serializing wallet data.")) + })?; + data_file.write_all(res_json.as_slice()).map_err(|e| { + Error::WalletData(format!("Error writing {}: {}", data_file_path, e)) + }) } /// Append a new output information to the wallet data. @@ -222,6 +249,16 @@ impl WalletData { self.outputs.push(out); } + pub fn lock_output(&mut self, out: &OutputData) { + if let Some(out_to_lock) = self.outputs.iter_mut().find(|out_to_lock| { + out_to_lock.n_child == out.n_child && + out_to_lock.fingerprint == out.fingerprint && + out_to_lock.value == out.value + }) { + out_to_lock.lock(); + } + } + /// Select a subset of unspent outputs to spend in a transaction /// transferring /// the provided amount. @@ -283,10 +320,9 @@ pub fn partial_tx_from_json(json_str: &str) -> Result<(u64, SecretKey, Transacti let blind_bin = util::from_hex(partial_tx.blind_sum)?; let blinding = SecretKey::from_slice(&secp, &blind_bin[..])?; let tx_bin = util::from_hex(partial_tx.tx)?; - let tx = - ser::deserialize(&mut &tx_bin[..]).map_err(|_| { - Error::Format("Could not deserialize transaction, invalid format.".to_string()) - })?; + let tx = ser::deserialize(&mut &tx_bin[..]).map_err(|_| { + Error::Format("Could not deserialize transaction, invalid format.".to_string()) + })?; Ok((partial_tx.amount, blinding, tx)) }