grin/src/bin/cmd/wallet.rs
Yeastplume 5ba163fa66
Provide wallet 'plugin' architecture ()
* remove receive_coinbase fn from wallet to wallet client

* start moving clients into separate mod

* rustfmt

* move wallet client + start listener into wallet 'clients'

* rustfmt

* refactor API to make it more modular and completely decouple sending

* rustfmt

* further decouple API from sending methods

* rustfmt

* remove wallet to wallet client trait

* rustfmt

* rename / refactor client + adapters

* rustfmt

* add adapter concept

* add midding node_clients dir

* add file and null adapters

* rustfmt

* remove receive from owner api

* factor out receiving slates into trait

* rustfmt

* adding listen trait

* rustfmt

* change listener to use trait

* rustfmt

* add test for file-based exchange

* replace http api send command

* rustfmt

* move controller out of libwallet and into top-level wallet dir

* rustfmt

* add moved controller
2018-11-19 19:47:40 +00:00

641 lines
18 KiB
Rust

// 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 clap::ArgMatches;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
/// Wallet commands processing
use std::thread;
use std::time::Duration;
use api::TLSConfig;
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, LMDBBackend,
NullWalletCommAdapter, WalletConfig, WalletSeed,
};
use keychain;
use servers::start_webwallet_server;
use util::file::get_first_line;
pub fn _init_wallet_seed(wallet_config: WalletConfig) {
if let Err(_) = WalletSeed::from_file(&wallet_config) {
WalletSeed::init_file(&wallet_config).expect("Failed to create wallet seed file.");
};
}
pub fn seed_exists(wallet_config: WalletConfig) -> bool {
let mut data_file_dir = PathBuf::new();
data_file_dir.push(wallet_config.data_file_dir);
data_file_dir.push(grin_wallet::SEED_FILE);
if data_file_dir.exists() {
true
} else {
false
}
}
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;
if let Some(t) = wallet_config.chain_type.clone() {
global::set_mining_mode(t);
}
if wallet_args.is_present("external") {
wallet_config.api_listen_interface = "0.0.0.0".to_string();
}
if let Some(dir) = wallet_args.value_of("dir") {
wallet_config.data_file_dir = dir.to_string().clone();
}
if let Some(sa) = wallet_args.value_of("api_server_address") {
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());
// Derive the keychain based on seed from seed file and specified passphrase.
// Generate the initial wallet seed if we are running "wallet init".
if let ("init", Some(_)) = wallet_args.subcommand() {
WalletSeed::init_file(&wallet_config).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(), "", 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;
}
let passphrase = match wallet_args.value_of("pass") {
None => {
error!("Failed to read passphrase.");
return 1;
}
Some(p) => p,
};
let account = match wallet_args.value_of("account") {
None => {
error!("Failed to read account.");
return 1;
}
Some(p) => p,
};
// 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 = HTTPWalletCommAdapter::new();
adapter
.listen(
params,
wallet_config.clone(),
passphrase,
account,
node_api_secret.clone(),
).unwrap_or_else(|e| {
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(),
);
// 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(),
);
// 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(),
);
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.new_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 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",
);
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(),
_ => 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))?;
Ok(())
})?;
}
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, fluff);
match result {
Ok(_) => {
info!("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 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))?;
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)?;
let _ = api.finalize_tx(&mut slate).expect("Finalize failed");
let result = api.post_tx(&slate, fluff);
match result {
Ok(_) => {
info!("Transaction sent successfully, check the wallet again for confirmation.");
Ok(())
}
Err(e) => {
error!("Tx not sent: {}", e);
Err(e)
}
}
}
("burn", Some(send_args)) => {
let amount = send_args
.value_of("amount")
.expect("Amount to burn required");
let amount = core::amount_from_hr_string(amount)
.expect("Could not parse amount as number with optional decimal point.");
let minimum_confirmations: u64 = send_args
.value_of("minimum_confirmations")
.unwrap()
.parse()
.expect("Could not parse minimum_confirmations as a whole number.");
let max_outputs = 500;
api.issue_burn_tx(amount, minimum_confirmations, max_outputs)
.unwrap_or_else(|e| {
panic!("Error burning tx: {:?} Config: {:?}", e, wallet_config)
});
Ok(())
}
("info", Some(_)) => {
let (validated, wallet_info) = api.retrieve_summary_info(true).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");
match dump_file {
None => {
let result = api.post_stored_tx(tx_id, fluff);
match result {
Ok(_) => {
info!("Reposted transaction at {}", tx_id);
Ok(())
}
Err(e) => {
error!("Transaction reposting failed: {}", e);
Err(e)
}
}
}
Some(f) => {
let result = api.dump_stored_tx(tx_id, true, f);
match result {
Ok(_) => {
warn!("Dumped transaction data for tx {} to {}", tx_id, f);
Ok(())
}
Err(e) => {
error!("Transaction reposting failed: {}", e);
Err(e)
}
}
}
}
}
("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());
}
}
});
// we need to give log output a chance to catch up before exiting
thread::sleep(Duration::from_millis(100));
if let Err(e) = res {
println!("Wallet command failed: {}", e);
1
} else {
0
}
}