mirror of
https://github.com/jwinterm/grin-wallet.git
synced 2026-10-17 23:31:42 +02:00
--encrypt-for is no longer required. `contract new` without it produces an unencrypted slatepack; `contract sign` without it replies to the incoming slate's sender when known, otherwise unencrypted. Passing --encrypt-for is unchanged. The slatepack layer already produced plaintext for an empty recipient list — this only removes the CLI-side requirement.
1958 lines
50 KiB
Rust
1958 lines
50 KiB
Rust
// Copyright 2021 The Grin Developers
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
//! Grin wallet command-line function implementations
|
|
use crate::api::TLSConfig;
|
|
use crate::apiwallet::{try_slatepack_sync_workflow, Owner};
|
|
use crate::config::{TorConfig, WalletConfig, WALLET_CONFIG_FILE_NAME};
|
|
use crate::core::{core, global};
|
|
use crate::error::Error;
|
|
use crate::impls::PathToSlatepack;
|
|
use crate::impls::SlateGetter as _;
|
|
use crate::keychain;
|
|
use crate::libwallet::api_impl::owner;
|
|
use crate::libwallet::contract::can_finalize;
|
|
use crate::libwallet::contract::types::{
|
|
ContractNewArgsAPI, ContractRevokeArgsAPI, ContractSetupArgsAPI, OutputSelectionArgs,
|
|
};
|
|
use crate::libwallet::{
|
|
self, InitTxArgs, IssueInvoiceTxArgs, NodeClient, PaymentProof, Slate, SlateState, Slatepack,
|
|
SlatepackAddress, Slatepacker, SlatepackerArgs, WalletLCProvider,
|
|
};
|
|
use crate::util::secp::key::SecretKey;
|
|
use crate::util::{Mutex, ZeroingString};
|
|
use crate::{controller, display};
|
|
use ::core::time;
|
|
use qr_code::QrCode;
|
|
use serde::{Deserialize, Serialize};
|
|
use serde_json as json;
|
|
use std::convert::TryFrom;
|
|
use std::fmt;
|
|
use std::fs::File;
|
|
use std::io;
|
|
use std::io::{Read, Write};
|
|
use std::sync::atomic::Ordering;
|
|
use std::sync::Arc;
|
|
use std::thread;
|
|
use std::time::Duration;
|
|
use uuid::Uuid;
|
|
|
|
fn show_recovery_phrase(phrase: ZeroingString) {
|
|
println!("Your recovery phrase is:");
|
|
println!();
|
|
println!("{}", &*phrase);
|
|
println!();
|
|
println!("Please back-up these words in a non-digital format.");
|
|
}
|
|
|
|
/// Arguments common to all wallet commands
|
|
#[derive(Clone)]
|
|
pub struct GlobalArgs {
|
|
pub account: String,
|
|
pub api_secret: Option<String>,
|
|
pub node_api_secret: Option<String>,
|
|
pub show_spent: bool,
|
|
pub password: Option<ZeroingString>,
|
|
pub tls_conf: Option<TLSConfig>,
|
|
}
|
|
|
|
/// Arguments for init command
|
|
pub struct InitArgs {
|
|
/// BIP39 recovery phrase length
|
|
pub list_length: usize,
|
|
pub password: ZeroingString,
|
|
pub config: WalletConfig,
|
|
pub recovery_phrase: Option<ZeroingString>,
|
|
pub restore: bool,
|
|
}
|
|
|
|
/// Write config (default if None), initiate the wallet
|
|
pub fn init<L, C, K>(
|
|
owner_api: &mut Owner<L, C, K>,
|
|
_g_args: &GlobalArgs,
|
|
args: InitArgs,
|
|
test_mode: bool,
|
|
) -> Result<(), Error>
|
|
where
|
|
L: WalletLCProvider<'static, C, K> + 'static,
|
|
C: NodeClient + 'static,
|
|
K: keychain::Keychain + 'static,
|
|
{
|
|
// Assume global chain type has already been initialized.
|
|
let chain_type = global::get_chain_type();
|
|
|
|
let mut w_lock = owner_api.wallet_inst.lock();
|
|
let p = w_lock.lc_provider()?;
|
|
p.create_config(&chain_type, WALLET_CONFIG_FILE_NAME, None, None, None)?;
|
|
p.create_wallet(
|
|
None,
|
|
args.recovery_phrase,
|
|
args.list_length,
|
|
args.password.clone(),
|
|
test_mode,
|
|
)?;
|
|
|
|
let m = p.get_mnemonic(None, args.password)?;
|
|
show_recovery_phrase(m);
|
|
Ok(())
|
|
}
|
|
|
|
/// Argument for recover
|
|
pub struct RecoverArgs {
|
|
pub passphrase: ZeroingString,
|
|
}
|
|
|
|
pub fn recover<L, C, K>(owner_api: &mut Owner<L, C, K>, args: RecoverArgs) -> Result<(), Error>
|
|
where
|
|
L: WalletLCProvider<'static, C, K> + 'static,
|
|
C: NodeClient + 'static,
|
|
K: keychain::Keychain + 'static,
|
|
{
|
|
let mut w_lock = owner_api.wallet_inst.lock();
|
|
let p = w_lock.lc_provider()?;
|
|
let m = p.get_mnemonic(None, args.passphrase)?;
|
|
show_recovery_phrase(m);
|
|
Ok(())
|
|
}
|
|
|
|
pub fn rewind_hash<'a, L, C, K>(
|
|
owner_api: &mut Owner<L, C, K>,
|
|
keychain_mask: Option<&SecretKey>,
|
|
) -> Result<(), Error>
|
|
where
|
|
L: WalletLCProvider<'static, C, K>,
|
|
C: NodeClient + 'static,
|
|
K: keychain::Keychain + 'static,
|
|
{
|
|
controller::owner_single_use(None, keychain_mask, Some(owner_api), |api, m| {
|
|
let rewind_hash = api.get_rewind_hash(m)?;
|
|
println!();
|
|
println!("Wallet Rewind Hash");
|
|
println!("-------------------------------------");
|
|
println!("{}", rewind_hash);
|
|
println!();
|
|
Ok(())
|
|
})?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Arguments for rewind hash view wallet scan command
|
|
pub struct ViewWalletScanArgs {
|
|
pub rewind_hash: String,
|
|
pub start_height: Option<u64>,
|
|
pub backwards_from_tip: Option<u64>,
|
|
}
|
|
|
|
pub fn scan_rewind_hash<L, C, K>(
|
|
owner_api: &mut Owner<L, C, K>,
|
|
args: ViewWalletScanArgs,
|
|
dark_scheme: bool,
|
|
) -> Result<(), Error>
|
|
where
|
|
L: WalletLCProvider<'static, C, K> + 'static,
|
|
C: NodeClient + 'static,
|
|
K: keychain::Keychain + 'static,
|
|
{
|
|
controller::owner_single_use(None, None, Some(owner_api), |api, m| {
|
|
let rewind_hash = args.rewind_hash;
|
|
let tip_height = api.node_height(m)?.height;
|
|
let start_height = match args.backwards_from_tip {
|
|
Some(b) => tip_height.saturating_sub(b),
|
|
None => match args.start_height {
|
|
Some(s) => s,
|
|
None => 1,
|
|
},
|
|
};
|
|
warn!(
|
|
"Starting view wallet output scan from height {} ...",
|
|
start_height
|
|
);
|
|
let result = api.scan_rewind_hash(rewind_hash, Some(start_height));
|
|
let deci_sec = time::Duration::from_millis(100);
|
|
thread::sleep(deci_sec);
|
|
match result {
|
|
Ok(res) => {
|
|
warn!("View wallet check complete");
|
|
if res.total_balance != 0 {
|
|
display::view_wallet_output(res.clone(), tip_height, dark_scheme)?;
|
|
}
|
|
display::view_wallet_balance(res.clone(), tip_height, dark_scheme);
|
|
Ok(())
|
|
}
|
|
Err(e) => {
|
|
error!("View wallet check failed: {}", e);
|
|
Err(e)
|
|
}
|
|
}
|
|
})?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Arguments for listen command
|
|
pub struct ListenArgs {}
|
|
|
|
pub fn listen<L, C, K>(
|
|
owner_api: &mut Owner<L, C, K>,
|
|
keychain_mask: Arc<Mutex<Option<SecretKey>>>,
|
|
config: &WalletConfig,
|
|
tor_config: &TorConfig,
|
|
_args: &ListenArgs,
|
|
g_args: &GlobalArgs,
|
|
cli_mode: bool,
|
|
test_mode: bool,
|
|
) -> Result<(), Error>
|
|
where
|
|
L: WalletLCProvider<'static, C, K> + 'static,
|
|
C: NodeClient + 'static,
|
|
K: keychain::Keychain + 'static,
|
|
{
|
|
let wallet_inst = owner_api.wallet_inst.clone();
|
|
let config = config.clone();
|
|
let tor_config = tor_config.clone();
|
|
let g_args = g_args.clone();
|
|
let api_thread = thread::Builder::new()
|
|
.name("wallet-http-listener".to_string())
|
|
.spawn(move || {
|
|
let res = controller::foreign_listener(
|
|
wallet_inst,
|
|
keychain_mask,
|
|
&config.api_listen_addr(),
|
|
g_args.tls_conf.clone(),
|
|
tor_config.use_tor_listener,
|
|
test_mode,
|
|
Some(tor_config.clone()),
|
|
);
|
|
if let Err(e) = res {
|
|
error!("Error starting listener: {}", e);
|
|
}
|
|
});
|
|
if let Ok(t) = api_thread {
|
|
if !cli_mode {
|
|
let r = t.join();
|
|
if let Err(_) = r {
|
|
error!("Error starting listener");
|
|
return Err(Error::ListenerError);
|
|
}
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub fn owner_api<L, C, K>(
|
|
owner_api: &mut Owner<L, C, K>,
|
|
keychain_mask: Option<SecretKey>,
|
|
config: &WalletConfig,
|
|
tor_config: &TorConfig,
|
|
g_args: &GlobalArgs,
|
|
test_mode: bool,
|
|
) -> Result<(), Error>
|
|
where
|
|
L: WalletLCProvider<'static, C, K> + Send + Sync + 'static,
|
|
C: NodeClient + 'static,
|
|
K: keychain::Keychain + 'static,
|
|
{
|
|
// keychain mask needs to be a sinlge instance, in case the foreign API is
|
|
// also being run at the same time
|
|
let km = Arc::new(Mutex::new(keychain_mask));
|
|
let res = controller::owner_listener(
|
|
owner_api.wallet_inst.clone(),
|
|
km,
|
|
config.owner_api_listen_addr().as_str(),
|
|
g_args.api_secret.clone(),
|
|
g_args.tls_conf.clone(),
|
|
config.owner_api_include_foreign,
|
|
Some(tor_config.clone()),
|
|
test_mode,
|
|
);
|
|
if let Err(e) = res {
|
|
return Err(Error::LibWallet(e));
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Arguments for account command
|
|
pub struct AccountArgs {
|
|
pub create: Option<String>,
|
|
}
|
|
|
|
pub fn account<L, C, K>(
|
|
owner_api: &mut Owner<L, C, K>,
|
|
keychain_mask: Option<&SecretKey>,
|
|
args: AccountArgs,
|
|
) -> Result<(), Error>
|
|
where
|
|
L: WalletLCProvider<'static, C, K> + 'static,
|
|
C: NodeClient + 'static,
|
|
K: keychain::Keychain + 'static,
|
|
{
|
|
if args.create.is_none() {
|
|
let res = controller::owner_single_use(None, keychain_mask, Some(owner_api), |api, m| {
|
|
let acct_mappings = api.accounts(m)?;
|
|
// give logging thread a moment to catch up
|
|
thread::sleep(Duration::from_millis(200));
|
|
display::accounts(acct_mappings);
|
|
Ok(())
|
|
});
|
|
if let Err(e) = res {
|
|
error!("Error listing accounts: {}", e);
|
|
return Err(Error::LibWallet(e));
|
|
}
|
|
} else {
|
|
let label = args.create.unwrap();
|
|
let res = controller::owner_single_use(None, keychain_mask, Some(owner_api), |api, m| {
|
|
api.create_account_path(m, &label)?;
|
|
thread::sleep(Duration::from_millis(200));
|
|
info!("Account: '{}' Created!", label);
|
|
Ok(())
|
|
});
|
|
if let Err(e) = res {
|
|
thread::sleep(Duration::from_millis(200));
|
|
error!("Error creating account '{}': {}", label, e);
|
|
return Err(Error::LibWallet(e));
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Arguments for the send command
|
|
#[derive(Clone)]
|
|
pub struct SendArgs {
|
|
pub amount: u64,
|
|
pub amount_includes_fee: bool,
|
|
pub use_max_amount: bool,
|
|
pub minimum_confirmations: u64,
|
|
pub selection_strategy: String,
|
|
pub estimate_selection_strategies: bool,
|
|
pub late_lock: bool,
|
|
pub dest: String,
|
|
pub change_outputs: usize,
|
|
pub fluff: bool,
|
|
pub max_outputs: usize,
|
|
pub target_slate_version: Option<u16>,
|
|
pub payment_proof_address: Option<SlatepackAddress>,
|
|
pub ttl_blocks: Option<u64>,
|
|
pub skip_tor: bool,
|
|
pub outfile: Option<String>,
|
|
pub bridge: Option<String>,
|
|
pub slatepack_qr: bool,
|
|
}
|
|
|
|
pub fn send<L, C, K>(
|
|
owner_api: &mut Owner<L, C, K>,
|
|
keychain_mask: Option<&SecretKey>,
|
|
tor_config: Option<TorConfig>,
|
|
args: SendArgs,
|
|
dark_scheme: bool,
|
|
test_mode: bool,
|
|
) -> Result<(), Error>
|
|
where
|
|
L: WalletLCProvider<'static, C, K> + 'static,
|
|
C: NodeClient + 'static,
|
|
K: keychain::Keychain + 'static,
|
|
{
|
|
let mut slate = Slate::blank(2, false);
|
|
let mut amount = args.amount;
|
|
if args.use_max_amount {
|
|
controller::owner_single_use(None, keychain_mask, Some(owner_api), |api, m| {
|
|
let (_, wallet_info) =
|
|
api.retrieve_summary_info(m, true, args.minimum_confirmations)?;
|
|
amount = wallet_info.amount_currently_spendable;
|
|
Ok(())
|
|
})?;
|
|
};
|
|
controller::owner_single_use(None, keychain_mask, Some(owner_api), |api, m| {
|
|
if args.estimate_selection_strategies {
|
|
let strategies = vec!["smallest", "all"]
|
|
.into_iter()
|
|
.map(|strategy| {
|
|
let init_args = InitTxArgs {
|
|
src_acct_name: None,
|
|
amount: amount,
|
|
amount_includes_fee: Some(args.amount_includes_fee),
|
|
minimum_confirmations: args.minimum_confirmations,
|
|
max_outputs: args.max_outputs as u32,
|
|
num_change_outputs: args.change_outputs as u32,
|
|
selection_strategy_is_use_all: strategy == "all",
|
|
estimate_only: Some(true),
|
|
..Default::default()
|
|
};
|
|
let slate = api.init_send_tx(m, init_args)?;
|
|
Ok((strategy, slate.amount, slate.fee_fields))
|
|
})
|
|
.collect::<Result<Vec<_>, grin_wallet_libwallet::Error>>()?;
|
|
display::estimate(amount, strategies, dark_scheme);
|
|
return Ok(());
|
|
} else {
|
|
let init_args = InitTxArgs {
|
|
src_acct_name: None,
|
|
amount: amount,
|
|
amount_includes_fee: Some(args.amount_includes_fee),
|
|
minimum_confirmations: args.minimum_confirmations,
|
|
max_outputs: args.max_outputs as u32,
|
|
num_change_outputs: args.change_outputs as u32,
|
|
selection_strategy_is_use_all: args.selection_strategy == "all",
|
|
target_slate_version: args.target_slate_version,
|
|
payment_proof_recipient_address: args.payment_proof_address.clone(),
|
|
ttl_blocks: args.ttl_blocks,
|
|
send_args: None,
|
|
late_lock: Some(args.late_lock),
|
|
..Default::default()
|
|
};
|
|
let result = api.init_send_tx(m, init_args);
|
|
slate = match result {
|
|
Ok(s) => {
|
|
info!(
|
|
"Tx created: {} grin to {} (strategy '{}')",
|
|
core::amount_to_hr_string(amount, false),
|
|
args.dest,
|
|
args.selection_strategy,
|
|
);
|
|
s
|
|
}
|
|
Err(e) => {
|
|
info!("Tx not created: {}", e);
|
|
return Err(e);
|
|
}
|
|
};
|
|
}
|
|
Ok(())
|
|
})?;
|
|
|
|
if args.estimate_selection_strategies {
|
|
return Ok(());
|
|
}
|
|
|
|
let tor_config = match tor_config {
|
|
Some(mut c) => {
|
|
if let Some(b) = args.bridge.clone() {
|
|
c.bridge.bridge_line = Some(b);
|
|
}
|
|
c.skip_send_attempt = Some(args.skip_tor);
|
|
Some(c)
|
|
}
|
|
None => None,
|
|
};
|
|
|
|
let res = try_slatepack_sync_workflow(&slate, &args.dest, tor_config, None, false, test_mode);
|
|
|
|
match res {
|
|
Ok(Some(s)) => {
|
|
controller::owner_single_use(None, keychain_mask, Some(owner_api), |api, m| {
|
|
api.tx_lock_outputs(m, &s)?;
|
|
let ret_slate = api.finalize_tx(m, &s)?;
|
|
let result = api.post_tx(m, &ret_slate, args.fluff);
|
|
match result {
|
|
Ok(_) => {
|
|
println!("Tx sent successfully",);
|
|
Ok(())
|
|
}
|
|
Err(e) => {
|
|
error!("Tx sent fail: {}", e);
|
|
Err(e.into())
|
|
}
|
|
}
|
|
})?;
|
|
}
|
|
Ok(None) => {
|
|
output_slatepack(
|
|
owner_api,
|
|
keychain_mask,
|
|
&slate,
|
|
args.dest.as_str(),
|
|
args.outfile,
|
|
true,
|
|
false,
|
|
args.slatepack_qr,
|
|
)?;
|
|
}
|
|
Err(e) => return Err(e.into()),
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub fn output_slatepack<L, C, K>(
|
|
owner_api: &mut Owner<L, C, K>,
|
|
keychain_mask: Option<&SecretKey>,
|
|
slate: &Slate,
|
|
dest: &str,
|
|
out_file_override: Option<String>,
|
|
lock: bool,
|
|
finalizing: bool,
|
|
show_qr: bool,
|
|
) -> Result<(), libwallet::Error>
|
|
where
|
|
L: WalletLCProvider<'static, C, K> + 'static,
|
|
C: NodeClient + 'static,
|
|
K: keychain::Keychain + 'static,
|
|
{
|
|
// Output the slatepack file to stdout and to a file
|
|
let mut message = String::from("");
|
|
let mut address = None;
|
|
let mut tld = String::from("");
|
|
controller::owner_single_use(None, keychain_mask, Some(owner_api), |api, m| {
|
|
address = match SlatepackAddress::try_from(dest) {
|
|
Ok(a) => Some(a),
|
|
Err(_) => None,
|
|
};
|
|
// encrypt for recipient by default
|
|
let recipients = match address.clone() {
|
|
Some(a) => vec![a],
|
|
None => vec![],
|
|
};
|
|
message = api.create_slatepack_message(m, &slate, Some(0), recipients)?;
|
|
tld = api.get_top_level_directory()?;
|
|
Ok(())
|
|
})?;
|
|
|
|
// create a directory to which files will be output
|
|
let slate_dir = format!("{}/{}", tld, "slatepack");
|
|
let _ = std::fs::create_dir_all(slate_dir.clone());
|
|
let out_file_name = match out_file_override {
|
|
None => format!("{}/{}.{}.slatepack", slate_dir, slate.id, slate.state),
|
|
Some(f) => f,
|
|
};
|
|
|
|
if lock {
|
|
controller::owner_single_use(None, keychain_mask, Some(owner_api), |api, m| {
|
|
api.tx_lock_outputs(m, &slate)?;
|
|
Ok(())
|
|
})?;
|
|
}
|
|
|
|
println!("{}", out_file_name);
|
|
let mut output = File::create(out_file_name.clone())?;
|
|
output.write_all(&message.as_bytes())?;
|
|
output.sync_all()?;
|
|
|
|
println!();
|
|
if !finalizing {
|
|
println!("Slatepack data follows. Please provide this output to the other party");
|
|
} else {
|
|
println!("Slatepack data follows.");
|
|
}
|
|
println!();
|
|
println!("--- CUT BELOW THIS LINE ---");
|
|
println!();
|
|
println!("{}", message);
|
|
println!("--- CUT ABOVE THIS LINE ---");
|
|
println!();
|
|
println!("Slatepack data was also output to");
|
|
println!();
|
|
println!("{}", out_file_name);
|
|
println!();
|
|
if show_qr {
|
|
if let Ok(qr_string) = QrCode::new(message) {
|
|
println!("{}", qr_string.to_string(false, 3));
|
|
println!();
|
|
}
|
|
}
|
|
if address.is_some() {
|
|
println!("The slatepack data is encrypted for the recipient only");
|
|
} else {
|
|
println!("The slatepack data is NOT encrypted");
|
|
}
|
|
println!();
|
|
Ok(())
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
|
pub struct SlatepackOut {
|
|
/// Is slatepack encrypted
|
|
pub is_encrypted: bool,
|
|
/// Is slatepack finalized
|
|
pub is_finalized: bool,
|
|
/// File where slatepack is saved
|
|
pub out_file: String,
|
|
/// Slatepack message. Encrypted or not.
|
|
pub message: String,
|
|
}
|
|
|
|
impl fmt::Display for SlatepackOut {
|
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
let start_meta = "--------------- SLATEPACK METADATA --------------";
|
|
let meta = format!(
|
|
"Slate encrypted: {}\nSlate finalized: {}\nSlate saved to file: {}",
|
|
self.is_encrypted, self.is_finalized, self.out_file
|
|
);
|
|
let start_slatepack = "-------------- CUT BELOW THIS LINE --------------";
|
|
let end_slatepack = "-------------- CUT ABOVE THIS LINE --------------";
|
|
write!(
|
|
f,
|
|
"{start_meta}\n\n{meta}\n\n{start_slatepack}\n\n{}\n\n{end_slatepack}",
|
|
self.message
|
|
)
|
|
}
|
|
}
|
|
|
|
impl SlatepackOut {
|
|
fn as_json(&self) -> String {
|
|
serde_json::to_string_pretty(&self).unwrap()
|
|
}
|
|
|
|
pub fn print(&self, as_json: bool) -> () {
|
|
if !self.is_finalized {
|
|
if as_json {
|
|
println!("{}", self.as_json());
|
|
} else {
|
|
println!("{}", self);
|
|
}
|
|
} else {
|
|
println!("Transaction was broadcasted."); // TODO: as_json makes no sense here, fix later.
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn print_slatepack<L, C, K>(
|
|
api: &mut Owner<L, C, K>,
|
|
keychain_mask: Option<&SecretKey>,
|
|
slate: &Slate,
|
|
counterparty_addr: &str,
|
|
out_file: Option<String>,
|
|
as_json: bool,
|
|
) -> ()
|
|
where
|
|
L: WalletLCProvider<'static, C, K> + 'static,
|
|
C: NodeClient + 'static,
|
|
K: keychain::Keychain + 'static,
|
|
{
|
|
// For now, we don't compact slates with sl.compact(). We first make them work without compaction.
|
|
let slate_out =
|
|
prepare_slatepack(api, keychain_mask, &slate, &counterparty_addr, out_file).unwrap();
|
|
slate_out.print(as_json);
|
|
}
|
|
|
|
pub fn prepare_slatepack<L, C, K>(
|
|
owner_api: &mut Owner<L, C, K>,
|
|
keychain_mask: Option<&SecretKey>,
|
|
slate: &Slate,
|
|
dest: &str,
|
|
out_file_override: Option<String>,
|
|
) -> Result<SlatepackOut, libwallet::Error>
|
|
where
|
|
L: WalletLCProvider<'static, C, K> + 'static,
|
|
C: NodeClient + 'static,
|
|
K: keychain::Keychain + 'static,
|
|
{
|
|
// Same as output_slatepack except that we don't write to stdout, care about locking or whether the slate was finalized.
|
|
|
|
// Output the slatepack file to stdout and to a file
|
|
let mut message = String::from("");
|
|
let mut address = None;
|
|
let mut tld = String::from("");
|
|
controller::owner_single_use(None, keychain_mask, Some(owner_api), |api, m| {
|
|
address = match SlatepackAddress::try_from(dest) {
|
|
Ok(a) => Some(a),
|
|
Err(_) => None,
|
|
};
|
|
// encrypt for recipient by default
|
|
let recipients = match address.clone() {
|
|
Some(a) => vec![a],
|
|
None => vec![],
|
|
};
|
|
// TODO: what is sender_index?
|
|
message = api.create_slatepack_message(m, &slate, Some(0), recipients)?;
|
|
// Trim the \n at the end.
|
|
let len_withoutcrlf = message.trim_end().len();
|
|
message.truncate(len_withoutcrlf);
|
|
|
|
tld = api.get_top_level_directory()?;
|
|
Ok(())
|
|
})?;
|
|
|
|
// create a directory to which files will be output
|
|
let slate_dir = format!("{}/{}", tld, "slatepack");
|
|
let _ = std::fs::create_dir_all(slate_dir.clone());
|
|
let out_file_name = match out_file_override {
|
|
None => format!("{}/{}.{}.slatepack", slate_dir, slate.id, slate.state),
|
|
Some(f) => f,
|
|
};
|
|
|
|
let mut output = File::create(out_file_name.clone())?;
|
|
output.write_all(&message.as_bytes())?;
|
|
output.sync_all()?;
|
|
|
|
// Since we always finalize if we can, we can also use this to know if the tx is finalized
|
|
let is_finalized = can_finalize(slate);
|
|
|
|
let slate_out = SlatepackOut {
|
|
is_encrypted: address.is_some(),
|
|
is_finalized: is_finalized,
|
|
out_file: out_file_name,
|
|
message: message,
|
|
};
|
|
|
|
// TODO: We save the slatepack, but it is encrypted for the counterparty. It seems hard to
|
|
// know which slatepack is which if we can't decrypt them. Either add some more metadata
|
|
// to slatepacks e.g. timestamp, counterparty address or save also a version that is encrypted with
|
|
// our own address so we can view it.
|
|
|
|
Ok(slate_out)
|
|
}
|
|
|
|
// Parse a slate and slatepack from a message
|
|
pub fn parse_slatepack<L, C, K>(
|
|
owner_api: &mut Owner<L, C, K>,
|
|
keychain_mask: Option<&SecretKey>,
|
|
filename: Option<String>,
|
|
message: Option<String>,
|
|
) -> Result<(Slate, Option<SlatepackAddress>), Error>
|
|
where
|
|
L: WalletLCProvider<'static, C, K>,
|
|
C: NodeClient + 'static,
|
|
K: keychain::Keychain + 'static,
|
|
{
|
|
let mut ret_address = None;
|
|
let slate = match filename {
|
|
Some(f) => {
|
|
// otherwise, get slate from slatepack
|
|
let mut sl = None;
|
|
controller::owner_single_use(None, keychain_mask, Some(owner_api), |api, m| {
|
|
let dec_key = api.get_slatepack_secret_key(m, 0)?;
|
|
let packer = Slatepacker::new(SlatepackerArgs {
|
|
sender: None,
|
|
recipients: vec![],
|
|
dec_key: Some(&dec_key),
|
|
});
|
|
let pts = PathToSlatepack::new(f.into(), &packer, true);
|
|
sl = Some(pts.get_tx()?.0);
|
|
ret_address = pts.get_slatepack(true)?.sender;
|
|
Ok(())
|
|
})?;
|
|
sl
|
|
}
|
|
None => None,
|
|
};
|
|
|
|
let slate = match slate {
|
|
Some(s) => s,
|
|
None => {
|
|
// try and parse directly from input_slatepack_message
|
|
let mut slate = Slate::blank(2, false);
|
|
match message {
|
|
Some(message) => {
|
|
controller::owner_single_use(
|
|
None,
|
|
keychain_mask,
|
|
Some(owner_api),
|
|
|api, m| {
|
|
slate =
|
|
api.slate_from_slatepack_message(m, message.clone(), vec![0])?;
|
|
let slatepack =
|
|
api.decode_slatepack_message(m, message.clone(), vec![0])?;
|
|
ret_address = slatepack.sender;
|
|
Ok(())
|
|
},
|
|
)?;
|
|
}
|
|
None => {
|
|
let msg = "No slate provided via file or direct input";
|
|
return Err(Error::GenericError(msg.into()).into());
|
|
}
|
|
}
|
|
slate
|
|
}
|
|
};
|
|
Ok((slate, ret_address))
|
|
}
|
|
|
|
/// Receive command argument
|
|
#[derive(Clone)]
|
|
pub struct ReceiveArgs {
|
|
pub input_file: Option<String>,
|
|
pub input_slatepack_message: Option<String>,
|
|
pub skip_tor: bool,
|
|
pub outfile: Option<String>,
|
|
pub bridge: Option<String>,
|
|
pub slatepack_qr: bool,
|
|
}
|
|
|
|
pub fn receive<L, C, K>(
|
|
owner_api: &mut Owner<L, C, K>,
|
|
keychain_mask: Option<&SecretKey>,
|
|
g_args: &GlobalArgs,
|
|
args: ReceiveArgs,
|
|
tor_config: Option<TorConfig>,
|
|
test_mode: bool,
|
|
) -> Result<(), Error>
|
|
where
|
|
L: WalletLCProvider<'static, C, K>,
|
|
C: NodeClient + 'static,
|
|
K: keychain::Keychain + 'static,
|
|
{
|
|
let (mut slate, ret_address) = parse_slatepack(
|
|
owner_api,
|
|
keychain_mask,
|
|
args.input_file,
|
|
args.input_slatepack_message,
|
|
)?;
|
|
|
|
let km = match keychain_mask.as_ref() {
|
|
None => None,
|
|
Some(&m) => Some(m.to_owned()),
|
|
};
|
|
|
|
let tor_config = match tor_config {
|
|
Some(mut c) => {
|
|
if let Some(b) = args.bridge {
|
|
c.bridge.bridge_line = Some(b);
|
|
}
|
|
c.skip_send_attempt = Some(args.skip_tor);
|
|
Some(c)
|
|
}
|
|
None => None,
|
|
};
|
|
|
|
controller::foreign_single_use(owner_api.wallet_inst.clone(), km, |api| {
|
|
slate = api.receive_tx(&slate, Some(&g_args.account), None)?;
|
|
Ok(())
|
|
})?;
|
|
|
|
let dest = match ret_address {
|
|
Some(a) => String::try_from(&a).unwrap(),
|
|
None => String::from(""),
|
|
};
|
|
|
|
let res = try_slatepack_sync_workflow(&slate, &dest, tor_config, None, true, test_mode);
|
|
|
|
match res {
|
|
Ok(Some(_)) => {
|
|
println!();
|
|
println!(
|
|
"Transaction recieved and sent back to sender at {} for finalization.",
|
|
dest
|
|
);
|
|
println!();
|
|
Ok(())
|
|
}
|
|
Ok(None) => {
|
|
output_slatepack(
|
|
owner_api,
|
|
keychain_mask,
|
|
&slate,
|
|
&dest,
|
|
args.outfile,
|
|
false,
|
|
false,
|
|
args.slatepack_qr,
|
|
)?;
|
|
Ok(())
|
|
}
|
|
Err(e) => Err(e.into()),
|
|
}
|
|
}
|
|
|
|
pub fn unpack<L, C, K>(
|
|
owner_api: &mut Owner<L, C, K>,
|
|
keychain_mask: Option<&SecretKey>,
|
|
args: ReceiveArgs,
|
|
) -> Result<(), Error>
|
|
where
|
|
L: WalletLCProvider<'static, C, K> + 'static,
|
|
C: NodeClient + 'static,
|
|
K: keychain::Keychain + 'static,
|
|
{
|
|
let mut slatepack = match args.input_file {
|
|
Some(f) => {
|
|
let packer = Slatepacker::new(SlatepackerArgs {
|
|
sender: None,
|
|
recipients: vec![],
|
|
dec_key: None,
|
|
});
|
|
PathToSlatepack::new(f.into(), &packer, true).get_slatepack(false)?
|
|
}
|
|
None => match args.input_slatepack_message {
|
|
Some(mes) => {
|
|
let mut sp = Slatepack::default();
|
|
controller::owner_single_use(None, keychain_mask, Some(owner_api), |api, m| {
|
|
sp = api.decode_slatepack_message(m, mes, vec![])?;
|
|
Ok(())
|
|
})?;
|
|
sp
|
|
}
|
|
None => {
|
|
return Err(Error::ArgumentError("Invalid Slatepack Input".into()).into());
|
|
}
|
|
},
|
|
};
|
|
println!();
|
|
println!("SLATEPACK CONTENTS");
|
|
println!("------------------");
|
|
println!("{}", slatepack);
|
|
println!("------------------");
|
|
|
|
let packer = Slatepacker::new(SlatepackerArgs {
|
|
sender: None,
|
|
recipients: vec![],
|
|
dec_key: None,
|
|
});
|
|
|
|
if slatepack.mode == 1 {
|
|
controller::owner_single_use(None, keychain_mask, Some(owner_api), |api, m| {
|
|
let dec_key = api.get_slatepack_secret_key(m, 0)?;
|
|
match slatepack.try_decrypt_payload(Some(&dec_key)) {
|
|
Ok(_) => {
|
|
println!("Slatepack is encrypted for this wallet");
|
|
println!();
|
|
println!("DECRYPTED SLATEPACK");
|
|
println!("-------------------");
|
|
println!("{}", slatepack);
|
|
let slate = packer.get_slate(&slatepack)?;
|
|
println!();
|
|
println!("DECRYPTED SLATE");
|
|
println!("---------------");
|
|
println!("{}", slate);
|
|
}
|
|
Err(_) => {
|
|
println!("Slatepack payload cannot be decrypted by this wallet");
|
|
}
|
|
}
|
|
Ok(())
|
|
})?;
|
|
} else {
|
|
let slate = packer.get_slate(&slatepack)?;
|
|
println!("Slatepack is not encrypted");
|
|
println!();
|
|
println!("SLATE");
|
|
println!("-----");
|
|
println!("{}", slate);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Finalize command args
|
|
#[derive(Clone)]
|
|
pub struct FinalizeArgs {
|
|
pub input_file: Option<String>,
|
|
pub input_slatepack_message: Option<String>,
|
|
pub fluff: bool,
|
|
pub nopost: bool,
|
|
pub outfile: Option<String>,
|
|
pub slatepack_qr: bool,
|
|
}
|
|
|
|
pub fn finalize<L, C, K>(
|
|
owner_api: &mut Owner<L, C, K>,
|
|
keychain_mask: Option<&SecretKey>,
|
|
args: FinalizeArgs,
|
|
) -> Result<(), Error>
|
|
where
|
|
L: WalletLCProvider<'static, C, K> + 'static,
|
|
C: NodeClient + 'static,
|
|
K: keychain::Keychain + 'static,
|
|
{
|
|
let (mut slate, _ret_address) = parse_slatepack(
|
|
owner_api,
|
|
keychain_mask,
|
|
args.input_file.clone(),
|
|
args.input_slatepack_message.clone(),
|
|
)?;
|
|
|
|
// Rather than duplicating the entire command, we'll just
|
|
// try to determine what kind of finalization this is
|
|
// based on the slate state
|
|
let is_invoice = slate.state == SlateState::Invoice2;
|
|
|
|
if is_invoice {
|
|
let km = match keychain_mask.as_ref() {
|
|
None => None,
|
|
Some(&m) => Some(m.to_owned()),
|
|
};
|
|
controller::foreign_single_use(owner_api.wallet_inst.clone(), km, |api| {
|
|
slate = api.finalize_tx(&slate, false)?;
|
|
Ok(())
|
|
})?;
|
|
} else {
|
|
controller::owner_single_use(None, keychain_mask, Some(owner_api), |api, m| {
|
|
slate = api.finalize_tx(m, &slate)?;
|
|
Ok(())
|
|
})?;
|
|
}
|
|
|
|
if !&args.nopost {
|
|
controller::owner_single_use(None, keychain_mask, Some(owner_api), |api, m| {
|
|
let result = api.post_tx(m, &slate, args.fluff);
|
|
match result {
|
|
Ok(_) => {
|
|
info!(
|
|
"Transaction sent successfully, check the wallet again for confirmation."
|
|
);
|
|
println!("Transaction posted");
|
|
Ok(())
|
|
}
|
|
Err(e) => {
|
|
error!("Tx not sent: {}", e);
|
|
Err(e)
|
|
}
|
|
}
|
|
})?;
|
|
}
|
|
|
|
println!("Transaction finalized successfully");
|
|
|
|
output_slatepack(
|
|
owner_api,
|
|
keychain_mask,
|
|
&slate,
|
|
"",
|
|
args.outfile,
|
|
false,
|
|
true,
|
|
args.slatepack_qr,
|
|
)?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Issue Invoice Args
|
|
pub struct IssueInvoiceArgs {
|
|
/// Slatepack address
|
|
pub dest: String,
|
|
/// issue invoice tx args
|
|
pub issue_args: IssueInvoiceTxArgs,
|
|
/// output file override
|
|
pub outfile: Option<String>,
|
|
/// show slatepack as QR code
|
|
pub slatepack_qr: bool,
|
|
}
|
|
|
|
pub fn issue_invoice_tx<L, C, K>(
|
|
owner_api: &mut Owner<L, C, K>,
|
|
keychain_mask: Option<&SecretKey>,
|
|
args: IssueInvoiceArgs,
|
|
) -> Result<(), Error>
|
|
where
|
|
L: WalletLCProvider<'static, C, K> + 'static,
|
|
C: NodeClient + 'static,
|
|
K: keychain::Keychain + 'static,
|
|
{
|
|
let issue_args = args.issue_args.clone();
|
|
|
|
let mut slate = Slate::blank(2, false);
|
|
controller::owner_single_use(None, keychain_mask, Some(owner_api), |api, m| {
|
|
slate = api.issue_invoice_tx(m, issue_args)?;
|
|
Ok(())
|
|
})?;
|
|
|
|
output_slatepack(
|
|
owner_api,
|
|
keychain_mask,
|
|
&slate,
|
|
args.dest.as_str(),
|
|
args.outfile,
|
|
false,
|
|
false,
|
|
args.slatepack_qr,
|
|
)?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Arguments for the process_invoice command
|
|
pub struct ProcessInvoiceArgs {
|
|
pub minimum_confirmations: u64,
|
|
pub selection_strategy: String,
|
|
pub ret_address: Option<SlatepackAddress>,
|
|
pub max_outputs: usize,
|
|
pub slate: Slate,
|
|
pub estimate_selection_strategies: bool,
|
|
pub ttl_blocks: Option<u64>,
|
|
pub skip_tor: bool,
|
|
pub outfile: Option<String>,
|
|
pub bridge: Option<String>,
|
|
pub slatepack_qr: bool,
|
|
}
|
|
|
|
/// Process invoice
|
|
pub fn process_invoice<L, C, K>(
|
|
owner_api: &mut Owner<L, C, K>,
|
|
keychain_mask: Option<&SecretKey>,
|
|
tor_config: Option<TorConfig>,
|
|
args: ProcessInvoiceArgs,
|
|
dark_scheme: bool,
|
|
test_mode: bool,
|
|
) -> Result<(), Error>
|
|
where
|
|
L: WalletLCProvider<'static, C, K> + 'static,
|
|
C: NodeClient + 'static,
|
|
K: keychain::Keychain + 'static,
|
|
{
|
|
let mut slate = args.slate.clone();
|
|
let dest = match args.ret_address.clone() {
|
|
Some(a) => String::try_from(&a).unwrap(),
|
|
None => String::from(""),
|
|
};
|
|
|
|
controller::owner_single_use(None, keychain_mask, Some(owner_api), |api, m| {
|
|
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(m, init_args).unwrap();
|
|
(strategy, slate.amount, slate.fee_fields)
|
|
})
|
|
.collect();
|
|
display::estimate(slate.amount, strategies, dark_scheme);
|
|
return Ok(());
|
|
} 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",
|
|
ttl_blocks: args.ttl_blocks,
|
|
send_args: None,
|
|
..Default::default()
|
|
};
|
|
let result = api.process_invoice_tx(m, &slate, init_args);
|
|
slate = match result {
|
|
Ok(s) => {
|
|
info!(
|
|
"Invoice processed: {} grin (strategy '{}')",
|
|
core::amount_to_hr_string(slate.amount, false),
|
|
args.selection_strategy,
|
|
);
|
|
s
|
|
}
|
|
Err(e) => {
|
|
info!("Tx not created: {}", e);
|
|
return Err(e);
|
|
}
|
|
};
|
|
}
|
|
Ok(())
|
|
})?;
|
|
|
|
let tor_config = match tor_config {
|
|
Some(mut c) => {
|
|
if let Some(b) = args.bridge {
|
|
c.bridge.bridge_line = Some(b);
|
|
}
|
|
c.skip_send_attempt = Some(args.skip_tor);
|
|
Some(c)
|
|
}
|
|
None => None,
|
|
};
|
|
|
|
let res = try_slatepack_sync_workflow(&slate, &dest, tor_config, None, true, test_mode);
|
|
|
|
match res {
|
|
Ok(Some(_)) => {
|
|
println!();
|
|
println!(
|
|
"Transaction paid and sent back to initiator at {} for finalization.",
|
|
dest
|
|
);
|
|
println!();
|
|
Ok(())
|
|
}
|
|
Ok(None) => {
|
|
output_slatepack(
|
|
owner_api,
|
|
keychain_mask,
|
|
&slate,
|
|
&dest,
|
|
args.outfile,
|
|
true,
|
|
false,
|
|
args.slatepack_qr,
|
|
)?;
|
|
Ok(())
|
|
}
|
|
Err(e) => Err(e.into()),
|
|
}
|
|
}
|
|
|
|
/// Info command args
|
|
pub struct InfoArgs {
|
|
pub minimum_confirmations: u64,
|
|
}
|
|
|
|
pub fn info<L, C, K>(
|
|
owner_api: &mut Owner<L, C, K>,
|
|
keychain_mask: Option<&SecretKey>,
|
|
g_args: &GlobalArgs,
|
|
args: InfoArgs,
|
|
dark_scheme: bool,
|
|
) -> Result<(), Error>
|
|
where
|
|
L: WalletLCProvider<'static, C, K> + 'static,
|
|
C: NodeClient + 'static,
|
|
K: keychain::Keychain + 'static,
|
|
{
|
|
let updater_running = owner_api.updater_running.load(Ordering::Relaxed);
|
|
controller::owner_single_use(None, keychain_mask, Some(owner_api), |api, m| {
|
|
let (validated, wallet_info) =
|
|
api.retrieve_summary_info(m, true, args.minimum_confirmations)?;
|
|
display::info(
|
|
&g_args.account,
|
|
&wallet_info,
|
|
validated || updater_running,
|
|
dark_scheme,
|
|
);
|
|
Ok(())
|
|
})?;
|
|
Ok(())
|
|
}
|
|
|
|
pub fn outputs<L, C, K>(
|
|
owner_api: &mut Owner<L, C, K>,
|
|
keychain_mask: Option<&SecretKey>,
|
|
g_args: &GlobalArgs,
|
|
dark_scheme: bool,
|
|
) -> Result<(), Error>
|
|
where
|
|
L: WalletLCProvider<'static, C, K> + 'static,
|
|
C: NodeClient + 'static,
|
|
K: keychain::Keychain + 'static,
|
|
{
|
|
let updater_running = owner_api.updater_running.load(Ordering::Relaxed);
|
|
controller::owner_single_use(None, keychain_mask, Some(owner_api), |api, m| {
|
|
let res = api.node_height(m)?;
|
|
let (validated, outputs) = api.retrieve_outputs(m, g_args.show_spent, true, None)?;
|
|
display::outputs(
|
|
&g_args.account,
|
|
res.height,
|
|
validated || updater_running,
|
|
outputs,
|
|
dark_scheme,
|
|
)?;
|
|
Ok(())
|
|
})?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Txs command args
|
|
pub struct TxsArgs {
|
|
pub id: Option<u32>,
|
|
pub tx_slate_id: Option<Uuid>,
|
|
pub count: Option<u32>,
|
|
}
|
|
|
|
pub fn txs<L, C, K>(
|
|
owner_api: &mut Owner<L, C, K>,
|
|
keychain_mask: Option<&SecretKey>,
|
|
g_args: &GlobalArgs,
|
|
args: TxsArgs,
|
|
dark_scheme: bool,
|
|
) -> Result<(), Error>
|
|
where
|
|
L: WalletLCProvider<'static, C, K> + 'static,
|
|
C: NodeClient + 'static,
|
|
K: keychain::Keychain + 'static,
|
|
{
|
|
let updater_running = owner_api.updater_running.load(Ordering::Relaxed);
|
|
controller::owner_single_use(None, keychain_mask, Some(owner_api), |api, m| {
|
|
let res = api.node_height(m)?;
|
|
// Note advanced query args not currently supported by command line client
|
|
let (validated, txs) = api.retrieve_txs(m, true, args.id, args.tx_slate_id, None)?;
|
|
let include_status = !args.id.is_some() && !args.tx_slate_id.is_some();
|
|
// If view count is specified, restrict the TX list to `txs.len() - count`
|
|
let first_tx = args
|
|
.count
|
|
.map_or(0, |c| txs.len().saturating_sub(c as usize));
|
|
display::txs(
|
|
&g_args.account,
|
|
res.height,
|
|
validated || updater_running,
|
|
&txs[first_tx..],
|
|
include_status,
|
|
dark_scheme,
|
|
)?;
|
|
|
|
// if given a particular transaction id or uuid, also get and display associated
|
|
// inputs/outputs and messages
|
|
let id = if args.id.is_some() {
|
|
args.id
|
|
} else if args.tx_slate_id.is_some() {
|
|
if let Some(tx) = txs.iter().find(|t| t.tx_slate_id == args.tx_slate_id) {
|
|
Some(tx.id)
|
|
} else {
|
|
println!("Could not find a transaction matching given txid.\n");
|
|
None
|
|
}
|
|
} else {
|
|
None
|
|
};
|
|
|
|
if id.is_some() {
|
|
let (_, outputs) = api.retrieve_outputs(m, true, false, id)?;
|
|
display::outputs(
|
|
&g_args.account,
|
|
res.height,
|
|
validated || updater_running,
|
|
outputs,
|
|
dark_scheme,
|
|
)?;
|
|
// should only be one here, but just in case
|
|
for tx in txs {
|
|
display::payment_proof(&tx)?;
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
})?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Post
|
|
#[derive(Clone)]
|
|
pub struct PostArgs {
|
|
pub input_file: Option<String>,
|
|
pub input_slatepack_message: Option<String>,
|
|
pub fluff: bool,
|
|
}
|
|
|
|
pub fn post<L, C, K>(
|
|
owner_api: &mut Owner<L, C, K>,
|
|
keychain_mask: Option<&SecretKey>,
|
|
args: PostArgs,
|
|
) -> Result<(), Error>
|
|
where
|
|
L: WalletLCProvider<'static, C, K> + 'static,
|
|
C: NodeClient + 'static,
|
|
K: keychain::Keychain + 'static,
|
|
{
|
|
let (slate, _ret_address) = parse_slatepack(
|
|
owner_api,
|
|
keychain_mask,
|
|
args.input_file,
|
|
args.input_slatepack_message,
|
|
)?;
|
|
|
|
let fluff = args.fluff;
|
|
controller::owner_single_use(None, keychain_mask, Some(owner_api), |api, m| {
|
|
api.post_tx(m, &slate, fluff)?;
|
|
info!("Posted transaction");
|
|
return Ok(());
|
|
})?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Repost
|
|
pub struct RepostArgs {
|
|
pub id: u32,
|
|
pub dump_file: Option<String>,
|
|
pub fluff: bool,
|
|
}
|
|
|
|
pub fn repost<L, C, K>(
|
|
owner_api: &mut Owner<L, C, K>,
|
|
keychain_mask: Option<&SecretKey>,
|
|
args: RepostArgs,
|
|
) -> Result<(), Error>
|
|
where
|
|
L: WalletLCProvider<'static, C, K> + 'static,
|
|
C: NodeClient + 'static,
|
|
K: keychain::Keychain + 'static,
|
|
{
|
|
controller::owner_single_use(None, keychain_mask, Some(owner_api), |api, m| {
|
|
let stored_tx_slate = match api.get_stored_tx(m, Some(args.id), None)? {
|
|
None => {
|
|
error!(
|
|
"Transaction with id {} does not have transaction data. Not reposting.",
|
|
args.id
|
|
);
|
|
return Ok(());
|
|
}
|
|
Some(s) => s,
|
|
};
|
|
let (_, txs) = api.retrieve_txs(m, true, Some(args.id), None, None)?;
|
|
match args.dump_file {
|
|
None => {
|
|
if txs[0].confirmed {
|
|
error!(
|
|
"Transaction with id {} is confirmed. Not reposting.",
|
|
args.id
|
|
);
|
|
return Ok(());
|
|
}
|
|
if libwallet::sig_is_blank(
|
|
&stored_tx_slate.tx.as_ref().unwrap().kernels()[0].excess_sig,
|
|
) {
|
|
error!("Transaction at {} has not been finalized.", args.id);
|
|
return Ok(());
|
|
}
|
|
|
|
match api.post_tx(m, &stored_tx_slate, args.fluff) {
|
|
Ok(_) => info!("Reposted transaction at {}", args.id),
|
|
Err(e) => error!("Could not repost transaction at {}. Reason: {}", args.id, e),
|
|
}
|
|
return Ok(());
|
|
}
|
|
Some(f) => {
|
|
let mut tx_file = File::create(f.clone())?;
|
|
tx_file.write_all(
|
|
json::to_string(&stored_tx_slate.tx.unwrap())
|
|
.unwrap()
|
|
.as_bytes(),
|
|
)?;
|
|
tx_file.sync_all()?;
|
|
info!("Dumped transaction data for tx {} to {}", args.id, f);
|
|
return Ok(());
|
|
}
|
|
}
|
|
})?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Cancel
|
|
pub struct CancelArgs {
|
|
pub tx_id: Option<u32>,
|
|
pub tx_slate_id: Option<Uuid>,
|
|
pub tx_id_string: String,
|
|
}
|
|
|
|
pub fn cancel<L, C, K>(
|
|
owner_api: &mut Owner<L, C, K>,
|
|
keychain_mask: Option<&SecretKey>,
|
|
args: CancelArgs,
|
|
) -> Result<(), Error>
|
|
where
|
|
L: WalletLCProvider<'static, C, K> + 'static,
|
|
C: NodeClient + 'static,
|
|
K: keychain::Keychain + 'static,
|
|
{
|
|
controller::owner_single_use(None, keychain_mask, Some(owner_api), |api, m| {
|
|
let result = api.cancel_tx(m, args.tx_id, args.tx_slate_id);
|
|
match result {
|
|
Ok(_) => {
|
|
info!("Transaction {} Cancelled", args.tx_id_string);
|
|
Ok(())
|
|
}
|
|
Err(e) => {
|
|
error!("TX Cancellation failed: {}", e);
|
|
Err(e)
|
|
}
|
|
}
|
|
})?;
|
|
Ok(())
|
|
}
|
|
|
|
/// wallet check
|
|
pub struct CheckArgs {
|
|
pub delete_unconfirmed: bool,
|
|
pub start_height: Option<u64>,
|
|
pub backwards_from_tip: Option<u64>,
|
|
}
|
|
|
|
pub fn scan<L, C, K>(
|
|
owner_api: &mut Owner<L, C, K>,
|
|
keychain_mask: Option<&SecretKey>,
|
|
args: CheckArgs,
|
|
) -> Result<(), Error>
|
|
where
|
|
L: WalletLCProvider<'static, C, K> + 'static,
|
|
C: NodeClient + 'static,
|
|
K: keychain::Keychain + 'static,
|
|
{
|
|
controller::owner_single_use(None, keychain_mask, Some(owner_api), |api, m| {
|
|
let tip_height = api.node_height(m)?.height;
|
|
let start_height = match args.backwards_from_tip {
|
|
Some(b) => tip_height.saturating_sub(b),
|
|
None => match args.start_height {
|
|
Some(s) => s,
|
|
None => 1,
|
|
},
|
|
};
|
|
warn!("Starting output scan from height {} ...", start_height);
|
|
let result = api.scan(m, Some(start_height), args.delete_unconfirmed);
|
|
match result {
|
|
Ok(_) => {
|
|
warn!("Wallet check complete",);
|
|
Ok(())
|
|
}
|
|
Err(e) => {
|
|
error!("Wallet check failed: {}", e);
|
|
Err(e)
|
|
}
|
|
}
|
|
})?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Payment Proof Address
|
|
pub fn address<L, C, K>(
|
|
owner_api: &mut Owner<L, C, K>,
|
|
g_args: &GlobalArgs,
|
|
keychain_mask: Option<&SecretKey>,
|
|
) -> Result<(), Error>
|
|
where
|
|
L: WalletLCProvider<'static, C, K> + 'static,
|
|
C: NodeClient + 'static,
|
|
K: keychain::Keychain + 'static,
|
|
{
|
|
controller::owner_single_use(None, keychain_mask, Some(owner_api), |api, m| {
|
|
// Just address at derivation index 0 for now
|
|
let address = api.get_slatepack_address(m, 0)?;
|
|
println!();
|
|
println!("Address for account - {}", g_args.account);
|
|
println!("-------------------------------------");
|
|
println!("{}", address);
|
|
println!();
|
|
Ok(())
|
|
})?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Proof Export Args
|
|
pub struct ProofExportArgs {
|
|
pub output_file: String,
|
|
pub id: Option<u32>,
|
|
pub tx_slate_id: Option<Uuid>,
|
|
}
|
|
|
|
pub fn proof_export<L, C, K>(
|
|
owner_api: &mut Owner<L, C, K>,
|
|
keychain_mask: Option<&SecretKey>,
|
|
args: ProofExportArgs,
|
|
) -> Result<(), Error>
|
|
where
|
|
L: WalletLCProvider<'static, C, K> + 'static,
|
|
C: NodeClient + 'static,
|
|
K: keychain::Keychain + 'static,
|
|
{
|
|
controller::owner_single_use(None, keychain_mask, Some(owner_api), |api, m| {
|
|
let result = api.retrieve_payment_proof(m, true, args.id, args.tx_slate_id);
|
|
match result {
|
|
Ok(p) => {
|
|
// actually export proof
|
|
let mut proof_file = File::create(args.output_file.clone())?;
|
|
proof_file.write_all(json::to_string_pretty(&p).unwrap().as_bytes())?;
|
|
proof_file.sync_all()?;
|
|
warn!("Payment proof exported to {}", args.output_file);
|
|
Ok(())
|
|
}
|
|
Err(e) => {
|
|
error!("Proof export failed: {}", e);
|
|
Err(e)
|
|
}
|
|
}
|
|
})?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Proof Verify Args
|
|
pub struct ProofVerifyArgs {
|
|
pub input_file: String,
|
|
}
|
|
|
|
pub fn proof_verify<L, C, K>(
|
|
owner_api: &mut Owner<L, C, K>,
|
|
keychain_mask: Option<&SecretKey>,
|
|
args: ProofVerifyArgs,
|
|
) -> Result<(), Error>
|
|
where
|
|
L: WalletLCProvider<'static, C, K> + 'static,
|
|
C: NodeClient + 'static,
|
|
K: keychain::Keychain + 'static,
|
|
{
|
|
controller::owner_single_use(None, keychain_mask, Some(owner_api), |api, m| {
|
|
let mut proof_f = match File::open(&args.input_file) {
|
|
Ok(p) => p,
|
|
Err(e) => {
|
|
let msg = format!("{}", e);
|
|
error!(
|
|
"Unable to open payment proof file at {}: {}",
|
|
args.input_file, e
|
|
);
|
|
return Err(libwallet::Error::PaymentProofParsing(msg));
|
|
}
|
|
};
|
|
let mut proof = String::new();
|
|
proof_f.read_to_string(&mut proof)?;
|
|
// read
|
|
let proof: PaymentProof = match json::from_str(&proof) {
|
|
Ok(p) => p,
|
|
Err(e) => {
|
|
let msg = format!("{}", e);
|
|
error!("Unable to parse payment proof file: {}", e);
|
|
return Err(libwallet::Error::PaymentProofParsing(msg));
|
|
}
|
|
};
|
|
let result = api.verify_payment_proof(m, &proof);
|
|
match result {
|
|
Ok((iam_sender, iam_recipient)) => {
|
|
println!("Payment proof's signatures are valid.");
|
|
if iam_sender {
|
|
println!("The proof's sender address belongs to this wallet.");
|
|
}
|
|
if iam_recipient {
|
|
println!("The proof's recipient address belongs to this wallet.");
|
|
}
|
|
if !iam_recipient && !iam_sender {
|
|
println!(
|
|
"Neither the proof's sender nor recipient address belongs to this wallet."
|
|
);
|
|
}
|
|
Ok(())
|
|
}
|
|
Err(e) => {
|
|
error!("Proof not valid: {}", e);
|
|
Err(e)
|
|
}
|
|
}
|
|
})?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Create new contract command arguments
|
|
#[derive(Clone)]
|
|
pub struct ContractNewArgs {
|
|
/// Address of the counterparty (None = produce an unencrypted slatepack)
|
|
pub counterparty_addr: Option<String>,
|
|
/// Receive amount
|
|
pub receive: Option<u64>,
|
|
/// Send amount
|
|
pub send: Option<u64>,
|
|
/// The human readable account name from which to draw outputs
|
|
/// for the transaction, overriding whatever the active account is as set via the
|
|
/// [`set_active_account`](../grin_wallet_api/owner/struct.Owner.html#method.set_active_account) method.
|
|
pub src_acct_name: Option<String>,
|
|
/// Number of participants in a contract (either 1 or 2)
|
|
pub num_participants: u8,
|
|
/// Show the resulting slatepack as JSON
|
|
pub as_json: bool,
|
|
/// Use the specified inputs (comma separated input commitments)
|
|
pub use_inputs: Option<String>,
|
|
/// How to separate outputs (command separated amounts)
|
|
pub make_outputs: Option<String>,
|
|
|
|
// Future features
|
|
/// Custom fee contribution
|
|
pub fee_rate: Option<u32>,
|
|
/// Save slatepack to a specific filename
|
|
pub outfile: Option<String>,
|
|
/// Select outputs early
|
|
pub add_outputs: bool,
|
|
}
|
|
|
|
impl ContractNewArgs {
|
|
fn get_net_change(&self) -> i64 {
|
|
// TODO: could the cast 'as i64' overflow or something?
|
|
match self.receive {
|
|
None => match self.send {
|
|
None => panic!("Send or receive not specified."),
|
|
Some(v) => -(v as i64), // negative net change on send
|
|
},
|
|
Some(v) => v as i64, // positive net change on receive
|
|
}
|
|
}
|
|
|
|
// Create a ContractNewArgsAPI from the ContractNewArgs
|
|
fn to_api_args(&self) -> ContractNewArgsAPI {
|
|
let net_change = self.get_net_change();
|
|
ContractNewArgsAPI {
|
|
setup_args: ContractSetupArgsAPI {
|
|
src_acct_name: match self.src_acct_name.as_ref() {
|
|
Some(v) => Some(v.to_string()),
|
|
None => None,
|
|
},
|
|
net_change: Some(net_change),
|
|
num_participants: self.num_participants,
|
|
add_outputs: self.add_outputs,
|
|
selection_args: OutputSelectionArgs {
|
|
use_inputs: match self.use_inputs.as_ref() {
|
|
Some(v) => Some(v.to_string()),
|
|
None => None,
|
|
},
|
|
make_outputs: match self.make_outputs.as_ref() {
|
|
Some(v) => Some(v.to_string()),
|
|
None => None,
|
|
},
|
|
..Default::default()
|
|
},
|
|
proof_args: Default::default(),
|
|
},
|
|
..Default::default()
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn contract_new<L, C, K>(
|
|
owner_api: &mut Owner<L, C, K>,
|
|
keychain_mask: Option<&SecretKey>,
|
|
args: ContractNewArgs,
|
|
) -> Result<(), Error>
|
|
where
|
|
L: WalletLCProvider<'static, C, K>,
|
|
C: NodeClient + 'static,
|
|
K: keychain::Keychain + 'static,
|
|
{
|
|
controller::owner_single_use(None, keychain_mask, Some(owner_api), |api, m| {
|
|
let contract_new_args = args.to_api_args();
|
|
|
|
let slate = api.contract_new(m, &contract_new_args)?;
|
|
|
|
print_slatepack(
|
|
api,
|
|
keychain_mask,
|
|
&slate,
|
|
args.counterparty_addr.as_deref().unwrap_or(""),
|
|
args.outfile,
|
|
args.as_json,
|
|
);
|
|
|
|
Ok(())
|
|
})?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Sign contract command argument
|
|
#[derive(Clone)]
|
|
pub struct ContractSetupArgs {
|
|
/// Address of the counterparty
|
|
pub counterparty_addr: Option<String>,
|
|
/// Receive amount
|
|
pub receive: Option<u64>,
|
|
/// Send amount
|
|
pub send: Option<u64>,
|
|
/// Show the resulting slatepack as JSON
|
|
pub as_json: bool,
|
|
/// Use the specified inputs (comma separated input commitments)
|
|
pub use_inputs: Option<String>,
|
|
/// How to separate outputs (command separated amounts)
|
|
pub make_outputs: Option<String>,
|
|
|
|
// Future features
|
|
/// Whether we should automatically sign a receive of any value
|
|
// pub auto_receive: Option<bool>,
|
|
/// Custom fee contribution
|
|
pub fee_rate: Option<u32>,
|
|
/// Save slatepack to a specific filename
|
|
pub outfile: Option<String>,
|
|
/// Add outputs
|
|
pub add_outputs: bool, // lock outputs early
|
|
}
|
|
|
|
impl ContractSetupArgs {
|
|
fn get_net_change(&self) -> Option<i64> {
|
|
let mut net_change: Option<i64> = None;
|
|
// TODO: Check bounds before casting to i64.
|
|
if self.receive.is_some() && self.send.is_some() {
|
|
panic!("Can't pass both --receive and --send parameters.");
|
|
}
|
|
if self.receive.is_some() {
|
|
net_change = Some(self.receive.unwrap() as i64);
|
|
}
|
|
if self.send.is_some() {
|
|
net_change = Some(-(self.send.unwrap() as i64));
|
|
}
|
|
net_change
|
|
}
|
|
|
|
// Create a ContractSetupArgsAPI from the ContractSetupArgs
|
|
fn to_api_args(&self) -> ContractSetupArgsAPI {
|
|
let net_change = self.get_net_change();
|
|
ContractSetupArgsAPI {
|
|
// TODO: num_participants is derived here. It should be an Option and read from the slate.
|
|
// Need to check no attack are possible regarding kernel fee contribution.
|
|
net_change: net_change,
|
|
add_outputs: self.add_outputs,
|
|
selection_args: OutputSelectionArgs {
|
|
use_inputs: match self.use_inputs.as_ref() {
|
|
Some(v) => Some(v.to_string()),
|
|
None => None,
|
|
},
|
|
make_outputs: match self.make_outputs.as_ref() {
|
|
Some(v) => Some(v.to_string()),
|
|
None => None,
|
|
},
|
|
..Default::default()
|
|
},
|
|
..Default::default()
|
|
}
|
|
}
|
|
}
|
|
|
|
// pub fn contract_setup<L, C, K>(
|
|
// owner_api: &mut Owner<L, C, K>,
|
|
// keychain_mask: Option<&SecretKey>,
|
|
// args: ContractSetupArgs,
|
|
// ) -> Result<(), Error>
|
|
// where
|
|
// L: WalletLCProvider<'static, C, K>,
|
|
// C: NodeClient + 'static,
|
|
// K: keychain::Keychain + 'static,
|
|
// {
|
|
// controller::owner_single_use(None, keychain_mask, Some(owner_api), |api, m| {
|
|
// // Read the slatepack from stdin
|
|
// println!("Paste slatepack:");
|
|
// let mut slatepack_msg = String::new();
|
|
// io::stdin()
|
|
// .read_line(&mut slatepack_msg)
|
|
// .expect("Failed to read from stdin");
|
|
|
|
// let mut contract_setup_args = args.to_api_args();
|
|
|
|
// // Decrypt the slate, perform setup on it and encrypt it for the next party
|
|
// // TODO: Make sure you get the counterparty_addr and slate with 1 call.
|
|
// let slatepack = owner::decode_slatepack_message(
|
|
// api.wallet_inst.clone(),
|
|
// keychain_mask,
|
|
// String::from(slatepack_msg.clone()),
|
|
// vec![0],
|
|
// )?;
|
|
|
|
// let counterparty_addr =
|
|
// if args.counterparty_addr.is_some() {
|
|
// args.counterparty_addr.unwrap()
|
|
// } else {
|
|
// if !slatepack.sender.is_some() {
|
|
// panic!("No address to encrypt for. Contracts only support encrypted slates right now.");
|
|
// }
|
|
// String::try_from(&slatepack.sender.unwrap())?
|
|
// };
|
|
// let mut slate = owner::slate_from_slatepack_message(
|
|
// api.wallet_inst.clone(),
|
|
// keychain_mask,
|
|
// String::from(slatepack_msg),
|
|
// vec![0],
|
|
// )?;
|
|
// // We read the number of participants from the slate that was already created. We need this to
|
|
// // avoid taking the default value of 2 in case of 3-party computation and thus incorrectly computing
|
|
// // our kernel cost contribution.
|
|
// contract_setup_args.num_participants = slate.num_participants;
|
|
|
|
// slate = api.contract_setup(m, &slate, &contract_setup_args)?;
|
|
|
|
// print_slatepack(
|
|
// api,
|
|
// keychain_mask,
|
|
// &slate,
|
|
// &counterparty_addr,
|
|
// args.outfile,
|
|
// args.as_json,
|
|
// );
|
|
|
|
// Ok(())
|
|
// })?;
|
|
|
|
// Ok(())
|
|
// }
|
|
|
|
pub fn contract_sign<L, C, K>(
|
|
owner_api: &mut Owner<L, C, K>,
|
|
keychain_mask: Option<&SecretKey>,
|
|
args: ContractSetupArgs,
|
|
broadcast_tx: bool,
|
|
) -> Result<(), Error>
|
|
where
|
|
L: WalletLCProvider<'static, C, K>,
|
|
C: NodeClient + 'static,
|
|
K: keychain::Keychain + 'static,
|
|
{
|
|
controller::owner_single_use(None, keychain_mask, Some(owner_api), |api, m| {
|
|
// Read the slatepack from stdin
|
|
println!("Paste slatepack:");
|
|
let mut slatepack_msg = String::new();
|
|
io::stdin()
|
|
.read_line(&mut slatepack_msg)
|
|
.expect("Failed to read from stdin");
|
|
|
|
// Args for signing are just setup args
|
|
let contract_sign_args = args.to_api_args();
|
|
|
|
// Decrypt the slate, sign it and encrypt it for the next party
|
|
// TODO: Make sure you get the counterparty_addr and slate with 1 call.
|
|
let slatepack = owner::decode_slatepack_message(
|
|
api.wallet_inst.clone(),
|
|
keychain_mask,
|
|
String::from(slatepack_msg.clone()),
|
|
vec![0],
|
|
)?;
|
|
|
|
// Encrypt the reply for --encrypt-for if given, else for the incoming slatepack's
|
|
// sender. If neither is known the incoming slate was plaintext, so reply plaintext
|
|
// (empty dest -> unencrypted slatepack).
|
|
let counterparty_addr = if let Some(addr) = args.counterparty_addr {
|
|
addr
|
|
} else if let Some(sender) = slatepack.sender {
|
|
String::try_from(&sender)?
|
|
} else {
|
|
String::new()
|
|
};
|
|
let mut slate = owner::slate_from_slatepack_message(
|
|
api.wallet_inst.clone(),
|
|
keychain_mask,
|
|
String::from(slatepack_msg),
|
|
vec![0],
|
|
)?;
|
|
|
|
slate = api.contract_sign(m, &slate, &contract_sign_args)?;
|
|
|
|
print_slatepack(
|
|
api,
|
|
keychain_mask,
|
|
&slate,
|
|
&counterparty_addr,
|
|
args.outfile,
|
|
args.as_json,
|
|
);
|
|
|
|
if broadcast_tx {
|
|
let is_finalized = can_finalize(&slate);
|
|
if is_finalized {
|
|
api.post_tx(keychain_mask, &slate, true)?;
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
})?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub struct ContractRevokeArgs {
|
|
/// Id of a transaction we want to cancel
|
|
pub tx_id: u32,
|
|
}
|
|
|
|
pub fn contract_revoke<L, C, K>(
|
|
owner_api: &mut Owner<L, C, K>,
|
|
keychain_mask: Option<&SecretKey>,
|
|
args: ContractRevokeArgs,
|
|
) -> Result<(), Error>
|
|
where
|
|
L: WalletLCProvider<'static, C, K>,
|
|
C: NodeClient + 'static,
|
|
K: keychain::Keychain + 'static,
|
|
{
|
|
controller::owner_single_use(None, keychain_mask, Some(owner_api), |api, m| {
|
|
let slate_opt = api.contract_revoke(m, &ContractRevokeArgsAPI { tx_id: args.tx_id })?;
|
|
// TODO: replace dest="nope" with our own address and add --as-json support
|
|
if slate_opt.is_some() {
|
|
let slate_out =
|
|
prepare_slatepack(api, keychain_mask, &slate_opt.unwrap(), "nope", None)?;
|
|
println!("{}", slate_out);
|
|
}
|
|
|
|
Ok(())
|
|
})?;
|
|
|
|
Ok(())
|
|
}
|