Feature/slate serialization (#2534)

* - Add backwards compatability
- Add hex serialization

* rustfmt

* rustfmt

* Windows Compatibility Fixes #1 (#2535)

* initial changes for windows build and unit/integration tests

* rustfmt

* wallet+store tests

* rustfmt

* fix linux daemonize

* better encapsulate file rename

* rustfmt

* remove daemonize commands

* rustfmt

* remove server start/stop commands

* add ability to drop pmmr backend files explicitly for txhashset unzip

* rustfmt

* fix pmmr tests

* rustfmt

* Windows TUI Fix (#2555)

* switch pancurses backend to win32

* revert changes to restore test

* compatibility fix + debug messages

* rustfmt

* Add content disposition for OK responses  (#2545)

* Testing http send and fixing accordingly

* add repost method into wallet owner api (#2553)

* add repost method into wallet owner api

* rustfmt

* Add ability to compare selection strategies (#2516)

Before tx creation user can estimate fee and locked amount
with different selection strategies by providing `-e` flag for
`wallet send` command.
This commit is contained in:
Yoni Svechinsky
2019-02-13 15:29:44 +02:00
committed by Yeastplume
parent 0d36acf01b
commit ee4eed71ea
39 changed files with 2408 additions and 1221 deletions
+5 -4
View File
@@ -16,10 +16,10 @@
use std::fs::File;
use std::io::{Read, Write};
use crate::adapters::util::{deserialize_slate, serialize_slate};
use crate::libwallet::slate::Slate;
use crate::libwallet::{Error, ErrorKind};
use crate::libwallet::Error;
use crate::{WalletCommAdapter, WalletConfig};
use serde_json as json;
use std::collections::HashMap;
#[derive(Clone)]
@@ -43,7 +43,8 @@ impl WalletCommAdapter for FileWalletCommAdapter {
fn send_tx_async(&self, dest: &str, slate: &Slate) -> Result<(), Error> {
let mut pub_tx = File::create(dest)?;
pub_tx.write_all(json::to_string(&slate).unwrap().as_bytes())?;
let slate_string = serialize_slate(slate);
pub_tx.write_all(slate_string.as_bytes())?;
pub_tx.sync_all()?;
Ok(())
}
@@ -52,7 +53,7 @@ impl WalletCommAdapter for FileWalletCommAdapter {
let mut pub_tx_f = File::open(params)?;
let mut content = String::new();
pub_tx_f.read_to_string(&mut content)?;
Ok(json::from_str(&content).map_err(|err| ErrorKind::Format(err.to_string()))?)
Ok(deserialize_slate(&content))
}
fn listen(
+5 -4
View File
@@ -12,9 +12,10 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use crate::adapters::util::get_versioned_slate;
use crate::api;
use crate::controller;
use crate::libwallet::slate::Slate;
use crate::libwallet::slate::{Slate, VersionedSlate};
use crate::libwallet::{Error, ErrorKind};
use crate::{instantiate_wallet, HTTPNodeClient, WalletCommAdapter, WalletConfig};
/// HTTP Wallet 'plugin' implementation
@@ -47,15 +48,15 @@ impl WalletCommAdapter for HTTPWalletCommAdapter {
}
let url = format!("{}/v1/wallet/foreign/receive_tx", dest);
debug!("Posting transaction slate to {}", url);
let res = api::client::post(url.as_str(), None, slate);
let slate = get_versioned_slate(slate);
let res: Result<VersionedSlate, _> = api::client::post(url.as_str(), None, &slate);
match res {
Err(e) => {
let report = format!("Posting transaction slate (is recipient listening?): {}", e);
error!("{}", report);
Err(ErrorKind::ClientCallback(report).into())
}
Ok(r) => Ok(r),
Ok(r) => Ok(r.into()),
}
}
+33 -17
View File
@@ -15,7 +15,8 @@
// Keybase Wallet Plugin
use crate::controller;
use crate::libwallet::slate::Slate;
use crate::libwallet::slate::{Slate, VersionedSlate};
use crate::libwallet::slate_versions::v0::SlateV0;
use crate::libwallet::{Error, ErrorKind};
use crate::{instantiate_wallet, HTTPNodeClient, WalletCommAdapter, WalletConfig};
use failure::ResultExt;
@@ -192,6 +193,7 @@ fn get_unread(topic: &str) -> Result<HashMap<String, String>, Error> {
/// Send a message to a keybase channel that self-destructs after ttl seconds.
fn send<T: Serialize>(message: T, channel: &str, topic: &str, ttl: u16) -> bool {
let seconds = format!("{}s", ttl);
let serialized = to_string(&message).unwrap();
let payload = to_string(&json!({
"method": "send",
"params": {
@@ -200,7 +202,7 @@ fn send<T: Serialize>(message: T, channel: &str, topic: &str, ttl: u16) -> bool
"name": channel, "topic_name": topic, "topic_type": "dev"
},
"message": {
"body": to_string(&message).unwrap()
"body": serialized
},
"exploding_lifetime": seconds
}
@@ -210,7 +212,10 @@ fn send<T: Serialize>(message: T, channel: &str, topic: &str, ttl: u16) -> bool
let response = api_send(&payload);
if let Ok(res) = response {
match res["result"]["message"].as_str() {
Some("message sent") => true,
Some("message sent") => {
debug!("Message sent to {}: {}", channel, serialized);
true
}
_ => false,
}
} else {
@@ -254,9 +259,10 @@ fn poll(nseconds: u64, channel: &str) -> Option<Slate> {
while start.elapsed().as_secs() < nseconds {
let unread = read_from_channel(channel, SLATE_SIGNED);
for msg in unread.unwrap().iter() {
let blob = from_str::<Slate>(msg);
let blob = from_str::<VersionedSlate>(msg);
match blob {
Ok(slate) => {
let slate: Slate = slate.into();
info!(
"keybase response message received from @{}, tx uuid: {}",
channel, slate.id,
@@ -288,8 +294,10 @@ impl WalletCommAdapter for KeybaseWalletCommAdapter {
return Err(ErrorKind::GenericError("Tx rejected".to_owned()))?;
}
let id = slate.id;
// Send original slate to recipient with the SLATE_NEW topic
match send(slate, addr, SLATE_NEW, TTL) {
match send(&slate, addr, SLATE_NEW, TTL) {
true => (),
false => {
return Err(ErrorKind::ClientCallback(
@@ -297,10 +305,7 @@ impl WalletCommAdapter for KeybaseWalletCommAdapter {
))?
}
}
info!(
"tx request has been sent to @{}, tx uuid: {}",
addr, slate.id
);
info!("tx request has been sent to @{}, tx uuid: {}", addr, id);
// Wait for response from recipient with SLATE_SIGNED topic
match poll(TTL as u64, addr) {
Some(slate) => return Ok(slate),
@@ -345,9 +350,10 @@ impl WalletCommAdapter for KeybaseWalletCommAdapter {
break;
}
for (msg, channel) in &unread.unwrap() {
let blob = from_str::<Slate>(msg);
let blob = from_str::<VersionedSlate>(msg);
match blob {
Ok(mut slate) => {
Ok(message) => {
let mut slate: Slate = message.clone().into();
let tx_uuid = slate.id;
// Reject multiple recipients channel for safety
@@ -378,19 +384,29 @@ impl WalletCommAdapter for KeybaseWalletCommAdapter {
Ok(())
}) {
// Reply to the same channel with topic SLATE_SIGNED
Ok(_) => match send(slate, channel, SLATE_SIGNED, TTL) {
true => {
Ok(_) => {
let success = match message {
// Send the same version of slate that was sent to us
VersionedSlate::V0(_) => {
send(SlateV0::from(slate), channel, SLATE_SIGNED, TTL)
}
VersionedSlate::V1(_) => {
send(slate, channel, SLATE_SIGNED, TTL)
}
};
if success {
notify_on_receive(
config.keybase_notify_ttl.unwrap_or(1440),
channel.to_string(),
tx_uuid.to_string(),
);
debug!("Returned slate to @{} via keybase", channel);
}
false => {
} else {
error!("Failed to return slate to @{} via keybase. Incoming tx failed", channel);
}
},
}
Err(e) => {
error!(
"Error on receiving tx via keybase: {}. Incoming tx failed",
@@ -399,7 +415,7 @@ impl WalletCommAdapter for KeybaseWalletCommAdapter {
}
}
}
Err(_) => (),
Err(_) => debug!("Failed to deserialize keybase message: {}", msg),
}
}
sleep(LISTEN_SLEEP_DURATION);
+1
View File
@@ -16,6 +16,7 @@ mod file;
mod http;
mod keybase;
mod null;
pub mod util;
pub use self::file::FileWalletCommAdapter;
pub use self::http::HTTPWalletCommAdapter;
+23
View File
@@ -0,0 +1,23 @@
use crate::libwallet::slate::{Slate, VersionedSlate};
use crate::libwallet::slate_versions::v0::SlateV0;
use crate::libwallet::ErrorKind;
use serde_json as json;
pub fn get_versioned_slate(slate: &Slate) -> VersionedSlate {
let slate = slate.clone();
match slate.version {
0 => VersionedSlate::V0(SlateV0::from(slate)),
_ => VersionedSlate::V1(slate),
}
}
pub fn serialize_slate(slate: &Slate) -> String {
json::to_string(&get_versioned_slate(slate)).unwrap()
}
pub fn deserialize_slate(raw_slate: &str) -> Slate {
let versioned_slate: VersionedSlate = json::from_str(&raw_slate)
.map_err(|err| ErrorKind::Format(err.to_string()))
.unwrap();
versioned_slate.into()
}
+77 -55
View File
@@ -200,6 +200,7 @@ pub struct SendArgs {
pub message: Option<String>,
pub minimum_confirmations: u64,
pub selection_strategy: String,
pub estimate_selection_strategies: bool,
pub method: String,
pub dest: String,
pub change_outputs: usize,
@@ -210,68 +211,89 @@ pub struct SendArgs {
pub fn send(
wallet: Arc<Mutex<WalletInst<impl NodeClient + 'static, keychain::ExtKeychain>>>,
args: SendArgs,
dark_scheme: bool,
) -> Result<(), Error> {
controller::owner_single_use(wallet.clone(), |api| {
let result = api.initiate_tx(
None,
args.amount,
args.minimum_confirmations,
args.max_outputs,
args.change_outputs,
args.selection_strategy == "all",
args.message.clone(),
);
let (mut slate, lock_fn) = match result {
Ok(s) => {
info!(
"Tx created: {} grin to {} (strategy '{}')",
core::amount_to_hr_string(args.amount, false),
args.dest,
args.selection_strategy,
);
s
}
Err(e) => {
info!("Tx not created: {}", e);
return Err(e);
}
};
let adapter = match args.method.as_str() {
"http" => HTTPWalletCommAdapter::new(),
"file" => FileWalletCommAdapter::new(),
"keybase" => KeybaseWalletCommAdapter::new(),
"self" => NullWalletCommAdapter::new(),
_ => NullWalletCommAdapter::new(),
};
if adapter.supports_sync() {
slate = adapter.send_tx_sync(&args.dest, &slate)?;
api.tx_lock_outputs(&slate, lock_fn)?;
if args.method == "self" {
controller::foreign_single_use(wallet, |api| {
api.receive_tx(&mut slate, Some(&args.dest), None)?;
Ok(())
})?;
}
if let Err(e) = api.verify_slate_messages(&slate) {
error!("Error validating participant messages: {}", e);
return Err(e);
}
api.finalize_tx(&mut slate)?;
if args.estimate_selection_strategies {
let strategies = vec!["smallest", "all"]
.into_iter()
.map(|strategy| {
let (total, fee) = api
.estimate_initiate_tx(
None,
args.amount,
args.minimum_confirmations,
args.max_outputs,
args.change_outputs,
strategy == "all",
)
.unwrap();
(strategy, total, fee)
})
.collect();
display::estimate(args.amount, strategies, dark_scheme);
} else {
adapter.send_tx_async(&args.dest, &slate)?;
api.tx_lock_outputs(&slate, lock_fn)?;
}
if adapter.supports_sync() {
let result = api.post_tx(&slate.tx, args.fluff);
match result {
Ok(_) => {
info!("Tx sent ok",);
return Ok(());
let result = api.initiate_tx(
None,
args.amount,
args.minimum_confirmations,
args.max_outputs,
args.change_outputs,
args.selection_strategy == "all",
args.message.clone(),
);
let (mut slate, lock_fn) = match result {
Ok(s) => {
info!(
"Tx created: {} grin to {} (strategy '{}')",
core::amount_to_hr_string(args.amount, false),
args.dest,
args.selection_strategy,
);
s
}
Err(e) => {
error!("Tx sent fail: {}", e);
info!("Tx not created: {}", e);
return Err(e);
}
};
let adapter = match args.method.as_str() {
"http" => HTTPWalletCommAdapter::new(),
"file" => FileWalletCommAdapter::new(),
"keybase" => KeybaseWalletCommAdapter::new(),
"self" => NullWalletCommAdapter::new(),
_ => NullWalletCommAdapter::new(),
};
if adapter.supports_sync() {
slate = adapter.send_tx_sync(&args.dest, &slate)?;
api.tx_lock_outputs(&slate, lock_fn)?;
if args.method == "self" {
controller::foreign_single_use(wallet, |api| {
api.receive_tx(&mut slate, Some(&args.dest), None)?;
Ok(())
})?;
}
if let Err(e) = api.verify_slate_messages(&slate) {
error!("Error validating participant messages: {}", e);
return Err(e);
}
api.finalize_tx(&mut slate)?;
} else {
adapter.send_tx_async(&args.dest, &slate)?;
api.tx_lock_outputs(&slate, lock_fn)?;
}
if adapter.supports_sync() {
let result = api.post_tx(&slate.tx, args.fluff);
match result {
Ok(_) => {
info!("Tx sent ok",);
return Ok(());
}
Err(e) => {
error!("Tx sent fail: {}", e);
return Err(e);
}
}
}
}
Ok(())
+108 -9
View File
@@ -15,13 +15,14 @@
//! Controller for wallet.. instantiates and handles listeners (or single-run
//! invocations) as needed.
//! Still experimental
use crate::adapters::util::get_versioned_slate;
use crate::adapters::{FileWalletCommAdapter, HTTPWalletCommAdapter, KeybaseWalletCommAdapter};
use crate::api::{ApiServer, BasicAuthMiddleware, Handler, ResponseFuture, Router, TLSConfig};
use crate::core::core;
use crate::core::core::Transaction;
use crate::keychain::Keychain;
use crate::libwallet::api::{APIForeign, APIOwner};
use crate::libwallet::slate::Slate;
use crate::libwallet::slate::{Slate, VersionedSlate};
use crate::libwallet::types::{
CbData, NodeClient, OutputData, SendTXArgs, TxLogEntry, WalletBackend, WalletInfo,
};
@@ -40,6 +41,7 @@ use std::marker::PhantomData;
use std::net::SocketAddr;
use std::sync::Arc;
use url::form_urlencoded;
use uuid::Uuid;
/// Instantiate wallet Owner API for a single-use (command line) call
/// Return a function containing a loaded API context to call
@@ -467,6 +469,87 @@ where
))
}
pub fn repost(
&self,
req: Request<Body>,
api: APIOwner<T, C, K>,
) -> Box<dyn Future<Item = (), Error = Error> + Send> {
let params = parse_params(&req);
let mut id_int: Option<u32> = None;
let mut tx_uuid: Option<Uuid> = None;
if let Some(id_string) = params.get("id") {
match id_string[0].parse() {
Ok(id) => id_int = Some(id),
Err(e) => {
error!("repost: could not parse id: {}", e);
return Box::new(err(ErrorKind::GenericError(
"repost: cannot repost transaction. Could not parse id in request."
.to_owned(),
)
.into()));
}
}
} else if let Some(tx_id_string) = params.get("tx_id") {
match tx_id_string[0].parse() {
Ok(tx_id) => tx_uuid = Some(tx_id),
Err(e) => {
error!("repost: could not parse tx_id: {}", e);
return Box::new(err(ErrorKind::GenericError(
"repost: cannot repost transaction. Could not parse tx_id in request."
.to_owned(),
)
.into()));
}
}
} else {
return Box::new(err(ErrorKind::GenericError(
"repost: Cannot repost transaction. Missing id or tx_id param in request."
.to_owned(),
)
.into()));
}
let res = api.retrieve_txs(true, id_int, tx_uuid);
if let Err(e) = res {
return Box::new(err(ErrorKind::GenericError(format!(
"repost: cannot repost transaction. retrieve_txs failed, err: {:?}",
e
))
.into()));
}
let (_, txs) = res.unwrap();
let res = api.get_stored_tx(&txs[0]);
if let Err(e) = res {
return Box::new(err(ErrorKind::GenericError(format!(
"repost: cannot repost transaction. get_stored_tx failed, err: {:?}",
e
))
.into()));
}
let stored_tx = res.unwrap();
if stored_tx.is_none() {
error!(
"Transaction with id {:?}/{:?} does not have transaction data. Not reposting.",
id_int, tx_uuid,
);
return Box::new(err(ErrorKind::GenericError(
"repost: Cannot repost transaction. Missing id or tx_id param in request."
.to_owned(),
)
.into()));
}
let fluff = params.get("fluff").is_some();
Box::new(match api.post_tx(&stored_tx.unwrap(), fluff) {
Ok(_) => ok(()),
Err(e) => {
error!("repost: failed with error: {}", e);
err(e)
}
})
}
fn handle_post_request(&self, req: Request<Body>) -> WalletResponseFuture {
let api = APIOwner::new(self.wallet.clone());
match req
@@ -487,10 +570,14 @@ where
),
"cancel_tx" => Box::new(
self.cancel_tx(req, api)
.and_then(|_| ok(response(StatusCode::OK, ""))),
.and_then(|_| ok(response(StatusCode::OK, "{}"))),
),
"post_tx" => Box::new(
self.post_tx(req, api)
.and_then(|_| ok(response(StatusCode::OK, "{}"))),
),
"repost" => Box::new(
self.repost(req, api)
.and_then(|_| ok(response(StatusCode::OK, ""))),
),
_ => Box::new(err(ErrorKind::GenericError(
@@ -574,16 +661,17 @@ where
&self,
req: Request<Body>,
mut api: APIForeign<T, C, K>,
) -> Box<dyn Future<Item = Slate, Error = Error> + Send> {
) -> Box<dyn Future<Item = VersionedSlate, Error = Error> + Send> {
Box::new(parse_body(req).and_then(
//TODO: No way to insert a message from the params
move |mut slate| {
move |slate: VersionedSlate| {
let mut slate: Slate = slate.into();
if let Err(e) = api.verify_slate_messages(&slate) {
error!("Error validating participant messages: {}", e);
err(e)
} else {
match api.receive_tx(&mut slate, None, None) {
Ok(_) => ok(slate.clone()),
Ok(_) => ok(get_versioned_slate(&slate.clone())),
Err(e) => {
error!("receive_tx: failed with error: {}", e);
err(e)
@@ -677,20 +765,31 @@ fn create_ok_response(json: &str) -> Response<Body> {
"access-control-allow-headers",
"Content-Type, Authorization",
)
.header(hyper::header::CONTENT_TYPE, "application/json")
.body(json.to_string().into())
.unwrap()
}
/// Build a new hyper Response with the status code and body provided.
///
/// Whenever the status code is `StatusCode::OK` the text parameter should be
/// valid JSON as the content type header will be set to `application/json'
fn response<T: Into<Body>>(status: StatusCode, text: T) -> Response<Body> {
Response::builder()
let mut builder = &mut Response::builder();
builder = builder
.status(status)
.header("access-control-allow-origin", "*")
.header(
"access-control-allow-headers",
"Content-Type, Authorization",
)
.body(text.into())
.unwrap()
);
if status == StatusCode::OK {
builder = builder.header(hyper::header::CONTENT_TYPE, "application/json");
}
builder.body(text.into()).unwrap()
}
fn parse_params(req: &Request<Body>) -> HashMap<String, Vec<String>> {
+43
View File
@@ -338,6 +338,49 @@ pub fn info(
);
}
}
/// Display summary info in a pretty way
pub fn estimate(
amount: u64,
strategies: Vec<(
&str, // strategy
u64, // total amount to be locked
u64, // fee
)>,
dark_background_color_scheme: bool,
) {
println!(
"\nEstimation for sending {}:\n",
amount_to_hr_string(amount, false)
);
let mut table = table!();
table.set_titles(row![
bMG->"Selection strategy",
bMG->"Fee",
bMG->"Will be locked",
]);
for (strategy, total, fee) in strategies {
if dark_background_color_scheme {
table.add_row(row![
bFC->strategy,
FR->amount_to_hr_string(fee, false),
FY->amount_to_hr_string(total, false),
]);
} else {
table.add_row(row![
bFD->strategy,
FR->amount_to_hr_string(fee, false),
FY->amount_to_hr_string(total, false),
]);
}
}
table.printstd();
println!();
}
/// Display list of wallet accounts in a pretty way
pub fn accounts(acct_mappings: Vec<AcctPathMapping>) {
println!("\n____ Wallet Accounts ____\n",);
+2 -1
View File
@@ -25,7 +25,8 @@ extern crate serde_derive;
extern crate log;
use failure;
use grin_api as api;
use grin_core as core;
#[macro_use]
extern crate grin_core as core;
use grin_keychain as keychain;
use grin_store as store;
use grin_util as util;
+68
View File
@@ -671,6 +671,74 @@ where
Ok((slate, lock_fn))
}
/// Estimates the amount to be locked and fee for the transaction without creating one
///
/// # Arguments
/// * `src_acct_name` - The human readable account name from which to draw outputs
/// for the transaction, overriding whatever the active account is as set via the
/// [`set_active_account`](struct.APIOwner.html#method.set_active_account) method.
/// If None, the transaction will use the active account.
/// * `amount` - The amount to send, in nanogrins. (`1 G = 1_000_000_000nG`)
/// * `minimum_confirmations` - The minimum number of confirmations an output
/// should have in order to be included in the transaction.
/// * `max_outputs` - By default, the wallet selects as many inputs as possible in a
/// transaction, to reduce the Output set and the fees. The wallet will attempt to spend
/// include up to `max_outputs` in a transaction, however if this is not enough to cover
/// the whole amount, the wallet will include more outputs. This parameter should be considered
/// a soft limit.
/// * `num_change_outputs` - The target number of change outputs to create in the transaction.
/// The actual number created will be `num_change_outputs` + whatever remainder is needed.
/// * `selection_strategy_is_use_all` - If `true`, attempt to use up as many outputs as
/// possible to create the transaction, up the 'soft limit' of `max_outputs`. This helps
/// to reduce the size of the UTXO set and the amount of data stored in the wallet, and
/// minimizes fees. This will generally result in many inputs and a large change output(s),
/// usually much larger than the amount being sent. If `false`, the transaction will include
/// as many outputs as are needed to meet the amount, (and no more) starting with the smallest
/// value outputs.
///
/// # Returns
/// * a result containing:
/// * (total, fee) - A tuple:
/// * Total amount to be locked.
/// * Transaction fee
pub fn estimate_initiate_tx(
&mut self,
src_acct_name: Option<&str>,
amount: u64,
minimum_confirmations: u64,
max_outputs: usize,
num_change_outputs: usize,
selection_strategy_is_use_all: bool,
) -> Result<
(
u64, // total
u64, // fee
),
Error,
> {
let mut w = self.wallet.lock();
w.open_with_credentials()?;
let parent_key_id = match src_acct_name {
Some(d) => {
let pm = w.get_acct_path(d.to_owned())?;
match pm {
Some(p) => p.path,
None => w.parent_key_id(),
}
}
None => w.parent_key_id(),
};
tx::estimate_send_tx(
&mut *w,
amount,
minimum_confirmations,
max_outputs,
num_change_outputs,
selection_strategy_is_use_all,
&parent_key_id,
)
}
/// Lock outputs associated with a given slate/transaction
pub fn tx_lock_outputs(
&mut self,
+47 -10
View File
@@ -242,6 +242,52 @@ pub fn select_send_tx<T: ?Sized, C, K>(
),
Error,
>
where
T: WalletBackend<C, K>,
C: NodeClient,
K: Keychain,
{
let (coins, _total, amount, fee) = select_coins_and_fee(
wallet,
amount,
current_height,
minimum_confirmations,
max_outputs,
change_outputs,
selection_strategy_is_use_all,
&parent_key_id,
)?;
// build transaction skeleton with inputs and change
let (mut parts, change_amounts_derivations) =
inputs_and_change(&coins, wallet, amount, fee, change_outputs)?;
// This is more proof of concept than anything but here we set lock_height
// on tx being sent (based on current chain height via api).
parts.push(build::with_lock_height(lock_height));
Ok((parts, coins, change_amounts_derivations, fee))
}
/// Select outputs and calculating fee.
pub fn select_coins_and_fee<T: ?Sized, C, K>(
wallet: &mut T,
amount: u64,
current_height: u64,
minimum_confirmations: u64,
max_outputs: usize,
change_outputs: usize,
selection_strategy_is_use_all: bool,
parent_key_id: &Identifier,
) -> Result<
(
Vec<OutputData>,
u64, // total
u64, // amount
u64, // fee
),
Error,
>
where
T: WalletBackend<C, K>,
C: NodeClient,
@@ -325,16 +371,7 @@ where
amount_with_fee = amount + fee;
}
}
// build transaction skeleton with inputs and change
let (mut parts, change_amounts_derivations) =
inputs_and_change(&coins, wallet, amount, fee, change_outputs)?;
// This is more proof of concept than anything but here we set lock_height
// on tx being sent (based on current chain height via api).
parts.push(build::with_lock_height(lock_height));
Ok((parts, coins, change_amounts_derivations, fee))
Ok((coins, total, amount, fee))
}
/// Selects inputs and change for a transaction
+46
View File
@@ -42,6 +42,52 @@ where
Ok(slate)
}
/// Estimates locked amount and fee for the transaction without creating one
pub fn estimate_send_tx<T: ?Sized, C, K>(
wallet: &mut T,
amount: u64,
minimum_confirmations: u64,
max_outputs: usize,
num_change_outputs: usize,
selection_strategy_is_use_all: bool,
parent_key_id: &Identifier,
) -> Result<
(
u64, // total
u64, // fee
),
Error,
>
where
T: WalletBackend<C, K>,
C: NodeClient,
K: Keychain,
{
// Get lock height
let current_height = wallet.w2n_client().get_chain_height()?;
// ensure outputs we're selecting are up to date
updater::refresh_outputs(wallet, parent_key_id, false)?;
// Sender selects outputs into a new slate and save our corresponding keys 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 (_coins, total, _amount, fee) = selection::select_coins_and_fee(
wallet,
amount,
current_height,
minimum_confirmations,
max_outputs,
num_change_outputs,
selection_strategy_is_use_all,
parent_key_id,
)?;
Ok((total, fee))
}
/// Add inputs to the slate (effectively becoming the sender)
pub fn add_inputs_to_slate<T: ?Sized, C, K>(
wallet: &mut T,
+1
View File
@@ -26,6 +26,7 @@ pub mod api;
mod error;
pub mod internal;
pub mod slate;
pub mod slate_versions;
pub mod types;
pub use crate::libwallet::error::{Error, ErrorKind};
+24
View File
@@ -18,6 +18,7 @@
use crate::blake2::blake2b::blake2b;
use crate::keychain::{BlindSum, BlindingFactor, Keychain};
use crate::libwallet::error::{Error, ErrorKind};
use crate::libwallet::slate_versions::v0::SlateV0;
use crate::util::secp;
use crate::util::secp::key::{PublicKey, SecretKey};
use crate::util::secp::Signature;
@@ -33,6 +34,25 @@ use uuid::Uuid;
const CURRENT_SLATE_VERSION: u64 = 1;
/// A wrapper around slates the enables support for versioning
#[derive(Serialize, Deserialize, Clone)]
#[serde(untagged)]
pub enum VersionedSlate {
/// Pre versioning version
V0(SlateV0),
/// Version 1 with versioning and hex serialization - current
V1(Slate),
}
impl From<VersionedSlate> for Slate {
fn from(ver: VersionedSlate) -> Self {
match ver {
VersionedSlate::V0(slate_v0) => Slate::from(slate_v0),
VersionedSlate::V1(slate) => slate,
}
}
}
/// Public data for each participant in the slate
#[derive(Serialize, Deserialize, Debug, Clone)]
@@ -40,14 +60,18 @@ pub struct ParticipantData {
/// Id of participant in the transaction. (For now, 0=sender, 1=rec)
pub id: u64,
/// Public key corresponding to private blinding factor
#[serde(with = "secp_ser::pubkey_serde")]
pub public_blind_excess: PublicKey,
/// Public key corresponding to private nonce
#[serde(with = "secp_ser::pubkey_serde")]
pub public_nonce: PublicKey,
/// Public partial signature
#[serde(with = "secp_ser::option_sig_serde")]
pub part_sig: Option<Signature>,
/// A message for other participants
pub message: Option<String>,
/// Signature, created with private key corresponding to 'public_blind_excess'
#[serde(with = "secp_ser::option_sig_serde")]
pub message_sig: Option<Signature>,
}
@@ -0,0 +1,18 @@
// Copyright 2019 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.
//! This module contains old slate versions and conversions to the newest slate version
//! Used for serialization and deserialization of slates in a backwards compatible way.
#[allow(missing_docs)]
pub mod v0;
+370
View File
@@ -0,0 +1,370 @@
// 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.
// Contains V0 of the slate
use crate::core::core::transaction::{
Input, KernelFeatures, Output, OutputFeatures, Transaction, TransactionBody, TxKernel,
};
use crate::keychain::BlindingFactor;
use crate::libwallet::slate::{ParticipantData, Slate};
use crate::util::secp;
use crate::util::secp::key::PublicKey;
use crate::util::secp::pedersen::{Commitment, RangeProof};
use crate::util::secp::Signature;
use uuid::Uuid;
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct SlateV0 {
/// The number of participants intended to take part in this transaction
pub num_participants: usize,
/// Unique transaction ID, selected by sender
pub id: Uuid,
/// The core transaction data:
/// inputs, outputs, kernels, kernel offset
pub tx: TransactionV0,
/// base amount (excluding fee)
pub amount: u64,
/// fee amount
pub fee: u64,
/// Block height for the transaction
pub height: u64,
/// Lock height
pub lock_height: u64,
/// Participant data, each participant in the transaction will
/// insert their public data here. For now, 0 is sender and 1
/// is receiver, though this will change for multi-party
pub participant_data: Vec<ParticipantDataV0>,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct ParticipantDataV0 {
/// Id of participant in the transaction. (For now, 0=sender, 1=rec)
pub id: u64,
/// Public key corresponding to private blinding factor
pub public_blind_excess: PublicKey,
/// Public key corresponding to private nonce
pub public_nonce: PublicKey,
/// Public partial signature
pub part_sig: Option<Signature>,
/// A message for other participants
pub message: Option<String>,
/// Signature, created with private key corresponding to 'public_blind_excess'
pub message_sig: Option<Signature>,
}
/// A transaction
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct TransactionV0 {
/// The kernel "offset" k2
/// excess is k1G after splitting the key k = k1 + k2
pub offset: BlindingFactor,
/// The transaction body - inputs/outputs/kernels
pub body: TransactionBodyV0,
}
/// TransactionBody is a common abstraction for transaction and block
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct TransactionBodyV0 {
/// List of inputs spent by the transaction.
pub inputs: Vec<InputV0>,
/// List of outputs the transaction produces.
pub outputs: Vec<OutputV0>,
/// List of kernels that make up this transaction (usually a single kernel).
pub kernels: Vec<TxKernelV0>,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct InputV0 {
/// The features of the output being spent.
/// We will check maturity for coinbase output.
pub features: OutputFeatures,
/// The commit referencing the output being spent.
pub commit: Commitment,
}
#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
pub struct OutputV0 {
/// Options for an output's structure or use
pub features: OutputFeatures,
/// The homomorphic commitment representing the output amount
pub commit: Commitment,
/// A proof that the commitment is in the right range
pub proof: RangeProof,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct TxKernelV0 {
/// Options for a kernel's structure or use
pub features: KernelFeatures,
/// Fee originally included in the transaction this proof is for.
pub fee: u64,
/// This kernel is not valid earlier than lock_height blocks
/// The max lock_height of all *inputs* to this transaction
pub lock_height: u64,
/// Remainder of the sum of all transaction commitments. If the transaction
/// is well formed, amounts components should sum to zero and the excess
/// is hence a valid public key.
pub excess: Commitment,
/// The signature proving the excess is a valid public key, which signs
/// the transaction fee.
pub excess_sig: secp::Signature,
}
impl From<SlateV0> for Slate {
fn from(slate: SlateV0) -> Slate {
let SlateV0 {
num_participants,
id,
tx,
amount,
fee,
height,
lock_height,
participant_data,
} = slate;
let tx = Transaction::from(tx);
let participant_data = map_vec!(participant_data, |data| ParticipantData::from(data));
let version = 0;
Slate {
num_participants,
id,
tx,
amount,
fee,
height,
lock_height,
participant_data,
version,
}
}
}
impl From<&ParticipantDataV0> for ParticipantData {
fn from(data: &ParticipantDataV0) -> ParticipantData {
let ParticipantDataV0 {
id,
public_blind_excess,
public_nonce,
part_sig,
message,
message_sig,
} = data;
let id = *id;
let public_blind_excess = *public_blind_excess;
let public_nonce = *public_nonce;
let part_sig = *part_sig;
let message: Option<String> = message.as_ref().map(|t| String::from(&**t));
let message_sig = *message_sig;
ParticipantData {
id,
public_blind_excess,
public_nonce,
part_sig,
message,
message_sig,
}
}
}
impl From<TransactionV0> for Transaction {
fn from(tx: TransactionV0) -> Transaction {
let TransactionV0 { offset, body } = tx;
let body = TransactionBody::from(&body);
let transaction = Transaction::new(body.inputs, body.outputs, body.kernels);
transaction.with_offset(offset)
}
}
impl From<&TransactionBodyV0> for TransactionBody {
fn from(body: &TransactionBodyV0) -> Self {
let TransactionBodyV0 {
inputs,
outputs,
kernels,
} = body;
let inputs = map_vec!(inputs, |inp| Input::from(inp));
let outputs = map_vec!(outputs, |out| Output::from(out));
let kernels = map_vec!(kernels, |kern| TxKernel::from(kern));
TransactionBody {
inputs,
outputs,
kernels,
}
}
}
impl From<&InputV0> for Input {
fn from(input: &InputV0) -> Input {
let InputV0 { features, commit } = *input;
Input { features, commit }
}
}
impl From<&OutputV0> for Output {
fn from(output: &OutputV0) -> Output {
let OutputV0 {
features,
commit,
proof,
} = *output;
Output {
features,
commit,
proof,
}
}
}
impl From<&TxKernelV0> for TxKernel {
fn from(kernel: &TxKernelV0) -> TxKernel {
let TxKernelV0 {
features,
fee,
lock_height,
excess,
excess_sig,
} = *kernel;
TxKernel {
features,
fee,
lock_height,
excess,
excess_sig,
}
}
}
impl From<Slate> for SlateV0 {
fn from(slate: Slate) -> SlateV0 {
let Slate {
num_participants,
id,
tx,
amount,
fee,
height,
lock_height,
participant_data,
version: _,
} = slate;
let tx = TransactionV0::from(tx);
let participant_data = map_vec!(participant_data, |data| ParticipantDataV0::from(data));
SlateV0 {
num_participants,
id,
tx,
amount,
fee,
height,
lock_height,
participant_data,
}
}
}
impl From<&ParticipantData> for ParticipantDataV0 {
fn from(data: &ParticipantData) -> ParticipantDataV0 {
let ParticipantData {
id,
public_blind_excess,
public_nonce,
part_sig,
message,
message_sig,
} = data;
let id = *id;
let public_blind_excess = *public_blind_excess;
let public_nonce = *public_nonce;
let part_sig = *part_sig;
let message: Option<String> = message.as_ref().map(|t| String::from(&**t));
let message_sig = *message_sig;
ParticipantDataV0 {
id,
public_blind_excess,
public_nonce,
part_sig,
message,
message_sig,
}
}
}
impl From<Transaction> for TransactionV0 {
fn from(tx: Transaction) -> TransactionV0 {
let offset = tx.offset;
let body: TransactionBody = tx.into();
let body = TransactionBodyV0::from(&body);
TransactionV0 { offset, body }
}
}
impl From<&TransactionBody> for TransactionBodyV0 {
fn from(body: &TransactionBody) -> Self {
let TransactionBody {
inputs,
outputs,
kernels,
} = body;
let inputs = map_vec!(inputs, |inp| InputV0::from(inp));
let outputs = map_vec!(outputs, |out| OutputV0::from(out));
let kernels = map_vec!(kernels, |kern| TxKernelV0::from(kern));
TransactionBodyV0 {
inputs,
outputs,
kernels,
}
}
}
impl From<&Input> for InputV0 {
fn from(input: &Input) -> Self {
let Input { features, commit } = *input;
InputV0 { features, commit }
}
}
impl From<&Output> for OutputV0 {
fn from(output: &Output) -> Self {
let Output {
features,
commit,
proof,
} = *output;
OutputV0 {
features,
commit,
proof,
}
}
}
impl From<&TxKernel> for TxKernelV0 {
fn from(kernel: &TxKernel) -> Self {
let TxKernel {
features,
fee,
lock_height,
excess,
excess_sig,
} = *kernel;
TxKernelV0 {
features,
fee,
lock_height,
excess,
excess_sig,
}
}
}
+4
View File
@@ -137,6 +137,10 @@ where
self.wallets.insert(addr.to_owned(), (tx, wallet));
}
pub fn stop(&mut self) {
self.running.store(false, Ordering::Relaxed);
}
/// Run the incoming message queue and respond more or less
/// synchronously
pub fn run(&mut self) -> Result<(), libwallet::Error> {
+13
View File
@@ -26,6 +26,7 @@ use grin_keychain as keychain;
use grin_util as util;
use grin_wallet as wallet;
use std::fs;
use std::sync::atomic::Ordering;
use std::thread;
use std::time::Duration;
@@ -44,6 +45,7 @@ fn restore_wallet(base_dir: &str, wallet_dir: &str) -> Result<(), libwallet::Err
let dest_dir = format!("{}/{}_restore", base_dir, wallet_dir);
fs::create_dir_all(dest_dir.clone())?;
let dest_seed = format!("{}/wallet.seed", dest_dir);
println!("Source: {}, Dest: {}", source_seed, dest_seed);
fs::copy(source_seed, dest_seed)?;
let mut wallet_proxy: WalletProxy<LocalWalletClient, ExtKeychain> = WalletProxy::new(base_dir);
@@ -54,6 +56,7 @@ fn restore_wallet(base_dir: &str, wallet_dir: &str) -> Result<(), libwallet::Err
wallet_proxy.add_wallet(wallet_dir, client.get_send_instance(), wallet.clone());
// Set the wallet proxy listener running
let wp_running = wallet_proxy.running.clone();
thread::spawn(move || {
if let Err(e) = wallet_proxy.run() {
error!("Wallet Proxy error: {}", e);
@@ -67,6 +70,9 @@ fn restore_wallet(base_dir: &str, wallet_dir: &str) -> Result<(), libwallet::Err
Ok(())
})?;
wp_running.store(false, Ordering::Relaxed);
//thread::sleep(Duration::from_millis(1000));
Ok(())
}
@@ -108,6 +114,7 @@ fn compare_wallet_restore(
}
// Set the wallet proxy listener running
let wp_running = wallet_proxy.running.clone();
thread::spawn(move || {
if let Err(e) = wallet_proxy.run() {
error!("Wallet Proxy error: {}", e);
@@ -164,6 +171,9 @@ fn compare_wallet_restore(
dest_accts.as_ref().unwrap().len()
);
wp_running.store(false, Ordering::Relaxed);
//thread::sleep(Duration::from_millis(1000));
Ok(())
}
@@ -208,6 +218,7 @@ fn setup_restore(test_dir: &str) -> Result<(), libwallet::Error> {
wallet_proxy.add_wallet("wallet3", client3.get_send_instance(), wallet3.clone());
// Set the wallet proxy listener running
let wp_running = wallet_proxy.running.clone();
thread::spawn(move || {
if let Err(e) = wallet_proxy.run() {
error!("Wallet Proxy error: {}", e);
@@ -327,6 +338,8 @@ fn setup_restore(test_dir: &str) -> Result<(), libwallet::Error> {
Ok(())
})?;
wp_running.store(false, Ordering::Relaxed);
Ok(())
}
+27
View File
@@ -226,6 +226,33 @@ fn basic_transaction_api(test_dir: &str) -> Result<(), libwallet::Error> {
Ok(())
})?;
// Estimate fee and locked amount for a transaction
wallet::controller::owner_single_use(wallet1.clone(), |sender_api| {
let (total, fee) = sender_api.estimate_initiate_tx(
None,
amount * 2, // amount
2, // minimum confirmations
500, // max outputs
1, // num change outputs
true, // select all outputs
)?;
assert_eq!(total, 600_000_000_000);
assert_eq!(fee, 4_000_000);
let (total, fee) = sender_api.estimate_initiate_tx(
None,
amount * 2, // amount
2, // minimum confirmations
500, // max outputs
1, // num change outputs
false, // select the smallest amount of outputs
)?;
assert_eq!(total, 180_000_000_000);
assert_eq!(fee, 6_000_000);
Ok(())
})?;
// Send another transaction, but don't post to chain immediately and use
// the stored transaction instead
wallet::controller::owner_single_use(wallet1.clone(), |sender_api| {