commit b1215f193291ff5039ed2804f6b4f305c88b6908
parent 4522920939cd43352cd414404be45eb7145d304d
Author: kernelkind <kernelkind@gmail.com>
Date: Sat, 15 Mar 2025 13:16:14 -0400
wallet
Signed-off-by: kernelkind <kernelkind@gmail.com>
Diffstat:
2 files changed, 227 insertions(+), 0 deletions(-)
diff --git a/crates/notedeck/src/lib.rs b/crates/notedeck/src/lib.rs
@@ -25,6 +25,7 @@ pub mod ui;
mod unknowns;
mod urls;
mod user_account;
+mod wallet;
pub use accounts::{AccountData, Accounts, AccountsAction, AddAccountAction, SwitchAccountAction};
pub use app::{App, Notedeck};
@@ -52,6 +53,7 @@ pub use timecache::TimeCached;
pub use unknowns::{get_unknown_note_ids, NoteRefsUnkIdAction, SingleUnkIdAction, UnknownIds};
pub use urls::{supported_mime_hosted_at_url, SupportedMimeType, UrlMimes};
pub use user_account::UserAccount;
+pub use wallet::{GlobalWallet, Wallet, WalletError, WalletState, WalletType, WalletUIState};
// export libs
pub use enostr;
diff --git a/crates/notedeck/src/wallet.rs b/crates/notedeck/src/wallet.rs
@@ -0,0 +1,225 @@
+use std::sync::Arc;
+
+use nwc::{
+ nostr::nips::nip47::{NostrWalletConnectURI, PayInvoiceRequest, PayInvoiceResponse},
+ NWC,
+};
+use poll_promise::Promise;
+use tokenator::TokenSerializable;
+use tokio::sync::RwLock;
+
+use crate::{DataPath, TokenHandler};
+
+#[derive(Debug)]
+pub enum WalletState<'a> {
+ Wallet {
+ wallet: &'a mut Wallet,
+ can_create_local_wallet: bool,
+ },
+ NoWallet {
+ state: &'a mut WalletUIState,
+ show_local_only: bool,
+ },
+}
+
+#[derive(Clone, Eq, PartialEq, Debug)]
+pub enum WalletType {
+ Auto,
+ Local,
+}
+
+#[derive(Default, Debug)]
+pub struct WalletUIState {
+ pub buf: String,
+ pub error_msg: Option<WalletError>,
+ pub for_local_only: bool,
+}
+
+#[derive(Debug)]
+pub enum WalletError {
+ InvalidURI,
+}
+
+pub struct Wallet {
+ pub uri: String,
+ wallet: Arc<RwLock<NWC>>,
+ balance: Option<Promise<Result<u64, nwc::Error>>>,
+}
+
+impl std::fmt::Debug for Wallet {
+ fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ write!(f, "Wallet({})", self.uri)
+ }
+}
+
+impl Wallet {
+ pub fn new(uri: String) -> Result<Self, crate::Error> {
+ let nwc_uri = NostrWalletConnectURI::parse(uri.clone())
+ .map_err(|e| crate::Error::Generic(e.to_string()))?;
+
+ let nwc = NWC::new(nwc_uri);
+
+ Ok(Self {
+ uri,
+ wallet: Arc::new(RwLock::new(nwc)),
+ balance: Default::default(),
+ })
+ }
+
+ pub fn get_balance(&mut self) -> Option<&Result<u64, nwc::Error>> {
+ if self.balance.is_none() {
+ self.balance = Some(get_balance(self.wallet.clone()));
+ return None;
+ }
+ let promise = self.balance.as_ref().unwrap();
+
+ if let Some(bal) = promise.ready() {
+ Some(bal)
+ } else {
+ None
+ }
+ }
+
+ pub fn pay_invoice(
+ &mut self,
+ invoice: &str,
+ ) -> Promise<Result<PayInvoiceResponse, nwc::Error>> {
+ pay_invoice(
+ self.wallet.clone(),
+ PayInvoiceRequest::new(invoice.to_owned()),
+ )
+ }
+}
+
+fn get_balance(nwc: Arc<RwLock<NWC>>) -> Promise<Result<u64, nwc::Error>> {
+ let (sender, promise) = Promise::new();
+
+ tokio::spawn(async move {
+ sender.send(nwc.read().await.get_balance().await);
+ });
+
+ promise
+}
+
+fn pay_invoice(
+ nwc: Arc<RwLock<NWC>>,
+ invoice: PayInvoiceRequest,
+) -> Promise<Result<PayInvoiceResponse, nwc::Error>> {
+ let (sender, promise) = Promise::new();
+
+ tokio::spawn(async move {
+ sender.send(nwc.read().await.pay_invoice(invoice).await);
+ });
+
+ promise
+}
+
+impl TokenSerializable for Wallet {
+ fn parse_from_tokens<'a>(
+ parser: &mut tokenator::TokenParser<'a>,
+ ) -> Result<Self, tokenator::ParseError<'a>> {
+ parser.parse_all(|p| {
+ p.parse_token("nwc_uri")?;
+
+ let raw_uri = p.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);
+ }
+}
+
+#[allow(dead_code)]
+pub struct GlobalWallet {
+ pub wallet: Option<Wallet>,
+ pub ui_state: WalletUIState,
+ wallet_handler: TokenHandler,
+}
+
+#[allow(dead_code)]
+impl GlobalWallet {
+ pub fn new(path: &DataPath) -> Self {
+ let wallet_handler =
+ TokenHandler::new(path, crate::DataPathType::Setting, "global_wallet.txt");
+ let wallet = construct_global_wallet(&wallet_handler);
+
+ Self {
+ wallet,
+ ui_state: WalletUIState::default(),
+ wallet_handler,
+ }
+ }
+
+ pub fn save_wallet(&self) {
+ let Some(wallet) = &self.wallet else {
+ // saving with no wallet means delete
+ if let Err(e) = self.wallet_handler.clear() {
+ tracing::error!("Could not clear wallet: {e}");
+ }
+
+ return;
+ };
+
+ match self.wallet_handler.save(wallet, "\t") {
+ Ok(_) => {}
+ Err(e) => tracing::error!("Could not save global wallet: {e}"),
+ }
+ }
+}
+
+fn construct_global_wallet(wallet_handler: &TokenHandler) -> Option<Wallet> {
+ let Ok(res) = wallet_handler.load::<Wallet>("\t") else {
+ return None;
+ };
+
+ let wallet = match res {
+ Ok(wallet) => wallet,
+ Err(e) => {
+ tracing::error!("Error parsing wallet: {:?}", e);
+ return None;
+ }
+ };
+
+ Some(wallet)
+}
+
+#[cfg(test)]
+mod tests {
+ use tokenator::{TokenParser, TokenSerializable, TokenWriter};
+
+ use crate::Wallet;
+
+ const URI: &str = "nostr+walletconnect://b889ff5b1513b641e2a139f661a661364979c5beee91842f8f0ef42ab558e9d4?relay=wss%3A%2F%2Frelay.damus.io&secret=71a8c14c1407c113601079c4302dab36460f0ccd0ad506f1f2dc73b5100e4f3c&lud16=nostr%40nostr.com";
+
+ #[test]
+
+ fn test_uri() {
+ assert!(Wallet::new(URI.to_owned()).is_ok())
+ }
+
+ #[test]
+ fn test_wallet_serialize_deserialize() {
+ let wallet = Wallet::new(URI.to_owned()).unwrap();
+
+ let mut writer = TokenWriter::new("\t");
+ wallet.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_wallet = Wallet::parse_from_tokens(&mut parser);
+
+ assert!(m_new_wallet.is_ok());
+
+ let new_wallet = m_new_wallet.unwrap();
+
+ assert_eq!(wallet.uri, new_wallet.uri);
+ }
+}