Factor out wallet communications ()

* move http calls out from libwallet internal

* rustfmt

* start to think about wallet communication traits

* rustfmt

* start of factoring out wallet client trait

* rustfmt

* move node_url trait fn into walletclient

* rustfmt

* comms factored out (with exception of wallet restore)

* rustfmt

* fix test

* rustfmt

* further test fix
This commit is contained in:
Yeastplume 2018-06-07 15:04:21 +01:00 committed by GitHub
parent fffee377dd
commit ebee05591b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 498 additions and 344 deletions

View file

@ -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));
}
}

View file

@ -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

222
wallet/src/client.rs Normal file
View file

@ -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<CbData, Error> {
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<Slate, Error> {
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::<hyper::Uri>().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<CbData, Error> {
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::<hyper::Uri>().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<u64, Error> {
let url = format!("{}/v1/chain", addr);
let res = api::client::get::<api::Tip>(url.as_str()).context(ErrorKind::Node)?;
Ok(res.height)
}
/// Retrieve outputs from node
pub fn get_outputs_from_node(
addr: &str,
wallet_outputs: Vec<pedersen::Commitment>,
) -> Result<HashMap<pedersen::Commitment, String>, Error> {
// build the necessary query params -
// ?id=xxx&id=yyy&id=zzz
let query_params: Vec<String> = 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<pedersen::Commitment, String> = 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::<Vec<api::Output>>(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<pedersen::Commitment>,
) -> Result<
(
HashMap<pedersen::Commitment, (u64, BlockIdentifier)>,
HashMap<pedersen::Commitment, MerkleProofWrapper>,
),
Error,
> {
let id_params: Vec<String> = 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<pedersen::Commitment, (u64, BlockIdentifier)> = HashMap::new();
let mut api_merkle_proofs: HashMap<pedersen::Commitment, MerkleProofWrapper> = 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::<Vec<api::BlockOutputs>>(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))
}

View file

@ -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<String, OutputData> {
&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<CbData, libwallet::Error> {
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<Slate, libwallet::Error> {
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<u64, libwallet::Error> {
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<pedersen::Commitment>,
) -> Result<HashMap<pedersen::Commitment, String>, 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<pedersen::Commitment>,
) -> Result<
(
HashMap<pedersen::Commitment, (u64, BlockIdentifier)>,
HashMap<pedersen::Commitment, MerkleProofWrapper>,
),
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<MerkleProofWrapper, libwallet::Error> {
Err(libwallet::ErrorKind::GenericError("Not Implemented"))?
}
}
impl FileWallet {
/// Create a new FileWallet instance
pub fn new(config: WalletConfig, passphrase: &str) -> Result<Self, Error> {

View file

@ -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;

View file

@ -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> {

View file

@ -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<CbData, Error> {
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<Slate, Error> {
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::<hyper::Uri>().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<CbData, Error> {
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::<hyper::Uri>().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)
}

View file

@ -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<F, T>(wallet: &mut T, f: F) -> Result<(), Error>
where
T: WalletBackend,
T: WalletBackend + WalletClient,
F: FnOnce(&mut APIOwner<T>) -> 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<F, T>(wallet: &mut T, f: F) -> Result<(), Error>
where
T: WalletBackend,
T: WalletBackend + WalletClient,
F: FnOnce(&mut APIForeign<T>) -> Result<(), Error>,
{
wallet.open_with_credentials()?;
@ -91,7 +91,7 @@ where
/// port and wrapping the calls
pub fn foreign_listener<T>(wallet: T, addr: &str) -> Result<(), Error>
where
T: WalletBackend,
T: WalletBackend + WalletClient,
ForeignAPIHandler<T>: Handler,
{
let api_handler = ForeignAPIHandler {
@ -125,7 +125,7 @@ where
impl<T> OwnerAPIHandler<T>
where
T: WalletBackend,
T: WalletBackend + WalletClient,
{
fn retrieve_outputs(
&self,
@ -177,7 +177,7 @@ where
impl<T> Handler for OwnerAPIHandler<T>
where
T: WalletBackend + Send + Sync + 'static,
T: WalletBackend + WalletClient + Send + Sync + 'static,
{
fn handle(&self, req: &mut Request) -> IronResult<Response> {
// every request should open with stored credentials,
@ -201,7 +201,7 @@ where
pub struct ForeignAPIHandler<T>
where
T: WalletBackend,
T: WalletBackend + WalletClient,
{
/// Wallet instance
pub wallet: Arc<Mutex<T>>,
@ -209,7 +209,7 @@ where
impl<T> ForeignAPIHandler<T>
where
T: WalletBackend,
T: WalletBackend + WalletClient,
{
fn build_coinbase(&self, req: &mut Request, api: &mut APIForeign<T>) -> Result<CbData, Error> {
let struct_body = req.get::<bodyparser::Struct<BlockFees>>();
@ -258,7 +258,7 @@ where
impl<T> Handler for ForeignAPIHandler<T>
where
T: WalletBackend + Send + Sync + 'static,
T: WalletBackend + WalletClient + Send + Sync + 'static,
{
fn handle(&self, req: &mut Request) -> IronResult<Response> {
// every request should open with stored credentials,

View file

@ -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,

View file

@ -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<u64, Error> {
let url = format!("{}/v1/chain", node_addr);
match api::client::get::<api::Tip>(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<MerkleProofWrapper, Error> {
let url = format!("{}/v1/txhashset/merkleproof?id={}", node_addr, commit);
@ -70,7 +53,7 @@ fn coinbase_status(output: &api::OutputPrintable) -> bool {
fn outputs_batch<T>(wallet: &T, start_height: u64, max: u64) -> Result<api::OutputListing, Error>
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<T: WalletBackend>(
fn find_outputs_with_key<T: WalletBackend + WalletClient>(
wallet: &mut T,
outputs: Vec<api::OutputPrintable>,
found_key_index: &mut Vec<u32>,
@ -120,7 +103,7 @@ fn find_outputs_with_key<T: WalletBackend>(
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<T: WalletBackend>(
}
/// Restore a wallet
pub fn restore<T: WalletBackend>(wallet: &mut T) -> Result<(), Error> {
pub fn restore<T: WalletBackend + WalletClient>(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))

View file

@ -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<T: WalletBackend>(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<T: WalletBackend>(
pub fn create_send_tx<T: WalletBackend + WalletClient>(
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<T: WalletBackend>(
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<T: WalletBackend>(
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<T: WalletBackend>(
pub fn issue_burn_tx<T: WalletBackend + WalletClient>(
wallet: &mut T,
amount: u64,
minimum_confirmations: u64,
max_outputs: usize,
) -> Result<(), Error> {
) -> Result<Transaction, Error> {
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<T: WalletBackend>(
// 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)]

View file

@ -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<T: WalletBackend>(
/// from a node
pub fn refresh_outputs<T>(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<T>(wallet: &mut T, tip: &api::Tip) -> Result<(), Error>
fn refresh_missing_block_hashes<T>(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<String> = 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<pedersen::Commitment, api::BlockHeaderInfo> = HashMap::new();
let mut api_merkle_proofs: HashMap<pedersen::Commitment, MerkleProofWrapper> = 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::<Vec<api::BlockOutputs>>(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<T>(
wallet: &mut T,
wallet_outputs: &HashMap<pedersen::Commitment, Identifier>,
api_outputs: &HashMap<pedersen::Commitment, api::Output>,
api_outputs: &HashMap<pedersen::Commitment, String>,
) -> 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<T>(wallet: &mut T, tip: &api::Tip) -> Result<(), Error>
fn refresh_output_state<T>(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<String> = 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<pedersen::Commitment, api::Output> = 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::<Vec<api::Output>>(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<T>(wallet: &mut T, tip: &api::Tip) -> Result<(), Error>
fn clean_old_unconfirmed<T>(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<api::Tip, Error> {
let url = format!("{}/v1/chain", addr);
api::client::get::<api::Tip>(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<T>(wallet: &mut T) -> Result<WalletInfo, Error>
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"),

View file

@ -23,7 +23,6 @@
#![warn(missing_docs)]
pub mod api;
pub mod client;
pub mod controller;
mod error;
pub mod internal;

View file

@ -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<String, OutputData>;
@ -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<CbData, Error>;
/// 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<Slate, Error>;
/// 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<u64, Error>;
/// 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<pedersen::Commitment>,
) -> Result<HashMap<pedersen::Commitment, String>, Error>;
/// Get any missing block hashes from node
fn get_missing_block_hashes_from_node(
&self,
addr: &str,
height: u64,
wallet_outputs: Vec<pedersen::Commitment>,
) -> Result<
(
HashMap<pedersen::Commitment, (u64, BlockIdentifier)>,
HashMap<pedersen::Commitment, MerkleProofWrapper>,
),
Error,
>;
/// retrieve merkle proof for a commit from a node
fn get_merkle_proof_for_commit(
&self,
addr: &str,
commit: &str,
) -> Result<MerkleProofWrapper, Error>;
}
/// 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.*/

View file

@ -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<T: WalletBackend>(
Ok(k) => Some(k),
})
.collect();
let mut api_outputs: HashMap<pedersen::Commitment, api::Output> = HashMap::new();
let mut api_outputs: HashMap<pedersen::Commitment, String> = 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 => {}
}