mirror of
https://github.com/mimblewimble/grin.git
synced 2025-01-20 19:11:08 +03:00
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:
parent
a5b2c7d3f2
commit
670aa11e5a
11 changed files with 193 additions and 90 deletions
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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]);
|
||||
|
|
112
src/bin/grin.rs
112
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"),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
44
wallet/src/info.rs
Normal 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
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
|
@ -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};
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue