From bacadfb5abcb6bc46d1df2b04d46a52320944dd6 Mon Sep 17 00:00:00 2001 From: Yeastplume Date: Thu, 12 Jul 2018 16:49:37 +0100 Subject: [PATCH] Wallet API Test Client (#1242) * beginnings to testclient implementation for more complete wallet API testing * rework TestWalletClient to be message-based proxy * test fix * wallet tests now exercising the API directly as much as possible, capable of instantiating multiple wallets * test in place to run both file and db wallets * ensure all wallet api functions lock wallet as needed. Split up transaction creation from posting to chain --- servers/tests/framework/mod.rs | 62 +++-- src/bin/grin.rs | 43 ++- wallet/src/client.rs | 67 +++-- wallet/src/error.rs | 2 +- wallet/src/file_wallet.rs | 3 +- wallet/src/libwallet/api.rs | 152 +++++++---- wallet/src/libwallet/controller.rs | 98 +++---- wallet/src/libwallet/error.rs | 2 +- wallet/src/libwallet/types.rs | 2 +- wallet/src/types.rs | 4 +- wallet/tests/common/mod.rs | 199 ++++++-------- wallet/tests/common/testclient.rs | 420 +++++++++++++++++++++++++++++ wallet/tests/transaction.rs | 338 +++++++++++------------ 13 files changed, 917 insertions(+), 475 deletions(-) create mode 100644 wallet/tests/common/testclient.rs diff --git a/servers/tests/framework/mod.rs b/servers/tests/framework/mod.rs index 2ffbfa434..f3c18cfb4 100644 --- a/servers/tests/framework/mod.rs +++ b/servers/tests/framework/mod.rs @@ -27,7 +27,7 @@ use std::default::Default; use std::sync::{Arc, Mutex}; use std::{fs, thread, time}; -use wallet::{HTTPWalletClient, FileWallet, WalletConfig}; +use wallet::{FileWallet, HTTPWalletClient, WalletConfig}; /// Just removes all results from previous runs pub fn clean_all_output(test_name_dir: &str) { @@ -276,13 +276,15 @@ impl LocalServerContainer { ) }); - wallet::controller::foreign_listener(Box::new(wallet), &self.wallet_config.api_listen_addr()) - .unwrap_or_else(|e| { - panic!( - "Error creating wallet listener: {:?} Config: {:?}", - e, self.wallet_config - ) - }); + wallet::controller::foreign_listener( + Box::new(wallet), + &self.wallet_config.api_listen_addr(), + ).unwrap_or_else(|e| { + panic!( + "Error creating wallet listener: {:?} Config: {:?}", + e, self.wallet_config + ) + }); self.wallet_is_running = true; } @@ -335,28 +337,30 @@ impl LocalServerContainer { .unwrap_or_else(|e| panic!("Error creating wallet: {:?} Config: {:?}", e, config)); wallet.keychain = Some(keychain); let _ = - wallet::controller::owner_single_use(Box::new(wallet), |api| { - let result = api.issue_send_tx( - amount, - minimum_confirmations, - dest, - max_outputs, - selection_strategy == "all", - fluff, - ); - match result { - Ok(_) => println!( - "Tx sent: {} grin to {} (strategy '{}')", - core::core::amount_to_hr_string(amount), + wallet::controller::owner_single_use( + Arc::new(Mutex::new(Box::new(wallet))), + |api| { + let result = api.issue_send_tx( + amount, + minimum_confirmations, dest, - selection_strategy, - ), - Err(e) => { - println!("Tx not sent to {}: {:?}", dest, e); - } - }; - Ok(()) - }).unwrap_or_else(|e| panic!("Error creating wallet: {:?} Config: {:?}", e, config)); + max_outputs, + selection_strategy == "all", + ); + match result { + Ok(_) => println!( + "Tx sent: {} grin to {} (strategy '{}')", + core::core::amount_to_hr_string(amount), + dest, + selection_strategy, + ), + Err(e) => { + println!("Tx not sent to {}: {:?}", dest, e); + } + }; + Ok(()) + }, + ).unwrap_or_else(|e| panic!("Error creating wallet: {:?} Config: {:?}", e, config)); } /// Stops the running wallet server diff --git a/src/bin/grin.rs b/src/bin/grin.rs index 1fb295e26..d0315cc7a 100644 --- a/src/bin/grin.rs +++ b/src/bin/grin.rs @@ -41,7 +41,7 @@ pub mod tui; use std::env::current_dir; use std::process::exit; use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::Arc; +use std::sync::{Arc, Mutex}; use std::thread; use std::time::Duration; @@ -53,8 +53,10 @@ use core::core::amount_to_hr_string; use core::global; use tui::ui; use util::{init_logger, LoggingConfig, LOGGER}; -use wallet::{libwallet, wallet_db_exists, FileWallet, - HTTPWalletClient, LMDBBackend, WalletConfig, WalletInst}; +use wallet::{ + libwallet, wallet_db_exists, FileWallet, HTTPWalletClient, LMDBBackend, WalletConfig, + WalletInst, +}; // include build information pub mod built_info { @@ -606,8 +608,8 @@ fn wallet_command(wallet_args: &ArgMatches, global_config: GlobalConfig) { info!(LOGGER, "Wallet seed file created"); if use_db { let client = HTTPWalletClient::new(&wallet_config.check_node_api_http_addr); - let _: LMDBBackend = LMDBBackend::new(wallet_config.clone(), "", client) - .unwrap_or_else(|e| { + let _: LMDBBackend = + LMDBBackend::new(wallet_config.clone(), "", client).unwrap_or_else(|e| { panic!( "Error creating DB wallet: {} Config: {:?}", e, wallet_config @@ -658,7 +660,11 @@ fn wallet_command(wallet_args: &ArgMatches, global_config: GlobalConfig) { // Handle single-use (command line) owner commands { - let wallet = instantiate_wallet(wallet_config.clone(), passphrase, use_db); + let wallet = Arc::new(Mutex::new(instantiate_wallet( + wallet_config.clone(), + passphrase, + use_db, + ))); let _res = wallet::controller::owner_single_use(wallet, |api| { match wallet_args.subcommand() { ("send", Some(send_args)) => { @@ -689,21 +695,20 @@ fn wallet_command(wallet_args: &ArgMatches, global_config: GlobalConfig) { dest, max_outputs, selection_strategy == "all", - fluff, ); - match result { - Ok(_) => { + let slate = match result { + Ok(s) => { info!( LOGGER, - "Tx sent: {} grin to {} (strategy '{}')", + "Tx created: {} grin to {} (strategy '{}')", amount_to_hr_string(amount), dest, selection_strategy, ); - Ok(()) + s } Err(e) => { - error!(LOGGER, "Tx not sent: {:?}", e); + error!(LOGGER, "Tx not created: {:?}", e); match e.kind() { // user errors, don't backtrace libwallet::ErrorKind::NotEnoughFunds { .. } => {} @@ -714,6 +719,20 @@ fn wallet_command(wallet_args: &ArgMatches, global_config: GlobalConfig) { error!(LOGGER, "Backtrace: {}", e.backtrace().unwrap()); } }; + panic!(); + } + }; + let result = api.post_tx(&slate, fluff); + match result { + Ok(_) => { + info!( + LOGGER, + "Tx sent", + ); + Ok(()) + } + Err(e) => { + error!(LOGGER, "Tx not sent: {:?}", e); Err(e) } } diff --git a/wallet/src/client.rs b/wallet/src/client.rs index 5702cf386..cc6a098e7 100644 --- a/wallet/src/client.rs +++ b/wallet/src/client.rs @@ -28,8 +28,8 @@ use tokio_core::reactor; use api; use error::{Error, ErrorKind}; -use libwallet; use libtx::slate::Slate; +use libwallet; use util::secp::pedersen; use util::{self, LOGGER}; @@ -40,7 +40,7 @@ pub struct HTTPWalletClient { impl HTTPWalletClient { /// Create a new client that will communicate with the given grin node - pub fn new(node_url:&str) -> HTTPWalletClient { + pub fn new(node_url: &str) -> HTTPWalletClient { HTTPWalletClient { node_url: node_url.to_owned(), } @@ -52,9 +52,14 @@ impl WalletClient for HTTPWalletClient { &self.node_url } - /// Call the wallet API to create a coinbase output for the given block_fees. - /// Will retry based on default "retry forever with backoff" behavior. - fn create_coinbase(&self, dest: &str, block_fees: &BlockFees) -> Result { + /// Call the wallet API to create a coinbase output for the given + /// block_fees. Will retry based on default "retry forever with backoff" + /// behavior. + fn create_coinbase( + &self, + dest: &str, + block_fees: &BlockFees, + ) -> Result { let url = format!("{}/v1/wallet/foreign/build_coinbase", dest); match single_create_coinbase(&url, &block_fees) { Err(e) => { @@ -64,7 +69,9 @@ impl WalletClient for HTTPWalletClient { ); error!(LOGGER, "Underlying Error: {}", e.cause().unwrap()); error!(LOGGER, "Backtrace: {}", e.backtrace().unwrap()); - Err(libwallet::ErrorKind::ClientCallback("Failed to get coinbase"))? + Err(libwallet::ErrorKind::ClientCallback( + "Failed to get coinbase", + ))? } Ok(res) => Ok(res), } @@ -83,35 +90,41 @@ impl WalletClient for HTTPWalletClient { let url = format!("{}/v1/wallet/foreign/receive_tx", dest); debug!(LOGGER, "Posting transaction slate to {}", url); - let mut core = reactor::Core::new() - .context(libwallet::ErrorKind::ClientCallback("Sending transaction: Initialise API"))?; + let mut core = reactor::Core::new().context(libwallet::ErrorKind::ClientCallback( + "Sending transaction: Initialise API", + ))?; let client = hyper::Client::new(&core.handle()); let url_pool = url.to_owned(); let mut req = Request::new( Method::Post, - url_pool.parse::() - .context(libwallet::ErrorKind::ClientCallback("Sending transaction: parsing URL"))? + url_pool + .parse::() + .context(libwallet::ErrorKind::ClientCallback( + "Sending transaction: parsing URL", + ))?, ); req.headers_mut().set(ContentType::json()); - let json = serde_json::to_string(&slate) - .context(libwallet::ErrorKind::ClientCallback("Sending transaction: parsing response"))?; + let json = serde_json::to_string(&slate).context(libwallet::ErrorKind::ClientCallback( + "Sending transaction: parsing response", + ))?; req.set_body(json); let work = client.request(req).and_then(|res| { res.body().concat2().and_then(move |body| { - let slate: Slate = - serde_json::from_slice(&body).map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; + let slate: Slate = serde_json::from_slice(&body) + .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; Ok(slate) }) }); let res = core.run(work) - .context(libwallet::ErrorKind::ClientCallback("Sending transaction: posting request"))?; + .context(libwallet::ErrorKind::ClientCallback( + "Sending transaction: posting request", + ))?; Ok(res) } - /// Posts a transaction to a grin node fn post_tx(&self, tx: &TxWrapper, fluff: bool) -> Result<(), libwallet::Error> { let url; @@ -121,8 +134,9 @@ impl WalletClient for HTTPWalletClient { } else { url = format!("{}/v1/pool/push", dest); } - api::client::post(url.as_str(), tx) - .context(libwallet::ErrorKind::ClientCallback("Posting transaction to node"))?; + api::client::post(url.as_str(), tx).context(libwallet::ErrorKind::ClientCallback( + "Posting transaction to node", + ))?; Ok(()) } @@ -130,8 +144,9 @@ impl WalletClient for HTTPWalletClient { fn get_chain_height(&self) -> Result { let addr = self.node_url(); let url = format!("{}/v1/chain", addr); - let res = api::client::get::(url.as_str()) - .context(libwallet::ErrorKind::ClientCallback("Getting chain height from node"))?; + let res = api::client::get::(url.as_str()).context( + libwallet::ErrorKind::ClientCallback("Getting chain height from node"), + )?; Ok(res.height) } @@ -205,7 +220,9 @@ impl WalletClient for HTTPWalletClient { LOGGER, "get_outputs_by_pmmr_index: unable to contact API {}. Error: {}", addr, e ); - Err(libwallet::ErrorKind::ClientCallback("unable to contact api"))? + Err(libwallet::ErrorKind::ClientCallback( + "unable to contact api", + ))? } } } @@ -230,8 +247,9 @@ pub fn create_coinbase(dest: &str, block_fees: &BlockFees) -> Result Result { - let mut core = - reactor::Core::new().context(ErrorKind::GenericError("Could not create reactor"))?; + let mut core = reactor::Core::new().context(ErrorKind::GenericError( + "Could not create reactor".to_owned(), + ))?; let client = hyper::Client::new(&core.handle()); let mut req = Request::new( @@ -253,7 +271,6 @@ fn single_create_coinbase(url: &str, block_fees: &BlockFees) -> Result Result<(), libwallet::Error> { + debug!(LOGGER, "Closing wallet keychain"); self.keychain = None; Ok(()) } diff --git a/wallet/src/libwallet/api.rs b/wallet/src/libwallet/api.rs index e0823b716..fb9df549d 100644 --- a/wallet/src/libwallet/api.rs +++ b/wallet/src/libwallet/api.rs @@ -18,6 +18,7 @@ //! Still experimental, not sure this is the best way to do this use std::marker::PhantomData; +use std::sync::{Arc, Mutex}; use core::ser; use keychain::Keychain; @@ -31,27 +32,27 @@ use util::{self, LOGGER}; /// Wrapper around internal API functions, containing a reference to /// the wallet/keychain that they're acting upon -pub struct APIOwner<'a, W: ?Sized, C, K> +pub struct APIOwner where - W: 'a + WalletBackend, + W: WalletBackend, C: WalletClient, K: Keychain, { /// Wallet, contains its keychain (TODO: Split these up into 2 traits /// perhaps) - pub wallet: &'a mut Box, + pub wallet: Arc>>, phantom: PhantomData, phantom_c: PhantomData, } -impl<'a, W: ?Sized, C, K> APIOwner<'a, W, C, K> +impl APIOwner where - W: 'a + WalletBackend, + W: WalletBackend, C: WalletClient, K: Keychain, { /// Create new API instance - pub fn new(wallet_in: &'a mut Box) -> APIOwner<'a, W, C, K> { + pub fn new(wallet_in: Arc>>) -> Self { APIOwner { wallet: wallet_in, phantom: PhantomData, @@ -62,18 +63,26 @@ where /// Attempt to update and retrieve outputs /// Return (whether the outputs were validated against a node, OutputData) pub fn retrieve_outputs( - &mut self, + &self, include_spent: bool, refresh_from_node: bool, ) -> Result<(bool, Vec), Error> { + + let mut w = self.wallet.lock().unwrap(); + w.open_with_credentials()?; + let mut validated = false; if refresh_from_node { - validated = self.update_outputs(); + validated = self.update_outputs(&mut w); } - Ok(( + + let res = Ok(( validated, - updater::retrieve_outputs(&mut **self.wallet, include_spent)?, - )) + updater::retrieve_outputs(&mut **w, include_spent)?, + )); + + w.close()?; + res } /// Retrieve summary info for wallet @@ -81,12 +90,19 @@ where &mut self, refresh_from_node: bool, ) -> Result<(bool, WalletInfo), Error> { + let mut w = self.wallet.lock().unwrap(); + w.open_with_credentials()?; + let mut validated = false; if refresh_from_node { - validated = self.update_outputs(); + validated = self.update_outputs(&mut w); } - let wallet_info = updater::retrieve_info(&mut **self.wallet)?; - Ok((validated, wallet_info)) + + let wallet_info = updater::retrieve_info(&mut **w)?; + let res = Ok((validated, wallet_info)); + + w.close()?; + res } /// Issues a send transaction and sends to recipient @@ -97,37 +113,42 @@ where dest: &str, max_outputs: usize, selection_strategy_is_use_all: bool, - fluff: bool, - ) -> Result<(), Error> { + ) -> Result { + let mut w = self.wallet.lock().unwrap(); + w.open_with_credentials()?; + + let client; + let mut slate_out: Slate; + let lock_fn_out; + + client = w.client().clone(); let (slate, context, lock_fn) = tx::create_send_tx( - &mut **self.wallet, + &mut **w, amount, minimum_confirmations, max_outputs, selection_strategy_is_use_all, )?; - let mut slate = match self.wallet.client().send_tx_slate(dest, &slate) { + lock_fn_out = lock_fn; + slate_out = match w.client().send_tx_slate(dest, &slate) { Ok(s) => s, Err(e) => { error!( - LOGGER, - "Communication with receiver failed on SenderInitiation send. Aborting transaction {:?}", - e, - ); + LOGGER, + "Communication with receiver failed on SenderInitiation send. Aborting transaction {:?}", + e, + ); return Err(e)?; } }; - tx::complete_tx(&mut **self.wallet, &mut slate, &context)?; + tx::complete_tx(&mut **w, &mut slate_out, &context)?; - // All good here, so let's post it - let tx_hex = util::to_hex(ser::ser_vec(&slate.tx).unwrap()); - self.wallet.client().post_tx(&TxWrapper { tx_hex: tx_hex }, fluff)?; - - // All good here, lock our inputs - lock_fn(self.wallet)?; - Ok(()) + // lock our inputs + lock_fn_out(&mut **w)?; + w.close()?; + Ok(slate_out) } /// Issue a burn TX @@ -137,40 +158,60 @@ where minimum_confirmations: u64, max_outputs: usize, ) -> Result<(), Error> { - let tx_burn = tx::issue_burn_tx( - &mut **self.wallet, - amount, - minimum_confirmations, - max_outputs, - )?; + let mut w = self.wallet.lock().unwrap(); + w.open_with_credentials()?; + let tx_burn = tx::issue_burn_tx(&mut **w, amount, minimum_confirmations, max_outputs)?; let tx_hex = util::to_hex(ser::ser_vec(&tx_burn).unwrap()); - self.wallet.client().post_tx(&TxWrapper { tx_hex: tx_hex }, false)?; + w.client().post_tx(&TxWrapper { tx_hex: tx_hex }, false)?; + w.close()?; + Ok(()) + } + + /// Posts a transaction to the chain + pub fn post_tx(&self, slate: &Slate, fluff: bool) -> Result<(), Error> { + let tx_hex = util::to_hex(ser::ser_vec(&slate.tx).unwrap()); + let client = { + let mut w = self.wallet.lock().unwrap(); + w.client().clone() + }; + client.post_tx(&TxWrapper { tx_hex: tx_hex }, fluff)?; Ok(()) } /// Attempt to restore contents of wallet pub fn restore(&mut self) -> Result<(), Error> { - self.wallet.restore() + let mut w = self.wallet.lock().unwrap(); + w.open_with_credentials()?; + let res = w.restore(); + w.close()?; + res } /// Retrieve current height from node pub fn node_height(&mut self) -> Result<(u64, bool), Error> { - match self.wallet.client().get_chain_height() { - Ok(height) => Ok((height, true)), + let mut w = self.wallet.lock().unwrap(); + w.open_with_credentials()?; + let res = w.client().get_chain_height(); + match res { + Ok(height) => { + w.close()?; + Ok((height, true)) + }, Err(_) => { let outputs = self.retrieve_outputs(true, false)?; let height = match outputs.1.iter().map(|out| out.height).max() { Some(height) => height, None => 0, }; + w.close()?; Ok((height, false)) } } } /// Attempt to update outputs in wallet, return whether it was successful - fn update_outputs(&mut self) -> bool { - match updater::refresh_outputs(&mut **self.wallet) { + fn update_outputs(&self, w: &mut W ) -> bool { + match updater::refresh_outputs(&mut *w) { Ok(_) => true, Err(_) => false, } @@ -179,41 +220,50 @@ where /// Wrapper around external API functions, intended to communicate /// with other parties -pub struct APIForeign<'a, W: ?Sized, C, K> +pub struct APIForeign where - W: 'a + WalletBackend, + W: WalletBackend, C: WalletClient, K: Keychain, { /// Wallet, contains its keychain (TODO: Split these up into 2 traits /// perhaps) - pub wallet: &'a mut Box, + pub wallet: Arc>>, phantom: PhantomData, phantom_c: PhantomData, } -impl<'a, W: ?Sized, C, K> APIForeign<'a, W, C, K> +impl<'a, W: ?Sized, C, K> APIForeign where W: WalletBackend, C: WalletClient, K: Keychain, { /// Create new API instance - pub fn new(wallet_in: &'a mut Box) -> APIForeign { - APIForeign { + pub fn new(wallet_in: Arc>>) -> Box { + Box::new(APIForeign { wallet: wallet_in, phantom: PhantomData, phantom_c: PhantomData, - } + }) } /// Build a new (potential) coinbase transaction in the wallet pub fn build_coinbase(&mut self, block_fees: &BlockFees) -> Result { - updater::build_coinbase(&mut **self.wallet, block_fees) + let mut w = self.wallet.lock().unwrap(); + w.open_with_credentials()?; + let res = updater::build_coinbase(&mut **w, block_fees); + w.close()?; + res + } /// Receive a transaction from a sender pub fn receive_tx(&mut self, slate: &mut Slate) -> Result<(), Error> { - tx::receive_tx(&mut **self.wallet, slate) + let mut w = self.wallet.lock().unwrap(); + w.open_with_credentials()?; + let res = tx::receive_tx(&mut **w, slate); + w.close()?; + res } } diff --git a/wallet/src/libwallet/controller.rs b/wallet/src/libwallet/controller.rs index 4bba1ad7b..a9935e7f6 100644 --- a/wallet/src/libwallet/controller.rs +++ b/wallet/src/libwallet/controller.rs @@ -32,7 +32,7 @@ use keychain::Keychain; use libtx::slate::Slate; use libwallet::api::{APIForeign, APIOwner}; use libwallet::types::{ - BlockFees, CbData, OutputData, SendTXArgs, WalletBackend, WalletClient, WalletInfo + BlockFees, CbData, OutputData, SendTXArgs, WalletBackend, WalletClient, WalletInfo, }; use libwallet::{Error, ErrorKind}; @@ -40,32 +40,33 @@ use util::LOGGER; /// Instantiate wallet Owner API for a single-use (command line) call /// Return a function containing a loaded API context to call -pub fn owner_single_use(wallet: Box, f: F) -> Result<(), Error> +pub fn owner_single_use( + wallet: Arc>>, + f: F, +) -> Result<(), Error> where T: WalletBackend, F: FnOnce(&mut APIOwner) -> Result<(), Error>, C: WalletClient, K: Keychain, { - let mut w = wallet; - w.open_with_credentials()?; - f(&mut APIOwner::new(&mut w))?; - w.close()?; + f(&mut APIOwner::new(wallet.clone()))?; Ok(()) } /// Instantiate wallet Foreign API for a single-use (command line) call /// Return a function containing a loaded API context to call -pub fn foreign_single_use(wallet: &mut Box, f: F) -> Result<(), Error> +pub fn foreign_single_use( + wallet: Arc>>, + f: F, +) -> Result<(), Error> where T: WalletBackend, F: FnOnce(&mut APIForeign) -> Result<(), Error>, C: WalletClient, K: Keychain, { - wallet.open_with_credentials()?; - f(&mut APIForeign::new(wallet))?; - wallet.close()?; + f(&mut APIForeign::new(wallet.clone()))?; Ok(()) } @@ -193,7 +194,11 @@ where api.node_height() } - fn handle_request(&self, req: &mut Request, api: &mut APIOwner) -> IronResult { + fn handle_request( + &self, + req: &mut Request, + api: &mut APIOwner, + ) -> IronResult { let url = req.url.clone(); let path_elems = url.path(); match *path_elems.last().unwrap() { @@ -218,16 +223,7 @@ where K: Keychain + 'static, { fn handle(&self, req: &mut Request) -> IronResult { - // every request should open with stored credentials, - // do its thing and then de-init whatever secrets have been - // stored - let mut wallet = self.wallet.lock().unwrap(); - wallet.open_with_credentials().map_err(|e| { - error!(LOGGER, "Error opening wallet: {:?}", e); - IronError::new(Fail::compat(e), status::BadRequest) - })?; - let mut w = wallet; - let mut api = APIOwner::new(&mut w); + let mut api = APIOwner::new(self.wallet.clone()); let mut resp_json = self.handle_request(req, &mut api); if !resp_json.is_err() { resp_json @@ -236,9 +232,6 @@ where .headers .set_raw("access-control-allow-origin", vec![b"*".to_vec()]); } - api.wallet - .close() - .map_err(|e| IronError::new(Fail::compat(e), status::BadRequest))?; resp_json } } @@ -271,7 +264,7 @@ where } } - fn issue_send_tx(&self, req: &mut Request, api: &mut APIOwner) -> Result<(), Error> { + fn issue_send_tx(&self, req: &mut Request, api: &mut APIOwner) -> Result { let struct_body = req.get::>(); match struct_body { Ok(Some(args)) => api.issue_send_tx( @@ -280,18 +273,17 @@ where &args.dest, args.max_outputs, args.selection_strategy_is_use_all, - args.fluff, ), Ok(None) => { error!(LOGGER, "Missing request body: issue_send_tx"); Err(ErrorKind::GenericError( - "Invalid request body: issue_send_tx", + "Invalid request body: issue_send_tx".to_owned(), ))? } Err(e) => { error!(LOGGER, "Invalid request body: issue_send_tx {:?}", e); Err(ErrorKind::GenericError( - "Invalid request body: issue_send_tx", + "Invalid request body: issue_send_tx".to_owned(), ))? } } @@ -302,14 +294,18 @@ where api.issue_burn_tx(60, 10, 1000) } - fn handle_request(&self, req: &mut Request, api: &mut APIOwner) -> Result { + fn handle_request( + &self, + req: &mut Request, + api: &mut APIOwner, + ) -> Result { let url = req.url.clone(); let path_elems = url.path(); match *path_elems.last().unwrap() { "issue_send_tx" => json_response_pretty(&self.issue_send_tx(req, api)?), "issue_burn_tx" => json_response_pretty(&self.issue_burn_tx(req, api)?), _ => Err(ErrorKind::GenericError( - "Unknown error handling post request", + "Unknown error handling post request".to_owned(), ))?, } } @@ -343,18 +339,7 @@ where K: Keychain + 'static, { fn handle(&self, req: &mut Request) -> IronResult { - // every request should open with stored credentials, - // do its thing and then de-init whatever secrets have been - // stored - { - let mut wallet = self.wallet.lock().unwrap(); - wallet.open_with_credentials().map_err(|e| { - error!(LOGGER, "Error opening wallet: {:?}", e); - IronError::new(Fail::compat(e), status::BadRequest) - })?; - } - let mut wallet = self.wallet.lock().unwrap(); - let mut api = APIOwner::new(&mut wallet); + let mut api = APIOwner::new(self.wallet.clone()); let resp = match self.handle_request(req, &mut api) { Ok(r) => self.create_ok_response(&r), Err(e) => { @@ -362,9 +347,6 @@ where self.create_error_response(e) } }; - api.wallet - .close() - .map_err(|e| IronError::new(Fail::compat(e), status::BadRequest))?; resp } } @@ -425,13 +407,13 @@ where Ok(None) => { error!(LOGGER, "Missing request body: build_coinbase"); Err(ErrorKind::GenericError( - "Invalid request body: build_coinbase", + "Invalid request body: build_coinbase".to_owned(), ))? } Err(e) => { error!(LOGGER, "Invalid request body: build_coinbase: {:?}", e); Err(ErrorKind::GenericError( - "Invalid request body: build_coinbase", + "Invalid request body: build_coinbase".to_owned(), ))? } } @@ -443,7 +425,9 @@ where api.receive_tx(&mut slate)?; Ok(slate.clone()) } else { - Err(ErrorKind::GenericError("Invalid request body: receive_tx"))? + Err(ErrorKind::GenericError( + "Invalid request body: receive_tx".to_owned(), + ))? } } @@ -473,22 +457,8 @@ where K: Keychain + 'static, { fn handle(&self, req: &mut Request) -> IronResult { - // every request should open with stored credentials, - // do its thing and then de-init whatever secrets have been - // stored - { - let mut wallet = self.wallet.lock().unwrap(); - wallet.open_with_credentials().map_err(|e| { - error!(LOGGER, "Error opening wallet: {:?}", e); - IronError::new(Fail::compat(e), status::BadRequest) - })?; - } - let mut wallet = self.wallet.lock().unwrap(); - let mut api = APIForeign::new(&mut wallet); - let resp_json = self.handle_request(req, &mut api); - api.wallet - .close() - .map_err(|e| IronError::new(Fail::compat(e), status::BadRequest))?; + let mut api = APIForeign::new(self.wallet.clone()); + let resp_json = self.handle_request(req, &mut *api); resp_json } } diff --git a/wallet/src/libwallet/error.rs b/wallet/src/libwallet/error.rs index cbc26f3c2..4309da538 100644 --- a/wallet/src/libwallet/error.rs +++ b/wallet/src/libwallet/error.rs @@ -134,7 +134,7 @@ pub enum ErrorKind { /// Other #[fail(display = "Generic error: {}", _0)] - GenericError(&'static str), + GenericError(String), } impl Display for Error { diff --git a/wallet/src/libwallet/types.rs b/wallet/src/libwallet/types.rs index 69387ae5d..ff97e39b7 100644 --- a/wallet/src/libwallet/types.rs +++ b/wallet/src/libwallet/types.rs @@ -306,7 +306,7 @@ impl BlockIdentifier { /// convert to hex string pub fn from_hex(hex: &str) -> Result { - let hash = Hash::from_hex(hex).context(ErrorKind::GenericError("Invalid hex"))?; + let hash = Hash::from_hex(hex).context(ErrorKind::GenericError("Invalid hex".to_owned()))?; Ok(BlockIdentifier(hash)) } } diff --git a/wallet/src/types.rs b/wallet/src/types.rs index 1d5bdfee7..febf3287e 100644 --- a/wallet/src/types.rs +++ b/wallet/src/types.rs @@ -78,8 +78,8 @@ impl WalletSeed { } fn from_hex(hex: &str) -> Result { - let bytes = - util::from_hex(hex.to_string()).context(ErrorKind::GenericError("Invalid hex"))?; + let bytes = util::from_hex(hex.to_string()) + .context(ErrorKind::GenericError("Invalid hex".to_owned()))?; Ok(WalletSeed::from_bytes(&bytes)) } diff --git a/wallet/tests/common/mod.rs b/wallet/tests/common/mod.rs index de4786b92..845ad2da5 100644 --- a/wallet/tests/common/mod.rs +++ b/wallet/tests/common/mod.rs @@ -12,112 +12,42 @@ // See the License for the specific language governing permissions and // limitations under the License. -//! Common functions to facilitate wallet, walletlib and transaction testing -use std::collections::HashMap; -use std::collections::hash_map::Entry; - +extern crate failure; extern crate grin_api as api; extern crate grin_chain as chain; extern crate grin_core as core; extern crate grin_keychain as keychain; extern crate grin_wallet as wallet; +extern crate serde_json; extern crate time; +use std::sync::{Arc, Mutex}; + use chain::Chain; -use core::core::hash::Hashed; -use core::core::{Output, OutputFeatures, OutputIdentifier, Transaction, TxKernel}; -use core::{consensus, global, pow}; -use keychain::ExtKeychain; -use wallet::{HTTPWalletClient, WalletConfig}; +use core::core::{OutputFeatures, OutputIdentifier, Transaction}; +use core::{consensus, global, pow, ser}; use wallet::file_wallet::FileWallet; -use wallet::libwallet::internal::updater; -use wallet::libwallet::types::{BlockFees, BlockIdentifier, OutputStatus, - WalletBackend, WalletClient}; -use wallet::libwallet::{Error, ErrorKind}; +use wallet::libwallet; +use wallet::libwallet::types::{BlockFees, CbData, WalletClient, WalletInst}; +use wallet::lmdb_wallet::LMDBBackend; +use wallet::WalletConfig; use util; 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(wallet: &mut T, chain: &chain::Chain) -> Result<(), Error> -where - T: WalletBackend, - C: WalletClient, - K: keychain::Keychain, -{ - let wallet_outputs = updater::map_wallet_outputs(wallet)?; - let chain_outputs: Vec> = wallet_outputs - .keys() - .map(|k| match get_output_local(chain, &k) { - Err(_) => None, - Ok(k) => Some(k), - }) - .collect(); - let mut api_outputs: HashMap = HashMap::new(); - for out in chain_outputs { - match out { - Some(o) => { - api_outputs.insert(o.commit.commit(), util::to_hex(o.commit.to_vec())); - } - None => {} - } - } - let height = chain.head().unwrap().height; - updater::apply_api_outputs(wallet, &wallet_outputs, &api_outputs, height)?; - Ok(()) -} +pub mod testclient; -/// Return the spendable wallet balance from the local chain -/// (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( - wallet: &mut T, - height: u64, -) -> Result<(u64, u64, u64, u64, u64), Error> -where - T: WalletBackend, - C: WalletClient, - K: keychain::Keychain, -{ - 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.keychain().clone(); - for out in wallet - .iter() - .filter(|out| out.root_key_id == keychain.root_key_id()) - { - if out.status == OutputStatus::Unspent { - unspent_total += out.value; - if out.lock_height > height { - unspent_but_locked_total += out.value; - } - } - if out.status == OutputStatus::Unconfirmed && !out.is_coinbase { - unconfirmed_total += out.value; - } - if out.status == OutputStatus::Locked { - locked_total += out.value; - } - } - - Ok(( - unspent_total + unconfirmed_total, //total - unconfirmed_total, //amount_awaiting_confirmation - unspent_but_locked_total, // confirmed but locked - unspent_total - unspent_but_locked_total, // currently spendable - locked_total, // locked total - )) +/// types of backends tests should iterate through +#[derive(Clone)] +pub enum BackendType { + /// File + FileBackend, + /// LMDB + LMDBBackend, } /// Get an output from the chain locally and present it back as an API output -fn get_output_local( - chain: &chain::Chain, - commit: &pedersen::Commitment, -) -> Result { +fn get_output_local(chain: &chain::Chain, commit: &pedersen::Commitment) -> Option { let outputs = [ OutputIdentifier::new(OutputFeatures::DEFAULT_OUTPUT, commit), OutputIdentifier::new(OutputFeatures::COINBASE_OUTPUT, commit), @@ -125,23 +55,25 @@ fn get_output_local( for x in outputs.iter() { if let Ok(_) = chain.is_unspent(&x) { - return Ok(api::Output::new(&commit)); + return Some(api::Output::new(&commit)); } } - Err(ErrorKind::GenericError( - "Can't get output from local instance of chain", - ))? + None } /// Adds a block with a given reward to the chain and mines it -pub fn add_block_with_reward(chain: &Chain, txs: Vec<&Transaction>, reward: (Output, TxKernel)) { +pub fn add_block_with_reward(chain: &Chain, txs: Vec<&Transaction>, reward: CbData) { let prev = chain.head_header().unwrap(); let difficulty = consensus::next_difficulty(chain.difficulty_iter()).unwrap(); + let out_bin = util::from_hex(reward.output).unwrap(); + let kern_bin = util::from_hex(reward.kernel).unwrap(); + let output = ser::deserialize(&mut &out_bin[..]).unwrap(); + let kernel = ser::deserialize(&mut &kern_bin[..]).unwrap(); let mut b = core::core::Block::new( &prev, txs.into_iter().cloned().collect(), difficulty.clone(), - reward, + (output, kernel), ).unwrap(); b.header.timestamp = prev.timestamp + time::Duration::seconds(60); chain.set_txhashset_roots(&mut b, false).unwrap(); @@ -158,57 +90,82 @@ 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(chain: &Chain, txs: Vec<&Transaction>, wallet: &mut T) +pub fn award_block_to_wallet( + chain: &Chain, + txs: Vec<&Transaction>, + wallet: Arc>>>, +) -> Result<(), libwallet::Error> where - T: WalletBackend, C: WalletClient, K: keychain::Keychain, { + // build block fees let prev = chain.head_header().unwrap(); let fee_amt = txs.iter().map(|tx| tx.fee()).sum(); - let fees = BlockFees { + let block_fees = BlockFees { fees: fee_amt, key_id: None, height: prev.height + 1, }; - let coinbase_tx = wallet::libwallet::internal::updater::receive_coinbase(wallet, &fees); - let (coinbase_tx, fees) = match coinbase_tx { - Ok(t) => ((t.0, t.1), t.2), - Err(e) => { - panic!("Unable to create block reward: {:?}", e); - } - }; - add_block_with_reward(chain, txs, coinbase_tx.clone()); - let output = wallet.get(&fees.key_id.unwrap()).unwrap(); - let mut batch = wallet.batch().unwrap(); - batch.save(output).unwrap(); - batch.commit().unwrap(); + // build coinbase (via api) and add block + libwallet::controller::foreign_single_use(wallet.clone(), |api| { + let coinbase_tx = api.build_coinbase(&block_fees)?; + add_block_with_reward(chain, txs, coinbase_tx.clone()); + Ok(()) + })?; + Ok(()) } -/// adds many block rewards to a wallet, no transactions -pub fn award_blocks_to_wallet(chain: &Chain, wallet: &mut T, num_rewards: usize) +/// Award a blocks to a wallet directly +pub fn award_blocks_to_wallet( + chain: &Chain, + wallet: Arc>>>, + number: usize, +) -> Result<(), libwallet::Error> where - T: WalletBackend, C: WalletClient, K: keychain::Keychain, { - for _ in 0..num_rewards { - award_block_to_wallet(chain, vec![], wallet); + for _ in 0..number { + award_block_to_wallet(chain, vec![], wallet.clone())?; } + Ok(()) } -/// Create a new wallet in a particular directory -pub fn create_wallet(dir: &str, client: HTTPWalletClient) -> FileWallet { +/// dispatch a wallet (extend later to optionally dispatch a db wallet) +pub fn create_wallet( + dir: &str, + client: C, + backend_type: BackendType, +) -> Arc>>> +where + C: WalletClient + 'static, + K: keychain::Keychain + 'static, +{ let mut wallet_config = WalletConfig::default(); wallet_config.data_file_dir = String::from(dir); - wallet::WalletSeed::init_file(&wallet_config).expect("Failed to create wallet seed file."); - let mut wallet = FileWallet::new(wallet_config.clone(), "", client) - .unwrap_or_else(|e| panic!("Error creating wallet: {:?} Config: {:?}", e, wallet_config)); + let _ = wallet::WalletSeed::init_file(&wallet_config); + let mut wallet: Box> = match backend_type { + BackendType::FileBackend => { + let mut wallet: FileWallet = FileWallet::new(wallet_config.clone(), "", client) + .unwrap_or_else(|e| { + panic!("Error creating wallet: {:?} Config: {:?}", e, wallet_config) + }); + Box::new(wallet) + } + BackendType::LMDBBackend => { + let mut wallet: LMDBBackend = LMDBBackend::new(wallet_config.clone(), "", client) + .unwrap_or_else(|e| { + panic!("Error creating wallet: {:?} Config: {:?}", e, wallet_config) + }); + Box::new(wallet) + } + }; wallet.open_with_credentials().unwrap_or_else(|e| { panic!( "Error initializing wallet: {:?} Config: {:?}", e, wallet_config ) }); - wallet + Arc::new(Mutex::new(wallet)) } diff --git a/wallet/tests/common/testclient.rs b/wallet/tests/common/testclient.rs new file mode 100644 index 000000000..f63b5e632 --- /dev/null +++ b/wallet/tests/common/testclient.rs @@ -0,0 +1,420 @@ +// 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. + +//! Test client that acts against a local instance of a node +//! so that wallet API can be fully exercised +//! Operates directly on a chain instance + +use std::collections::HashMap; +use std::marker::PhantomData; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::mpsc::{channel, Receiver, Sender}; +use std::sync::{Arc, Mutex}; +use std::thread; +use std::time::Duration; + +use common::api; +use common::serde_json; +use store; +use util::secp::pedersen::Commitment; +use util::{self, LOGGER}; + +use common::failure::ResultExt; + +use chain::types::NoopAdapter; +use chain::Chain; +use core::core::Transaction; +use core::global::{set_mining_mode, ChainTypes}; +use core::{pow, ser}; +use keychain::Keychain; + +use util::secp::pedersen; +use wallet::libtx::slate::Slate; +use wallet::libwallet; +use wallet::libwallet::types::*; + +use common; + +/// Messages to simulate wallet requests/responses +#[derive(Clone, Debug)] +pub struct WalletProxyMessage { + /// sender ID + pub sender_id: String, + /// destination wallet (or server) + pub dest: String, + /// method (like a GET url) + pub method: String, + /// payload (json body) + pub body: String, +} + +/// communicates with a chain instance or other wallet +/// listener APIs via message queues +pub struct WalletProxy +where + C: WalletClient, + K: Keychain, +{ + /// directory to create the chain in + pub chain_dir: String, + /// handle to chain itself + pub chain: Arc, + /// list of interested wallets + pub wallets: HashMap< + String, + ( + Sender, + Arc>>>, + ), + >, + /// simulate json send to another client + /// address, method, payload (simulate HTTP request) + pub tx: Sender, + /// simulate json receiving + pub rx: Receiver, + /// queue control + pub running: Arc, + /// Phantom + phantom_c: PhantomData, + /// Phantom + phantom_k: PhantomData, +} + +impl WalletProxy +where + C: WalletClient, + K: Keychain, +{ + /// Create a new client that will communicate with the given grin node + pub fn new(chain_dir: &str) -> Self { + set_mining_mode(ChainTypes::AutomatedTesting); + let genesis_block = pow::mine_genesis_block().unwrap(); + let dir_name = format!("{}/.grin", chain_dir); + let db_env = Arc::new(store::new_env(dir_name.to_string())); + let c = Chain::init( + dir_name.to_string(), + db_env, + Arc::new(NoopAdapter {}), + genesis_block, + pow::verify_size, + ).unwrap(); + let (tx, rx) = channel(); + let retval = WalletProxy { + chain_dir: chain_dir.to_owned(), + chain: Arc::new(c), + tx: tx, + rx: rx, + wallets: HashMap::new(), + running: Arc::new(AtomicBool::new(false)), + phantom_c: PhantomData, + phantom_k: PhantomData, + }; + retval + } + + /// Add wallet with a given "address" + pub fn add_wallet( + &mut self, + addr: &str, + tx: Sender, + wallet: Arc>>>, + ) { + self.wallets.insert(addr.to_owned(), (tx, wallet)); + } + + /// Run the incoming message queue and respond more or less + /// synchronously + pub fn run(&mut self) -> Result<(), libwallet::Error> { + self.running.store(true, Ordering::Relaxed); + loop { + thread::sleep(Duration::from_millis(10)); + // read queue + let m = self.rx.recv().unwrap(); + trace!(LOGGER, "Wallet Client Proxy Received: {:?}", m); + let resp = match m.method.as_ref() { + "get_chain_height" => self.get_chain_height(m)?, + "get_outputs_from_node" => self.get_outputs_from_node(m)?, + "send_tx_slate" => self.send_tx_slate(m)?, + "post_tx" => self.post_tx(m)?, + _ => panic!("Unknown Wallet Proxy Message"), + }; + + self.respond(resp); + if !self.running.load(Ordering::Relaxed) { + return Ok(()); + } + } + } + + /// Return a message to a given wallet client + fn respond(&mut self, m: WalletProxyMessage) { + if let Some(s) = self.wallets.get_mut(&m.dest) { + if let Err(e) = s.0.send(m.clone()) { + panic!("Error sending response from proxy: {:?}, {}", m, e); + } + } else { + panic!("Unknown wallet recipient for response message: {:?}", m); + } + } + + /// post transaction to the chain (and mine it, taking the reward) + fn post_tx(&mut self, m: WalletProxyMessage) -> Result { + let dest_wallet = self.wallets.get_mut(&m.sender_id).unwrap().1.clone(); + let wrapper: TxWrapper = serde_json::from_str(&m.body).context( + libwallet::ErrorKind::ClientCallback("Error parsing TxWrapper"), + )?; + + let tx_bin = util::from_hex(wrapper.tx_hex).context(libwallet::ErrorKind::ClientCallback( + "Error parsing TxWrapper: tx_bin", + ))?; + + let tx: Transaction = ser::deserialize(&mut &tx_bin[..]).context( + libwallet::ErrorKind::ClientCallback("Error parsing TxWrapper: tx"), + )?; + + common::award_block_to_wallet(&self.chain, vec![&tx], dest_wallet)?; + + Ok(WalletProxyMessage { + sender_id: "node".to_owned(), + dest: m.sender_id, + method: m.method, + body: "".to_owned(), + }) + } + + /// send tx slate + fn send_tx_slate( + &mut self, + m: WalletProxyMessage, + ) -> Result { + let dest_wallet = self.wallets.get_mut(&m.dest); + if let None = dest_wallet { + panic!("Unknown wallet destination for send_tx_slate: {:?}", m); + } + let w = dest_wallet.unwrap().1.clone(); + let mut slate = serde_json::from_str(&m.body).unwrap(); + libwallet::controller::foreign_single_use(w.clone(), |listener_api| { + listener_api.receive_tx(&mut slate)?; + Ok(()) + })?; + Ok(WalletProxyMessage { + sender_id: m.dest, + dest: m.sender_id, + method: m.method, + body: serde_json::to_string(&slate).unwrap(), + }) + } + + /// get chain height + fn get_chain_height( + &mut self, + m: WalletProxyMessage, + ) -> Result { + Ok(WalletProxyMessage { + sender_id: "node".to_owned(), + dest: m.sender_id, + method: m.method, + body: format!("{}", self.chain.head().unwrap().height).to_owned(), + }) + } + + /// get api outputs + fn get_outputs_from_node( + &mut self, + m: WalletProxyMessage, + ) -> Result { + let split = m.body.split(","); + //let mut api_outputs: HashMap = HashMap::new(); + let mut outputs: Vec = vec![]; + for o in split { + let c = util::from_hex(String::from(o)).unwrap(); + let commit = Commitment::from_vec(c); + let out = common::get_output_local(&self.chain.clone(), &commit); + if let Some(o) = out { + outputs.push(o); + } + } + Ok(WalletProxyMessage { + sender_id: "node".to_owned(), + dest: m.sender_id, + method: m.method, + body: serde_json::to_string(&outputs).unwrap(), + }) + } +} + +#[derive(Clone)] +pub struct LocalWalletClient { + /// wallet identifier for the proxy queue + pub id: String, + /// proxy's tx queue (recieve messsages from other wallets or node + pub proxy_tx: Arc>>, + /// my rx queue + pub rx: Arc>>, + /// my tx queue + pub tx: Arc>>, +} + +impl LocalWalletClient { + /// new + pub fn new(id: &str, proxy_rx: Sender) -> Self { + let (tx, rx) = channel(); + LocalWalletClient { + id: id.to_owned(), + proxy_tx: Arc::new(Mutex::new(proxy_rx)), + rx: Arc::new(Mutex::new(rx)), + tx: Arc::new(Mutex::new(tx)), + } + } + + /// get an instance of the send queue for other senders + pub fn get_send_instance(&self) -> Sender { + self.tx.lock().unwrap().clone() + } +} + +impl WalletClient for LocalWalletClient { + fn node_url(&self) -> &str { + "node" + } + + /// Call the wallet API to create a coinbase output for the given + /// block_fees. Will retry based on default "retry forever with backoff" + /// behavior. + fn create_coinbase( + &self, + _dest: &str, + _block_fees: &BlockFees, + ) -> Result { + unimplemented!(); + } + + /// Send the slate to a listening wallet instance + fn send_tx_slate(&self, dest: &str, slate: &Slate) -> Result { + let m = WalletProxyMessage { + sender_id: self.id.clone(), + dest: dest.to_owned(), + method: "send_tx_slate".to_owned(), + body: serde_json::to_string(slate).unwrap(), + }; + { + let p = self.proxy_tx.lock().unwrap(); + p.send(m) + .context(libwallet::ErrorKind::ClientCallback("Send TX Slate"))?; + } + let r = self.rx.lock().unwrap(); + let m = r.recv().unwrap(); + trace!(LOGGER, "Received send_tx_slate response: {:?}", m.clone()); + Ok( + serde_json::from_str(&m.body).context(libwallet::ErrorKind::ClientCallback( + "Parsing send_tx_slate response", + ))?, + ) + } + + /// Posts a transaction to a grin node + /// In this case it will create a new block with award rewarded to + fn post_tx(&self, tx: &TxWrapper, _fluff: bool) -> Result<(), libwallet::Error> { + let m = WalletProxyMessage { + sender_id: self.id.clone(), + dest: self.node_url().to_owned(), + method: "post_tx".to_owned(), + body: serde_json::to_string(tx).unwrap(), + }; + { + let p = self.proxy_tx.lock().unwrap(); + p.send(m) + .context(libwallet::ErrorKind::ClientCallback("post_tx send"))?; + } + let r = self.rx.lock().unwrap(); + let m = r.recv().unwrap(); + trace!(LOGGER, "Received post_tx response: {:?}", m.clone()); + Ok(()) + } + + /// Return the chain tip from a given node + fn get_chain_height(&self) -> Result { + let m = WalletProxyMessage { + sender_id: self.id.clone(), + dest: self.node_url().to_owned(), + method: "get_chain_height".to_owned(), + body: "".to_owned(), + }; + { + let p = self.proxy_tx.lock().unwrap(); + p.send(m).context(libwallet::ErrorKind::ClientCallback( + "Get chain height send", + ))?; + } + let r = self.rx.lock().unwrap(); + let m = r.recv().unwrap(); + trace!( + LOGGER, + "Received get_chain_height response: {:?}", + m.clone() + ); + Ok(m.body + .parse::() + .context(libwallet::ErrorKind::ClientCallback( + "Parsing get_height response", + ))?) + } + + /// Retrieve outputs from node + fn get_outputs_from_node( + &self, + wallet_outputs: Vec, + ) -> Result, libwallet::Error> { + let query_params: Vec = wallet_outputs + .iter() + .map(|commit| format!("{}", util::to_hex(commit.as_ref().to_vec()))) + .collect(); + let query_str = query_params.join(","); + let m = WalletProxyMessage { + sender_id: self.id.clone(), + dest: self.node_url().to_owned(), + method: "get_outputs_from_node".to_owned(), + body: query_str, + }; + { + let p = self.proxy_tx.lock().unwrap(); + p.send(m).context(libwallet::ErrorKind::ClientCallback( + "Get outputs from node send", + ))?; + } + let r = self.rx.lock().unwrap(); + let m = r.recv().unwrap(); + let outputs: Vec = serde_json::from_str(&m.body).unwrap(); + let mut api_outputs: HashMap = HashMap::new(); + for out in outputs { + api_outputs.insert(out.commit.commit(), util::to_hex(out.commit.to_vec())); + } + Ok(api_outputs) + } + + fn get_outputs_by_pmmr_index( + &self, + _start_height: u64, + _max_outputs: u64, + ) -> Result< + ( + u64, + u64, + Vec<(pedersen::Commitment, pedersen::RangeProof, bool)>, + ), + libwallet::Error, + > { + unimplemented!(); + } +} diff --git a/wallet/tests/transaction.rs b/wallet/tests/transaction.rs index 5aa72be53..d17b265fe 100644 --- a/wallet/tests/transaction.rs +++ b/wallet/tests/transaction.rs @@ -26,186 +26,192 @@ extern crate time; extern crate uuid; mod common; +use common::testclient::{LocalWalletClient, WalletProxy}; use std::fs; -use std::sync::Arc; +use std::thread; +use std::time::Duration; -use chain::Chain; -use chain::types::NoopAdapter; +use core::global; use core::global::ChainTypes; -use core::{global, pow}; +use keychain::ExtKeychain; use util::LOGGER; -use wallet::HTTPWalletClient; -use wallet::libwallet::internal::selection; fn clean_output_dir(test_dir: &str) { let _ = fs::remove_dir_all(test_dir); } -fn setup(test_dir: &str, chain_dir: &str) -> Chain { +fn setup(test_dir: &str) { util::init_test_logger(); clean_output_dir(test_dir); global::set_mining_mode(ChainTypes::AutomatedTesting); - let genesis_block = pow::mine_genesis_block().unwrap(); - let dir_name = format!("{}/{}", test_dir, chain_dir); - let db_env = Arc::new(store::new_env(dir_name.to_string())); - chain::Chain::init( - dir_name.to_string(), - db_env, - Arc::new(NoopAdapter {}), - genesis_block, - pow::verify_size, - ).unwrap() } -/// Build and test new version of sending API +/// Exercises the Transaction API fully with a test WalletClient operating +/// directly on a chain instance +/// Callable with any type of wallet +fn basic_transaction_api(test_dir: &str, backend_type: common::BackendType) { + setup(test_dir); + // Create a new proxy to simulate server and wallet responses + let mut wallet_proxy: WalletProxy = WalletProxy::new(test_dir); + let chain = wallet_proxy.chain.clone(); + + // Create a new wallet test client, and set its queues to communicate with the + // proxy + let client = LocalWalletClient::new("wallet1", wallet_proxy.tx.clone()); + let wallet1 = common::create_wallet( + &format!("{}/wallet1", test_dir), + client.clone(), + backend_type.clone(), + ); + wallet_proxy.add_wallet("wallet1", client.get_send_instance(), wallet1.clone()); + + // define recipient wallet, add to proxy + let client = LocalWalletClient::new("wallet2", wallet_proxy.tx.clone()); + let wallet2 = common::create_wallet( + &format!("{}/wallet2", test_dir), + client.clone(), + backend_type.clone(), + ); + wallet_proxy.add_wallet("wallet2", client.get_send_instance(), wallet2.clone()); + + // Set the wallet proxy listener running + thread::spawn(move || { + if let Err(e) = wallet_proxy.run() { + error!(LOGGER, "Wallet Proxy error: {}", e); + } + }); + + // few values to keep things shorter + let reward = core::consensus::REWARD; + let cm = global::coinbase_maturity(); + // mine a few blocks + let _ = common::award_blocks_to_wallet(&chain, wallet1.clone(), 10); + + // Check wallet 1 contents are as expected + let sender_res = wallet::controller::owner_single_use(wallet1.clone(), |api| { + let (wallet1_refreshed, wallet1_info) = api.retrieve_summary_info(true)?; + debug!( + LOGGER, + "Wallet 1 Info Pre-Transaction, after {} blocks: {:?}", + wallet1_info.last_confirmed_height, + wallet1_info + ); + assert!(wallet1_refreshed); + assert_eq!( + wallet1_info.amount_currently_spendable, + (wallet1_info.last_confirmed_height - cm) * reward + ); + assert_eq!(wallet1_info.amount_immature, cm * reward); + Ok(()) + }); + if let Err(e) = sender_res { + println!("Error starting sender API: {}", e); + } + + // assert wallet contents + // and a single use api for a send command + let amount = 60_000_000_000; + let sender_res = wallet::controller::owner_single_use(wallet1.clone(), |sender_api| { + // note this will increment the block count as part of the transaction "Posting" + let issue_tx_res = sender_api.issue_send_tx( + amount, // amount + 2, // minimum confirmations + "wallet2", // dest + 500, // max outputs + true, // select all outputs + ); + if issue_tx_res.is_err() { + panic!("Error issuing send tx: {}", issue_tx_res.err().unwrap()); + } + let post_res = sender_api.post_tx(&issue_tx_res.unwrap(), false); + if post_res.is_err() { + panic!("Error posting tx: {}", post_res.err().unwrap()); + } + + Ok(()) + }); + if let Err(e) = sender_res { + panic!("Error starting sender API: {}", e); + } + + // Check wallet 1 contents are as expected + let sender_res = wallet::controller::owner_single_use(wallet1.clone(), |api| { + let (wallet1_refreshed, wallet1_info) = api.retrieve_summary_info(true)?; + debug!( + LOGGER, + "Wallet 1 Info Post Transaction, after {} blocks: {:?}", + wallet1_info.last_confirmed_height, + wallet1_info + ); + let fee = wallet::libtx::tx_fee( + wallet1_info.last_confirmed_height as usize - 1 - cm as usize, + 2, + None, + ); + assert!(wallet1_refreshed); + // wallet 1 recieved fees, so amount should be the same + assert_eq!( + wallet1_info.total, + amount * wallet1_info.last_confirmed_height - amount + ); + assert_eq!( + wallet1_info.amount_currently_spendable, + (wallet1_info.last_confirmed_height - cm) * reward - amount - fee + ); + assert_eq!(wallet1_info.amount_immature, cm * reward + fee); + Ok(()) + }); + if let Err(e) = sender_res { + println!("Error starting sender API: {}", e); + } + + // mine a few more blocks + let _ = common::award_blocks_to_wallet(&chain, wallet1.clone(), 3); + + // refresh wallets and retrieve info/tests for each wallet after maturity + let sender_res = wallet::controller::owner_single_use(wallet1.clone(), |api| { + let (wallet1_refreshed, wallet1_info) = api.retrieve_summary_info(true)?; + debug!(LOGGER, "Wallet 1 Info: {:?}", wallet1_info); + assert!(wallet1_refreshed); + assert_eq!( + wallet1_info.total, + amount * wallet1_info.last_confirmed_height - amount + ); + assert_eq!( + wallet1_info.amount_currently_spendable, + (wallet1_info.last_confirmed_height - cm - 1) * reward + ); + Ok(()) + }); + if let Err(e) = sender_res { + println!("Error starting sender API: {}", e); + } + + let sender_res = wallet::controller::owner_single_use(wallet2.clone(), |api| { + let (wallet2_refreshed, wallet2_info) = api.retrieve_summary_info(true)?; + assert!(wallet2_refreshed); + assert_eq!(wallet2_info.amount_currently_spendable, amount); + Ok(()) + }); + if let Err(e) = sender_res { + println!("Error starting sender API: {}", e); + } + + // let logging finish + thread::sleep(Duration::from_millis(200)); +} + #[test] -fn build_transaction() { - let client = HTTPWalletClient::new(""); - let chain = setup("test_output", "build_transaction_2/.grin"); - let mut wallet1 = common::create_wallet("test_output/build_transaction_2/wallet1", client.clone()); - let mut wallet2 = common::create_wallet("test_output/build_transaction_2/wallet2", client); - 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 libtx - - // Get lock height - let chain_tip = chain.head().unwrap(); - let amount = 300_000_000_000; - let min_confirmations = 3; - - // ensure outputs we're selecting are up to date - let res = common::refresh_output_state_local(&mut wallet1, &chain); - - if let Err(e) = res { - panic!("Unable to refresh sender wallet outputs: {}", e); - } - - // TRANSACTION WORKFLOW STARTS HERE - // Sender selects outputs into a new slate and save our corresponding IDs in - // a transaction context. The secret key in our transaction context will be - // randomly selected. This returns the public slate, and a closure that locks - // our inputs and outputs once we're convinced the transaction exchange went - // according to plan - // 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( - &mut wallet1, - 2, - amount, - chain_tip.height, - min_confirmations, - chain_tip.height, - 1000, - true, - ).unwrap(); - - // Generate a kernel offset and subtract from our context's secret key. Store - // the offset in the slate's transaction kernel, and adds our public key - // information to the slate - let _ = slate - .fill_round_1( - wallet1.keychain.as_ref().unwrap(), - &mut sender_context.sec_key, - &sender_context.sec_nonce, - 0, - ) - .unwrap(); - - debug!(LOGGER, "Transaction Slate after step 1: sender initiation"); - debug!(LOGGER, "-----------------------------------------"); - debug!(LOGGER, "{:?}", slate); - - // Now, just like the sender did, recipient is going to select a target output, - // add it to the transaction, and keep track of the corresponding wallet - // Identifier Again, this is a helper to do that, which returns a closure that - // creates the output when we're satisfied the process was successful - let (_, mut recp_context, receiver_create_fn) = - selection::build_recipient_output_with_slate(&mut wallet2, &mut slate).unwrap(); - - let _ = slate - .fill_round_1( - wallet2.keychain.as_ref().unwrap(), - &mut recp_context.sec_key, - &recp_context.sec_nonce, - 1, - ) - .unwrap(); - - // recipient can proceed to round 2 now - let _ = receiver_create_fn(&mut wallet2); - - let _ = slate - .fill_round_2( - wallet1.keychain.as_ref().unwrap(), - &recp_context.sec_key, - &recp_context.sec_nonce, - 1, - ) - .unwrap(); - - debug!( - LOGGER, - "Transaction Slate after step 2: receiver initiation" - ); - debug!(LOGGER, "-----------------------------------------"); - debug!(LOGGER, "{:?}", slate); - - // SENDER Part 3: Sender confirmation - let _ = slate - .fill_round_2( - wallet1.keychain.as_ref().unwrap(), - &sender_context.sec_key, - &sender_context.sec_nonce, - 0, - ) - .unwrap(); - - debug!(LOGGER, "PartialTx after step 3: sender confirmation"); - debug!(LOGGER, "--------------------------------------------"); - debug!(LOGGER, "{:?}", slate); - - // Final transaction can be built by anyone at this stage - let res = slate.finalize(wallet1.keychain.as_ref().unwrap()); - - if let Err(e) = res { - panic!("Error creating final tx: {:?}", e); - } - - debug!(LOGGER, "Final transaction is:"); - debug!(LOGGER, "--------------------------------------------"); - debug!(LOGGER, "{:?}", slate.tx); - - // All okay, lock sender's outputs - 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], &mut wallet1); - common::award_blocks_to_wallet(&chain, &mut wallet1, 5); - - // Refresh wallets - 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(&mut wallet2, chain_tip.height).unwrap(); - - assert_eq!(balances.3, 300_000_000_000); - - // check sender wallet - 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(&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 - assert_eq!( - balances.3, - (chain_tip.height - min_confirmations) * 60_000_000_000 - amount - ); +fn file_wallet_basic_transaction_api() { + let test_dir = "test_output/basic_transaction_api_file"; + basic_transaction_api(test_dir, common::BackendType::FileBackend); +} + +// not yet ready +#[ignore] +#[test] +fn db_wallet_basic_transaction_api() { + let test_dir = "test_output/basic_transaction_api_db"; + basic_transaction_api(test_dir, common::BackendType::LMDBBackend); }