use super::{
data_payments::PaymentDetails,
error::{Error, Result},
keys::{get_main_pubkey, store_new_pubkey},
local_store::WalletExclusiveAccess,
wallet_file::{get_wallet, store_created_cash_notes, store_wallet, wallet_lockfile_name},
KeyLessWallet,
};
use crate::{CashNote, MainPubkey, NanoTokens, UniquePubkey};
use fs2::FileExt;
use std::{
collections::{BTreeMap, BTreeSet},
fs::OpenOptions,
path::{Path, PathBuf},
};
use xor_name::XorName;
#[derive(serde::Serialize, serde::Deserialize)]
pub struct WatchOnlyWallet {
main_pubkey: MainPubkey,
wallet_dir: PathBuf,
keyless_wallet: KeyLessWallet,
}
impl WatchOnlyWallet {
#[cfg(test)]
pub(super) fn new(
main_pubkey: MainPubkey,
wallet_dir: &Path,
keyless_wallet: KeyLessWallet,
) -> Self {
Self {
main_pubkey,
wallet_dir: wallet_dir.to_path_buf(),
keyless_wallet,
}
}
pub fn load_from(wallet_dir: &Path, main_pubkey: MainPubkey) -> Result<Self> {
let main_pubkey = match get_main_pubkey(wallet_dir)? {
Some(pk) if pk != main_pubkey => {
return Err(Error::PubKeyMismatch(wallet_dir.to_path_buf()))
}
Some(pk) => pk,
None => {
warn!("No main pub key found when loading wallet from path, storing it now: {main_pubkey:?}");
std::fs::create_dir_all(wallet_dir)?;
store_new_pubkey(wallet_dir, &main_pubkey)?;
main_pubkey
}
};
let keyless_wallet = match get_wallet(wallet_dir)? {
Some(keyless_wallet) => {
debug!(
"Loaded wallet from {wallet_dir:#?} with balance {:?}",
keyless_wallet.balance()
);
keyless_wallet
}
None => {
let keyless_wallet = KeyLessWallet::default();
store_wallet(wallet_dir, &keyless_wallet)?;
keyless_wallet
}
};
Ok(Self {
main_pubkey,
wallet_dir: wallet_dir.to_path_buf(),
keyless_wallet,
})
}
pub fn address(&self) -> MainPubkey {
self.main_pubkey
}
pub fn balance(&self) -> NanoTokens {
self.keyless_wallet.balance()
}
pub fn wallet_dir(&self) -> &Path {
&self.wallet_dir
}
pub fn deposit<'a, T>(&mut self, received_cash_notes: T) -> Result<()>
where
T: IntoIterator<Item = &'a CashNote>,
{
for cash_note in received_cash_notes {
let id = cash_note.unique_pubkey();
if self.keyless_wallet.spent_cash_notes.contains(&id) {
debug!("skipping: cash_note is spent");
continue;
}
if cash_note.derived_pubkey(&self.main_pubkey).is_err() {
debug!("skipping: cash_note is not our key");
continue;
}
let value = cash_note.value()?;
self.keyless_wallet.available_cash_notes.insert(id, value);
}
Ok(())
}
pub fn deposit_and_store_to_disk(&mut self, received_cash_notes: &Vec<CashNote>) -> Result<()> {
if received_cash_notes.is_empty() {
return Ok(());
}
std::fs::create_dir_all(&self.wallet_dir)?;
let exclusive_access = self.lock()?;
self.reload()?;
trace!("Wallet locked and loaded!");
for cash_note in received_cash_notes {
let id = cash_note.unique_pubkey();
if self.keyless_wallet.spent_cash_notes.contains(&id) {
debug!("skipping: cash_note is spent");
continue;
}
if cash_note.derived_pubkey(&self.main_pubkey).is_err() {
debug!("skipping: cash_note is not our key");
continue;
}
let value = cash_note.value()?;
self.keyless_wallet.available_cash_notes.insert(id, value);
store_created_cash_notes([cash_note], &self.wallet_dir)?;
}
self.store(exclusive_access)
}
pub fn reload(&mut self) -> Result<()> {
*self = Self::load_from(&self.wallet_dir, self.main_pubkey)?;
Ok(())
}
pub fn reload_from_disk_or_recreate(&mut self) -> Result<()> {
std::fs::create_dir_all(&self.wallet_dir)?;
let _exclusive_access = self.lock()?;
self.reload()?;
Ok(())
}
pub fn available_cash_notes(&self) -> &BTreeMap<UniquePubkey, NanoTokens> {
&self.keyless_wallet.available_cash_notes
}
pub fn mark_notes_as_spent<'a, T>(&mut self, unique_pubkeys: T)
where
T: IntoIterator<Item = &'a UniquePubkey>,
{
for k in unique_pubkeys {
self.keyless_wallet.available_cash_notes.remove(k);
self.keyless_wallet.spent_cash_notes.insert(*k);
}
}
pub fn spent_cash_notes(&self) -> &BTreeSet<UniquePubkey> {
&self.keyless_wallet.spent_cash_notes
}
pub fn insert_spent_cash_notes<'a, T>(&mut self, spent_cash_notes: T)
where
T: IntoIterator<Item = &'a UniquePubkey>,
{
for pk in spent_cash_notes {
let _ = self.keyless_wallet.spent_cash_notes.insert(*pk);
}
}
pub fn cash_notes_created_for_others(&self) -> &BTreeSet<UniquePubkey> {
&self.keyless_wallet.cash_notes_created_for_others
}
pub fn insert_cash_note_created_for_others(&mut self, unique_pubkey: UniquePubkey) {
let _ = self
.keyless_wallet
.cash_notes_created_for_others
.insert(unique_pubkey);
}
pub fn get_payment_transaction(&self, name: &XorName) -> Option<&PaymentDetails> {
self.keyless_wallet.payment_transactions.get(name)
}
pub fn insert_payment_transaction(&mut self, name: XorName, payment: PaymentDetails) {
self.keyless_wallet
.payment_transactions
.insert(name, payment);
}
pub(super) fn store(&self, exclusive_access: WalletExclusiveAccess) -> Result<()> {
store_wallet(&self.wallet_dir, &self.keyless_wallet)?;
trace!("Releasing wallet lock");
std::mem::drop(exclusive_access);
Ok(())
}
pub(super) fn lock(&self) -> Result<WalletExclusiveAccess> {
let lock = wallet_lockfile_name(&self.wallet_dir);
let file = OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(lock)?;
file.lock_exclusive()?;
Ok(file)
}
}
#[cfg(test)]
mod tests {
use super::WatchOnlyWallet;
use crate::{
genesis::{create_first_cash_note_from_key, GENESIS_CASHNOTE_AMOUNT},
wallet::KeyLessWallet,
MainSecretKey, NanoTokens,
};
use assert_fs::TempDir;
use eyre::Result;
#[test]
fn watchonly_wallet_basics() -> Result<()> {
let main_sk = MainSecretKey::random();
let main_pubkey = main_sk.main_pubkey();
let wallet_dir = TempDir::new()?;
let wallet = WatchOnlyWallet::new(main_pubkey, &wallet_dir, KeyLessWallet::default());
assert_eq!(wallet_dir.path(), wallet.wallet_dir());
assert_eq!(main_pubkey, wallet.address());
assert_eq!(NanoTokens::zero(), wallet.balance());
assert!(wallet.cash_notes_created_for_others().is_empty());
assert!(wallet.spent_cash_notes().is_empty());
assert!(wallet.available_cash_notes().is_empty());
Ok(())
}
#[tokio::test]
async fn watchonly_wallet_to_and_from_file() -> Result<()> {
let main_sk = MainSecretKey::random();
let main_pubkey = main_sk.main_pubkey();
let cash_note = create_first_cash_note_from_key(&main_sk)?;
let wallet_dir = TempDir::new()?;
let mut wallet = WatchOnlyWallet::new(main_pubkey, &wallet_dir, KeyLessWallet::default());
wallet.deposit_and_store_to_disk(&vec![cash_note])?;
let deserialised = WatchOnlyWallet::load_from(&wallet_dir, main_pubkey)?;
assert_eq!(deserialised.wallet_dir(), wallet.wallet_dir());
assert_eq!(deserialised.address(), wallet.address());
assert_eq!(GENESIS_CASHNOTE_AMOUNT, wallet.balance().as_nano());
assert_eq!(GENESIS_CASHNOTE_AMOUNT, deserialised.balance().as_nano());
assert!(wallet.cash_notes_created_for_others().is_empty());
assert!(deserialised.cash_notes_created_for_others().is_empty());
assert!(wallet.spent_cash_notes().is_empty());
assert!(deserialised.spent_cash_notes().is_empty());
assert_eq!(1, wallet.available_cash_notes().len());
assert_eq!(1, deserialised.available_cash_notes().len());
assert_eq!(
deserialised.available_cash_notes(),
wallet.available_cash_notes()
);
Ok(())
}
#[tokio::test]
async fn watchonly_wallet_deposit_cash_notes() -> Result<()> {
let main_sk = MainSecretKey::random();
let main_pubkey = main_sk.main_pubkey();
let wallet_dir = TempDir::new()?;
let mut wallet = WatchOnlyWallet::new(main_pubkey, &wallet_dir, KeyLessWallet::default());
let owned_cash_note = create_first_cash_note_from_key(&main_sk)?;
wallet.deposit(&vec![owned_cash_note.clone()])?;
assert_eq!(GENESIS_CASHNOTE_AMOUNT, wallet.balance().as_nano());
let non_owned_cash_note = create_first_cash_note_from_key(&MainSecretKey::random())?;
wallet.deposit(&vec![non_owned_cash_note])?;
assert_eq!(GENESIS_CASHNOTE_AMOUNT, wallet.balance().as_nano());
wallet.deposit(&vec![owned_cash_note])?;
assert_eq!(GENESIS_CASHNOTE_AMOUNT, wallet.balance().as_nano());
Ok(())
}
#[tokio::test]
async fn watchonly_wallet_reload() -> Result<()> {
let main_sk = MainSecretKey::random();
let main_pubkey = main_sk.main_pubkey();
let wallet_dir = TempDir::new()?;
let mut wallet = WatchOnlyWallet::new(main_pubkey, &wallet_dir, KeyLessWallet::default());
let cash_note = create_first_cash_note_from_key(&main_sk)?;
wallet.deposit(&vec![cash_note.clone()])?;
assert_eq!(GENESIS_CASHNOTE_AMOUNT, wallet.balance().as_nano());
wallet.reload()?;
assert_eq!(NanoTokens::zero(), wallet.balance());
wallet.deposit_and_store_to_disk(&vec![cash_note])?;
assert_eq!(GENESIS_CASHNOTE_AMOUNT, wallet.balance().as_nano());
wallet.reload()?;
assert_eq!(GENESIS_CASHNOTE_AMOUNT, wallet.balance().as_nano());
Ok(())
}
}