From 82ed2806254d34c6d7bbb93df53b86162d89519f Mon Sep 17 00:00:00 2001 From: Yeastplume Date: Wed, 30 May 2018 17:48:32 +0100 Subject: [PATCH] [WIP] Factoring out wallet traits and continued wallet library work (#1096) * rename wallet libs * rename transaction.rs to slate.rs * rename transaction.rs to slate.rs * move some types into libwallet/types * rustfmt * rename libtransaction libtx * rename libtransaction libtx * change types.rs to file_wallet * rustfmt * rename WalletData to FileWallet * refactoring WalletBackend types out * rustfmt * All compiling, at least * rustfmt * fix FileWallet paths to persist * rustfmt * re-ignore wallet integration test --- chain/tests/data_file_integrity.rs | 10 +- chain/tests/mine_simple_chain.rs | 10 +- chain/tests/store_indices.rs | 8 +- chain/tests/test_coinbase_maturity.rs | 16 +- core/tests/block.rs | 14 +- core/tests/common/mod.rs | 6 +- core/tests/core.rs | 10 +- core/tests/transaction.rs | 2 +- pool/tests/graph.rs | 4 +- pool/tests/pool.rs | 2 +- servers/src/mining/mine_block.rs | 2 +- servers/tests/framework/mod.rs | 23 +- src/bin/grin.rs | 39 +- wallet/src/checker.rs | 110 +-- wallet/src/client.rs | 4 +- wallet/src/file_wallet.rs | 465 ++++++++++ wallet/src/handlers.rs | 30 +- wallet/src/info.rs | 28 +- wallet/src/lib.rs | 8 +- wallet/src/{libwallet => libtx}/aggsig.rs | 2 +- wallet/src/{libwallet => libtx}/build.rs | 2 +- wallet/src/{libwallet => libtx}/error.rs | 0 wallet/src/{grinwallet => libtx}/mod.rs | 32 +- wallet/src/{libwallet => libtx}/proof.rs | 2 +- wallet/src/libtx/reward.rs | 79 ++ .../transaction.rs => libtx/slate.rs} | 17 +- wallet/src/{grinwallet => libwallet}/keys.rs | 36 +- wallet/src/libwallet/mod.rs | 18 +- wallet/src/libwallet/reward.rs | 4 +- .../{grinwallet => libwallet}/selection.rs | 132 +-- .../{grinwallet => libwallet}/sigcontext.rs | 2 +- wallet/src/libwallet/types.rs | 454 ++++++++++ wallet/src/outputs.rs | 17 +- wallet/src/receiver.rs | 112 ++- wallet/src/restore.rs | 73 +- wallet/src/sender.rs | 64 +- wallet/src/server.rs | 25 +- wallet/src/types.rs | 803 ------------------ wallet/tests/common/mod.rs | 45 +- wallet/tests/libwallet.rs | 6 +- wallet/tests/transaction.rs | 45 +- 41 files changed, 1512 insertions(+), 1249 deletions(-) create mode 100644 wallet/src/file_wallet.rs rename wallet/src/{libwallet => libtx}/aggsig.rs (99%) rename wallet/src/{libwallet => libtx}/build.rs (99%) rename wallet/src/{libwallet => libtx}/error.rs (100%) rename wallet/src/{grinwallet => libtx}/mod.rs (53%) rename wallet/src/{libwallet => libtx}/proof.rs (99%) create mode 100644 wallet/src/libtx/reward.rs rename wallet/src/{libwallet/transaction.rs => libtx/slate.rs} (96%) rename wallet/src/{grinwallet => libwallet}/keys.rs (54%) rename wallet/src/{grinwallet => libwallet}/selection.rs (77%) rename wallet/src/{grinwallet => libwallet}/sigcontext.rs (99%) create mode 100644 wallet/src/libwallet/types.rs delete mode 100644 wallet/src/types.rs diff --git a/chain/tests/data_file_integrity.rs b/chain/tests/data_file_integrity.rs index 37bb677c1..2937760ec 100644 --- a/chain/tests/data_file_integrity.rs +++ b/chain/tests/data_file_integrity.rs @@ -26,14 +26,14 @@ use std::sync::Arc; use chain::Chain; use chain::types::*; -use core::core::{Block, BlockHeader, Transaction}; use core::core::target::Difficulty; -use core::{consensus, genesis}; +use core::core::{Block, BlockHeader, Transaction}; use core::global; use core::global::ChainTypes; +use core::{consensus, genesis}; use keychain::Keychain; -use wallet::libwallet; +use wallet::libtx; use core::pow; @@ -75,7 +75,7 @@ fn data_files() { let prev = chain.head_header().unwrap(); let difficulty = consensus::next_difficulty(chain.difficulty_iter()).unwrap(); let pk = keychain.derive_key_id(n as u32).unwrap(); - let reward = libwallet::reward::output(&keychain, &pk, 0, prev.height).unwrap(); + let reward = libtx::reward::output(&keychain, &pk, 0, prev.height).unwrap(); let mut b = core::core::Block::new(&prev, vec![], difficulty.clone(), reward).unwrap(); b.header.timestamp = prev.timestamp + time::Duration::seconds(60); @@ -159,7 +159,7 @@ fn _prepare_block_nosum( let key_id = kc.derive_key_id(diff as u32).unwrap(); let fees = txs.iter().map(|tx| tx.fee()).sum(); - let reward = libwallet::reward::output(&kc, &key_id, fees, prev.height).unwrap(); + let reward = libtx::reward::output(&kc, &key_id, fees, prev.height).unwrap(); let mut b = match core::core::Block::new(prev, txs, Difficulty::from_num(diff), reward) { Err(e) => panic!("{:?}", e), Ok(b) => b, diff --git a/chain/tests/mine_simple_chain.rs b/chain/tests/mine_simple_chain.rs index a73eb68b2..543733ab1 100644 --- a/chain/tests/mine_simple_chain.rs +++ b/chain/tests/mine_simple_chain.rs @@ -26,13 +26,13 @@ use std::sync::Arc; use chain::Chain; use chain::types::*; -use core::core::{Block, BlockHeader, OutputFeatures, OutputIdentifier, Transaction}; +use core::consensus; use core::core::hash::Hashed; use core::core::target::Difficulty; -use core::consensus; +use core::core::{Block, BlockHeader, OutputFeatures, OutputIdentifier, Transaction}; use core::global; use core::global::ChainTypes; -use wallet::libwallet::{self, build}; +use wallet::libtx::{self, build}; use keychain::Keychain; @@ -64,7 +64,7 @@ fn mine_empty_chain() { let prev = chain.head_header().unwrap(); let difficulty = consensus::next_difficulty(chain.difficulty_iter()).unwrap(); let pk = keychain.derive_key_id(n as u32).unwrap(); - let reward = libwallet::reward::output(&keychain, &pk, 0, prev.height).unwrap(); + let reward = libtx::reward::output(&keychain, &pk, 0, prev.height).unwrap(); let mut b = core::core::Block::new(&prev, vec![], difficulty.clone(), reward).unwrap(); b.header.timestamp = prev.timestamp + time::Duration::seconds(60); @@ -413,7 +413,7 @@ fn prepare_block_nosum( let key_id = kc.derive_key_id(diff as u32).unwrap(); let fees = txs.iter().map(|tx| tx.fee()).sum(); - let reward = libwallet::reward::output(&kc, &key_id, fees, prev.height).unwrap(); + let reward = libtx::reward::output(&kc, &key_id, fees, prev.height).unwrap(); let mut b = match core::core::Block::new(prev, txs, Difficulty::from_num(diff), reward) { Err(e) => panic!("{:?}", e), Ok(b) => b, diff --git a/chain/tests/store_indices.rs b/chain/tests/store_indices.rs index 4e9d39a5c..b35ae88b3 100644 --- a/chain/tests/store_indices.rs +++ b/chain/tests/store_indices.rs @@ -22,16 +22,16 @@ extern crate rand; use std::fs; use chain::{ChainStore, Tip}; -use core::core::hash::Hashed; use core::core::Block; use core::core::BlockHeader; +use core::core::hash::Hashed; use core::core::target::Difficulty; -use keychain::Keychain; use core::global; use core::global::ChainTypes; use core::pow; +use keychain::Keychain; -use wallet::libwallet; +use wallet::libtx; fn clean_output_dir(dir_name: &str) { let _ = fs::remove_dir_all(dir_name); @@ -56,7 +56,7 @@ fn test_various_store_indices() { .setup_height(&genesis.header, &Tip::new(genesis.hash())) .unwrap(); - let reward = libwallet::reward::output(&keychain, &key_id, 0, 1).unwrap(); + let reward = libtx::reward::output(&keychain, &key_id, 0, 1).unwrap(); let block = Block::new(&genesis.header, vec![], Difficulty::one(), reward).unwrap(); let block_hash = block.hash(); diff --git a/chain/tests/test_coinbase_maturity.rs b/chain/tests/test_coinbase_maturity.rs index a4ae32564..62997b244 100644 --- a/chain/tests/test_coinbase_maturity.rs +++ b/chain/tests/test_coinbase_maturity.rs @@ -24,16 +24,16 @@ use std::fs; use std::sync::Arc; use chain::types::*; -use wallet::libwallet::build; +use core::consensus; +use core::core::OutputIdentifier; use core::core::target::Difficulty; use core::core::transaction; -use core::core::OutputIdentifier; -use core::consensus; use core::global; use core::global::ChainTypes; +use wallet::libtx::build; use keychain::Keychain; -use wallet::libwallet; +use wallet::libtx; use core::pow; @@ -64,7 +64,7 @@ fn test_coinbase_maturity() { let key_id3 = keychain.derive_key_id(3).unwrap(); let key_id4 = keychain.derive_key_id(4).unwrap(); - let reward = libwallet::reward::output(&keychain, &key_id1, 0, prev.height).unwrap(); + let reward = libtx::reward::output(&keychain, &key_id1, 0, prev.height).unwrap(); let mut block = core::core::Block::new(&prev, vec![], Difficulty::one(), reward).unwrap(); block.header.timestamp = prev.timestamp + time::Duration::seconds(60); @@ -118,7 +118,7 @@ fn test_coinbase_maturity() { let txs = vec![&coinbase_txn]; let fees = txs.iter().map(|tx| tx.fee()).sum(); - let reward = libwallet::reward::output(&keychain, &key_id3, fees, prev.height).unwrap(); + let reward = libtx::reward::output(&keychain, &key_id3, fees, prev.height).unwrap(); let mut block = core::core::Block::new(&prev, txs, Difficulty::one(), reward).unwrap(); block.header.timestamp = prev.timestamp + time::Duration::seconds(60); @@ -144,7 +144,7 @@ fn test_coinbase_maturity() { let keychain = Keychain::from_random_seed().unwrap(); let pk = keychain.derive_key_id(1).unwrap(); - let reward = libwallet::reward::output(&keychain, &pk, 0, prev.height).unwrap(); + let reward = libtx::reward::output(&keychain, &pk, 0, prev.height).unwrap(); let mut block = core::core::Block::new(&prev, vec![], Difficulty::one(), reward).unwrap(); block.header.timestamp = prev.timestamp + time::Duration::seconds(60); @@ -175,7 +175,7 @@ fn test_coinbase_maturity() { let txs = vec![&coinbase_txn]; let fees = txs.iter().map(|tx| tx.fee()).sum(); - let reward = libwallet::reward::output(&keychain, &key_id4, fees, prev.height).unwrap(); + let reward = libtx::reward::output(&keychain, &key_id4, fees, prev.height).unwrap(); let mut block = core::core::Block::new(&prev, vec![&coinbase_txn], Difficulty::one(), reward).unwrap(); diff --git a/core/tests/block.rs b/core/tests/block.rs index bbdfeef5b..9706c82ad 100644 --- a/core/tests/block.rs +++ b/core/tests/block.rs @@ -19,17 +19,17 @@ extern crate grin_wallet as wallet; pub mod common; -use grin_core::core::{Block, BlockHeader, CompactBlock, KernelFeatures, OutputFeatures}; -use grin_core::core::hash::Hashed; -use grin_core::core::block::Error; -use grin_core::core::id::{ShortId, ShortIdentifiable}; -use wallet::libwallet::build::{self, input, output, with_fee}; use common::{new_block, tx1i2o, tx2i1o, txspend1i1o}; -use keychain::Keychain; use grin_core::consensus::{BLOCK_OUTPUT_WEIGHT, MAX_BLOCK_WEIGHT}; -use grin_core::ser; +use grin_core::core::block::Error; +use grin_core::core::hash::Hashed; +use grin_core::core::id::{ShortId, ShortIdentifiable}; +use grin_core::core::{Block, BlockHeader, CompactBlock, KernelFeatures, OutputFeatures}; use grin_core::global; +use grin_core::ser; +use keychain::Keychain; use std::time::Instant; +use wallet::libtx::build::{self, input, output, with_fee}; use util::{secp, secp_static}; diff --git a/core/tests/common/mod.rs b/core/tests/common/mod.rs index 87b958097..4527e4899 100644 --- a/core/tests/common/mod.rs +++ b/core/tests/common/mod.rs @@ -19,12 +19,12 @@ extern crate grin_keychain as keychain; extern crate grin_util as util; extern crate grin_wallet as wallet; +use grin_core::core::Transaction; use grin_core::core::block::{Block, BlockHeader}; use grin_core::core::target::Difficulty; -use grin_core::core::Transaction; use keychain::{Identifier, Keychain}; -use wallet::libwallet::build::{self, input, output, with_fee}; -use wallet::libwallet::reward; +use wallet::libtx::build::{self, input, output, with_fee}; +use wallet::libtx::reward; // utility producing a transaction with 2 inputs and a single outputs pub fn tx2i1o() -> Transaction { diff --git a/core/tests/core.rs b/core/tests/core.rs index 055a212b9..871146cdf 100644 --- a/core/tests/core.rs +++ b/core/tests/core.rs @@ -20,17 +20,17 @@ extern crate grin_wallet as wallet; pub mod common; -use grin_core::core::hash::{Hashed, ZERO_HASH}; +use common::{new_block, tx1i1o, tx1i2o, tx2i1o}; use grin_core::core::block::BlockHeader; +use grin_core::core::block::Error::KernelLockHeight; +use grin_core::core::hash::{Hashed, ZERO_HASH}; use grin_core::core::{aggregate, aggregate_with_cut_through, deaggregate, KernelFeatures, Output, Transaction}; -use wallet::libwallet::build::{self, initial_tx, input, output, with_excess, with_fee, - with_lock_height}; -use grin_core::core::block::Error::KernelLockHeight; use grin_core::ser; use keychain::Keychain; use util::{secp_static, static_secp_instance}; -use common::{new_block, tx1i1o, tx1i2o, tx2i1o}; +use wallet::libtx::build::{self, initial_tx, input, output, with_excess, with_fee, + with_lock_height}; #[test] fn simple_tx_ser() { diff --git a/core/tests/transaction.rs b/core/tests/transaction.rs index e9cc172e6..6e83e3a9a 100644 --- a/core/tests/transaction.rs +++ b/core/tests/transaction.rs @@ -24,7 +24,7 @@ use grin_core::core::{Output, OutputFeatures}; use grin_core::ser; use keychain::Keychain; use util::secp; -use wallet::libwallet::proof; +use wallet::libtx::proof; #[test] fn test_output_ser_deser() { diff --git a/pool/tests/graph.rs b/pool/tests/graph.rs index 0dcf878aa..96b70dc24 100644 --- a/pool/tests/graph.rs +++ b/pool/tests/graph.rs @@ -21,10 +21,10 @@ extern crate grin_wallet as wallet; extern crate rand; -use keychain::Keychain; use core::core::OutputFeatures; use core::core::transaction::ProofMessageElements; -use wallet::libwallet::proof; +use keychain::Keychain; +use wallet::libtx::proof; #[test] fn test_add_entry() { diff --git a/pool/tests/pool.rs b/pool/tests/pool.rs index 58b3ef83f..e401d7cc7 100644 --- a/pool/tests/pool.rs +++ b/pool/tests/pool.rs @@ -42,7 +42,7 @@ use std::sync::{Arc, RwLock}; use types::PoolError::InvalidTx; use keychain::Keychain; -use wallet::libwallet::{build, proof, reward}; +use wallet::libtx::{build, proof, reward}; use pool::types::*; diff --git a/servers/src/mining/mine_block.rs b/servers/src/mining/mine_block.rs index 3396cf0bc..d4ad91875 100644 --- a/servers/src/mining/mine_block.rs +++ b/servers/src/mining/mine_block.rs @@ -226,7 +226,7 @@ fn burn_reward(block_fees: BlockFees) -> Result<(core::Output, core::TxKernel, B let keychain = Keychain::from_random_seed().unwrap(); let key_id = keychain.derive_key_id(1).unwrap(); let (out, kernel) = - wallet::libwallet::reward::output(&keychain, &key_id, block_fees.fees, block_fees.height) + wallet::libtx::reward::output(&keychain, &key_id, block_fees.fees, block_fees.height) .unwrap(); Ok((out, kernel, block_fees)) } diff --git a/servers/tests/framework/mod.rs b/servers/tests/framework/mod.rs index 7fad52ee0..108e8ac8c 100644 --- a/servers/tests/framework/mod.rs +++ b/servers/tests/framework/mod.rs @@ -29,7 +29,7 @@ use std::sync::{Arc, Mutex}; use std::thread; use std::time; -use wallet::WalletConfig; +use wallet::{FileWallet, WalletConfig}; /// Just removes all results from previous runs pub fn clean_all_output(test_name_dir: &str) { @@ -275,7 +275,14 @@ impl LocalServerContainer { .derive_keychain("grin_test") .expect("Failed to derive keychain from seed file and passphrase."); - wallet::server::start_rest_apis(self.wallet_config.clone(), keychain); + let wallet = FileWallet::new(self.wallet_config.clone(), keychain).unwrap_or_else(|e| { + panic!( + "Error creating wallet: {:?} Config: {:?}", + e, self.wallet_config + ) + }); + + wallet::server::start_rest_apis(wallet, &self.wallet_config.api_listen_addr()); self.wallet_is_running = true; } @@ -294,8 +301,9 @@ impl LocalServerContainer { let keychain = wallet_seed .derive_keychain("grin_test") .expect("Failed to derive keychain from seed file and passphrase."); - - wallet::retrieve_info(config, &keychain) + let mut wallet = FileWallet::new(config.clone(), keychain) + .unwrap_or_else(|e| panic!("Error creating wallet: {:?} Config: {:?}", e, config)); + wallet::retrieve_info(&mut wallet) } pub fn send_amount_to( @@ -312,13 +320,14 @@ impl LocalServerContainer { let wallet_seed = wallet::WalletSeed::from_file(config).expect("Failed to read wallet seed file."); - let mut keychain = wallet_seed + let keychain = wallet_seed .derive_keychain("grin_test") .expect("Failed to derive keychain from seed file and passphrase."); let max_outputs = 500; + let mut wallet = FileWallet::new(config.clone(), keychain) + .unwrap_or_else(|e| panic!("Error creating wallet: {:?} Config: {:?}", e, config)); let result = wallet::issue_send_tx( - config, - &mut keychain, + &mut wallet, amount, minimum_confirmations, dest.to_string(), diff --git a/src/bin/grin.rs b/src/bin/grin.rs index e78a41051..1dad0e5d9 100644 --- a/src/bin/grin.rs +++ b/src/bin/grin.rs @@ -37,20 +37,21 @@ extern crate grin_wallet as wallet; mod client; pub mod tui; -use std::thread; -use std::sync::Arc; -use std::time::Duration; use std::env::current_dir; use std::process::exit; +use std::sync::Arc; +use std::thread; +use std::time::Duration; use clap::{App, Arg, ArgMatches, SubCommand}; use daemonize::Daemonize; use config::GlobalConfig; -use core::global; use core::core::amount_to_hr_string; -use util::{init_logger, LoggingConfig, LOGGER}; +use core::global; use tui::ui; +use util::{init_logger, LoggingConfig, LOGGER}; +use wallet::FileWallet; // include build information pub mod built_info { @@ -422,7 +423,10 @@ fn server_command(server_args: Option<&ArgMatches>, mut global_config: GlobalCon let _ = thread::Builder::new() .name("wallet_listener".to_string()) .spawn(move || { - wallet::server::start_rest_apis(wallet_config, keychain); + let wallet = FileWallet::new(wallet_config.clone(), keychain).unwrap_or_else(|e| { + panic!("Error creating wallet: {:?} Config: {:?}", e, wallet_config) + }); + wallet::server::start_rest_apis(wallet, &wallet_config.api_listen_addr()); }); } @@ -530,16 +534,18 @@ fn wallet_command(wallet_args: &ArgMatches, global_config: GlobalConfig) { let passphrase = wallet_args .value_of("pass") .expect("Failed to read passphrase."); - let mut keychain = wallet_seed + let keychain = wallet_seed .derive_keychain(&passphrase) .expect("Failed to derive keychain from seed file and passphrase."); + let mut wallet = FileWallet::new(wallet_config.clone(), keychain) + .unwrap_or_else(|e| panic!("Error creating wallet: {:?} Config: {:?}", e, wallet_config)); 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(); } - wallet::server::start_rest_apis(wallet_config, keychain); + wallet::server::start_rest_apis(wallet, &wallet_config.api_listen_addr()); } ("send", Some(send_args)) => { let amount = send_args @@ -564,8 +570,7 @@ fn wallet_command(wallet_args: &ArgMatches, global_config: GlobalConfig) { } let max_outputs = 500; let result = wallet::issue_send_tx( - &wallet_config, - &mut keychain, + &mut wallet, amount, minimum_confirmations, dest.to_string(), @@ -618,22 +623,16 @@ fn wallet_command(wallet_args: &ArgMatches, global_config: GlobalConfig) { .parse() .expect("Could not parse minimum_confirmations as a whole number."); let max_outputs = 500; - wallet::issue_burn_tx( - &wallet_config, - &keychain, - amount, - minimum_confirmations, - max_outputs, - ).unwrap(); + wallet::issue_burn_tx(&mut wallet, amount, minimum_confirmations, max_outputs).unwrap(); } ("info", Some(_)) => { - wallet::show_info(&wallet_config, &keychain); + wallet::show_info(&mut wallet); } ("outputs", Some(_)) => { - wallet::show_outputs(&wallet_config, &keychain, show_spent); + wallet::show_outputs(&mut wallet, show_spent); } ("restore", Some(_)) => { - let _ = wallet::restore(&wallet_config, &keychain); + let _ = wallet::restore(&mut wallet); } _ => panic!("Unknown wallet command, use 'grin help wallet' for details"), } diff --git a/wallet/src/checker.rs b/wallet/src/checker.rs index 9120f22a8..2a4ace0aa 100644 --- a/wallet/src/checker.rs +++ b/wallet/src/checker.rs @@ -20,29 +20,31 @@ use std::collections::HashMap; use std::collections::hash_map::Entry; use api; -use keychain::{Identifier, Keychain}; -use types::*; +use keychain::Identifier; +use libwallet::types::*; use util; use util::LOGGER; use util::secp::pedersen; -pub fn refresh_outputs(config: &WalletConfig, keychain: &Keychain) -> Result<(), Error> { - let tip = get_tip_from_node(config)?; - refresh_output_state(config, keychain, &tip)?; - refresh_missing_block_hashes(config, keychain, &tip)?; +pub fn refresh_outputs(wallet: &mut T) -> Result<(), Error> +where + T: WalletBackend, +{ + let tip = get_tip_from_node(&wallet.node_url())?; + refresh_output_state(wallet, &tip)?; + refresh_missing_block_hashes(wallet, &tip)?; Ok(()) } // TODO - this might be slow if we have really old outputs that have never been // refreshed -fn refresh_missing_block_hashes( - config: &WalletConfig, - keychain: &Keychain, - tip: &api::Tip, -) -> Result<(), Error> { +fn refresh_missing_block_hashes(wallet: &mut T, tip: &api::Tip) -> Result<(), Error> +where + T: WalletBackend, +{ // build a local map of wallet outputs keyed by commit // and a list of outputs we want to query the node for - let wallet_outputs = map_wallet_outputs_missing_block(config, keychain)?; + let wallet_outputs = map_wallet_outputs_missing_block(wallet)?; // nothing to do so return (otherwise we hit the api with a monster query...) if wallet_outputs.is_empty() { @@ -69,7 +71,7 @@ fn refresh_missing_block_hashes( for mut query_chunk in id_params.chunks(1000) { let url = format!( "{}/v1/chain/outputs/byheight?{}", - config.check_node_api_http_addr, + wallet.node_url(), [&height_params, query_chunk].concat().join("&"), ); @@ -94,10 +96,10 @@ fn refresh_missing_block_hashes( // the corresponding api output (if it exists) // and refresh it in-place in the wallet. // Note: minimizing the time we spend holding the wallet lock. - WalletData::with_wallet(&config.data_file_dir, |wallet_data| { + wallet.with_wallet(|wallet_data| { for commit in wallet_outputs.keys() { let id = wallet_outputs.get(&commit).unwrap(); - if let Entry::Occupied(mut output) = wallet_data.outputs.entry(id.to_hex()) { + if let Entry::Occupied(mut output) = wallet_data.outputs().entry(id.to_hex()) { if let Some(b) = api_blocks.get(&commit) { let output = output.get_mut(); output.block = Some(BlockIdentifier::from_hex(&b.hash).unwrap()); @@ -113,16 +115,20 @@ fn refresh_missing_block_hashes( /// build a local map of wallet outputs keyed by commit /// and a list of outputs we want to query the node for -pub fn map_wallet_outputs( - config: &WalletConfig, - keychain: &Keychain, -) -> Result, Error> { +pub fn map_wallet_outputs( + wallet: &mut T, +) -> Result, Error> +where + T: WalletBackend, +{ let mut wallet_outputs: HashMap = HashMap::new(); - let _ = WalletData::read_wallet(&config.data_file_dir, |wallet_data| { + let _ = wallet.read_wallet(|wallet_data| { + let keychain = wallet_data.keychain().clone(); + let root_key_id = keychain.root_key_id().clone(); let unspents = wallet_data - .outputs + .outputs() .values() - .filter(|x| x.root_key_id == keychain.root_key_id() && x.status != OutputStatus::Spent); + .filter(|x| x.root_key_id == root_key_id && x.status != OutputStatus::Spent); for out in unspents { let commit = keychain .commit_with_key_index(out.value, out.n_child) @@ -136,14 +142,17 @@ pub fn map_wallet_outputs( /// As above, but only return unspent outputs with missing block hashes /// and a list of outputs we want to query the node for -pub fn map_wallet_outputs_missing_block( - config: &WalletConfig, - keychain: &Keychain, -) -> Result, Error> { +pub fn map_wallet_outputs_missing_block( + wallet: &mut T, +) -> Result, Error> +where + T: WalletBackend, +{ let mut wallet_outputs: HashMap = HashMap::new(); - let _ = WalletData::read_wallet(&config.data_file_dir, |wallet_data| { - for out in wallet_data.outputs.values().filter(|x| { - x.root_key_id == keychain.root_key_id() && x.block.is_none() + let _ = wallet.read_wallet(|wallet_data| { + let keychain = wallet_data.keychain().clone(); + for out in wallet_data.outputs().clone().values().filter(|x| { + x.root_key_id == wallet_data.keychain().root_key_id() && x.block.is_none() && x.status == OutputStatus::Unspent }) { let commit = keychain @@ -157,18 +166,21 @@ pub fn map_wallet_outputs_missing_block( } /// Apply refreshed API output data to the wallet -pub fn apply_api_outputs( - config: &WalletConfig, +pub fn apply_api_outputs( + wallet: &mut T, wallet_outputs: &HashMap, api_outputs: &HashMap, -) -> Result<(), Error> { +) -> Result<(), Error> +where + T: WalletBackend, +{ // now for each commit, find the output in the wallet and the corresponding // api output (if it exists) and refresh it in-place in the wallet. // Note: minimizing the time we spend holding the wallet lock. - WalletData::with_wallet(&config.data_file_dir, |wallet_data| { + wallet.with_wallet(|wallet_data| { for commit in wallet_outputs.keys() { let id = wallet_outputs.get(&commit).unwrap(); - if let Entry::Occupied(mut output) = wallet_data.outputs.entry(id.to_hex()) { + if let Entry::Occupied(mut output) = wallet_data.outputs().entry(id.to_hex()) { match api_outputs.get(&commit) { Some(_) => output.get_mut().mark_unspent(), None => output.get_mut().mark_spent(), @@ -180,16 +192,15 @@ pub fn apply_api_outputs( /// Builds a single api query to retrieve the latest output data from the node. /// So we can refresh the local wallet outputs. -fn refresh_output_state( - config: &WalletConfig, - keychain: &Keychain, - tip: &api::Tip, -) -> Result<(), Error> { +fn refresh_output_state(wallet: &mut T, tip: &api::Tip) -> Result<(), Error> +where + T: WalletBackend, +{ debug!(LOGGER, "Refreshing wallet outputs"); // build a local map of wallet outputs keyed by commit // and a list of outputs we want to query the node for - let wallet_outputs = map_wallet_outputs(config, keychain)?; + let wallet_outputs = map_wallet_outputs(wallet)?; // build the necessary query params - // ?id=xxx&id=yyy&id=zzz @@ -204,7 +215,7 @@ fn refresh_output_state( for query_chunk in query_params.chunks(1000) { let url = format!( "{}/v1/chain/outputs/byids?{}", - config.check_node_api_http_addr, + wallet.node_url(), query_chunk.join("&"), ); @@ -220,25 +231,28 @@ fn refresh_output_state( } } - apply_api_outputs(config, &wallet_outputs, &api_outputs)?; - clean_old_unconfirmed(config, tip)?; + apply_api_outputs(wallet, &wallet_outputs, &api_outputs)?; + clean_old_unconfirmed(wallet, tip)?; Ok(()) } -fn clean_old_unconfirmed(config: &WalletConfig, tip: &api::Tip) -> Result<(), Error> { +fn clean_old_unconfirmed(wallet: &mut T, tip: &api::Tip) -> Result<(), Error> +where + T: WalletBackend, +{ if tip.height < 500 { return Ok(()); } - WalletData::with_wallet(&config.data_file_dir, |wallet_data| { - wallet_data.outputs.retain(|_, ref mut out| { + wallet.with_wallet(|wallet_data| { + wallet_data.outputs().retain(|_, ref mut out| { !(out.status == OutputStatus::Unconfirmed && out.height > 0 && out.height < tip.height - 500) }); }) } -pub fn get_tip_from_node(config: &WalletConfig) -> Result { - let url = format!("{}/v1/chain", config.check_node_api_http_addr); +pub fn get_tip_from_node(addr: &str) -> Result { + let url = format!("{}/v1/chain", addr); api::client::get::(url.as_str()) .context(ErrorKind::Node) .map_err(|e| e.into()) diff --git a/wallet/src/client.rs b/wallet/src/client.rs index c28f943c6..6ce4e93ed 100644 --- a/wallet/src/client.rs +++ b/wallet/src/client.rs @@ -17,12 +17,12 @@ use futures::{Future, Stream}; use hyper; use hyper::header::ContentType; use hyper::{Method, Request}; -use libwallet::transaction::Slate; +use libtx::slate::Slate; use serde_json; use tokio_core::reactor; +use libwallet::types::*; use std::io; -use types::*; use util::LOGGER; /// Call the wallet API to create a coinbase output for the given block_fees. diff --git a/wallet/src/file_wallet.rs b/wallet/src/file_wallet.rs new file mode 100644 index 000000000..37af4c4df --- /dev/null +++ b/wallet/src/file_wallet.rs @@ -0,0 +1,465 @@ +// 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 blake2; +use rand::{thread_rng, Rng}; +use std::cmp::min; +use std::collections::HashMap; +use std::fs::{self, File}; +use std::io::{Read, Write}; +use std::path::MAIN_SEPARATOR; +use std::path::Path; + +use serde_json; +use tokio_core::reactor; +use tokio_retry::Retry; +use tokio_retry::strategy::FibonacciBackoff; + +use failure::{Fail, ResultExt}; + +use keychain::{self, Keychain}; +use util; +use util::LOGGER; + +use libwallet::types::*; + +const DAT_FILE: &'static str = "wallet.dat"; +const BCK_FILE: &'static str = "wallet.bck"; +const LOCK_FILE: &'static str = "wallet.lock"; +const SEED_FILE: &'static str = "wallet.seed"; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct WalletConfig { + // Right now the decision to run or not a wallet is based on the command. + // This may change in the near-future. + // pub enable_wallet: bool, + + // The api interface/ip_address that this api server (i.e. this wallet) will run + // by default this is 127.0.0.1 (and will not accept connections from external clients) + pub api_listen_interface: String, + // The port this wallet will run on + pub api_listen_port: u16, + // The api address of a running server node against which transaction inputs + // will be checked during send + pub check_node_api_http_addr: String, + // The directory in which wallet files are stored + pub data_file_dir: String, +} + +impl Default for WalletConfig { + fn default() -> WalletConfig { + WalletConfig { + // enable_wallet: false, + api_listen_interface: "127.0.0.1".to_string(), + api_listen_port: 13415, + check_node_api_http_addr: "http://127.0.0.1:13413".to_string(), + data_file_dir: ".".to_string(), + } + } +} + +impl WalletConfig { + pub fn api_listen_addr(&self) -> String { + format!("{}:{}", self.api_listen_interface, self.api_listen_port) + } +} + +#[derive(Clone, PartialEq)] +pub struct WalletSeed([u8; 32]); + +impl WalletSeed { + pub fn from_bytes(bytes: &[u8]) -> WalletSeed { + let mut seed = [0; 32]; + for i in 0..min(32, bytes.len()) { + seed[i] = bytes[i]; + } + WalletSeed(seed) + } + + fn from_hex(hex: &str) -> Result { + let bytes = + util::from_hex(hex.to_string()).context(ErrorKind::GenericError("Invalid hex"))?; + Ok(WalletSeed::from_bytes(&bytes)) + } + + pub fn to_hex(&self) -> String { + util::to_hex(self.0.to_vec()) + } + + pub fn derive_keychain(&self, password: &str) -> Result { + let seed = blake2::blake2b::blake2b(64, &password.as_bytes(), &self.0); + let result = keychain::Keychain::from_seed(seed.as_bytes()).context(ErrorKind::Keychain)?; + Ok(result) + } + + pub fn init_new() -> WalletSeed { + let seed: [u8; 32] = thread_rng().gen(); + WalletSeed(seed) + } + + pub fn init_file(wallet_config: &WalletConfig) -> Result { + // create directory if it doesn't exist + fs::create_dir_all(&wallet_config.data_file_dir).context(ErrorKind::IO)?; + + let seed_file_path = &format!( + "{}{}{}", + wallet_config.data_file_dir, MAIN_SEPARATOR, SEED_FILE, + ); + + debug!(LOGGER, "Generating wallet seed file at: {}", seed_file_path,); + + if Path::new(seed_file_path).exists() { + Err(ErrorKind::WalletSeedExists)? + } else { + let seed = WalletSeed::init_new(); + let mut file = File::create(seed_file_path).context(ErrorKind::IO)?; + file.write_all(&seed.to_hex().as_bytes()) + .context(ErrorKind::IO)?; + Ok(seed) + } + } + + pub fn from_file(wallet_config: &WalletConfig) -> Result { + // create directory if it doesn't exist + fs::create_dir_all(&wallet_config.data_file_dir).context(ErrorKind::IO)?; + + let seed_file_path = &format!( + "{}{}{}", + wallet_config.data_file_dir, MAIN_SEPARATOR, SEED_FILE, + ); + + debug!(LOGGER, "Using wallet seed file at: {}", seed_file_path,); + + if Path::new(seed_file_path).exists() { + let mut file = File::open(seed_file_path).context(ErrorKind::IO)?; + let mut buffer = String::new(); + file.read_to_string(&mut buffer).context(ErrorKind::IO)?; + let wallet_seed = WalletSeed::from_hex(&buffer)?; + Ok(wallet_seed) + } else { + error!( + LOGGER, + "wallet seed file {} could not be opened (grin wallet init). \ + Run \"grin wallet init\" to initialize a new wallet.", + seed_file_path + ); + Err(ErrorKind::WalletSeedDoesntExist)? + } + } +} + +/// Wallet information tracking all our outputs. Based on HD derivation and +/// avoids storing any key data, only storing output amounts and child index. +#[derive(Debug, Clone)] +pub struct FileWallet { + /// Keychain + pub keychain: Keychain, + /// Configuration + pub config: WalletConfig, + /// List of outputs + pub outputs: HashMap, + /// Data file path + pub data_file_path: String, + /// Backup file path + pub backup_file_path: String, + /// lock file path + pub lock_file_path: String, +} + +impl WalletBackend for FileWallet { + /// Return the keychain being used + fn keychain(&mut self) -> &mut Keychain { + &mut self.keychain + } + + /// Return URL for check node + fn node_url(&self) -> &str { + &self.config.check_node_api_http_addr + } + + /// Return the outputs directly + fn outputs(&mut self) -> &mut HashMap { + &mut self.outputs + } + + /// Allows for reading wallet data (without needing to acquire the write + /// lock). + fn read_wallet(&mut self, f: F) -> Result + where + F: FnOnce(&mut Self) -> Result, + { + self.read_or_create_paths()?; + f(self) + } + + /// Allows the reading and writing of the wallet data within a file lock. + /// Just provide a closure taking a mutable FileWallet. The lock should + /// be held for as short a period as possible to avoid contention. + /// Note that due to the impossibility to do an actual file lock easily + /// across operating systems, this just creates a lock file with a "should + /// not exist" option. + fn with_wallet(&mut self, f: F) -> Result + where + F: FnOnce(&mut Self) -> T, + { + // create directory if it doesn't exist + fs::create_dir_all(self.config.data_file_dir.clone()).unwrap_or_else(|why| { + info!(LOGGER, "! {:?}", why.kind()); + }); + + info!(LOGGER, "Acquiring wallet lock ..."); + + let lock_file_path = self.lock_file_path.clone(); + let action = || { + trace!(LOGGER, "making lock file for wallet lock"); + fs::create_dir(&lock_file_path) + }; + + // use tokio_retry to cleanly define some retry logic + let mut core = reactor::Core::new().unwrap(); + let retry_strategy = FibonacciBackoff::from_millis(1000).take(10); + let retry_future = Retry::spawn(core.handle(), retry_strategy, action); + let retry_result = core.run(retry_future); + + match retry_result { + Ok(_) => {} + Err(e) => { + error!( + LOGGER, + "Failed to acquire wallet lock file (multiple retries)", + ); + return Err( + e.context(ErrorKind::FileWallet("Failed to acquire lock file")) + .into(), + ); + } + } + + // We successfully acquired the lock - so do what needs to be done. + self.read_or_create_paths()?; + self.write(&self.backup_file_path)?; + let res = f(self); + self.write(&self.data_file_path)?; + + // delete the lock file + fs::remove_dir(&self.lock_file_path).context(ErrorKind::FileWallet( + "Could not remove wallet lock file. Maybe insufficient rights?", + ))?; + + info!(LOGGER, "... released wallet lock"); + + Ok(res) + } + + /// Append a new output data to the wallet data. + /// TODO - we should check for overwriting here - only really valid for + /// unconfirmed coinbase + fn add_output(&mut self, out: OutputData) { + self.outputs.insert(out.key_id.to_hex(), out.clone()); + } + + // TODO - careful with this, only for Unconfirmed (maybe Locked)? + fn delete_output(&mut self, id: &keychain::Identifier) { + self.outputs.remove(&id.to_hex()); + } + + /// Lock an output data. + /// TODO - we should track identifier on these outputs (not just n_child) + fn lock_output(&mut self, out: &OutputData) { + if let Some(out_to_lock) = self.outputs.get_mut(&out.key_id.to_hex()) { + if out_to_lock.value == out.value { + out_to_lock.lock() + } + } + } + + /// get a single output + fn get_output(&self, key_id: &keychain::Identifier) -> Option<&OutputData> { + self.outputs.get(&key_id.to_hex()) + } + + /// Next child index when we want to create a new output. + fn next_child(&self, root_key_id: keychain::Identifier) -> u32 { + let mut max_n = 0; + for out in self.outputs.values() { + if max_n < out.n_child && out.root_key_id == root_key_id { + max_n = out.n_child; + } + } + max_n + 1 + } + + /// Select spendable coins from the wallet. + /// Default strategy is to spend the maximum number of outputs (up to + /// max_outputs). Alternative strategy is to spend smallest outputs first + /// but only as many as necessary. When we introduce additional strategies + /// we should pass something other than a bool in. + fn select_coins( + &self, + root_key_id: keychain::Identifier, + amount: u64, + current_height: u64, + minimum_confirmations: u64, + max_outputs: usize, + select_all: bool, + ) -> Vec { + // first find all eligible outputs based on number of confirmations + let mut eligible = self.outputs + .values() + .filter(|out| { + out.root_key_id == root_key_id + && out.eligible_to_spend(current_height, minimum_confirmations) + }) + .cloned() + .collect::>(); + + // sort eligible outputs by increasing value + eligible.sort_by_key(|out| out.value); + + // use a sliding window to identify potential sets of possible outputs to spend + // Case of amount > total amount of max_outputs(500): + // The limit exists because by default, we always select as many inputs as + // possible in a transaction, to reduce both the Output set and the fees. + // But that only makes sense up to a point, hence the limit to avoid being too + // greedy. But if max_outputs(500) is actually not enought to cover the whole + // amount, the wallet should allow going over it to satisfy what the user + // wants to send. So the wallet considers max_outputs more of a soft limit. + if eligible.len() > max_outputs { + for window in eligible.windows(max_outputs) { + let windowed_eligibles = window.iter().cloned().collect::>(); + if let Some(outputs) = self.select_from(amount, select_all, windowed_eligibles) { + return outputs; + } + } + // Not exist in any window of which total amount >= amount. + // Then take coins from the smallest one up to the total amount of selected + // coins = the amount. + if let Some(outputs) = self.select_from(amount, false, eligible.clone()) { + debug!( + LOGGER, + "Extending maximum number of outputs. {} outputs selected.", + outputs.len() + ); + return outputs; + } + } else { + if let Some(outputs) = self.select_from(amount, select_all, eligible.clone()) { + return outputs; + } + } + + // we failed to find a suitable set of outputs to spend, + // so return the largest amount we can so we can provide guidance on what is + // possible + eligible.reverse(); + eligible.iter().take(max_outputs).cloned().collect() + } +} + +impl FileWallet { + /// Create a new FileWallet instance + pub fn new(config: WalletConfig, keychain: Keychain) -> Result { + let mut retval = FileWallet { + keychain: keychain, + config: config.clone(), + outputs: HashMap::new(), + data_file_path: format!("{}{}{}", config.data_file_dir, MAIN_SEPARATOR, DAT_FILE), + backup_file_path: format!("{}{}{}", config.data_file_dir, MAIN_SEPARATOR, BCK_FILE), + lock_file_path: format!("{}{}{}", config.data_file_dir, MAIN_SEPARATOR, LOCK_FILE), + }; + match retval.read_or_create_paths() { + Ok(_) => Ok(retval), + Err(e) => Err(e), + } + } + + /// Read the wallet data or create brand files if the data + /// files don't yet exist + fn read_or_create_paths(&mut self) -> Result<(), Error> { + if !Path::new(&self.config.data_file_dir.clone()).exists() { + fs::create_dir_all(&self.config.data_file_dir.clone()).unwrap_or_else(|why| { + info!(LOGGER, "! {:?}", why.kind()); + }); + } + if Path::new(&self.data_file_path.clone()).exists() { + self.read()?; + } + Ok(()) + } + + /// Read output_data vec from disk. + fn read_outputs(&self) -> Result, Error> { + let data_file = File::open(self.data_file_path.clone()) + .context(ErrorKind::FileWallet(&"Could not open wallet file"))?; + serde_json::from_reader(data_file).map_err(|e| { + e.context(ErrorKind::FileWallet(&"Error reading wallet file ")) + .into() + }) + } + + /// Populate wallet_data with output_data from disk. + fn read(&mut self) -> Result<(), Error> { + let outputs = self.read_outputs()?; + self.outputs = HashMap::new(); + for out in outputs { + self.add_output(out); + } + Ok(()) + } + + /// 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| e.context(ErrorKind::FileWallet(&"Could not create ")))?; + let mut outputs = self.outputs.values().collect::>(); + outputs.sort(); + let res_json = serde_json::to_vec_pretty(&outputs) + .map_err(|e| e.context(ErrorKind::FileWallet("Error serializing wallet data")))?; + data_file + .write_all(res_json.as_slice()) + .context(ErrorKind::FileWallet(&"Error writing wallet file")) + .map_err(|e| e.into()) + } + + // Select the full list of outputs if we are using the select_all strategy. + // Otherwise select just enough outputs to cover the desired amount. + fn select_from( + &self, + amount: u64, + select_all: bool, + outputs: Vec, + ) -> Option> { + let total = outputs.iter().fold(0, |acc, x| acc + x.value); + if total >= amount { + if select_all { + return Some(outputs.iter().cloned().collect()); + } else { + let mut selected_amount = 0; + return Some( + outputs + .iter() + .take_while(|out| { + let res = selected_amount < amount; + selected_amount += out.value; + res + }) + .cloned() + .collect(), + ); + } + } else { + None + } + } +} diff --git a/wallet/src/handlers.rs b/wallet/src/handlers.rs index f6a304ebe..ee348bc05 100644 --- a/wallet/src/handlers.rs +++ b/wallet/src/handlers.rs @@ -11,6 +11,7 @@ // 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::sync::{Arc, RwLock}; use bodyparser; use iron::Handler; @@ -20,20 +21,24 @@ use serde_json; use core::ser; use failure::{Fail, ResultExt}; -use keychain::Keychain; +use libwallet::types::*; use receiver::receive_coinbase; -use types::*; use util; -pub struct CoinbaseHandler { - pub config: WalletConfig, - pub keychain: Keychain, +pub struct CoinbaseHandler +where + T: WalletBackend, +{ + pub wallet: Arc>, } -impl CoinbaseHandler { - fn build_coinbase(&self, block_fees: &BlockFees) -> Result { +impl CoinbaseHandler +where + T: WalletBackend, +{ + fn build_coinbase(&self, wallet: &mut T, block_fees: &BlockFees) -> Result { let (out, kern, block_fees) = - receive_coinbase(&self.config, &self.keychain, block_fees).context(ErrorKind::Node)?; + receive_coinbase(wallet, block_fees).context(ErrorKind::Node)?; let out_bin = ser::ser_vec(&out).context(ErrorKind::Node)?; @@ -54,12 +59,15 @@ impl CoinbaseHandler { // TODO - error handling - what to return if we fail to get the wallet lock for // some reason... -impl Handler for CoinbaseHandler { +impl Handler for CoinbaseHandler +where + T: WalletBackend + Send + Sync + 'static, +{ fn handle(&self, req: &mut Request) -> IronResult { let struct_body = req.get::>(); - + let mut wallet = self.wallet.write().unwrap(); if let Ok(Some(block_fees)) = struct_body { - let coinbase = self.build_coinbase(&block_fees) + let coinbase = self.build_coinbase(&mut wallet, &block_fees) .map_err(|e| IronError::new(Fail::compat(e), status::BadRequest))?; if let Ok(json) = serde_json::to_string(&coinbase) { Ok(Response::with((status::Ok, json))) diff --git a/wallet/src/info.rs b/wallet/src/info.rs index b3de38ee5..e72b22695 100644 --- a/wallet/src/info.rs +++ b/wallet/src/info.rs @@ -14,12 +14,14 @@ use checker; use core::core::amount_to_hr_string; -use keychain::Keychain; +use libwallet::types::*; use prettytable; -use types::{OutputStatus, WalletConfig, WalletData, WalletInfo}; -pub fn show_info(config: &WalletConfig, keychain: &Keychain) { - let wallet_info = retrieve_info(config, keychain); +pub fn show_info(wallet: &mut T) +where + T: WalletBackend, +{ + let wallet_info = retrieve_info(wallet); println!( "\n____ Wallet Summary Info at {} ({}) ____\n", wallet_info.current_height, wallet_info.data_confirmed_from @@ -45,13 +47,16 @@ pub fn show_info(config: &WalletConfig, keychain: &Keychain) { } } -pub fn retrieve_info(config: &WalletConfig, keychain: &Keychain) -> WalletInfo { - let result = checker::refresh_outputs(&config, &keychain); +pub fn retrieve_info(wallet: &mut T) -> WalletInfo +where + T: WalletBackend, +{ + let result = checker::refresh_outputs(wallet); - let ret_val = WalletData::read_wallet(&config.data_file_dir, |wallet_data| { - let (current_height, from) = match checker::get_tip_from_node(config) { + let ret_val = wallet.read_wallet(|wallet_data| { + let (current_height, from) = match checker::get_tip_from_node(&wallet_data.node_url()) { Ok(tip) => (tip.height, "from server node"), - Err(_) => match wallet_data.outputs.values().map(|out| out.height).max() { + Err(_) => match wallet_data.outputs().values().map(|out| out.height).max() { Some(height) => (height, "from wallet"), None => (0, "node/wallet unavailable"), }, @@ -61,9 +66,10 @@ pub fn retrieve_info(config: &WalletConfig, keychain: &Keychain) -> WalletInfo { let mut unconfirmed_total = 0; let mut locked_total = 0; for out in wallet_data - .outputs + .outputs() + .clone() .values() - .filter(|out| out.root_key_id == keychain.root_key_id()) + .filter(|out| out.root_key_id == wallet_data.keychain().root_key_id()) { if out.status == OutputStatus::Unspent { unspent_total += out.value; diff --git a/wallet/src/lib.rs b/wallet/src/lib.rs index 35d0c1fda..32d2d126e 100644 --- a/wallet/src/lib.rs +++ b/wallet/src/lib.rs @@ -48,21 +48,21 @@ extern crate grin_util as util; pub mod checker; pub mod client; -pub mod grinwallet; +pub mod file_wallet; mod handlers; mod info; +pub mod libtx; pub mod libwallet; mod outputs; pub mod receiver; mod restore; mod sender; pub mod server; -pub mod types; +pub use file_wallet::{FileWallet, WalletConfig, WalletSeed}; pub use info::{retrieve_info, show_info}; +pub use libwallet::types::{BlockFees, CbData, Error, ErrorKind, WalletInfo, WalletReceiveRequest}; pub use outputs::show_outputs; pub use receiver::WalletReceiver; pub use restore::restore; pub use sender::{issue_burn_tx, issue_send_tx}; -pub use types::{BlockFees, CbData, Error, ErrorKind, WalletConfig, WalletInfo, - WalletReceiveRequest, WalletSeed}; diff --git a/wallet/src/libwallet/aggsig.rs b/wallet/src/libtx/aggsig.rs similarity index 99% rename from wallet/src/libwallet/aggsig.rs rename to wallet/src/libtx/aggsig.rs index cbea06d8e..801b6e6fb 100644 --- a/wallet/src/libwallet/aggsig.rs +++ b/wallet/src/libtx/aggsig.rs @@ -16,7 +16,7 @@ use keychain::Keychain; use keychain::blind::BlindingFactor; use keychain::extkey::Identifier; -use libwallet::error::Error; +use libtx::error::Error; use util::kernel_sig_msg; use util::secp::key::{PublicKey, SecretKey}; use util::secp::pedersen::Commitment; diff --git a/wallet/src/libwallet/build.rs b/wallet/src/libtx/build.rs similarity index 99% rename from wallet/src/libwallet/build.rs rename to wallet/src/libtx/build.rs index f9534f442..c92e096ee 100644 --- a/wallet/src/libwallet/build.rs +++ b/wallet/src/libtx/build.rs @@ -32,7 +32,7 @@ use core::core::pmmr::MerkleProof; use core::core::{Input, Output, OutputFeatures, ProofMessageElements, Transaction, TxKernel}; use keychain; use keychain::{BlindSum, BlindingFactor, Identifier, Keychain}; -use libwallet::{aggsig, proof}; +use libtx::{aggsig, proof}; use util::LOGGER; /// Context information available to transaction combinators. diff --git a/wallet/src/libwallet/error.rs b/wallet/src/libtx/error.rs similarity index 100% rename from wallet/src/libwallet/error.rs rename to wallet/src/libtx/error.rs diff --git a/wallet/src/grinwallet/mod.rs b/wallet/src/libtx/mod.rs similarity index 53% rename from wallet/src/grinwallet/mod.rs rename to wallet/src/libtx/mod.rs index 4371e24dc..72fb13444 100644 --- a/wallet/src/grinwallet/mod.rs +++ b/wallet/src/libtx/mod.rs @@ -12,12 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -//! Library specific to the Grin wallet implementation, as distinct from -//! libwallet, which should build transactions without any knowledge of the -//! wallet implementation. - -// TODO: Once this is working, extract a set of traits that wallet -// implementations would need to provide +//! Wallet lib... should be used by clients to build wallets and +//! encapsulate all functions needed to build transactions and operate a wallet #![deny(non_upper_case_globals)] #![deny(non_camel_case_types)] @@ -25,6 +21,24 @@ #![deny(unused_mut)] #![warn(missing_docs)] -pub mod keys; -pub mod selection; -pub mod sigcontext; +pub mod aggsig; +pub mod build; +pub mod error; +pub mod proof; +pub mod reward; +pub mod slate; + +use core::consensus; +use core::core::Transaction; + +const DEFAULT_BASE_FEE: u64 = consensus::MILLI_GRIN; + +/// Transaction fee calculation +pub fn tx_fee(input_len: usize, output_len: usize, proof_len: usize, base_fee: Option) -> u64 { + let use_base_fee = match base_fee { + Some(bf) => bf, + None => DEFAULT_BASE_FEE, + }; + + (Transaction::weight(input_len, output_len, proof_len) as u64) * use_base_fee +} diff --git a/wallet/src/libwallet/proof.rs b/wallet/src/libtx/proof.rs similarity index 99% rename from wallet/src/libwallet/proof.rs rename to wallet/src/libtx/proof.rs index aada38291..6f14bb2a4 100644 --- a/wallet/src/libwallet/proof.rs +++ b/wallet/src/libtx/proof.rs @@ -17,7 +17,7 @@ use blake2; use keychain::Keychain; use keychain::extkey::Identifier; -use libwallet::error::Error; +use libtx::error::Error; use util::logger::LOGGER; use util::secp::key::SecretKey; use util::secp::pedersen::{Commitment, ProofInfo, ProofMessage, RangeProof}; diff --git a/wallet/src/libtx/reward.rs b/wallet/src/libtx/reward.rs new file mode 100644 index 000000000..7c9629699 --- /dev/null +++ b/wallet/src/libtx/reward.rs @@ -0,0 +1,79 @@ +// 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. + +//! Builds the blinded output and related signature proof for the block +//! reward. +use keychain; + +use core::consensus::reward; +use core::core::KernelFeatures; +use core::core::{Output, OutputFeatures, ProofMessageElements, TxKernel}; +use libtx::error::Error; +use libtx::{aggsig, proof}; +use util::{kernel_sig_msg, secp, static_secp_instance, LOGGER}; + +/// output a reward output +pub fn output( + keychain: &keychain::Keychain, + key_id: &keychain::Identifier, + fees: u64, + height: u64, +) -> Result<(Output, TxKernel), Error> { + let value = reward(fees); + let commit = keychain.commit(value, key_id)?; + let msg = ProofMessageElements::new(value, key_id); + + trace!(LOGGER, "Block reward - Pedersen Commit is: {:?}", commit,); + + let rproof = proof::create( + keychain, + value, + key_id, + commit, + None, + msg.to_proof_message(), + )?; + + let output = Output { + features: OutputFeatures::COINBASE_OUTPUT, + commit: commit, + proof: rproof, + }; + + let secp = static_secp_instance(); + let secp = secp.lock().unwrap(); + let over_commit = secp.commit_value(reward(fees))?; + let out_commit = output.commitment(); + let excess = secp.commit_sum(vec![out_commit], vec![over_commit])?; + + // NOTE: Remember we sign the fee *and* the lock_height. + // For a coinbase output the fee is 0 and the lock_height is + // the lock_height of the coinbase output itself, + // not the lock_height of the tx (there is no tx for a coinbase output). + // This output will not be spendable earlier than lock_height (and we sign this + // here). + let msg = secp::Message::from_slice(&kernel_sig_msg(0, height))?; + let sig = aggsig::sign_from_key_id(&secp, keychain, &msg, &key_id)?; + + let proof = TxKernel { + features: KernelFeatures::COINBASE_KERNEL, + excess: excess, + excess_sig: sig, + fee: 0, + // lock_height here is the height of the block (tx should be valid immediately) + // *not* the lock_height of the coinbase output (only spendable 1,000 blocks later) + lock_height: height, + }; + Ok((output, proof)) +} diff --git a/wallet/src/libwallet/transaction.rs b/wallet/src/libtx/slate.rs similarity index 96% rename from wallet/src/libwallet/transaction.rs rename to wallet/src/libtx/slate.rs index cb0b344f3..f605e01fa 100644 --- a/wallet/src/libwallet/transaction.rs +++ b/wallet/src/libtx/slate.rs @@ -17,18 +17,15 @@ use rand::thread_rng; use uuid::Uuid; -use core::consensus; use core::core::{amount_to_hr_string, Committed, Transaction}; use keychain::{BlindSum, BlindingFactor, Keychain}; -use libwallet::error::Error; -use libwallet::{aggsig, build}; +use libtx::error::Error; +use libtx::{aggsig, build, tx_fee}; use util::secp::Signature; use util::secp::key::{PublicKey, SecretKey}; use util::{secp, LOGGER}; -const DEFAULT_BASE_FEE: u64 = consensus::MILLI_GRIN; - /// Public data for each participant in the slate #[derive(Serialize, Deserialize, Debug, Clone)] @@ -380,13 +377,3 @@ impl Slate { Ok(()) } } - -/// Transaction fee calculation -pub fn tx_fee(input_len: usize, output_len: usize, proof_len: usize, base_fee: Option) -> u64 { - let use_base_fee = match base_fee { - Some(bf) => bf, - None => DEFAULT_BASE_FEE, - }; - - (Transaction::weight(input_len, output_len, proof_len) as u64) * use_base_fee -} diff --git a/wallet/src/grinwallet/keys.rs b/wallet/src/libwallet/keys.rs similarity index 54% rename from wallet/src/grinwallet/keys.rs rename to wallet/src/libwallet/keys.rs index b77c97ce7..3d99e3eae 100644 --- a/wallet/src/grinwallet/keys.rs +++ b/wallet/src/libwallet/keys.rs @@ -12,31 +12,35 @@ // See the License for the specific language governing permissions and // limitations under the License. -//! Grin Wallet specific key management functions -use keychain::{Identifier, Keychain}; -use types::*; +//! Wallet key management functions +use keychain::Identifier; +use libwallet::types::{Error, WalletBackend}; /// Get our next available key -pub fn new_output_key( - config: &WalletConfig, - keychain: &Keychain, -) -> Result<(Identifier, u32), Error> { - WalletData::with_wallet(&config.data_file_dir, |wallet_data| { - next_available_key(&wallet_data, keychain) - }) +pub fn new_output_key(wallet: &mut T) -> Result<(Identifier, u32), Error> +where + T: WalletBackend, +{ + wallet.with_wallet(|wallet_data| next_available_key(wallet_data)) } /// Get next available key in the wallet -pub fn next_available_key(wallet_data: &WalletData, keychain: &Keychain) -> (Identifier, u32) { - let root_key_id = keychain.root_key_id(); - let derivation = wallet_data.next_child(root_key_id.clone()); - let key_id = keychain.derive_key_id(derivation).unwrap(); +pub fn next_available_key(wallet: &mut T) -> (Identifier, u32) +where + T: WalletBackend, +{ + let root_key_id = wallet.keychain().root_key_id(); + let derivation = wallet.next_child(root_key_id.clone()); + let key_id = wallet.keychain().derive_key_id(derivation).unwrap(); (key_id, derivation) } /// Retrieve an existing key from a wallet -pub fn retrieve_existing_key(wallet_data: &WalletData, key_id: Identifier) -> (Identifier, u32) { - if let Some(existing) = wallet_data.get_output(&key_id) { +pub fn retrieve_existing_key(wallet: &T, key_id: Identifier) -> (Identifier, u32) +where + T: WalletBackend, +{ + if let Some(existing) = wallet.get_output(&key_id) { let key_id = existing.key_id.clone(); let derivation = existing.n_child; (key_id, derivation) diff --git a/wallet/src/libwallet/mod.rs b/wallet/src/libwallet/mod.rs index 2bb863bc5..47e6598a3 100644 --- a/wallet/src/libwallet/mod.rs +++ b/wallet/src/libwallet/mod.rs @@ -12,8 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. -//! Wallet lib... should be used by clients to build wallets and -//! encapsulate all functions needed to build transactions and operate a wallet +//! Library specific to the Grin wallet implementation, as distinct from +//! libwallet, which should build transactions without any knowledge of the +//! wallet implementation. + +// TODO: Once this is working, extract a set of traits that wallet +// implementations would need to provide #![deny(non_upper_case_globals)] #![deny(non_camel_case_types)] @@ -21,9 +25,7 @@ #![deny(unused_mut)] #![warn(missing_docs)] -pub mod aggsig; -pub mod build; -pub mod error; -pub mod proof; -pub mod reward; -pub mod transaction; +pub mod keys; +pub mod selection; +pub mod sigcontext; +pub mod types; diff --git a/wallet/src/libwallet/reward.rs b/wallet/src/libwallet/reward.rs index 63ac2f0f8..7c9629699 100644 --- a/wallet/src/libwallet/reward.rs +++ b/wallet/src/libwallet/reward.rs @@ -19,8 +19,8 @@ use keychain; use core::consensus::reward; use core::core::KernelFeatures; use core::core::{Output, OutputFeatures, ProofMessageElements, TxKernel}; -use libwallet::error::Error; -use libwallet::{aggsig, proof}; +use libtx::error::Error; +use libtx::{aggsig, proof}; use util::{kernel_sig_msg, secp, static_secp_instance, LOGGER}; /// output a reward output diff --git a/wallet/src/grinwallet/selection.rs b/wallet/src/libwallet/selection.rs similarity index 77% rename from wallet/src/grinwallet/selection.rs rename to wallet/src/libwallet/selection.rs index ba26db944..54e24a289 100644 --- a/wallet/src/grinwallet/selection.rs +++ b/wallet/src/libwallet/selection.rs @@ -15,19 +15,18 @@ //! Selection of inputs for building transactions use failure::ResultExt; -use grinwallet::{keys, sigcontext}; -use keychain::{Identifier, Keychain}; -use libwallet::{build, transaction}; -use types::*; +use keychain::Identifier; +use libtx::{build, tx_fee, slate::Slate}; +use libwallet::types::*; +use libwallet::{keys, sigcontext}; /// Initialise a transaction on the sender side, returns a corresponding /// libwallet transaction slate with the appropriate inputs selected, /// and saves the private wallet identifiers of our selected outputs /// into our transaction context -pub fn build_send_tx_slate( - config: &WalletConfig, - keychain: &Keychain, +pub fn build_send_tx_slate( + wallet: &mut T, num_participants: usize, amount: u64, current_height: u64, @@ -37,15 +36,17 @@ pub fn build_send_tx_slate( selection_strategy_is_use_all: bool, ) -> Result< ( - transaction::Slate, + Slate, sigcontext::Context, - impl FnOnce() -> Result<(), Error>, + impl FnOnce(&mut T) -> Result<(), Error>, ), Error, -> { +> +where + T: WalletBackend, +{ let (elems, inputs, change_id, amount, fee) = select_send_tx( - config, - keychain, + wallet, amount, current_height, minimum_confirmations, @@ -55,19 +56,21 @@ pub fn build_send_tx_slate( )?; // Create public slate - let mut slate = transaction::Slate::blank(num_participants); + let mut slate = Slate::blank(num_participants); slate.amount = amount; slate.height = current_height; slate.lock_height = lock_height; slate.fee = fee; + let keychain = wallet.keychain().clone(); + let blinding = slate - .add_transaction_elements(keychain, elems) + .add_transaction_elements(&keychain, elems) .context(ErrorKind::LibWalletError)?; // Create our own private context let mut context = sigcontext::Context::new( - keychain.secp(), - blinding.secret_key(keychain.secp()).unwrap(), + wallet.keychain().secp(), + blinding.secret_key(&keychain.secp()).unwrap(), ); // Store our private identifiers for each input @@ -82,12 +85,11 @@ pub fn build_send_tx_slate( let lock_inputs = context.get_inputs().clone(); let _lock_outputs = context.get_outputs().clone(); - let data_file_dir = config.data_file_dir.clone(); // Return a closure to acquire wallet lock and lock the coins being spent // so we avoid accidental double spend attempt. - let update_sender_wallet_fn = move || { - WalletData::with_wallet(&data_file_dir, |wallet_data| { + let update_sender_wallet_fn = move |wallet: &mut T| { + wallet.with_wallet(|wallet_data| { for id in lock_inputs { let coin = wallet_data.get_output(&id).unwrap().clone(); wallet_data.lock_output(&coin); @@ -107,43 +109,46 @@ pub fn build_send_tx_slate( /// returning the key of the fresh output and a closure /// that actually performs the addition of the output to the /// wallet -pub fn build_recipient_output_with_slate( - config: &WalletConfig, - keychain: &Keychain, - slate: &mut transaction::Slate, +pub fn build_recipient_output_with_slate( + wallet: &mut T, + slate: &mut Slate, ) -> Result< ( Identifier, sigcontext::Context, - impl FnOnce() -> Result<(), Error>, + impl FnOnce(&mut T) -> Result<(), Error>, ), Error, -> { +> +where + T: WalletBackend, +{ // Create a potential output for this transaction - let (key_id, derivation) = keys::new_output_key(config, keychain)?; + let (key_id, derivation) = keys::new_output_key(wallet)?; - let data_file_dir = config.data_file_dir.clone(); - let root_key_id = keychain.root_key_id(); + let root_key_id = wallet.keychain().root_key_id(); let key_id_inner = key_id.clone(); let amount = slate.amount; let height = slate.height; + let keychain = wallet.keychain().clone(); + let blinding = slate - .add_transaction_elements(keychain, vec![build::output(amount, key_id.clone())]) + .add_transaction_elements(&keychain, vec![build::output(amount, key_id.clone())]) .context(ErrorKind::LibWalletError)?; // Add blinding sum to our context let mut context = sigcontext::Context::new( keychain.secp(), - blinding.secret_key(keychain.secp()).unwrap(), + blinding.secret_key(wallet.keychain().secp()).unwrap(), ); context.add_output(&key_id); // Create closure that adds the output to recipient's wallet // (up to the caller to decide when to do) - let wallet_add_fn = move || { - WalletData::with_wallet(&data_file_dir, |wallet_data| { + let wallet_add_fn = move |wallet: &mut T| { + wallet.with_wallet(|wallet_data| { wallet_data.add_output(OutputData { root_key_id: root_key_id, key_id: key_id_inner, @@ -164,9 +169,8 @@ pub fn build_recipient_output_with_slate( /// Builds a transaction to send to someone from the HD seed associated with the /// wallet and the amount to send. Handles reading through the wallet data file, /// selecting outputs to spend and building the change. -pub fn select_send_tx( - config: &WalletConfig, - keychain: &Keychain, +pub fn select_send_tx( + wallet: &mut T, amount: u64, current_height: u64, minimum_confirmations: u64, @@ -182,11 +186,14 @@ pub fn select_send_tx( u64, // fee ), Error, -> { - let key_id = keychain.clone().root_key_id(); +> +where + T: WalletBackend, +{ + let key_id = wallet.keychain().clone().root_key_id(); // select some spendable coins from the wallet - let mut coins = WalletData::read_wallet(&config.data_file_dir, |wallet_data| { + let mut coins = wallet.read_wallet(|wallet_data| { Ok(wallet_data.select_coins( key_id.clone(), amount, @@ -198,23 +205,25 @@ pub fn select_send_tx( })?; // Get the maximum number of outputs in the wallet - let max_outputs = WalletData::read_wallet(&config.data_file_dir, |wallet_data| { - Ok(wallet_data.select_coins( - key_id.clone(), - amount, - current_height, - minimum_confirmations, - max_outputs, - true, - )) - })?.len(); + let max_outputs = wallet + .read_wallet(|wallet_data| { + Ok(wallet_data.select_coins( + key_id.clone(), + amount, + current_height, + minimum_confirmations, + max_outputs, + true, + )) + })? + .len(); // sender is responsible for setting the fee on the partial tx // recipient should double check the fee calculation and not blindly trust the // sender let mut fee; // First attempt to spend without change - fee = transaction::tx_fee(coins.len(), 1, coins_proof_count(&coins), None); + fee = tx_fee(coins.len(), 1, coins_proof_count(&coins), None); let mut total: u64 = coins.iter().map(|c| c.value).sum(); let mut amount_with_fee = amount + fee; @@ -224,7 +233,7 @@ pub fn select_send_tx( // Check if we need to use a change address if total > amount_with_fee { - fee = transaction::tx_fee(coins.len(), 2, coins_proof_count(&coins), None); + fee = tx_fee(coins.len(), 2, coins_proof_count(&coins), None); amount_with_fee = amount + fee; // Here check if we have enough outputs for the amount including fee otherwise @@ -236,7 +245,7 @@ pub fn select_send_tx( } // select some spendable coins from the wallet - coins = WalletData::read_wallet(&config.data_file_dir, |wallet_data| { + coins = wallet.read_wallet(|wallet_data| { Ok(wallet_data.select_coins( key_id.clone(), amount_with_fee, @@ -246,15 +255,14 @@ pub fn select_send_tx( selection_strategy_is_use_all, )) })?; - fee = transaction::tx_fee(coins.len(), 2, coins_proof_count(&coins), None); + fee = tx_fee(coins.len(), 2, coins_proof_count(&coins), None); total = coins.iter().map(|c| c.value).sum(); amount_with_fee = amount + fee; } } // build transaction skeleton with inputs and change - let (mut parts, change_key) = - inputs_and_change(&coins, config, keychain, current_height, amount, fee)?; + let (mut parts, change_key) = inputs_and_change(&coins, wallet, current_height, amount, fee)?; // This is more proof of concept than anything but here we set lock_height // on tx being sent (based on current chain height via api). @@ -269,14 +277,16 @@ pub fn coins_proof_count(coins: &Vec) -> usize { } /// Selects inputs and change for a transaction -pub fn inputs_and_change( +pub fn inputs_and_change( coins: &Vec, - config: &WalletConfig, - keychain: &Keychain, + wallet: &mut T, height: u64, amount: u64, fee: u64, -) -> Result<(Vec>, Option), Error> { +) -> Result<(Vec>, Option), Error> +where + T: WalletBackend, +{ let mut parts = vec![]; // calculate the total across all inputs, and how much is left @@ -291,7 +301,8 @@ pub fn inputs_and_change( // build inputs using the appropriate derived key_ids for coin in coins { - let key_id = keychain + let key_id = wallet + .keychain() .derive_key_id(coin.n_child) .context(ErrorKind::Keychain)?; if coin.is_coinbase { @@ -312,7 +323,8 @@ pub fn inputs_and_change( let change_key; if change != 0 { // track the output representing our change - change_key = WalletData::with_wallet(&config.data_file_dir, |wallet_data| { + change_key = wallet.with_wallet(|wallet_data| { + let keychain = wallet_data.keychain().clone(); let root_key_id = keychain.root_key_id(); let change_derivation = wallet_data.next_child(root_key_id.clone()); let change_key = keychain.derive_key_id(change_derivation).unwrap(); diff --git a/wallet/src/grinwallet/sigcontext.rs b/wallet/src/libwallet/sigcontext.rs similarity index 99% rename from wallet/src/grinwallet/sigcontext.rs rename to wallet/src/libwallet/sigcontext.rs index d49fab7a6..831c19cce 100644 --- a/wallet/src/grinwallet/sigcontext.rs +++ b/wallet/src/libwallet/sigcontext.rs @@ -13,7 +13,7 @@ // limitations under the License. //! Signature context holder helper (may be removed or replaced eventually) use keychain::extkey::Identifier; -use libwallet::aggsig; +use libtx::aggsig; use util::secp::key::{PublicKey, SecretKey}; use util::secp::{self, Secp256k1}; diff --git a/wallet/src/libwallet/types.rs b/wallet/src/libwallet/types.rs new file mode 100644 index 000000000..f675cd8da --- /dev/null +++ b/wallet/src/libwallet/types.rs @@ -0,0 +1,454 @@ +// 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. + +//! Types and traits that should be provided by a wallet +//! implementation +use std::collections::HashMap; +use std::fmt::{self, Display}; + +use serde; + +use failure::{Backtrace, Context, Fail, ResultExt}; + +use core::core::hash::Hash; +use core::core::pmmr::MerkleProof; + +use keychain::{Identifier, Keychain}; + +/// TODO: +/// Wallets should implement this backend for their storage. All functions +/// here expect that the wallet instance has instantiated itself or stored +/// whatever credentials it needs +pub trait WalletBackend { + /// Return the keychain being used + fn keychain(&mut self) -> &mut Keychain; + + /// Return the URL of the check node + fn node_url(&self) -> &str; + + /// Return the outputs directly + fn outputs(&mut self) -> &mut HashMap; + + /// Allows for reading wallet data (read-only) + fn read_wallet(&mut self, f: F) -> Result + where + F: FnOnce(&mut Self) -> Result; + + /// Get all outputs from a wallet impl (probably with some sort + /// of query param), read+write. Implementor should save + /// any changes to its data and perform any locking needed + fn with_wallet(&mut self, f: F) -> Result + where + F: FnOnce(&mut Self) -> T; + + /// Add an output + fn add_output(&mut self, out: OutputData); + + /// Delete an output + fn delete_output(&mut self, id: &Identifier); + + /// Lock an output + fn lock_output(&mut self, out: &OutputData); + + /// get a single output + fn get_output(&self, key_id: &Identifier) -> Option<&OutputData>; + + /// Next child ID when we want to create a new output + fn next_child(&self, root_key_id: Identifier) -> u32; + + /// Select spendable coins from the wallet + fn select_coins( + &self, + root_key_id: Identifier, + amount: u64, + current_height: u64, + minimum_confirmations: u64, + max_outputs: usize, + select_all: bool, + ) -> Vec; +} + +/// Information about an output that's being tracked by the wallet. Must be +/// enough to reconstruct the commitment associated with the ouput when the +/// root private key is known.*/ + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, PartialOrd, Eq, Ord)] +pub struct OutputData { + /// Root key_id that the key for this output is derived from + pub root_key_id: Identifier, + /// Derived key for this output + pub key_id: Identifier, + /// How many derivations down from the root key + pub n_child: u32, + /// Value of the output, necessary to rebuild the commitment + pub value: u64, + /// Current status of the output + pub status: OutputStatus, + /// Height of the output + pub height: u64, + /// Height we are locked until + pub lock_height: u64, + /// Is this a coinbase output? Is it subject to coinbase locktime? + pub is_coinbase: bool, + /// Hash of the block this output originated from. + pub block: Option, + pub merkle_proof: Option, +} + +impl OutputData { + /// Lock a given output to avoid conflicting use + pub fn lock(&mut self) { + self.status = OutputStatus::Locked; + } + + /// How many confirmations has this output received? + /// If height == 0 then we are either Unconfirmed or the output was + /// cut-through + /// so we do not actually know how many confirmations this output had (and + /// never will). + pub fn num_confirmations(&self, current_height: u64) -> u64 { + if self.status == OutputStatus::Unconfirmed { + 0 + } else if self.height == 0 { + 0 + } else { + // if an output has height n and we are at block n + // then we have a single confirmation (the block it originated in) + 1 + (current_height - self.height) + } + } + + /// Check if output is eligible to spend based on state and height and + /// confirmations + pub fn eligible_to_spend(&self, current_height: u64, minimum_confirmations: u64) -> bool { + if [OutputStatus::Spent, OutputStatus::Locked].contains(&self.status) { + return false; + } else if self.status == OutputStatus::Unconfirmed && self.is_coinbase { + return false; + } else if self.is_coinbase && self.block.is_none() { + // if we do not have a block hash for coinbase output we cannot spent it + // block index got compacted before we refreshed our wallet? + return false; + } else if self.is_coinbase && self.merkle_proof.is_none() { + // if we do not have a Merkle proof for coinbase output we cannot spent it + // block index got compacted before we refreshed our wallet? + return false; + } else if self.lock_height > current_height { + return false; + } else if self.status == OutputStatus::Unspent + && self.num_confirmations(current_height) >= minimum_confirmations + { + return true; + } else if self.status == OutputStatus::Unconfirmed && minimum_confirmations == 0 { + return true; + } else { + return false; + } + } + + /// Marks this output as unspent if it was previously unconfirmed + pub fn mark_unspent(&mut self) { + match self.status { + OutputStatus::Unconfirmed => self.status = OutputStatus::Unspent, + _ => (), + } + } + + pub fn mark_spent(&mut self) { + match self.status { + OutputStatus::Unspent => self.status = OutputStatus::Spent, + OutputStatus::Locked => self.status = OutputStatus::Spent, + _ => (), + } + } +} +/// Status of an output that's being tracked by the wallet. Can either be +/// unconfirmed, spent, unspent, or locked (when it's been used to generate +/// a transaction but we don't have confirmation that the transaction was +/// broadcasted or mined). +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Ord, PartialOrd)] +pub enum OutputStatus { + Unconfirmed, + Unspent, + Locked, + Spent, +} + +impl fmt::Display for OutputStatus { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + OutputStatus::Unconfirmed => write!(f, "Unconfirmed"), + OutputStatus::Unspent => write!(f, "Unspent"), + OutputStatus::Locked => write!(f, "Locked"), + OutputStatus::Spent => write!(f, "Spent"), + } + } +} + +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord)] +pub struct MerkleProofWrapper(pub MerkleProof); + +impl MerkleProofWrapper { + pub fn merkle_proof(&self) -> MerkleProof { + self.0.clone() + } +} + +impl serde::ser::Serialize for MerkleProofWrapper { + fn serialize(&self, serializer: S) -> Result + where + S: serde::ser::Serializer, + { + serializer.serialize_str(&self.0.to_hex()) + } +} + +impl<'de> serde::de::Deserialize<'de> for MerkleProofWrapper { + fn deserialize(deserializer: D) -> Result + where + D: serde::de::Deserializer<'de>, + { + deserializer.deserialize_str(MerkleProofWrapperVisitor) + } +} + +struct MerkleProofWrapperVisitor; + +impl<'de> serde::de::Visitor<'de> for MerkleProofWrapperVisitor { + type Value = MerkleProofWrapper; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a merkle proof") + } + + fn visit_str(self, s: &str) -> Result + where + E: serde::de::Error, + { + let merkle_proof = MerkleProof::from_hex(s).unwrap(); + Ok(MerkleProofWrapper(merkle_proof)) + } +} + +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord)] +pub struct BlockIdentifier(pub Hash); + +impl BlockIdentifier { + pub fn hash(&self) -> Hash { + self.0 + } + + pub fn from_hex(hex: &str) -> Result { + let hash = Hash::from_hex(hex).context(ErrorKind::GenericError("Invalid hex"))?; + Ok(BlockIdentifier(hash)) + } +} + +impl serde::ser::Serialize for BlockIdentifier { + fn serialize(&self, serializer: S) -> Result + where + S: serde::ser::Serializer, + { + serializer.serialize_str(&self.0.to_hex()) + } +} + +impl<'de> serde::de::Deserialize<'de> for BlockIdentifier { + fn deserialize(deserializer: D) -> Result + where + D: serde::de::Deserializer<'de>, + { + deserializer.deserialize_str(BlockIdentifierVisitor) + } +} + +struct BlockIdentifierVisitor; + +impl<'de> serde::de::Visitor<'de> for BlockIdentifierVisitor { + type Value = BlockIdentifier; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a block hash") + } + + fn visit_str(self, s: &str) -> Result + where + E: serde::de::Error, + { + let block_hash = Hash::from_hex(s).unwrap(); + Ok(BlockIdentifier(block_hash)) + } +} + +/// Amount in request to build a coinbase output. +#[derive(Serialize, Deserialize, Debug, Clone)] +pub enum WalletReceiveRequest { + Coinbase(BlockFees), + PartialTransaction(String), + Finalize(String), +} + +/// Fees in block to use for coinbase amount calculation +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct BlockFees { + pub fees: u64, + pub height: u64, + pub key_id: Option, +} + +impl BlockFees { + pub fn key_id(&self) -> Option { + self.key_id.clone() + } +} + +/// Response to build a coinbase output. +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct CbData { + pub output: String, + pub kernel: String, + pub key_id: String, +} + +/// a contained wallet info struct, so automated tests can parse wallet info +/// can add more fields here over time as needed +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct WalletInfo { + // height from which info was taken + pub current_height: u64, + // total amount in the wallet + pub total: u64, + // amount awaiting confirmation + pub amount_awaiting_confirmation: u64, + // confirmed but locked + pub amount_confirmed_but_locked: u64, + // amount currently spendable + pub amount_currently_spendable: u64, + // amount locked by previous transactions + pub amount_locked: u64, + // whether the data was confirmed against a live node + pub data_confirmed: bool, + // node confirming the data + pub data_confirmed_from: String, +} + +#[derive(Debug)] +pub struct Error { + inner: Context, +} + +/// Wallet errors, mostly wrappers around underlying crypto or I/O errors. +#[derive(Copy, Clone, Eq, PartialEq, Debug, Fail)] +pub enum ErrorKind { + #[fail(display = "Not enough funds")] + NotEnoughFunds(u64), + + #[fail(display = "Fee dispute: sender fee {}, recipient fee {}", sender_fee, recipient_fee)] + FeeDispute { sender_fee: u64, recipient_fee: u64 }, + + #[fail(display = "Fee exceeds amount: sender amount {}, recipient fee {}", sender_amount, + recipient_fee)] + FeeExceedsAmount { + sender_amount: u64, + recipient_fee: u64, + }, + + #[fail(display = "Keychain error")] + Keychain, + + #[fail(display = "Transaction error")] + Transaction, + + #[fail(display = "Secp error")] + Secp, + + #[fail(display = "LibWallet error")] + LibWalletError, + + #[fail(display = "Wallet data error: {}", _0)] + FileWallet(&'static str), + + /// An error in the format of the JSON structures exchanged by the wallet + #[fail(display = "JSON format error")] + Format, + + #[fail(display = "I/O error")] + IO, + + /// Error when contacting a node through its API + #[fail(display = "Node API error")] + Node, + + /// Error originating from hyper. + #[fail(display = "Hyper error")] + Hyper, + + /// Error originating from hyper uri parsing. + #[fail(display = "Uri parsing error")] + Uri, + + #[fail(display = "Signature error")] + Signature(&'static str), + + /// Attempt to use duplicate transaction id in separate transactions + #[fail(display = "Duplicate transaction ID error")] + DuplicateTransactionId, + + /// Wallet seed already exists + #[fail(display = "Wallet seed exists error")] + WalletSeedExists, + + /// Wallet seed doesn't exist + #[fail(display = "Wallet seed doesn't exist error")] + WalletSeedDoesntExist, + + #[fail(display = "Generic error: {}", _0)] + GenericError(&'static str), +} + +impl Fail for Error { + fn cause(&self) -> Option<&Fail> { + self.inner.cause() + } + + fn backtrace(&self) -> Option<&Backtrace> { + self.inner.backtrace() + } +} + +impl Display for Error { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + Display::fmt(&self.inner, f) + } +} + +impl Error { + pub fn kind(&self) -> ErrorKind { + *self.inner.get_context() + } +} + +impl From for Error { + fn from(kind: ErrorKind) -> Error { + Error { + inner: Context::new(kind), + } + } +} + +impl From> for Error { + fn from(inner: Context) -> Error { + Error { inner: inner } + } +} diff --git a/wallet/src/outputs.rs b/wallet/src/outputs.rs index a3c554b38..f9df2ce4f 100644 --- a/wallet/src/outputs.rs +++ b/wallet/src/outputs.rs @@ -14,30 +14,29 @@ use checker; use core::core; -use keychain::Keychain; +use libwallet::types::*; use prettytable; use std::io::prelude::*; use term; -use types::{OutputStatus, WalletConfig, WalletData}; -pub fn show_outputs(config: &WalletConfig, keychain: &Keychain, show_spent: bool) { - let root_key_id = keychain.root_key_id(); - let result = checker::refresh_outputs(&config, &keychain); +pub fn show_outputs(wallet: &mut T, show_spent: bool) { + let root_key_id = wallet.keychain().clone().root_key_id(); + let result = checker::refresh_outputs(wallet); // just read the wallet here, no need for a write lock - let _ = WalletData::read_wallet(&config.data_file_dir, |wallet_data| { + let _ = wallet.read_wallet(|wallet_data| { // get the current height via the api // if we cannot get the current height use the max height known to the wallet - let current_height = match checker::get_tip_from_node(config) { + let current_height = match checker::get_tip_from_node(wallet_data.node_url()) { Ok(tip) => tip.height, - Err(_) => match wallet_data.outputs.values().map(|out| out.height).max() { + Err(_) => match wallet_data.outputs().values().map(|out| out.height).max() { Some(height) => height, None => 0, }, }; let mut outputs = wallet_data - .outputs + .outputs() .values() .filter(|out| out.root_key_id == root_key_id) .filter(|out| { diff --git a/wallet/src/receiver.rs b/wallet/src/receiver.rs index 37e5bdfd2..c2e9c106f 100644 --- a/wallet/src/receiver.rs +++ b/wallet/src/receiver.rs @@ -16,6 +16,8 @@ //! receiving money in MimbleWimble requires an interactive exchange, a //! wallet server that's running at all time is required in many cases. +use std::sync::{Arc, RwLock}; + use bodyparser; use iron::Handler; use iron::prelude::*; @@ -26,12 +28,10 @@ use api; use core::consensus::reward; use core::core::{Output, TxKernel}; use core::global; -use failure::Fail; -use failure::ResultExt; -use grinwallet::{keys, selection}; -use keychain::Keychain; -use libwallet::{reward, transaction}; -use types::*; +use failure::{Fail, ResultExt}; +use libtx::{reward, slate::Slate}; +use libwallet::types::*; +use libwallet::{keys, selection}; use util::LOGGER; /// Dummy wrapper for the hex-encoded serialized transaction. @@ -40,45 +40,57 @@ pub struct TxWrapper { pub tx_hex: String, } -fn handle_send( - config: &WalletConfig, - keychain: &Keychain, - slate: &mut transaction::Slate, -) -> Result<(), Error> { - // create an output using the amount in the slate - let (_, mut context, receiver_create_fn) = - selection::build_recipient_output_with_slate(config, keychain, slate).unwrap(); - - // fill public keys - let _ = slate - .fill_round_1(&keychain, &mut context.sec_key, &context.sec_nonce, 1) - .context(ErrorKind::LibWalletError)?; - - // perform partial sig - let _ = slate - .fill_round_2(&keychain, &context.sec_key, &context.sec_nonce, 1) - .context(ErrorKind::LibWalletError)?; - - // Save output in wallet - let _ = receiver_create_fn(); - - Ok(()) -} - /// Component used to receive coins, implements all the receiving end of the /// wallet REST API as well as some of the command-line operations. #[derive(Clone)] -pub struct WalletReceiver { - pub keychain: Keychain, - pub config: WalletConfig, +pub struct WalletReceiver +where + T: WalletBackend, +{ + pub wallet: Arc>, } -impl Handler for WalletReceiver { +impl WalletReceiver +where + T: WalletBackend, +{ + fn handle_send(&self, wallet: &mut T, slate: &mut Slate) -> Result<(), Error> { + // create an output using the amount in the slate + let (_, mut context, receiver_create_fn) = + selection::build_recipient_output_with_slate(wallet, slate).unwrap(); + + // fill public keys + let _ = slate + .fill_round_1( + wallet.keychain(), + &mut context.sec_key, + &context.sec_nonce, + 1, + ) + .context(ErrorKind::LibWalletError)?; + + // perform partial sig + let _ = slate + .fill_round_2(wallet.keychain(), &context.sec_key, &context.sec_nonce, 1) + .context(ErrorKind::LibWalletError)?; + + // Save output in wallet + let _ = receiver_create_fn(wallet); + + Ok(()) + } +} + +impl Handler for WalletReceiver +where + T: WalletBackend + Send + Sync + 'static, +{ fn handle(&self, req: &mut Request) -> IronResult { - let struct_body = req.get::>(); + let struct_body = req.get::>(); + let mut wallet = self.wallet.write().unwrap(); if let Ok(Some(mut slate)) = struct_body { - let _ = handle_send(&self.config, &self.keychain, &mut slate) + let _ = self.handle_send(&mut wallet, &mut slate) .map_err(|e| { error!( LOGGER, @@ -99,22 +111,24 @@ impl Handler for WalletReceiver { //TODO: Split up the output creation and the wallet insertion /// Build a coinbase output and the corresponding kernel -pub fn receive_coinbase( - config: &WalletConfig, - keychain: &Keychain, +pub fn receive_coinbase( + wallet: &mut T, block_fees: &BlockFees, -) -> Result<(Output, TxKernel, BlockFees), Error> { - let root_key_id = keychain.root_key_id(); +) -> Result<(Output, TxKernel, BlockFees), Error> +where + T: WalletBackend, +{ + let root_key_id = wallet.keychain().root_key_id(); let height = block_fees.height; let lock_height = height + global::coinbase_maturity(); // Now acquire the wallet lock and write the new output. - let (key_id, derivation) = WalletData::with_wallet(&config.data_file_dir, |wallet_data| { + let (key_id, derivation) = wallet.with_wallet(|wallet_data| { let key_id = block_fees.key_id(); let (key_id, derivation) = match key_id { - Some(key_id) => keys::retrieve_existing_key(&wallet_data, key_id), - None => keys::next_available_key(&wallet_data, keychain), + Some(key_id) => keys::retrieve_existing_key(wallet_data, key_id), + None => keys::next_available_key(wallet_data), }; // track the new output and return the stuff needed for reward @@ -146,8 +160,12 @@ pub fn receive_coinbase( debug!(LOGGER, "receive_coinbase: {:?}", block_fees); - let (out, kern) = - reward::output(&keychain, &key_id, block_fees.fees, block_fees.height).unwrap(); + let (out, kern) = reward::output( + &wallet.keychain(), + &key_id, + block_fees.fees, + block_fees.height, + ).unwrap(); /* .context(ErrorKind::Keychain)?; */ Ok((out, kern, block_fees)) } diff --git a/wallet/src/restore.rs b/wallet/src/restore.rs index f0d0292e3..1c19ed9e5 100644 --- a/wallet/src/restore.rs +++ b/wallet/src/restore.rs @@ -16,16 +16,15 @@ use byteorder::{BigEndian, ByteOrder}; use core::core::transaction::ProofMessageElements; use core::global; use failure::{Fail, ResultExt}; -use keychain::{Identifier, Keychain}; -use libwallet::proof; -use types::{Error, ErrorKind, MerkleProofWrapper, OutputData, OutputStatus, WalletConfig, - WalletData}; +use keychain::Identifier; +use libtx::proof; +use libwallet::types::*; use util; use util::LOGGER; use util::secp::pedersen; -pub fn get_chain_height(config: &WalletConfig) -> Result { - let url = format!("{}/v1/chain", config.check_node_api_http_addr); +pub fn get_chain_height(node_addr: &str) -> Result { + let url = format!("{}/v1/chain", node_addr); match api::client::get::(url.as_str()) { Ok(tip) => Ok(tip.height), @@ -34,7 +33,7 @@ pub fn get_chain_height(config: &WalletConfig) -> Result { error!( LOGGER, "get_chain_height: Restore failed... unable to contact API {}. Error: {}", - config.check_node_api_http_addr, + node_addr, e ); Err(e.context(ErrorKind::Node).into()) @@ -43,13 +42,10 @@ pub fn get_chain_height(config: &WalletConfig) -> Result { } pub fn get_merkle_proof_for_commit( - config: &WalletConfig, + node_addr: &str, commit: &str, ) -> Result { - let url = format!( - "{}/v1/txhashset/merkleproof?id={}", - config.check_node_api_http_addr, commit - ); + let url = format!("{}/v1/txhashset/merkleproof?id={}", node_addr, commit); match api::client::get::(url.as_str()) { Ok(output) => Ok(MerkleProofWrapper(output.merkle_proof.unwrap())), @@ -72,17 +68,17 @@ fn coinbase_status(output: &api::OutputPrintable) -> bool { } } -pub fn outputs_batch( - config: &WalletConfig, +pub fn outputs_batch( + wallet: &T, start_height: u64, max: u64, -) -> Result { +) -> Result +where + T: WalletBackend, +{ let query_param = format!("start_index={}&max={}", start_height, max); - let url = format!( - "{}/v1/txhashset/outputs?{}", - config.check_node_api_http_addr, query_param, - ); + let url = format!("{}/v1/txhashset/outputs?{}", wallet.node_url(), query_param,); match api::client::get::(url.as_str()) { Ok(o) => Ok(o), @@ -91,7 +87,7 @@ pub fn outputs_batch( error!( LOGGER, "outputs_batch: Restore failed... unable to contact API {}. Error: {}", - config.check_node_api_http_addr, + wallet.node_url(), e ); Err(e.context(ErrorKind::Node))? @@ -100,9 +96,8 @@ pub fn outputs_batch( } // TODO - wrap the many return values in a struct -fn find_outputs_with_key( - config: &WalletConfig, - keychain: &Keychain, +fn find_outputs_with_key( + wallet: &mut T, outputs: Vec, found_key_index: &mut Vec, ) -> Vec<( @@ -129,10 +124,10 @@ fn find_outputs_with_key( let max_derivations = 1_000_000; info!(LOGGER, "Scanning {} outputs", outputs.len(),); - let current_chain_height = get_chain_height(config).unwrap(); + let current_chain_height = get_chain_height(wallet.node_url()).unwrap(); // skey doesn't matter in this case - let skey = keychain.derive_key_id(1).unwrap(); + let skey = wallet.keychain().derive_key_id(1).unwrap(); for output in outputs.iter().filter(|x| !x.spent) { // attempt to unwind message from the RP and get a value.. note // this will only return okay if the value is included in the @@ -140,7 +135,7 @@ fn find_outputs_with_key( // to unwind in this case will be meaningless. With only the nonce known // only the first 32 bytes of the recovered message will be accurate let info = proof::rewind( - keychain, + wallet.keychain(), &skey, output.commit, None, @@ -171,14 +166,14 @@ fn find_outputs_with_key( /*if found_key_index.contains(&(i as u32)) { continue; }*/ - let key_id = &keychain.derive_key_id(i as u32).unwrap(); + let key_id = &wallet.keychain().derive_key_id(i as u32).unwrap(); if !message.compare_bf_first_8(key_id) { continue; } found = true; // we have a partial match, let's just confirm let info = proof::rewind( - keychain, + wallet.keychain(), key_id, output.commit, None, @@ -202,7 +197,8 @@ fn find_outputs_with_key( info!(LOGGER, "Amount: {}", value); - let commit = keychain + let commit = wallet + .keychain() .commit_with_key_index(BigEndian::read_u64(&commit_id), i as u32) .expect("commit with key index"); @@ -210,7 +206,8 @@ fn find_outputs_with_key( let commit_str = util::to_hex(output.commit.as_ref().to_vec()); if is_coinbase { - merkle_proof = Some(get_merkle_proof_for_commit(config, &commit_str).unwrap()); + merkle_proof = + Some(get_merkle_proof_for_commit(wallet.node_url(), &commit_str).unwrap()); } let height = current_chain_height; @@ -248,11 +245,11 @@ fn find_outputs_with_key( wallet_outputs } -pub fn restore(config: &WalletConfig, keychain: &Keychain) -> Result<(), Error> { +pub fn restore(wallet: &mut T) -> Result<(), Error> { // Don't proceed if wallet.dat has anything in it - let is_empty = WalletData::read_wallet(&config.data_file_dir, |wallet_data| { - Ok(wallet_data.outputs.len() == 0) - }).context(ErrorKind::WalletData("could not read wallet"))?; + let is_empty = wallet + .read_wallet(|wallet_data| Ok(wallet_data.outputs().len() == 0)) + .context(ErrorKind::FileWallet("could not read wallet"))?; if !is_empty { error!( LOGGER, @@ -271,7 +268,7 @@ pub fn restore(config: &WalletConfig, keychain: &Keychain) -> Result<(), Error> // this will start here, then lower as outputs are found, moving backwards on // the chain loop { - let output_listing = outputs_batch(config, start_index, batch_size)?; + let output_listing = outputs_batch(wallet, start_index, batch_size)?; info!( LOGGER, "Retrieved {} outputs, up to index {}. (Highest index: {})", @@ -280,10 +277,10 @@ pub fn restore(config: &WalletConfig, keychain: &Keychain) -> Result<(), Error> output_listing.highest_index ); - let _ = WalletData::with_wallet(&config.data_file_dir, |wallet_data| { + let _ = wallet.with_wallet(|wallet_data| { + let keychain = wallet_data.keychain().clone(); let result_vec = find_outputs_with_key( - config, - keychain, + wallet_data, output_listing.outputs.clone(), &mut found_key_index, ); diff --git a/wallet/src/sender.rs b/wallet/src/sender.rs index bc81103d3..4d1a07397 100644 --- a/wallet/src/sender.rs +++ b/wallet/src/sender.rs @@ -18,11 +18,11 @@ use client; use core::core::amount_to_hr_string; use core::ser; use failure::ResultExt; -use grinwallet::selection; use keychain::{Identifier, Keychain}; -use libwallet::{build, transaction}; +use libtx::{build, tx_fee}; +use libwallet::selection; +use libwallet::types::*; use receiver::TxWrapper; -use types::*; use util; use util::LOGGER; @@ -30,9 +30,8 @@ use util::LOGGER; /// wallet /// Outputs. The destination can be "stdout" (for command line) (currently /// disabled) or a URL to the recipients wallet receiver (to be implemented). -pub fn issue_send_tx( - config: &WalletConfig, - keychain: &Keychain, +pub fn issue_send_tx( + wallet: &mut T, amount: u64, minimum_confirmations: u64, dest: String, @@ -48,13 +47,13 @@ pub fn issue_send_tx( ); } - checker::refresh_outputs(config, keychain)?; + checker::refresh_outputs(wallet)?; // Get lock height - let chain_tip = checker::get_tip_from_node(config)?; + let chain_tip = checker::get_tip_from_node(wallet.node_url())?; let current_height = chain_tip.height; // ensure outputs we're selecting are up to date - checker::refresh_outputs(config, keychain)?; + checker::refresh_outputs(wallet)?; let lock_height = current_height; @@ -66,8 +65,7 @@ pub fn issue_send_tx( // This function is just a big helper to do all of that, in theory // this process can be split up in any way let (mut slate, mut context, sender_lock_fn) = selection::build_send_tx_slate( - config, - keychain, + wallet, 2, amount, current_height, @@ -81,7 +79,12 @@ pub fn issue_send_tx( // the offset in the slate's transaction kernel, and adds our public key // information to the slate let _ = slate - .fill_round_1(keychain, &mut context.sec_key, &context.sec_nonce, 0) + .fill_round_1( + wallet.keychain(), + &mut context.sec_key, + &context.sec_nonce, + 0, + ) .unwrap(); let url = format!("{}/v1/receive/transaction", &dest); @@ -109,48 +112,46 @@ pub fn issue_send_tx( }; let _ = slate - .fill_round_2(keychain, &context.sec_key, &context.sec_nonce, 0) + .fill_round_2(wallet.keychain(), &context.sec_key, &context.sec_nonce, 0) .context(ErrorKind::LibWalletError)?; // Final transaction can be built by anyone at this stage - slate.finalize(keychain).context(ErrorKind::LibWalletError)?; + slate + .finalize(wallet.keychain()) + .context(ErrorKind::LibWalletError)?; // So let's post it let tx_hex = util::to_hex(ser::ser_vec(&slate.tx).unwrap()); let url; if fluff { - url = format!( - "{}/v1/pool/push?fluff", - config.check_node_api_http_addr.as_str() - ); + url = format!("{}/v1/pool/push?fluff", wallet.node_url(),); } else { - url = format!("{}/v1/pool/push", config.check_node_api_http_addr.as_str()); + url = format!("{}/v1/pool/push", wallet.node_url()); } api::client::post(url.as_str(), &TxWrapper { tx_hex: tx_hex }).context(ErrorKind::Node)?; // All good so, lock our inputs - sender_lock_fn()?; + sender_lock_fn(wallet)?; Ok(()) } -pub fn issue_burn_tx( - config: &WalletConfig, - keychain: &Keychain, +pub fn issue_burn_tx( + wallet: &mut T, amount: u64, minimum_confirmations: u64, max_outputs: usize, ) -> Result<(), Error> { - let keychain = &Keychain::burn_enabled(keychain, &Identifier::zero()); + let keychain = &Keychain::burn_enabled(wallet.keychain(), &Identifier::zero()); - let chain_tip = checker::get_tip_from_node(config)?; + let chain_tip = checker::get_tip_from_node(wallet.node_url())?; let current_height = chain_tip.height; - let _ = checker::refresh_outputs(config, keychain); + let _ = checker::refresh_outputs(wallet); let key_id = keychain.root_key_id(); // select some spendable coins from the wallet - let coins = WalletData::read_wallet(&config.data_file_dir, |wallet_data| { + let coins = wallet.read_wallet(|wallet_data| { Ok(wallet_data.select_coins( key_id.clone(), amount, @@ -163,9 +164,8 @@ pub fn issue_burn_tx( debug!(LOGGER, "selected some coins - {}", coins.len()); - let fee = transaction::tx_fee(coins.len(), 2, selection::coins_proof_count(&coins), None); - let (mut parts, _) = - selection::inputs_and_change(&coins, config, keychain, current_height, amount, fee)?; + let fee = tx_fee(coins.len(), 2, selection::coins_proof_count(&coins), None); + let (mut parts, _) = selection::inputs_and_change(&coins, wallet, current_height, amount, fee)?; // add burn output and fees parts.push(build::output(amount - fee, Identifier::zero())); @@ -175,7 +175,7 @@ pub fn issue_burn_tx( tx_burn.validate().context(ErrorKind::Transaction)?; let tx_hex = util::to_hex(ser::ser_vec(&tx_burn).unwrap()); - let url = format!("{}/v1/pool/push", config.check_node_api_http_addr.as_str()); + let url = format!("{}/v1/pool/push", wallet.node_url()); let _: () = api::client::post(url.as_str(), &TxWrapper { tx_hex: tx_hex }).context(ErrorKind::Node)?; Ok(()) @@ -184,7 +184,7 @@ pub fn issue_burn_tx( #[cfg(test)] mod test { use keychain::Keychain; - use libwallet::build; + use libtx::build; #[test] // demonstrate that input.commitment == referenced output.commitment diff --git a/wallet/src/server.rs b/wallet/src/server.rs index 20a38b59d..251c95bb8 100644 --- a/wallet/src/server.rs +++ b/wallet/src/server.rs @@ -14,25 +14,30 @@ use api::ApiServer; use handlers::CoinbaseHandler; -use keychain::Keychain; +use iron::Handler; +use libwallet::types::WalletBackend; use receiver::WalletReceiver; -use types::WalletConfig; +use std::sync::{Arc, RwLock}; use util::LOGGER; -pub fn start_rest_apis(wallet_config: WalletConfig, keychain: Keychain) { +pub fn start_rest_apis(in_wallet: T, api_listen_addr: &str) +where + T: WalletBackend, + CoinbaseHandler: Handler, + WalletReceiver: Handler, +{ info!( LOGGER, - "Starting the Grin wallet receiving daemon at {}...", - wallet_config.api_listen_addr() + "Starting the Grin wallet receiving daemon at {}...", api_listen_addr ); + let wallet = Arc::new(RwLock::new(in_wallet)); + let receive_tx_handler = WalletReceiver { - config: wallet_config.clone(), - keychain: keychain.clone(), + wallet: wallet.clone(), }; let coinbase_handler = CoinbaseHandler { - config: wallet_config.clone(), - keychain: keychain.clone(), + wallet: wallet.clone(), }; let router = router!( @@ -42,7 +47,7 @@ pub fn start_rest_apis(wallet_config: WalletConfig, keychain: Keychain) { let mut apis = ApiServer::new("/v1".to_string()); apis.register_handler(router); - match apis.start(wallet_config.api_listen_addr()) { + match apis.start(api_listen_addr) { Err(e) => error!(LOGGER, "Failed to start Grin wallet listener: {}.", e), Ok(_) => info!(LOGGER, "Wallet listener started"), }; diff --git a/wallet/src/types.rs b/wallet/src/types.rs deleted file mode 100644 index 552b1e4e9..000000000 --- a/wallet/src/types.rs +++ /dev/null @@ -1,803 +0,0 @@ -// 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 blake2; -use rand::{thread_rng, Rng}; -use std::cmp::min; -use std::collections::HashMap; -use std::convert::From; -use std::fmt; -use std::fmt::Display; -use std::fs::{self, File}; -use std::io::{Read, Write}; -use std::path::MAIN_SEPARATOR; -use std::path::Path; - -use serde; -use serde_json; -use tokio_core::reactor; -use tokio_retry::Retry; -use tokio_retry::strategy::FibonacciBackoff; - -use failure::{Backtrace, Context, Fail, ResultExt}; - -use core::core::hash::Hash; -use core::core::pmmr::MerkleProof; -use keychain; -use util; -use util::LOGGER; - -const DAT_FILE: &'static str = "wallet.dat"; -const BCK_FILE: &'static str = "wallet.bck"; -const LOCK_FILE: &'static str = "wallet.lock"; -const SEED_FILE: &'static str = "wallet.seed"; - -#[derive(Debug)] -pub struct Error { - inner: Context, -} - -/// Wallet errors, mostly wrappers around underlying crypto or I/O errors. -#[derive(Copy, Clone, Eq, PartialEq, Debug, Fail)] -pub enum ErrorKind { - #[fail(display = "Not enough funds")] - NotEnoughFunds(u64), - - #[fail(display = "Fee dispute: sender fee {}, recipient fee {}", sender_fee, recipient_fee)] - FeeDispute { sender_fee: u64, recipient_fee: u64 }, - - #[fail(display = "Fee exceeds amount: sender amount {}, recipient fee {}", sender_amount, - recipient_fee)] - FeeExceedsAmount { - sender_amount: u64, - recipient_fee: u64, - }, - - #[fail(display = "Keychain error")] - Keychain, - - #[fail(display = "Transaction error")] - Transaction, - - #[fail(display = "Secp error")] - Secp, - - #[fail(display = "LibWallet error")] - LibWalletError, - - #[fail(display = "Wallet data error: {}", _0)] - WalletData(&'static str), - - /// An error in the format of the JSON structures exchanged by the wallet - #[fail(display = "JSON format error")] - Format, - - #[fail(display = "I/O error")] - IO, - - /// Error when contacting a node through its API - #[fail(display = "Node API error")] - Node, - - /// Error originating from hyper. - #[fail(display = "Hyper error")] - Hyper, - - /// Error originating from hyper uri parsing. - #[fail(display = "Uri parsing error")] - Uri, - - #[fail(display = "Signature error")] - Signature(&'static str), - - /// Attempt to use duplicate transaction id in separate transactions - #[fail(display = "Duplicate transaction ID error")] - DuplicateTransactionId, - - /// Wallet seed already exists - #[fail(display = "Wallet seed exists error")] - WalletSeedExists, - - /// Wallet seed doesn't exist - #[fail(display = "Wallet seed doesn't exist error")] - WalletSeedDoesntExist, - - #[fail(display = "Generic error: {}", _0)] - GenericError(&'static str), -} - -impl Fail for Error { - fn cause(&self) -> Option<&Fail> { - self.inner.cause() - } - - fn backtrace(&self) -> Option<&Backtrace> { - self.inner.backtrace() - } -} - -impl Display for Error { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - Display::fmt(&self.inner, f) - } -} - -impl Error { - pub fn kind(&self) -> ErrorKind { - *self.inner.get_context() - } -} - -impl From for Error { - fn from(kind: ErrorKind) -> Error { - Error { - inner: Context::new(kind), - } - } -} - -impl From> for Error { - fn from(inner: Context) -> Error { - Error { inner: inner } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct WalletConfig { - // Right now the decision to run or not a wallet is based on the command. - // This may change in the near-future. - // pub enable_wallet: bool, - - // The api interface/ip_address that this api server (i.e. this wallet) will run - // by default this is 127.0.0.1 (and will not accept connections from external clients) - pub api_listen_interface: String, - // The port this wallet will run on - pub api_listen_port: u16, - // The api address of a running server node against which transaction inputs - // will be checked during send - pub check_node_api_http_addr: String, - // The directory in which wallet files are stored - pub data_file_dir: String, -} - -impl Default for WalletConfig { - fn default() -> WalletConfig { - WalletConfig { - // enable_wallet: false, - api_listen_interface: "127.0.0.1".to_string(), - api_listen_port: 13415, - check_node_api_http_addr: "http://127.0.0.1:13413".to_string(), - data_file_dir: ".".to_string(), - } - } -} - -impl WalletConfig { - pub fn api_listen_addr(&self) -> String { - format!("{}:{}", self.api_listen_interface, self.api_listen_port) - } -} - -/// Status of an output that's being tracked by the wallet. Can either be -/// unconfirmed, spent, unspent, or locked (when it's been used to generate -/// a transaction but we don't have confirmation that the transaction was -/// broadcasted or mined). -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Ord, PartialOrd)] -pub enum OutputStatus { - Unconfirmed, - Unspent, - Locked, - Spent, -} - -impl fmt::Display for OutputStatus { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match *self { - OutputStatus::Unconfirmed => write!(f, "Unconfirmed"), - OutputStatus::Unspent => write!(f, "Unspent"), - OutputStatus::Locked => write!(f, "Locked"), - OutputStatus::Spent => write!(f, "Spent"), - } - } -} - -#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord)] -pub struct MerkleProofWrapper(pub MerkleProof); - -impl MerkleProofWrapper { - pub fn merkle_proof(&self) -> MerkleProof { - self.0.clone() - } -} - -impl serde::ser::Serialize for MerkleProofWrapper { - fn serialize(&self, serializer: S) -> Result - where - S: serde::ser::Serializer, - { - serializer.serialize_str(&self.0.to_hex()) - } -} - -impl<'de> serde::de::Deserialize<'de> for MerkleProofWrapper { - fn deserialize(deserializer: D) -> Result - where - D: serde::de::Deserializer<'de>, - { - deserializer.deserialize_str(MerkleProofWrapperVisitor) - } -} - -struct MerkleProofWrapperVisitor; - -impl<'de> serde::de::Visitor<'de> for MerkleProofWrapperVisitor { - type Value = MerkleProofWrapper; - - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str("a merkle proof") - } - - fn visit_str(self, s: &str) -> Result - where - E: serde::de::Error, - { - let merkle_proof = MerkleProof::from_hex(s).unwrap(); - Ok(MerkleProofWrapper(merkle_proof)) - } -} - -#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord)] -pub struct BlockIdentifier(pub Hash); - -impl BlockIdentifier { - pub fn hash(&self) -> Hash { - self.0 - } - - pub fn from_hex(hex: &str) -> Result { - let hash = Hash::from_hex(hex).context(ErrorKind::GenericError("Invalid hex"))?; - Ok(BlockIdentifier(hash)) - } -} - -impl serde::ser::Serialize for BlockIdentifier { - fn serialize(&self, serializer: S) -> Result - where - S: serde::ser::Serializer, - { - serializer.serialize_str(&self.0.to_hex()) - } -} - -impl<'de> serde::de::Deserialize<'de> for BlockIdentifier { - fn deserialize(deserializer: D) -> Result - where - D: serde::de::Deserializer<'de>, - { - deserializer.deserialize_str(BlockIdentifierVisitor) - } -} - -struct BlockIdentifierVisitor; - -impl<'de> serde::de::Visitor<'de> for BlockIdentifierVisitor { - type Value = BlockIdentifier; - - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str("a block hash") - } - - fn visit_str(self, s: &str) -> Result - where - E: serde::de::Error, - { - let block_hash = Hash::from_hex(s).unwrap(); - Ok(BlockIdentifier(block_hash)) - } -} - -/// Information about an output that's being tracked by the wallet. Must be -/// enough to reconstruct the commitment associated with the ouput when the -/// root private key is known. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, PartialOrd, Eq, Ord)] -pub struct OutputData { - /// Root key_id that the key for this output is derived from - pub root_key_id: keychain::Identifier, - /// Derived key for this output - pub key_id: keychain::Identifier, - /// How many derivations down from the root key - pub n_child: u32, - /// Value of the output, necessary to rebuild the commitment - pub value: u64, - /// Current status of the output - pub status: OutputStatus, - /// Height of the output - pub height: u64, - /// Height we are locked until - pub lock_height: u64, - /// Is this a coinbase output? Is it subject to coinbase locktime? - pub is_coinbase: bool, - /// Hash of the block this output originated from. - pub block: Option, - pub merkle_proof: Option, -} - -impl OutputData { - /// Lock a given output to avoid conflicting use - fn lock(&mut self) { - self.status = OutputStatus::Locked; - } - - /// How many confirmations has this output received? - /// If height == 0 then we are either Unconfirmed or the output was - /// cut-through - /// so we do not actually know how many confirmations this output had (and - /// never will). - pub fn num_confirmations(&self, current_height: u64) -> u64 { - if self.status == OutputStatus::Unconfirmed { - 0 - } else if self.height == 0 { - 0 - } else { - // if an output has height n and we are at block n - // then we have a single confirmation (the block it originated in) - 1 + (current_height - self.height) - } - } - - /// Check if output is eligible to spend based on state and height and - /// confirmations - pub fn eligible_to_spend(&self, current_height: u64, minimum_confirmations: u64) -> bool { - if [OutputStatus::Spent, OutputStatus::Locked].contains(&self.status) { - return false; - } else if self.status == OutputStatus::Unconfirmed && self.is_coinbase { - return false; - } else if self.is_coinbase && self.block.is_none() { - // if we do not have a block hash for coinbase output we cannot spent it - // block index got compacted before we refreshed our wallet? - return false; - } else if self.is_coinbase && self.merkle_proof.is_none() { - // if we do not have a Merkle proof for coinbase output we cannot spent it - // block index got compacted before we refreshed our wallet? - return false; - } else if self.lock_height > current_height { - return false; - } else if self.status == OutputStatus::Unspent - && self.num_confirmations(current_height) >= minimum_confirmations - { - return true; - } else if self.status == OutputStatus::Unconfirmed && minimum_confirmations == 0 { - return true; - } else { - return false; - } - } - - /// Marks this output as unspent if it was previously unconfirmed - pub fn mark_unspent(&mut self) { - match self.status { - OutputStatus::Unconfirmed => self.status = OutputStatus::Unspent, - _ => (), - } - } - - pub fn mark_spent(&mut self) { - match self.status { - OutputStatus::Unspent => self.status = OutputStatus::Spent, - OutputStatus::Locked => self.status = OutputStatus::Spent, - _ => (), - } - } -} - -#[derive(Clone, PartialEq)] -pub struct WalletSeed([u8; 32]); - -impl WalletSeed { - pub fn from_bytes(bytes: &[u8]) -> WalletSeed { - let mut seed = [0; 32]; - for i in 0..min(32, bytes.len()) { - seed[i] = bytes[i]; - } - WalletSeed(seed) - } - - fn from_hex(hex: &str) -> Result { - let bytes = - util::from_hex(hex.to_string()).context(ErrorKind::GenericError("Invalid hex"))?; - Ok(WalletSeed::from_bytes(&bytes)) - } - - pub fn to_hex(&self) -> String { - util::to_hex(self.0.to_vec()) - } - - pub fn derive_keychain(&self, password: &str) -> Result { - let seed = blake2::blake2b::blake2b(64, &password.as_bytes(), &self.0); - let result = keychain::Keychain::from_seed(seed.as_bytes()).context(ErrorKind::Keychain)?; - Ok(result) - } - - pub fn init_new() -> WalletSeed { - let seed: [u8; 32] = thread_rng().gen(); - WalletSeed(seed) - } - - pub fn init_file(wallet_config: &WalletConfig) -> Result { - // create directory if it doesn't exist - fs::create_dir_all(&wallet_config.data_file_dir).context(ErrorKind::IO)?; - - let seed_file_path = &format!( - "{}{}{}", - wallet_config.data_file_dir, MAIN_SEPARATOR, SEED_FILE, - ); - - debug!(LOGGER, "Generating wallet seed file at: {}", seed_file_path,); - - if Path::new(seed_file_path).exists() { - Err(ErrorKind::WalletSeedExists)? - } else { - let seed = WalletSeed::init_new(); - let mut file = File::create(seed_file_path).context(ErrorKind::IO)?; - file.write_all(&seed.to_hex().as_bytes()) - .context(ErrorKind::IO)?; - Ok(seed) - } - } - - pub fn from_file(wallet_config: &WalletConfig) -> Result { - // create directory if it doesn't exist - fs::create_dir_all(&wallet_config.data_file_dir).context(ErrorKind::IO)?; - - let seed_file_path = &format!( - "{}{}{}", - wallet_config.data_file_dir, MAIN_SEPARATOR, SEED_FILE, - ); - - debug!(LOGGER, "Using wallet seed file at: {}", seed_file_path,); - - if Path::new(seed_file_path).exists() { - let mut file = File::open(seed_file_path).context(ErrorKind::IO)?; - let mut buffer = String::new(); - file.read_to_string(&mut buffer).context(ErrorKind::IO)?; - let wallet_seed = WalletSeed::from_hex(&buffer)?; - Ok(wallet_seed) - } else { - error!( - LOGGER, - "wallet seed file {} could not be opened (grin wallet init). \ - Run \"grin wallet init\" to initialize a new wallet.", - seed_file_path - ); - Err(ErrorKind::WalletSeedDoesntExist)? - } - } -} - -/// Wallet information tracking all our outputs. Based on HD derivation and -/// avoids storing any key data, only storing output amounts and child index. -#[derive(Serialize, Deserialize, Debug, Clone)] -pub struct WalletData { - pub outputs: HashMap, -} - -impl WalletData { - /// Allows for reading wallet data (without needing to acquire the write - /// lock). - pub fn read_wallet(data_file_dir: &str, f: F) -> Result - where - F: FnOnce(&WalletData) -> Result, - { - // open the wallet readonly and do what needs to be done with it - let data_file_path = &format!("{}{}{}", data_file_dir, MAIN_SEPARATOR, DAT_FILE); - let wdat = WalletData::read_or_create(data_file_path)?; - f(&wdat) - } - - /// Allows the reading and writing of the wallet data within a file lock. - /// Just provide a closure taking a mutable WalletData. The lock should - /// be held for as short a period as possible to avoid contention. - /// Note that due to the impossibility to do an actual file lock easily - /// across operating systems, this just creates a lock file with a "should - /// not exist" option. - pub fn with_wallet(data_file_dir: &str, f: F) -> Result - where - F: FnOnce(&mut WalletData) -> T, - { - // create directory if it doesn't exist - fs::create_dir_all(data_file_dir).unwrap_or_else(|why| { - info!(LOGGER, "! {:?}", why.kind()); - }); - - let data_file_path = &format!("{}{}{}", data_file_dir, MAIN_SEPARATOR, DAT_FILE); - let backup_file_path = &format!("{}{}{}", data_file_dir, MAIN_SEPARATOR, BCK_FILE); - let lock_file_path = &format!("{}{}{}", data_file_dir, MAIN_SEPARATOR, LOCK_FILE); - - info!(LOGGER, "Acquiring wallet lock ..."); - - let action = || { - trace!(LOGGER, "making lock file for wallet lock"); - fs::create_dir(lock_file_path) - }; - - // use tokio_retry to cleanly define some retry logic - let mut core = reactor::Core::new().unwrap(); - let retry_strategy = FibonacciBackoff::from_millis(1000).take(10); - let retry_future = Retry::spawn(core.handle(), retry_strategy, action); - let retry_result = core.run(retry_future); - - match retry_result { - Ok(_) => {} - Err(e) => { - error!( - LOGGER, - "Failed to acquire wallet lock file (multiple retries)", - ); - return Err( - e.context(ErrorKind::WalletData("Failed to acquire lock file")) - .into(), - ); - } - } - - // We successfully acquired the lock - so do what needs to be done. - let mut wdat = WalletData::read_or_create(data_file_path)?; - wdat.write(backup_file_path)?; - let res = f(&mut wdat); - wdat.write(data_file_path)?; - - // delete the lock file - fs::remove_dir(lock_file_path).context(ErrorKind::WalletData( - "Could not remove wallet lock file. Maybe insufficient rights?", - ))?; - - info!(LOGGER, "... released wallet lock"); - - Ok(res) - } - - /// Read the wallet data or created a brand new one if it doesn't exist yet - fn read_or_create(data_file_path: &str) -> Result { - if Path::new(data_file_path).exists() { - WalletData::read(data_file_path) - } else { - // just create a new instance, it will get written afterward - Ok(WalletData { - outputs: HashMap::new(), - }) - } - } - - /// Read output_data vec from disk. - fn read_outputs(data_file_path: &str) -> Result, Error> { - let data_file = File::open(data_file_path) - .context(ErrorKind::WalletData(&"Could not open wallet file"))?; - serde_json::from_reader(data_file).map_err(|e| { - e.context(ErrorKind::WalletData(&"Error reading wallet file ")) - .into() - }) - } - - /// Populate wallet_data with output_data from disk. - fn read(data_file_path: &str) -> Result { - let outputs = WalletData::read_outputs(data_file_path)?; - let mut wallet_data = WalletData { - outputs: HashMap::new(), - }; - for out in outputs { - wallet_data.add_output(out); - } - Ok(wallet_data) - } - - /// 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| e.context(ErrorKind::WalletData(&"Could not create ")))?; - let mut outputs = self.outputs.values().collect::>(); - outputs.sort(); - let res_json = serde_json::to_vec_pretty(&outputs) - .map_err(|e| e.context(ErrorKind::WalletData("Error serializing wallet data")))?; - data_file - .write_all(res_json.as_slice()) - .context(ErrorKind::WalletData(&"Error writing wallet file")) - .map_err(|e| e.into()) - } - - /// Append a new output data to the wallet data. - /// TODO - we should check for overwriting here - only really valid for - /// unconfirmed coinbase - pub fn add_output(&mut self, out: OutputData) { - self.outputs.insert(out.key_id.to_hex(), out.clone()); - } - - // TODO - careful with this, only for Unconfirmed (maybe Locked)? - pub fn delete_output(&mut self, id: &keychain::Identifier) { - self.outputs.remove(&id.to_hex()); - } - - /// Lock an output data. - /// TODO - we should track identifier on these outputs (not just n_child) - pub fn lock_output(&mut self, out: &OutputData) { - if let Some(out_to_lock) = self.outputs.get_mut(&out.key_id.to_hex()) { - if out_to_lock.value == out.value { - out_to_lock.lock() - } - } - } - - pub fn get_output(&self, key_id: &keychain::Identifier) -> Option<&OutputData> { - self.outputs.get(&key_id.to_hex()) - } - - /// Select spendable coins from the wallet. - /// Default strategy is to spend the maximum number of outputs (up to - /// max_outputs). Alternative strategy is to spend smallest outputs first - /// but only as many as necessary. When we introduce additional strategies - /// we should pass something other than a bool in. - pub fn select_coins( - &self, - root_key_id: keychain::Identifier, - amount: u64, - current_height: u64, - minimum_confirmations: u64, - max_outputs: usize, - select_all: bool, - ) -> Vec { - // first find all eligible outputs based on number of confirmations - let mut eligible = self.outputs - .values() - .filter(|out| { - out.root_key_id == root_key_id - && out.eligible_to_spend(current_height, minimum_confirmations) - }) - .cloned() - .collect::>(); - - // sort eligible outputs by increasing value - eligible.sort_by_key(|out| out.value); - - // use a sliding window to identify potential sets of possible outputs to spend - // Case of amount > total amount of max_outputs(500): - // The limit exists because by default, we always select as many inputs as - // possible in a transaction, to reduce both the Output set and the fees. - // But that only makes sense up to a point, hence the limit to avoid being too - // greedy. But if max_outputs(500) is actually not enought to cover the whole - // amount, the wallet should allow going over it to satisfy what the user - // wants to send. So the wallet considers max_outputs more of a soft limit. - if eligible.len() > max_outputs { - for window in eligible.windows(max_outputs) { - let windowed_eligibles = window.iter().cloned().collect::>(); - if let Some(outputs) = self.select_from(amount, select_all, windowed_eligibles) { - return outputs; - } - } - // Not exist in any window of which total amount >= amount. - // Then take coins from the smallest one up to the total amount of selected - // coins = the amount. - if let Some(outputs) = self.select_from(amount, false, eligible.clone()) { - debug!( - LOGGER, - "Extending maximum number of outputs. {} outputs selected.", - outputs.len() - ); - return outputs; - } - } else { - if let Some(outputs) = self.select_from(amount, select_all, eligible.clone()) { - return outputs; - } - } - - // we failed to find a suitable set of outputs to spend, - // so return the largest amount we can so we can provide guidance on what is - // possible - eligible.reverse(); - eligible.iter().take(max_outputs).cloned().collect() - } - - // Select the full list of outputs if we are using the select_all strategy. - // Otherwise select just enough outputs to cover the desired amount. - fn select_from( - &self, - amount: u64, - select_all: bool, - outputs: Vec, - ) -> Option> { - let total = outputs.iter().fold(0, |acc, x| acc + x.value); - if total >= amount { - if select_all { - return Some(outputs.iter().cloned().collect()); - } else { - let mut selected_amount = 0; - return Some( - outputs - .iter() - .take_while(|out| { - let res = selected_amount < amount; - selected_amount += out.value; - res - }) - .cloned() - .collect(), - ); - } - } else { - None - } - } - - /// Next child index when we want to create a new output. - pub fn next_child(&self, root_key_id: keychain::Identifier) -> u32 { - let mut max_n = 0; - for out in self.outputs.values() { - if max_n < out.n_child && out.root_key_id == root_key_id { - max_n = out.n_child; - } - } - max_n + 1 - } -} - -/// Amount in request to build a coinbase output. -#[derive(Serialize, Deserialize, Debug, Clone)] -pub enum WalletReceiveRequest { - Coinbase(BlockFees), - PartialTransaction(String), - Finalize(String), -} - -/// Fees in block to use for coinbase amount calculation -#[derive(Serialize, Deserialize, Debug, Clone)] -pub struct BlockFees { - pub fees: u64, - pub height: u64, - pub key_id: Option, -} - -impl BlockFees { - pub fn key_id(&self) -> Option { - self.key_id.clone() - } -} - -/// Response to build a coinbase output. -#[derive(Serialize, Deserialize, Debug, Clone)] -pub struct CbData { - pub output: String, - pub kernel: String, - pub key_id: String, -} - -/// a contained wallet info struct, so automated tests can parse wallet info -/// can add more fields here over time as needed -#[derive(Serialize, Deserialize, Debug, Clone)] -pub struct WalletInfo { - // height from which info was taken - pub current_height: u64, - // total amount in the wallet - pub total: u64, - // amount awaiting confirmation - pub amount_awaiting_confirmation: u64, - // confirmed but locked - pub amount_confirmed_but_locked: u64, - // amount currently spendable - pub amount_currently_spendable: u64, - // amount locked by previous transactions - pub amount_locked: u64, - // whether the data was confirmed against a live node - pub data_confirmed: bool, - // node confirming the data - pub data_confirmed_from: String, -} diff --git a/wallet/tests/common/mod.rs b/wallet/tests/common/mod.rs index 459328d4e..8992d9543 100644 --- a/wallet/tests/common/mod.rs +++ b/wallet/tests/common/mod.rs @@ -27,21 +27,19 @@ use chain::Chain; use core::core::hash::Hashed; use core::core::{Output, OutputFeatures, OutputIdentifier, Transaction, TxKernel}; use core::{consensus, global, pow}; -use keychain::Keychain; -use wallet::types::{BlockIdentifier, Error, ErrorKind, MerkleProofWrapper, OutputStatus, - WalletConfig, WalletData}; +use wallet::file_wallet::*; +use wallet::libwallet::types::*; use wallet::{checker, BlockFees}; use util::secp::pedersen; /// Mostly for testing, refreshes output state against a local chain instance /// instead of via an http API call -pub fn refresh_output_state_local( - config: &WalletConfig, - keychain: &Keychain, +pub fn refresh_output_state_local( + wallet: &mut T, chain: &chain::Chain, ) -> Result<(), Error> { - let wallet_outputs = checker::map_wallet_outputs(config, keychain)?; + let wallet_outputs = checker::map_wallet_outputs(wallet)?; let chain_outputs: Vec> = wallet_outputs .keys() .map(|k| match get_output_local(chain, &k) { @@ -58,7 +56,7 @@ pub fn refresh_output_state_local( None => {} } } - checker::apply_api_outputs(config, &wallet_outputs, &api_outputs)?; + checker::apply_api_outputs(wallet, &wallet_outputs, &api_outputs)?; Ok(()) } @@ -66,18 +64,18 @@ pub fn refresh_output_state_local( /// (0:total, 1:amount_awaiting_confirmation, 2:confirmed but locked, /// 3:currently_spendable, 4:locked total) TODO: Should be a wallet lib /// function with nicer return values -pub fn get_wallet_balances( - config: &WalletConfig, - keychain: &Keychain, +pub fn get_wallet_balances( + wallet: &mut T, height: u64, ) -> Result<(u64, u64, u64, u64, u64), Error> { - let ret_val = WalletData::read_wallet(&config.data_file_dir, |wallet_data| { + let ret_val = wallet.read_wallet(|wallet_data| { let mut unspent_total = 0; let mut unspent_but_locked_total = 0; let mut unconfirmed_total = 0; let mut locked_total = 0; + let keychain = wallet_data.keychain().clone(); for out in wallet_data - .outputs + .outputs() .values() .filter(|out| out.root_key_id == keychain.root_key_id()) { @@ -144,10 +142,10 @@ pub fn add_block_with_reward(chain: &Chain, txs: Vec<&Transaction>, reward: (Out /// adds a reward output to a wallet, includes that reward in a block, mines /// the block and adds it to the chain, with option transactions included. /// Helpful for building up precise wallet balances for testing. -pub fn award_block_to_wallet( +pub fn award_block_to_wallet( chain: &Chain, txs: Vec<&Transaction>, - wallet: &(WalletConfig, Keychain), + wallet: &mut T, ) { let prev = chain.head_header().unwrap(); let fee_amt = txs.iter().map(|tx| tx.fee()).sum(); @@ -156,7 +154,7 @@ pub fn award_block_to_wallet( key_id: None, height: prev.height + 1, }; - let coinbase_tx = wallet::receiver::receive_coinbase(&wallet.0, &wallet.1, &fees); + let coinbase_tx = wallet::receiver::receive_coinbase(wallet, &fees); let (coinbase_tx, fees) = match coinbase_tx { Ok(t) => ((t.0, t.1), t.2), Err(e) => { @@ -168,9 +166,9 @@ pub fn award_block_to_wallet( let output_id = OutputIdentifier::from_output(&coinbase_tx.0.clone()); let m_proof = chain.get_merkle_proof(&output_id, &chain.head_header().unwrap()); let block_id = Some(BlockIdentifier(chain.head_header().unwrap().hash())); - let _ = WalletData::with_wallet(&wallet.0.data_file_dir, |wallet_data| { + let _ = wallet.with_wallet(|wallet_data| { if let Entry::Occupied(mut output) = wallet_data - .outputs + .outputs() .entry(fees.key_id.as_ref().unwrap().to_hex()) { let output = output.get_mut(); @@ -181,23 +179,20 @@ pub fn award_block_to_wallet( } /// adds many block rewards to a wallet, no transactions -pub fn award_blocks_to_wallet( - chain: &Chain, - wallet: &(WalletConfig, Keychain), - num_rewards: usize, -) { +pub fn award_blocks_to_wallet(chain: &Chain, wallet: &mut T, num_rewards: usize) { for _ in 0..num_rewards { award_block_to_wallet(chain, vec![], wallet); } } /// Create a new wallet in a particular directory -pub fn create_wallet(dir: &str) -> (WalletConfig, Keychain) { +pub fn create_wallet(dir: &str) -> FileWallet { let mut wallet_config = WalletConfig::default(); wallet_config.data_file_dir = String::from(dir); let wallet_seed = wallet::WalletSeed::init_file(&wallet_config).unwrap(); let keychain = wallet_seed .derive_keychain("") .expect("Failed to derive keychain from seed file and passphrase."); - (wallet_config, keychain) + FileWallet::new(wallet_config.clone(), keychain) + .unwrap_or_else(|e| panic!("Error creating wallet: {:?} Config: {:?}", e, wallet_config)) } diff --git a/wallet/tests/libwallet.rs b/wallet/tests/libwallet.rs index f23f4c73a..8e1d21b21 100644 --- a/wallet/tests/libwallet.rs +++ b/wallet/tests/libwallet.rs @@ -11,7 +11,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -//! libwallet specific tests +//! libtx specific tests extern crate grin_core as core; extern crate grin_keychain as keychain; extern crate grin_util as util; @@ -24,8 +24,8 @@ use keychain::{BlindSum, BlindingFactor, Keychain}; use util::secp::key::{PublicKey, SecretKey}; use util::secp::pedersen::ProofMessage; use util::{kernel_sig_msg, secp}; -use wallet::grinwallet::sigcontext; -use wallet::libwallet::{aggsig, proof}; +use wallet::libtx::{aggsig, proof}; +use wallet::libwallet::sigcontext; use rand::thread_rng; diff --git a/wallet/tests/transaction.rs b/wallet/tests/transaction.rs index fe887e0bb..951bacad7 100644 --- a/wallet/tests/transaction.rs +++ b/wallet/tests/transaction.rs @@ -11,7 +11,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -//! tests for transactions building within libwallet +//! tests for transactions building within libtx extern crate grin_chain as chain; extern crate grin_core as core; extern crate grin_keychain as keychain; @@ -34,7 +34,7 @@ use chain::types::*; use core::global::ChainTypes; use core::{global, pow}; use util::LOGGER; -use wallet::grinwallet::selection; +use wallet::libwallet::selection; fn clean_output_dir(test_dir: &str) { let _ = fs::remove_dir_all(test_dir); @@ -58,11 +58,11 @@ fn setup(test_dir: &str, chain_dir: &str) -> Chain { #[test] fn build_transaction() { let chain = setup("test_output", "build_transaction_2/.grin"); - let wallet1 = common::create_wallet("test_output/build_transaction_2/wallet1"); - let wallet2 = common::create_wallet("test_output/build_transaction_2/wallet2"); - common::award_blocks_to_wallet(&chain, &wallet1, 10); + let mut wallet1 = common::create_wallet("test_output/build_transaction_2/wallet1"); + let mut wallet2 = common::create_wallet("test_output/build_transaction_2/wallet2"); + common::award_blocks_to_wallet(&chain, &mut wallet1, 10); // Wallet 1 has 600 Grins, wallet 2 has 0. Create a transaction that sends - // 300 Grins from wallet 1 to wallet 2, using libwallet + // 300 Grins from wallet 1 to wallet 2, using libtx // Get lock height let chain_tip = chain.head().unwrap(); @@ -70,7 +70,7 @@ fn build_transaction() { let min_confirmations = 3; // ensure outputs we're selecting are up to date - let res = common::refresh_output_state_local(&wallet1.0, &wallet1.1, &chain); + let res = common::refresh_output_state_local(&mut wallet1, &chain); if let Err(e) = res { panic!("Unable to refresh sender wallet outputs: {}", e); @@ -85,8 +85,7 @@ fn build_transaction() { // This function is just a big helper to do all of that, in theory // this process can be split up in any way let (mut slate, mut sender_context, sender_lock_fn) = selection::build_send_tx_slate( - &wallet1.0, - &wallet1.1, + &mut wallet1, 2, amount, chain_tip.height, @@ -101,7 +100,7 @@ fn build_transaction() { // information to the slate let _ = slate .fill_round_1( - &wallet1.1, + &wallet1.keychain, &mut sender_context.sec_key, &sender_context.sec_nonce, 0, @@ -117,11 +116,11 @@ fn build_transaction() { // Identifier Again, this is a helper to do that, which returns a closure that // creates the output when we're satisified the process was successful let (_, mut recp_context, receiver_create_fn) = - selection::build_recipient_output_with_slate(&wallet2.0, &wallet2.1, &mut slate).unwrap(); + selection::build_recipient_output_with_slate(&mut wallet2, &mut slate).unwrap(); let _ = slate .fill_round_1( - &wallet2.1, + &wallet2.keychain, &mut recp_context.sec_key, &recp_context.sec_nonce, 1, @@ -129,11 +128,11 @@ fn build_transaction() { .unwrap(); // recipient can proceed to round 2 now - let _ = receiver_create_fn(); + let _ = receiver_create_fn(&mut wallet2); let _ = slate .fill_round_2( - &wallet2.1, + &wallet1.keychain, &recp_context.sec_key, &recp_context.sec_nonce, 1, @@ -150,7 +149,7 @@ fn build_transaction() { // SENDER Part 3: Sender confirmation let _ = slate .fill_round_2( - &wallet1.1, + &wallet1.keychain, &sender_context.sec_key, &sender_context.sec_nonce, 0, @@ -162,7 +161,7 @@ fn build_transaction() { debug!(LOGGER, "{:?}", slate); // Final transaction can be built by anyone at this stage - let res = slate.finalize(&wallet1.1); + let res = slate.finalize(&wallet1.keychain); if let Err(e) = res { panic!("Error creating final tx: {:?}", e); @@ -173,30 +172,30 @@ fn build_transaction() { debug!(LOGGER, "{:?}", slate.tx); // All okay, lock sender's outputs - let _ = sender_lock_fn(); + let _ = sender_lock_fn(&mut wallet1); // Insert this transaction into a new block, then mine till confirmation - common::award_block_to_wallet(&chain, vec![&slate.tx], &wallet1); - common::award_blocks_to_wallet(&chain, &wallet1, 5); + common::award_block_to_wallet(&chain, vec![&slate.tx], &mut wallet1); + common::award_blocks_to_wallet(&chain, &mut wallet1, 5); // Refresh wallets - let res = common::refresh_output_state_local(&wallet2.0, &wallet2.1, &chain); + let res = common::refresh_output_state_local(&mut wallet2, &chain); if let Err(e) = res { panic!("Error refreshing output state for wallet: {:?}", e); } // check recipient wallet let chain_tip = chain.head().unwrap(); - let balances = common::get_wallet_balances(&wallet2.0, &wallet2.1, chain_tip.height).unwrap(); + let balances = common::get_wallet_balances(&mut wallet2, chain_tip.height).unwrap(); assert_eq!(balances.3, 300_000_000_000); // check sender wallet - let res = common::refresh_output_state_local(&wallet1.0, &wallet1.1, &chain); + let res = common::refresh_output_state_local(&mut wallet1, &chain); if let Err(e) = res { panic!("Error refreshing output state for wallet: {:?}", e); } - let balances = common::get_wallet_balances(&wallet1.0, &wallet1.1, chain_tip.height).unwrap(); + let balances = common::get_wallet_balances(&mut wallet1, chain_tip.height).unwrap(); println!("tip height: {:?}", chain_tip.height); println!("Sender balances: {:?}", balances); // num blocks * grins per block, and wallet1 mined the fee