Add file based transaction in owner API (#1484)

* Add file based transaction in owner API
* Add finalize tx in owner API
* Code cleanup and placed file_receive in correct API
* Output an explicit error when http dest seems incorrect
* Add doc on send method
* Add cancel tx endpoint in owner API
* Add dump stored tx endpoint in owner API
*  Add missing parameters
This commit is contained in:
Quentin Le Sceller 2018-09-11 02:18:10 +00:00 committed by Ignotus Peverell
parent 4030b3d2f1
commit dd1cef2148
7 changed files with 270 additions and 118 deletions

View file

@ -144,6 +144,12 @@ Outputs in your wallet will appear as unconfirmed or locked until the transactio
Other flags here are: Other flags here are:
* `-m` 'Method', which can be 'http' or 'file'. In the first case, the transaction will be sent to the IP address which follows the `-d` flag. In the second case, Grin wallet will generate a partial transaction file under the file name specified in the `-d` flag. This file needs to be signed by the recipient using the `grin wallet receive -i filename` command and finalize by the sender using the `grin wallet finalize -i filename.response` command. To create a partial transaction file, use:
```
[host]$ grin wallet send -d "transaction" -m file 60.00
```
* `-s` 'Selection strategy', which can be 'all' or 'smallest'. Since it's advantageous for outputs to be removed from the Grin chain, * `-s` 'Selection strategy', which can be 'all' or 'smallest'. Since it's advantageous for outputs to be removed from the Grin chain,
the default strategy for selecting inputs in Step 1 above is to use as many outputs as possible to consolidate your balance into a the default strategy for selecting inputs in Step 1 above is to use as many outputs as possible to consolidate your balance into a
couple of outputs. This also drastically reduces your wallet size, so everyone wins. The downside is that the entire contents of couple of outputs. This also drastically reduces your wallet size, so everyone wins. The downside is that the entire contents of

View file

@ -12,6 +12,9 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
use serde_json as json;
use std::fs::File;
use std::io::Read;
use std::path::PathBuf; use std::path::PathBuf;
/// Wallet commands processing /// Wallet commands processing
use std::process::exit; use std::process::exit;
@ -166,7 +169,7 @@ pub fn wallet_command(wallet_args: &ArgMatches, config: GlobalWalletConfig) {
wallet_config.clone(), wallet_config.clone(),
passphrase, passphrase,
))); )));
let res = controller::owner_single_use(wallet, |api| { let res = controller::owner_single_use(wallet.clone(), |api| {
match wallet_args.subcommand() { match wallet_args.subcommand() {
("send", Some(send_args)) => { ("send", Some(send_args)) => {
let amount = send_args let amount = send_args
@ -182,6 +185,9 @@ pub fn wallet_command(wallet_args: &ArgMatches, config: GlobalWalletConfig) {
let selection_strategy = send_args let selection_strategy = send_args
.value_of("selection_strategy") .value_of("selection_strategy")
.expect("Selection strategy required"); .expect("Selection strategy required");
let method = send_args
.value_of("method")
.expect("Payment method required");
let dest = send_args let dest = send_args
.value_of("dest") .value_of("dest")
.expect("Destination wallet address required"); .expect("Destination wallet address required");
@ -192,54 +198,63 @@ pub fn wallet_command(wallet_args: &ArgMatches, config: GlobalWalletConfig) {
.expect("Failed to parse number of change outputs."); .expect("Failed to parse number of change outputs.");
let fluff = send_args.is_present("fluff"); let fluff = send_args.is_present("fluff");
let max_outputs = 500; let max_outputs = 500;
if dest.starts_with("http") { if method == "http" {
let result = api.issue_send_tx( if dest.starts_with("http://") {
amount, let result = api.issue_send_tx(
minimum_confirmations, amount,
dest, minimum_confirmations,
max_outputs, dest,
change_outputs, max_outputs,
selection_strategy == "all", change_outputs,
); selection_strategy == "all",
let slate = match result { );
Ok(s) => { let slate = match result {
info!( Ok(s) => {
LOGGER, info!(
"Tx created: {} grin to {} (strategy '{}')", LOGGER,
core::amount_to_hr_string(amount, false), "Tx created: {} grin to {} (strategy '{}')",
dest, core::amount_to_hr_string(amount, false),
selection_strategy, dest,
); selection_strategy,
s );
} s
Err(e) => { }
error!(LOGGER, "Tx not created: {:?}", e); Err(e) => {
match e.kind() { error!(LOGGER, "Tx not created: {:?}", e);
// user errors, don't backtrace match e.kind() {
libwallet::ErrorKind::NotEnoughFunds { .. } => {} // user errors, don't backtrace
libwallet::ErrorKind::FeeDispute { .. } => {} libwallet::ErrorKind::NotEnoughFunds { .. } => {}
libwallet::ErrorKind::FeeExceedsAmount { .. } => {} libwallet::ErrorKind::FeeDispute { .. } => {}
_ => { libwallet::ErrorKind::FeeExceedsAmount { .. } => {}
// otherwise give full dump _ => {
error!(LOGGER, "Backtrace: {}", e.backtrace().unwrap()); // otherwise give full dump
} error!(LOGGER, "Backtrace: {}", e.backtrace().unwrap());
}; }
panic!(); };
} panic!();
}; }
let result = api.post_tx(&slate, fluff); };
match result { let result = api.post_tx(&slate, fluff);
Ok(_) => { match result {
info!(LOGGER, "Tx sent",); Ok(_) => {
Ok(()) info!(LOGGER, "Tx sent",);
} Ok(())
Err(e) => { }
error!(LOGGER, "Tx not sent: {:?}", e); Err(e) => {
Err(e) error!(LOGGER, "Tx not sent: {:?}", e);
Err(e)
}
} }
} else {
error!(
LOGGER,
"HTTP Destination should start with http://: {}", dest
);
panic!();
} }
} else { } else if method == "file" {
api.file_send_tx( api.send_tx(
true,
amount, amount,
minimum_confirmations, minimum_confirmations,
dest, dest,
@ -248,21 +263,36 @@ pub fn wallet_command(wallet_args: &ArgMatches, config: GlobalWalletConfig) {
selection_strategy == "all", selection_strategy == "all",
).expect("Send failed"); ).expect("Send failed");
Ok(()) Ok(())
} else {
error!(LOGGER, "unsupported payment method: {}", method);
panic!();
} }
} }
("receive", Some(send_args)) => { ("receive", Some(send_args)) => {
let tx_file = send_args let mut receive_result: Result<(), grin_wallet::libwallet::Error> = Ok(());
.value_of("input") let res = controller::foreign_single_use(wallet, |api| {
.expect("Transaction file required"); let tx_file = send_args
api.file_receive_tx(tx_file).expect("Receive failed"); .value_of("input")
Ok(()) .expect("Transaction file required");
receive_result = api.file_receive_tx(tx_file);
Ok(())
});
if res.is_err() {
exit(1);
}
receive_result
} }
("finalize", Some(send_args)) => { ("finalize", Some(send_args)) => {
let fluff = send_args.is_present("fluff"); let fluff = send_args.is_present("fluff");
let tx_file = send_args let tx_file = send_args
.value_of("input") .value_of("input")
.expect("Receiver's transaction file required"); .expect("Receiver's transaction file required");
let slate = api.file_finalize_tx(tx_file).expect("Finalize failed"); let mut pub_tx_f = File::open(tx_file)?;
let mut content = String::new();
pub_tx_f.read_to_string(&mut content)?;
let mut slate: grin_wallet::libtx::slate::Slate = json::from_str(&content)
.map_err(|_| grin_wallet::libwallet::ErrorKind::Format)?;
let _ = api.finalize_tx(&mut slate).expect("Finalize failed");
let result = api.post_tx(&slate, fluff); let result = api.post_tx(&slate, fluff);
match result { match result {
@ -377,7 +407,7 @@ pub fn wallet_command(wallet_args: &ArgMatches, config: GlobalWalletConfig) {
} }
} }
Some(f) => { Some(f) => {
let result = api.dump_stored_tx(tx_id, f); let result = api.dump_stored_tx(tx_id, true, f);
match result { match result {
Ok(_) => { Ok(_) => {
warn!(LOGGER, "Dumped transaction data for tx {} to {}", tx_id, f); warn!(LOGGER, "Dumped transaction data for tx {} to {}", tx_id, f);

View file

@ -213,6 +213,13 @@ fn main() {
.long("change_outputs") .long("change_outputs")
.default_value("1") .default_value("1")
.takes_value(true)) .takes_value(true))
.arg(Arg::with_name("method")
.help("Method for sending this transaction.")
.short("m")
.long("method")
.possible_values(&["http", "file"])
.default_value("http")
.takes_value(true))
.arg(Arg::with_name("dest") .arg(Arg::with_name("dest")
.help("Send the transaction to the provided server (start with http://) or save as file.") .help("Send the transaction to the provided server (start with http://) or save as file.")
.short("d") .short("d")

View file

@ -185,15 +185,16 @@ where
/// Write a transaction to send to file so a user can transmit it to the /// 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). /// receiver in whichever way they see fit (aka carrier pigeon mode).
pub fn file_send_tx( pub fn send_tx(
&mut self, &mut self,
write_to_disk: bool,
amount: u64, amount: u64,
minimum_confirmations: u64, minimum_confirmations: u64,
dest: &str, dest: &str,
max_outputs: usize, max_outputs: usize,
num_change_outputs: usize, num_change_outputs: usize,
selection_strategy_is_use_all: bool, selection_strategy_is_use_all: bool,
) -> Result<(), Error> { ) -> Result<Slate, Error> {
let mut w = self.wallet.lock().unwrap(); let mut w = self.wallet.lock().unwrap();
w.open_with_credentials()?; w.open_with_credentials()?;
@ -205,10 +206,11 @@ where
num_change_outputs, num_change_outputs,
selection_strategy_is_use_all, selection_strategy_is_use_all,
)?; )?;
if write_to_disk {
let mut pub_tx = File::create(dest)?; let mut pub_tx = File::create(dest)?;
pub_tx.write_all(json::to_string(&slate).unwrap().as_bytes())?; pub_tx.write_all(json::to_string(&slate).unwrap().as_bytes())?;
pub_tx.sync_all()?; pub_tx.sync_all()?;
}
{ {
let mut batch = w.batch()?; let mut batch = w.batch()?;
@ -221,60 +223,19 @@ where
// lock our inputs // lock our inputs
lock_fn(&mut **w, &tx_hex)?; lock_fn(&mut **w, &tx_hex)?;
w.close()?; w.close()?;
Ok(()) Ok(slate)
}
/// 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 finalization of the transaction. Takes the file returned by the
/// sender as well as the private file generate on the first send step. /// 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 /// Builds the complete transaction and sends it to a grin node for
/// propagation. /// propagation.
pub fn file_finalize_tx(&mut self, receiver_file: &str) -> Result<Slate, Error> { pub fn finalize_tx(&mut self, slate: &mut Slate) -> Result<(), 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 w = self.wallet.lock().unwrap(); let mut w = self.wallet.lock().unwrap();
w.open_with_credentials()?; w.open_with_credentials()?;
let context = w.get_private_context(slate.id.as_bytes())?; let context = w.get_private_context(slate.id.as_bytes())?;
tx::complete_tx(&mut **w, &mut slate, &context)?; tx::complete_tx(&mut **w, slate, &context)?;
{ {
let mut batch = w.batch()?; let mut batch = w.batch()?;
batch.delete_private_context(slate.id.as_bytes())?; batch.delete_private_context(slate.id.as_bytes())?;
@ -282,7 +243,7 @@ where
} }
w.close()?; w.close()?;
Ok(slate) Ok(())
} }
/// Roll back a transaction and all associated outputs with a given /// Roll back a transaction and all associated outputs with a given
@ -342,7 +303,12 @@ where
} }
/// Writes stored transaction data to a given file /// Writes stored transaction data to a given file
pub fn dump_stored_tx(&self, tx_id: u32, dest: &str) -> Result<(), Error> { pub fn dump_stored_tx(
&self,
tx_id: u32,
write_to_disk: bool,
dest: &str,
) -> Result<Transaction, Error> {
let (confirmed, tx_hex) = { let (confirmed, tx_hex) = {
let mut w = self.wallet.lock().unwrap(); let mut w = self.wallet.lock().unwrap();
w.open_with_credentials()?; w.open_with_credentials()?;
@ -365,10 +331,12 @@ where
} }
let tx_bin = util::from_hex(tx_hex.unwrap()).unwrap(); let tx_bin = util::from_hex(tx_hex.unwrap()).unwrap();
let tx = ser::deserialize::<Transaction>(&mut &tx_bin[..])?; let tx = ser::deserialize::<Transaction>(&mut &tx_bin[..])?;
let mut tx_file = File::create(dest)?; if write_to_disk {
tx_file.write_all(json::to_string(&tx).unwrap().as_bytes())?; let mut tx_file = File::create(dest)?;
tx_file.sync_all()?; tx_file.write_all(json::to_string(&tx).unwrap().as_bytes())?;
Ok(()) tx_file.sync_all()?;
}
Ok(tx)
} }
/// (Re)Posts a transaction that's already been stored to the chain /// (Re)Posts a transaction that's already been stored to the chain
@ -498,6 +466,42 @@ where
res res
} }
/// 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(())
}
/// Receive a transaction from a sender /// Receive a transaction from a sender
pub fn receive_tx(&mut self, slate: &mut Slate) -> Result<(), Error> { pub fn receive_tx(&mut self, slate: &mut Slate) -> Result<(), Error> {
let mut w = self.wallet.lock().unwrap(); let mut w = self.wallet.lock().unwrap();

View file

@ -28,6 +28,7 @@ use hyper::{Body, Request, Response, StatusCode};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json; use serde_json;
use core::core::Transaction;
use keychain::Keychain; use keychain::Keychain;
use libtx::slate::Slate; use libtx::slate::Slate;
use libwallet::api::{APIForeign, APIOwner}; use libwallet::api::{APIForeign, APIOwner};
@ -229,6 +230,35 @@ where
api.retrieve_txs(update_from_node, id) api.retrieve_txs(update_from_node, id)
} }
fn dump_stored_tx(
&self,
req: &Request<Body>,
api: APIOwner<T, C, K>,
) -> Result<Transaction, Error> {
let params = parse_params(req);
if let Some(id_string) = params.get("id") {
match id_string[0].parse() {
Ok(id) => match api.dump_stored_tx(id, false, "") {
Ok(tx) => Ok(tx),
Err(e) => {
error!(LOGGER, "dump_stored_tx: failed with error: {}", e);
Err(e)
}
},
Err(e) => {
error!(LOGGER, "dump_stored_tx: could not parse id: {}", e);
Err(ErrorKind::TransactionDumpError(
"dump_stored_tx: cannot dump transaction. Could not parse id in request.",
).into())
}
}
} else {
Err(ErrorKind::TransactionDumpError(
"dump_stored_tx: Cannot dump transaction. Missing id param in request.",
).into())
}
}
fn retrieve_summary_info( fn retrieve_summary_info(
&self, &self,
req: &Request<Body>, req: &Request<Body>,
@ -260,6 +290,7 @@ where
"retrieve_summary_info" => json_response(&self.retrieve_summary_info(req, api)?), "retrieve_summary_info" => json_response(&self.retrieve_summary_info(req, api)?),
"node_height" => json_response(&self.node_height(req, api)?), "node_height" => json_response(&self.node_height(req, api)?),
"retrieve_txs" => json_response(&self.retrieve_txs(req, api)?), "retrieve_txs" => json_response(&self.retrieve_txs(req, api)?),
"dump_stored_tx" => json_response(&self.dump_stored_tx(req, api)?),
_ => response(StatusCode::BAD_REQUEST, ""), _ => response(StatusCode::BAD_REQUEST, ""),
}) })
} }
@ -270,17 +301,77 @@ where
mut api: APIOwner<T, C, K>, mut api: APIOwner<T, C, K>,
) -> Box<Future<Item = Slate, Error = Error> + Send> { ) -> Box<Future<Item = Slate, Error = Error> + Send> {
Box::new(parse_body(req).and_then(move |args: SendTXArgs| { Box::new(parse_body(req).and_then(move |args: SendTXArgs| {
api.issue_send_tx( if args.method == "http" {
args.amount, api.issue_send_tx(
args.minimum_confirmations, args.amount,
&args.dest, args.minimum_confirmations,
args.max_outputs, &args.dest,
args.num_change_outputs, args.max_outputs,
args.selection_strategy_is_use_all, args.num_change_outputs,
) args.selection_strategy_is_use_all,
)
} else if args.method == "file" {
api.send_tx(
false,
args.amount,
args.minimum_confirmations,
&args.dest,
args.max_outputs,
args.num_change_outputs,
args.selection_strategy_is_use_all,
)
} else {
error!(LOGGER, "unsupported payment method: {}", args.method);
return Err(ErrorKind::ClientCallback("unsupported payment method"))?;
}
})) }))
} }
fn finalize_tx(
&self,
req: Request<Body>,
mut api: APIOwner<T, C, K>,
) -> Box<Future<Item = Slate, Error = Error> + Send> {
Box::new(
parse_body(req).and_then(move |mut slate| match api.finalize_tx(&mut slate) {
Ok(_) => ok(slate.clone()),
Err(e) => {
error!(LOGGER, "finalize_tx: failed with error: {}", e);
err(e)
}
}),
)
}
fn cancel_tx(
&self,
req: Request<Body>,
mut api: APIOwner<T, C, K>,
) -> Box<Future<Item = (), Error = Error> + Send> {
let params = parse_params(&req);
if let Some(id_string) = params.get("id") {
Box::new(match id_string[0].parse() {
Ok(id) => match api.cancel_tx(id) {
Ok(_) => ok(()),
Err(e) => {
error!(LOGGER, "finalize_tx: failed with error: {}", e);
err(e)
}
},
Err(e) => {
error!(LOGGER, "finalize_tx: could not parse id: {}", e);
err(ErrorKind::TransactionCancellationError(
"finalize_tx: cannot cancel transaction. Could not parse id in request.",
).into())
}
})
} else {
Box::new(err(ErrorKind::TransactionCancellationError(
"finalize_tx: Cannot cancel transaction. Missing id param in request.",
).into()))
}
}
fn issue_burn_tx( fn issue_burn_tx(
&self, &self,
_req: Request<Body>, _req: Request<Body>,
@ -307,6 +398,14 @@ where
self.issue_send_tx(req, api) self.issue_send_tx(req, api)
.and_then(|slate| ok(json_response_pretty(&slate))), .and_then(|slate| ok(json_response_pretty(&slate))),
), ),
"finalize_tx" => Box::new(
self.finalize_tx(req, api)
.and_then(|slate| ok(json_response_pretty(&slate))),
),
"cancel_tx" => Box::new(
self.cancel_tx(req, api)
.and_then(|_| ok(response(StatusCode::OK, ""))),
),
"issue_burn_tx" => Box::new( "issue_burn_tx" => Box::new(
self.issue_burn_tx(req, api) self.issue_burn_tx(req, api)
.and_then(|_| ok(response(StatusCode::OK, ""))), .and_then(|_| ok(response(StatusCode::OK, ""))),

View file

@ -152,6 +152,10 @@ pub enum ErrorKind {
#[fail(display = "Cancellation Error: {}", _0)] #[fail(display = "Cancellation Error: {}", _0)]
TransactionCancellationError(&'static str), TransactionCancellationError(&'static str),
/// Cancellation error
#[fail(display = "Tx dump Error: {}", _0)]
TransactionDumpError(&'static str),
/// Attempt to repost a transaction that's already confirmed /// Attempt to repost a transaction that's already confirmed
#[fail(display = "Transaction already confirmed error")] #[fail(display = "Transaction already confirmed error")]
TransactionAlreadyConfirmed, TransactionAlreadyConfirmed,

View file

@ -642,6 +642,8 @@ pub struct SendTXArgs {
pub amount: u64, pub amount: u64,
/// minimum confirmations /// minimum confirmations
pub minimum_confirmations: u64, pub minimum_confirmations: u64,
/// payment method
pub method: String,
/// destination url /// destination url
pub dest: String, pub dest: String,
/// Max number of outputs /// Max number of outputs