File-based transaction building (#1317)

* First implementation of file-based transaction send, receive and finalize in libwallet
* Switch from TOML to JSON (needless complication). Plug in send, receive and finalize wallet commands.
* Pretty JSON for response is too large (all whitespace)
This commit is contained in:
Ignotus Peverell 2018-08-07 11:17:33 -07:00 committed by GitHub
parent e383cb594f
commit f0bdb2c8fa
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 223 additions and 50 deletions

View file

@ -160,48 +160,85 @@ pub fn wallet_command(wallet_args: &ArgMatches, global_config: GlobalConfig) {
let dest = send_args
.value_of("dest")
.expect("Destination wallet address required");
let mut fluff = false;
if send_args.is_present("fluff") {
fluff = true;
}
let fluff = send_args.is_present("fluff");
let max_outputs = 500;
let result = api.issue_send_tx(
amount,
minimum_confirmations,
dest,
max_outputs,
selection_strategy == "all",
);
let slate = match result {
Ok(s) => {
info!(
LOGGER,
"Tx created: {} grin to {} (strategy '{}')",
core::amount_to_hr_string(amount),
dest,
selection_strategy,
);
s
if dest.starts_with("http") {
let result = api.issue_send_tx(
amount,
minimum_confirmations,
dest,
max_outputs,
selection_strategy == "all",
);
let slate = match result {
Ok(s) => {
info!(
LOGGER,
"Tx created: {} grin to {} (strategy '{}')",
core::amount_to_hr_string(amount),
dest,
selection_strategy,
);
s
}
Err(e) => {
error!(LOGGER, "Tx not created: {:?}", e);
match e.kind() {
// user errors, don't backtrace
libwallet::ErrorKind::NotEnoughFunds { .. } => {}
libwallet::ErrorKind::FeeDispute { .. } => {}
libwallet::ErrorKind::FeeExceedsAmount { .. } => {}
_ => {
// otherwise give full dump
error!(LOGGER, "Backtrace: {}", e.backtrace().unwrap());
}
};
panic!();
}
};
let result = api.post_tx(&slate, fluff);
match result {
Ok(_) => {
info!(LOGGER, "Tx sent",);
Ok(())
}
Err(e) => {
error!(LOGGER, "Tx not sent: {:?}", e);
Err(e)
}
}
Err(e) => {
error!(LOGGER, "Tx not created: {:?}", e);
match e.kind() {
// user errors, don't backtrace
libwallet::ErrorKind::NotEnoughFunds { .. } => {}
libwallet::ErrorKind::FeeDispute { .. } => {}
libwallet::ErrorKind::FeeExceedsAmount { .. } => {}
_ => {
// otherwise give full dump
error!(LOGGER, "Backtrace: {}", e.backtrace().unwrap());
}
};
panic!();
}
};
} else {
api.file_send_tx(
amount,
minimum_confirmations,
dest,
max_outputs,
selection_strategy == "all",
).expect("Send failed");
Ok(())
}
}
("receive", Some(send_args)) => {
let tx_file = send_args
.value_of("input")
.expect("Transaction file required");
api.file_receive_tx(tx_file).expect("Receive failed");
Ok(())
}
("finalize", Some(send_args)) => {
let fluff = send_args.is_present("fluff");
let tx_file = send_args
.value_of("input")
.expect("Receiver's transaction file required");
let priv_file = send_args
.value_of("private")
.expect("Private transaction file required");
let slate = api.file_finalize_tx(priv_file, tx_file).expect("Finalize failed");
let result = api.post_tx(&slate, fluff);
match result {
Ok(_) => {
info!(LOGGER, "Tx sent",);
info!(LOGGER, "Tx sent");
Ok(())
}
Err(e) => {

View file

@ -179,14 +179,6 @@ fn main() {
.subcommand(SubCommand::with_name("owner_api")
.about("Runs the wallet's local web API."))
.subcommand(SubCommand::with_name("receive")
.about("Processes a JSON transaction file.")
.arg(Arg::with_name("input")
.help("Partial transaction to process, expects a JSON file.")
.short("i")
.long("input")
.takes_value(true)))
.subcommand(SubCommand::with_name("send")
.about("Builds a transaction to send coins and sends it to the specified \
listener directly.")
@ -216,6 +208,31 @@ fn main() {
.short("f")
.long("fluff")))
.subcommand(SubCommand::with_name("receive")
.about("Processes a transaction file to accept a transfer from a sender.")
.arg(Arg::with_name("input")
.help("Partial transaction to process, expects the sender's transaction file.")
.short("i")
.long("input")
.takes_value(true)))
.subcommand(SubCommand::with_name("finalize")
.about("Processes a receiver's transaction file to finalize a transfer.")
.arg(Arg::with_name("input")
.help("Partial transaction to process, expects the receiver's transaction file.")
.short("i")
.long("input")
.takes_value(true))
.arg(Arg::with_name("private")
.help("Private transaction file previously generated by send.")
.short("p")
.long("private")
.takes_value(true))
.arg(Arg::with_name("fluff")
.help("Fluff the transaction (ignore Dandelion relay protocol)")
.short("f")
.long("fluff")))
.subcommand(SubCommand::with_name("burn")
.about("** TESTING ONLY ** Burns the provided amount to a known \
key. Similar to send but burns an output to allow single-party \

View file

@ -14,6 +14,7 @@
//! Functions for building partial transactions to be passed
//! around during an interactive wallet exchange
use rand::thread_rng;
use uuid::Uuid;

View file

@ -17,13 +17,17 @@
//! vs. functions to interact with someone else)
//! Still experimental, not sure this is the best way to do this
use std::fs::File;
use std::io::{Read, Write};
use std::marker::PhantomData;
use std::sync::{Arc, Mutex};
use serde_json as json;
use core::ser;
use keychain::Keychain;
use libtx::slate::Slate;
use libwallet::internal::{tx, updater};
use libwallet::internal::{tx, updater, selection, sigcontext};
use libwallet::types::{
BlockFees, CbData, OutputData, TxLogEntry, TxWrapper, WalletBackend, WalletClient, WalletInfo,
};
@ -173,6 +177,109 @@ where
Ok(slate_out)
}
/// Write a transaction to send to file so a user can transmit it to the
/// receiver in whichever way they see fit (aka carrier pigeon mode).
pub fn file_send_tx(
&mut self,
amount: u64,
minimum_confirmations: u64,
dest: &str,
max_outputs: usize,
selection_strategy_is_use_all: bool,
) -> Result<(), Error> {
let mut w = self.wallet.lock().unwrap();
w.open_with_credentials()?;
let (slate, context, lock_fn) = tx::create_send_tx(
&mut **w,
amount,
minimum_confirmations,
max_outputs,
selection_strategy_is_use_all,
)?;
let mut pub_tx = File::create(dest)?;
pub_tx.write_all(json::to_string(&slate).unwrap().as_bytes())?;
pub_tx.sync_all()?;
let mut priv_tx = File::create(dest.to_owned() + ".private")?;
priv_tx.write_all(json::to_string(&context).unwrap().as_bytes())?;
priv_tx.sync_all()?;
// lock our inputs
lock_fn(&mut **w)?;
w.close()?;
Ok(())
}
/// A sender provided a transaction file with appropriate public keys and
/// metadata. Complete the receivers' end of it to generate another file
/// to send back.
pub fn file_receive_tx(
&mut self,
source: &str,
) -> Result<(), Error> {
let mut pub_tx_f = File::open(source)?;
let mut content = String::new();
pub_tx_f.read_to_string(&mut content)?;
let mut slate: Slate = json::from_str(&content).map_err(|_| ErrorKind::Format)?;
let mut wallet = self.wallet.lock().unwrap();
wallet.open_with_credentials()?;
// create an output using the amount in the slate
let (_, mut context, receiver_create_fn) =
selection::build_recipient_output_with_slate(&mut **wallet, &mut slate)?;
// fill public keys
let _ = slate.fill_round_1(
wallet.keychain(),
&mut context.sec_key,
&context.sec_nonce,
1,
)?;
// perform partial sig
let _ = slate.fill_round_2(wallet.keychain(), &context.sec_key, &context.sec_nonce, 1)?;
// save to file
let mut pub_tx = File::create(source.to_owned() + ".response")?;
pub_tx.write_all(json::to_string(&slate).unwrap().as_bytes())?;
// Save output in wallet
let _ = receiver_create_fn(&mut wallet);
Ok(())
}
/// Sender finalization of the transaction. Takes the file returned by the
/// sender as well as the private file generate on the first send step.
/// Builds the complete transaction and sends it to a grin node for
/// propagation.
pub fn file_finalize_tx(
&mut self,
private_tx_file: &str,
receiver_file: &str,
) -> Result<Slate, Error> {
let mut pub_tx_f = File::open(receiver_file)?;
let mut content = String::new();
pub_tx_f.read_to_string(&mut content)?;
let mut slate: Slate = json::from_str(&content).map_err(|_| ErrorKind::Format)?;
let mut priv_tx_f = File::open(private_tx_file)?;
let mut content = String::new();
priv_tx_f.read_to_string(&mut content)?;
let context: sigcontext::Context = json::from_str(&content).map_err(|_| ErrorKind::Format)?;
let mut w = self.wallet.lock().unwrap();
w.open_with_credentials()?;
tx::complete_tx(&mut **w, &mut slate, &context)?;
w.close()?;
Ok(slate)
}
/// Roll back a transaction and all associated outputs with a given
/// transaction id This means delete all change outputs, (or recipient
/// output if you're recipient), and unlock all locked outputs associated

View file

@ -13,12 +13,15 @@
// limitations under the License.
//! Error types for libwallet
use keychain;
use libtx;
use std::fmt::{self, Display};
use std::io;
use failure::{Backtrace, Context, Fail};
use core::core::transaction;
use failure::{Backtrace, Context, Fail};
use keychain;
use libtx;
/// Error definition
#[derive(Debug, Fail)]
@ -184,6 +187,14 @@ impl From<Context<ErrorKind>> for Error {
}
}
impl From<io::Error> for Error {
fn from(_error: io::Error) -> Error {
Error {
inner: Context::new(ErrorKind::IO),
}
}
}
impl From<keychain::Error> for Error {
fn from(error: keychain::Error) -> Error {
Error {

View file

@ -17,7 +17,7 @@ use libtx::aggsig;
use util::secp::key::{PublicKey, SecretKey};
use util::secp::{self, Secp256k1};
#[derive(Clone, Debug)]
#[derive(Serialize, Deserialize, Clone, Debug)]
/// Holds the context for a single aggsig transaction
pub struct Context {
/// Secret key (of which public is shared)