notedeck

One damus client to rule them all
git clone git://jb55.com/notedeck
Log | Files | Refs | README | LICENSE

commit f357935cca316514bddf53c69933316c6574a9bc
parent 10d6d740b837427c687c68f95702351184ad78f8
Author: kernelkind <kernelkind@gmail.com>
Date:   Wed, 25 Jun 2025 16:49:24 -0400

move (de)serialization of wallets & accounts to own structs

for easy cloning

Signed-off-by: kernelkind <kernelkind@gmail.com>

Diffstat:
Mcrates/notedeck/src/account/accounts.rs | 42+++++++++++++++++++++++++++++++++++++++---
Mcrates/notedeck/src/storage/account_storage.rs | 20+++++++++++---------
Mcrates/notedeck/src/user_account.rs | 51+++++++++++++++++++++++++++++++++++++++------------
Mcrates/notedeck/src/wallet.rs | 149+++++++++++++++++++++++++++++++++++++++++++++++--------------------------------
Mcrates/notedeck/src/zaps/default_zap.rs | 12+++++++++++-
5 files changed, 188 insertions(+), 86 deletions(-)

diff --git a/crates/notedeck/src/account/accounts.rs b/crates/notedeck/src/account/accounts.rs @@ -3,6 +3,7 @@ use tracing::{debug, error, info}; use crate::account::cache::AccountCache; use crate::account::mute::AccountMutedData; use crate::account::relay::{AccountRelayData, RelayDefaults}; +use crate::user_account::UserAccountSerializable; use crate::{AccountStorage, MuteFun, RelaySpec, SingleUnkIdAction, UserAccount}; use enostr::{ClientMessage, FilledKeypair, Keypair, Pubkey, RelayPool}; use nostrdb::{Ndb, Note, Transaction}; @@ -32,7 +33,8 @@ impl Accounts { match keystore.get_accounts() { Ok(accounts) => { for account in accounts { - cache.add(account); + // TODO(kernelkind): this will get processed in a later commit + let _ = add_account_from_storage(&mut cache, account); } } Err(e) => { @@ -94,7 +96,7 @@ impl Accounts { }; if let Some(key_store) = &self.key_store { - if let Err(e) = key_store.write_account(acc.get_acc()) { + if let Err(e) = key_store.write_account(&acc.get_acc().into()) { tracing::error!("Could not add key for {:?}: {e}", kp.pubkey); } } @@ -118,7 +120,7 @@ impl Accounts { return false; }; - if let Err(err) = key_store.write_account(cur_acc) { + if let Err(err) = key_store.write_account(&cur_acc.into()) { tracing::error!("Could not add account {:?} to storage: {err}", cur_acc.key); return false; } @@ -450,6 +452,40 @@ impl<'a> AccType<'a> { } } +fn add_account_from_storage( + cache: &mut AccountCache, + user_account_serializable: UserAccountSerializable, +) -> SingleUnkIdAction { + let Some(acc) = get_acc_from_storage(user_account_serializable) else { + return SingleUnkIdAction::NoAction; + }; + + let pk = acc.key.pubkey; + cache.add(acc); + + SingleUnkIdAction::pubkey(pk) +} + +fn get_acc_from_storage(user_account_serializable: UserAccountSerializable) -> Option<UserAccount> { + let keypair = user_account_serializable.key; + + let mut wallet = None; + if let Some(wallet_s) = user_account_serializable.wallet { + let m_wallet: Result<crate::ZapWallet, crate::Error> = wallet_s.into(); + match m_wallet { + Ok(w) => wallet = Some(w), + Err(e) => { + tracing::error!("Problem creating wallet from disk: {e}"); + } + }; + } + + Some(UserAccount { + key: keypair, + wallet, + }) +} + enum RelayAction { Add, Remove, diff --git a/crates/notedeck/src/storage/account_storage.rs b/crates/notedeck/src/storage/account_storage.rs @@ -1,4 +1,4 @@ -use crate::{Result, UserAccount}; +use crate::{user_account::UserAccountSerializable, Result}; use enostr::{Keypair, Pubkey, SerializableKeypair}; use tokenator::{TokenParser, TokenSerializable, TokenWriter}; @@ -21,7 +21,7 @@ impl AccountStorage { } } - pub fn write_account(&self, account: &UserAccount) -> Result<()> { + pub fn write_account(&self, account: &UserAccountSerializable) -> Result<()> { let mut writer = TokenWriter::new("\t"); account.serialize_tokens(&mut writer); write_file( @@ -31,7 +31,7 @@ impl AccountStorage { ) } - pub fn get_accounts(&self) -> Result<Vec<UserAccount>> { + pub fn get_accounts(&self) -> Result<Vec<UserAccountSerializable>> { let keys = self .accounts_directory .get_files()? @@ -79,16 +79,18 @@ impl AccountStorage { } } -fn deserialize_storage(serialized: &str) -> Result<UserAccount> { +fn deserialize_storage(serialized: &str) -> Result<UserAccountSerializable> { let data = serialized.split("\t").collect::<Vec<&str>>(); let mut parser = TokenParser::new(&data); - if let Ok(acc) = UserAccount::parse_from_tokens(&mut parser) { + if let Ok(acc) = UserAccountSerializable::parse_from_tokens(&mut parser) { return Ok(acc); } // try old deserialization way - Ok(UserAccount::new(old_deserialization(serialized)?)) + Ok(UserAccountSerializable::new(old_deserialization( + serialized, + )?)) } fn old_deserialization(serialized: &str) -> Result<Keypair> { @@ -118,7 +120,7 @@ mod tests { fn test_basic() { let kp = enostr::FullKeypair::generate().to_keypair(); let storage = AccountStorage::mock().unwrap(); - let resp = storage.write_account(&UserAccount::new(kp.clone())); + let resp = storage.write_account(&UserAccountSerializable::new(kp.clone())); assert!(resp.is_ok()); assert_num_storage(&storage.get_accounts(), 1); @@ -127,7 +129,7 @@ mod tests { assert_num_storage(&storage.get_accounts(), 0); } - fn assert_num_storage(keys_response: &Result<Vec<UserAccount>>, n: usize) { + fn assert_num_storage(keys_response: &Result<Vec<UserAccountSerializable>>, n: usize) { match keys_response { Ok(keys) => { assert_eq!(keys.len(), n); @@ -143,7 +145,7 @@ mod tests { let kp = enostr::FullKeypair::generate().to_keypair(); let storage = AccountStorage::mock().unwrap(); - let _ = storage.write_account(&UserAccount::new(kp.clone())); + let _ = storage.write_account(&UserAccountSerializable::new(kp.clone())); assert_num_storage(&storage.get_accounts(), 1); let resp = storage.select_key(Some(kp.pubkey)); diff --git a/crates/notedeck/src/user_account.rs b/crates/notedeck/src/user_account.rs @@ -1,7 +1,7 @@ use enostr::{Keypair, KeypairUnowned}; use tokenator::{ParseError, TokenParser, TokenSerializable}; -use crate::wallet::ZapWallet; +use crate::wallet::{WalletSerializable, ZapWallet}; pub struct UserAccount { pub key: Keypair, @@ -26,12 +26,37 @@ impl UserAccount { } } +pub struct UserAccountSerializable { + pub key: Keypair, + pub wallet: Option<WalletSerializable>, +} + +impl UserAccountSerializable { + pub fn new(key: Keypair) -> Self { + Self { key, wallet: None } + } + + pub fn with_wallet(mut self, wallet: WalletSerializable) -> Self { + self.wallet = Some(wallet); + self + } +} + +impl From<&UserAccount> for UserAccountSerializable { + fn from(value: &UserAccount) -> Self { + Self { + key: value.key.clone(), + wallet: value.wallet.as_ref().map(|z| z.into()), + } + } +} + enum UserAccountRoute { Key(Keypair), - Wallet(ZapWallet), + Wallet(WalletSerializable), } -impl TokenSerializable for UserAccount { +impl TokenSerializable for UserAccountSerializable { fn parse_from_tokens<'a>( parser: &mut tokenator::TokenParser<'a>, ) -> Result<Self, tokenator::ParseError<'a>> { @@ -43,7 +68,11 @@ impl TokenSerializable for UserAccount { parser, &[ |p| Ok(UserAccountRoute::Key(Keypair::parse_from_tokens(p)?)), - |p| Ok(UserAccountRoute::Wallet(ZapWallet::parse_from_tokens(p)?)), + |p| { + Ok(UserAccountRoute::Wallet( + WalletSerializable::parse_from_tokens(p)?, + )) + }, ], ); @@ -63,7 +92,7 @@ impl TokenSerializable for UserAccount { return Err(ParseError::DecodeFailed); }; - let mut user_acc = UserAccount::new(key); + let mut user_acc = UserAccountSerializable::new(key); if let Some(wallet) = m_wallet { user_acc = user_acc.with_wallet(wallet); @@ -88,17 +117,15 @@ mod tests { use enostr::FullKeypair; use tokenator::{TokenParser, TokenSerializable, TokenWriter}; - use crate::Wallet; - - use super::UserAccount; + use crate::{user_account::UserAccountSerializable, wallet::WalletSerializable}; const URI: &str = "nostr+walletconnect://b889ff5b1513b641e2a139f661a661364979c5beee91842f8f0ef42ab558e9d4?relay=wss%3A%2F%2Frelay.damus.io&secret=71a8c14c1407c113601079c4302dab36460f0ccd0ad506f1f2dc73b5100e4f3c&lud16=nostr%40nostr.com"; #[test] fn test_user_account_serialize_deserialize() { let kp = FullKeypair::generate(); - let acc = UserAccount::new(kp.to_keypair()) - .with_wallet(Wallet::new(URI.to_owned()).unwrap().into()); + let acc = UserAccountSerializable::new(kp.to_keypair()) + .with_wallet(WalletSerializable::new(URI.to_owned())); let mut writer = TokenWriter::new("\t"); acc.serialize_tokens(&mut writer); @@ -107,7 +134,7 @@ mod tests { let data = &serialized.split("\t").collect::<Vec<&str>>(); let mut parser = TokenParser::new(data); - let m_new_acc = UserAccount::parse_from_tokens(&mut parser); + let m_new_acc = UserAccountSerializable::parse_from_tokens(&mut parser); assert!(m_new_acc.is_ok()); let new_acc = m_new_acc.unwrap(); @@ -118,6 +145,6 @@ mod tests { panic!(); }; - assert_eq!(wallet.wallet.uri, URI); + assert_eq!(wallet.uri, URI); } } diff --git a/crates/notedeck/src/wallet.rs b/crates/notedeck/src/wallet.rs @@ -62,6 +62,21 @@ pub struct Wallet { balance: Option<Promise<Result<u64, nwc::Error>>>, } +#[derive(Clone)] +pub struct WalletSerializable { + pub uri: String, + pub default_mzap: Option<UserZapMsats>, +} + +impl WalletSerializable { + pub fn new(uri: String) -> Self { + Self { + uri, + default_mzap: None, + } + } +} + impl std::fmt::Debug for Wallet { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { write!(f, "Wallet({})", self.uri) @@ -127,26 +142,6 @@ fn pay_invoice( promise } -impl TokenSerializable for Wallet { - fn parse_from_tokens<'a>( - parser: &mut tokenator::TokenParser<'a>, - ) -> Result<Self, tokenator::ParseError<'a>> { - parser.parse_token("nwc_uri")?; - - let raw_uri = parser.pull_token()?; - - let wallet = - Wallet::new(raw_uri.to_owned()).map_err(|_| tokenator::ParseError::DecodeFailed)?; - - Ok(wallet) - } - - fn serialize_tokens(&self, writer: &mut tokenator::TokenWriter) { - writer.write_token("nwc_uri"); - writer.write_token(&self.uri); - } -} - pub struct GlobalWallet { pub wallet: Option<ZapWallet>, pub ui_state: WalletUIState, @@ -176,7 +171,8 @@ impl GlobalWallet { return; }; - match self.wallet_handler.save(wallet, "\t") { + let serializable: WalletSerializable = wallet.into(); + match self.wallet_handler.save(&serializable, "\t") { Ok(_) => {} Err(e) => tracing::error!("Could not save global wallet: {e}"), } @@ -184,12 +180,15 @@ impl GlobalWallet { } fn construct_global_wallet(wallet_handler: &TokenHandler) -> Option<ZapWallet> { - let Ok(res) = wallet_handler.load::<ZapWallet>("\t") else { + let Ok(res) = wallet_handler.load::<WalletSerializable>("\t") else { return None; }; let wallet = match res { - Ok(wallet) => wallet, + Ok(wallet) => { + let m_zap_wallet: Result<ZapWallet, crate::Error> = wallet.into(); + m_zap_wallet.ok()? + } Err(e) => { tracing::error!("Error parsing wallet: {:?}", e); return None; @@ -206,11 +205,49 @@ pub struct ZapWallet { } enum ZapWalletRoute { - Wallet(Wallet), + Wallet(String), DefaultZapMsats(UserZapMsats), } -impl TokenSerializable for ZapWallet { +impl ZapWallet { + pub fn new(wallet: Wallet) -> Self { + Self { + wallet, + default_zap: DefaultZapMsats::default(), + } + } + + pub fn with_default_zap_msats(mut self, msats: u64) -> Self { + self.default_zap.set_user_selection(msats); + self + } +} + +impl From<Wallet> for ZapWallet { + fn from(value: Wallet) -> Self { + ZapWallet::new(value) + } +} + +impl From<&ZapWallet> for WalletSerializable { + fn from(value: &ZapWallet) -> Self { + Self { + uri: value.wallet.uri.to_string(), + default_mzap: value.default_zap.try_into_user(), + } + } +} + +impl From<WalletSerializable> for Result<ZapWallet, crate::Error> { + fn from(value: WalletSerializable) -> Result<ZapWallet, crate::Error> { + Ok(ZapWallet { + wallet: Wallet::new(value.uri)?, + default_zap: DefaultZapMsats::from_user(value.default_mzap), + }) + } +} + +impl TokenSerializable for WalletSerializable { fn parse_from_tokens<'a>( parser: &mut tokenator::TokenParser<'a>, ) -> Result<Self, tokenator::ParseError<'a>> { @@ -220,7 +257,12 @@ impl TokenSerializable for ZapWallet { let res = TokenParser::alt( parser, &[ - |p| Ok(ZapWalletRoute::Wallet(Wallet::parse_from_tokens(p)?)), + |p| { + p.parse_token("nwc_uri")?; + let raw_uri = p.pull_token()?; + + Ok(ZapWalletRoute::Wallet(raw_uri.to_string())) + }, |p| { Ok(ZapWalletRoute::DefaultZapMsats( UserZapMsats::parse_from_tokens(p)?, @@ -245,49 +287,27 @@ impl TokenSerializable for ZapWallet { return Err(ParseError::DecodeFailed); }; - let mut zap_wallet = ZapWallet::new(wallet); - - let default_zap = DefaultZapMsats::from_user(m_default_zap); - - zap_wallet.default_zap = default_zap; - - Ok(zap_wallet) + Ok(WalletSerializable { + uri: wallet, + default_mzap: m_default_zap, + }) } fn serialize_tokens(&self, writer: &mut tokenator::TokenWriter) { - self.wallet.serialize_tokens(writer); - - if let Some(user_zap_msats) = self.default_zap.try_into_user() { - user_zap_msats.serialize_tokens(writer); - } - } -} + writer.write_token("nwc_uri"); + writer.write_token(&self.uri); -impl ZapWallet { - pub fn new(wallet: Wallet) -> Self { - Self { - wallet, - default_zap: DefaultZapMsats::default(), + if let Some(msats) = &self.default_mzap { + msats.serialize_tokens(writer); } } - - pub fn with_default_zap_msats(mut self, msats: u64) -> Self { - self.default_zap.set_user_selection(msats); - self - } -} - -impl From<Wallet> for ZapWallet { - fn from(value: Wallet) -> Self { - ZapWallet::new(value) - } } #[cfg(test)] mod tests { use tokenator::{TokenParser, TokenSerializable, TokenWriter}; - use crate::Wallet; + use crate::{wallet::WalletSerializable, Wallet}; use super::ZapWallet; @@ -301,7 +321,7 @@ mod tests { #[test] fn test_wallet_serialize_deserialize() { - let wallet = Wallet::new(URI.to_owned()).unwrap(); + let wallet = WalletSerializable::new(URI.to_owned()); let mut writer = TokenWriter::new("\t"); wallet.serialize_tokens(&mut writer); @@ -309,7 +329,7 @@ mod tests { let data = &serialized.split("\t").collect::<Vec<&str>>(); let mut parser = TokenParser::new(data); - let m_new_wallet = Wallet::parse_from_tokens(&mut parser); + let m_new_wallet = WalletSerializable::parse_from_tokens(&mut parser); assert!(m_new_wallet.is_ok()); @@ -325,13 +345,20 @@ mod tests { ZapWallet::new(Wallet::new(URI.to_owned()).unwrap()).with_default_zap_msats(MSATS); let mut writer = TokenWriter::new("\t"); - zap_wallet.serialize_tokens(&mut writer); + + let serializable: WalletSerializable = (&zap_wallet).into(); + serializable.serialize_tokens(&mut writer); let serialized = writer.str(); let data = &serialized.split("\t").collect::<Vec<&str>>(); let mut parser = TokenParser::new(data); - let m_new_zap_wallet = ZapWallet::parse_from_tokens(&mut parser); + let m_deserialized = WalletSerializable::parse_from_tokens(&mut parser); + assert!(m_deserialized.is_ok()); + + let deserialized = m_deserialized.unwrap(); + + let m_new_zap_wallet: Result<ZapWallet, crate::Error> = deserialized.into(); assert!(m_new_zap_wallet.is_ok()); diff --git a/crates/notedeck/src/zaps/default_zap.rs b/crates/notedeck/src/zaps/default_zap.rs @@ -11,6 +11,16 @@ pub struct DefaultZapMsats { } impl DefaultZapMsats { + pub fn from_msats(msats: Option<u64>) -> Self { + let mut default = DefaultZapMsats::default(); + + if let Some(msats) = msats { + default.set_user_selection(msats); + default.pending.write_msats(msats); + } + + default + } pub fn from_user(value: Option<UserZapMsats>) -> Self { let mut obj = match value { Some(user_msats) => { @@ -50,7 +60,7 @@ impl DefaultZapMsats { } } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct UserZapMsats { pub msats: u64, }