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
This commit is contained in:
AntiochP 2017-09-24 00:40:31 -04:00 committed by Ignotus Peverell
parent a5b2c7d3f2
commit 670aa11e5a
11 changed files with 193 additions and 90 deletions

View file

@ -132,7 +132,10 @@ impl<T> ApiEndpoint for PoolApi<T>
.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(())
}
}

View file

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

View file

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

View file

@ -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]);

View file

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

View file

@ -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<api::Tip, Error> {
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::<api::Tip>(url.as_str())
.map_err(|e| Error::Node(e))
}

44
wallet/src/info.rs Normal file
View file

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

View file

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

View file

@ -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(())
}

View file

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

View file

@ -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<WalletData, Error> {
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))
}