diff --git a/servers/src/mining/mine_block.rs b/servers/src/mining/mine_block.rs index bf56cfdb..9463fa59 100644 --- a/servers/src/mining/mine_block.rs +++ b/servers/src/mining/mine_block.rs @@ -239,13 +239,7 @@ fn get_coinbase( return burn_reward(block_fees); } Some(wallet_listener_url) => { - // Get the wallet coinbase - let url = format!( - "{}/v1/wallet/foreign/build_coinbase", - wallet_listener_url.as_str() - ); - - let res = wallet::libwallet::client::create_coinbase(&url, &block_fees)?; + let res = wallet::create_coinbase(&wallet_listener_url, &block_fees)?; let out_bin = util::from_hex(res.output).unwrap(); let kern_bin = util::from_hex(res.kernel).unwrap(); let key_id_bin = util::from_hex(res.key_id).unwrap(); @@ -258,7 +252,6 @@ fn get_coinbase( }; debug!(LOGGER, "get_coinbase: {:?}", block_fees); - return Ok((output, kernel, block_fees)); } } diff --git a/servers/tests/framework/mod.rs b/servers/tests/framework/mod.rs index 616eb041..9fdd8d19 100644 --- a/servers/tests/framework/mod.rs +++ b/servers/tests/framework/mod.rs @@ -326,29 +326,32 @@ impl LocalServerContainer { .expect("Failed to derive keychain from seed file and passphrase."); let max_outputs = 500; - let mut wallet = FileWallet::new(config.clone(), "grin_test") + let mut wallet = FileWallet::new(config.clone(), "") .unwrap_or_else(|e| panic!("Error creating wallet: {:?} Config: {:?}", e, config)); wallet.keychain = Some(keychain); - let result = wallet::libwallet::internal::tx::issue_send_tx( - &mut wallet, - 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), - dest, - selection_strategy, - ), - Err(e) => { - println!("Tx not sent to {}: {:?}", dest, e); - } - }; + let _ = + wallet::controller::owner_single_use(&mut 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), + 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/wallet/src/client.rs b/wallet/src/client.rs new file mode 100644 index 00000000..730084f1 --- /dev/null +++ b/wallet/src/client.rs @@ -0,0 +1,222 @@ +// 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. + +//! Client functions, implementations of the WalletClient trait +//! specific to the FileWallet + +use failure::ResultExt; +use libwallet::types::*; +use std::collections::HashMap; +use std::io; + +use futures::{Future, Stream}; +use hyper; +use hyper::header::ContentType; +use hyper::{Method, Request}; +use serde_json; +use tokio_core::reactor; + +use api; +use error::{Error, ErrorKind}; +use libtx::slate::Slate; +use util::secp::pedersen; +use util::{self, LOGGER}; + +/// Call the wallet API to create a coinbase output for the given block_fees. +/// Will retry based on default "retry forever with backoff" behavior. +pub fn create_coinbase(dest: &str, block_fees: &BlockFees) -> Result { + let url = format!("{}/v1/wallet/foreign/build_coinbase", dest); + match single_create_coinbase(&url, &block_fees) { + Err(e) => { + error!( + LOGGER, + "Failed to get coinbase from {}. Run grin wallet listen?", url + ); + error!(LOGGER, "Underlying Error: {}", e.cause().unwrap()); + error!(LOGGER, "Backtrace: {}", e.backtrace().unwrap()); + Err(e) + } + Ok(res) => Ok(res), + } +} + +/// Send the slate to a listening wallet instance +pub fn send_tx_slate(dest: &str, slate: &Slate) -> Result { + if &dest[..4] != "http" { + error!( + LOGGER, + "dest formatted as {} but send -d expected stdout or http://IP:port", dest + ); + Err(ErrorKind::Node)? + } + let url = format!("{}/v1/wallet/foreign/receive_tx", dest); + debug!(LOGGER, "Posting transaction slate to {}", url); + + let mut core = reactor::Core::new().context(ErrorKind::Hyper)?; + let client = hyper::Client::new(&core.handle()); + + let url_pool = url.to_owned(); + + let mut req = Request::new( + Method::Post, + url_pool.parse::().context(ErrorKind::Hyper)?, + ); + req.headers_mut().set(ContentType::json()); + let json = serde_json::to_string(&slate).context(ErrorKind::Hyper)?; + 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))?; + Ok(slate) + }) + }); + let res = core.run(work).context(ErrorKind::Hyper)?; + Ok(res) +} + +/// Makes a single request to the wallet API to create a new coinbase output. +fn single_create_coinbase(url: &str, block_fees: &BlockFees) -> Result { + let mut core = + reactor::Core::new().context(ErrorKind::GenericError("Could not create reactor"))?; + let client = hyper::Client::new(&core.handle()); + + let mut req = Request::new( + Method::Post, + url.parse::().context(ErrorKind::Uri)?, + ); + req.headers_mut().set(ContentType::json()); + let json = serde_json::to_string(&block_fees).context(ErrorKind::Format)?; + trace!(LOGGER, "Sending coinbase request: {:?}", json); + req.set_body(json); + + let work = client.request(req).and_then(|res| { + res.body().concat2().and_then(move |body| { + trace!(LOGGER, "Returned Body: {:?}", body); + let coinbase: CbData = + serde_json::from_slice(&body).map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; + Ok(coinbase) + }) + }); + + let res = core.run(work) + .context(ErrorKind::GenericError("Could not run core"))?; + Ok(res) +} + +/// Posts a tranaction to a grin node +pub fn post_tx(dest: &str, tx: &TxWrapper, fluff: bool) -> Result<(), Error> { + let url; + if fluff { + url = format!("{}/v1/pool/push?fluff", dest); + } else { + url = format!("{}/v1/pool/push", dest); + } + let res = api::client::post(url.as_str(), tx).context(ErrorKind::Node)?; + Ok(res) +} + +/// Return the chain tip from a given node +pub fn get_chain_height(addr: &str) -> Result { + let url = format!("{}/v1/chain", addr); + let res = api::client::get::(url.as_str()).context(ErrorKind::Node)?; + Ok(res.height) +} + +/// Retrieve outputs from node +pub fn get_outputs_from_node( + addr: &str, + wallet_outputs: Vec, +) -> Result, Error> { + // build the necessary query params - + // ?id=xxx&id=yyy&id=zzz + let query_params: Vec = wallet_outputs + .iter() + .map(|commit| format!("id={}", util::to_hex(commit.as_ref().to_vec()))) + .collect(); + + // build a map of api outputs by commit so we can look them up efficiently + let mut api_outputs: HashMap = HashMap::new(); + + for query_chunk in query_params.chunks(1000) { + let url = format!("{}/v1/chain/outputs/byids?{}", addr, query_chunk.join("&"),); + + match api::client::get::>(url.as_str()) { + Ok(outputs) => for out in outputs { + api_outputs.insert(out.commit.commit(), util::to_hex(out.commit.to_vec())); + }, + Err(e) => { + // if we got anything other than 200 back from server, don't attempt to refresh + // the wallet data after + return Err(e).context(ErrorKind::Node)?; + } + } + } + Ok(api_outputs) +} + +/// Get any missing block hashes from node +pub fn get_missing_block_hashes_from_node( + addr: &str, + height: u64, + wallet_outputs: Vec, +) -> Result< + ( + HashMap, + HashMap, + ), + Error, +> { + let id_params: Vec = wallet_outputs + .iter() + .map(|commit| format!("id={}", util::to_hex(commit.as_ref().to_vec()))) + .collect(); + + let height_params = [format!("start_height={}&end_height={}", 0, height)]; + let mut api_blocks: HashMap = HashMap::new(); + let mut api_merkle_proofs: HashMap = HashMap::new(); + + // Split up into separate requests, to avoid hitting http limits + for mut query_chunk in id_params.chunks(1000) { + let url = format!( + "{}/v1/chain/outputs/byheight?{}", + addr, + [&height_params, query_chunk].concat().join("&"), + ); + + match api::client::get::>(url.as_str()) { + Ok(blocks) => for block in blocks { + for out in block.outputs { + api_blocks.insert( + out.commit, + ( + block.header.height, + BlockIdentifier::from_hex(&block.header.hash).unwrap(), + ), + ); + if let Some(merkle_proof) = out.merkle_proof { + let wrapper = MerkleProofWrapper(merkle_proof); + api_merkle_proofs.insert(out.commit, wrapper); + } + } + }, + Err(e) => { + // if we got anything other than 200 back from server, bye + return Err(e).context(ErrorKind::Node)?; + } + } + } + Ok((api_blocks, api_merkle_proofs)) +} diff --git a/wallet/src/file_wallet.rs b/wallet/src/file_wallet.rs index 52300149..8e520cb3 100644 --- a/wallet/src/file_wallet.rs +++ b/wallet/src/file_wallet.rs @@ -31,8 +31,12 @@ use failure::ResultExt; use keychain::{self, Keychain}; use util; use util::LOGGER; +use util::secp::pedersen; use error::{Error, ErrorKind}; + +use client; +use libtx::slate::Slate; use libwallet; use libwallet::types::*; @@ -205,11 +209,6 @@ impl WalletBackend for FileWallet { self.keychain.as_mut().unwrap() } - /// 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 @@ -399,6 +398,81 @@ impl WalletBackend for FileWallet { } } +impl WalletClient for FileWallet { + /// Return URL for check node + fn node_url(&self) -> &str { + &self.config.check_node_api_http_addr + } + + /// Call the wallet API to create a coinbase transaction + fn create_coinbase( + &self, + dest: &str, + block_fees: &BlockFees, + ) -> Result { + let res = + client::create_coinbase(dest, block_fees).context(libwallet::ErrorKind::WalletComms)?; + Ok(res) + } + + /// Send a transaction slate to another listening wallet and return result + fn send_tx_slate(&self, dest: &str, slate: &Slate) -> Result { + let res = client::send_tx_slate(dest, slate).context(libwallet::ErrorKind::WalletComms)?; + Ok(res) + } + + /// Posts a tranaction to a grin node + fn post_tx(&self, dest: &str, tx: &TxWrapper, fluff: bool) -> Result<(), libwallet::Error> { + let res = client::post_tx(dest, tx, fluff).context(libwallet::ErrorKind::Node)?; + Ok(res) + } + + /// retrieves the current tip from the specified grin node + fn get_chain_height(&self, addr: &str) -> Result { + let res = client::get_chain_height(addr).context(libwallet::ErrorKind::Node)?; + Ok(res) + } + + /// retrieve a list of outputs from the specified grin node + /// need "by_height" and "by_id" variants + fn get_outputs_from_node( + &self, + addr: &str, + wallet_outputs: Vec, + ) -> Result, libwallet::Error> { + let res = client::get_outputs_from_node(addr, wallet_outputs) + .context(libwallet::ErrorKind::Node)?; + Ok(res) + } + + /// Get any missing block hashes from node + fn get_missing_block_hashes_from_node( + &self, + addr: &str, + height: u64, + wallet_outputs: Vec, + ) -> Result< + ( + HashMap, + HashMap, + ), + libwallet::Error, + > { + let res = client::get_missing_block_hashes_from_node(addr, height, wallet_outputs) + .context(libwallet::ErrorKind::Node)?; + Ok(res) + } + + /// retrieve merkle proof for a commit from a node + fn get_merkle_proof_for_commit( + &self, + addr: &str, + commit: &str, + ) -> Result { + Err(libwallet::ErrorKind::GenericError("Not Implemented"))? + } +} + impl FileWallet { /// Create a new FileWallet instance pub fn new(config: WalletConfig, passphrase: &str) -> Result { diff --git a/wallet/src/lib.rs b/wallet/src/lib.rs index 8df12347..145b27c7 100644 --- a/wallet/src/lib.rs +++ b/wallet/src/lib.rs @@ -46,12 +46,14 @@ extern crate grin_core as core; extern crate grin_keychain as keychain; extern crate grin_util as util; +mod client; pub mod display; mod error; pub mod file_wallet; pub mod libtx; pub mod libwallet; +pub use client::create_coinbase; pub use error::{Error, ErrorKind}; pub use file_wallet::{FileWallet, WalletConfig, WalletSeed}; pub use libwallet::controller; diff --git a/wallet/src/libwallet/api.rs b/wallet/src/libwallet/api.rs index 9b1961a6..e9e4a050 100644 --- a/wallet/src/libwallet/api.rs +++ b/wallet/src/libwallet/api.rs @@ -20,13 +20,17 @@ use libtx::slate::Slate; use libwallet::Error; use libwallet::internal::{tx, updater}; -use libwallet::types::{BlockFees, CbData, OutputData, WalletBackend, WalletInfo}; +use libwallet::types::{BlockFees, CbData, OutputData, TxWrapper, WalletBackend, WalletClient, + WalletInfo}; + +use core::ser; +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> where - W: 'a + WalletBackend, + W: 'a + WalletBackend + WalletClient, { /// Wallet, contains its keychain (TODO: Split these up into 2 traits /// perhaps) @@ -35,7 +39,7 @@ where impl<'a, W> APIOwner<'a, W> where - W: 'a + WalletBackend, + W: 'a + WalletBackend + WalletClient, { /// Create new API instance pub fn new(wallet_in: &'a mut W) -> APIOwner<'a, W> { @@ -62,7 +66,6 @@ where } /// Issues a send transaction and sends to recipient - /// (TODO: Split into separate functions, create tx, send, complete tx) pub fn issue_send_tx( &mut self, amount: u64, @@ -72,15 +75,35 @@ where selection_strategy_is_use_all: bool, fluff: bool, ) -> Result<(), Error> { - tx::issue_send_tx( + let (slate, context, lock_fn) = tx::create_send_tx( self.wallet, amount, minimum_confirmations, - dest, max_outputs, selection_strategy_is_use_all, - fluff, - ) + )?; + + let mut slate = match self.wallet.send_tx_slate(dest, &slate) { + Ok(s) => s, + Err(e) => { + error!( + LOGGER, + "Communication with receiver failed on SenderInitiation send. Aborting transaction" + ); + return Err(e)?; + } + }; + + tx::complete_tx(self.wallet, &mut slate, &context)?; + + // All good here, so let's post it + let tx_hex = util::to_hex(ser::ser_vec(&slate.tx).unwrap()); + self.wallet + .post_tx(self.wallet.node_url(), &TxWrapper { tx_hex: tx_hex }, fluff)?; + + // All good here, lock our inputs + lock_fn(self.wallet)?; + Ok(()) } /// Issue a burn TX @@ -90,7 +113,11 @@ where minimum_confirmations: u64, max_outputs: usize, ) -> Result<(), Error> { - tx::issue_burn_tx(self.wallet, amount, minimum_confirmations, max_outputs) + let tx_burn = tx::issue_burn_tx(self.wallet, amount, minimum_confirmations, max_outputs)?; + let tx_hex = util::to_hex(ser::ser_vec(&tx_burn).unwrap()); + self.wallet + .post_tx(self.wallet.node_url(), &TxWrapper { tx_hex: tx_hex }, false)?; + Ok(()) } /// Attempt to restore contents of wallet @@ -100,8 +127,8 @@ where /// Retrieve current height from node pub fn node_height(&mut self) -> Result<(u64, bool), Error> { - match updater::get_tip_from_node(self.wallet.node_url()) { - Ok(tip) => Ok((tip.height, true)), + match self.wallet.get_chain_height(self.wallet.node_url()) { + Ok(height) => Ok((height, true)), Err(_) => { let outputs = self.retrieve_outputs(true)?; let height = match outputs.1.iter().map(|out| out.height).max() { @@ -126,7 +153,7 @@ where /// with other parties pub struct APIForeign<'a, W> where - W: 'a + WalletBackend, + W: 'a + WalletBackend + WalletClient, { /// Wallet, contains its keychain (TODO: Split these up into 2 traits /// perhaps) @@ -135,7 +162,7 @@ where impl<'a, W> APIForeign<'a, W> where - W: 'a + WalletBackend, + W: 'a + WalletBackend + WalletClient, { /// Create new API instance pub fn new(wallet_in: &'a mut W) -> APIForeign<'a, W> { diff --git a/wallet/src/libwallet/client.rs b/wallet/src/libwallet/client.rs deleted file mode 100644 index fc93e4e0..00000000 --- a/wallet/src/libwallet/client.rs +++ /dev/null @@ -1,106 +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. - -//! Client functions: TODO: doesn't really belong here or needs to be -//! traited out - -use failure::ResultExt; -use futures::{Future, Stream}; -use hyper; -use hyper::header::ContentType; -use hyper::{Method, Request}; -use libtx::slate::Slate; -use serde_json; -use tokio_core::reactor; - -use error::{Error, ErrorKind}; -use libwallet::types::*; -use std::io; -use util::LOGGER; - -/// Call the wallet API to create a coinbase output for the given block_fees. -/// Will retry based on default "retry forever with backoff" behavior. -pub fn create_coinbase(url: &str, block_fees: &BlockFees) -> Result { - match single_create_coinbase(&url, &block_fees) { - Err(e) => { - error!( - LOGGER, - "Failed to get coinbase from {}. Run grin wallet listen?", url - ); - error!(LOGGER, "Underlying Error: {}", e.cause().unwrap()); - error!(LOGGER, "Backtrace: {}", e.backtrace().unwrap()); - Err(e) - } - Ok(res) => Ok(res), - } -} - -/// Send the slate to a listening wallet instance -pub fn send_slate(url: &str, slate: &Slate, fluff: bool) -> Result { - let mut core = reactor::Core::new().context(ErrorKind::Hyper)?; - let client = hyper::Client::new(&core.handle()); - - // In case we want to do an express send - let mut url_pool = url.to_owned(); - if fluff { - url_pool = format!("{}{}", url, "?fluff"); - } - - let mut req = Request::new( - Method::Post, - url_pool.parse::().context(ErrorKind::Hyper)?, - ); - req.headers_mut().set(ContentType::json()); - let json = serde_json::to_string(&slate).context(ErrorKind::Hyper)?; - 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))?; - Ok(slate) - }) - }); - let res = core.run(work).context(ErrorKind::Hyper)?; - Ok(res) -} - -/// Makes a single request to the wallet API to create a new coinbase output. -fn single_create_coinbase(url: &str, block_fees: &BlockFees) -> Result { - let mut core = - reactor::Core::new().context(ErrorKind::GenericError("Could not create reactor"))?; - let client = hyper::Client::new(&core.handle()); - - let mut req = Request::new( - Method::Post, - url.parse::().context(ErrorKind::Uri)?, - ); - req.headers_mut().set(ContentType::json()); - let json = serde_json::to_string(&block_fees).context(ErrorKind::Format)?; - trace!(LOGGER, "Sending coinbase request: {:?}", json); - req.set_body(json); - - let work = client.request(req).and_then(|res| { - res.body().concat2().and_then(move |body| { - trace!(LOGGER, "Returned Body: {:?}", body); - let coinbase: CbData = - serde_json::from_slice(&body).map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; - Ok(coinbase) - }) - }); - - let res = core.run(work) - .context(ErrorKind::GenericError("Could not run core"))?; - Ok(res) -} diff --git a/wallet/src/libwallet/controller.rs b/wallet/src/libwallet/controller.rs index 0eafbe2e..f1a8ad7c 100644 --- a/wallet/src/libwallet/controller.rs +++ b/wallet/src/libwallet/controller.rs @@ -29,7 +29,7 @@ use failure::Fail; use libtx::slate::Slate; use libwallet::api::{APIForeign, APIOwner}; -use libwallet::types::{BlockFees, CbData, OutputData, WalletBackend, WalletInfo}; +use libwallet::types::{BlockFees, CbData, OutputData, WalletBackend, WalletClient, WalletInfo}; use libwallet::{Error, ErrorKind}; use util::LOGGER; @@ -38,7 +38,7 @@ use util::LOGGER; /// Return a function containing a loaded API context to call pub fn owner_single_use(wallet: &mut T, f: F) -> Result<(), Error> where - T: WalletBackend, + T: WalletBackend + WalletClient, F: FnOnce(&mut APIOwner) -> Result<(), Error>, { wallet.open_with_credentials()?; @@ -51,7 +51,7 @@ where /// Return a function containing a loaded API context to call pub fn foreign_single_use(wallet: &mut T, f: F) -> Result<(), Error> where - T: WalletBackend, + T: WalletBackend + WalletClient, F: FnOnce(&mut APIForeign) -> Result<(), Error>, { wallet.open_with_credentials()?; @@ -91,7 +91,7 @@ where /// port and wrapping the calls pub fn foreign_listener(wallet: T, addr: &str) -> Result<(), Error> where - T: WalletBackend, + T: WalletBackend + WalletClient, ForeignAPIHandler: Handler, { let api_handler = ForeignAPIHandler { @@ -125,7 +125,7 @@ where impl OwnerAPIHandler where - T: WalletBackend, + T: WalletBackend + WalletClient, { fn retrieve_outputs( &self, @@ -177,7 +177,7 @@ where impl Handler for OwnerAPIHandler where - T: WalletBackend + Send + Sync + 'static, + T: WalletBackend + WalletClient + Send + Sync + 'static, { fn handle(&self, req: &mut Request) -> IronResult { // every request should open with stored credentials, @@ -201,7 +201,7 @@ where pub struct ForeignAPIHandler where - T: WalletBackend, + T: WalletBackend + WalletClient, { /// Wallet instance pub wallet: Arc>, @@ -209,7 +209,7 @@ where impl ForeignAPIHandler where - T: WalletBackend, + T: WalletBackend + WalletClient, { fn build_coinbase(&self, req: &mut Request, api: &mut APIForeign) -> Result { let struct_body = req.get::>(); @@ -258,7 +258,7 @@ where impl Handler for ForeignAPIHandler where - T: WalletBackend + Send + Sync + 'static, + T: WalletBackend + WalletClient + Send + Sync + 'static, { fn handle(&self, req: &mut Request) -> IronResult { // every request should open with stored credentials, diff --git a/wallet/src/libwallet/error.rs b/wallet/src/libwallet/error.rs index fcb86e83..8fc71980 100644 --- a/wallet/src/libwallet/error.rs +++ b/wallet/src/libwallet/error.rs @@ -93,6 +93,10 @@ pub enum ErrorKind { #[fail(display = "Node API error")] Node, + /// Error contacting wallet API + #[fail(display = "Wallet communication error")] + WalletComms, + /// Error originating from hyper. #[fail(display = "Hyper error")] Hyper, diff --git a/wallet/src/libwallet/internal/restore.rs b/wallet/src/libwallet/internal/restore.rs index 8d61fe13..62bc1608 100644 --- a/wallet/src/libwallet/internal/restore.rs +++ b/wallet/src/libwallet/internal/restore.rs @@ -13,6 +13,7 @@ // limitations under the License. //! Functions to restore a wallet's outputs from just the master seed +/// TODO: Remove api use api; use byteorder::{BigEndian, ByteOrder}; use core::core::transaction::ProofMessageElements; @@ -26,24 +27,6 @@ use util; use util::LOGGER; use util::secp::pedersen; -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), - Err(e) => { - // if we got anything other than 200 back from server, bye - error!( - LOGGER, - "get_chain_height: Restore failed... unable to contact API {}. Error: {}", - node_addr, - e - ); - Err(e.context(ErrorKind::Node).into()) - } - } -} - fn get_merkle_proof_for_commit(node_addr: &str, commit: &str) -> Result { let url = format!("{}/v1/txhashset/merkleproof?id={}", node_addr, commit); @@ -70,7 +53,7 @@ fn coinbase_status(output: &api::OutputPrintable) -> bool { fn outputs_batch(wallet: &T, start_height: u64, max: u64) -> Result where - T: WalletBackend, + T: WalletBackend + WalletClient, { let query_param = format!("start_index={}&max={}", start_height, max); @@ -92,7 +75,7 @@ where } // TODO - wrap the many return values in a struct -fn find_outputs_with_key( +fn find_outputs_with_key( wallet: &mut T, outputs: Vec, found_key_index: &mut Vec, @@ -120,7 +103,7 @@ fn find_outputs_with_key( let max_derivations = 1_000_000; info!(LOGGER, "Scanning {} outputs", outputs.len(),); - let current_chain_height = get_chain_height(wallet.node_url()).unwrap(); + let current_chain_height = wallet.get_chain_height(wallet.node_url()).unwrap(); // skey doesn't matter in this case let skey = wallet.keychain().derive_key_id(1).unwrap(); @@ -242,7 +225,7 @@ fn find_outputs_with_key( } /// Restore a wallet -pub fn restore(wallet: &mut T) -> Result<(), Error> { +pub fn restore(wallet: &mut T) -> Result<(), Error> { // Don't proceed if wallet.dat has anything in it let is_empty = wallet .read_wallet(|wallet_data| Ok(wallet_data.outputs().len() == 0)) diff --git a/wallet/src/libwallet/internal/tx.rs b/wallet/src/libwallet/internal/tx.rs index 5243715a..e6473e1f 100644 --- a/wallet/src/libwallet/internal/tx.rs +++ b/wallet/src/libwallet/internal/tx.rs @@ -12,19 +12,15 @@ // See the License for the specific language governing permissions and // limitations under the License. -//! Transaction buinding functions +//! Transaction building functions -use api; -use core::ser; -use failure::ResultExt; +use core::core::Transaction; use keychain::{Identifier, Keychain}; use libtx::slate::Slate; use libtx::{build, tx_fee}; -use libwallet::client; -use libwallet::internal::{selection, updater}; -use libwallet::types::{TxWrapper, WalletBackend}; +use libwallet::internal::{selection, sigcontext, updater}; +use libwallet::types::{WalletBackend, WalletClient}; use libwallet::{Error, ErrorKind}; -use util; use util::LOGGER; /// Receive a tranaction, modifying the slate accordingly (which can then be @@ -53,32 +49,22 @@ pub fn receive_tx(wallet: &mut T, slate: &mut Slate) -> Result /// Issue a new transaction to the provided sender by spending some of our /// wallet -/// Outputs. The destination can be "stdout" (for command line) (currently -/// disabled) or a URL to the recipients wallet receiver (to be implemented). -/// TBD: this just does a straight http request to recipient.. split this out -/// somehow -pub fn issue_send_tx( +pub fn create_send_tx( wallet: &mut T, amount: u64, minimum_confirmations: u64, - dest: &str, max_outputs: usize, selection_strategy_is_use_all: bool, - fluff: bool, -) -> Result<(), Error> { - // TODO: Stdout option, probably in a separate implementation - if &dest[..4] != "http" { - panic!( - "dest formatted as {} but send -d expected stdout or http://IP:port", - dest - ); - } - - updater::refresh_outputs(wallet)?; - +) -> Result< + ( + Slate, + sigcontext::Context, + impl FnOnce(&mut T) -> Result<(), Error>, + ), + Error, +> { // Get lock height - let chain_tip = updater::get_tip_from_node(wallet.node_url())?; - let current_height = chain_tip.height; + let current_height = wallet.get_chain_height(wallet.node_url())?; // ensure outputs we're selecting are up to date updater::refresh_outputs(wallet)?; @@ -112,50 +98,34 @@ pub fn issue_send_tx( 0, )?; - let url = format!("{}/v1/wallet/foreign/receive_tx", dest); - debug!(LOGGER, "Posting partial transaction to {}", url); - let mut slate = match client::send_slate(&url, &slate, fluff).context(ErrorKind::Node) { - Ok(s) => s, - Err(e) => { - error!( - LOGGER, - "Communication with receiver failed on SenderInitiation send. Aborting transaction" - ); - return Err(e)?; - } - }; + Ok((slate, context, sender_lock_fn)) +} +/// Complete a transaction as the sender +pub fn complete_tx( + wallet: &mut T, + slate: &mut Slate, + context: &sigcontext::Context, +) -> Result<(), Error> { let _ = slate.fill_round_2(wallet.keychain(), &context.sec_key, &context.sec_nonce, 0)?; - // Final transaction can be built by anyone at this stage - slate.finalize(wallet.keychain())?; - - // 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", wallet.node_url(),); - } else { - url = format!("{}/v1/pool/push", wallet.node_url()); + let res = slate.finalize(wallet.keychain()); + if let Err(e) = res { + Err(ErrorKind::LibTX(e.kind()))? } - api::client::post(url.as_str(), &TxWrapper { tx_hex: tx_hex }).context(ErrorKind::Node)?; - - // All good so, lock our inputs - sender_lock_fn(wallet)?; Ok(()) } /// Issue a burn tx -pub fn issue_burn_tx( +pub fn issue_burn_tx( wallet: &mut T, amount: u64, minimum_confirmations: u64, max_outputs: usize, -) -> Result<(), Error> { +) -> Result { let keychain = &Keychain::burn_enabled(wallet.keychain(), &Identifier::zero()); - let chain_tip = updater::get_tip_from_node(wallet.node_url())?; - let current_height = chain_tip.height; + let current_height = wallet.get_chain_height(wallet.node_url())?; let _ = updater::refresh_outputs(wallet); @@ -184,12 +154,7 @@ pub fn issue_burn_tx( // finalize the burn transaction and send let tx_burn = build::transaction(parts, &keychain)?; tx_burn.validate()?; - - let tx_hex = util::to_hex(ser::ser_vec(&tx_burn).unwrap()); - 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(()) + Ok(tx_burn) } #[cfg(test)] diff --git a/wallet/src/libwallet/internal/updater.rs b/wallet/src/libwallet/internal/updater.rs index a7387614..d4b9b208 100644 --- a/wallet/src/libwallet/internal/updater.rs +++ b/wallet/src/libwallet/internal/updater.rs @@ -19,7 +19,6 @@ use failure::ResultExt; use std::collections::HashMap; use std::collections::hash_map::Entry; -use api; use core::consensus::reward; use core::core::{Output, TxKernel}; use core::global; @@ -69,24 +68,26 @@ pub fn retrieve_outputs( /// from a node pub fn refresh_outputs(wallet: &mut T) -> Result<(), Error> where - T: WalletBackend, + T: WalletBackend + WalletClient, { - let tip = get_tip_from_node(&wallet.node_url())?; - refresh_output_state(wallet, &tip)?; - refresh_missing_block_hashes(wallet, &tip)?; + let height = wallet.get_chain_height(wallet.node_url())?; + refresh_output_state(wallet, height)?; + refresh_missing_block_hashes(wallet, height)?; Ok(()) } // TODO - this might be slow if we have really old outputs that have never been // refreshed -fn refresh_missing_block_hashes(wallet: &mut T, tip: &api::Tip) -> Result<(), Error> +fn refresh_missing_block_hashes(wallet: &mut T, height: u64) -> Result<(), Error> where - T: WalletBackend, + T: WalletBackend + WalletClient, { // 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(wallet)?; + let wallet_output_keys = wallet_outputs.keys().map(|commit| commit.clone()).collect(); + // nothing to do so return (otherwise we hit the api with a monster query...) if wallet_outputs.is_empty() { return Ok(()); @@ -98,40 +99,8 @@ where wallet_outputs.len(), ); - let id_params: Vec = wallet_outputs - .keys() - .map(|commit| format!("id={}", util::to_hex(commit.as_ref().to_vec()))) - .collect(); - - let height_params = [format!("start_height={}&end_height={}", 0, tip.height)]; - - let mut api_blocks: HashMap = HashMap::new(); - let mut api_merkle_proofs: HashMap = HashMap::new(); - - // Split up into separate requests, to avoid hitting http limits - for mut query_chunk in id_params.chunks(1000) { - let url = format!( - "{}/v1/chain/outputs/byheight?{}", - wallet.node_url(), - [&height_params, query_chunk].concat().join("&"), - ); - - match api::client::get::>(url.as_str()) { - Ok(blocks) => for block in blocks { - for out in block.outputs { - api_blocks.insert(out.commit, block.header.clone()); - if let Some(merkle_proof) = out.merkle_proof { - let wrapper = MerkleProofWrapper(merkle_proof); - api_merkle_proofs.insert(out.commit, wrapper); - } - } - }, - Err(e) => { - // if we got anything other than 200 back from server, bye - return Err(e).context(ErrorKind::Node)?; - } - } - } + let (api_blocks, api_merkle_proofs) = + wallet.get_missing_block_hashes_from_node(wallet.node_url(), height, wallet_output_keys)?; // now for each commit, find the output in the wallet and // the corresponding api output (if it exists) @@ -143,8 +112,8 @@ where 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()); - output.height = b.height; + output.height = b.0; + output.block = Some(b.1.clone()); if let Some(merkle_proof) = api_merkle_proofs.get(&commit) { output.merkle_proof = Some(merkle_proof.clone()); } @@ -206,7 +175,7 @@ where pub fn apply_api_outputs( wallet: &mut T, wallet_outputs: &HashMap, - api_outputs: &HashMap, + api_outputs: &HashMap, ) -> Result<(), Error> where T: WalletBackend, @@ -229,9 +198,9 @@ where /// 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(wallet: &mut T, tip: &api::Tip) -> Result<(), Error> +fn refresh_output_state(wallet: &mut T, height: u64) -> Result<(), Error> where - T: WalletBackend, + T: WalletBackend + WalletClient, { debug!(LOGGER, "Refreshing wallet outputs"); @@ -239,73 +208,41 @@ where // and a list of outputs we want to query the node for let wallet_outputs = map_wallet_outputs(wallet)?; - // build the necessary query params - - // ?id=xxx&id=yyy&id=zzz - let query_params: Vec = wallet_outputs - .keys() - .map(|commit| format!("id={}", util::to_hex(commit.as_ref().to_vec()))) - .collect(); - - // build a map of api outputs by commit so we can look them up efficiently - let mut api_outputs: HashMap = HashMap::new(); - - for query_chunk in query_params.chunks(1000) { - let url = format!( - "{}/v1/chain/outputs/byids?{}", - wallet.node_url(), - query_chunk.join("&"), - ); - - match api::client::get::>(url.as_str()) { - Ok(outputs) => for out in outputs { - api_outputs.insert(out.commit.commit(), out); - }, - Err(e) => { - // if we got anything other than 200 back from server, don't attempt to refresh - // the wallet data after - return Err(e).context(ErrorKind::Node)?; - } - } - } + let wallet_output_keys = wallet_outputs.keys().map(|commit| commit.clone()).collect(); + let api_outputs = wallet.get_outputs_from_node(wallet.node_url(), wallet_output_keys)?; apply_api_outputs(wallet, &wallet_outputs, &api_outputs)?; - clean_old_unconfirmed(wallet, tip)?; + clean_old_unconfirmed(wallet, height)?; Ok(()) } -fn clean_old_unconfirmed(wallet: &mut T, tip: &api::Tip) -> Result<(), Error> +fn clean_old_unconfirmed(wallet: &mut T, height: u64) -> Result<(), Error> where T: WalletBackend, { - if tip.height < 500 { + if height < 500 { return Ok(()); } wallet.with_wallet(|wallet_data| { wallet_data.outputs().retain(|_, ref mut out| { !(out.status == OutputStatus::Unconfirmed && out.height > 0 - && out.height < tip.height - 500) + && out.height < height - 500) }); }) } -/// Return the chain tip from a given node -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()) -} - -/// Retrieve summar info about the wallet +/// Retrieve summary info about the wallet pub fn retrieve_info(wallet: &mut T) -> Result where - T: WalletBackend, + T: WalletBackend + WalletClient, { let result = refresh_outputs(wallet); + let height_res = wallet.get_chain_height(&wallet.node_url()); + let ret_val = wallet.read_wallet(|wallet_data| { - let (current_height, from) = match get_tip_from_node(&wallet_data.node_url()) { - Ok(tip) => (tip.height, "from server node"), + let (current_height, from) = match height_res { + Ok(height) => (height, "from server node"), Err(_) => match wallet_data.outputs().values().map(|out| out.height).max() { Some(height) => (height, "from wallet"), None => (0, "node/wallet unavailable"), diff --git a/wallet/src/libwallet/mod.rs b/wallet/src/libwallet/mod.rs index b9ca215c..2b2f2407 100644 --- a/wallet/src/libwallet/mod.rs +++ b/wallet/src/libwallet/mod.rs @@ -23,7 +23,6 @@ #![warn(missing_docs)] pub mod api; -pub mod client; pub mod controller; mod error; pub mod internal; diff --git a/wallet/src/libwallet/types.rs b/wallet/src/libwallet/types.rs index 65840e0b..413a12e0 100644 --- a/wallet/src/libwallet/types.rs +++ b/wallet/src/libwallet/types.rs @@ -14,6 +14,7 @@ //! Types and traits that should be provided by a wallet //! implementation + use std::collections::HashMap; use std::fmt; @@ -26,8 +27,11 @@ use core::core::pmmr::MerkleProof; use keychain::{Identifier, Keychain}; +use libtx::slate::Slate; use libwallet::error::{Error, ErrorKind}; +use util::secp::pedersen; + /// TODO: /// Wallets should implement this backend for their storage. All functions /// here expect that the wallet instance has instantiated itself or stored @@ -42,9 +46,6 @@ 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; @@ -90,6 +91,55 @@ pub trait WalletBackend { fn restore(&mut self) -> Result<(), Error>; } +/// Encapsulate all communication functions. No functions within libwallet +/// should care about communication details +pub trait WalletClient { + /// Return the URL of the check node + fn node_url(&self) -> &str; + + /// Call the wallet API to create a coinbase transaction + fn create_coinbase(&self, dest: &str, block_fees: &BlockFees) -> Result; + + /// Send a transaction slate to another listening wallet and return result + /// TODO: Probably need a slate wrapper type + fn send_tx_slate(&self, dest: &str, slate: &Slate) -> Result; + + /// Posts a tranaction to a grin node + fn post_tx(&self, dest: &str, tx: &TxWrapper, fluff: bool) -> Result<(), Error>; + + /// retrieves the current tip from the specified grin node + fn get_chain_height(&self, addr: &str) -> Result; + + /// retrieve a list of outputs from the specified grin node + /// need "by_height" and "by_id" variants + fn get_outputs_from_node( + &self, + addr: &str, + wallet_outputs: Vec, + ) -> Result, Error>; + + /// Get any missing block hashes from node + fn get_missing_block_hashes_from_node( + &self, + addr: &str, + height: u64, + wallet_outputs: Vec, + ) -> Result< + ( + HashMap, + HashMap, + ), + Error, + >; + + /// retrieve merkle proof for a commit from a node + fn get_merkle_proof_for_commit( + &self, + addr: &str, + commit: &str, + ) -> Result; +} + /// 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.*/ diff --git a/wallet/tests/common/mod.rs b/wallet/tests/common/mod.rs index 13cfcdca..3a0c3eb4 100644 --- a/wallet/tests/common/mod.rs +++ b/wallet/tests/common/mod.rs @@ -32,6 +32,7 @@ use wallet::libwallet::internal::updater; use wallet::libwallet::types::*; use wallet::libwallet::{Error, ErrorKind}; +use util; use util::secp::pedersen; /// Mostly for testing, refreshes output state against a local chain instance @@ -48,11 +49,11 @@ pub fn refresh_output_state_local( Ok(k) => Some(k), }) .collect(); - let mut api_outputs: HashMap = HashMap::new(); + let mut api_outputs: HashMap = HashMap::new(); for out in chain_outputs { match out { Some(o) => { - api_outputs.insert(o.commit.commit(), o); + api_outputs.insert(o.commit.commit(), util::to_hex(o.commit.to_vec())); } None => {} }