mirror of
https://github.com/mimblewimble/grin.git
synced 2025-01-21 03:21:08 +03:00
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:
parent
e383cb594f
commit
f0bdb2c8fa
6 changed files with 223 additions and 50 deletions
|
@ -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) => {
|
||||
|
|
|
@ -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 \
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in a new issue