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:
* `-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,
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

View file

@ -12,6 +12,9 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use serde_json as json;
use std::fs::File;
use std::io::Read;
use std::path::PathBuf;
/// Wallet commands processing
use std::process::exit;
@ -166,7 +169,7 @@ pub fn wallet_command(wallet_args: &ArgMatches, config: GlobalWalletConfig) {
wallet_config.clone(),
passphrase,
)));
let res = controller::owner_single_use(wallet, |api| {
let res = controller::owner_single_use(wallet.clone(), |api| {
match wallet_args.subcommand() {
("send", Some(send_args)) => {
let amount = send_args
@ -182,6 +185,9 @@ pub fn wallet_command(wallet_args: &ArgMatches, config: GlobalWalletConfig) {
let selection_strategy = send_args
.value_of("selection_strategy")
.expect("Selection strategy required");
let method = send_args
.value_of("method")
.expect("Payment method required");
let dest = send_args
.value_of("dest")
.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.");
let fluff = send_args.is_present("fluff");
let max_outputs = 500;
if dest.starts_with("http") {
let result = api.issue_send_tx(
amount,
minimum_confirmations,
dest,
max_outputs,
change_outputs,
selection_strategy == "all",
);
let slate = match result {
Ok(s) => {
info!(
LOGGER,
"Tx created: {} grin to {} (strategy '{}')",
core::amount_to_hr_string(amount, false),
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)
if method == "http" {
if dest.starts_with("http://") {
let result = api.issue_send_tx(
amount,
minimum_confirmations,
dest,
max_outputs,
change_outputs,
selection_strategy == "all",
);
let slate = match result {
Ok(s) => {
info!(
LOGGER,
"Tx created: {} grin to {} (strategy '{}')",
core::amount_to_hr_string(amount, false),
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)
}
}
} else {
error!(
LOGGER,
"HTTP Destination should start with http://: {}", dest
);
panic!();
}
} else {
api.file_send_tx(
} else if method == "file" {
api.send_tx(
true,
amount,
minimum_confirmations,
dest,
@ -248,21 +263,36 @@ pub fn wallet_command(wallet_args: &ArgMatches, config: GlobalWalletConfig) {
selection_strategy == "all",
).expect("Send failed");
Ok(())
} else {
error!(LOGGER, "unsupported payment method: {}", method);
panic!();
}
}
("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(())
let mut receive_result: Result<(), grin_wallet::libwallet::Error> = Ok(());
let res = controller::foreign_single_use(wallet, |api| {
let tx_file = send_args
.value_of("input")
.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)) => {
let fluff = send_args.is_present("fluff");
let tx_file = send_args
.value_of("input")
.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);
match result {
@ -377,7 +407,7 @@ pub fn wallet_command(wallet_args: &ArgMatches, config: GlobalWalletConfig) {
}
}
Some(f) => {
let result = api.dump_stored_tx(tx_id, f);
let result = api.dump_stored_tx(tx_id, true, f);
match result {
Ok(_) => {
warn!(LOGGER, "Dumped transaction data for tx {} to {}", tx_id, f);

View file

@ -213,6 +213,13 @@ fn main() {
.long("change_outputs")
.default_value("1")
.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")
.help("Send the transaction to the provided server (start with http://) or save as file.")
.short("d")

View file

@ -185,15 +185,16 @@ where
/// 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(
pub fn send_tx(
&mut self,
write_to_disk: bool,
amount: u64,
minimum_confirmations: u64,
dest: &str,
max_outputs: usize,
num_change_outputs: usize,
selection_strategy_is_use_all: bool,
) -> Result<(), Error> {
) -> Result<Slate, Error> {
let mut w = self.wallet.lock().unwrap();
w.open_with_credentials()?;
@ -205,10 +206,11 @@ where
num_change_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()?;
if write_to_disk {
let mut pub_tx = File::create(dest)?;
pub_tx.write_all(json::to_string(&slate).unwrap().as_bytes())?;
pub_tx.sync_all()?;
}
{
let mut batch = w.batch()?;
@ -221,60 +223,19 @@ where
// lock our inputs
lock_fn(&mut **w, &tx_hex)?;
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(())
Ok(slate)
}
/// 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, 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)?;
pub fn finalize_tx(&mut self, slate: &mut Slate) -> Result<(), Error> {
let mut w = self.wallet.lock().unwrap();
w.open_with_credentials()?;
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()?;
batch.delete_private_context(slate.id.as_bytes())?;
@ -282,7 +243,7 @@ where
}
w.close()?;
Ok(slate)
Ok(())
}
/// Roll back a transaction and all associated outputs with a given
@ -342,7 +303,12 @@ where
}
/// 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 mut w = self.wallet.lock().unwrap();
w.open_with_credentials()?;
@ -365,10 +331,12 @@ where
}
let tx_bin = util::from_hex(tx_hex.unwrap()).unwrap();
let tx = ser::deserialize::<Transaction>(&mut &tx_bin[..])?;
let mut tx_file = File::create(dest)?;
tx_file.write_all(json::to_string(&tx).unwrap().as_bytes())?;
tx_file.sync_all()?;
Ok(())
if write_to_disk {
let mut tx_file = File::create(dest)?;
tx_file.write_all(json::to_string(&tx).unwrap().as_bytes())?;
tx_file.sync_all()?;
}
Ok(tx)
}
/// (Re)Posts a transaction that's already been stored to the chain
@ -498,6 +466,42 @@ where
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
pub fn receive_tx(&mut self, slate: &mut Slate) -> Result<(), Error> {
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_json;
use core::core::Transaction;
use keychain::Keychain;
use libtx::slate::Slate;
use libwallet::api::{APIForeign, APIOwner};
@ -229,6 +230,35 @@ where
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(
&self,
req: &Request<Body>,
@ -260,6 +290,7 @@ where
"retrieve_summary_info" => json_response(&self.retrieve_summary_info(req, api)?),
"node_height" => json_response(&self.node_height(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, ""),
})
}
@ -270,17 +301,77 @@ where
mut api: APIOwner<T, C, K>,
) -> Box<Future<Item = Slate, Error = Error> + Send> {
Box::new(parse_body(req).and_then(move |args: SendTXArgs| {
api.issue_send_tx(
args.amount,
args.minimum_confirmations,
&args.dest,
args.max_outputs,
args.num_change_outputs,
args.selection_strategy_is_use_all,
)
if args.method == "http" {
api.issue_send_tx(
args.amount,
args.minimum_confirmations,
&args.dest,
args.max_outputs,
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(
&self,
_req: Request<Body>,
@ -307,6 +398,14 @@ where
self.issue_send_tx(req, api)
.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(
self.issue_burn_tx(req, api)
.and_then(|_| ok(response(StatusCode::OK, ""))),

View file

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

View file

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