mirror of
https://github.com/mimblewimble/grin-wallet.git
synced 2025-01-20 19:11:09 +03:00
Command line implementation of invoice commands (#96)
* add issue_invoice_tx command * rustfmt * add first pass at process_invoice command * start of process_invoice fn * rustfmt * rename issue invoice and process invoice to invoice and pay * add prompting and display information to pay invoice command * rustfmt * support invoice transactions in finalize command * rustfmt
This commit is contained in:
parent
7a39c7cf3c
commit
6f875c5e92
8 changed files with 530 additions and 15 deletions
|
@ -35,7 +35,7 @@ use crate::impls::{
|
|||
LMDBBackend, NullWalletCommAdapter,
|
||||
};
|
||||
use crate::impls::{HTTPNodeClient, WalletSeed};
|
||||
use crate::libwallet::{InitTxArgs, NodeClient, WalletInst};
|
||||
use crate::libwallet::{InitTxArgs, IssueInvoiceTxArgs, NodeClient, WalletInst};
|
||||
use crate::{controller, display};
|
||||
|
||||
/// Arguments common to all wallet commands
|
||||
|
@ -377,13 +377,45 @@ pub fn finalize(
|
|||
) -> Result<(), Error> {
|
||||
let adapter = FileWalletCommAdapter::new();
|
||||
let mut slate = adapter.receive_tx_async(&args.input)?;
|
||||
controller::owner_single_use(wallet.clone(), |api| {
|
||||
if let Err(e) = api.verify_slate_messages(&slate) {
|
||||
error!("Error validating participant messages: {}", e);
|
||||
return Err(e);
|
||||
// Rather than duplicating the entire command, we'll just
|
||||
// try to determine what kind of finalization this is
|
||||
// based on the slate contents
|
||||
// for now, we can tell this is an invoice transaction
|
||||
// if the receipient (participant 1) hasn't completed sigs
|
||||
let part_data = slate.participant_with_id(1);
|
||||
let is_invoice = {
|
||||
match part_data {
|
||||
None => {
|
||||
error!("Expected slate participant data missing");
|
||||
return Err(ErrorKind::ArgumentError(
|
||||
"Expected Slate participant data missing".into(),
|
||||
))?;
|
||||
}
|
||||
Some(p) => !p.is_complete(),
|
||||
}
|
||||
slate = api.finalize_tx(&mut slate).expect("Finalize failed");
|
||||
};
|
||||
|
||||
if is_invoice {
|
||||
controller::foreign_single_use(wallet.clone(), |api| {
|
||||
if let Err(e) = api.verify_slate_messages(&slate) {
|
||||
error!("Error validating participant messages: {}", e);
|
||||
return Err(e);
|
||||
}
|
||||
slate = api.finalize_invoice_tx(&mut slate)?;
|
||||
Ok(())
|
||||
})?;
|
||||
} else {
|
||||
controller::owner_single_use(wallet.clone(), |api| {
|
||||
if let Err(e) = api.verify_slate_messages(&slate) {
|
||||
error!("Error validating participant messages: {}", e);
|
||||
return Err(e);
|
||||
}
|
||||
slate = api.finalize_tx(&mut slate)?;
|
||||
Ok(())
|
||||
})?;
|
||||
}
|
||||
|
||||
controller::owner_single_use(wallet.clone(), |api| {
|
||||
let result = api.post_tx(&slate.tx, args.fluff);
|
||||
match result {
|
||||
Ok(_) => {
|
||||
|
@ -396,9 +428,130 @@ pub fn finalize(
|
|||
}
|
||||
}
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Issue Invoice Args
|
||||
pub struct IssueInvoiceArgs {
|
||||
/// output file
|
||||
pub dest: String,
|
||||
/// issue invoice tx args
|
||||
pub issue_args: IssueInvoiceTxArgs,
|
||||
}
|
||||
|
||||
pub fn issue_invoice_tx(
|
||||
wallet: Arc<Mutex<WalletInst<impl NodeClient + 'static, keychain::ExtKeychain>>>,
|
||||
args: IssueInvoiceArgs,
|
||||
) -> Result<(), Error> {
|
||||
controller::owner_single_use(wallet.clone(), |api| {
|
||||
let slate = api.issue_invoice_tx(args.issue_args)?;
|
||||
let mut tx_file = File::create(args.dest.clone())?;
|
||||
tx_file.write_all(json::to_string(&slate).unwrap().as_bytes())?;
|
||||
tx_file.sync_all()?;
|
||||
Ok(())
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Arguments for the process_invoice command
|
||||
pub struct ProcessInvoiceArgs {
|
||||
pub message: Option<String>,
|
||||
pub minimum_confirmations: u64,
|
||||
pub selection_strategy: String,
|
||||
pub method: String,
|
||||
pub dest: String,
|
||||
pub max_outputs: usize,
|
||||
pub target_slate_version: Option<u16>,
|
||||
pub input: String,
|
||||
pub estimate_selection_strategies: bool,
|
||||
}
|
||||
|
||||
/// Process invoice
|
||||
pub fn process_invoice(
|
||||
wallet: Arc<Mutex<WalletInst<impl NodeClient + 'static, keychain::ExtKeychain>>>,
|
||||
args: ProcessInvoiceArgs,
|
||||
dark_scheme: bool,
|
||||
) -> Result<(), Error> {
|
||||
let adapter = FileWalletCommAdapter::new();
|
||||
let slate = adapter.receive_tx_async(&args.input)?;
|
||||
controller::owner_single_use(wallet.clone(), |api| {
|
||||
if args.estimate_selection_strategies {
|
||||
let strategies = vec!["smallest", "all"]
|
||||
.into_iter()
|
||||
.map(|strategy| {
|
||||
let init_args = InitTxArgs {
|
||||
src_acct_name: None,
|
||||
amount: slate.amount,
|
||||
minimum_confirmations: args.minimum_confirmations,
|
||||
max_outputs: args.max_outputs as u32,
|
||||
num_change_outputs: 1u32,
|
||||
selection_strategy_is_use_all: strategy == "all",
|
||||
estimate_only: Some(true),
|
||||
..Default::default()
|
||||
};
|
||||
let slate = api.init_send_tx(init_args).unwrap();
|
||||
(strategy, slate.amount, slate.fee)
|
||||
})
|
||||
.collect();
|
||||
display::estimate(slate.amount, strategies, dark_scheme);
|
||||
} else {
|
||||
let init_args = InitTxArgs {
|
||||
src_acct_name: None,
|
||||
amount: 0,
|
||||
minimum_confirmations: args.minimum_confirmations,
|
||||
max_outputs: args.max_outputs as u32,
|
||||
num_change_outputs: 1u32,
|
||||
selection_strategy_is_use_all: args.selection_strategy == "all",
|
||||
message: args.message.clone(),
|
||||
target_slate_version: args.target_slate_version,
|
||||
send_args: None,
|
||||
..Default::default()
|
||||
};
|
||||
if let Err(e) = api.verify_slate_messages(&slate) {
|
||||
error!("Error validating participant messages: {}", e);
|
||||
return Err(e);
|
||||
}
|
||||
let result = api.process_invoice_tx(&slate, init_args);
|
||||
let mut slate = match result {
|
||||
Ok(s) => {
|
||||
info!(
|
||||
"Invoice processed: {} grin to {} (strategy '{}')",
|
||||
core::amount_to_hr_string(slate.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(),
|
||||
"self" => NullWalletCommAdapter::new(),
|
||||
_ => NullWalletCommAdapter::new(),
|
||||
};
|
||||
if adapter.supports_sync() {
|
||||
slate = adapter.send_tx_sync(&args.dest, &slate)?;
|
||||
api.tx_lock_outputs(&slate)?;
|
||||
if args.method == "self" {
|
||||
controller::foreign_single_use(wallet, |api| {
|
||||
slate = api.finalize_invoice_tx(&slate)?;
|
||||
Ok(())
|
||||
})?;
|
||||
}
|
||||
} else {
|
||||
adapter.send_tx_async(&args.dest, &slate)?;
|
||||
api.tx_lock_outputs(&slate)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
/// Info command args
|
||||
pub struct InfoArgs {
|
||||
pub minimum_confirmations: u64,
|
||||
|
|
|
@ -124,6 +124,7 @@ fn invoice_tx_impl(test_dir: &str) -> Result<(), libwallet::Error> {
|
|||
..Default::default()
|
||||
};
|
||||
slate = api.process_invoice_tx(&slate, args)?;
|
||||
api.tx_lock_outputs(&slate)?;
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
|
|
|
@ -330,9 +330,6 @@ where
|
|||
batch.commit()?;
|
||||
}
|
||||
|
||||
// Always lock the context for now
|
||||
selection::lock_tx_context(&mut *w, slate, &context)?;
|
||||
tx::update_message(&mut *w, &mut ret_slate)?;
|
||||
Ok(ret_slate)
|
||||
}
|
||||
|
||||
|
|
|
@ -27,14 +27,14 @@ use crate::grin_core::core::verifier_cache::LruVerifierCache;
|
|||
use crate::grin_core::libtx::{aggsig, build, secp_ser, tx_fee};
|
||||
use crate::grin_core::map_vec;
|
||||
use crate::grin_keychain::{BlindSum, BlindingFactor, Keychain};
|
||||
use crate::grin_util::secp;
|
||||
use crate::grin_util::secp::key::{PublicKey, SecretKey};
|
||||
use crate::grin_util::secp::Signature;
|
||||
use crate::grin_util::RwLock;
|
||||
use crate::grin_util::{self, secp, RwLock};
|
||||
use failure::ResultExt;
|
||||
use rand::rngs::mock::StepRng;
|
||||
use rand::thread_rng;
|
||||
use serde_json;
|
||||
use std::fmt;
|
||||
use std::sync::Arc;
|
||||
use uuid::Uuid;
|
||||
|
||||
|
@ -113,6 +113,36 @@ impl ParticipantMessageData {
|
|||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ParticipantMessageData {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
writeln!(f, "")?;
|
||||
write!(f, "Participant ID {} ", self.id)?;
|
||||
if self.id == 0 {
|
||||
writeln!(f, "(Sender)")?;
|
||||
} else {
|
||||
writeln!(f, "(Recipient)")?;
|
||||
}
|
||||
writeln!(f, "---------------------")?;
|
||||
let static_secp = grin_util::static_secp_instance();
|
||||
let static_secp = static_secp.lock();
|
||||
writeln!(
|
||||
f,
|
||||
"Public Key: {}",
|
||||
&grin_util::to_hex(self.public_key.serialize_vec(&static_secp, true).to_vec())
|
||||
)?;
|
||||
let message = match self.message.clone() {
|
||||
None => "None".to_owned(),
|
||||
Some(m) => m,
|
||||
};
|
||||
writeln!(f, "Message: {}", message)?;
|
||||
let message_sig = match self.message_sig.clone() {
|
||||
None => "None".to_owned(),
|
||||
Some(m) => grin_util::to_hex(m.to_raw_data().to_vec()),
|
||||
};
|
||||
writeln!(f, "Message Signature: {}", message_sig)
|
||||
}
|
||||
}
|
||||
|
||||
/// A 'Slate' is passed around to all parties to build up all of the public
|
||||
/// transaction data needed to create a finalized transaction. Callers can pass
|
||||
/// the slate around by whatever means they choose, (but we can provide some
|
||||
|
@ -344,7 +374,6 @@ impl Slate {
|
|||
|
||||
/// Creates the final signature, callable by either the sender or recipient
|
||||
/// (after phase 3: sender confirmation)
|
||||
/// TODO: Only callable by receiver at the moment
|
||||
pub fn finalize<K>(&mut self, keychain: &K) -> Result<(), Error>
|
||||
where
|
||||
K: Keychain,
|
||||
|
@ -353,6 +382,16 @@ impl Slate {
|
|||
self.finalize_transaction(keychain, &final_sig)
|
||||
}
|
||||
|
||||
/// Return the participant with the given id
|
||||
pub fn participant_with_id(&self, id: usize) -> Option<ParticipantData> {
|
||||
for p in self.participant_data.iter() {
|
||||
if p.id as usize == id {
|
||||
return Some(p.clone());
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Return the sum of public nonces
|
||||
fn pub_nonce_sum(&self, secp: &secp::Secp256k1) -> Result<PublicKey, Error> {
|
||||
let pub_nonces = self
|
||||
|
|
|
@ -29,7 +29,7 @@
|
|||
//! * TxKernel fields serialized as hex strings instead of arrays:
|
||||
//! commit
|
||||
//! signature
|
||||
//! * version_info field removed
|
||||
//! * version field removed
|
||||
//! * VersionCompatInfo struct created with fields and added to beginning of struct
|
||||
//! version: u16
|
||||
//! orig_verion: u16,
|
||||
|
|
|
@ -21,9 +21,10 @@ use failure::Fail;
|
|||
use grin_wallet_config::WalletConfig;
|
||||
use grin_wallet_controller::command;
|
||||
use grin_wallet_controller::{Error, ErrorKind};
|
||||
use grin_wallet_impls::{instantiate_wallet, WalletSeed};
|
||||
use grin_wallet_libwallet::{NodeClient, WalletInst};
|
||||
use grin_wallet_impls::{instantiate_wallet, FileWalletCommAdapter, WalletSeed};
|
||||
use grin_wallet_libwallet::{IssueInvoiceTxArgs, NodeClient, Slate, WalletInst};
|
||||
use grin_wallet_util::grin_core as core;
|
||||
use grin_wallet_util::grin_core::core::amount_to_hr_string;
|
||||
use grin_wallet_util::grin_keychain as keychain;
|
||||
use linefeed::terminal::Signal;
|
||||
use linefeed::{Interface, ReadResult};
|
||||
|
@ -140,6 +141,62 @@ fn prompt_recovery_phrase() -> Result<ZeroingString, ParseError> {
|
|||
Ok(phrase)
|
||||
}
|
||||
|
||||
fn prompt_pay_invoice(slate: &Slate, method: &str, dest: &str) -> Result<bool, ParseError> {
|
||||
let interface = Arc::new(Interface::new("pay")?);
|
||||
let amount = amount_to_hr_string(slate.amount, false);
|
||||
interface.set_report_signal(Signal::Interrupt, true);
|
||||
interface.set_prompt(
|
||||
"To proceed, type the exact amount of the invoice as displayed above (or Q/q to quit) > ",
|
||||
)?;
|
||||
println!();
|
||||
println!(
|
||||
"This command will pay the amount specified in the invoice using your wallet's funds."
|
||||
);
|
||||
println!("After you confirm, the following will occur: ");
|
||||
println!();
|
||||
println!(
|
||||
"* {} of your wallet funds will be added to the transaction to pay this invoice.",
|
||||
amount
|
||||
);
|
||||
if method == "http" {
|
||||
println!("* The resulting transaction will IMMEDIATELY be sent to the wallet listening at: '{}'.", dest);
|
||||
} else {
|
||||
println!("* The resulting transaction will be saved to the file '{}', which you can manually send back to the invoice creator.", dest);
|
||||
}
|
||||
println!();
|
||||
println!("The invoice slate's participant info is:");
|
||||
for m in slate.participant_messages().messages {
|
||||
println!("{}", m);
|
||||
}
|
||||
println!("Please review the above information carefully before proceeding");
|
||||
println!();
|
||||
loop {
|
||||
let res = interface.read_line()?;
|
||||
match res {
|
||||
ReadResult::Eof => return Ok(false),
|
||||
ReadResult::Signal(sig) => {
|
||||
if sig == Signal::Interrupt {
|
||||
interface.cancel_read_line()?;
|
||||
return Err(ParseError::CancelledError);
|
||||
}
|
||||
}
|
||||
ReadResult::Input(line) => {
|
||||
match line.trim() {
|
||||
"Q" | "q" => return Err(ParseError::CancelledError),
|
||||
result => {
|
||||
if result == amount {
|
||||
return Ok(true);
|
||||
} else {
|
||||
println!("Please enter exact amount of the invoice as shown above or Q to quit");
|
||||
println!();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// instantiate wallet (needed by most functions)
|
||||
|
||||
pub fn inst_wallet(
|
||||
|
@ -457,6 +514,140 @@ pub fn parse_finalize_args(args: &ArgMatches) -> Result<command::FinalizeArgs, P
|
|||
})
|
||||
}
|
||||
|
||||
pub fn parse_issue_invoice_args(
|
||||
args: &ArgMatches,
|
||||
) -> Result<command::IssueInvoiceArgs, ParseError> {
|
||||
let amount = parse_required(args, "amount")?;
|
||||
let amount = core::core::amount_from_hr_string(amount);
|
||||
let amount = match amount {
|
||||
Ok(a) => a,
|
||||
Err(e) => {
|
||||
let msg = format!(
|
||||
"Could not parse amount as a number with optional decimal point. e={}",
|
||||
e
|
||||
);
|
||||
return Err(ParseError::ArgumentError(msg));
|
||||
}
|
||||
};
|
||||
// message
|
||||
let message = match args.is_present("message") {
|
||||
true => Some(args.value_of("message").unwrap().to_owned()),
|
||||
false => None,
|
||||
};
|
||||
// target slate version to create
|
||||
let target_slate_version = {
|
||||
match args.is_present("slate_version") {
|
||||
true => {
|
||||
let v = parse_required(args, "slate_version")?;
|
||||
Some(parse_u64(v, "slate_version")? as u16)
|
||||
}
|
||||
false => None,
|
||||
}
|
||||
};
|
||||
// dest (output file)
|
||||
let dest = parse_required(args, "dest")?;
|
||||
Ok(command::IssueInvoiceArgs {
|
||||
dest: dest.into(),
|
||||
issue_args: IssueInvoiceTxArgs {
|
||||
dest_acct_name: None,
|
||||
amount,
|
||||
message,
|
||||
target_slate_version,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
pub fn parse_process_invoice_args(
|
||||
args: &ArgMatches,
|
||||
) -> Result<command::ProcessInvoiceArgs, ParseError> {
|
||||
// TODO: display and prompt for confirmation of what we're doing
|
||||
// message
|
||||
let message = match args.is_present("message") {
|
||||
true => Some(args.value_of("message").unwrap().to_owned()),
|
||||
false => None,
|
||||
};
|
||||
|
||||
// minimum_confirmations
|
||||
let min_c = parse_required(args, "minimum_confirmations")?;
|
||||
let min_c = parse_u64(min_c, "minimum_confirmations")?;
|
||||
|
||||
// selection_strategy
|
||||
let selection_strategy = parse_required(args, "selection_strategy")?;
|
||||
|
||||
// estimate_selection_strategies
|
||||
let estimate_selection_strategies = args.is_present("estimate_selection_strategies");
|
||||
|
||||
// method
|
||||
let method = parse_required(args, "method")?;
|
||||
|
||||
// dest
|
||||
let dest = {
|
||||
if method == "self" {
|
||||
match args.value_of("dest") {
|
||||
Some(d) => d,
|
||||
None => "default",
|
||||
}
|
||||
} else {
|
||||
if !estimate_selection_strategies {
|
||||
parse_required(args, "dest")?
|
||||
} else {
|
||||
""
|
||||
}
|
||||
}
|
||||
};
|
||||
if !estimate_selection_strategies
|
||||
&& method == "http"
|
||||
&& !dest.starts_with("http://")
|
||||
&& !dest.starts_with("https://")
|
||||
{
|
||||
let msg = format!(
|
||||
"HTTP Destination should start with http://: or https://: {}",
|
||||
dest,
|
||||
);
|
||||
return Err(ParseError::ArgumentError(msg));
|
||||
}
|
||||
|
||||
// max_outputs
|
||||
let max_outputs = 500;
|
||||
|
||||
// target slate version to create/send
|
||||
let target_slate_version = {
|
||||
match args.is_present("slate_version") {
|
||||
true => {
|
||||
let v = parse_required(args, "slate_version")?;
|
||||
Some(parse_u64(v, "slate_version")? as u16)
|
||||
}
|
||||
false => None,
|
||||
}
|
||||
};
|
||||
|
||||
// file input only
|
||||
let tx_file = parse_required(args, "input")?;
|
||||
|
||||
// Now we need to prompt the user whether they want to do this,
|
||||
// which requires reading the slate
|
||||
let adapter = FileWalletCommAdapter::new();
|
||||
let slate = match adapter.receive_tx_async(&tx_file) {
|
||||
Ok(s) => s,
|
||||
Err(e) => return Err(ParseError::ArgumentError(format!("{}", e))),
|
||||
};
|
||||
|
||||
#[cfg(not(test))] // don't prompt during automated testing
|
||||
prompt_pay_invoice(&slate, method, dest)?;
|
||||
|
||||
Ok(command::ProcessInvoiceArgs {
|
||||
message: message,
|
||||
minimum_confirmations: min_c,
|
||||
selection_strategy: selection_strategy.to_owned(),
|
||||
estimate_selection_strategies,
|
||||
method: method.to_owned(),
|
||||
dest: dest.to_owned(),
|
||||
max_outputs: max_outputs,
|
||||
target_slate_version: target_slate_version,
|
||||
input: tx_file.to_owned(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn parse_info_args(args: &ArgMatches) -> Result<command::InfoArgs, ParseError> {
|
||||
// minimum_confirmations
|
||||
let mc = parse_required(args, "minimum_confirmations")?;
|
||||
|
@ -610,6 +801,18 @@ pub fn wallet_command(
|
|||
let a = arg_parse!(parse_finalize_args(&args));
|
||||
command::finalize(inst_wallet(), a)
|
||||
}
|
||||
("invoice", Some(args)) => {
|
||||
let a = arg_parse!(parse_issue_invoice_args(&args));
|
||||
command::issue_invoice_tx(inst_wallet(), a)
|
||||
}
|
||||
("pay", Some(args)) => {
|
||||
let a = arg_parse!(parse_process_invoice_args(&args));
|
||||
command::process_invoice(
|
||||
inst_wallet(),
|
||||
a,
|
||||
wallet_config.dark_background_color_scheme.unwrap_or(true),
|
||||
)
|
||||
}
|
||||
("info", Some(args)) => {
|
||||
let a = arg_parse!(parse_info_args(&args));
|
||||
command::info(
|
||||
|
|
|
@ -480,6 +480,54 @@ mod wallet_tests {
|
|||
];
|
||||
execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?;
|
||||
|
||||
// issue an invoice tx, wallet 2
|
||||
let file_name = format!("{}/invoice.slate", test_dir);
|
||||
let arg_vec = vec![
|
||||
"grin-wallet",
|
||||
"-p",
|
||||
"password",
|
||||
"invoice",
|
||||
"-d",
|
||||
&file_name,
|
||||
"-g",
|
||||
"Please give me your precious grins. Love, Yeast",
|
||||
"65",
|
||||
];
|
||||
execute_command(&app, test_dir, "wallet2", &client2, arg_vec)?;
|
||||
let output_file_name = format!("{}/invoice.slate.paid", test_dir);
|
||||
|
||||
// now pay the invoice tx, wallet 1
|
||||
let arg_vec = vec![
|
||||
"grin-wallet",
|
||||
"-a",
|
||||
"mining",
|
||||
"-p",
|
||||
"password",
|
||||
"pay",
|
||||
"-i",
|
||||
&file_name,
|
||||
"-d",
|
||||
&output_file_name,
|
||||
"-g",
|
||||
"Here you go",
|
||||
];
|
||||
execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?;
|
||||
|
||||
// and finalize, wallet 2
|
||||
let arg_vec = vec![
|
||||
"grin-wallet",
|
||||
"-p",
|
||||
"password",
|
||||
"finalize",
|
||||
"-i",
|
||||
&output_file_name,
|
||||
];
|
||||
execute_command(&app, test_dir, "wallet2", &client2, arg_vec)?;
|
||||
|
||||
// bit more mining
|
||||
let _ = test_framework::award_blocks_to_wallet(&chain, wallet1.clone(), 5, false);
|
||||
//bh += 5;
|
||||
|
||||
// txs and outputs (mostly spit out for a visual in test logs)
|
||||
let arg_vec = vec!["grin-wallet", "-p", "password", "-a", "mining", "txs"];
|
||||
execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?;
|
||||
|
@ -501,6 +549,12 @@ mod wallet_tests {
|
|||
let arg_vec = vec!["grin-wallet", "-p", "password", "-a", "mining", "outputs"];
|
||||
execute_command(&app, test_dir, "wallet1", &client1, arg_vec)?;
|
||||
|
||||
let arg_vec = vec!["grin-wallet", "-p", "password", "txs"];
|
||||
execute_command(&app, test_dir, "wallet2", &client2, arg_vec)?;
|
||||
|
||||
let arg_vec = vec!["grin-wallet", "-p", "password", "outputs"];
|
||||
execute_command(&app, test_dir, "wallet2", &client2, arg_vec)?;
|
||||
|
||||
// let logging finish
|
||||
thread::sleep(Duration::from_millis(200));
|
||||
Ok(())
|
||||
|
|
|
@ -162,6 +162,74 @@ subcommands:
|
|||
help: Fluff the transaction (ignore Dandelion relay protocol)
|
||||
short: f
|
||||
long: fluff
|
||||
- invoice:
|
||||
about: Initialize an invoice transction.
|
||||
args:
|
||||
- amount:
|
||||
help: Number of coins to invoice with optional fraction, e.g. 12.423
|
||||
index: 1
|
||||
- message:
|
||||
help: Optional participant message to include
|
||||
short: g
|
||||
long: message
|
||||
takes_value: true
|
||||
- slate_version:
|
||||
help: Target slate version to output/send to receiver
|
||||
short: v
|
||||
long: slate_version
|
||||
takes_value: true
|
||||
- dest:
|
||||
help: Name of destination slate output file
|
||||
short: d
|
||||
long: dest
|
||||
takes_value: true
|
||||
- pay:
|
||||
about: Spend coins to pay the provided invoice transaction
|
||||
args:
|
||||
- minimum_confirmations:
|
||||
help: Minimum number of confirmations required for an output to be spendable
|
||||
short: c
|
||||
long: min_conf
|
||||
default_value: "10"
|
||||
takes_value: true
|
||||
- selection_strategy:
|
||||
help: Coin/Output selection strategy.
|
||||
short: s
|
||||
long: selection
|
||||
possible_values:
|
||||
- all
|
||||
- smallest
|
||||
default_value: all
|
||||
takes_value: true
|
||||
- estimate_selection_strategies:
|
||||
help: Estimates all possible Coin/Output selection strategies.
|
||||
short: e
|
||||
long: estimate-selection
|
||||
- method:
|
||||
help: Method for sending the processed invoice back to the invoice creator
|
||||
short: m
|
||||
long: method
|
||||
possible_values:
|
||||
- file
|
||||
- http
|
||||
- self
|
||||
default_value: file
|
||||
takes_value: true
|
||||
- dest:
|
||||
help: Send the transaction to the provided server (start with http://) or save as file.
|
||||
short: d
|
||||
long: dest
|
||||
takes_value: true
|
||||
- message:
|
||||
help: Optional participant message to include
|
||||
short: g
|
||||
long: message
|
||||
takes_value: true
|
||||
- input:
|
||||
help: Partial transaction to process, expects the invoicer's transaction file.
|
||||
short: i
|
||||
long: input
|
||||
takes_value: true
|
||||
- outputs:
|
||||
about: Raw wallet output info (list of outputs)
|
||||
- txs:
|
||||
|
|
Loading…
Reference in a new issue