Refactor wallet commands (#2067)

* start wallet command refactoring

* another re-structuring attempt

* rustfmt

* begin splitting up wallet commands

* rustfmt

* clean up wallet arg checking

* rustfmt

* macro for arg parsing

* rustfmt

* factor out init commands

* rustfmt

* move recover to new format

* rustfmt

* add listen command to new format

* rustfmt

* Finish moving commands to new format

* rustfmt

* rustfmt

* propogate errors more cleanly

* rustfmt

* error handling cleanup
This commit is contained in:
Yeastplume 2018-12-06 12:04:02 +00:00 committed by GitHub
parent e8a481e3d4
commit b8c8840cec
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
37 changed files with 1348 additions and 1000 deletions

558
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -30,6 +30,8 @@ serde_json = "1"
log = "0.4"
term = "0.5"
rpassword = "2.0.0"
failure = "0.1"
failure_derive = "0.1"
grin_api = { path = "./api", version = "0.4.2" }
grin_config = { path = "./config", version = "0.4.2" }

View file

@ -91,6 +91,7 @@ impl From<Context<ErrorKind>> for Error {
}
/// TLS config
#[derive(Clone)]
pub struct TLSConfig {
pub certificate: String,
pub private_key: String,

View file

@ -32,10 +32,10 @@ use chain::Chain;
use core::core::verifier_cache::LruVerifierCache;
use core::core::{Block, BlockHeader, Transaction};
use core::global::{self, ChainTypes};
use core::libtx;
use core::pow::{self, Difficulty};
use core::{consensus, genesis};
use keychain::{ExtKeychain, ExtKeychainPath, Keychain};
use core::libtx;
fn clean_output_dir(dir_name: &str) {
let _ = fs::remove_dir_all(dir_name);

View file

@ -32,10 +32,10 @@ use core::core::hash::Hashed;
use core::core::verifier_cache::LruVerifierCache;
use core::core::{Block, BlockHeader, OutputFeatures, OutputIdentifier, Transaction};
use core::global::ChainTypes;
use core::libtx::{self, build};
use core::pow::Difficulty;
use core::{consensus, global, pow};
use keychain::{ExtKeychain, ExtKeychainPath, Keychain};
use core::libtx::{self, build};
fn clean_output_dir(dir_name: &str) {
let _ = fs::remove_dir_all(dir_name);

View file

@ -27,9 +27,9 @@ use chain::{Error, Tip};
use core::core::hash::Hashed;
use core::core::Block;
use core::global::{self, ChainTypes};
use core::libtx;
use core::pow::{self, Difficulty};
use keychain::{ExtKeychain, ExtKeychainPath, Keychain};
use core::libtx;
fn clean_output_dir(dir_name: &str) {
let _ = fs::remove_dir_all(dir_name);

View file

@ -32,10 +32,10 @@ use chain::ErrorKind;
use core::core::transaction;
use core::core::verifier_cache::LruVerifierCache;
use core::global::{self, ChainTypes};
use core::libtx::{self, build};
use core::pow::Difficulty;
use core::{consensus, pow};
use keychain::{ExtKeychain, ExtKeychainPath, Keychain};
use core::libtx::{self, build};
fn clean_output_dir(dir_name: &str) {
let _ = fs::remove_dir_all(dir_name);

View file

@ -4,11 +4,11 @@ extern crate grin_wallet;
use grin_core::core::target::Difficulty;
use grin_core::core::{Block, BlockHeader, CompactBlock, Transaction};
use grin_core::libtx::build::{input, output, transaction, with_fee};
use grin_core::libtx::reward;
use grin_core::ser;
use grin_keychain::keychain::ExtKeychain;
use grin_keychain::Keychain;
use grin_core::libtx::build::{input, output, transaction, with_fee};
use grin_core::libtx::reward;
use std::fs::{self, File};
use std::path::Path;

View file

@ -120,10 +120,11 @@ pub fn genesis_testnet4() -> core::Block {
nonces: vec![
0x46f3b4, 0x1135f8c, 0x1a1596f, 0x1e10f71, 0x41c03ea, 0x63fe8e7, 0x65af34f,
0x73c16d3, 0x8216dc3, 0x9bc75d0, 0xae7d9ad, 0xc1cb12b, 0xc65e957, 0xf67a152,
0xfac6559, 0x100c3d71, 0x11eea08b, 0x1225dfbb, 0x124d61a1, 0x132a14b4, 0x13f4ec38,
0x1542d236, 0x155f2df0, 0x1577394e, 0x163c3513, 0x19349845, 0x19d46953, 0x19f65ed4,
0x1a0411b9, 0x1a2fa039, 0x1a72a06c, 0x1b02ddd2, 0x1b594d59, 0x1b7bffd3, 0x1befe12e,
0x1c82e4cd, 0x1d492478, 0x1de132a5, 0x1e578b3c, 0x1ed96855, 0x1f222896, 0x1fea0da6,
0xfac6559, 0x100c3d71, 0x11eea08b, 0x1225dfbb, 0x124d61a1, 0x132a14b4,
0x13f4ec38, 0x1542d236, 0x155f2df0, 0x1577394e, 0x163c3513, 0x19349845,
0x19d46953, 0x19f65ed4, 0x1a0411b9, 0x1a2fa039, 0x1a72a06c, 0x1b02ddd2,
0x1b594d59, 0x1b7bffd3, 0x1befe12e, 0x1c82e4cd, 0x1d492478, 0x1de132a5,
0x1e578b3c, 0x1ed96855, 0x1f222896, 0x1fea0da6,
],
edge_bits: 29,
},

View file

@ -39,9 +39,9 @@ extern crate serde_derive;
extern crate siphasher;
#[macro_use]
extern crate log;
extern crate uuid;
extern crate chrono;
extern crate failure;
extern crate uuid;
#[macro_use]
extern crate failure_derive;

View file

@ -32,8 +32,8 @@ use grin_core::core::id::ShortIdentifiable;
use grin_core::core::verifier_cache::{LruVerifierCache, VerifierCache};
use grin_core::core::Committed;
use grin_core::core::{Block, BlockHeader, CompactBlock, KernelFeatures, OutputFeatures};
use grin_core::{global, ser};
use grin_core::libtx::build::{self, input, output, with_fee};
use grin_core::{global, ser};
use keychain::{BlindingFactor, ExtKeychain, Keychain};
use util::secp;

View file

@ -20,10 +20,10 @@ extern crate grin_util as util;
use grin_core::core::block::{Block, BlockHeader};
use grin_core::core::Transaction;
use grin_core::pow::Difficulty;
use keychain::{Identifier, Keychain};
use grin_core::libtx::build::{self, input, output, with_fee};
use grin_core::libtx::reward;
use grin_core::pow::Difficulty;
use keychain::{Identifier, Keychain};
// utility producing a transaction with 2 inputs and a single outputs
pub fn tx2i1o() -> Transaction {

View file

@ -24,8 +24,8 @@ pub mod common;
use grin_core::core::verifier_cache::{LruVerifierCache, VerifierCache};
use grin_core::core::{Output, OutputFeatures};
use keychain::{ExtKeychain, Keychain};
use grin_core::libtx::proof;
use keychain::{ExtKeychain, Keychain};
fn verifier_cache() -> Arc<RwLock<VerifierCache>> {
Arc::new(RwLock::new(LruVerifierCache::new()))

View file

@ -33,8 +33,8 @@ use core::core::verifier_cache::LruVerifierCache;
use core::core::{Block, BlockHeader, Transaction};
use core::pow::Difficulty;
use keychain::{ExtKeychain, Keychain};
use core::libtx;
use keychain::{ExtKeychain, Keychain};
use common::*;

View file

@ -33,9 +33,9 @@ use core::core::{Block, BlockHeader};
use common::*;
use core::core::hash::Hashed;
use core::core::verifier_cache::LruVerifierCache;
use core::libtx;
use core::pow::Difficulty;
use keychain::{ExtKeychain, Keychain};
use core::libtx;
#[test]
fn test_transaction_pool_block_reconciliation() {

View file

@ -39,8 +39,8 @@ use chain::store::ChainStore;
use chain::types::Tip;
use pool::*;
use keychain::{ExtKeychain, Keychain};
use core::libtx;
use keychain::{ExtKeychain, Keychain};
use pool::types::*;
use pool::TransactionPool;

View file

@ -31,9 +31,9 @@ use util::RwLock;
use common::*;
use core::core::verifier_cache::LruVerifierCache;
use core::core::{transaction, Block, BlockHeader};
use core::libtx;
use core::pow::Difficulty;
use keychain::{ExtKeychain, Keychain};
use core::libtx;
/// Test we can add some txs to the pool (both stempool and txpool).
#[test]

View file

@ -173,8 +173,7 @@ fn burn_reward(block_fees: BlockFees) -> Result<(core::Output, core::TxKernel, B
let keychain = ExtKeychain::from_random_seed().unwrap();
let key_id = ExtKeychain::derive_key_id(1, 1, 0, 0, 0);
let (out, kernel) =
libtx::reward::output(&keychain, &key_id, block_fees.fees, block_fees.height)
.unwrap();
libtx::reward::output(&keychain, &key_id, block_fees.fees, block_fees.height).unwrap();
Ok((out, kernel, block_fees))
}

View file

@ -16,6 +16,7 @@ mod client;
mod config;
mod server;
mod wallet;
mod wallet_args;
pub use self::client::client_command;
pub use self::config::{config_command_server, config_command_wallet};

View file

@ -13,28 +13,28 @@
// limitations under the License.
use clap::ArgMatches;
use rpassword;
use std::collections::HashMap;
use std::fs::File;
use std::io::Write;
use std::path::{Path, PathBuf};
use std::path::PathBuf;
use std::thread;
use std::time::Duration;
use serde_json as json;
use api::TLSConfig;
use super::wallet_args;
use config::GlobalWalletConfig;
use core::{core, global};
use grin_wallet::libwallet::ErrorKind;
use grin_wallet::{self, controller, display, libwallet};
use grin_wallet::{
instantiate_wallet, FileWalletCommAdapter, HTTPNodeClient, HTTPWalletCommAdapter,
KeybaseWalletCommAdapter, LMDBBackend, NullWalletCommAdapter, WalletConfig, WalletSeed,
};
use keychain;
use core::global;
use grin_wallet::{self, command, WalletConfig, WalletSeed};
use servers::start_webwallet_server;
use util::file::get_first_line;
// define what to do on argument error
macro_rules! arg_parse {
( $r:expr ) => {
match $r {
Ok(res) => res,
Err(e) => {
println!("{}", e);
return 0;
}
}
};
}
pub fn _init_wallet_seed(wallet_config: WalletConfig, password: &str) {
if let Err(_) = WalletSeed::from_file(&wallet_config, password) {
@ -54,33 +54,6 @@ pub fn seed_exists(wallet_config: WalletConfig) -> bool {
}
}
pub fn prompt_password(args: &ArgMatches) -> String {
match args.value_of("pass") {
None => {
println!("Temporary note:");
println!(
"If this is your first time running your wallet since BIP32 (word lists) \
were implemented, your seed will be converted to \
the new format. Please ensure the provided password is correct."
);
println!("If this goes wrong, your old 'wallet.seed' file has been saved as 'wallet.seed.bak' \
Rename this file to back to `wallet.seed` and try again");
rpassword::prompt_password_stdout("Password: ").unwrap()
}
Some(p) => p.to_owned(),
}
}
pub fn prompt_password_confirm() -> String {
let first = rpassword::prompt_password_stdout("Password: ").unwrap();
let second = rpassword::prompt_password_stdout("Confirm Password: ").unwrap();
if first != second {
println!("Passwords do not match");
std::process::exit(0);
}
first
}
pub fn wallet_command(wallet_args: &ArgMatches, config: GlobalWalletConfig) -> i32 {
// just get defaults from the global config
let mut wallet_config = config.members.unwrap().wallet;
@ -101,669 +74,95 @@ pub fn wallet_command(wallet_args: &ArgMatches, config: GlobalWalletConfig) -> i
wallet_config.check_node_api_http_addr = sa.to_string().clone();
}
let mut show_spent = false;
if wallet_args.is_present("show_spent") {
show_spent = true;
}
let node_api_secret = get_first_line(wallet_config.node_api_secret_path.clone());
let global_wallet_args =
arg_parse!(wallet_args::parse_global_args(&wallet_config, &wallet_args));
// Decrypt the seed from the seed file and derive the keychain.
// Generate the initial wallet seed if we are running "wallet init".
if let ("init", Some(r)) = wallet_args.subcommand() {
if let Err(e) = WalletSeed::seed_file_exists(&wallet_config) {
println!(
"Not creating wallet - Wallet seed file already exists at {}",
e.inner
);
return 0;
}
let list_length = match r.is_present("short_wordlist") {
false => 32,
true => 16,
};
println!("Please enter a password for your new wallet");
let passphrase = prompt_password_confirm();
WalletSeed::init_file(&wallet_config, list_length, &passphrase)
.expect("Failed to init wallet seed file.");
info!("Wallet seed file created");
let client_n =
HTTPNodeClient::new(&wallet_config.check_node_api_http_addr, node_api_secret);
let _: LMDBBackend<HTTPNodeClient, keychain::ExtKeychain> =
LMDBBackend::new(wallet_config.clone(), &passphrase, client_n).unwrap_or_else(|e| {
panic!(
"Error creating DB for wallet: {} Config: {:?}",
e, wallet_config
);
});
info!("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
return 0;
}
// Recover a seed from a recovery phrase
if let ("recover", Some(r)) = wallet_args.subcommand() {
if !r.is_present("recovery_phrase") {
// only needed to display phrase
let passphrase = prompt_password(wallet_args);
let seed = match WalletSeed::from_file(&wallet_config, &passphrase) {
Ok(s) => s,
Err(e) => {
println!("Can't open wallet seed file (check password): {}", e);
std::process::exit(0);
}
};
let _ = seed.show_recovery_phrase();
// closure to instantiate wallet as needed by each subcommand
let inst_wallet = || {
let res = wallet_args::instantiate_wallet(wallet_config.clone(), &global_wallet_args);
res.unwrap_or_else(|e| {
println!("{}", e);
std::process::exit(0);
}
let word_list = match r.value_of("recovery_phrase") {
Some(w) => w,
None => {
println!("Recovery word phrase must be provided (in quotes)");
std::process::exit(0);
}
};
// check word list is okay before asking for password
if WalletSeed::from_mnemonic(word_list).is_err() {
println!("Recovery word phrase is invalid");
std::process::exit(0);
}
println!("Please provide a new password for the recovered wallet");
let passphrase = prompt_password_confirm();
let res = WalletSeed::recover_from_phrase(&wallet_config, word_list, &passphrase);
if let Err(e) = res {
thread::sleep(Duration::from_millis(200));
error!("Error recovering seed with list '{}' - {}", word_list, e);
return 0;
}
thread::sleep(Duration::from_millis(200));
return 0;
}
let account = match wallet_args.value_of("account") {
None => {
error!("Failed to read account.");
return 1;
}
Some(p) => p,
})
};
// all further commands always need a password
let passphrase = prompt_password(wallet_args);
// Handle listener startup commands
{
let api_secret = get_first_line(wallet_config.api_secret_path.clone());
let tls_conf = match wallet_config.tls_certificate_file.clone() {
None => None,
Some(file) => Some(TLSConfig::new(
file,
wallet_config
.tls_certificate_key
.clone()
.unwrap_or_else(|| {
panic!("Private key for certificate is not set");
}),
)),
};
match wallet_args.subcommand() {
("listen", Some(listen_args)) => {
if let Some(port) = listen_args.value_of("port") {
wallet_config.api_listen_port = port.parse().unwrap();
}
let mut params = HashMap::new();
params.insert(
"api_listen_addr".to_owned(),
wallet_config.api_listen_addr(),
);
if let Some(t) = tls_conf {
params.insert("certificate".to_owned(), t.certificate);
params.insert("private_key".to_owned(), t.private_key);
}
let adapter = match listen_args.value_of("method") {
Some("http") => HTTPWalletCommAdapter::new(),
Some("keybase") => KeybaseWalletCommAdapter::new(),
_ => {
std::process::exit(1);
}
};
adapter
.listen(
params,
wallet_config.clone(),
&passphrase,
account,
node_api_secret.clone(),
).unwrap_or_else(|e| {
if e.kind() == ErrorKind::WalletSeedDecryption {
println!("Error decrypting wallet seed (check provided password)");
std::process::exit(0);
}
panic!(
"Error creating wallet listener: {:?} Config: {:?}",
e, wallet_config
);
});
}
("owner_api", Some(_api_args)) => {
let wallet = instantiate_wallet(
wallet_config.clone(),
&passphrase,
account,
node_api_secret.clone(),
).unwrap_or_else(|e| {
if e.kind() == grin_wallet::ErrorKind::Encryption {
println!("Error decrypting wallet seed (check provided password)");
std::process::exit(0);
}
panic!(
"Error creating wallet listener: {:?} Config: {:?}",
e, wallet_config
);
});
// TLS is disabled because we bind to localhost
controller::owner_listener(wallet.clone(), "127.0.0.1:13420", api_secret, None)
.unwrap_or_else(|e| {
panic!(
"Error creating wallet api listener: {:?} Config: {:?}",
e, wallet_config
);
});
}
("web", Some(_api_args)) => {
let wallet = instantiate_wallet(
wallet_config.clone(),
&passphrase,
account,
node_api_secret.clone(),
).unwrap_or_else(|e| {
if e.kind() == grin_wallet::ErrorKind::Encryption {
println!("Error decrypting wallet seed (check provided password)");
std::process::exit(0);
}
panic!(
"Error creating wallet listener: {:?} Config: {:?}",
e, wallet_config
);
});
// start owner listener and run static file server
start_webwallet_server();
controller::owner_listener(wallet.clone(), "127.0.0.1:13420", api_secret, tls_conf)
.unwrap_or_else(|e| {
panic!(
"Error creating wallet api listener: {:?} Config: {:?}",
e, wallet_config
);
});
}
_ => {}
};
}
let wallet = instantiate_wallet(
wallet_config.clone(),
&passphrase,
account,
node_api_secret.clone(),
).unwrap_or_else(|e| {
if e.kind() == grin_wallet::ErrorKind::Encryption {
println!("Error decrypting wallet seed (check provided password)");
std::process::exit(0);
let res = match wallet_args.subcommand() {
("init", Some(args)) => {
let a = arg_parse!(wallet_args::parse_init_args(&wallet_config, &args));
command::init(&global_wallet_args, a)
}
panic!(
"Error instantiating wallet: {:?} Config: {:?}",
e, wallet_config
);
});
let res = controller::owner_single_use(wallet.clone(), |api| {
match wallet_args.subcommand() {
("account", Some(acct_args)) => {
let create = acct_args.value_of("create");
if create.is_none() {
let res = controller::owner_single_use(wallet, |api| {
let acct_mappings = api.accounts()?;
// give logging thread a moment to catch up
thread::sleep(Duration::from_millis(200));
display::accounts(acct_mappings);
Ok(())
});
if let Err(e) = res {
error!("Error listing accounts: {}", e);
return Err(e);
}
} else {
let label = create.unwrap();
let res = controller::owner_single_use(wallet, |api| {
api.create_account_path(label)?;
thread::sleep(Duration::from_millis(200));
println!("Account: '{}' Created!", label);
Ok(())
});
if let Err(e) = res {
thread::sleep(Duration::from_millis(200));
error!("Error creating account '{}': {}", label, e);
return Err(e);
}
}
Ok(())
}
("send", Some(send_args)) => {
let amount = send_args.value_of("amount").ok_or_else(|| {
ErrorKind::GenericError("Amount to send required".to_string())
})?;
let amount = core::amount_from_hr_string(amount).map_err(|e| {
ErrorKind::GenericError(format!(
"Could not parse amount as a number with optional decimal point. e={:?}",
e
))
})?;
let message = match send_args.is_present("message") {
true => Some(send_args.value_of("message").unwrap().to_owned()),
false => None,
};
let minimum_confirmations: u64 = send_args
.value_of("minimum_confirmations")
.ok_or_else(|| {
ErrorKind::GenericError(
"Minimum confirmations to send required".to_string(),
)
}).and_then(|v| {
v.parse().map_err(|e| {
ErrorKind::GenericError(format!(
"Could not parse minimum_confirmations as a whole number. e={:?}",
e
))
})
})?;
let selection_strategy =
send_args.value_of("selection_strategy").ok_or_else(|| {
ErrorKind::GenericError("Selection strategy required".to_string())
})?;
let method = send_args.value_of("method").ok_or_else(|| {
ErrorKind::GenericError("Payment method required".to_string())
})?;
let dest = {
if method == "self" {
match send_args.value_of("dest") {
Some(d) => d,
None => "default",
}
} else {
send_args.value_of("dest").ok_or_else(|| {
ErrorKind::GenericError(
"Destination wallet address required".to_string(),
)
})?
}
};
let change_outputs = send_args
.value_of("change_outputs")
.ok_or_else(|| ErrorKind::GenericError("Change outputs required".to_string()))
.and_then(|v| {
v.parse().map_err(|e| {
ErrorKind::GenericError(format!(
"Failed to parse number of change outputs. e={:?}",
e
))
})
})?;
let fluff = send_args.is_present("fluff");
let max_outputs = 500;
if method == "http" && !dest.starts_with("http://") && !dest.starts_with("https://")
{
return Err(ErrorKind::GenericError(format!(
"HTTP Destination should start with http://: or https://: {}",
dest
)).into());
}
let result = api.initiate_tx(
None,
amount,
minimum_confirmations,
max_outputs,
change_outputs,
selection_strategy == "all",
message,
);
let (mut slate, lock_fn) = match result {
Ok(s) => {
info!(
"Tx created: {} grin to {} (strategy '{}')",
core::amount_to_hr_string(amount, false),
dest,
selection_strategy,
);
s
}
Err(e) => {
error!("Tx not created: {}", e);
match e.kind() {
// user errors, don't backtrace
libwallet::ErrorKind::NotEnoughFunds { .. } => {}
libwallet::ErrorKind::FeeDispute { .. } => {}
libwallet::ErrorKind::FeeExceedsAmount { .. } => {}
_ => {
// otherwise give full dump
error!("Backtrace: {}", e.backtrace().unwrap());
}
};
return Err(e);
}
};
let adapter = match method {
"http" => HTTPWalletCommAdapter::new(),
"file" => FileWalletCommAdapter::new(),
"self" => NullWalletCommAdapter::new(),
"keybase" => KeybaseWalletCommAdapter::new(),
_ => NullWalletCommAdapter::new(),
};
if adapter.supports_sync() {
slate = adapter.send_tx_sync(dest, &slate)?;
if method == "self" {
controller::foreign_single_use(wallet, |api| {
api.receive_tx(&mut slate, Some(dest), None)?;
Ok(())
})?;
}
api.tx_lock_outputs(&slate, lock_fn)?;
if let Err(e) = api.verify_slate_messages(&slate) {
error!("Error validating participant messages: {}", e);
return Err(e);
}
api.finalize_tx(&mut slate)?;
} else {
adapter.send_tx_async(dest, &slate)?;
api.tx_lock_outputs(&slate, lock_fn)?;
}
if adapter.supports_sync() {
let result = api.post_tx(&slate.tx, fluff);
match result {
Ok(_) => {
info!("Tx sent",);
println!("Tx sent",);
return Ok(());
}
Err(e) => {
error!("Tx not sent: {}", e);
return Err(e);
}
}
}
Ok(())
}
("receive", Some(send_args)) => {
let mut receive_result: Result<(), grin_wallet::libwallet::Error> = Ok(());
let message = match send_args.is_present("message") {
true => Some(send_args.value_of("message").unwrap().to_owned()),
false => None,
};
let tx_file = send_args.value_of("input").ok_or_else(|| {
ErrorKind::GenericError("Transaction file required".to_string())
})?;
if !Path::new(tx_file).is_file() {
return Err(
ErrorKind::GenericError(format!("File {} not found.", tx_file)).into(),
);
}
let adapter = FileWalletCommAdapter::new();
let mut slate = adapter.receive_tx_async(tx_file)?;
controller::foreign_single_use(wallet, |api| {
api.receive_tx(&mut slate, Some(account), message)?;
Ok(())
})?;
let send_tx = format!("{}.response", tx_file);
adapter.send_tx_async(&send_tx, &slate)?;
info!(
"Response file {}.response generated, sending it back to the transaction originator.",
tx_file,
);
receive_result
}
("finalize", Some(send_args)) => {
let fluff = send_args.is_present("fluff");
let tx_file = send_args.value_of("input").ok_or_else(|| {
ErrorKind::GenericError("Receiver's transaction file required".to_string())
})?;
if !Path::new(tx_file).is_file() {
return Err(
ErrorKind::GenericError(format!("File {} not found.", tx_file)).into(),
);
}
let adapter = FileWalletCommAdapter::new();
let mut slate = adapter.receive_tx_async(tx_file)?;
if let Err(e) = api.verify_slate_messages(&slate) {
error!("Error validating participant messages: {}", e);
return Err(e);
}
let _ = api.finalize_tx(&mut slate).expect("Finalize failed");
let result = api.post_tx(&slate.tx, fluff);
match result {
Ok(_) => {
info!("Transaction sent successfully, check the wallet again for confirmation.");
Ok(())
}
Err(e) => {
error!("Tx not sent: {}", e);
Err(e)
}
}
}
("info", Some(args)) => {
let minimum_confirmations: u64 = args
.value_of("minimum_confirmations")
.ok_or_else(|| {
ErrorKind::GenericError("Minimum confirmations required".to_string())
}).and_then(|v| {
v.parse().map_err(|e| {
ErrorKind::GenericError(format!(
"Could not parse minimum_confirmations as a whole number. e={:?}",
e
))
})
})?;
let (validated, wallet_info) = api
.retrieve_summary_info(true, minimum_confirmations)
.map_err(|e| {
ErrorKind::GenericError(format!(
"Error getting wallet info: {:?} Config: {:?}",
e, wallet_config
))
})?;
display::info(
account,
&wallet_info,
validated,
wallet_config.dark_background_color_scheme.unwrap_or(true),
);
Ok(())
}
("outputs", Some(_)) => {
let (height, _) = api.node_height()?;
let (validated, outputs) = api.retrieve_outputs(show_spent, true, None)?;
display::outputs(
account,
height,
validated,
outputs,
wallet_config.dark_background_color_scheme.unwrap_or(true),
).map_err(|e| {
ErrorKind::GenericError(format!(
"Error getting wallet outputs: {:?} Config: {:?}",
e, wallet_config
))
})?;
Ok(())
}
("txs", Some(txs_args)) => {
let tx_id = match txs_args.value_of("id") {
None => None,
Some(tx) => match tx.parse() {
Ok(t) => Some(t),
Err(_) => {
return Err(ErrorKind::GenericError(
"Unable to parse argument 'id' as a number".to_string(),
).into());
}
},
};
let (height, _) = api.node_height()?;
let (validated, txs) = api.retrieve_txs(true, tx_id, None)?;
let include_status = !tx_id.is_some();
display::txs(
account,
height,
validated,
txs,
include_status,
wallet_config.dark_background_color_scheme.unwrap_or(true),
).map_err(|e| {
ErrorKind::GenericError(format!(
"Error getting wallet outputs: {} Config: {:?}",
e, wallet_config
))
})?;
// if given a particular transaction id, also get and display associated
// inputs/outputs
if tx_id.is_some() {
let (_, outputs) = api.retrieve_outputs(true, false, tx_id)?;
display::outputs(
account,
height,
validated,
outputs,
wallet_config.dark_background_color_scheme.unwrap_or(true),
).map_err(|e| {
ErrorKind::GenericError(format!(
"Error getting wallet outputs: {} Config: {:?}",
e, wallet_config
))
})?;
};
Ok(())
}
("repost", Some(repost_args)) => {
let tx_id = repost_args
.value_of("id")
.ok_or_else(|| {
ErrorKind::GenericError("Transaction of a completed but unconfirmed transaction required (specify with --id=[id])".to_string())
}).and_then(|v|{
v.parse().map_err(|e| {
ErrorKind::GenericError(format!(
"Unable to parse argument 'id' as a number. e={:?}",
e
))
})})?;
let dump_file = repost_args.value_of("dumpfile");
let fluff = repost_args.is_present("fluff");
let (_, txs) = api.retrieve_txs(true, Some(tx_id), None)?;
let stored_tx = txs[0].get_stored_tx();
if stored_tx.is_none() {
println!(
"Transaction with id {} does not have transaction data. Not reposting.",
tx_id
);
std::process::exit(0);
}
match dump_file {
None => {
if txs[0].confirmed {
println!("Transaction with id {} is confirmed. Not reposting.", tx_id);
std::process::exit(0);
}
api.post_tx(&stored_tx.unwrap(), fluff)?;
info!("Reposted transaction at {}", tx_id);
println!("Reposted transaction at {}", tx_id);
Ok(())
}
Some(f) => {
let mut tx_file = File::create(f)?;
tx_file.write_all(json::to_string(&stored_tx).unwrap().as_bytes())?;
tx_file.sync_all()?;
info!("Dumped transaction data for tx {} to {}", tx_id, f);
println!("Dumped transaction data for tx {} to {}", tx_id, f);
Ok(())
}
}
}
("cancel", Some(tx_args)) => {
let mut tx_id_string = "";
let tx_id = match tx_args.value_of("id") {
None => None,
Some(tx) => match tx.parse() {
Ok(t) => {
tx_id_string = tx;
Some(t)
}
Err(e) => {
return Err(ErrorKind::GenericError(format!(
"Could not parse id parameter. e={:?}",
e,
)).into());
}
},
};
let tx_slate_id = match tx_args.value_of("txid") {
None => None,
Some(tx) => match tx.parse() {
Ok(t) => {
tx_id_string = tx;
Some(t)
}
Err(e) => {
return Err(ErrorKind::GenericError(format!(
"Could not parse txid parameter. e={:?}",
e,
)).into());
}
},
};
if (tx_id.is_none() && tx_slate_id.is_none())
|| (tx_id.is_some() && tx_slate_id.is_some())
{
return Err(ErrorKind::GenericError(format!(
"'id' (-i) or 'txid' (-t) argument is required."
)).into());
}
let result = api.cancel_tx(tx_id, tx_slate_id);
match result {
Ok(_) => {
info!("Transaction {} Cancelled", tx_id_string);
Ok(())
}
Err(e) => {
error!("TX Cancellation failed: {}", e);
Err(e)
}
}
}
("restore", Some(_)) => {
let result = api.restore();
match result {
Ok(_) => {
info!("Wallet restore complete",);
Ok(())
}
Err(e) => {
error!("Wallet restore failed: {}", e);
error!("Backtrace: {}", e.backtrace().unwrap());
Err(e)
}
}
}
_ => {
return Err(ErrorKind::GenericError(
"Unknown wallet command, use 'grin help wallet' for details".to_string(),
).into());
}
("recover", Some(args)) => {
let a = arg_parse!(wallet_args::parse_recover_args(&global_wallet_args, &args));
command::recover(&wallet_config, a)
}
});
("listen", Some(args)) => {
let mut c = wallet_config.clone();
let mut g = global_wallet_args.clone();
arg_parse!(wallet_args::parse_listen_args(&mut c, &mut g, &args));
command::listen(&wallet_config, &g)
}
("owner_api", Some(_)) => {
let mut g = global_wallet_args.clone();
g.tls_conf = None;
command::owner_api(inst_wallet(), &g)
}
("web", Some(_)) => {
start_webwallet_server();
command::owner_api(inst_wallet(), &global_wallet_args)
}
("account", Some(args)) => {
let a = arg_parse!(wallet_args::parse_account_args(&args));
command::account(inst_wallet(), a)
}
("send", Some(args)) => {
let a = arg_parse!(wallet_args::parse_send_args(&args));
command::send(inst_wallet(), a)
}
("receive", Some(args)) => {
let a = arg_parse!(wallet_args::parse_receive_args(&args));
command::receive(inst_wallet(), &global_wallet_args, a)
}
("finalize", Some(args)) => {
let a = arg_parse!(wallet_args::parse_finalize_args(&args));
command::finalize(inst_wallet(), a)
}
("info", Some(args)) => {
let a = arg_parse!(wallet_args::parse_info_args(&args));
command::info(
inst_wallet(),
&global_wallet_args,
a,
wallet_config.dark_background_color_scheme.unwrap_or(true),
)
}
("outputs", Some(_)) => command::outputs(
inst_wallet(),
&global_wallet_args,
wallet_config.dark_background_color_scheme.unwrap_or(true),
),
("txs", Some(args)) => {
let a = arg_parse!(wallet_args::parse_txs_args(&args));
command::txs(
inst_wallet(),
&global_wallet_args,
a,
wallet_config.dark_background_color_scheme.unwrap_or(true),
)
}
("repost", Some(args)) => {
let a = arg_parse!(wallet_args::parse_repost_args(&args));
command::repost(inst_wallet(), a)
}
("cancel", Some(args)) => {
let a = arg_parse!(wallet_args::parse_cancel_args(&args));
command::cancel(inst_wallet(), a)
}
("restore", Some(_)) => command::restore(inst_wallet()),
_ => {
println!("Unknown wallet command, use 'grin help wallet' for details");
return 0;
}
};
// we need to give log output a chance to catch up before exiting
thread::sleep(Duration::from_millis(100));
@ -771,6 +170,10 @@ pub fn wallet_command(wallet_args: &ArgMatches, config: GlobalWalletConfig) -> i
println!("Wallet command failed: {}", e);
1
} else {
println!(
"Command '{}' completed successfully",
wallet_args.subcommand().0
);
0
}
}

393
src/bin/cmd/wallet_args.rs Normal file
View file

@ -0,0 +1,393 @@
// 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.
/// Argument parsing and error handling for wallet commands
use clap::ArgMatches;
use failure::Fail;
use api::TLSConfig;
use core::core;
use grin_wallet::{self, command, WalletConfig, WalletSeed};
use std::path::Path;
use util::file::get_first_line;
/// Simple error definition, just so we can return errors from all commands
/// and let the caller figure out what to do
#[derive(Clone, Eq, PartialEq, Debug, Fail)]
pub enum Error {
#[fail(display = "Invalid Arguments: {}", _0)]
ArgumentError(String),
}
pub fn prompt_password(password: &Option<String>) -> String {
match password {
None => {
println!("Temporary note:");
println!(
"If this is your first time running your wallet since BIP32 (word lists) \
were implemented, your seed will be converted to \
the new format. Please ensure the provided password is correct."
);
println!("If this goes wrong, your old 'wallet.seed' file has been saved as 'wallet.seed.bak' \
Rename this file to back to `wallet.seed` and try again");
rpassword::prompt_password_stdout("Password: ").unwrap()
}
Some(p) => p.to_owned(),
}
}
fn prompt_password_confirm() -> String {
let first = rpassword::prompt_password_stdout("Password: ").unwrap();
let second = rpassword::prompt_password_stdout("Confirm Password: ").unwrap();
if first != second {
println!("Passwords do not match");
std::process::exit(0);
}
first
}
// instantiate wallet (needed by most functions)
pub fn instantiate_wallet(
config: WalletConfig,
g_args: &command::GlobalArgs,
) -> Result<command::WalletRef, Error> {
let passphrase = prompt_password(&g_args.password);
let res = grin_wallet::instantiate_wallet(
config.clone(),
&passphrase,
&g_args.account,
g_args.node_api_secret.clone(),
);
match res {
Ok(p) => Ok(p),
Err(e) => {
let msg = {
match e.kind() {
grin_wallet::ErrorKind::Encryption => {
format!("Error decrypting wallet seed (check provided password)")
}
_ => format!("Error instantiating wallet: {}", e),
}
};
Err(Error::ArgumentError(msg))
}
}
}
// parses a required value, or throws error with message otherwise
fn parse_required<'a>(args: &'a ArgMatches, name: &str) -> Result<&'a str, Error> {
let arg = args.value_of(name);
match arg {
Some(ar) => Ok(ar),
None => {
let msg = format!("Value for argument '{}' is required in this context", name,);
Err(Error::ArgumentError(msg))
}
}
}
// parses a number, or throws error with message otherwise
fn parse_u64(arg: &str, name: &str) -> Result<u64, Error> {
let val = arg.parse::<u64>();
match val {
Ok(v) => Ok(v),
Err(e) => {
let msg = format!("Could not parse {} as a whole number. e={}", name, e);
Err(Error::ArgumentError(msg))
}
}
}
pub fn parse_global_args(
config: &WalletConfig,
args: &ArgMatches,
) -> Result<command::GlobalArgs, Error> {
let account = parse_required(args, "account")?;
let mut show_spent = false;
if args.is_present("show_spent") {
show_spent = true;
}
let node_api_secret = get_first_line(config.node_api_secret_path.clone());
let password = match args.value_of("pass") {
None => None,
Some(p) => Some(p.to_owned()),
};
let tls_conf = match config.tls_certificate_file.clone() {
None => None,
Some(file) => {
let key = match config.tls_certificate_key.clone() {
Some(k) => k,
None => {
let msg = format!("Private key for certificate is not set");
return Err(Error::ArgumentError(msg));
}
};
Some(TLSConfig::new(file, key))
}
};
Ok(command::GlobalArgs {
account: account.to_owned(),
show_spent: show_spent,
node_api_secret: node_api_secret,
password: password,
tls_conf: tls_conf,
})
}
pub fn parse_init_args(
config: &WalletConfig,
args: &ArgMatches,
) -> Result<command::InitArgs, Error> {
if let Err(e) = WalletSeed::seed_file_exists(config) {
let msg = format!("Not creating wallet - {}", e.inner);
return Err(Error::ArgumentError(msg));
}
let list_length = match args.is_present("short_wordlist") {
false => 32,
true => 16,
};
println!("Please enter a password for your new wallet");
let password = prompt_password_confirm();
Ok(command::InitArgs {
list_length: list_length,
password: password,
config: config.clone(),
})
}
pub fn parse_recover_args(
g_args: &command::GlobalArgs,
args: &ArgMatches,
) -> Result<command::RecoverArgs, Error> {
let (passphrase, recovery_phrase) = {
match args.value_of("recovery_phrase") {
None => (prompt_password(&g_args.password), None),
Some(l) => {
if WalletSeed::from_mnemonic(l).is_err() {
let msg = format!("Recovery word phrase is invalid");
return Err(Error::ArgumentError(msg));
}
println!("Please provide a new password for the recovered wallet");
(prompt_password_confirm(), Some(l.to_owned()))
}
}
};
Ok(command::RecoverArgs {
passphrase: passphrase,
recovery_phrase: recovery_phrase,
})
}
pub fn parse_listen_args(
config: &mut WalletConfig,
g_args: &mut command::GlobalArgs,
args: &ArgMatches,
) -> Result<(), Error> {
// listen args
let pass = match args.value_of("pass") {
Some(p) => Some(p.to_owned()),
None => Some(prompt_password(&None)),
};
g_args.password = pass;
if let Some(port) = args.value_of("port") {
config.api_listen_port = port.parse().unwrap();
}
Ok(())
}
pub fn parse_account_args(account_args: &ArgMatches) -> Result<command::AccountArgs, Error> {
let create = match account_args.value_of("create") {
None => None,
Some(s) => Some(s.to_owned()),
};
Ok(command::AccountArgs { create: create })
}
pub fn parse_send_args(args: &ArgMatches) -> Result<command::SendArgs, Error> {
// amount
let amount = parse_required(args, "amount")?;
let amount = core::amount_from_hr_string(amount);
let amount = match amount {
Ok(a) => a,
Err(e) => {
let msg = format!(
"Could not parse amount as a number with optional decimal point. e={}",
e
);
return Err(Error::ArgumentError(msg));
}
};
// message
let message = match args.is_present("message") {
true => Some(args.value_of("message").unwrap().to_owned()),
false => None,
};
// minimum_confirmations
let min_c = parse_required(args, "minimum_confirmations")?;
let min_c = parse_u64(min_c, "minimum_confirmations")?;
// selection_strategy
let selection_strategy = parse_required(args, "selection_strategy")?;
// method
let method = parse_required(args, "method")?;
// dest
let dest = {
if method == "self" {
match args.value_of("dest") {
Some(d) => d,
None => "default",
}
} else {
parse_required(args, "dest")?
}
};
if method == "http" && !dest.starts_with("http://") && !dest.starts_with("https://") {
let msg = format!(
"HTTP Destination should start with http://: or https://: {}",
dest,
);
return Err(Error::ArgumentError(msg));
}
// change_outputs
let change_outputs = parse_required(args, "change_outputs")?;
let change_outputs = parse_u64(change_outputs, "change_outputs")? as usize;
// fluff
let fluff = args.is_present("fluff");
// max_outputs
let max_outputs = 500;
Ok(command::SendArgs {
amount: amount,
message: message,
minimum_confirmations: min_c,
selection_strategy: selection_strategy.to_owned(),
method: method.to_owned(),
dest: dest.to_owned(),
change_outputs: change_outputs,
fluff: fluff,
max_outputs: max_outputs,
})
}
pub fn parse_receive_args(receive_args: &ArgMatches) -> Result<command::ReceiveArgs, Error> {
// message
let message = match receive_args.is_present("message") {
true => Some(receive_args.value_of("message").unwrap().to_owned()),
false => None,
};
// input
let tx_file = parse_required(receive_args, "input")?;
// validate input
if !Path::new(&tx_file).is_file() {
let msg = format!("File {} not found.", &tx_file);
return Err(Error::ArgumentError(msg));
}
Ok(command::ReceiveArgs {
input: tx_file.to_owned(),
message: message,
})
}
pub fn parse_finalize_args(args: &ArgMatches) -> Result<command::FinalizeArgs, Error> {
let fluff = args.is_present("fluff");
let tx_file = parse_required(args, "input")?;
if !Path::new(&tx_file).is_file() {
let msg = format!("File {} not found.", tx_file);
return Err(Error::ArgumentError(msg));
}
Ok(command::FinalizeArgs {
input: tx_file.to_owned(),
fluff: fluff,
})
}
pub fn parse_info_args(args: &ArgMatches) -> Result<command::InfoArgs, Error> {
// minimum_confirmations
let mc = parse_required(args, "minimum_confirmations")?;
let mc = parse_u64(mc, "minimum_confirmations")?;
Ok(command::InfoArgs {
minimum_confirmations: mc,
})
}
pub fn parse_txs_args(args: &ArgMatches) -> Result<command::TxsArgs, Error> {
let tx_id = match args.value_of("id") {
None => None,
Some(tx) => Some(parse_u64(tx, "id")? as u32),
};
Ok(command::TxsArgs { id: tx_id })
}
pub fn parse_repost_args(args: &ArgMatches) -> Result<command::RepostArgs, Error> {
let tx_id = match args.value_of("id") {
None => None,
Some(tx) => Some(parse_u64(tx, "id")? as u32),
};
let fluff = args.is_present("fluff");
let dump_file = match args.value_of("dumpfile") {
None => None,
Some(d) => Some(d.to_owned()),
};
Ok(command::RepostArgs {
id: tx_id.unwrap(),
dump_file: dump_file,
fluff: fluff,
})
}
pub fn parse_cancel_args(args: &ArgMatches) -> Result<command::CancelArgs, Error> {
let mut tx_id_string = "";
let tx_id = match args.value_of("id") {
None => None,
Some(tx) => Some(parse_u64(tx, "id")? as u32),
};
let tx_slate_id = match args.value_of("txid") {
None => None,
Some(tx) => match tx.parse() {
Ok(t) => {
tx_id_string = tx;
Some(t)
}
Err(e) => {
let msg = format!("Could not parse txid parameter. e={}", e);
return Err(Error::ArgumentError(msg));
}
},
};
if (tx_id.is_none() && tx_slate_id.is_none()) || (tx_id.is_some() && tx_slate_id.is_some()) {
let msg = format!("'id' (-i) or 'txid' (-t) argument is required.");
return Err(Error::ArgumentError(msg));
}
Ok(command::CancelArgs {
tx_id: tx_id,
tx_slate_id: tx_slate_id,
tx_id_string: tx_id_string.to_owned(),
})
}

View file

@ -25,8 +25,11 @@ extern crate serde;
extern crate serde_json;
#[macro_use]
extern crate log;
extern crate failure;
extern crate rpassword;
extern crate term;
#[macro_use]
extern crate failure_derive;
extern crate grin_api as api;
extern crate grin_config as config;

455
wallet/src/command.rs Normal file
View file

@ -0,0 +1,455 @@
// 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.
use std::collections::HashMap;
/// Grin wallet command-line function implementations
use std::fs::File;
use std::io::Write;
use std::sync::Arc;
use std::thread;
use std::time::Duration;
use util::Mutex;
use serde_json as json;
use uuid::Uuid;
use api::TLSConfig;
use core::core;
use keychain;
use error::{Error, ErrorKind};
use {controller, display, HTTPNodeClient, WalletConfig, WalletInst, WalletSeed};
use {FileWalletCommAdapter, HTTPWalletCommAdapter, LMDBBackend, NullWalletCommAdapter};
pub type WalletRef = Arc<Mutex<WalletInst<HTTPNodeClient, keychain::ExtKeychain>>>;
/// Arguments common to all wallet commands
#[derive(Clone)]
pub struct GlobalArgs {
pub account: String,
pub node_api_secret: Option<String>,
pub show_spent: bool,
pub password: Option<String>,
pub tls_conf: Option<TLSConfig>,
}
/// Arguments for init command
pub struct InitArgs {
/// BIP39 recovery phrase length
pub list_length: usize,
pub password: String,
pub config: WalletConfig,
}
pub fn init(g_args: &GlobalArgs, args: InitArgs) -> Result<(), Error> {
WalletSeed::init_file(&args.config, args.list_length, &args.password)?;
info!("Wallet seed file created");
let client_n = HTTPNodeClient::new(
&args.config.check_node_api_http_addr,
g_args.node_api_secret.clone(),
);
let _: LMDBBackend<HTTPNodeClient, keychain::ExtKeychain> =
LMDBBackend::new(args.config.clone(), &args.password, client_n)?;
info!("Wallet database backend created");
Ok(())
}
/// Argument for recover
pub struct RecoverArgs {
pub recovery_phrase: Option<String>,
pub passphrase: String,
}
pub fn recover(config: &WalletConfig, args: RecoverArgs) -> Result<(), Error> {
if args.recovery_phrase.is_none() {
let res = WalletSeed::from_file(config, &args.passphrase);
if let Err(e) = res {
error!("Error loading wallet seed (check password): {}", e);
return Err(e);
}
let _ = res.unwrap().show_recovery_phrase();
} else {
let res = WalletSeed::recover_from_phrase(
&config,
&args.recovery_phrase.as_ref().unwrap(),
&args.passphrase,
);
if let Err(e) = res {
error!(
"Error recovering seed with list '{}' - {}",
&args.recovery_phrase.as_ref().unwrap(),
e
);
return Err(e);
}
}
Ok(())
}
/// Arguments for listen command
pub struct ListenArgs {
pub port: String,
}
pub fn listen(config: &WalletConfig, g_args: &GlobalArgs) -> Result<(), Error> {
let mut params = HashMap::new();
params.insert("api_listen_addr".to_owned(), config.api_listen_addr());
if let Some(t) = g_args.tls_conf.as_ref() {
params.insert("certificate".to_owned(), t.certificate.clone());
params.insert("private_key".to_owned(), t.private_key.clone());
}
let adapter = HTTPWalletCommAdapter::new();
let res = adapter.listen(
params,
config.clone(),
&g_args.password.clone().unwrap(),
&g_args.account,
g_args.node_api_secret.clone(),
);
if let Err(e) = res {
return Err(ErrorKind::LibWallet(e.kind(), e.cause_string()).into());
}
Ok(())
}
pub fn owner_api(wallet: WalletRef, g_args: &GlobalArgs) -> Result<(), Error> {
let res = controller::owner_listener(
wallet,
"127.0.0.1:13420",
g_args.node_api_secret.clone(),
g_args.tls_conf.clone(),
);
if let Err(e) = res {
return Err(ErrorKind::LibWallet(e.kind(), e.cause_string()).into());
}
Ok(())
}
/// Arguments for account command
pub struct AccountArgs {
pub create: Option<String>,
}
pub fn account(wallet: WalletRef, args: AccountArgs) -> Result<(), Error> {
if args.create.is_none() {
let res = controller::owner_single_use(wallet, |api| {
let acct_mappings = api.accounts()?;
// give logging thread a moment to catch up
thread::sleep(Duration::from_millis(200));
display::accounts(acct_mappings);
Ok(())
});
if let Err(e) = res {
error!("Error listing accounts: {}", e);
return Err(ErrorKind::LibWallet(e.kind(), e.cause_string()).into());
}
} else {
let label = args.create.unwrap();
let res = controller::owner_single_use(wallet, |api| {
api.create_account_path(&label)?;
thread::sleep(Duration::from_millis(200));
info!("Account: '{}' Created!", label);
Ok(())
});
if let Err(e) = res {
thread::sleep(Duration::from_millis(200));
error!("Error creating account '{}': {}", label, e);
return Err(ErrorKind::LibWallet(e.kind(), e.cause_string()).into());
}
}
Ok(())
}
/// Arguments for the send command
pub struct SendArgs {
pub amount: u64,
pub message: Option<String>,
pub minimum_confirmations: u64,
pub selection_strategy: String,
pub method: String,
pub dest: String,
pub change_outputs: usize,
pub fluff: bool,
pub max_outputs: usize,
}
pub fn send(wallet: WalletRef, args: SendArgs) -> Result<(), Error> {
controller::owner_single_use(wallet.clone(), |api| {
let result = api.initiate_tx(
None,
args.amount,
args.minimum_confirmations,
args.max_outputs,
args.change_outputs,
args.selection_strategy == "all",
args.message.clone(),
);
let (mut slate, lock_fn) = match result {
Ok(s) => {
info!(
"Tx created: {} grin to {} (strategy '{}')",
core::amount_to_hr_string(args.amount, false),
args.dest,
args.selection_strategy,
);
s
}
Err(e) => {
info!("Tx not created: {}", e);
return Err(e);
}
};
let adapter = match args.method.as_str() {
"http" => HTTPWalletCommAdapter::new(),
"file" => FileWalletCommAdapter::new(),
"self" => NullWalletCommAdapter::new(),
_ => NullWalletCommAdapter::new(),
};
if adapter.supports_sync() {
slate = adapter.send_tx_sync(&args.dest, &slate)?;
if args.method == "self" {
controller::foreign_single_use(wallet, |api| {
api.receive_tx(&mut slate, Some(&args.dest), None)?;
Ok(())
})?;
}
api.tx_lock_outputs(&slate, lock_fn)?;
if let Err(e) = api.verify_slate_messages(&slate) {
error!("Error validating participant messages: {}", e);
return Err(e);
}
api.finalize_tx(&mut slate)?;
} else {
adapter.send_tx_async(&args.dest, &slate)?;
api.tx_lock_outputs(&slate, lock_fn)?;
}
if adapter.supports_sync() {
let result = api.post_tx(&slate.tx, args.fluff);
match result {
Ok(_) => {
info!("Tx sent",);
return Ok(());
}
Err(e) => {
error!("Tx not sent: {}", e);
return Err(e);
}
}
}
Ok(())
})?;
Ok(())
}
/// Receive command argument
pub struct ReceiveArgs {
pub input: String,
pub message: Option<String>,
}
pub fn receive(wallet: WalletRef, g_args: &GlobalArgs, args: ReceiveArgs) -> Result<(), Error> {
let adapter = FileWalletCommAdapter::new();
let mut slate = adapter.receive_tx_async(&args.input)?;
controller::foreign_single_use(wallet, |api| {
api.receive_tx(&mut slate, Some(&g_args.account), args.message.clone())?;
Ok(())
})?;
let send_tx = format!("{}.response", args.input);
adapter.send_tx_async(&send_tx, &slate)?;
info!(
"Response file {}.response generated, sending it back to the transaction originator.",
args.input
);
Ok(())
}
/// Finalize command args
pub struct FinalizeArgs {
pub input: String,
pub fluff: bool,
}
pub fn finalize(wallet: WalletRef, args: FinalizeArgs) -> Result<(), Error> {
let adapter = FileWalletCommAdapter::new();
let mut slate = adapter.receive_tx_async(&args.input)?;
controller::owner_single_use(wallet.clone(), |api| {
if let Err(e) = api.verify_slate_messages(&slate) {
error!("Error validating participant messages: {}", e);
return Err(e);
}
let _ = api.finalize_tx(&mut slate).expect("Finalize failed");
let result = api.post_tx(&slate.tx, args.fluff);
match result {
Ok(_) => {
info!("Transaction sent successfully, check the wallet again for confirmation.");
Ok(())
}
Err(e) => {
error!("Tx not sent: {}", e);
Err(e)
}
}
})?;
Ok(())
}
/// Info command args
pub struct InfoArgs {
pub minimum_confirmations: u64,
}
pub fn info(
wallet: WalletRef,
g_args: &GlobalArgs,
args: InfoArgs,
dark_scheme: bool,
) -> Result<(), Error> {
controller::owner_single_use(wallet.clone(), |api| {
let (validated, wallet_info) =
api.retrieve_summary_info(true, args.minimum_confirmations)?;
display::info(&g_args.account, &wallet_info, validated, dark_scheme);
Ok(())
})?;
Ok(())
}
pub fn outputs(wallet: WalletRef, g_args: &GlobalArgs, dark_scheme: bool) -> Result<(), Error> {
controller::owner_single_use(wallet.clone(), |api| {
let (height, _) = api.node_height()?;
let (validated, outputs) = api.retrieve_outputs(g_args.show_spent, true, None)?;
display::outputs(&g_args.account, height, validated, outputs, dark_scheme)?;
Ok(())
})?;
Ok(())
}
/// Txs command args
pub struct TxsArgs {
pub id: Option<u32>,
}
pub fn txs(
wallet: WalletRef,
g_args: &GlobalArgs,
args: TxsArgs,
dark_scheme: bool,
) -> Result<(), Error> {
controller::owner_single_use(wallet.clone(), |api| {
let (height, _) = api.node_height()?;
let (validated, txs) = api.retrieve_txs(true, args.id, None)?;
let include_status = !args.id.is_some();
display::txs(
&g_args.account,
height,
validated,
txs,
include_status,
dark_scheme,
)?;
// if given a particular transaction id, also get and display associated
// inputs/outputs
if args.id.is_some() {
let (_, outputs) = api.retrieve_outputs(true, false, args.id)?;
display::outputs(&g_args.account, height, validated, outputs, dark_scheme)?;
};
Ok(())
})?;
Ok(())
}
/// Repost
pub struct RepostArgs {
pub id: u32,
pub dump_file: Option<String>,
pub fluff: bool,
}
pub fn repost(wallet: WalletRef, args: RepostArgs) -> Result<(), Error> {
controller::owner_single_use(wallet.clone(), |api| {
let (_, txs) = api.retrieve_txs(true, Some(args.id), None)?;
let stored_tx = txs[0].get_stored_tx();
if stored_tx.is_none() {
error!(
"Transaction with id {} does not have transaction data. Not reposting.",
args.id
);
return Ok(());
}
match args.dump_file {
None => {
if txs[0].confirmed {
error!(
"Transaction with id {} is confirmed. Not reposting.",
args.id
);
return Ok(());
}
api.post_tx(&stored_tx.unwrap(), args.fluff)?;
info!("Reposted transaction at {}", args.id);
return Ok(());
}
Some(f) => {
let mut tx_file = File::create(f.clone())?;
tx_file.write_all(json::to_string(&stored_tx).unwrap().as_bytes())?;
tx_file.sync_all()?;
info!("Dumped transaction data for tx {} to {}", args.id, f);
return Ok(());
}
}
})?;
Ok(())
}
/// Cancel
pub struct CancelArgs {
pub tx_id: Option<u32>,
pub tx_slate_id: Option<Uuid>,
pub tx_id_string: String,
}
pub fn cancel(wallet: WalletRef, args: CancelArgs) -> Result<(), Error> {
controller::owner_single_use(wallet.clone(), |api| {
let result = api.cancel_tx(args.tx_id, args.tx_slate_id);
match result {
Ok(_) => {
info!("Transaction {} Cancelled", args.tx_id_string);
Ok(())
}
Err(e) => {
error!("TX Cancellation failed: {}", e);
Err(e)
}
}
})?;
Ok(())
}
pub fn restore(wallet: WalletRef) -> Result<(), Error> {
controller::owner_single_use(wallet.clone(), |api| {
let result = api.restore();
match result {
Ok(_) => {
info!("Wallet restore complete",);
Ok(())
}
Err(e) => {
error!("Wallet restore failed: {}", e);
error!("Backtrace: {}", e.backtrace().unwrap());
Err(e)
}
}
})?;
Ok(())
}

View file

@ -19,12 +19,12 @@ use adapters::{FileWalletCommAdapter, HTTPWalletCommAdapter, KeybaseWalletCommAd
use api::{ApiServer, BasicAuthMiddleware, Handler, ResponseFuture, Router, TLSConfig};
use core::core;
use core::core::Transaction;
use core::libtx::slate::Slate;
use failure::ResultExt;
use futures::future::{err, ok};
use futures::{Future, Stream};
use hyper::{Body, Request, Response, StatusCode};
use keychain::Keychain;
use core::libtx::slate::Slate;
use libwallet::api::{APIForeign, APIOwner};
use libwallet::types::{
CbData, NodeClient, OutputData, SendTXArgs, TxLogEntry, WalletBackend, WalletInfo,

View file

@ -14,9 +14,10 @@
//! Implementation specific error types
use api;
use keychain;
use core::libtx;
use keychain;
use libwallet;
use std::env;
use std::fmt::{self, Display};
use core::core::transaction;
@ -36,8 +37,8 @@ pub enum ErrorKind {
LibTX(libtx::ErrorKind),
/// LibWallet Error
#[fail(display = "LibWallet Error")]
LibWallet(libwallet::ErrorKind),
#[fail(display = "LibWallet Error: {}", _1)]
LibWallet(libwallet::ErrorKind, String),
/// Keychain error
#[fail(display = "Keychain error")]
@ -80,7 +81,7 @@ pub enum ErrorKind {
DuplicateTransactionId,
/// Wallet seed already exists
#[fail(display = "{}", _0)]
#[fail(display = "Wallet seed file exists: {}", _0)]
WalletSeedExists(String),
/// Wallet seed doesn't exist
@ -88,7 +89,7 @@ pub enum ErrorKind {
WalletSeedDoesntExist,
/// Enc/Decryption Error
#[fail(display = "Enc/Decryption error")]
#[fail(display = "Enc/Decryption error (check password?)")]
Encryption,
/// BIP 39 word list
@ -112,18 +113,24 @@ impl Fail for Error {
impl Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let cause = match self.cause() {
Some(c) => format!("{}", c),
None => String::from("Unknown"),
let show_bt = match env::var("RUST_BACKTRACE") {
Ok(r) => if r == "1" {
true
} else {
false
},
Err(_) => false,
};
let backtrace = match self.backtrace() {
Some(b) => format!("{}", b),
None => String::from("Unknown"),
};
let output = format!(
"{} \n Cause: {} \n Backtrace: {}",
self.inner, cause, backtrace
);
let inner_output = format!("{}", self.inner,);
let backtrace_output = format!("\nBacktrace: {}", backtrace);
let mut output = inner_output.clone();
if show_bt {
output.push_str(&backtrace_output);
}
Display::fmt(&output, f)
}
}
@ -184,7 +191,7 @@ impl From<transaction::Error> for Error {
impl From<libwallet::Error> for Error {
fn from(error: libwallet::Error) -> Error {
Error {
inner: Context::new(ErrorKind::LibWallet(error.kind())),
inner: Context::new(ErrorKind::LibWallet(error.kind(), format!("{}", error))),
}
}
}

View file

@ -48,6 +48,7 @@ extern crate grin_store as store;
extern crate grin_util as util;
mod adapters;
pub mod command;
pub mod controller;
pub mod display;
mod error;

View file

@ -36,9 +36,9 @@ use uuid::Uuid;
use core::core::hash::Hashed;
use core::core::Transaction;
use core::libtx::slate::Slate;
use core::ser;
use keychain::{Identifier, Keychain};
use core::libtx::slate::Slate;
use libwallet::internal::{keys, tx, updater};
use libwallet::types::{
AcctPathMapping, BlockFees, CbData, NodeClient, OutputData, TxLogEntry, TxWrapper,

View file

@ -14,6 +14,7 @@
//! Error types for libwallet
use std::env;
use std::fmt::{self, Display};
use std::io;
@ -21,8 +22,8 @@ use failure::{Backtrace, Context, Fail};
use core;
use core::core::transaction;
use keychain;
use core::libtx;
use keychain;
/// Error definition
#[derive(Debug, Fail)]
@ -195,18 +196,24 @@ pub enum ErrorKind {
impl Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let cause = match self.cause() {
Some(c) => format!("{}", c),
None => String::from("Unknown"),
let show_bt = match env::var("RUST_BACKTRACE") {
Ok(r) => if r == "1" {
true
} else {
false
},
Err(_) => false,
};
let backtrace = match self.backtrace() {
Some(b) => format!("{}", b),
None => String::from("Unknown"),
};
let output = format!(
"{} \n Cause: {} \n Backtrace: {}",
self.inner, cause, backtrace
);
let inner_output = format!("{}", self.inner,);
let backtrace_output = format!("\n Backtrace: {}", backtrace);
let mut output = inner_output.clone();
if show_bt {
output.push_str(&backtrace_output);
}
Display::fmt(&output, f)
}
}
@ -216,6 +223,13 @@ impl Error {
pub fn kind(&self) -> ErrorKind {
self.inner.get_context().clone()
}
/// get cause string
pub fn cause_string(&self) -> String {
match self.cause() {
Some(k) => format!("{}", k),
None => format!("Unknown"),
}
}
/// get cause
pub fn cause(&self) -> Option<&Fail> {
self.inner.cause()

View file

@ -14,8 +14,8 @@
//! Functions to restore a wallet's outputs from just the master seed
use core::global;
use keychain::{ExtKeychain, Identifier, Keychain};
use core::libtx::proof;
use keychain::{ExtKeychain, Identifier, Keychain};
use libwallet::internal::keys;
use libwallet::types::*;
use libwallet::Error;

View file

@ -14,8 +14,8 @@
//! Selection of inputs for building transactions
use keychain::{Identifier, Keychain};
use core::libtx::{build, slate::Slate, tx_fee};
use keychain::{Identifier, Keychain};
use libwallet::error::{Error, ErrorKind};
use libwallet::internal::keys;
use libwallet::types::*;

View file

@ -17,9 +17,9 @@
use util;
use uuid::Uuid;
use core::libtx::slate::Slate;
use core::ser;
use keychain::{Identifier, Keychain};
use core::libtx::slate::Slate;
use libwallet::internal::{selection, updater};
use libwallet::types::{Context, NodeClient, TxLogEntryType, WalletBackend};
use libwallet::{Error, ErrorKind};
@ -233,8 +233,8 @@ where
#[cfg(test)]
mod test {
use keychain::{ExtKeychain, ExtKeychainPath, Keychain};
use core::libtx::build;
use keychain::{ExtKeychain, ExtKeychainPath, Keychain};
#[test]
// demonstrate that input.commitment == referenced output.commitment

View file

@ -21,9 +21,9 @@ use uuid::Uuid;
use core::consensus::reward;
use core::core::{Output, TxKernel};
use core::libtx::reward;
use core::{global, ser};
use keychain::{Identifier, Keychain};
use core::libtx::reward;
use libwallet;
use libwallet::error::{Error, ErrorKind};
use libwallet::internal::keys;

View file

@ -41,8 +41,8 @@ use core::global::{set_mining_mode, ChainTypes};
use core::{pow, ser};
use keychain::Keychain;
use util::secp::pedersen;
use core::libtx::slate::Slate;
use util::secp::pedersen;
use wallet::libwallet::types::*;
use wallet::{controller, libwallet, WalletCommAdapter, WalletConfig};

View file

@ -21,10 +21,10 @@ extern crate rand;
extern crate uuid;
use core::core::transaction::kernel_sig_msg;
use core::libtx::{aggsig, proof};
use keychain::{BlindSum, BlindingFactor, ExtKeychain, Keychain};
use util::secp;
use util::secp::key::{PublicKey, SecretKey};
use core::libtx::{aggsig, proof};
use wallet::libwallet::types::Context;
use wallet::{EncryptedWalletSeed, WalletSeed};

View file

@ -34,8 +34,8 @@ use std::time::Duration;
use core::global;
use core::global::ChainTypes;
use keychain::ExtKeychain;
use core::libtx::slate::Slate;
use keychain::ExtKeychain;
use wallet::{libwallet, FileWalletCommAdapter};
fn clean_output_dir(test_dir: &str) {

View file

@ -34,8 +34,8 @@ use std::time::Duration;
use core::global;
use core::global::ChainTypes;
use keychain::{ExtKeychain, Identifier, Keychain};
use core::libtx::slate::Slate;
use keychain::{ExtKeychain, Identifier, Keychain};
use wallet::libwallet;
use wallet::libwallet::types::AcctPathMapping;

View file

@ -34,8 +34,8 @@ use std::time::Duration;
use core::global;
use core::global::ChainTypes;
use keychain::ExtKeychain;
use core::libtx::slate::Slate;
use keychain::ExtKeychain;
use wallet::libwallet;
use wallet::libwallet::types::OutputStatus;