|
|
@ -198,7 +198,7 @@ impl LightWallet { |
|
|
|
let zdustaddress = zdustextfvk.default_address().unwrap().1; |
|
|
|
|
|
|
|
(zdustaddress) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
pub fn is_shielded_address(addr: &String, config: &LightClientConfig) -> bool { |
|
|
|
match address::RecipientAddress::from_str(addr, |
|
|
@ -403,7 +403,7 @@ impl LightWallet { |
|
|
|
|
|
|
|
Ok((TxId{0: txid_bytes}, WalletTx::read(r).unwrap())) |
|
|
|
})?; |
|
|
|
let txs = txs_tuples.into_iter().collect::<HashMap<TxId, WalletTx>>(); |
|
|
|
let mut txs = txs_tuples.into_iter().collect::<HashMap<TxId, WalletTx>>(); |
|
|
|
|
|
|
|
let chain_name = utils::read_string(&mut reader)?; |
|
|
|
|
|
|
@ -414,7 +414,24 @@ impl LightWallet { |
|
|
|
|
|
|
|
let birthday = reader.read_u64::<LittleEndian>()?; |
|
|
|
|
|
|
|
Ok(LightWallet{ |
|
|
|
// If version <= 8, adjust the "is_spendable" status of each note data
|
|
|
|
if version <= 8 { |
|
|
|
// Collect all spendable keys
|
|
|
|
let spendable_keys: Vec<_> = zkeys.iter() |
|
|
|
.filter(|zk| zk.have_spending_key()).map(|zk| zk.extfvk.clone()) |
|
|
|
.collect(); |
|
|
|
txs.values_mut().for_each(|tx| { |
|
|
|
tx.notes.iter_mut().for_each(|nd| { |
|
|
|
nd.is_spendable = spendable_keys.contains(&nd.extfvk); |
|
|
|
if !nd.is_spendable { |
|
|
|
nd.witnesses.clear(); |
|
|
|
} |
|
|
|
}) |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
let lw = LightWallet{ |
|
|
|
encrypted: encrypted, |
|
|
|
unlocked: !encrypted, // When reading from disk, if wallet is encrypted, it starts off locked.
|
|
|
|
enc_seed: enc_seed, |
|
|
@ -428,7 +445,14 @@ impl LightWallet { |
|
|
|
config: config.clone(), |
|
|
|
birthday, |
|
|
|
total_scan_duration: Arc::new(RwLock::new(vec![Duration::new(0, 0)])), |
|
|
|
}) |
|
|
|
}; |
|
|
|
|
|
|
|
// Do a one-time fix of the spent_at_height for older wallets
|
|
|
|
if version <= 7 { |
|
|
|
lw.fix_spent_at_height(); |
|
|
|
} |
|
|
|
|
|
|
|
Ok(lw) |
|
|
|
} |
|
|
|
|
|
|
|
pub fn write<W: Write>(&self, mut writer: W) -> io::Result<()> { |
|
|
@ -1091,6 +1115,48 @@ impl LightWallet { |
|
|
|
.sum::<u64>() as u64 |
|
|
|
} |
|
|
|
|
|
|
|
pub fn unverified_zbalance(&self, addr: Option<String>) -> u64 { |
|
|
|
let anchor_height = match self.get_target_height_and_anchor_offset() { |
|
|
|
Some((height, anchor_offset)) => height - anchor_offset as u32 - 1, |
|
|
|
None => return 0, |
|
|
|
}; |
|
|
|
|
|
|
|
self.txs |
|
|
|
.read() |
|
|
|
.unwrap() |
|
|
|
.values() |
|
|
|
.map(|tx| { |
|
|
|
tx.notes |
|
|
|
.iter() |
|
|
|
.filter(|nd| nd.spent.is_none() && nd.unconfirmed_spent.is_none()) |
|
|
|
.filter(|nd| { |
|
|
|
// Check to see if we have this note's spending key.
|
|
|
|
self.have_spendingkey_for_extfvk(&nd.extfvk) |
|
|
|
}) |
|
|
|
.filter(|nd| { // TODO, this whole section is shared with verified_balance. Refactor it.
|
|
|
|
match addr.clone() { |
|
|
|
Some(a) => a == encode_payment_address( |
|
|
|
self.config.hrp_sapling_address(), |
|
|
|
&nd.extfvk.fvk.vk |
|
|
|
.into_payment_address(nd.diversifier, &JUBJUB).unwrap() |
|
|
|
), |
|
|
|
None => true |
|
|
|
} |
|
|
|
}) |
|
|
|
.map(|nd| { |
|
|
|
if tx.block as u32 <= anchor_height { |
|
|
|
// If confirmed, then unconfirmed is 0
|
|
|
|
0 |
|
|
|
} else { |
|
|
|
// If confirmed but dont have anchor yet, it is unconfirmed
|
|
|
|
nd.note.value |
|
|
|
} |
|
|
|
}) |
|
|
|
.sum::<u64>() |
|
|
|
}) |
|
|
|
.sum::<u64>() |
|
|
|
} |
|
|
|
|
|
|
|
pub fn verified_zbalance(&self, addr: Option<String>) -> u64 { |
|
|
|
let anchor_height = match self.get_target_height_and_anchor_offset() { |
|
|
|
Some((height, anchor_offset)) => height - anchor_offset as u32 , |
|
|
@ -1835,6 +1901,17 @@ impl LightWallet { |
|
|
|
// Create a write lock
|
|
|
|
let mut txs = self.txs.write().unwrap(); |
|
|
|
|
|
|
|
// Remove the older witnesses from the SaplingNoteData, so that we don't save them
|
|
|
|
// in the wallet, taking up unnecessary space
|
|
|
|
txs.values_mut().for_each(|wtx| { |
|
|
|
wtx.notes |
|
|
|
.iter_mut() |
|
|
|
.filter(|nd| nd.spent.is_some() && nd.spent_at_height.is_some() && nd.spent_at_height.unwrap() < height - (MAX_REORG as i32) - 1) |
|
|
|
.for_each(|nd| { |
|
|
|
nd.witnesses.clear() |
|
|
|
}) |
|
|
|
}); |
|
|
|
|
|
|
|
// Create a Vec containing all unspent nullifiers.
|
|
|
|
// Include only the confirmed spent nullifiers, since unconfirmed ones still need to be included
|
|
|
|
// during scan_block below.
|
|
|
@ -1857,14 +1934,16 @@ impl LightWallet { |
|
|
|
for tx in txs.values_mut() { |
|
|
|
for nd in tx.notes.iter_mut() { |
|
|
|
// Duplicate the most recent witness
|
|
|
|
if let Some(witness) = nd.witnesses.last() { |
|
|
|
let clone = witness.clone(); |
|
|
|
nd.witnesses.push(clone); |
|
|
|
if nd.is_spendable { |
|
|
|
if let Some(witness) = nd.witnesses.last() { |
|
|
|
let clone = witness.clone(); |
|
|
|
nd.witnesses.push(clone); |
|
|
|
} |
|
|
|
// Trim the oldest witnesses
|
|
|
|
nd.witnesses = nd |
|
|
|
.witnesses |
|
|
|
.split_off(nd.witnesses.len().saturating_sub(100)); |
|
|
|
} |
|
|
|
// Trim the oldest witnesses
|
|
|
|
nd.witnesses = nd |
|
|
|
.witnesses |
|
|
|
.split_off(nd.witnesses.len().saturating_sub(100)); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
@ -1872,11 +1951,25 @@ impl LightWallet { |
|
|
|
let nf_refs = nfs.iter().map(|(nf, account, _)| (nf.to_vec(), *account)).collect::<Vec<_>>(); |
|
|
|
let extfvks: Vec<ExtendedFullViewingKey> = self.zkeys.read().unwrap().iter().map(|zk| zk.extfvk.clone()).collect(); |
|
|
|
|
|
|
|
// Create a single mutable slice of all the newly-added witnesses.
|
|
|
|
// Create a single mutable slice of all the wallet's note's witnesses.
|
|
|
|
let mut witness_refs: Vec<_> = txs |
|
|
|
.values_mut() |
|
|
|
.map(|tx| tx.notes.iter_mut().filter_map( |
|
|
|
|nd| if nd.spent.is_none() && nd.unconfirmed_spent.is_none() { nd.witnesses.last_mut() } else { None })) |
|
|
|
.map(|tx|
|
|
|
|
tx.notes.iter_mut() |
|
|
|
.filter_map(|nd|
|
|
|
|
if !nd.is_spendable {
|
|
|
|
// If the note is not spendable, then no point updating it
|
|
|
|
None |
|
|
|
} else if nd.spent.is_none() && nd.unconfirmed_spent.is_none() { |
|
|
|
// Note was not spent
|
|
|
|
nd.witnesses.last_mut()
|
|
|
|
} else if nd.spent.is_some() && nd.spent_at_height.is_some() && nd.spent_at_height.unwrap() < height - (MAX_REORG as i32) - 1 { |
|
|
|
// Note was spent in the last 100 blocks
|
|
|
|
nd.witnesses.last_mut()
|
|
|
|
} else {
|
|
|
|
// If note was old (spent NOT in the last 100 blocks)
|
|
|
|
None
|
|
|
|
})) |
|
|
|
.flatten() |
|
|
|
.collect(); |
|
|
|
|
|
|
@ -1930,6 +2023,7 @@ impl LightWallet { |
|
|
|
// Mark the note as spent, and remove the unconfirmed part of it
|
|
|
|
info!("Marked a note as spent"); |
|
|
|
spent_note.spent = Some(tx.txid); |
|
|
|
spent_note.spent_at_height = Some(height); |
|
|
|
spent_note.unconfirmed_spent = None::<TxId>; |
|
|
|
|
|
|
|
total_shielded_value_spent += spent_note.note.value; |
|
|
@ -1946,7 +2040,7 @@ impl LightWallet { |
|
|
|
// Save notes.
|
|
|
|
for output in tx.shielded_outputs |
|
|
|
{ |
|
|
|
let new_note = SaplingNoteData::new(&self.zkeys.read().unwrap()[output.account].extfvk, output); |
|
|
|
let new_note = SaplingNoteData::new(&self.zkeys.read().unwrap()[output.account], output); |
|
|
|
match LightWallet::note_address(self.config.hrp_sapling_address(), &new_note) { |
|
|
|
Some(a) => { |
|
|
|
info!("Received sapling output to {}", a); |
|
|
@ -1992,6 +2086,22 @@ impl LightWallet { |
|
|
|
Ok(all_txs) |
|
|
|
} |
|
|
|
|
|
|
|
// Add the spent_at_height for each sapling note that has been spent. This field was added in wallet version 8,
|
|
|
|
// so for older wallets, it will need to be added
|
|
|
|
pub fn fix_spent_at_height(&self) { |
|
|
|
// First, build an index of all the txids and the heights at which they were spent.
|
|
|
|
let spent_txid_map: HashMap<_, _> = self.txs.read().unwrap().iter().map(|(txid, wtx)| (txid.clone(), wtx.block)).collect(); |
|
|
|
|
|
|
|
// Go over all the sapling notes that might need updating
|
|
|
|
self.txs.write().unwrap().values_mut().for_each(|wtx| { |
|
|
|
wtx.notes.iter_mut() |
|
|
|
.filter(|nd| nd.spent.is_some() && nd.spent_at_height.is_none()) |
|
|
|
.for_each(|nd| { |
|
|
|
nd.spent_at_height = spent_txid_map.get(&nd.spent.unwrap()).map(|b| *b); |
|
|
|
}) |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
pub fn send_to_address<F> ( |
|
|
|
&self, |
|
|
|
consensus_branch_id: u32, |
|
|
@ -2062,34 +2172,41 @@ impl LightWallet { |
|
|
|
println!("{}: Selecting notes", now() - start_time); |
|
|
|
let target_value = Amount::from_u64(total_value).unwrap() + DEFAULT_FEE ; |
|
|
|
// Select the candidate notes that are eligible to be spent
|
|
|
|
let notes: Vec<_> = self.txs.read().unwrap().iter() |
|
|
|
.map(|(txid, tx)| tx.notes.iter().map(move |note| (*txid, note))) |
|
|
|
.flatten() |
|
|
|
.filter_map(|(txid, note)| { |
|
|
|
// Filter out notes that are already spent
|
|
|
|
if note.spent.is_some() || note.unconfirmed_spent.is_some() { |
|
|
|
None |
|
|
|
} else { |
|
|
|
// Get the spending key for the selected fvk, if we have it
|
|
|
|
let extsk = self.zkeys.read().unwrap().iter() |
|
|
|
.find(|zk| zk.extfvk == note.extfvk) |
|
|
|
.and_then(|zk| zk.extsk.clone()); |
|
|
|
SpendableNote::from(txid, note, anchor_offset, &extsk) |
|
|
|
} |
|
|
|
}) |
|
|
|
.scan(0, |running_total, spendable| { |
|
|
|
|
|
|
|
let value = spendable.note.value; |
|
|
|
let ret = if *running_total < u64::from(target_value) { |
|
|
|
Some(spendable) |
|
|
|
} else { |
|
|
|
None |
|
|
|
}; |
|
|
|
*running_total = *running_total + value; |
|
|
|
ret |
|
|
|
}) |
|
|
|
.collect(); |
|
|
|
let mut candidate_notes: Vec<_> = if transparent_only { |
|
|
|
vec![] |
|
|
|
} else { |
|
|
|
self.txs.read().unwrap().iter() |
|
|
|
.map(|(txid, tx)| tx.notes.iter().map(move |note| (*txid, note))) |
|
|
|
.flatten() |
|
|
|
.filter_map(|(txid, note)| { |
|
|
|
// Filter out notes that are already spent
|
|
|
|
if note.spent.is_some() || note.unconfirmed_spent.is_some() { |
|
|
|
None |
|
|
|
} else { |
|
|
|
// Get the spending key for the selected fvk, if we have it
|
|
|
|
let extsk = self.zkeys.read().unwrap().iter() |
|
|
|
.find(|zk| zk.extfvk == note.extfvk) |
|
|
|
.and_then(|zk| zk.extsk.clone()); |
|
|
|
SpendableNote::from(txid, note, anchor_offset, &extsk) |
|
|
|
} |
|
|
|
}).collect() |
|
|
|
}; |
|
|
|
|
|
|
|
// Sort by highest value-notes first.
|
|
|
|
candidate_notes.sort_by(|a, b| b.note.value.cmp(&a.note.value)); |
|
|
|
// Select the minimum number of notes required to satisfy the target value
|
|
|
|
let notes: Vec<_> = candidate_notes.iter() |
|
|
|
.scan(0, |running_total, spendable| { |
|
|
|
let value = spendable.note.value; |
|
|
|
let ret = if *running_total < u64::from(target_value) { |
|
|
|
Some(spendable) |
|
|
|
} else { |
|
|
|
None |
|
|
|
}; |
|
|
|
*running_total = *running_total + value; |
|
|
|
ret |
|
|
|
}) |
|
|
|
.collect(); |
|
|
|
let mut builder = Builder::new(height); |
|
|
|
|
|
|
|
// A note on t addresses
|
|
|
|