diff --git a/bip39bug.md b/bip39bug.md
new file mode 100644
index 0000000..bb62319
--- /dev/null
+++ b/bip39bug.md
@@ -0,0 +1,13 @@
+## Zecwallet-cli BIP39 derivation bug
+
+In v1.0 of zecwallet-cli, there was a bug that incorrectly derived HD wallet keys after the first key. That is, the first key, address was correct, but subsequent ones were not.
+
+The issue was that the 32-byte seed was directly being used to derive then subsequent addresses instead of the 64-byte pkdf2(seed). The issue affected both t and z addresses.
+
+Note that no funds are at risk. The issue is that, if in the future, you import the seed into a different wallet, you might not see all your addresses in the new wallet, so it's better to fix it now.
+
+## Fix
+If you start a wallet that has this bug, you'll be notified.
+The bug can be automatically fixed by the wallet by running the `fixbip39bug` command. Just start `zecwallet-cli` and type `fixbip39bug`.
+
+If you have any funds in the incorrect addresses, they'll be sent to yourself, and the correct addresses re-derived.
\ No newline at end of file
diff --git a/lib/Cargo.toml b/lib/Cargo.toml
index 5c7a0f4..b7d7632 100644
--- a/lib/Cargo.toml
+++ b/lib/Cargo.toml
@@ -34,37 +34,38 @@ webpki-roots = "0.16.0"
tower-h2 = { git = "https://github.com/tower-rs/tower-h2" }
rust-embed = { version = "5.1.0", features = ["debug-embed"] }
rand = "0.7.2"
+sodiumoxide = "0.2.5"
[dependencies.bellman]
git = "https://github.com/DenioD/librustzcash.git"
-rev= "af5f25e87347e9ac42f29c37d43250e9de5c21de"
+rev= "44a1c3981df37b73d87f0e625ca3557d8534e6f3"
default-features = false
features = ["groth16"]
[dependencies.pairing]
git = "https://github.com/DenioD/librustzcash.git"
-rev= "af5f25e87347e9ac42f29c37d43250e9de5c21de"
+rev= "44a1c3981df37b73d87f0e625ca3557d8534e6f3"
[dependencies.zcash_client_backend]
git = "https://github.com/DenioD/librustzcash.git"
-rev= "af5f25e87347e9ac42f29c37d43250e9de5c21de"
+rev= "44a1c3981df37b73d87f0e625ca3557d8534e6f3"
default-features = false
[dependencies.zcash_primitives]
git = "https://github.com/DenioD/librustzcash.git"
-rev= "af5f25e87347e9ac42f29c37d43250e9de5c21de"
+rev= "44a1c3981df37b73d87f0e625ca3557d8534e6f3"
default-features = false
features = ["transparent-inputs"]
[dependencies.zcash_proofs]
git = "https://github.com/DenioD/librustzcash.git"
-rev= "af5f25e87347e9ac42f29c37d43250e9de5c21de"
+rev= "44a1c3981df37b73d87f0e625ca3557d8534e6f3"
default-features = false
[dependencies.ff]
git = "https://github.com/DenioD/librustzcash.git"
-rev= "af5f25e87347e9ac42f29c37d43250e9de5c21de"
+rev= "44a1c3981df37b73d87f0e625ca3557d8534e6f3"
features = ["ff_derive"]
[build-dependencies]
diff --git a/lib/src/commands.rs b/lib/src/commands.rs
index bfa2806..7a2120a 100644
--- a/lib/src/commands.rs
+++ b/lib/src/commands.rs
@@ -196,11 +196,131 @@ impl Command for ExportCommand {
}
let address = if args.is_empty() { None } else { Some(args[0].to_string()) };
-
- format!("{}", lightclient.do_export(address).pretty(2))
+ match lightclient.do_export(address) {
+ Ok(j) => j,
+ Err(e) => object!{ "error" => e }
+ }.pretty(2)
}
}
+struct EncryptCommand {}
+impl Command for EncryptCommand {
+ fn help(&self) -> String {
+ let mut h = vec![];
+ h.push("Encrypt the wallet with a password");
+ h.push("Note 1: This will encrypt the seed and the sapling and transparent private keys.");
+ h.push(" Use 'unlock' to temporarily unlock the wallet for spending or 'decrypt' ");
+ h.push(" to permanatly remove the encryption");
+ h.push("Note 2: If you forget the password, the only way to recover the wallet is to restore");
+ h.push(" from the seed phrase.");
+ h.push("Usage:");
+ h.push("encrypt password");
+ h.push("");
+ h.push("Example:");
+ h.push("encrypt my_strong_password");
+
+ h.join("\n")
+ }
+
+ fn short_help(&self) -> String {
+ "Encrypt the wallet with a password".to_string()
+ }
+
+ fn exec(&self, args: &[&str], lightclient: &LightClient) -> String {
+ if args.len() != 1 {
+ return self.help();
+ }
+
+ let passwd = args[0].to_string();
+
+ match lightclient.wallet.write().unwrap().encrypt(passwd) {
+ Ok(_) => object!{ "result" => "success" },
+ Err(e) => object!{
+ "result" => "error",
+ "error" => e.to_string()
+ }
+ }.pretty(2)
+ }
+}
+
+struct DecryptCommand {}
+impl Command for DecryptCommand {
+ fn help(&self) -> String {
+ let mut h = vec![];
+ h.push("Completely remove wallet encryption, storing the wallet in plaintext on disk");
+ h.push("Note 1: This will decrypt the seed and the sapling and transparent private keys and store them on disk.");
+ h.push(" Use 'unlock' to temporarily unlock the wallet for spending");
+ h.push("Note 2: If you've forgotten the password, the only way to recover the wallet is to restore");
+ h.push(" from the seed phrase.");
+ h.push("Usage:");
+ h.push("decrypt password");
+ h.push("");
+ h.push("Example:");
+ h.push("decrypt my_strong_password");
+
+ h.join("\n")
+ }
+
+ fn short_help(&self) -> String {
+ "Completely remove wallet encryption".to_string()
+ }
+
+ fn exec(&self, args: &[&str], lightclient: &LightClient) -> String {
+ if args.len() != 1 {
+ return self.help();
+ }
+
+ let passwd = args[0].to_string();
+
+ match lightclient.wallet.write().unwrap().remove_encryption(passwd) {
+ Ok(_) => object!{ "result" => "success" },
+ Err(e) => object!{
+ "result" => "error",
+ "error" => e.to_string()
+ }
+ }.pretty(2)
+ }
+}
+
+
+struct UnlockCommand {}
+impl Command for UnlockCommand {
+ fn help(&self) -> String {
+ let mut h = vec![];
+ h.push("Unlock the wallet's encryption in memory, allowing spending from this wallet.");
+ h.push("Note 1: This will decrypt spending keys in memory only. The wallet remains encrypted on disk");
+ h.push(" Use 'decrypt' to remove the encryption permanatly.");
+ h.push("Note 2: If you've forgotten the password, the only way to recover the wallet is to restore");
+ h.push(" from the seed phrase.");
+ h.push("Usage:");
+ h.push("unlock password");
+ h.push("");
+ h.push("Example:");
+ h.push("unlock my_strong_password");
+
+ h.join("\n")
+ }
+
+ fn short_help(&self) -> String {
+ "Unlock wallet encryption for spending".to_string()
+ }
+
+ fn exec(&self, args: &[&str], lightclient: &LightClient) -> String {
+ if args.len() != 1 {
+ return self.help();
+ }
+
+ let passwd = args[0].to_string();
+
+ match lightclient.wallet.write().unwrap().unlock(passwd) {
+ Ok(_) => object!{ "result" => "success" },
+ Err(e) => object!{
+ "result" => "error",
+ "error" => e.to_string()
+ }
+ }.pretty(2)
+ }
+}
struct SendCommand {}
impl Command for SendCommand {
@@ -208,7 +328,9 @@ impl Command for SendCommand {
let mut h = vec![];
h.push("Send HUSH to a given address");
h.push("Usage:");
- h.push("send
\"optional_memo\"");
+ h.push("send \"optional_memo\"");
+ h.push("OR");
+ h.push("send '[{'address': , 'amount': , 'memo': }, ...]'");
h.push("");
h.push("Example:");
h.push("send ztestsapling1x65nq4dgp0qfywgxcwk9n0fvm4fysmapgr2q00p85ju252h6l7mmxu2jg9cqqhtvzd69jwhgv8d 200000 \"Hello from the command line\"");
@@ -222,25 +344,69 @@ impl Command for SendCommand {
}
fn exec(&self, args: &[&str], lightclient: &LightClient) -> String {
- // Parse the args.
+ // Parse the args. There are two argument types.
+ // 1 - A set of 2(+1 optional) arguments for a single address send representing address, value, memo?
+ // 2 - A single argument in the form of a JSON string that is "[{address: address, value: value, memo: memo},...]"
+
// 1 - Destination address. T or Z address
- if args.len() < 2 || args.len() > 3 {
+ if args.len() < 1 || args.len() > 3 {
return self.help();
}
- // Make sure we can parse the amount
- let value = match args[1].parse::() {
- Ok(amt) => amt,
- Err(e) => {
- return format!("Couldn't parse amount: {}", e);;
+ // Check for a single argument that can be parsed as JSON
+ if args.len() == 1 {
+ // Sometimes on the command line, people use "'" for the quotes, which json::parse doesn't
+ // understand. So replace it with double-quotes
+ let arg_list = args[0].replace("'", "\"");
+
+ let json_args = match json::parse(&arg_list) {
+ Ok(j) => j,
+ Err(e) => {
+ let es = format!("Couldn't understand JSON: {}", e);
+ return format!("{}\n{}", es, self.help());
+ }
+ };
+
+ if !json_args.is_array() {
+ return format!("Couldn't parse argument as array\n{}", self.help());
}
- };
- let memo = if args.len() == 3 { Some(args[2].to_string()) } else {None};
-
- lightclient.do_sync(true);
+ let maybe_send_args = json_args.members().map( |j| {
+ if !j.has_key("address") || !j.has_key("amount") {
+ Err(format!("Need 'address' and 'amount'\n"))
+ } else {
+ Ok((j["address"].as_str().unwrap(), j["amount"].as_u64().unwrap(), j["memo"].as_str().map(|s| s.to_string())))
+ }
+ }).collect::)>, String>>();
- lightclient.do_send(args[0], value, memo)
+ let send_args = match maybe_send_args {
+ Ok(a) => a,
+ Err(s) => { return format!("Error: {}\n{}", s, self.help()); }
+ };
+
+ lightclient.do_sync(true);
+ match lightclient.do_send(send_args) {
+ Ok(txid) => { object!{ "txid" => txid } },
+ Err(e) => { object!{ "error" => e } }
+ }.pretty(2)
+ } else if args.len() == 2 || args.len() == 3 {
+ // Make sure we can parse the amount
+ let value = match args[1].parse::() {
+ Ok(amt) => amt,
+ Err(e) => {
+ return format!("Couldn't parse amount: {}", e);
+ }
+ };
+
+ let memo = if args.len() == 3 { Some(args[2].to_string()) } else {None};
+ lightclient.do_sync(true);
+ match lightclient.do_send(vec!((args[0], value, memo))) {
+ Ok(txid) => { object!{ "txid" => txid } },
+ Err(e) => { object!{ "error" => e } }
+ }.pretty(2)
+ } else {
+ self.help()
+ }
}
}
@@ -263,7 +429,19 @@ impl Command for SaveCommand {
}
fn exec(&self, _args: &[&str], lightclient: &LightClient) -> String {
- lightclient.do_save()
+ match lightclient.do_save() {
+ Ok(_) => {
+ let r = object!{ "result" => "success" };
+ r.pretty(2)
+ },
+ Err(e) => {
+ let r = object!{
+ "result" => "error",
+ "error" => e
+ };
+ r.pretty(2)
+ }
+ }
}
}
@@ -285,7 +463,10 @@ impl Command for SeedCommand {
}
fn exec(&self, _args: &[&str], lightclient: &LightClient) -> String {
- format!("{}", lightclient.do_seed_phrase().pretty(2))
+ match lightclient.do_seed_phrase() {
+ Ok(j) => j,
+ Err(e) => object!{ "error" => e }
+ }.pretty(2)
}
}
@@ -360,7 +541,10 @@ impl Command for NewAddressCommand {
return format!("No address type specified\n{}", self.help());
}
- format!("{}", lightclient.do_new_address(args[0]).pretty(2))
+ match lightclient.do_new_address(args[0]) {
+ Ok(j) => j,
+ Err(e) => object!{ "error" => e }
+ }.pretty(2)
}
}
@@ -403,6 +587,28 @@ impl Command for NotesCommand {
}
}
+struct FixBip39BugCommand {}
+impl Command for FixBip39BugCommand {
+ fn help(&self) -> String {
+ let mut h = vec![];
+ h.push("Detect if the wallet has the Bip39 derivation bug, and fix it automatically");
+ h.push("Usage:");
+ h.push("fixbip39bug");
+ h.push("");
+
+ h.join("\n")
+ }
+
+ fn short_help(&self) -> String {
+ "Detect if the wallet has the Bip39 derivation bug, and fix it automatically".to_string()
+ }
+
+ fn exec(&self, _args: &[&str], lightclient: &LightClient) -> String {
+ use crate::lightwallet::bugs::BugBip39Derivation;
+
+ BugBip39Derivation::fix_bug(lightclient)
+ }
+}
struct QuitCommand {}
impl Command for QuitCommand {
@@ -421,28 +627,35 @@ impl Command for QuitCommand {
}
fn exec(&self, _args: &[&str], lightclient: &LightClient) -> String {
- lightclient.do_save()
+ match lightclient.do_save() {
+ Ok(_) => {"".to_string()},
+ Err(e) => e
+ }
}
}
pub fn get_commands() -> Box>> {
let mut map: HashMap> = HashMap::new();
- map.insert("sync".to_string(), Box::new(SyncCommand{}));
- map.insert("rescan".to_string(), Box::new(RescanCommand{}));
- map.insert("help".to_string(), Box::new(HelpCommand{}));
- map.insert("balance".to_string(), Box::new(BalanceCommand{}));
- map.insert("addresses".to_string(), Box::new(AddressCommand{}));
- map.insert("height".to_string(), Box::new(HeightCommand{}));
- map.insert("export".to_string(), Box::new(ExportCommand{}));
- map.insert("info".to_string(), Box::new(InfoCommand{}));
- map.insert("send".to_string(), Box::new(SendCommand{}));
- map.insert("save".to_string(), Box::new(SaveCommand{}));
- map.insert("quit".to_string(), Box::new(QuitCommand{}));
- map.insert("list".to_string(), Box::new(TransactionsCommand{}));
- map.insert("notes".to_string(), Box::new(NotesCommand{}));
- map.insert("new".to_string(), Box::new(NewAddressCommand{}));
- map.insert("seed".to_string(), Box::new(SeedCommand{}));
+ map.insert("sync".to_string(), Box::new(SyncCommand{}));
+ map.insert("rescan".to_string(), Box::new(RescanCommand{}));
+ map.insert("help".to_string(), Box::new(HelpCommand{}));
+ map.insert("balance".to_string(), Box::new(BalanceCommand{}));
+ map.insert("addresses".to_string(), Box::new(AddressCommand{}));
+ map.insert("height".to_string(), Box::new(HeightCommand{}));
+ map.insert("export".to_string(), Box::new(ExportCommand{}));
+ map.insert("info".to_string(), Box::new(InfoCommand{}));
+ map.insert("send".to_string(), Box::new(SendCommand{}));
+ map.insert("save".to_string(), Box::new(SaveCommand{}));
+ map.insert("quit".to_string(), Box::new(QuitCommand{}));
+ map.insert("list".to_string(), Box::new(TransactionsCommand{}));
+ map.insert("notes".to_string(), Box::new(NotesCommand{}));
+ map.insert("new".to_string(), Box::new(NewAddressCommand{}));
+ map.insert("seed".to_string(), Box::new(SeedCommand{}));
+ map.insert("encrypt".to_string(), Box::new(EncryptCommand{}));
+ map.insert("decrypt".to_string(), Box::new(DecryptCommand{}));
+ map.insert("unlock".to_string(), Box::new(UnlockCommand{}));
+ map.insert("fixbip39bug".to_string(), Box::new(FixBip39BugCommand{}));
Box::new(map)
}
diff --git a/lib/src/grpcconnector.rs b/lib/src/grpcconnector.rs
index 03bef21..10d0d88 100644
--- a/lib/src/grpcconnector.rs
+++ b/lib/src/grpcconnector.rs
@@ -1,4 +1,3 @@
-
use log::{error};
use std::sync::{Arc};
@@ -280,7 +279,12 @@ pub fn broadcast_raw_tx(uri: &http::Uri, no_cert: bool, tx_bytes: Box<[u8]>) ->
.and_then(move |response| {
let sendresponse = response.into_inner();
if sendresponse.error_code == 0 {
- Ok(format!("Successfully broadcast Tx: {}", sendresponse.error_message))
+ let mut txid = sendresponse.error_message;
+ if txid.starts_with("\"") && txid.ends_with("\"") {
+ txid = txid[1..txid.len()-1].to_string();
+ }
+
+ Ok(txid)
} else {
Err(format!("Error: {:?}", sendresponse))
}
diff --git a/lib/src/lib.rs b/lib/src/lib.rs
index 3e4fe27..13672b7 100644
--- a/lib/src/lib.rs
+++ b/lib/src/lib.rs
@@ -1,6 +1,7 @@
#[macro_use]
extern crate rust_embed;
+pub mod startup_helpers;
pub mod lightclient;
pub mod grpcconnector;
pub mod lightwallet;
diff --git a/lib/src/lightclient.rs b/lib/src/lightclient.rs
index 17c374d..69d47fc 100644
--- a/lib/src/lightclient.rs
+++ b/lib/src/lightclient.rs
@@ -7,10 +7,13 @@ use std::sync::{Arc, RwLock};
use std::sync::atomic::{AtomicU64, AtomicI32, AtomicUsize, Ordering};
use std::path::Path;
use std::fs::File;
+use std::collections::HashMap;
use std::io;
use std::io::prelude::*;
use std::io::{BufReader, BufWriter, Error, ErrorKind};
+use protobuf::parse_from_bytes;
+
use json::{object, array, JsonValue};
use zcash_primitives::transaction::{TxId, Transaction};
use zcash_client_backend::{
@@ -23,8 +26,8 @@ use crate::SaplingParams;
use crate::ANCHOR_OFFSET;
pub const DEFAULT_SERVER: &str = "https://";
-pub const WALLET_NAME: &str = "silentdragonlite-cli.dat";
-pub const LOGFILE_NAME: &str = "silentdragonlite-cli.debug.log";
+pub const WALLET_NAME: &str = "silentdragonlite-cli-wallet.dat";
+pub const LOGFILE_NAME: &str = "silentdragonlite-cli-wallet.debug.log";
#[derive(Clone, Debug)]
@@ -39,6 +42,18 @@ pub struct LightClientConfig {
impl LightClientConfig {
+ // Create an unconnected (to any server) config to test for local wallet etc...
+ pub fn create_unconnected(chain_name: String) -> LightClientConfig {
+ LightClientConfig {
+ server : http::Uri::default(),
+ chain_name : chain_name,
+ sapling_activation_height : 0,
+ consensus_branch_id : "".to_string(),
+ anchor_offset : ANCHOR_OFFSET,
+ no_cert_verification : false,
+ }
+ }
+
pub fn create(server: http::Uri, dangerous: bool) -> io::Result<(LightClientConfig, u64)> {
// Do a getinfo first, before opening the wallet
let info = grpcconnector::get_info(server.clone(), dangerous)
@@ -64,7 +79,7 @@ impl LightClientConfig {
zcash_data_location.push("HUSH3");
} else {
zcash_data_location = dirs::home_dir().expect("Couldn't determine home directory!");
- zcash_data_location.push(".komodo/HUSH3/SilentDragonLite/");
+ zcash_data_location.push(".komodo/HUSH3/");
};
match &self.chain_name[..] {
@@ -84,6 +99,10 @@ impl LightClientConfig {
wallet_location.into_boxed_path()
}
+ pub fn wallet_exists(&self) -> bool {
+ return self.get_wallet_path().exists()
+ }
+
pub fn get_log_path(&self) -> Box {
let mut log_path = self.get_zcash_data_path().into_path_buf();
log_path.push(LOGFILE_NAME);
@@ -99,7 +118,7 @@ impl LightClientConfig {
)),
"main" => Some((105944,
"0000000313b0ec7c5a1e9b997ce44a7763b56c5505526c36634a004ed52d7787",
- ""
+ ""
)),
_ => None
}
@@ -149,8 +168,8 @@ impl LightClientConfig {
pub fn base58_pubkey_address(&self) -> [u8; 1] {
match &self.chain_name[..] {
"main" => mainnet::B58_PUBKEY_ADDRESS_PREFIX,
-
-
+
+
c => panic!("Unknown chain {}", c)
}
}
@@ -160,7 +179,7 @@ impl LightClientConfig {
match &self.chain_name[..] {
"main" => mainnet::B58_SCRIPT_ADDRESS_PREFIX,
-
+
c => panic!("Unknown chain {}", c)
}
}
@@ -176,7 +195,7 @@ impl LightClientConfig {
}
pub struct LightClient {
- pub wallet : Arc,
+ pub wallet : Arc>,
pub config : LightClientConfig,
@@ -193,63 +212,106 @@ impl LightClient {
let state = self.config.get_initial_state();
match state {
- Some((height, hash, tree)) => self.wallet.set_initial_block(height.try_into().unwrap(), hash, tree),
+ Some((height, hash, tree)) => self.wallet.read().unwrap().set_initial_block(height.try_into().unwrap(), hash, tree),
_ => true,
};
}
- pub fn new(seed_phrase: Option, config: &LightClientConfig, latest_block: u64) -> io::Result {
- let mut lc = if config.get_wallet_path().exists() {
- // Make sure that if a wallet exists, there is no seed phrase being attempted
- if !seed_phrase.is_none() {
- return Err(Error::new(ErrorKind::AlreadyExists,
- "Cannot create a new wallet from seed, because a wallet already exists"));
- }
+ fn read_sapling_params(&mut self) {
+ // Read Sapling Params
+ self.sapling_output.extend_from_slice(SaplingParams::get("sapling-output.params").unwrap().as_ref());
+ self.sapling_spend.extend_from_slice(SaplingParams::get("sapling-spend.params").unwrap().as_ref());
- let mut file_buffer = BufReader::new(File::open(config.get_wallet_path())?);
-
- let wallet = LightWallet::read(&mut file_buffer, config)?;
- LightClient {
- wallet : Arc::new(wallet),
- config : config.clone(),
- sapling_output : vec![],
- sapling_spend : vec![]
- }
- } else {
- let l = LightClient {
- wallet : Arc::new(LightWallet::new(seed_phrase, config, latest_block)?),
+ }
+
+ /// Method to create a test-only version of the LightClient
+ #[allow(dead_code)]
+ fn unconnected(seed_phrase: String) -> io::Result {
+ let config = LightClientConfig::create_unconnected("test".to_string());
+ let mut l = LightClient {
+ wallet : Arc::new(RwLock::new(LightWallet::new(Some(seed_phrase), &config, 0)?)),
config : config.clone(),
sapling_output : vec![],
sapling_spend : vec![]
};
- l.set_wallet_initial_state();
+ l.set_wallet_initial_state();
+ l.read_sapling_params();
- l
+ info!("Created new wallet!");
+ info!("Created LightClient to {}", &config.server);
+
+ Ok(l)
+ }
+
+ pub fn new_from_phrase(seed_phrase: String, config: &LightClientConfig, latest_block: u64) -> io::Result {
+ if config.get_wallet_path().exists() {
+ return Err(Error::new(ErrorKind::AlreadyExists,
+ "Cannot create a new wallet from seed, because a wallet already exists"));
+ }
+
+ let mut l = LightClient {
+ wallet : Arc::new(RwLock::new(LightWallet::new(Some(seed_phrase), config, latest_block)?)),
+ config : config.clone(),
+ sapling_output : vec![],
+ sapling_spend : vec![]
+ };
+
+ l.set_wallet_initial_state();
+ l.read_sapling_params();
+
+ info!("Created new wallet!");
+ info!("Created LightClient to {}", &config.server);
+
+ Ok(l)
+ }
+
+ pub fn read_from_disk(config: &LightClientConfig) -> io::Result {
+ if !config.get_wallet_path().exists() {
+ return Err(Error::new(ErrorKind::AlreadyExists,
+ format!("Cannot read wallet. No file at {}", config.get_wallet_path().display())));
+ }
+
+ let mut file_buffer = BufReader::new(File::open(config.get_wallet_path())?);
+
+ let wallet = LightWallet::read(&mut file_buffer, config)?;
+ let mut lc = LightClient {
+ wallet : Arc::new(RwLock::new(wallet)),
+ config : config.clone(),
+ sapling_output : vec![],
+ sapling_spend : vec![]
};
- info!("Read wallet with birthday {}", lc.wallet.get_first_tx_block());
-
- // Read Sapling Params
- lc.sapling_output.extend_from_slice(SaplingParams::get("sapling-output.params").unwrap().as_ref());
- lc.sapling_spend.extend_from_slice(SaplingParams::get("sapling-spend.params").unwrap().as_ref());
+ lc.read_sapling_params();
+ info!("Read wallet with birthday {}", lc.wallet.read().unwrap().get_first_tx_block());
info!("Created LightClient to {}", &config.server);
+ if crate::lightwallet::bugs::BugBip39Derivation::has_bug(&lc) {
+ let m = format!("WARNING!!!\nYour wallet has a bip39derivation bug that's showing incorrect addresses.\nPlease run 'fixbip39bug' to automatically fix the address derivation in your wallet!\nPlease see: https://github.com/adityapk00/zecwallet-light-cli/blob/master/bip39bug.md");
+ info!("{}", m);
+ println!("{}", m);
+ }
+
Ok(lc)
}
pub fn last_scanned_height(&self) -> u64 {
- self.wallet.last_scanned_height() as u64
+ self.wallet.read().unwrap().last_scanned_height() as u64
}
// Export private keys
- pub fn do_export(&self, addr: Option) -> JsonValue {
+ pub fn do_export(&self, addr: Option) -> Result {
+ if !self.wallet.read().unwrap().is_unlocked_for_spending() {
+ error!("Wallet is locked");
+ return Err("Wallet is locked");
+ }
+
// Clone address so it can be moved into the closure
let address = addr.clone();
-
+ let wallet = self.wallet.read().unwrap();
// Go over all z addresses
- let z_keys = self.wallet.get_z_private_keys().iter()
+ let z_keys = wallet.get_z_private_keys().iter()
.filter( move |(addr, _)| address.is_none() || address.as_ref() == Some(addr))
.map( |(addr, pk)|
object!{
@@ -262,7 +324,7 @@ impl LightClient {
let address = addr.clone();
// Go over all t addresses
- let t_keys = self.wallet.get_t_secret_keys().iter()
+ let t_keys = wallet.get_t_secret_keys().iter()
.filter( move |(addr, _)| address.is_none() || address.as_ref() == Some(addr))
.map( |(addr, sk)|
object!{
@@ -275,19 +337,20 @@ impl LightClient {
all_keys.extend_from_slice(&z_keys);
all_keys.extend_from_slice(&t_keys);
- all_keys.into()
+ Ok(all_keys.into())
}
pub fn do_address(&self) -> JsonValue {
+ let wallet = self.wallet.read().unwrap();
+
// Collect z addresses
- let z_addresses = self.wallet.address.read().unwrap().iter().map( |ad| {
+ let z_addresses = wallet.zaddress.read().unwrap().iter().map( |ad| {
encode_payment_address(self.config.hrp_sapling_address(), &ad)
}).collect::>();
// Collect t addresses
- let t_addresses = self.wallet.tkeys.read().unwrap().iter().map( |sk| {
- self.wallet.address_from_sk(&sk)
- }).collect::>();
+ let t_addresses = wallet.taddresses.read().unwrap().iter().map( |a| a.clone() )
+ .collect::>();
object!{
"z_addresses" => z_addresses,
@@ -296,47 +359,66 @@ impl LightClient {
}
pub fn do_balance(&self) -> JsonValue {
+ let wallet = self.wallet.read().unwrap();
+
// Collect z addresses
- let z_addresses = self.wallet.address.read().unwrap().iter().map( |ad| {
+ let z_addresses = wallet.zaddress.read().unwrap().iter().map( |ad| {
let address = encode_payment_address(self.config.hrp_sapling_address(), &ad);
object!{
"address" => address.clone(),
- "zbalance" => self.wallet.zbalance(Some(address.clone())),
- "verified_zbalance" => self.wallet.verified_zbalance(Some(address)),
+ "zbalance" => wallet.zbalance(Some(address.clone())),
+ "verified_zbalance" => wallet.verified_zbalance(Some(address)),
}
}).collect::>();
// Collect t addresses
- let t_addresses = self.wallet.tkeys.read().unwrap().iter().map( |sk| {
- let address = self.wallet.address_from_sk(&sk);
-
+ let t_addresses = wallet.taddresses.read().unwrap().iter().map( |address| {
// Get the balance for this address
- let balance = self.wallet.tbalance(Some(address.clone()));
+ let balance = wallet.tbalance(Some(address.clone()));
object!{
- "address" => address,
+ "address" => address.clone(),
"balance" => balance,
}
}).collect::>();
object!{
- "zbalance" => self.wallet.zbalance(None),
- "verified_zbalance" => self.wallet.verified_zbalance(None),
- "tbalance" => self.wallet.tbalance(None),
+ "zbalance" => wallet.zbalance(None),
+ "verified_zbalance" => wallet.verified_zbalance(None),
+ "tbalance" => wallet.tbalance(None),
"z_addresses" => z_addresses,
"t_addresses" => t_addresses,
}
}
- pub fn do_save(&self) -> String {
+ pub fn do_save(&self) -> Result<(), String> {
+ // If the wallet is encrypted but unlocked, lock it again.
+ {
+ let mut wallet = self.wallet.write().unwrap();
+ if wallet.is_encrypted() && wallet.is_unlocked_for_spending() {
+ match wallet.lock() {
+ Ok(_) => {},
+ Err(e) => {
+ let err = format!("ERR: {}", e);
+ error!("{}", err);
+ return Err(e.to_string());
+ }
+ }
+ }
+ }
+
let mut file_buffer = BufWriter::with_capacity(
1_000_000, // 1 MB write buffer
File::create(self.config.get_wallet_path()).unwrap());
- self.wallet.write(&mut file_buffer).unwrap();
- info!("Saved wallet");
-
- format!("Saved Wallet")
+ match self.wallet.write().unwrap().write(&mut file_buffer) {
+ Ok(_) => Ok(()),
+ Err(e) => {
+ let err = format!("ERR: {}", e);
+ error!("{}", err);
+ Err(e.to_string())
+ }
+ }
}
pub fn get_server_uri(&self) -> http::Uri {
@@ -361,11 +443,17 @@ impl LightClient {
}
}
- pub fn do_seed_phrase(&self) -> JsonValue {
- object!{
- "seed" => self.wallet.get_seed_phrase(),
- "birthday" => self.wallet.get_birthday()
+ pub fn do_seed_phrase(&self) -> Result {
+ if !self.wallet.read().unwrap().is_unlocked_for_spending() {
+ error!("Wallet is locked");
+ return Err("Wallet is locked");
}
+
+ let wallet = self.wallet.read().unwrap();
+ Ok(object!{
+ "seed" => wallet.get_seed_phrase(),
+ "birthday" => wallet.get_birthday()
+ })
}
// Return a list of all notes, spent and unspent
@@ -374,108 +462,95 @@ impl LightClient {
let mut spent_notes : Vec = vec![];
let mut pending_notes: Vec = vec![];
- // Collect Sapling notes
- self.wallet.txs.read().unwrap().iter()
- .flat_map( |(txid, wtx)| {
- wtx.notes.iter().filter_map(move |nd|
- if !all_notes && nd.spent.is_some() {
- None
+ {
+ // Collect Sapling notes
+ let wallet = self.wallet.read().unwrap();
+ wallet.txs.read().unwrap().iter()
+ .flat_map( |(txid, wtx)| {
+ wtx.notes.iter().filter_map(move |nd|
+ if !all_notes && nd.spent.is_some() {
+ None
+ } else {
+ Some(object!{
+ "created_in_block" => wtx.block,
+ "datetime" => wtx.datetime,
+ "created_in_txid" => format!("{}", txid),
+ "value" => nd.note.value,
+ "is_change" => nd.is_change,
+ "address" => LightWallet::note_address(self.config.hrp_sapling_address(), nd),
+ "spent" => nd.spent.map(|spent_txid| format!("{}", spent_txid)),
+ "unconfirmed_spent" => nd.unconfirmed_spent.map(|spent_txid| format!("{}", spent_txid)),
+ })
+ }
+ )
+ })
+ .for_each( |note| {
+ if note["spent"].is_null() && note["unconfirmed_spent"].is_null() {
+ unspent_notes.push(note);
+ } else if !note["spent"].is_null() {
+ spent_notes.push(note);
} else {
- Some(object!{
- "created_in_block" => wtx.block,
- "created_in_txid" => format!("{}", txid),
- "value" => nd.note.value,
- "is_change" => nd.is_change,
- "address" => self.wallet.note_address(nd),
- "spent" => nd.spent.map(|spent_txid| format!("{}", spent_txid)),
- "unconfirmed_spent" => nd.unconfirmed_spent.map(|spent_txid| format!("{}", spent_txid)),
- })
+ pending_notes.push(note);
}
- )
- })
- .for_each( |note| {
- if note["spent"].is_null() && note["unconfirmed_spent"].is_null() {
- unspent_notes.push(note);
- } else if !note["spent"].is_null() {
- spent_notes.push(note);
- } else {
- pending_notes.push(note);
- }
- });
+ });
+ }
- // Collect UTXOs
- let utxos = self.wallet.get_utxos().iter()
- .filter(|utxo| utxo.unconfirmed_spent.is_none()) // Filter out unconfirmed from the list of utxos
- .map(|utxo| {
- object!{
- "created_in_block" => utxo.height,
- "created_in_txid" => format!("{}", utxo.txid),
- "value" => utxo.value,
- "scriptkey" => hex::encode(utxo.script.clone()),
- "is_change" => false, // TODO: Identify notes as change if we send change to taddrs
- "address" => utxo.address.clone(),
- "spent" => utxo.spent.map(|spent_txid| format!("{}", spent_txid)),
- "unconfirmed_spent" => utxo.unconfirmed_spent.map(|spent_txid| format!("{}", spent_txid)),
- }
- })
- .collect::>();
-
- // Collect pending UTXOs
- let pending_utxos = self.wallet.get_utxos().iter()
- .filter(|utxo| utxo.unconfirmed_spent.is_some()) // Filter to include only unconfirmed utxos
- .map(|utxo|
- object!{
- "created_in_block" => utxo.height,
- "created_in_txid" => format!("{}", utxo.txid),
- "value" => utxo.value,
- "scriptkey" => hex::encode(utxo.script.clone()),
- "is_change" => false, // TODO: Identify notes as change if we send change to taddrs
- "address" => utxo.address.clone(),
- "spent" => utxo.spent.map(|spent_txid| format!("{}", spent_txid)),
- "unconfirmed_spent" => utxo.unconfirmed_spent.map(|spent_txid| format!("{}", spent_txid)),
- }
- )
- .collect::>();;
+ let mut unspent_utxos: Vec = vec![];
+ let mut spent_utxos : Vec = vec![];
+ let mut pending_utxos: Vec = vec![];
+
+ {
+ let wallet = self.wallet.read().unwrap();
+ wallet.txs.read().unwrap().iter()
+ .flat_map( |(txid, wtx)| {
+ wtx.utxos.iter().filter_map(move |utxo|
+ if !all_notes && utxo.spent.is_some() {
+ None
+ } else {
+ Some(object!{
+ "created_in_block" => wtx.block,
+ "datetime" => wtx.datetime,
+ "created_in_txid" => format!("{}", txid),
+ "value" => utxo.value,
+ "scriptkey" => hex::encode(utxo.script.clone()),
+ "is_change" => false, // TODO: Identify notes as change if we send change to taddrs
+ "address" => utxo.address.clone(),
+ "spent" => utxo.spent.map(|spent_txid| format!("{}", spent_txid)),
+ "unconfirmed_spent" => utxo.unconfirmed_spent.map(|spent_txid| format!("{}", spent_txid)),
+ })
+ }
+ )
+ })
+ .for_each( |utxo| {
+ if utxo["spent"].is_null() && utxo["unconfirmed_spent"].is_null() {
+ unspent_utxos.push(utxo);
+ } else if !utxo["spent"].is_null() {
+ spent_utxos.push(utxo);
+ } else {
+ pending_utxos.push(utxo);
+ }
+ });
+ }
let mut res = object!{
"unspent_notes" => unspent_notes,
"pending_notes" => pending_notes,
- "utxos" => utxos,
+ "utxos" => unspent_utxos,
"pending_utxos" => pending_utxos,
};
if all_notes {
res["spent_notes"] = JsonValue::Array(spent_notes);
- }
-
- // If all notes, also add historical utxos
- if all_notes {
- res["spent_utxos"] = JsonValue::Array(self.wallet.txs.read().unwrap().values()
- .flat_map(|wtx| {
- wtx.utxos.iter()
- .filter(|utxo| utxo.spent.is_some())
- .map(|utxo| {
- object!{
- "created_in_block" => wtx.block,
- "created_in_txid" => format!("{}", utxo.txid),
- "value" => utxo.value,
- "scriptkey" => hex::encode(utxo.script.clone()),
- "is_change" => false, // TODO: Identify notes as change if we send change to taddrs
- "address" => utxo.address.clone(),
- "spent" => utxo.spent.map(|spent_txid| format!("{}", spent_txid)),
- "unconfirmed_spent" => utxo.unconfirmed_spent.map(|spent_txid| format!("{}", spent_txid)),
- }
- }).collect::>()
- }).collect::>()
- );
+ res["spent_utxos"] = JsonValue::Array(spent_utxos);
}
res
}
pub fn do_list_transactions(&self) -> JsonValue {
+ let wallet = self.wallet.read().unwrap();
// Create a list of TransactionItems
- let mut tx_list = self.wallet.txs.read().unwrap().iter()
+ let mut tx_list = wallet.txs.read().unwrap().iter()
.flat_map(| (_k, v) | {
let mut txns: Vec = vec![];
@@ -501,6 +576,7 @@ impl LightClient {
txns.push(object! {
"block_height" => v.block,
+ "datetime" => v.datetime,
"txid" => format!("{}", v.txid),
"amount" => total_change as i64
- v.total_shielded_value_spent as i64
@@ -515,9 +591,10 @@ impl LightClient {
.map ( |nd|
object! {
"block_height" => v.block,
+ "datetime" => v.datetime,
"txid" => format!("{}", v.txid),
"amount" => nd.note.value as i64,
- "address" => self.wallet.note_address(nd),
+ "address" => LightWallet::note_address(self.config.hrp_sapling_address(), nd),
"memo" => LightWallet::memo_str(&nd.memo),
})
);
@@ -528,6 +605,7 @@ impl LightClient {
// Create an input transaction for the transparent value as well.
txns.push(object!{
"block_height" => v.block,
+ "datetime" => v.datetime,
"txid" => format!("{}", v.txid),
"amount" => total_transparent_received as i64 - v.total_transparent_value_spent as i64,
"address" => v.utxos.iter().map(|u| u.address.clone()).collect::>().join(","),
@@ -550,26 +628,31 @@ impl LightClient {
}
/// Create a new address, deriving it from the seed.
- pub fn do_new_address(&self, addr_type: &str) -> JsonValue {
+ pub fn do_new_address(&self, addr_type: &str) -> Result {
+ if !self.wallet.read().unwrap().is_unlocked_for_spending() {
+ error!("Wallet is locked");
+ return Err("Wallet is locked".to_string());
+ }
+
+ let wallet = self.wallet.write().unwrap();
+
let new_address = match addr_type {
- "z" => self.wallet.add_zaddr(),
- "t" => self.wallet.add_taddr(),
+ "z" => wallet.add_zaddr(),
+ "t" => wallet.add_taddr(),
_ => {
let e = format!("Unrecognized address type: {}", addr_type);
error!("{}", e);
- return object!{
- "error" => e
- };
+ return Err(e);
}
};
- array![new_address]
+ Ok(array![new_address])
}
pub fn do_rescan(&self) -> String {
info!("Rescan starting");
// First, clear the state from the wallet
- self.wallet.clear_blocks();
+ self.wallet.read().unwrap().clear_blocks();
// Then set the initial block
self.set_wallet_initial_state();
@@ -587,7 +670,7 @@ impl LightClient {
// 2. Get all the blocks that we don't have
// 3. Find all new Txns that don't have the full Tx, and get them as full transactions
// and scan them, mainly to get the memos
- let mut last_scanned_height = self.wallet.last_scanned_height() as u64;
+ let mut last_scanned_height = self.wallet.read().unwrap().last_scanned_height() as u64;
// This will hold the latest block fetched from the RPC
let latest_block_height = Arc::new(AtomicU64::new(0));
@@ -626,6 +709,10 @@ impl LightClient {
// Fetch CompactBlocks in increments
loop {
+ // Collect all block times, because we'll need to update transparent tx
+ // datetime via the block height timestamp
+ let block_times = Arc::new(RwLock::new(HashMap::new()));
+
let local_light_wallet = self.wallet.clone();
let local_bytes_downloaded = bytes_downloaded.clone();
@@ -640,7 +727,9 @@ impl LightClient {
// Fetch compact blocks
info!("Fetching blocks {}-{}", start_height, end_height);
+
let all_txs = all_new_txs.clone();
+ let block_times_inner = block_times.clone();
let last_invalid_height = Arc::new(AtomicI32::new(0));
let last_invalid_height_inner = last_invalid_height.clone();
@@ -651,8 +740,18 @@ impl LightClient {
return;
}
- match local_light_wallet.scan_block(encoded_block) {
+ let block: Result
+ = parse_from_bytes(encoded_block);
+ match block {
+ Ok(b) => {
+ block_times_inner.write().unwrap().insert(b.height, b.time);
+ },
+ Err(_) => {}
+ }
+
+ match local_light_wallet.read().unwrap().scan_block(encoded_block) {
Ok(block_txns) => {
+ // Add to global tx list
all_txs.write().unwrap().extend_from_slice(&block_txns.iter().map(|txid| (txid.clone(), height as i32)).collect::>()[..]);
},
Err(invalid_height) => {
@@ -667,7 +766,7 @@ impl LightClient {
// Check if there was any invalid block, which means we might have to do a reorg
let invalid_height = last_invalid_height.load(Ordering::SeqCst);
if invalid_height > 0 {
- total_reorg += self.wallet.invalidate_block(invalid_height);
+ total_reorg += self.wallet.read().unwrap().invalidate_block(invalid_height);
warn!("Invalidated block at height {}. Total reorg is now {}", invalid_height, total_reorg);
}
@@ -693,18 +792,28 @@ impl LightClient {
total_reorg = 0;
// We'll also fetch all the txids that our transparent addresses are involved with
- // TODO: Use for all t addresses
- let address = self.wallet.address_from_sk(&self.wallet.tkeys.read().unwrap()[0]);
- let wallet = self.wallet.clone();
- fetch_transparent_txids(&self.get_server_uri(), address, start_height, end_height, self.config.no_cert_verification,
- move |tx_bytes: &[u8], height: u64 | {
- let tx = Transaction::read(tx_bytes).unwrap();
+ {
+ // Copy over addresses so as to not lock up the wallet, which we'll use inside the callback below.
+ let addresses = self.wallet.read().unwrap()
+ .taddresses.read().unwrap().iter().map(|a| a.clone())
+ .collect::>();
+ for address in addresses {
+ let wallet = self.wallet.clone();
+ let block_times_inner = block_times.clone();
- // Scan this Tx for transparent inputs and outputs
- wallet.scan_full_tx(&tx, height as i32);
+ fetch_transparent_txids(&self.get_server_uri(), address, start_height, end_height, self.config.no_cert_verification,
+ move |tx_bytes: &[u8], height: u64| {
+ let tx = Transaction::read(tx_bytes).unwrap();
+
+ // Scan this Tx for transparent inputs and outputs
+ let datetime = block_times_inner.read().unwrap().get(&height).map(|v| *v).unwrap_or(0);
+ wallet.read().unwrap().scan_full_tx(&tx, height as i32, datetime as u64);
+ }
+ );
}
- );
+ }
+ // Do block height accounting
last_scanned_height = end_height;
end_height = last_scanned_height + 1000;
@@ -712,8 +821,9 @@ impl LightClient {
break;
} else if end_height > latest_block {
end_height = latest_block;
- }
+ }
}
+
if print_updates{
println!(""); // New line to finish up the updates
}
@@ -727,10 +837,10 @@ impl LightClient {
// We need to first copy over the Txids from the wallet struct, because
// we need to free the read lock from here (Because we'll self.wallet.txs later)
- let mut txids_to_fetch: Vec<(TxId, i32)> = self.wallet.txs.read().unwrap().values()
- .filter(|wtx| wtx.full_tx_scanned == false)
- .map(|wtx| (wtx.txid, wtx.block))
- .collect::>();
+ let mut txids_to_fetch: Vec<(TxId, i32)> = self.wallet.read().unwrap().txs.read().unwrap().values()
+ .filter(|wtx| wtx.full_tx_scanned == false)
+ .map(|wtx| (wtx.txid.clone(), wtx.block))
+ .collect::>();
info!("Fetching {} new txids, total {} with decoy", txids_to_fetch.len(), all_new_txs.read().unwrap().len());
txids_to_fetch.extend_from_slice(&all_new_txs.read().unwrap()[..]);
@@ -742,7 +852,6 @@ impl LightClient {
// And go and fetch the txids, getting the full transaction, so we can
// read the memos
-
for (txid, height) in txids_to_fetch {
let light_wallet_clone = self.wallet.clone();
info!("Fetching full Tx: {}", txid);
@@ -750,27 +859,88 @@ impl LightClient {
fetch_full_tx(&self.get_server_uri(), txid, self.config.no_cert_verification, move |tx_bytes: &[u8] | {
let tx = Transaction::read(tx_bytes).unwrap();
- light_wallet_clone.scan_full_tx(&tx, height);
+ light_wallet_clone.read().unwrap().scan_full_tx(&tx, height, 0);
});
};
responses.join("\n")
}
- pub fn do_send(&self, addr: &str, value: u64, memo: Option) -> String {
+ pub fn do_send(&self, addrs: Vec<(&str, u64, Option)>) -> Result {
+ if !self.wallet.read().unwrap().is_unlocked_for_spending() {
+ error!("Wallet is locked");
+ return Err("Wallet is locked".to_string());
+ }
+
info!("Creating transaction");
- let rawtx = self.wallet.send_to_address(
- u32::from_str_radix(&self.config.consensus_branch_id, 16).unwrap(), // Blossom ID
+
+ let rawtx = self.wallet.write().unwrap().send_to_address(
+ u32::from_str_radix(&self.config.consensus_branch_id, 16).unwrap(),
&self.sapling_spend, &self.sapling_output,
- vec![(&addr, value, memo)]
+ addrs
);
match rawtx {
- Ok(txbytes) => match broadcast_raw_tx(&self.get_server_uri(), self.config.no_cert_verification, txbytes) {
- Ok(k) => k,
- Err(e) => e,
- },
- Err(e) => format!("No Tx to broadcast. Error was: {}", e)
+ Ok(txbytes) => broadcast_raw_tx(&self.get_server_uri(), self.config.no_cert_verification, txbytes),
+ Err(e) => Err(format!("Error: No Tx to broadcast. Error was: {}", e))
}
}
}
+
+
+pub mod tests {
+ use lazy_static::lazy_static;
+ //use super::LightClient;
+
+ lazy_static!{
+ static ref TEST_SEED: String = "youth strong sweet gorilla hammer unhappy congress stamp left stereo riot salute road tag clean toilet artefact fork certain leopard entire civil degree wonder".to_string();
+ }
+
+ #[test]
+ pub fn test_encrypt_decrypt() {
+ let lc = super::LightClient::unconnected(TEST_SEED.to_string()).unwrap();
+
+ assert!(!lc.do_export(None).is_err());
+ assert!(!lc.do_new_address("z").is_err());
+ assert!(!lc.do_new_address("t").is_err());
+ assert_eq!(lc.do_seed_phrase().unwrap()["seed"], TEST_SEED.to_string());
+
+ // Encrypt and Lock the wallet
+ lc.wallet.write().unwrap().encrypt("password".to_string()).unwrap();
+ assert!(lc.do_export(None).is_err());
+ assert!(lc.do_seed_phrase().is_err());
+ assert!(lc.do_new_address("t").is_err());
+ assert!(lc.do_new_address("z").is_err());
+ assert!(lc.do_send(vec![("z", 0, None)]).is_err());
+
+ // Do a unlock, and make sure it all works now
+ lc.wallet.write().unwrap().unlock("password".to_string()).unwrap();
+ assert!(!lc.do_export(None).is_err());
+ assert!(!lc.do_seed_phrase().is_err());
+ assert!(!lc.do_new_address("t").is_err());
+ assert!(!lc.do_new_address("z").is_err());
+ }
+
+ #[test]
+ pub fn test_addresses() {
+ let lc = super::LightClient::unconnected(TEST_SEED.to_string()).unwrap();
+
+ // Add new z and t addresses
+
+ let taddr1 = lc.do_new_address("t").unwrap()[0].as_str().unwrap().to_string();
+ let taddr2 = lc.do_new_address("t").unwrap()[0].as_str().unwrap().to_string();
+ let zaddr1 = lc.do_new_address("z").unwrap()[0].as_str().unwrap().to_string();
+ let zaddr2 = lc.do_new_address("z").unwrap()[0].as_str().unwrap().to_string();
+
+ let addresses = lc.do_address();
+ assert_eq!(addresses["z_addresses"].len(), 3);
+ assert_eq!(addresses["z_addresses"][1], zaddr1);
+ assert_eq!(addresses["z_addresses"][2], zaddr2);
+
+ assert_eq!(addresses["t_addresses"].len(), 3);
+ assert_eq!(addresses["t_addresses"][1], taddr1);
+ assert_eq!(addresses["t_addresses"][2], taddr2);
+ }
+
+
+}
\ No newline at end of file
diff --git a/lib/src/lightwallet.rs b/lib/src/lightwallet.rs
index e3a9c70..ffe5d6d 100644
--- a/lib/src/lightwallet.rs
+++ b/lib/src/lightwallet.rs
@@ -47,6 +47,7 @@ mod extended_key;
mod utils;
mod address;
mod prover;
+pub mod bugs;
use data::{BlockData, WalletTx, Utxo, SaplingNoteData, SpendableNote, OutgoingTxMetadata};
use extended_key::{KeyIndex, ExtendedPrivKey};
@@ -90,17 +91,30 @@ impl ToBase58Check for [u8] {
}
pub struct LightWallet {
- seed: [u8; 32], // Seed phrase for this wallet.
+ // Is the wallet encrypted? If it is, then when writing to disk, the seed is always encrypted
+ // and the individual spending keys are not written
+ encrypted: bool,
- // List of keys, actually in this wallet. This may include more
- // than keys derived from the seed, for example, if user imports
- // a private key
+ // In memory only (i.e, this field is not written to disk). Is the wallet unlocked and are
+ // the spending keys present to allow spending from this wallet?
+ unlocked: bool,
+
+ enc_seed: [u8; 48], // If locked, this contains the encrypted seed
+ nonce: Vec, // Nonce used to encrypt the wallet.
+
+ seed: [u8; 32], // Seed phrase for this wallet. If wallet is locked, this is 0
+
+ // List of keys, actually in this wallet. If the wallet is locked, the `extsks` will be
+ // encrypted (but the fvks are not encrpyted)
extsks: Arc>>,
extfvks: Arc>>,
- pub address: Arc>>>,
+
+ pub zaddress: Arc>>>,
- // Transparent keys. TODO: Make it not pubic
- pub tkeys: Arc>>,
+ // Transparent keys. If the wallet is locked, then the secret keys will be encrypted,
+ // but the addresses will be present.
+ tkeys: Arc>>,
+ pub taddresses: Arc>>,
blocks: Arc>>,
pub txs: Arc>>,
@@ -115,10 +129,12 @@ pub struct LightWallet {
impl LightWallet {
pub fn serialized_version() -> u64 {
- return 3;
+ return 4;
}
fn get_taddr_from_bip39seed(config: &LightClientConfig, bip39_seed: &[u8], pos: u32) -> SecretKey {
+ assert_eq!(bip39_seed.len(), 64);
+
let ext_t_key = ExtendedPrivKey::with_seed(bip39_seed).unwrap();
ext_t_key
.derive_private_key(KeyIndex::hardened_from_normalize_index(44).unwrap()).unwrap()
@@ -130,10 +146,12 @@ impl LightWallet {
}
- fn get_zaddr_from_bip39seed(config: &LightClientConfig, bip39seed: &[u8], pos: u32) ->
+ fn get_zaddr_from_bip39seed(config: &LightClientConfig, bip39_seed: &[u8], pos: u32) ->
(ExtendedSpendingKey, ExtendedFullViewingKey, PaymentAddress) {
+ assert_eq!(bip39_seed.len(), 64);
+
let extsk: ExtendedSpendingKey = ExtendedSpendingKey::from_path(
- &ExtendedSpendingKey::master(bip39seed),
+ &ExtendedSpendingKey::master(bip39_seed),
&[
ChildIndex::Hardened(32),
ChildIndex::Hardened(config.get_coin_type()),
@@ -163,8 +181,9 @@ impl LightWallet {
// we need to get the 64 byte bip39 entropy
let bip39_seed = bip39::Seed::new(&Mnemonic::from_entropy(&seed_bytes, Language::English).unwrap(), "");
- // Derive only the first address
+ // Derive only the first sk and address
let tpk = LightWallet::get_taddr_from_bip39seed(&config, &bip39_seed.as_bytes(), 0);
+ let taddr = LightWallet::address_from_prefix_sk(&config.base58_pubkey_address(), &tpk);
// TODO: We need to monitor addresses, and always keep 1 "free" address, so
// users can import a seed phrase and automatically get all used addresses
@@ -172,23 +191,50 @@ impl LightWallet {
= LightWallet::get_zaddr_from_bip39seed(&config, &bip39_seed.as_bytes(), 0);
Ok(LightWallet {
- seed: seed_bytes,
- extsks: Arc::new(RwLock::new(vec![extsk])),
- extfvks: Arc::new(RwLock::new(vec![extfvk])),
- address: Arc::new(RwLock::new(vec![address])),
- tkeys: Arc::new(RwLock::new(vec![tpk])),
- blocks: Arc::new(RwLock::new(vec![])),
- txs: Arc::new(RwLock::new(HashMap::new())),
- config: config.clone(),
- birthday: latest_block,
+ encrypted: false,
+ unlocked: true,
+ enc_seed: [0u8; 48],
+ nonce: vec![],
+ seed: seed_bytes,
+ extsks: Arc::new(RwLock::new(vec![extsk])),
+ extfvks: Arc::new(RwLock::new(vec![extfvk])),
+ zaddress: Arc::new(RwLock::new(vec![address])),
+ tkeys: Arc::new(RwLock::new(vec![tpk])),
+ taddresses: Arc::new(RwLock::new(vec![taddr])),
+ blocks: Arc::new(RwLock::new(vec![])),
+ txs: Arc::new(RwLock::new(HashMap::new())),
+ config: config.clone(),
+ birthday: latest_block,
})
}
pub fn read(mut reader: R, config: &LightClientConfig) -> io::Result {
let version = reader.read_u64::()?;
- assert!(version <= LightWallet::serialized_version());
+ if version > LightWallet::serialized_version() {
+ let e = format!("Don't know how to read wallet version {}. Do you have the latest version?", version);
+ error!("{}", e);
+ return Err(io::Error::new(ErrorKind::InvalidData, e));
+ }
+
info!("Reading wallet version {}", version);
+ let encrypted = if version >= 4 {
+ reader.read_u8()? > 0
+ } else {
+ false
+ };
+
+ let mut enc_seed = [0u8; 48];
+ if version >= 4 {
+ reader.read_exact(&mut enc_seed)?;
+ }
+
+ let nonce = if version >= 4 {
+ Vector::read(&mut reader, |r| r.read_u8())?
+ } else {
+ vec![]
+ };
+
// Seed
let mut seed_bytes = [0u8; 32];
reader.read_exact(&mut seed_bytes)?;
@@ -196,9 +242,14 @@ impl LightWallet {
// Read the spending keys
let extsks = Vector::read(&mut reader, |r| ExtendedSpendingKey::read(r))?;
- // Calculate the viewing keys
- let extfvks = extsks.iter().map(|sk| ExtendedFullViewingKey::from(sk))
- .collect::>();
+ let extfvks = if version >= 4 {
+ // Read the viewing keys
+ Vector::read(&mut reader, |r| ExtendedFullViewingKey::read(r))?
+ } else {
+ // Calculate the viewing keys
+ extsks.iter().map(|sk| ExtendedFullViewingKey::from(sk))
+ .collect::>()
+ };
// Calculate the addresses
let addresses = extfvks.iter().map( |fvk| fvk.default_address().unwrap().1 )
@@ -210,6 +261,14 @@ impl LightWallet {
secp256k1::SecretKey::from_slice(&tpk_bytes).map_err(|e| io::Error::new(ErrorKind::InvalidData, e))
})?;
+ let taddresses = if version >= 4 {
+ // Read the addresses
+ Vector::read(&mut reader, |r| utils::read_string(r))?
+ } else {
+ // Calculate the addresses
+ tkeys.iter().map(|sk| LightWallet::address_from_prefix_sk(&config.base58_pubkey_address(), sk)).collect()
+ };
+
let blocks = Vector::read(&mut reader, |r| BlockData::read(r))?;
let txs_tuples = Vector::read(&mut reader, |r| {
@@ -230,22 +289,41 @@ impl LightWallet {
let birthday = reader.read_u64::()?;
Ok(LightWallet{
- seed: seed_bytes,
- extsks: Arc::new(RwLock::new(extsks)),
- extfvks: Arc::new(RwLock::new(extfvks)),
- address: Arc::new(RwLock::new(addresses)),
- tkeys: Arc::new(RwLock::new(tkeys)),
- blocks: Arc::new(RwLock::new(blocks)),
- txs: Arc::new(RwLock::new(txs)),
- config: config.clone(),
+ encrypted: encrypted,
+ unlocked: !encrypted, // When reading from disk, if wallet is encrypted, it starts off locked.
+ enc_seed: enc_seed,
+ nonce: nonce,
+ seed: seed_bytes,
+ extsks: Arc::new(RwLock::new(extsks)),
+ extfvks: Arc::new(RwLock::new(extfvks)),
+ zaddress: Arc::new(RwLock::new(addresses)),
+ tkeys: Arc::new(RwLock::new(tkeys)),
+ taddresses: Arc::new(RwLock::new(taddresses)),
+ blocks: Arc::new(RwLock::new(blocks)),
+ txs: Arc::new(RwLock::new(txs)),
+ config: config.clone(),
birthday,
})
}
pub fn write(&self, mut writer: W) -> io::Result<()> {
+ if self.encrypted && self.unlocked {
+ return Err(Error::new(ErrorKind::InvalidInput,
+ format!("Cannot write while wallet is unlocked while encrypted.")));
+ }
+
// Write the version
writer.write_u64::(LightWallet::serialized_version())?;
+ // Write if it is locked
+ writer.write_u8(if self.encrypted {1} else {0})?;
+
+ // Write the encrypted seed bytes
+ writer.write_all(&self.enc_seed)?;
+
+ // Write the nonce
+ Vector::write(&mut writer, &self.nonce, |w, b| w.write_u8(*b))?;
+
// Write the seed
writer.write_all(&self.seed)?;
@@ -257,11 +335,21 @@ impl LightWallet {
|w, sk| sk.write(w)
)?;
- // Write the transparent private key
+ // Write the FVKs
+ Vector::write(&mut writer, &self.extfvks.read().unwrap(),
+ |w, fvk| fvk.write(w)
+ )?;
+
+ // Write the transparent private keys
Vector::write(&mut writer, &self.tkeys.read().unwrap(),
|w, pk| w.write_all(&pk[..])
)?;
+ // Write the transparent addresses
+ Vector::write(&mut writer, &self.taddresses.read().unwrap(),
+ |w, a| utils::write_string(w, a)
+ )?;
+
Vector::write(&mut writer, &self.blocks.read().unwrap(), |w, b| b.write(w))?;
// The hashmap, write as a set of tuples
@@ -279,9 +367,9 @@ impl LightWallet {
Ok(())
}
- pub fn note_address(&self, note: &SaplingNoteData) -> Option {
+ pub fn note_address(hrp: &str, note: &SaplingNoteData) -> Option {
match note.extfvk.fvk.vk.into_payment_address(note.diversifier, &JUBJUB) {
- Some(pa) => Some(encode_payment_address(self.config.hrp_sapling_address(), &pa)),
+ Some(pa) => Some(encode_payment_address(hrp, &pa)),
None => None
}
}
@@ -317,7 +405,8 @@ impl LightWallet {
/// Get all t-address private keys. Returns a Vector of (address, secretkey)
pub fn get_t_secret_keys(&self) -> Vec<(String, String)> {
self.tkeys.read().unwrap().iter().map(|sk| {
- (self.address_from_sk(sk), sk[..].to_base58check(&self.config.base58_secretkey_prefix(), &[0x01]))
+ (self.address_from_sk(sk),
+ sk[..].to_base58check(&self.config.base58_secretkey_prefix(), &[0x01]))
}).collect::>()
}
@@ -325,14 +414,20 @@ impl LightWallet {
/// at the next position and add it to the wallet.
/// NOTE: This does NOT rescan
pub fn add_zaddr(&self) -> String {
+ if !self.unlocked {
+ return "".to_string();
+ }
+
let pos = self.extsks.read().unwrap().len() as u32;
+ let bip39_seed = bip39::Seed::new(&Mnemonic::from_entropy(&self.seed, Language::English).unwrap(), "");
+
let (extsk, extfvk, address) =
- LightWallet::get_zaddr_from_bip39seed(&self.config, &self.seed, pos);
+ LightWallet::get_zaddr_from_bip39seed(&self.config, &bip39_seed.as_bytes(), pos);
let zaddr = encode_payment_address(self.config.hrp_sapling_address(), &address);
self.extsks.write().unwrap().push(extsk);
self.extfvks.write().unwrap().push(extfvk);
- self.address.write().unwrap().push(address);
+ self.zaddress.write().unwrap().push(address);
zaddr
}
@@ -341,12 +436,20 @@ impl LightWallet {
/// at the next position.
/// NOTE: This is not rescan the wallet
pub fn add_taddr(&self) -> String {
+ if !self.unlocked {
+ return "".to_string();
+ }
+
let pos = self.tkeys.read().unwrap().len() as u32;
- let sk = LightWallet::get_taddr_from_bip39seed(&self.config, &self.seed, pos);
+ let bip39_seed = bip39::Seed::new(&Mnemonic::from_entropy(&self.seed, Language::English).unwrap(), "");
+
+ let sk = LightWallet::get_taddr_from_bip39seed(&self.config, &bip39_seed.as_bytes(), pos);
+ let address = self.address_from_sk(&sk);
self.tkeys.write().unwrap().push(sk);
+ self.taddresses.write().unwrap().push(address.clone());
- self.address_from_sk(&sk)
+ address
}
/// Clears all the downloaded blocks and resets the state back to the initial block.
@@ -453,7 +556,7 @@ impl LightWallet {
}
}
- pub fn address_from_sk(&self, sk: &secp256k1::SecretKey) -> String {
+ pub fn address_from_prefix_sk(prefix: &[u8; 1], sk: &secp256k1::SecretKey) -> String {
let secp = secp256k1::Secp256k1::new();
let pk = secp256k1::PublicKey::from_secret_key(&secp, &sk);
@@ -461,7 +564,11 @@ impl LightWallet {
let mut hash160 = ripemd160::Ripemd160::new();
hash160.input(Sha256::digest(&pk.serialize()[..].to_vec()));
- hash160.result().to_base58check(&self.config.base58_pubkey_address(), &[])
+ hash160.result().to_base58check(prefix, &[])
+ }
+
+ pub fn address_from_sk(&self, sk: &secp256k1::SecretKey) -> String {
+ LightWallet::address_from_prefix_sk(&self.config.base58_pubkey_address(), sk)
}
pub fn address_from_pubkeyhash(&self, ta: Option) -> Option {
@@ -477,11 +584,149 @@ impl LightWallet {
}
pub fn get_seed_phrase(&self) -> String {
+ if !self.unlocked {
+ return "".to_string();
+ }
+
Mnemonic::from_entropy(&self.seed,
Language::English,
).unwrap().phrase().to_string()
}
+ pub fn encrypt(&mut self, passwd: String) -> io::Result<()> {
+ use sodiumoxide::crypto::secretbox;
+
+ if self.encrypted && !self.unlocked {
+ return Err(io::Error::new(ErrorKind::AlreadyExists, "Wallet is already encrypted and locked"));
+ }
+
+ // Get the doublesha256 of the password, which is the right length
+ let key = secretbox::Key::from_slice(&double_sha256(passwd.as_bytes())).unwrap();
+ let nonce = secretbox::gen_nonce();
+
+ let cipher = secretbox::seal(&self.seed, &nonce, &key);
+
+ self.enc_seed.copy_from_slice(&cipher);
+ self.nonce = vec![];
+ self.nonce.extend_from_slice(nonce.as_ref());
+
+ self.encrypted = true;
+ self.lock()?;
+
+ Ok(())
+ }
+
+ pub fn lock(&mut self) -> io::Result<()> {
+ // Empty the seed and the secret keys
+ self.seed.copy_from_slice(&[0u8; 32]);
+ self.tkeys = Arc::new(RwLock::new(vec![]));
+ self.extsks = Arc::new(RwLock::new(vec![]));
+
+ self.unlocked = false;
+
+ Ok(())
+ }
+
+ pub fn unlock(&mut self, passwd: String) -> io::Result<()> {
+ use sodiumoxide::crypto::secretbox;
+
+ if !self.encrypted {
+ return Err(Error::new(ErrorKind::AlreadyExists, "Wallet is not encrypted"));
+ }
+
+ if self.encrypted && self.unlocked {
+ return Err(Error::new(ErrorKind::AlreadyExists, "Wallet is already unlocked"));
+ }
+
+ // Get the doublesha256 of the password, which is the right length
+ let key = secretbox::Key::from_slice(&double_sha256(passwd.as_bytes())).unwrap();
+ let nonce = secretbox::Nonce::from_slice(&self.nonce).unwrap();
+
+ let seed = match secretbox::open(&self.enc_seed, &nonce, &key) {
+ Ok(s) => s,
+ Err(_) => {return Err(io::Error::new(ErrorKind::InvalidData, "Decryption failed. Is your password correct?"));}
+ };
+
+ // Now that we have the seed, we'll generate the extsks and tkeys, and verify the fvks and addresses
+ // respectively match
+
+ // The seed bytes is the raw entropy. To pass it to HD wallet generation,
+ // we need to get the 64 byte bip39 entropy
+ let bip39_seed = bip39::Seed::new(&Mnemonic::from_entropy(&seed, Language::English).unwrap(), "");
+
+ // Sapling keys
+ let mut extsks = vec![];
+ for pos in 0..self.zaddress.read().unwrap().len() {
+ let (extsk, extfvk, address) =
+ LightWallet::get_zaddr_from_bip39seed(&self.config, &bip39_seed.as_bytes(), pos as u32);
+
+ if address != self.zaddress.read().unwrap()[pos] {
+ return Err(io::Error::new(ErrorKind::InvalidData,
+ format!("zaddress mismatch at {}. {:?} vs {:?}", pos, address, self.zaddress.read().unwrap()[pos])));
+ }
+
+ if extfvk != self.extfvks.read().unwrap()[pos] {
+ return Err(io::Error::new(ErrorKind::InvalidData,
+ format!("fvk mismatch at {}. {:?} vs {:?}", pos, extfvk, self.extfvks.read().unwrap()[pos])));
+ }
+
+ // Don't add it to self yet, we'll do that at the end when everything is verified
+ extsks.push(extsk);
+ }
+
+ // Transparent keys
+ let mut tkeys = vec![];
+ for pos in 0..self.taddresses.read().unwrap().len() {
+ let sk = LightWallet::get_taddr_from_bip39seed(&self.config, &bip39_seed.as_bytes(), pos as u32);
+ let address = self.address_from_sk(&sk);
+
+ if address != self.taddresses.read().unwrap()[pos] {
+ return Err(io::Error::new(ErrorKind::InvalidData,
+ format!("taddress mismatch at {}. {} vs {}", pos, address, self.taddresses.read().unwrap()[pos])));
+ }
+
+ tkeys.push(sk);
+ }
+
+ // Everything checks out, so we'll update our wallet with the decrypted values
+ self.extsks = Arc::new(RwLock::new(extsks));
+ self.tkeys = Arc::new(RwLock::new(tkeys));
+ self.seed.copy_from_slice(&seed);
+
+ self.encrypted = true;
+ self.unlocked = true;
+
+ Ok(())
+ }
+
+ // Removing encryption means unlocking it and setting the self.encrypted = false,
+ // permanantly removing the encryption
+ pub fn remove_encryption(&mut self, passwd: String) -> io::Result<()> {
+ if !self.encrypted {
+ return Err(Error::new(ErrorKind::AlreadyExists, "Wallet is not encrypted"));
+ }
+
+ // Unlock the wallet if it's locked
+ if !self.unlocked {
+ self.unlock(passwd)?;
+ }
+
+ // Permanantly remove the encryption
+ self.encrypted = false;
+ self.nonce = vec![];
+ self.enc_seed.copy_from_slice(&[0u8; 48]);
+
+ Ok(())
+ }
+
+ pub fn is_encrypted(&self) -> bool {
+ return self.encrypted;
+ }
+
+ pub fn is_unlocked_for_spending(&self) -> bool {
+ return self.unlocked;
+ }
+
pub fn zbalance(&self, addr: Option) -> u64 {
self.txs.read().unwrap()
.values()
@@ -560,12 +805,12 @@ impl LightWallet {
.sum::()
}
- fn add_toutput_to_wtx(&self, height: i32, txid: &TxId, vout: &TxOut, n: u64) {
+ fn add_toutput_to_wtx(&self, height: i32, timestamp: u64, txid: &TxId, vout: &TxOut, n: u64) {
let mut txs = self.txs.write().unwrap();
// Find the existing transaction entry, or create a new one.
if !txs.contains_key(&txid) {
- let tx_entry = WalletTx::new(height, &txid);
+ let tx_entry = WalletTx::new(height, timestamp, &txid);
txs.insert(txid.clone(), tx_entry);
}
let tx_entry = txs.get_mut(&txid).unwrap();
@@ -600,7 +845,7 @@ impl LightWallet {
}
// Scan the full Tx and update memos for incoming shielded transactions
- pub fn scan_full_tx(&self, tx: &Transaction, height: i32) {
+ pub fn scan_full_tx(&self, tx: &Transaction, height: i32, datetime: u64) {
// Scan all the inputs to see if we spent any transparent funds in this tx
// TODO: Save this object
@@ -639,7 +884,7 @@ impl LightWallet {
let mut txs = self.txs.write().unwrap();
if !txs.contains_key(&tx.txid()) {
- let tx_entry = WalletTx::new(height, &tx.txid());
+ let tx_entry = WalletTx::new(height, datetime, &tx.txid());
txs.insert(tx.txid().clone(), tx_entry);
}
@@ -661,7 +906,7 @@ impl LightWallet {
Some(TransparentAddress::PublicKey(hash)) => {
if hash[..] == ripemd160::Ripemd160::digest(&Sha256::digest(&pubkey))[..] {
// This is our address. Add this as an output to the txid
- self.add_toutput_to_wtx(height, &tx.txid(), &vout, n as u64);
+ self.add_toutput_to_wtx(height, datetime, &tx.txid(), &vout, n as u64);
}
},
_ => {}
@@ -676,8 +921,8 @@ impl LightWallet {
// outgoing metadata
// Collect our t-addresses
- let wallet_taddrs = self.tkeys.read().unwrap().iter()
- .map(|sk| self.address_from_sk(sk))
+ let wallet_taddrs = self.taddresses.read().unwrap().iter()
+ .map(|a| a.clone())
.collect::>();
for vout in tx.vout.iter() {
@@ -745,7 +990,7 @@ impl LightWallet {
// First, collect all our z addresses, to check for change
// Collect z addresses
- let z_addresses = self.address.read().unwrap().iter().map( |ad| {
+ let z_addresses = self.zaddress.read().unwrap().iter().map( |ad| {
encode_payment_address(self.config.hrp_sapling_address(), &ad)
}).collect::>();
@@ -1013,7 +1258,7 @@ impl LightWallet {
// Find the existing transaction entry, or create a new one.
if !txs.contains_key(&tx.txid) {
- let tx_entry = WalletTx::new(block_data.height as i32, &tx.txid);
+ let tx_entry = WalletTx::new(block_data.height as i32, block.time as u64, &tx.txid);
txs.insert(tx.txid, tx_entry);
}
let tx_entry = txs.get_mut(&tx.txid).unwrap();
@@ -1065,6 +1310,10 @@ impl LightWallet {
output_params: &[u8],
tos: Vec<(&str, u64, Option)>
) -> Result, String> {
+ if !self.unlocked {
+ return Err("Cannot spend while wallet is locked".to_string());
+ }
+
let start_time = now();
let total_value = tos.iter().map(|to| to.1).sum::();
@@ -1175,7 +1424,7 @@ impl LightWallet {
if selected_value < u64::from(target_value) {
let e = format!(
- "Insufficient verified funds (have {}, need {:?}).\nNote: funds need {} confirmations before they can be spent",
+ "Insufficient verified funds (have {}, need {:?}). NOTE: funds need {} confirmations before they can be spent.",
selected_value, target_value, self.config.anchor_offset
);
error!("{}", e);
@@ -1669,7 +1918,7 @@ pub mod tests {
tx.add_t_output(&pk, AMOUNT1);
let txid1 = tx.get_tx().txid();
- wallet.scan_full_tx(&tx.get_tx(), 100); // Pretend it is at height 100
+ wallet.scan_full_tx(&tx.get_tx(), 100, 0); // Pretend it is at height 100
{
let txs = wallet.txs.read().unwrap();
@@ -1694,7 +1943,7 @@ pub mod tests {
tx.add_t_input(txid1, 0);
let txid2 = tx.get_tx().txid();
- wallet.scan_full_tx(&tx.get_tx(), 101); // Pretent it is at height 101
+ wallet.scan_full_tx(&tx.get_tx(), 101, 0); // Pretent it is at height 101
{
// Make sure the txid was spent
@@ -1741,7 +1990,7 @@ pub mod tests {
tx.add_t_output(&non_wallet_pk, 25);
let txid1 = tx.get_tx().txid();
- wallet.scan_full_tx(&tx.get_tx(), 100); // Pretend it is at height 100
+ wallet.scan_full_tx(&tx.get_tx(), 100, 0); // Pretend it is at height 100
{
let txs = wallet.txs.read().unwrap();
@@ -1766,7 +2015,7 @@ pub mod tests {
tx.add_t_input(txid1, 1); // Ours was at position 1 in the input tx
let txid2 = tx.get_tx().txid();
- wallet.scan_full_tx(&tx.get_tx(), 101); // Pretent it is at height 101
+ wallet.scan_full_tx(&tx.get_tx(), 101, 0); // Pretent it is at height 101
{
// Make sure the txid was spent
@@ -1815,7 +2064,7 @@ pub mod tests {
let mut tx = FakeTransaction::new_with_txid(txid1);
tx.add_t_output(&pk, TAMOUNT1);
- wallet.scan_full_tx(&tx.get_tx(), 0); // Height 0
+ wallet.scan_full_tx(&tx.get_tx(), 0, 0); // Height 0
const AMOUNT2:u64 = 2;
@@ -1828,7 +2077,7 @@ pub mod tests {
let mut tx = FakeTransaction::new_with_txid(txid2);
tx.add_t_input(txid1, 0);
- wallet.scan_full_tx(&tx.get_tx(), 1); // Height 1
+ wallet.scan_full_tx(&tx.get_tx(), 1, 0); // Height 1
// Now, the original note should be spent and there should be a change
assert_eq!(wallet.zbalance(None), AMOUNT1 - AMOUNT2 ); // The t addr amount is received + spent, so it cancels out
@@ -1847,7 +2096,7 @@ pub mod tests {
assert_eq!(wallet.extsks.read().unwrap().len(), wallet2.extsks.read().unwrap().len());
assert_eq!(wallet.extsks.read().unwrap()[0], wallet2.extsks.read().unwrap()[0]);
assert_eq!(wallet.extfvks.read().unwrap()[0], wallet2.extfvks.read().unwrap()[0]);
- assert_eq!(wallet.address.read().unwrap()[0], wallet2.address.read().unwrap()[0]);
+ assert_eq!(wallet.zaddress.read().unwrap()[0], wallet2.zaddress.read().unwrap()[0]);
assert_eq!(wallet.tkeys.read().unwrap().len(), wallet2.tkeys.read().unwrap().len());
assert_eq!(wallet.tkeys.read().unwrap()[0], wallet2.tkeys.read().unwrap()[0]);
@@ -1916,7 +2165,7 @@ pub mod tests {
assert_eq!(wallet2.tkeys.read().unwrap().len(), 2);
assert_eq!(wallet2.extsks.read().unwrap().len(), 2);
assert_eq!(wallet2.extfvks.read().unwrap().len(), 2);
- assert_eq!(wallet2.address.read().unwrap().len(), 2);
+ assert_eq!(wallet2.zaddress.read().unwrap().len(), 2);
assert_eq!(taddr1, wallet.address_from_sk(&wallet.tkeys.read().unwrap()[0]));
assert_eq!(taddr2, wallet.address_from_sk(&wallet.tkeys.read().unwrap()[1]));
@@ -2023,7 +2272,7 @@ pub mod tests {
}
// Now, full scan the Tx, which should populate the Outgoing Meta data
- wallet.scan_full_tx(&sent_tx, 2);
+ wallet.scan_full_tx(&sent_tx, 2, 0);
// Check Outgoing Metadata
{
@@ -2063,7 +2312,7 @@ pub mod tests {
let mut cb3 = FakeCompactBlock::new(2, block_hash);
cb3.add_tx(&sent_tx);
wallet.scan_block(&cb3.as_bytes()).unwrap();
- wallet.scan_full_tx(&sent_tx, 2);
+ wallet.scan_full_tx(&sent_tx, 2, 0);
// Because the builder will randomize notes outputted, we need to find
// which note number is the change and which is the output note (Because this tx
@@ -2116,7 +2365,7 @@ pub mod tests {
let mut cb4 = FakeCompactBlock::new(3, cb3.hash());
cb4.add_tx(&sent_tx);
wallet.scan_block(&cb4.as_bytes()).unwrap();
- wallet.scan_full_tx(&sent_tx, 3);
+ wallet.scan_full_tx(&sent_tx, 3, 0);
{
// Both notes should be spent now.
@@ -2187,7 +2436,7 @@ pub mod tests {
}
// Now, full scan the Tx, which should populate the Outgoing Meta data
- wallet.scan_full_tx(&sent_tx, 2);
+ wallet.scan_full_tx(&sent_tx, 2, 0);
// Check Outgoing Metadata for t address
{
@@ -2215,7 +2464,7 @@ pub mod tests {
tx.add_t_output(&pk, AMOUNT_T);
let txid_t = tx.get_tx().txid();
- wallet.scan_full_tx(&tx.get_tx(), 1); // Pretend it is at height 1
+ wallet.scan_full_tx(&tx.get_tx(), 1, 0); // Pretend it is at height 1
{
let txs = wallet.txs.read().unwrap();
@@ -2277,7 +2526,7 @@ pub mod tests {
// Scan the compact block and the full Tx
wallet.scan_block(&cb3.as_bytes()).unwrap();
- wallet.scan_full_tx(&sent_tx, 2);
+ wallet.scan_full_tx(&sent_tx, 2, 0);
// Now this new Spent tx should be in, so the note should be marked confirmed spent
{
@@ -2327,7 +2576,7 @@ pub mod tests {
wallet.scan_block(&cb3.as_bytes()).unwrap();
// And scan the Full Tx to get the memo
- wallet.scan_full_tx(&sent_tx, 2);
+ wallet.scan_full_tx(&sent_tx, 2, 0);
{
let txs = wallet.txs.read().unwrap();
@@ -2336,7 +2585,7 @@ pub mod tests {
assert_eq!(txs[&sent_txid].notes[0].extfvk, wallet.extfvks.read().unwrap()[0]);
assert_eq!(txs[&sent_txid].notes[0].note.value, AMOUNT1 - fee);
- assert_eq!(wallet.note_address(&txs[&sent_txid].notes[0]), Some(my_address));
+ assert_eq!(LightWallet::note_address(wallet.config.hrp_sapling_address(), &txs[&sent_txid].notes[0]), Some(my_address));
assert_eq!(LightWallet::memo_str(&txs[&sent_txid].notes[0].memo), Some(memo));
}
}
@@ -2366,7 +2615,7 @@ pub mod tests {
wallet.scan_block(&cb3.as_bytes()).unwrap();
// And scan the Full Tx to get the memo
- wallet.scan_full_tx(&sent_tx, 2);
+ wallet.scan_full_tx(&sent_tx, 2, 0);
{
let txs = wallet.txs.read().unwrap();
@@ -2424,7 +2673,7 @@ pub mod tests {
let mut cb3 = FakeCompactBlock::new(2, block_hash);
cb3.add_tx(&sent_tx);
wallet.scan_block(&cb3.as_bytes()).unwrap();
- wallet.scan_full_tx(&sent_tx, 2);
+ wallet.scan_full_tx(&sent_tx, 2, 0);
// Check that the send to the second taddr worked
{
@@ -2468,7 +2717,7 @@ pub mod tests {
let mut cb4 = FakeCompactBlock::new(3, cb3.hash());
cb4.add_tx(&sent_tx);
wallet.scan_block(&cb4.as_bytes()).unwrap();
- wallet.scan_full_tx(&sent_tx, 3);
+ wallet.scan_full_tx(&sent_tx, 3, 0);
// Quickly check we have it
{
@@ -2505,7 +2754,7 @@ pub mod tests {
let mut cb5 = FakeCompactBlock::new(4, cb4.hash());
cb5.add_tx(&sent_tx);
wallet.scan_block(&cb5.as_bytes()).unwrap();
- wallet.scan_full_tx(&sent_tx, 4);
+ wallet.scan_full_tx(&sent_tx, 4, 0);
{
let txs = wallet.txs.read().unwrap();
@@ -2561,7 +2810,7 @@ pub mod tests {
let mut cb3 = FakeCompactBlock::new(2, block_hash);
cb3.add_tx(&sent_tx);
wallet.scan_block(&cb3.as_bytes()).unwrap();
- wallet.scan_full_tx(&sent_tx, 2);
+ wallet.scan_full_tx(&sent_tx, 2, 0);
// Make sure all the outputs are there!
{
@@ -2633,7 +2882,7 @@ pub mod tests {
let mut cb4 = FakeCompactBlock::new(3, cb3.hash());
cb4.add_tx(&sent_tx);
wallet.scan_block(&cb4.as_bytes()).unwrap();
- wallet.scan_full_tx(&sent_tx, 3);
+ wallet.scan_full_tx(&sent_tx, 3, 0);
// Make sure all the outputs are there!
{
@@ -2811,7 +3060,7 @@ pub mod tests {
let mut cb3 = FakeCompactBlock::new(7, blk6_hash);
cb3.add_tx(&sent_tx);
wallet.scan_block(&cb3.as_bytes()).unwrap();
- wallet.scan_full_tx(&sent_tx, 7);
+ wallet.scan_full_tx(&sent_tx, 7, 0);
// Make sure the Tx is in.
{
@@ -2868,9 +3117,15 @@ pub mod tests {
// Test the addresses against https://iancoleman.io/bip39/
let (taddr, pk) = &wallet.get_t_secret_keys()[0];
- assert_eq!(taddr, "RVog7rQu2Zo2iAQCjbZGXsiQm7SYr9bcaq");
+ assert_eq!(taddr, "t1eQ63fwkQ4n4Eo5uCrPGaAV8FWB2tmx7ui");
assert_eq!(pk, "Kz9ybX4giKag4NtnP1pi8WQF2B2hZDkFU85S7Dciz3UUhM59AnhE");
+ // Test a couple more
+ wallet.add_taddr();
+ let (taddr, pk) = &wallet.get_t_secret_keys()[1];
+ assert_eq!(taddr, "t1NoS6ZgaUTpmjkge2cVpXGcySasdYDrXqh");
+ assert_eq!(pk, "KxdmS38pxskS6bbKX43zhTu8ppWckNmWjKsQFX1hwidvhRRgRd3c");
+
let (zaddr, sk) = &wallet.get_z_private_keys()[0];
assert_eq!(zaddr, "zs1q6xk3q783t5k92kjqt2rkuuww8pdw2euzy5rk6jytw97enx8fhpazdv3th4xe7vsk6e9sfpawfg");
assert_eq!(sk, "secret-extended-key-main1qvpa0qr8qqqqpqxn4l054nzxpxzp3a8r2djc7sekdek5upce8mc2j2z0arzps4zv940qeg706hd0wq6g5snzvhp332y6vhwyukdn8dhekmmsk7fzvzkqm6ypc99uy63tpesqwxhpre78v06cx8k5xpp9mrhtgqs5dvp68cqx2yrvthflmm2ynl8c0506dekul0f6jkcdmh0292lpphrksyc5z3pxwws97zd5els3l2mjt2s7hntap27mlmt6w0drtfmz36vz8pgu7ec0twfrq");
@@ -2878,9 +3133,112 @@ pub mod tests {
assert_eq!(seed_phrase, Some(wallet.get_seed_phrase()));
}
+ #[test]
+ fn test_lock_unlock() {
+ const AMOUNT: u64 = 500000;
+
+ let (mut wallet, _, _) = get_test_wallet(AMOUNT);
+ let config = wallet.config.clone();
+
+ // Add some addresses
+ let zaddr0 = encode_payment_address(config.hrp_sapling_address(),
+ &wallet.extfvks.read().unwrap()[0].default_address().unwrap().1);
+ let zaddr1 = wallet.add_zaddr();
+ let zaddr2 = wallet.add_zaddr();
+
+ let taddr0 = wallet.address_from_sk(&wallet.tkeys.read().unwrap()[0]);
+ let taddr1 = wallet.add_taddr();
+ let taddr2 = wallet.add_taddr();
+
+ let seed = wallet.seed;
+
+ wallet.encrypt("somepassword".to_string()).unwrap();
+
+ // Encrypting an already encrypted wallet should fail
+ assert!(wallet.encrypt("somepassword".to_string()).is_err());
+
+ // Serialize a locked wallet
+ let mut serialized_data = vec![];
+ wallet.write(&mut serialized_data).expect("Serialize wallet");
+
+ // Should fail when there's a wrong password
+ assert!(wallet.unlock("differentpassword".to_string()).is_err());
+
+ // Properly unlock
+ wallet.unlock("somepassword".to_string()).unwrap();
+
+ assert_eq!(seed, wallet.seed);
+ {
+ let extsks = wallet.extsks.read().unwrap();
+ let tkeys = wallet.tkeys.read().unwrap();
+ assert_eq!(extsks.len(), 3);
+ assert_eq!(tkeys.len(), 3);
+
+ assert_eq!(zaddr0, encode_payment_address(config.hrp_sapling_address(),
+ &ExtendedFullViewingKey::from(&extsks[0]).default_address().unwrap().1));
+ assert_eq!(zaddr1, encode_payment_address(config.hrp_sapling_address(),
+ &ExtendedFullViewingKey::from(&extsks[1]).default_address().unwrap().1));
+ assert_eq!(zaddr2, encode_payment_address(config.hrp_sapling_address(),
+ &ExtendedFullViewingKey::from(&extsks[2]).default_address().unwrap().1));
+
+ assert_eq!(taddr0, wallet.address_from_sk(&tkeys[0]));
+ assert_eq!(taddr1, wallet.address_from_sk(&tkeys[1]));
+ assert_eq!(taddr2, wallet.address_from_sk(&tkeys[2]));
+ }
+
+ // Unlocking an already unlocked wallet should fail
+ assert!(wallet.unlock("somepassword".to_string()).is_err());
+
+ // Trying to serialize a encrypted but unlocked wallet should fail
+ assert!(wallet.write(&mut vec![]).is_err());
+
+ // ...but if we lock it again, it should serialize
+ wallet.lock().unwrap();
+ wallet.write(&mut vec![]).expect("Serialize wallet");
+
+ // Try from a deserialized, locked wallet
+ let mut wallet2 = LightWallet::read(&serialized_data[..], &config).unwrap();
+ wallet2.unlock("somepassword".to_string()).unwrap();
+
+ assert_eq!(seed, wallet2.seed);
+ {
+ let extsks = wallet2.extsks.read().unwrap();
+ let tkeys = wallet2.tkeys.read().unwrap();
+ assert_eq!(extsks.len(), 3);
+ assert_eq!(tkeys.len(), 3);
+
+ assert_eq!(zaddr0, encode_payment_address(wallet2.config.hrp_sapling_address(),
+ &ExtendedFullViewingKey::from(&extsks[0]).default_address().unwrap().1));
+ assert_eq!(zaddr1, encode_payment_address(wallet2.config.hrp_sapling_address(),
+ &ExtendedFullViewingKey::from(&extsks[1]).default_address().unwrap().1));
+ assert_eq!(zaddr2, encode_payment_address(wallet2.config.hrp_sapling_address(),
+ &ExtendedFullViewingKey::from(&extsks[2]).default_address().unwrap().1));
+
+ assert_eq!(taddr0, wallet2.address_from_sk(&tkeys[0]));
+ assert_eq!(taddr1, wallet2.address_from_sk(&tkeys[1]));
+ assert_eq!(taddr2, wallet2.address_from_sk(&tkeys[2]));
+ }
+ }
+
+ #[test]
+ #[should_panic]
+ fn test_invalid_bip39_t() {
+ // Passing a 32-byte seed to bip32 should fail.
+ let config = get_test_config();
+ LightWallet::get_taddr_from_bip39seed(&config, &[0u8; 32], 0);
+ }
+
+ #[test]
+ #[should_panic]
+ fn test_invalid_bip39_z() {
+ // Passing a 32-byte seed to bip32 should fail.
+ let config = get_test_config();
+ LightWallet::get_zaddr_from_bip39seed(&config, &[0u8; 32], 0);
+ }
+
#[test]
fn test_invalid_scan_blocks() {
- const AMOUNT: u64 = 50000;
+ const AMOUNT: u64 = 500000;
let (wallet, _txid1, block_hash) = get_test_wallet(AMOUNT);
let prev_hash = add_blocks(&wallet, 2, 1, block_hash).unwrap();
diff --git a/lib/src/lightwallet/bugs.rs b/lib/src/lightwallet/bugs.rs
new file mode 100644
index 0000000..48eeba7
--- /dev/null
+++ b/lib/src/lightwallet/bugs.rs
@@ -0,0 +1,127 @@
+///
+/// In v1.0 of zecwallet-cli, there was a bug that incorrectly derived HD wallet keys after the first key. That is, the
+/// first key, address was correct, but subsequent ones were not.
+///
+/// The issue was that the 32-byte seed was directly being used to derive then subsequent addresses instead of the
+/// 64-byte pkdf2(seed). The issue affected both t and z addresses
+///
+/// To fix the bug, we need to:
+/// 1. Check if the wallet has more than 1 address for t or z addresses
+/// 2. Move any funds in these addresses to the first address
+/// 3. Re-derive the addresses
+
+use super::LightWallet;
+use crate::lightclient::LightClient;
+
+use json::object;
+use bip39::{Mnemonic, Language};
+
+pub struct BugBip39Derivation {}
+
+impl BugBip39Derivation {
+
+ /// Check if this bug exists in the wallet
+ pub fn has_bug(client: &LightClient) -> bool {
+ let wallet = client.wallet.read().unwrap();
+
+ if wallet.zaddress.read().unwrap().len() <= 1 {
+ return false;
+ }
+
+ // The seed bytes is the raw entropy. To pass it to HD wallet generation,
+ // we need to get the 64 byte bip39 entropy
+ let bip39_seed = bip39::Seed::new(&Mnemonic::from_entropy(&wallet.seed, Language::English).unwrap(), "");
+
+ // Check z addresses
+ for pos in 0..wallet.zaddress.read().unwrap().len() {
+ let (_, _, address) =
+ LightWallet::get_zaddr_from_bip39seed(&wallet.config, &bip39_seed.as_bytes(), pos as u32);
+
+ if address != wallet.zaddress.read().unwrap()[pos] {
+ return true;
+ }
+ }
+
+ // Check t addresses
+ for pos in 0..wallet.taddresses.read().unwrap().len() {
+ let sk = LightWallet::get_taddr_from_bip39seed(&wallet.config, &bip39_seed.as_bytes(), pos as u32);
+ let address = wallet.address_from_sk(&sk);
+
+ if address != wallet.taddresses.read().unwrap()[pos] {
+ return true;
+ }
+ }
+
+ false
+ }
+
+ /// Automatically fix the bug if it exists in the wallet
+ pub fn fix_bug(client: &LightClient) -> String {
+ use zcash_primitives::transaction::components::amount::DEFAULT_FEE;
+ use std::convert::TryInto;
+
+ if !BugBip39Derivation::has_bug(client) {
+ let r = object!{
+ "has_bug" => false
+ };
+
+ return r.pretty(2);
+ }
+
+ // Tranfer money
+ // 1. The desination is z address #0
+ println!("Sending funds to ourself.");
+ let zaddr = client.do_address()["z_addresses"][0].as_str().unwrap().to_string();
+ let balance_json = client.do_balance();
+ let fee: u64 = DEFAULT_FEE.try_into().unwrap();
+ let amount: u64 = balance_json["zbalance"].as_u64().unwrap()
+ + balance_json["tbalance"].as_u64().unwrap()
+ - fee;
+
+ let txid = if amount > 0 {
+ match client.do_send(vec![(&zaddr, amount, None)]) {
+ Ok(txid) => txid,
+ Err(e) => {
+ let r = object!{
+ "has_bug" => true,
+ "fixed" => false,
+ "error" => e,
+ };
+
+ return r.pretty(2);
+ }
+ }
+ } else {
+ "".to_string()
+ };
+
+
+ // regen addresses
+ let wallet = client.wallet.read().unwrap();
+ let num_zaddrs = wallet.zaddress.read().unwrap().len();
+ let num_taddrs = wallet.taddresses.read().unwrap().len();
+
+ wallet.extsks.write().unwrap().truncate(1);
+ wallet.extfvks.write().unwrap().truncate(1);
+ wallet.zaddress.write().unwrap().truncate(1);
+
+ wallet.tkeys.write().unwrap().truncate(1);
+ wallet.taddresses.write().unwrap().truncate(1);
+
+ for _ in 1..num_zaddrs {
+ wallet.add_zaddr();
+ }
+
+ for _ in 1..num_taddrs {
+ wallet.add_taddr();
+ }
+
+ let r = object!{
+ "has_bug" => true,
+ "fixed" => true,
+ "txid" => txid,
+ };
+
+ return r.pretty(2);
+ }
+}
\ No newline at end of file
diff --git a/lib/src/lightwallet/data.rs b/lib/src/lightwallet/data.rs
index 1ee340e..38128ca 100644
--- a/lib/src/lightwallet/data.rs
+++ b/lib/src/lightwallet/data.rs
@@ -267,7 +267,7 @@ impl Utxo {
let mut address_bytes = vec![0; address_len as usize];
reader.read_exact(&mut address_bytes)?;
let address = String::from_utf8(address_bytes).unwrap();
- assert_eq!(address.chars().take(1).collect::>()[0], 'R');
+ assert_eq!(address.chars().take(1).collect::>()[0], 't');
let mut txid_bytes = [0; 32];
reader.read_exact(&mut txid_bytes)?;
@@ -362,8 +362,12 @@ impl OutgoingTxMetadata {
}
pub struct WalletTx {
+ // Block in which this tx was included
pub block: i32,
+ // Timestamp of Tx. Added in v4
+ pub datetime: u64,
+
// Txid of this transaction. It's duplicated here (It is also the Key in the HashMap that points to this
// WalletTx in LightWallet::txs)
pub txid: TxId,
@@ -386,17 +390,19 @@ pub struct WalletTx {
// All outgoing sapling sends to addresses outside this wallet
pub outgoing_metadata: Vec,
+ // Whether this TxID was downloaded from the server and scanned for Memos
pub full_tx_scanned: bool,
}
impl WalletTx {
pub fn serialized_version() -> u64 {
- return 3;
+ return 4;
}
- pub fn new(height: i32, txid: &TxId) -> Self {
+ pub fn new(height: i32, datetime: u64, txid: &TxId) -> Self {
WalletTx {
block: height,
+ datetime,
txid: txid.clone(),
notes: vec![],
utxos: vec![],
@@ -413,6 +419,12 @@ impl WalletTx {
let block = reader.read_i32::()?;
+ let datetime = if version >= 4 {
+ reader.read_u64::()?
+ } else {
+ 0
+ };
+
let mut txid_bytes = [0u8; 32];
reader.read_exact(&mut txid_bytes)?;
@@ -431,6 +443,7 @@ impl WalletTx {
Ok(WalletTx{
block,
+ datetime,
txid,
notes,
utxos,
@@ -446,6 +459,8 @@ impl WalletTx {
writer.write_i32::(self.block)?;
+ writer.write_u64::(self.datetime)?;
+
writer.write_all(&self.txid.0)?;
Vector::write(&mut writer, &self.notes, |w, nd| nd.write(w))?;
@@ -475,7 +490,8 @@ pub struct SpendableNote {
impl SpendableNote {
pub fn from(txid: TxId, nd: &SaplingNoteData, anchor_offset: usize, extsk: &ExtendedSpendingKey) -> Option {
// Include only notes that haven't been spent, or haven't been included in an unconfirmed spend yet.
- if nd.spent.is_none() && nd.unconfirmed_spent.is_none() {
+ if nd.spent.is_none() && nd.unconfirmed_spent.is_none() &&
+ nd.witnesses.len() >= (anchor_offset + 1) {
let witness = nd.witnesses.get(nd.witnesses.len() - anchor_offset - 1);
witness.map(|w| SpendableNote {
diff --git a/lib/src/lightwallet/startup_helpers.rs b/lib/src/lightwallet/startup_helpers.rs
new file mode 100644
index 0000000..0b5ac59
--- /dev/null
+++ b/lib/src/lightwallet/startup_helpers.rs
@@ -0,0 +1,20 @@
+pub fn report_permission_error() {
+ let user = std::env::var("USER").expect(
+ "Unexpected error reading value of $USER!");
+ let home = std::env::var("HOME").expect(
+ "Unexpected error reading value of $HOME!");
+ let current_executable = std::env::current_exe()
+ .expect("Unexpected error reporting executable path!");
+ eprintln!("USER: {}", user);
+ eprintln!("HOME: {}", home);
+ eprintln!("Executable: {}", current_executable.display());
+ if home == "/" {
+ eprintln!("User {} must have permission to write to '{}.komodo/HUSH3/' .",
+ user,
+ home);
+ } else {
+ eprintln!("User {} must have permission to write to '{}/.komodo/HUSH3/ .",
+ user,
+ home);
+ }
+}
\ No newline at end of file
diff --git a/lib/src/startup_helpers.rs b/lib/src/startup_helpers.rs
new file mode 100644
index 0000000..c597c5d
--- /dev/null
+++ b/lib/src/startup_helpers.rs
@@ -0,0 +1,20 @@
+pub fn report_permission_error() {
+ let user = std::env::var("USER").expect(
+ "Unexpected error reading value of $USER!");
+ let home = std::env::var("HOME").expect(
+ "Unexpected error reading value of $HOME!");
+ let current_executable = std::env::current_exe()
+ .expect("Unexpected error reporting executable path!");
+ eprintln!("USER: {}", user);
+ eprintln!("HOME: {}", home);
+ eprintln!("Executable: {}", current_executable.display());
+ if home == "/" {
+ eprintln!("User {} must have permission to write to '{}.komodo/HUSH3/' .",
+ user,
+ home);
+ } else {
+ eprintln!("User {} must have permission to write to '{}/.komodo/HUSH3/ .",
+ user,
+ home);
+ }
+}
diff --git a/src/main.rs b/src/main.rs
index 8f0e28d..c2ab390 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -2,7 +2,7 @@ use std::io::{Result, Error, ErrorKind};
use std::sync::Arc;
use std::sync::mpsc::{channel, Sender, Receiver};
-use silentdragonlitelib::{commands,
+use silentdragonlitelib::{commands, startup_helpers,
lightclient::{self, LightClient, LightClientConfig},
};
@@ -111,12 +111,17 @@ pub fn main() {
let dangerous = matches.is_present("dangerous");
let nosync = matches.is_present("nosync");
-
let (command_tx, resp_rx) = match startup(server, dangerous, seed, !nosync, command.is_none()) {
Ok(c) => c,
Err(e) => {
eprintln!("Error during startup: {}", e);
error!("Error during startup: {}", e);
+ match e.raw_os_error() {
+ Some(13) => {
+ startup_helpers::report_permission_error();
+ },
+ _ => eprintln!("Something else!")
+ }
return;
}
};
@@ -137,6 +142,10 @@ pub fn main() {
error!("{}", e);
}
}
+
+ // Save before exit
+ command_tx.send(("save".to_string(), vec![])).unwrap();
+ resp_rx.recv().unwrap();
}
}
@@ -151,7 +160,10 @@ fn startup(server: http::Uri, dangerous: bool, seed: Option, first_sync:
std::io::Error::new(ErrorKind::Other, e)
})?;
- let lightclient = Arc::new(LightClient::new(seed, &config, latest_block_height)?);
+ let lightclient = match seed {
+ Some(phrase) => Arc::new(LightClient::new_from_phrase(phrase, &config, latest_block_height)?),
+ None => Arc::new(LightClient::read_from_disk(&config)?)
+ };
// Print startup Messages
info!(""); // Blank line