Procházet zdrojové kódy

feat(wallet): make wallet single mint and unit

feat(wallet): cli use mint with one url and unit

feat(wallet): remove p2pk keys from wallet

feat(wallet): multimint wallet
thesimplekid před 8 měsíci
rodič
revize
04a463be1f

+ 0 - 2
.github/workflows/ci.yml

@@ -30,7 +30,6 @@ jobs:
             -p cdk --no-default-features,
             -p cdk --no-default-features --features wallet,
             -p cdk --no-default-features --features mint,
-            -p cdk --no-default-features --features wallet --features nostr,
             -p cdk-redb,
             -p cdk-sqlite,
             --bin cdk-cli,
@@ -68,7 +67,6 @@ jobs:
             -p cdk,
             -p cdk --no-default-features,
             -p cdk --no-default-features --features wallet,
-            -p cdk --no-default-features --features wallet --features nostr,
             -p cdk-js
           ]
     steps:

+ 1 - 1
.helix/languages.toml

@@ -1,2 +1,2 @@
 [language-server.rust-analyzer.config]
-cargo = { features = ["wallet", "mint", "nostr"] }
+cargo = { features = ["wallet", "mint"] }

+ 34 - 132
bindings/cdk-js/src/wallet.rs

@@ -1,12 +1,10 @@
 //! Wallet Js Bindings
 
 use std::ops::Deref;
-use std::str::FromStr;
 use std::sync::Arc;
 
 use cdk::amount::SplitTarget;
-use cdk::nuts::Proofs;
-use cdk::url::UncheckedUrl;
+use cdk::nuts::{Proofs, SecretKey};
 use cdk::wallet::Wallet;
 use cdk::Amount;
 use cdk_rexie::RexieWalletDatabase;
@@ -43,38 +41,10 @@ impl From<Wallet> for JsWallet {
 #[wasm_bindgen(js_class = Wallet)]
 impl JsWallet {
     #[wasm_bindgen(constructor)]
-    pub async fn new(seed: Vec<u8>, p2pk_signing_keys: Vec<JsSecretKey>) -> Self {
+    pub async fn new(mints_url: String, unit: JsCurrencyUnit, seed: Vec<u8>) -> Self {
         let db = RexieWalletDatabase::new().await.unwrap();
 
-        Wallet::new(
-            Arc::new(db),
-            &seed,
-            p2pk_signing_keys
-                .into_iter()
-                .map(|s| s.deref().clone())
-                .collect(),
-        )
-        .into()
-    }
-
-    #[wasm_bindgen(js_name = unitBalance)]
-    pub async fn unit_balance(&self, unit: JsCurrencyUnit) -> Result<JsAmount> {
-        Ok(self
-            .inner
-            .unit_balance(unit.into())
-            .await
-            .map_err(into_err)?
-            .into())
-    }
-
-    #[wasm_bindgen(js_name = pendingUnitBalance)]
-    pub async fn pending_unit_balance(&self, unit: JsCurrencyUnit) -> Result<JsAmount> {
-        Ok(self
-            .inner
-            .pending_unit_balance(unit.into())
-            .await
-            .map_err(into_err)?
-            .into())
+        Wallet::new(&mints_url, unit.into(), Arc::new(db), &seed).into()
     }
 
     #[wasm_bindgen(js_name = totalBalance)]
@@ -92,64 +62,36 @@ impl JsWallet {
     }
 
     #[wasm_bindgen(js_name = checkAllPendingProofs)]
-    pub async fn check_all_pending_proofs(
-        &self,
-        mint_url: Option<String>,
-        unit: Option<JsCurrencyUnit>,
-    ) -> Result<JsAmount> {
-        let mint_url = match mint_url {
-            Some(url) => Some(UncheckedUrl::from_str(&url).map_err(into_err)?),
-            None => None,
-        };
-
+    pub async fn check_all_pending_proofs(&self) -> Result<JsAmount> {
         Ok(self
             .inner
-            .check_all_pending_proofs(mint_url, unit.map(|u| u.into()))
+            .check_all_pending_proofs()
             .await
             .map_err(into_err)?
             .into())
     }
 
-    #[wasm_bindgen(js_name = mintBalances)]
-    pub async fn mint_balances(&self) -> Result<JsValue> {
-        let mint_balances = self.inner.mint_balances().await.map_err(into_err)?;
-
-        Ok(serde_wasm_bindgen::to_value(&mint_balances)?)
-    }
-
-    #[wasm_bindgen(js_name = addMint)]
-    pub async fn add_mint(&self, mint_url: String) -> Result<Option<JsMintInfo>> {
-        let mint_url = UncheckedUrl::from_str(&mint_url).map_err(into_err)?;
-
+    #[wasm_bindgen(js_name = getMintInfo)]
+    pub async fn get_mint_info(&self) -> Result<Option<JsMintInfo>> {
         Ok(self
             .inner
-            .add_mint(mint_url)
+            .get_mint_info()
             .await
             .map_err(into_err)?
             .map(|i| i.into()))
     }
 
     #[wasm_bindgen(js_name = refreshMint)]
-    pub async fn refresh_mint_keys(&self, mint_url: String) -> Result<()> {
-        let mint_url = UncheckedUrl::from_str(&mint_url).map_err(into_err)?;
-        self.inner
-            .refresh_mint_keys(&mint_url)
-            .await
-            .map_err(into_err)?;
+    pub async fn refresh_mint_keys(&self) -> Result<()> {
+        self.inner.refresh_mint_keys().await.map_err(into_err)?;
         Ok(())
     }
 
     #[wasm_bindgen(js_name = mintQuote)]
-    pub async fn mint_quote(
-        &mut self,
-        mint_url: String,
-        amount: u64,
-        unit: JsCurrencyUnit,
-    ) -> Result<JsMintQuote> {
-        let mint_url = UncheckedUrl::from_str(&mint_url).map_err(into_err)?;
+    pub async fn mint_quote(&mut self, amount: u64) -> Result<JsMintQuote> {
         let quote = self
             .inner
-            .mint_quote(mint_url, unit.into(), amount.into())
+            .mint_quote(amount.into())
             .await
             .map_err(into_err)?;
 
@@ -157,16 +99,10 @@ impl JsWallet {
     }
 
     #[wasm_bindgen(js_name = mintQuoteStatus)]
-    pub async fn mint_quote_status(
-        &self,
-        mint_url: String,
-        quote_id: String,
-    ) -> Result<JsMintQuoteBolt11Response> {
-        let mint_url = UncheckedUrl::from_str(&mint_url).map_err(into_err)?;
-
+    pub async fn mint_quote_status(&self, quote_id: String) -> Result<JsMintQuoteBolt11Response> {
         let quote = self
             .inner
-            .mint_quote_status(mint_url, &quote_id)
+            .mint_quote_status(&quote_id)
             .await
             .map_err(into_err)?;
 
@@ -183,7 +119,6 @@ impl JsWallet {
     #[wasm_bindgen(js_name = mint)]
     pub async fn mint(
         &mut self,
-        mint_url: String,
         quote_id: String,
         p2pk_condition: Option<JsP2PKSpendingConditions>,
         htlc_condition: Option<JsHTLCSpendingConditions>,
@@ -192,7 +127,6 @@ impl JsWallet {
         let target = split_target_amount
             .map(|a| SplitTarget::Value(*a.deref()))
             .unwrap_or_default();
-        let mint_url = UncheckedUrl::from_str(&mint_url).map_err(into_err)?;
         let conditions = match (p2pk_condition, htlc_condition) {
             (Some(_), Some(_)) => {
                 return Err(JsValue::from_str(
@@ -206,7 +140,7 @@ impl JsWallet {
 
         Ok(self
             .inner
-            .mint(mint_url, &quote_id, target, conditions)
+            .mint(&quote_id, target, conditions)
             .await
             .map_err(into_err)?
             .into())
@@ -215,20 +149,12 @@ impl JsWallet {
     #[wasm_bindgen(js_name = meltQuote)]
     pub async fn melt_quote(
         &mut self,
-        mint_url: String,
-        unit: JsCurrencyUnit,
         request: String,
         mpp_amount: Option<JsAmount>,
     ) -> Result<JsMeltQuote> {
-        let mint_url = UncheckedUrl::from_str(&mint_url).map_err(into_err)?;
         let melt_quote = self
             .inner
-            .melt_quote(
-                mint_url,
-                unit.into(),
-                request,
-                mpp_amount.map(|a| *a.deref()),
-            )
+            .melt_quote(request, mpp_amount.map(|a| *a.deref()))
             .await
             .map_err(into_err)?;
 
@@ -236,16 +162,10 @@ impl JsWallet {
     }
 
     #[wasm_bindgen(js_name = meltQuoteStatus)]
-    pub async fn melt_quote_status(
-        &self,
-        mint_url: String,
-        quote_id: String,
-    ) -> Result<JsMeltQuoteBolt11Response> {
-        let mint_url = UncheckedUrl::from_str(&mint_url).map_err(into_err)?;
-
+    pub async fn melt_quote_status(&self, quote_id: String) -> Result<JsMeltQuoteBolt11Response> {
         let quote = self
             .inner
-            .melt_quote_status(mint_url, &quote_id)
+            .melt_quote_status(&quote_id)
             .await
             .map_err(into_err)?;
 
@@ -255,31 +175,35 @@ impl JsWallet {
     #[wasm_bindgen(js_name = melt)]
     pub async fn melt(
         &mut self,
-        mint_url: String,
         quote_id: String,
         split_target_amount: Option<JsAmount>,
     ) -> Result<JsMelted> {
         let target = split_target_amount
             .map(|a| SplitTarget::Value(*a.deref()))
             .unwrap_or_default();
-        let mint_url = UncheckedUrl::from_str(&mint_url).map_err(into_err)?;
 
-        let melted = self
-            .inner
-            .melt(&mint_url, &quote_id, target)
-            .await
-            .map_err(into_err)?;
+        let melted = self.inner.melt(&quote_id, target).await.map_err(into_err)?;
 
         Ok(melted.into())
     }
 
     #[wasm_bindgen(js_name = receive)]
-    pub async fn receive(&mut self, encoded_token: String, preimages: JsValue) -> Result<JsAmount> {
-        let preimages: Option<Vec<String>> = serde_wasm_bindgen::from_value(preimages)?;
+    pub async fn receive(
+        &mut self,
+        encoded_token: String,
+        signing_keys: Vec<JsSecretKey>,
+        preimages: Vec<String>,
+    ) -> Result<JsAmount> {
+        let signing_keys: Vec<SecretKey> = signing_keys.iter().map(|s| s.deref().clone()).collect();
 
         Ok(self
             .inner
-            .receive(&encoded_token, &SplitTarget::default(), preimages)
+            .receive(
+                &encoded_token,
+                &SplitTarget::default(),
+                &signing_keys,
+                &preimages,
+            )
             .await
             .map_err(into_err)?
             .into())
@@ -289,8 +213,6 @@ impl JsWallet {
     #[wasm_bindgen(js_name = send)]
     pub async fn send(
         &mut self,
-        mint_url: String,
-        unit: JsCurrencyUnit,
         memo: Option<String>,
         amount: u64,
         p2pk_condition: Option<JsP2PKSpendingConditions>,
@@ -308,20 +230,11 @@ impl JsWallet {
             (None, None) => None,
         };
 
-        let mint_url = UncheckedUrl::from_str(&mint_url).map_err(into_err)?;
-
         let target = split_target_amount
             .map(|a| SplitTarget::Value(*a.deref()))
             .unwrap_or_default();
         self.inner
-            .send(
-                &mint_url,
-                unit.into(),
-                Amount::from(amount),
-                memo,
-                conditions,
-                &target,
-            )
+            .send(Amount::from(amount), memo, conditions, &target)
             .await
             .map_err(into_err)
     }
@@ -330,8 +243,6 @@ impl JsWallet {
     #[wasm_bindgen(js_name = swap)]
     pub async fn swap(
         &mut self,
-        mint_url: String,
-        unit: JsCurrencyUnit,
         amount: u64,
         input_proofs: Vec<JsProof>,
         p2pk_condition: Option<JsP2PKSpendingConditions>,
@@ -349,8 +260,6 @@ impl JsWallet {
             (None, None) => None,
         };
 
-        let mint_url = UncheckedUrl::from_str(&mint_url).map_err(into_err)?;
-
         let proofs: Proofs = input_proofs.iter().map(|p| p.deref()).cloned().collect();
 
         let target = split_target_amount
@@ -358,14 +267,7 @@ impl JsWallet {
             .unwrap_or_default();
         let post_swap_proofs = self
             .inner
-            .swap(
-                &mint_url,
-                &unit.into(),
-                Some(Amount::from(amount)),
-                &target,
-                proofs,
-                conditions,
-            )
+            .swap(Some(Amount::from(amount)), &target, proofs, conditions)
             .await
             .map_err(into_err)?;
 

+ 7 - 3
crates/cdk-cli/Cargo.toml

@@ -13,9 +13,9 @@ license.workspace = true
 [dependencies]
 anyhow = "1.0.75"
 bip39.workspace = true
-cdk = { workspace = true, default-features = false, features = ["wallet", "nostr"] }
-cdk-redb = { workspace = true, default-features = false, features = ["wallet", "nostr"] }
-cdk-sqlite = { workspace = true, default-features = false, features = ["wallet", "nostr"] }
+cdk = { workspace = true, default-features = false, features = ["wallet"] }
+cdk-redb = { workspace = true, default-features = false, features = ["wallet"] }
+cdk-sqlite = { workspace = true, default-features = false, features = ["wallet"] }
 clap = { version = "4.4.8", features = ["derive", "env"] }
 serde = { workspace = true, features = ["derive"] }
 serde_json.workspace = true
@@ -24,3 +24,7 @@ tracing.workspace = true
 tracing-subscriber = "0.3.18"
 rand = "0.8.5"
 home = "0.5.9"
+nostr-sdk = { version = "0.31.0", default-features = false, features = [
+    "nip04",
+    "nip44"
+]}

+ 38 - 12
crates/cdk-cli/src/main.rs

@@ -1,3 +1,4 @@
+use std::collections::HashMap;
 use std::fs;
 use std::path::PathBuf;
 use std::str::FromStr;
@@ -5,9 +6,9 @@ use std::sync::Arc;
 
 use anyhow::{bail, Result};
 use bip39::Mnemonic;
-use cdk::cdk_database;
 use cdk::cdk_database::WalletDatabase;
 use cdk::wallet::Wallet;
+use cdk::{cdk_database, UncheckedUrl};
 use cdk_redb::RedbWalletDatabase;
 use cdk_sqlite::WalletSQLiteDatabase;
 use clap::{Parser, Subcommand};
@@ -117,38 +118,63 @@ async fn main() -> Result<()> {
         }
     };
 
-    let wallet = Wallet::new(localstore, &mnemonic.to_seed_normalized(""), vec![]);
+    let mut wallets: HashMap<UncheckedUrl, Wallet> = HashMap::new();
+
+    let mints = localstore.get_mints().await?;
+
+    for (mint, _) in mints {
+        let wallet = Wallet::new(
+            &mint.to_string(),
+            cdk::nuts::CurrencyUnit::Sat,
+            localstore.clone(),
+            &mnemonic.to_seed_normalized(""),
+        );
+
+        wallets.insert(mint, wallet);
+    }
 
     match &args.command {
         Commands::DecodeToken(sub_command_args) => {
             sub_commands::decode_token::decode_token(sub_command_args)
         }
-        Commands::Balance => sub_commands::balance::balance(wallet).await,
+        Commands::Balance => sub_commands::balance::balance(wallets).await,
         Commands::Melt(sub_command_args) => {
-            sub_commands::melt::melt(wallet, sub_command_args).await
+            sub_commands::melt::melt(wallets, sub_command_args).await
         }
         Commands::Receive(sub_command_args) => {
-            sub_commands::receive::receive(wallet, sub_command_args).await
+            sub_commands::receive::receive(
+                wallets,
+                &mnemonic.to_seed_normalized(""),
+                localstore,
+                sub_command_args,
+            )
+            .await
         }
         Commands::Send(sub_command_args) => {
-            sub_commands::send::send(wallet, sub_command_args).await
+            sub_commands::send::send(wallets, sub_command_args).await
         }
-        Commands::CheckSpendable => sub_commands::check_spent::check_spent(wallet).await,
+        Commands::CheckSpendable => sub_commands::check_spent::check_spent(wallets).await,
         Commands::MintInfo(sub_command_args) => {
             sub_commands::mint_info::mint_info(sub_command_args).await
         }
         Commands::Mint(sub_command_args) => {
-            sub_commands::mint::mint(wallet, sub_command_args).await
+            sub_commands::mint::mint(
+                wallets,
+                &mnemonic.to_seed_normalized(""),
+                localstore,
+                sub_command_args,
+            )
+            .await
         }
-        Commands::PendingMint => sub_commands::pending_mints::pending_mints(wallet).await,
+        Commands::PendingMint => sub_commands::pending_mints::pending_mints(wallets).await,
         Commands::Burn(sub_command_args) => {
-            sub_commands::burn::burn(wallet, sub_command_args).await
+            sub_commands::burn::burn(wallets, sub_command_args).await
         }
         Commands::Restore(sub_command_args) => {
-            sub_commands::restore::restore(wallet, sub_command_args).await
+            sub_commands::restore::restore(wallets, sub_command_args).await
         }
         Commands::UpdateMintUrl(sub_command_args) => {
-            sub_commands::update_mint_url::update_mint_url(wallet, sub_command_args).await
+            sub_commands::update_mint_url::update_mint_url(wallets, sub_command_args).await
         }
     }
 }

+ 11 - 15
crates/cdk-cli/src/sub_commands/balance.rs

@@ -1,29 +1,25 @@
 use std::collections::HashMap;
 
 use anyhow::Result;
-use cdk::nuts::CurrencyUnit;
 use cdk::url::UncheckedUrl;
 use cdk::wallet::Wallet;
 use cdk::Amount;
 
-pub async fn balance(wallet: Wallet) -> Result<()> {
-    let _ = mint_balances(&wallet).await;
+pub async fn balance(wallets: HashMap<UncheckedUrl, Wallet>) -> Result<()> {
+    mint_balances(wallets).await?;
     Ok(())
 }
 
 pub async fn mint_balances(
-    wallet: &Wallet,
-) -> Result<Vec<(UncheckedUrl, HashMap<CurrencyUnit, Amount>)>> {
-    let mints_amounts: Vec<(UncheckedUrl, HashMap<_, _>)> =
-        wallet.mint_balances().await?.into_iter().collect();
+    wallets: HashMap<UncheckedUrl, Wallet>,
+) -> Result<Vec<(Wallet, Amount)>> {
+    let mut wallets_vec: Vec<(Wallet, Amount)> = Vec::with_capacity(wallets.capacity());
 
-    for (i, (mint, balance)) in mints_amounts.iter().enumerate() {
-        println!("{i}: {mint}:");
-        for (unit, amount) in balance {
-            println!("- {amount} {unit}");
-        }
-        println!("---------");
+    for (i, (mint_url, wallet)) in wallets.iter().enumerate() {
+        let mint_url = mint_url.clone();
+        let amount = wallet.total_balance().await?;
+        println!("{i}: {mint_url} {amount}");
+        wallets_vec.push((wallet.clone(), amount));
     }
-
-    Ok(mints_amounts)
+    Ok(wallets_vec)
 }

+ 22 - 6
crates/cdk-cli/src/sub_commands/burn.rs

@@ -1,18 +1,34 @@
+use std::collections::HashMap;
+
 use anyhow::Result;
 use cdk::wallet::Wallet;
+use cdk::{Amount, UncheckedUrl};
 use clap::Args;
 
 #[derive(Args)]
 pub struct BurnSubCommand {
     /// Mint Url
-    mint_url: Option<String>,
+    mint_url: Option<UncheckedUrl>,
 }
 
-pub async fn burn(wallet: Wallet, sub_command_args: &BurnSubCommand) -> Result<()> {
-    let amount_burnt = wallet
-        .check_all_pending_proofs(sub_command_args.mint_url.clone().map(|u| u.into()), None)
-        .await?;
+pub async fn burn(
+    wallets: HashMap<UncheckedUrl, Wallet>,
+    sub_command_args: &BurnSubCommand,
+) -> Result<()> {
+    let mut total_burnt = Amount::ZERO;
+    match &sub_command_args.mint_url {
+        Some(mint_url) => {
+            let wallet = wallets.get(mint_url).unwrap();
+            total_burnt = wallet.check_all_pending_proofs().await?;
+        }
+        None => {
+            for wallet in wallets.values() {
+                let amount_burnt = wallet.check_all_pending_proofs().await?;
+                total_burnt += amount_burnt;
+            }
+        }
+    }
 
-    println!("{amount_burnt} burned");
+    println!("{total_burnt} burned");
     Ok(())
 }

+ 6 - 31
crates/cdk-cli/src/sub_commands/check_spent.rs

@@ -1,40 +1,15 @@
 use std::collections::HashMap;
-use std::io::Write;
-use std::{io, println};
+use std::println;
 
-use anyhow::{bail, Result};
+use anyhow::Result;
 use cdk::url::UncheckedUrl;
 use cdk::wallet::Wallet;
 
-pub async fn check_spent(wallet: Wallet) -> Result<()> {
-    let mints_amounts: Vec<(UncheckedUrl, HashMap<_, _>)> =
-        wallet.mint_balances().await?.into_iter().collect();
+pub async fn check_spent(wallets: HashMap<UncheckedUrl, Wallet>) -> Result<()> {
+    for wallet in wallets.values() {
+        let amount = wallet.check_all_pending_proofs().await?;
 
-    for (i, (mint, amount)) in mints_amounts.iter().enumerate() {
-        println!("{}: {}, {:?} sats", i, mint, amount);
-    }
-
-    println!("Enter mint number to create token");
-
-    let mut user_input = String::new();
-    let stdin = io::stdin();
-    io::stdout().flush().unwrap();
-    stdin.read_line(&mut user_input)?;
-
-    let mint_number: usize = user_input.trim().parse()?;
-
-    if mint_number.gt(&(mints_amounts.len() - 1)) {
-        bail!("Invalid mint number");
-    }
-
-    let mint_url = mints_amounts[mint_number].0.clone();
-
-    let proofs = wallet.get_proofs(mint_url.clone()).await?.unwrap();
-
-    let send_proofs = wallet.check_proofs_spent(mint_url, proofs.to_vec()).await?;
-
-    for proof in send_proofs {
-        println!("{:#?}", proof);
+        println!("Amount marked as spent: {}", amount);
     }
 
     Ok(())

+ 11 - 20
crates/cdk-cli/src/sub_commands/melt.rs

@@ -1,12 +1,12 @@
+use std::collections::HashMap;
 use std::io::Write;
 use std::str::FromStr;
 use std::{io, println};
 
 use anyhow::{bail, Result};
 use cdk::amount::SplitTarget;
-use cdk::nuts::CurrencyUnit;
 use cdk::wallet::Wallet;
-use cdk::Bolt11Invoice;
+use cdk::{Bolt11Invoice, UncheckedUrl};
 use clap::Args;
 
 use crate::sub_commands::balance::mint_balances;
@@ -14,8 +14,11 @@ use crate::sub_commands::balance::mint_balances;
 #[derive(Args)]
 pub struct MeltSubCommand {}
 
-pub async fn melt(wallet: Wallet, _sub_command_args: &MeltSubCommand) -> Result<()> {
-    let mints_amounts = mint_balances(&wallet).await?;
+pub async fn melt(
+    wallets: HashMap<UncheckedUrl, Wallet>,
+    _sub_command_args: &MeltSubCommand,
+) -> Result<()> {
+    let mints_amounts = mint_balances(wallets).await?;
 
     println!("Enter mint number to create token");
 
@@ -30,7 +33,7 @@ pub async fn melt(wallet: Wallet, _sub_command_args: &MeltSubCommand) -> Result<
         bail!("Invalid mint number");
     }
 
-    let mint_url = mints_amounts[mint_number].0.clone();
+    let wallet = mints_amounts[mint_number].0.clone();
 
     println!("Enter bolt11 invoice request");
 
@@ -43,26 +46,14 @@ pub async fn melt(wallet: Wallet, _sub_command_args: &MeltSubCommand) -> Result<
     if bolt11
         .amount_milli_satoshis()
         .unwrap()
-        .gt(&(<cdk::Amount as Into<u64>>::into(
-            *mints_amounts[mint_number]
-                .1
-                .get(&CurrencyUnit::Sat)
-                .unwrap(),
-        ) * 1000_u64))
+        .gt(&(<cdk::Amount as Into<u64>>::into(mints_amounts[mint_number].1) * 1000_u64))
     {
         bail!("Not enough funds");
     }
-    let quote = wallet
-        .melt_quote(
-            mint_url.clone(),
-            cdk::nuts::CurrencyUnit::Sat,
-            bolt11.to_string(),
-            None,
-        )
-        .await?;
+    let quote = wallet.melt_quote(bolt11.to_string(), None).await?;
 
     let melt = wallet
-        .melt(&mint_url, &quote.id, SplitTarget::default())
+        .melt(&quote.id, SplitTarget::default())
         .await
         .unwrap();
 

+ 16 - 12
crates/cdk-cli/src/sub_commands/mint.rs

@@ -1,7 +1,10 @@
+use std::collections::HashMap;
+use std::sync::Arc;
 use std::time::Duration;
 
 use anyhow::Result;
 use cdk::amount::SplitTarget;
+use cdk::cdk_database::{Error, WalletDatabase};
 use cdk::nuts::CurrencyUnit;
 use cdk::url::UncheckedUrl;
 use cdk::wallet::Wallet;
@@ -19,15 +22,20 @@ pub struct MintSubCommand {
     unit: String,
 }
 
-pub async fn mint(wallet: Wallet, sub_command_args: &MintSubCommand) -> Result<()> {
+pub async fn mint(
+    wallets: HashMap<UncheckedUrl, Wallet>,
+    seed: &[u8],
+    localstore: Arc<dyn WalletDatabase<Err = Error> + Sync + Send>,
+    sub_command_args: &MintSubCommand,
+) -> Result<()> {
     let mint_url = sub_command_args.mint_url.clone();
+    let wallet = match wallets.get(&mint_url) {
+        Some(wallet) => wallet.clone(),
+        None => Wallet::new(&mint_url.to_string(), CurrencyUnit::Sat, localstore, seed),
+    };
 
     let quote = wallet
-        .mint_quote(
-            mint_url.clone(),
-            CurrencyUnit::from(&sub_command_args.unit),
-            Amount::from(sub_command_args.amount),
-        )
+        .mint_quote(Amount::from(sub_command_args.amount))
         .await?;
 
     println!("Quote: {:#?}", quote);
@@ -35,9 +43,7 @@ pub async fn mint(wallet: Wallet, sub_command_args: &MintSubCommand) -> Result<(
     println!("Please pay: {}", quote.request);
 
     loop {
-        let status = wallet
-            .mint_quote_status(mint_url.clone(), &quote.id)
-            .await?;
+        let status = wallet.mint_quote_status(&quote.id).await?;
 
         if status.paid {
             break;
@@ -46,9 +52,7 @@ pub async fn mint(wallet: Wallet, sub_command_args: &MintSubCommand) -> Result<(
         sleep(Duration::from_secs(2)).await;
     }
 
-    let receive_amount = wallet
-        .mint(mint_url.clone(), &quote.id, SplitTarget::default(), None)
-        .await?;
+    let receive_amount = wallet.mint(&quote.id, SplitTarget::default(), None).await?;
 
     println!("Received {receive_amount} from mint {mint_url}");
 

+ 9 - 2
crates/cdk-cli/src/sub_commands/pending_mints.rs

@@ -1,8 +1,15 @@
+use std::collections::HashMap;
+
 use anyhow::Result;
 use cdk::wallet::Wallet;
+use cdk::{Amount, UncheckedUrl};
 
-pub async fn pending_mints(wallet: Wallet) -> Result<()> {
-    let amount_claimed = wallet.check_all_mint_quotes().await?;
+pub async fn pending_mints(wallets: HashMap<UncheckedUrl, Wallet>) -> Result<()> {
+    let mut amount_claimed = Amount::ZERO;
+    for wallet in wallets.values() {
+        let claimed = wallet.check_all_mint_quotes().await?;
+        amount_claimed += claimed;
+    }
 
     println!("Amount minted: {amount_claimed}");
     Ok(())

+ 151 - 37
crates/cdk-cli/src/sub_commands/receive.rs

@@ -1,10 +1,17 @@
+use std::collections::{HashMap, HashSet};
 use std::str::FromStr;
+use std::sync::Arc;
 
 use anyhow::{anyhow, Result};
 use cdk::amount::SplitTarget;
-use cdk::nuts::SecretKey;
+use cdk::cdk_database::{Error, WalletDatabase};
+use cdk::nuts::{CurrencyUnit, SecretKey, Token};
+use cdk::util::unix_time;
 use cdk::wallet::Wallet;
+use cdk::{Amount, UncheckedUrl};
 use clap::Args;
+use nostr_sdk::nips::nip04;
+use nostr_sdk::{Filter, Keys, Kind, Timestamp};
 
 #[derive(Args)]
 pub struct ReceiveSubCommand {
@@ -27,54 +34,82 @@ pub struct ReceiveSubCommand {
     preimage: Vec<String>,
 }
 
-pub async fn receive(wallet: Wallet, sub_command_args: &ReceiveSubCommand) -> Result<()> {
-    let nostr_key = match sub_command_args.nostr_key.as_ref() {
-        Some(nostr_key) => {
-            let secret_key = SecretKey::from_str(nostr_key)?;
-            wallet.add_p2pk_signing_key(secret_key.clone()).await;
-            Some(secret_key)
-        }
-        None => None,
-    };
+pub async fn receive(
+    wallets: HashMap<UncheckedUrl, Wallet>,
+    seed: &[u8],
+    localstore: Arc<dyn WalletDatabase<Err = Error> + Sync + Send>,
+    sub_command_args: &ReceiveSubCommand,
+) -> Result<()> {
+    let mut signing_keys = Vec::new();
 
     if !sub_command_args.signing_key.is_empty() {
-        let signing_keys: Vec<SecretKey> = sub_command_args
+        let mut s_keys: Vec<SecretKey> = sub_command_args
             .signing_key
             .iter()
             .map(|s| SecretKey::from_str(s).unwrap())
             .collect();
+        signing_keys.append(&mut s_keys);
+    }
 
-        for signing_key in signing_keys {
-            wallet.add_p2pk_signing_key(signing_key).await;
+    let amount = match &sub_command_args.token {
+        Some(token_str) => {
+            receive_token(
+                token_str,
+                wallets,
+                seed,
+                &localstore,
+                &signing_keys,
+                &sub_command_args.preimage,
+            )
+            .await?
         }
-    }
+        None => {
+            //wallet.add_p2pk_signing_key(nostr_signing_key).await;
+            let nostr_key = match sub_command_args.nostr_key.as_ref() {
+                Some(nostr_key) => {
+                    let secret_key = SecretKey::from_str(nostr_key)?;
+                    Some(secret_key)
+                }
+                None => None,
+            };
 
-    let preimage = match sub_command_args.preimage.is_empty() {
-        true => None,
-        false => Some(sub_command_args.preimage.clone()),
-    };
+            let nostr_key =
+                nostr_key.ok_or(anyhow!("Nostr key required if token is not provided"))?;
+
+            signing_keys.push(nostr_key.clone());
 
-    let amount = match nostr_key {
-        Some(nostr_key) => {
-            assert!(!sub_command_args.relay.is_empty());
-            wallet
-                .add_nostr_relays(sub_command_args.relay.clone())
+            let relays = sub_command_args.relay.clone();
+            let since = localstore
+                .get_nostr_last_checked(&nostr_key.public_key())
                 .await?;
-            wallet
-                .nostr_receive(nostr_key, sub_command_args.since, SplitTarget::default())
-                .await?
-        }
-        None => {
-            wallet
-                .receive(
-                    sub_command_args
-                        .token
-                        .as_ref()
-                        .ok_or(anyhow!("Token Required"))?,
-                    &SplitTarget::default(),
-                    preimage,
+
+            let tokens = nostr_receive(relays, nostr_key.clone(), since).await?;
+
+            let mut total_amount = Amount::ZERO;
+            for token_str in &tokens {
+                match receive_token(
+                    token_str,
+                    wallets.clone(),
+                    seed,
+                    &localstore,
+                    &signing_keys,
+                    &sub_command_args.preimage,
                 )
-                .await?
+                .await
+                {
+                    Ok(amount) => {
+                        total_amount += amount;
+                    }
+                    Err(err) => {
+                        println!("{}", err);
+                    }
+                }
+            }
+
+            localstore
+                .add_nostr_last_checked(nostr_key.public_key(), unix_time() as u32)
+                .await?;
+            total_amount
         }
     };
 
@@ -82,3 +117,82 @@ pub async fn receive(wallet: Wallet, sub_command_args: &ReceiveSubCommand) -> Re
 
     Ok(())
 }
+
+async fn receive_token(
+    token_str: &str,
+    wallets: HashMap<UncheckedUrl, Wallet>,
+    seed: &[u8],
+    localstore: &Arc<dyn WalletDatabase<Err = Error> + Sync + Send>,
+    signing_keys: &[SecretKey],
+    preimage: &[String],
+) -> Result<Amount> {
+    let token = Token::from_str(token_str)?;
+    let mint_url = token.token.first().unwrap().mint.clone();
+
+    let wallet = match wallets.get(&mint_url) {
+        Some(wallet) => wallet.clone(),
+        None => Wallet::new(
+            &mint_url.to_string(),
+            CurrencyUnit::Sat,
+            Arc::clone(localstore),
+            seed,
+        ),
+    };
+
+    let amount = wallet
+        .receive(token_str, &SplitTarget::default(), signing_keys, preimage)
+        .await?;
+    Ok(amount)
+}
+
+/// Receive tokens sent to nostr pubkey via dm
+async fn nostr_receive(
+    relays: Vec<String>,
+    nostr_signing_key: SecretKey,
+    since: Option<u32>,
+) -> Result<HashSet<String>> {
+    let verifying_key = nostr_signing_key.public_key();
+
+    let x_only_pubkey = verifying_key.x_only_public_key();
+
+    let nostr_pubkey = nostr_sdk::PublicKey::from_hex(x_only_pubkey.to_string())?;
+
+    let since = since.map(|s| Timestamp::from(s as u64));
+
+    let filter = match since {
+        Some(since) => Filter::new()
+            .pubkey(nostr_pubkey)
+            .kind(Kind::EncryptedDirectMessage)
+            .since(since),
+        None => Filter::new()
+            .pubkey(nostr_pubkey)
+            .kind(Kind::EncryptedDirectMessage),
+    };
+
+    let client = nostr_sdk::Client::default();
+
+    client.add_relays(relays).await?;
+
+    client.connect().await;
+
+    let events = client.get_events_of(vec![filter], None).await?;
+
+    let mut tokens: HashSet<String> = HashSet::new();
+
+    let keys = Keys::from_str(&(nostr_signing_key).to_secret_hex())?;
+
+    for event in events {
+        if event.kind() == Kind::EncryptedDirectMessage {
+            if let Ok(msg) = nip04::decrypt(keys.secret_key()?, event.author_ref(), event.content())
+            {
+                if let Some(token) = cdk::wallet::util::token_from_text(&msg) {
+                    tokens.insert(token.to_string());
+                }
+            } else {
+                tracing::error!("Impossible to decrypt direct message");
+            }
+        }
+    }
+
+    Ok(tokens)
+}

+ 11 - 4
crates/cdk-cli/src/sub_commands/restore.rs

@@ -1,4 +1,6 @@
-use anyhow::Result;
+use std::collections::HashMap;
+
+use anyhow::{anyhow, Result};
 use cdk::url::UncheckedUrl;
 use cdk::wallet::Wallet;
 use clap::Args;
@@ -9,10 +11,15 @@ pub struct RestoreSubCommand {
     mint_url: UncheckedUrl,
 }
 
-pub async fn restore(wallet: Wallet, sub_command_args: &RestoreSubCommand) -> Result<()> {
-    let mint_url = sub_command_args.mint_url.clone();
+pub async fn restore(
+    wallets: HashMap<UncheckedUrl, Wallet>,
+    sub_command_args: &RestoreSubCommand,
+) -> Result<()> {
+    let wallet = wallets
+        .get(&sub_command_args.mint_url)
+        .ok_or(anyhow!("Unknown mint url"))?;
 
-    let amount = wallet.restore(mint_url).await?;
+    let amount = wallet.restore().await?;
 
     println!("Restored {}", amount);
 

+ 11 - 13
crates/cdk-cli/src/sub_commands/send.rs

@@ -1,12 +1,13 @@
+use std::collections::HashMap;
 use std::io::Write;
 use std::str::FromStr;
 use std::{io, println};
 
 use anyhow::{bail, Result};
 use cdk::amount::SplitTarget;
-use cdk::nuts::{Conditions, CurrencyUnit, PublicKey, SpendingConditions};
+use cdk::nuts::{Conditions, PublicKey, SpendingConditions};
 use cdk::wallet::Wallet;
-use cdk::Amount;
+use cdk::{Amount, UncheckedUrl};
 use clap::Args;
 
 use crate::sub_commands::balance::mint_balances;
@@ -33,8 +34,11 @@ pub struct SendSubCommand {
     refund_keys: Vec<String>,
 }
 
-pub async fn send(wallet: Wallet, sub_command_args: &SendSubCommand) -> Result<()> {
-    let mints_amounts = mint_balances(&wallet).await?;
+pub async fn send(
+    wallets: HashMap<UncheckedUrl, Wallet>,
+    sub_command_args: &SendSubCommand,
+) -> Result<()> {
+    let mints_amounts = mint_balances(wallets).await?;
 
     println!("Enter mint number to create token");
 
@@ -49,8 +53,6 @@ pub async fn send(wallet: Wallet, sub_command_args: &SendSubCommand) -> Result<(
         bail!("Invalid mint number");
     }
 
-    let mint_url = mints_amounts[mint_number].0.clone();
-
     println!("Enter value of token in sats");
 
     let mut user_input = String::new();
@@ -59,11 +61,7 @@ pub async fn send(wallet: Wallet, sub_command_args: &SendSubCommand) -> Result<(
     stdin.read_line(&mut user_input)?;
     let token_amount = Amount::from(user_input.trim().parse::<u64>()?);
 
-    if token_amount.gt(mints_amounts[mint_number]
-        .1
-        .get(&CurrencyUnit::Sat)
-        .unwrap())
-    {
+    if token_amount.gt(&mints_amounts[mint_number].1) {
         bail!("Not enough funds");
     }
 
@@ -145,10 +143,10 @@ pub async fn send(wallet: Wallet, sub_command_args: &SendSubCommand) -> Result<(
         },
     };
 
+    let wallet = mints_amounts[mint_number].0.clone();
+
     let token = wallet
         .send(
-            &mint_url,
-            CurrencyUnit::Sat,
             token_amount,
             sub_command_args.memo.clone(),
             conditions,

+ 10 - 5
crates/cdk-cli/src/sub_commands/update_mint_url.rs

@@ -1,4 +1,6 @@
-use anyhow::Result;
+use std::collections::HashMap;
+
+use anyhow::{anyhow, Result};
 use cdk::url::UncheckedUrl;
 use cdk::wallet::Wallet;
 use clap::Args;
@@ -12,7 +14,7 @@ pub struct UpdateMintUrlSubCommand {
 }
 
 pub async fn update_mint_url(
-    wallet: Wallet,
+    wallets: HashMap<UncheckedUrl, Wallet>,
     sub_command_args: &UpdateMintUrlSubCommand,
 ) -> Result<()> {
     let UpdateMintUrlSubCommand {
@@ -20,9 +22,12 @@ pub async fn update_mint_url(
         new_mint_url,
     } = sub_command_args;
 
-    wallet
-        .update_mint_url(old_mint_url.clone(), new_mint_url.clone())
-        .await?;
+    let mut wallet = wallets
+        .get(old_mint_url)
+        .ok_or(anyhow!("Unknown mint url"))?
+        .clone();
+
+    wallet.update_mint_url(new_mint_url.clone()).await?;
 
     println!("Mint Url changed from {} to {}", old_mint_url, new_mint_url);
 

+ 0 - 1
crates/cdk-redb/Cargo.toml

@@ -12,7 +12,6 @@ rust-version.workspace = true
 default = ["mint", "wallet"]
 mint = ["cdk/mint"]
 wallet = ["cdk/wallet"]
-nostr = ["cdk/nostr"]
 
 [dependencies]
 async-trait.workspace = true

+ 0 - 4
crates/cdk-redb/src/wallet.rs

@@ -31,7 +31,6 @@ const MINT_KEYS_TABLE: TableDefinition<&str, &str> = TableDefinition::new("mint_
 const PROOFS_TABLE: TableDefinition<&[u8], &str> = TableDefinition::new("proofs");
 const CONFIG_TABLE: TableDefinition<&str, &str> = TableDefinition::new("config");
 const KEYSET_COUNTER: TableDefinition<&str, u32> = TableDefinition::new("keyset_counter");
-#[cfg(feature = "nostr")]
 const NOSTR_LAST_CHECKED: TableDefinition<&str, u32> = TableDefinition::new("keyset_counter");
 
 const DATABASE_VERSION: u32 = 0;
@@ -73,7 +72,6 @@ impl RedbWalletDatabase {
                     let _ = write_txn.open_table(MINT_KEYS_TABLE)?;
                     let _ = write_txn.open_table(PROOFS_TABLE)?;
                     let _ = write_txn.open_table(KEYSET_COUNTER)?;
-                    #[cfg(feature = "nostr")]
                     let _ = write_txn.open_table(NOSTR_LAST_CHECKED)?;
                     table.insert("db_version", "0")?;
                 }
@@ -655,7 +653,6 @@ impl WalletDatabase for RedbWalletDatabase {
         Ok(counter.map(|c| c.value()))
     }
 
-    #[cfg(feature = "nostr")]
     #[instrument(skip(self))]
     async fn get_nostr_last_checked(
         &self,
@@ -673,7 +670,6 @@ impl WalletDatabase for RedbWalletDatabase {
 
         Ok(last_checked.map(|c| c.value()))
     }
-    #[cfg(feature = "nostr")]
     #[instrument(skip(self))]
     async fn add_nostr_last_checked(
         &self,

+ 54 - 1
crates/cdk-rexie/src/wallet.rs

@@ -24,8 +24,9 @@ const MELT_QUOTES: &str = "melt_quotes";
 const PROOFS: &str = "proofs";
 const CONFIG: &str = "config";
 const KEYSET_COUNTER: &str = "keyset_counter";
+const NOSTR_LAST_CHECKED: &str = "nostr_last_check";
 
-const DATABASE_VERSION: u32 = 2;
+const DATABASE_VERSION: u32 = 3;
 
 #[derive(Debug, Error)]
 pub enum Error {
@@ -87,6 +88,7 @@ impl RexieWalletDatabase {
             .add_object_store(ObjectStore::new(MELT_QUOTES))
             .add_object_store(ObjectStore::new(CONFIG))
             .add_object_store(ObjectStore::new(KEYSET_COUNTER))
+            .add_object_store(ObjectStore::new(NOSTR_LAST_CHECKED))
             // Build the database
             .build()
             .await
@@ -712,4 +714,55 @@ impl WalletDatabase for RexieWalletDatabase {
 
         Ok(current_count)
     }
+
+    async fn add_nostr_last_checked(
+        &self,
+        verifying_key: PublicKey,
+        last_checked: u32,
+    ) -> Result<(), Self::Err> {
+        let rexie = self.db.lock().await;
+
+        let transaction = rexie
+            .transaction(&[NOSTR_LAST_CHECKED], TransactionMode::ReadWrite)
+            .map_err(Error::from)?;
+
+        let counter_store = transaction.store(NOSTR_LAST_CHECKED).map_err(Error::from)?;
+
+        let verifying_key = serde_wasm_bindgen::to_value(&verifying_key).map_err(Error::from)?;
+
+        let last_checked = serde_wasm_bindgen::to_value(&last_checked).map_err(Error::from)?;
+
+        counter_store
+            .put(&last_checked, Some(&verifying_key))
+            .await
+            .map_err(Error::from)?;
+
+        transaction.done().await.map_err(Error::from)?;
+
+        Ok(())
+    }
+
+    async fn get_nostr_last_checked(
+        &self,
+        verifying_key: &PublicKey,
+    ) -> Result<Option<u32>, Self::Err> {
+        let rexie = self.db.lock().await;
+
+        let transaction = rexie
+            .transaction(&[NOSTR_LAST_CHECKED], TransactionMode::ReadOnly)
+            .map_err(Error::from)?;
+
+        let nostr_last_check_store = transaction.store(NOSTR_LAST_CHECKED).map_err(Error::from)?;
+
+        let verifying_key = serde_wasm_bindgen::to_value(verifying_key).map_err(Error::from)?;
+
+        let last_checked = nostr_last_check_store
+            .get(&verifying_key)
+            .await
+            .map_err(Error::from)?;
+        let last_checked: Option<u32> =
+            serde_wasm_bindgen::from_value(last_checked).map_err(Error::from)?;
+
+        Ok(last_checked)
+    }
 }

+ 0 - 1
crates/cdk-sqlite/Cargo.toml

@@ -12,7 +12,6 @@ rust-version.workspace = true
 default = ["mint", "wallet"]
 mint = ["cdk/mint"]
 wallet = ["cdk/wallet"]
-nostr = ["cdk/nostr"]
 
 [dependencies]
 bitcoin.workspace = true

+ 0 - 2
crates/cdk-sqlite/src/wallet/mod.rs

@@ -627,7 +627,6 @@ WHERE id=?;
         Ok(count)
     }
 
-    #[cfg(feature = "nostr")]
     async fn get_nostr_last_checked(
         &self,
         verifying_key: &PublicKey,
@@ -656,7 +655,6 @@ WHERE key=?;
 
         Ok(count)
     }
-    #[cfg(feature = "nostr")]
     async fn add_nostr_last_checked(
         &self,
         verifying_key: PublicKey,

+ 0 - 5
crates/cdk/Cargo.toml

@@ -13,7 +13,6 @@ license.workspace = true
 default = ["mint", "wallet"]
 mint = []
 wallet = ["dep:reqwest"]
-nostr = ["dep:nostr-sdk"]
 
 
 [dependencies]
@@ -42,10 +41,6 @@ tracing = { version = "0.1", default-features = false, features = [
 thiserror = "1"
 url = "2.3"
 uuid = { version = "1", features = ["v4"] }
-nostr-sdk = { version = "0.31.0", default-features = false, features = [
-    "nip04",
-    "nip44"
-], optional = true }
 
 [target.'cfg(not(target_arch = "wasm32"))'.dependencies]
 tokio = { workspace = true, features = [

+ 7 - 14
crates/cdk/examples/mint-token.rs

@@ -1,4 +1,3 @@
-use std::str::FromStr;
 use std::sync::Arc;
 use std::time::Duration;
 
@@ -7,7 +6,7 @@ use cdk::cdk_database::WalletMemoryDatabase;
 use cdk::error::Error;
 use cdk::nuts::CurrencyUnit;
 use cdk::wallet::Wallet;
-use cdk::{Amount, UncheckedUrl};
+use cdk::Amount;
 use rand::Rng;
 use tokio::time::sleep;
 
@@ -16,24 +15,18 @@ async fn main() -> Result<(), Error> {
     let localstore = WalletMemoryDatabase::default();
     let seed = rand::thread_rng().gen::<[u8; 32]>();
 
-    let mint_url = UncheckedUrl::from_str("https://testnut.cashu.space").unwrap();
+    let mint_url = "https://testnut.cashu.space";
     let unit = CurrencyUnit::Sat;
     let amount = Amount::from(10);
 
-    let wallet = Wallet::new(Arc::new(localstore), &seed, vec![]);
+    let wallet = Wallet::new(mint_url, unit, Arc::new(localstore), &seed);
 
-    let quote = wallet
-        .mint_quote(mint_url.clone(), unit.clone(), amount)
-        .await
-        .unwrap();
+    let quote = wallet.mint_quote(amount).await.unwrap();
 
     println!("Quote: {:#?}", quote);
 
     loop {
-        let status = wallet
-            .mint_quote_status(mint_url.clone(), &quote.id)
-            .await
-            .unwrap();
+        let status = wallet.mint_quote_status(&quote.id).await.unwrap();
 
         println!("Quote status: {}", status.paid);
 
@@ -45,14 +38,14 @@ async fn main() -> Result<(), Error> {
     }
 
     let receive_amount = wallet
-        .mint(mint_url.clone(), &quote.id, SplitTarget::default(), None)
+        .mint(&quote.id, SplitTarget::default(), None)
         .await
         .unwrap();
 
     println!("Received {receive_amount} from mint {mint_url}");
 
     let token = wallet
-        .send(&mint_url, unit, amount, None, None, &SplitTarget::default())
+        .send(amount, None, None, &SplitTarget::default())
         .await
         .unwrap();
 

+ 8 - 24
crates/cdk/examples/p2pk.rs

@@ -1,4 +1,3 @@
-use std::str::FromStr;
 use std::sync::Arc;
 use std::time::Duration;
 
@@ -7,7 +6,7 @@ use cdk::cdk_database::WalletMemoryDatabase;
 use cdk::error::Error;
 use cdk::nuts::{CurrencyUnit, SecretKey, SpendingConditions};
 use cdk::wallet::Wallet;
-use cdk::{Amount, UncheckedUrl};
+use cdk::Amount;
 use rand::Rng;
 use tokio::time::sleep;
 
@@ -16,24 +15,18 @@ async fn main() -> Result<(), Error> {
     let localstore = WalletMemoryDatabase::default();
     let seed = rand::thread_rng().gen::<[u8; 32]>();
 
-    let mint_url = UncheckedUrl::from_str("https://testnut.cashu.space").unwrap();
+    let mint_url = "https://testnut.cashu.space";
     let unit = CurrencyUnit::Sat;
     let amount = Amount::from(10);
 
-    let wallet = Wallet::new(Arc::new(localstore), &seed, vec![]);
+    let wallet = Wallet::new(mint_url, unit.clone(), Arc::new(localstore), &seed);
 
-    let quote = wallet
-        .mint_quote(mint_url.clone(), unit.clone(), amount)
-        .await
-        .unwrap();
+    let quote = wallet.mint_quote(amount).await.unwrap();
 
     println!("Minting nuts ...");
 
     loop {
-        let status = wallet
-            .mint_quote_status(mint_url.clone(), &quote.id)
-            .await
-            .unwrap();
+        let status = wallet.mint_quote_status(&quote.id).await.unwrap();
 
         println!("Quote status: {}", status.paid);
 
@@ -45,7 +38,7 @@ async fn main() -> Result<(), Error> {
     }
 
     let _receive_amount = wallet
-        .mint(mint_url.clone(), &quote.id, SplitTarget::default(), None)
+        .mint(&quote.id, SplitTarget::default(), None)
         .await
         .unwrap();
 
@@ -54,24 +47,15 @@ async fn main() -> Result<(), Error> {
     let spending_conditions = SpendingConditions::new_p2pk(secret.public_key(), None);
 
     let token = wallet
-        .send(
-            &mint_url,
-            unit,
-            amount,
-            None,
-            Some(spending_conditions),
-            &SplitTarget::None,
-        )
+        .send(amount, None, Some(spending_conditions), &SplitTarget::None)
         .await
         .unwrap();
 
     println!("Created token locked to pubkey: {}", secret.public_key());
     println!("{}", token);
 
-    wallet.add_p2pk_signing_key(secret).await;
-
     let amount = wallet
-        .receive(&token, &SplitTarget::default(), None)
+        .receive(&token, &SplitTarget::default(), &[secret], &[])
         .await
         .unwrap();
 

+ 0 - 2
crates/cdk/src/cdk_database/mod.rs

@@ -104,12 +104,10 @@ pub trait WalletDatabase: Debug {
     async fn increment_keyset_counter(&self, keyset_id: &Id, count: u32) -> Result<(), Self::Err>;
     async fn get_keyset_counter(&self, keyset_id: &Id) -> Result<Option<u32>, Self::Err>;
 
-    #[cfg(feature = "nostr")]
     async fn get_nostr_last_checked(
         &self,
         verifying_key: &PublicKey,
     ) -> Result<Option<u32>, Self::Err>;
-    #[cfg(feature = "nostr")]
     async fn add_nostr_last_checked(
         &self,
         verifying_key: PublicKey,

+ 1 - 5
crates/cdk/src/cdk_database/wallet_memory.rs

@@ -25,7 +25,6 @@ pub struct WalletMemoryDatabase {
     mint_keys: Arc<RwLock<HashMap<Id, Keys>>>,
     proofs: Arc<RwLock<HashMap<PublicKey, ProofInfo>>>,
     keyset_counter: Arc<RwLock<HashMap<Id, u32>>>,
-    #[cfg(feature = "nostr")]
     nostr_last_checked: Arc<RwLock<HashMap<PublicKey, u32>>>,
 }
 
@@ -35,7 +34,7 @@ impl WalletMemoryDatabase {
         melt_quotes: Vec<MeltQuote>,
         mint_keys: Vec<Keys>,
         keyset_counter: HashMap<Id, u32>,
-        #[cfg(feature = "nostr")] nostr_last_checked: HashMap<PublicKey, u32>,
+        nostr_last_checked: HashMap<PublicKey, u32>,
     ) -> Self {
         Self {
             mints: Arc::new(RwLock::new(HashMap::new())),
@@ -52,7 +51,6 @@ impl WalletMemoryDatabase {
             )),
             proofs: Arc::new(RwLock::new(HashMap::new())),
             keyset_counter: Arc::new(RwLock::new(keyset_counter)),
-            #[cfg(feature = "nostr")]
             nostr_last_checked: Arc::new(RwLock::new(nostr_last_checked)),
         }
     }
@@ -321,7 +319,6 @@ impl WalletDatabase for WalletMemoryDatabase {
         Ok(self.keyset_counter.read().await.get(id).cloned())
     }
 
-    #[cfg(feature = "nostr")]
     async fn get_nostr_last_checked(
         &self,
         verifying_key: &PublicKey,
@@ -333,7 +330,6 @@ impl WalletDatabase for WalletMemoryDatabase {
             .get(verifying_key)
             .cloned())
     }
-    #[cfg(feature = "nostr")]
     async fn add_nostr_last_checked(
         &self,
         verifying_key: PublicKey,

+ 9 - 0
crates/cdk/src/wallet/error.rs

@@ -63,6 +63,15 @@ pub enum Error {
     ///  Unknown error response
     #[error("Unknown Error response: `{0}`")]
     UnknownErrorResponse(String),
+    /// Unknown Wallet
+    #[error("Unknown Wallet: `{0}`")]
+    UnknownWallet(String),
+    /// Unknown Wallet
+    #[error("Unknown Wallet: `{0}`")]
+    IncorrectWallet(String),
+    /// Max Fee Ecxeded
+    #[error("Max fee exceeded")]
+    MaxFeeExceeded,
     /// CDK Error
     #[error(transparent)]
     Cashu(#[from] crate::error::Error),

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 177 - 336
crates/cdk/src/wallet/mod.rs


+ 299 - 0
crates/cdk/src/wallet/multi_mint_wallet.rs

@@ -0,0 +1,299 @@
+//! MultiMint Wallet
+//!
+//! Wrapper around core [`Wallet`] that enables the use of multiple mint unit pairs
+
+use std::collections::{HashMap, HashSet};
+use std::fmt;
+use std::str::FromStr;
+use std::sync::Arc;
+
+use serde::{Deserialize, Serialize};
+use tokio::sync::Mutex;
+use tracing::instrument;
+
+use super::Error;
+use crate::amount::SplitTarget;
+use crate::nuts::{CurrencyUnit, SecretKey, SpendingConditions, Token};
+use crate::types::{Melted, MintQuote};
+use crate::{Amount, UncheckedUrl, Wallet};
+
+#[derive(Debug, Clone)]
+pub struct MultiMintWallet {
+    pub wallets: Arc<Mutex<HashMap<WalletKey, Wallet>>>,
+}
+
+#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
+pub struct WalletKey {
+    mint_url: UncheckedUrl,
+    unit: CurrencyUnit,
+}
+
+impl fmt::Display for WalletKey {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "mint_url: {}, unit: {}", self.mint_url, self.unit,)
+    }
+}
+
+impl WalletKey {
+    pub fn new(mint_url: UncheckedUrl, unit: CurrencyUnit) -> Self {
+        Self { mint_url, unit }
+    }
+}
+
+impl MultiMintWallet {
+    /// New Multimint wallet
+    pub fn new(wallets: Vec<Wallet>) -> Self {
+        Self {
+            wallets: Arc::new(Mutex::new(
+                wallets
+                    .into_iter()
+                    .map(|w| (WalletKey::new(w.mint_url.clone(), w.unit.clone()), w))
+                    .collect(),
+            )),
+        }
+    }
+
+    /// Add wallet to MultiMintWallet
+    #[instrument(skip(self, wallet))]
+    pub async fn add_wallet(&self, wallet: Wallet) {
+        let wallet_key = WalletKey::new(wallet.mint_url.clone(), wallet.unit.clone());
+
+        let mut wallets = self.wallets.lock().await;
+
+        wallets.insert(wallet_key, wallet);
+    }
+
+    /// Remove Wallet from MultiMintWallet
+    #[instrument(skip(self))]
+    pub async fn remove_wallet(&self, wallet_key: &WalletKey) {
+        let mut wallets = self.wallets.lock().await;
+
+        wallets.remove(wallet_key);
+    }
+
+    /// Get Wallets from MultiMintWallet
+    #[instrument(skip(self))]
+    pub async fn get_wallets(&self) -> Vec<Wallet> {
+        self.wallets.lock().await.values().cloned().collect()
+    }
+
+    /// Get Wallet from MultiMintWallet
+    #[instrument(skip(self))]
+    pub async fn get_wallet(&self, wallet_key: &WalletKey) -> Option<Wallet> {
+        let wallets = self.wallets.lock().await;
+
+        wallets.get(wallet_key).cloned()
+    }
+
+    /// Check if mint unit pair is in wallet
+    #[instrument(skip(self))]
+    pub async fn has(&self, wallet_key: &WalletKey) -> bool {
+        self.wallets.lock().await.contains_key(wallet_key)
+    }
+
+    /// Get wallet balances
+    #[instrument(skip(self))]
+    pub async fn get_balances(
+        &self,
+        unit: &CurrencyUnit,
+    ) -> Result<HashMap<UncheckedUrl, Amount>, Error> {
+        let mut balances = HashMap::new();
+
+        for (WalletKey { mint_url, unit: u }, wallet) in self.wallets.lock().await.iter() {
+            if unit == u {
+                let wallet_balance = wallet.total_balance().await?;
+                balances.insert(mint_url.clone(), wallet_balance);
+            }
+        }
+
+        Ok(balances)
+    }
+
+    /// Create cashu token
+    #[instrument(skip(self))]
+    pub async fn send(
+        &self,
+        wallet_key: &WalletKey,
+        amount: Amount,
+        memo: Option<String>,
+        conditions: Option<SpendingConditions>,
+    ) -> Result<String, Error> {
+        let wallet = self
+            .get_wallet(wallet_key)
+            .await
+            .ok_or(Error::UnknownWallet(wallet_key.to_string()))?;
+
+        wallet
+            .send(amount, memo, conditions, &SplitTarget::default())
+            .await
+    }
+
+    /// Mint quote for wallet
+    #[instrument(skip(self))]
+    pub async fn mint_quote(
+        &self,
+        wallet_key: &WalletKey,
+        amount: Amount,
+    ) -> Result<MintQuote, Error> {
+        let wallet = self
+            .get_wallet(wallet_key)
+            .await
+            .ok_or(Error::UnknownWallet(wallet_key.to_string()))?;
+
+        wallet.mint_quote(amount).await
+    }
+
+    /// Check all mint quotes
+    /// If quote is paid, wallet will mint
+    #[instrument(skip(self))]
+    pub async fn check_all_mint_quotes(
+        &self,
+        wallet_key: Option<WalletKey>,
+    ) -> Result<HashMap<CurrencyUnit, Amount>, Error> {
+        let mut amount_minted = HashMap::new();
+        match wallet_key {
+            Some(wallet_key) => {
+                let wallet = self
+                    .get_wallet(&wallet_key)
+                    .await
+                    .ok_or(Error::UnknownWallet(wallet_key.to_string()))?;
+
+                let amount = wallet.check_all_mint_quotes().await?;
+                amount_minted.insert(wallet.unit.clone(), amount);
+            }
+            None => {
+                for (_, wallet) in self.wallets.lock().await.iter() {
+                    let amount = wallet.check_all_mint_quotes().await?;
+
+                    amount_minted
+                        .entry(wallet.unit.clone())
+                        .and_modify(|b| *b += amount)
+                        .or_insert(amount);
+                }
+            }
+        }
+
+        Ok(amount_minted)
+    }
+
+    /// Mint a specific quote
+    #[instrument(skip(self))]
+    pub async fn mint(
+        &self,
+        wallet_key: &WalletKey,
+        quote_id: &str,
+        conditions: Option<SpendingConditions>,
+    ) -> Result<Amount, Error> {
+        let wallet = self
+            .get_wallet(wallet_key)
+            .await
+            .ok_or(Error::UnknownWallet(wallet_key.to_string()))?;
+        wallet
+            .mint(quote_id, SplitTarget::default(), conditions)
+            .await
+    }
+
+    /// Receive token
+    /// Wallet must be already added to multimintwallet
+    #[instrument(skip_all)]
+    pub async fn receive(
+        &self,
+        encoded_token: &str,
+        p2pk_signing_keys: &[SecretKey],
+        preimages: &[String],
+    ) -> Result<Amount, Error> {
+        let token_data = Token::from_str(encoded_token)?;
+        let unit = token_data.unit.unwrap_or_default();
+        let mint_url = token_data.token.first().unwrap().mint.clone();
+
+        let mints: HashSet<&UncheckedUrl> = token_data.token.iter().map(|d| &d.mint).collect();
+
+        // Check that all mints in tokes have wallets
+        for mint in mints {
+            let wallet_key = WalletKey::new(mint.clone(), unit.clone());
+            if !self.has(&wallet_key).await {
+                return Err(Error::UnknownWallet(wallet_key.to_string()));
+            }
+        }
+
+        let wallet_key = WalletKey::new(mint_url, unit);
+        let wallet = self
+            .get_wallet(&wallet_key)
+            .await
+            .ok_or(Error::UnknownWallet(wallet_key.to_string()))?;
+
+        wallet
+            .receive(
+                encoded_token,
+                &SplitTarget::default(),
+                p2pk_signing_keys,
+                preimages,
+            )
+            .await
+    }
+
+    /// Pay an bolt11 invoice from specific wallet
+    #[instrument(skip(self, bolt11))]
+    pub async fn pay_invoice_for_wallet(
+        &self,
+        bolt11: &str,
+        wallet_key: &WalletKey,
+        max_fee: Option<Amount>,
+    ) -> Result<Melted, Error> {
+        let wallet = self
+            .get_wallet(wallet_key)
+            .await
+            .ok_or(Error::UnknownWallet(wallet_key.to_string()))?;
+
+        let quote = wallet.melt_quote(bolt11.to_string(), None).await?;
+        if let Some(max_fee) = max_fee {
+            if quote.fee_reserve > max_fee {
+                return Err(Error::MaxFeeExceeded);
+            }
+        }
+
+        wallet.melt(&quote.id, SplitTarget::default()).await
+    }
+
+    // Restore
+    #[instrument(skip(self))]
+    pub async fn restore(&self, wallet_key: &WalletKey) -> Result<Amount, Error> {
+        let wallet = self
+            .get_wallet(wallet_key)
+            .await
+            .ok_or(Error::UnknownWallet(wallet_key.to_string()))?;
+
+        wallet.restore().await
+    }
+
+    /// Verify token matches p2pk conditions
+    #[instrument(skip(self, token))]
+    pub async fn verify_token_p2pk(
+        &self,
+        wallet_key: &WalletKey,
+        token: &Token,
+        conditions: SpendingConditions,
+    ) -> Result<(), Error> {
+        let wallet = self
+            .get_wallet(wallet_key)
+            .await
+            .ok_or(Error::UnknownWallet(wallet_key.to_string()))?;
+
+        wallet.verify_token_p2pk(token, conditions)
+    }
+
+    /// Verifys all proofs in toke have valid dleq proof
+    #[instrument(skip(self, token))]
+    pub async fn verify_token_dleq(
+        &self,
+        wallet_key: &WalletKey,
+        token: &Token,
+    ) -> Result<(), Error> {
+        let wallet = self
+            .get_wallet(wallet_key)
+            .await
+            .ok_or(Error::UnknownWallet(wallet_key.to_string()))?;
+
+        wallet.verify_token_dleq(token).await
+    }
+}

+ 0 - 118
crates/cdk/src/wallet/nostr.rs

@@ -1,118 +0,0 @@
-//! Wallet Nostr functions
-
-use std::collections::HashSet;
-use std::str::FromStr;
-
-use nostr_sdk::nips::nip04;
-use nostr_sdk::{Filter, Timestamp};
-use tracing::instrument;
-
-use super::error::Error;
-use super::{util, Wallet};
-use crate::amount::{Amount, SplitTarget};
-use crate::nuts::SecretKey;
-
-impl Wallet {
-    /// Add nostr relays to client
-    #[instrument(skip(self))]
-    pub async fn add_nostr_relays(&self, relays: Vec<String>) -> Result<(), Error> {
-        self.nostr_client.add_relays(relays).await?;
-        Ok(())
-    }
-
-    /// Remove nostr relays to client
-    #[instrument(skip(self))]
-    pub async fn remove_nostr_relays(&self, relay: String) -> Result<(), Error> {
-        self.nostr_client.remove_relay(relay).await?;
-        Ok(())
-    }
-
-    /// Nostr relays
-    #[instrument(skip(self))]
-    pub async fn nostr_relays(&self) -> Vec<String> {
-        self.nostr_client
-            .relays()
-            .await
-            .keys()
-            .map(|url| url.to_string())
-            .collect()
-    }
-
-    /// Receive tokens sent to nostr pubkey via dm
-    #[instrument(skip_all)]
-    pub async fn nostr_receive(
-        &self,
-        nostr_signing_key: SecretKey,
-        since: Option<u64>,
-        amount_split_target: SplitTarget,
-    ) -> Result<Amount, Error> {
-        use nostr_sdk::{Keys, Kind};
-
-        use crate::util::unix_time;
-        use crate::Amount;
-
-        let verifying_key = nostr_signing_key.public_key();
-
-        let x_only_pubkey = verifying_key.x_only_public_key();
-
-        let nostr_pubkey = nostr_sdk::PublicKey::from_hex(x_only_pubkey.to_string())?;
-
-        let keys = Keys::from_str(&(nostr_signing_key).to_secret_hex())?;
-        self.add_p2pk_signing_key(nostr_signing_key).await;
-
-        let since = match since {
-            Some(since) => Some(Timestamp::from(since)),
-            None => self
-                .localstore
-                .get_nostr_last_checked(&verifying_key)
-                .await?
-                .map(|s| Timestamp::from(s as u64)),
-        };
-
-        let filter = match since {
-            Some(since) => Filter::new()
-                .pubkey(nostr_pubkey)
-                .kind(Kind::EncryptedDirectMessage)
-                .since(since),
-            None => Filter::new()
-                .pubkey(nostr_pubkey)
-                .kind(Kind::EncryptedDirectMessage),
-        };
-
-        self.nostr_client.connect().await;
-
-        let events = self.nostr_client.get_events_of(vec![filter], None).await?;
-
-        let mut tokens: HashSet<String> = HashSet::new();
-
-        for event in events {
-            if event.kind() == Kind::EncryptedDirectMessage {
-                if let Ok(msg) =
-                    nip04::decrypt(keys.secret_key()?, event.author_ref(), event.content())
-                {
-                    if let Some(token) = util::token_from_text(&msg) {
-                        tokens.insert(token.to_string());
-                    }
-                } else {
-                    tracing::error!("Impossible to decrypt direct message");
-                }
-            }
-        }
-
-        let mut total_received = Amount::ZERO;
-        for token in tokens.iter() {
-            match self.receive(token, &amount_split_target, None).await {
-                Ok(amount) => total_received += amount,
-                Err(err) => {
-                    tracing::error!("Could not receive token: {}", err);
-                }
-            }
-        }
-
-        self.localstore
-            .add_nostr_last_checked(verifying_key, unix_time() as u32)
-            .await?;
-
-        Ok(total_received)
-    }
-}

+ 1 - 2
crates/cdk/src/wallet/util.rs

@@ -5,8 +5,7 @@ use crate::nuts::{CurrencyUnit, Proofs, Token};
 use crate::UncheckedUrl;
 
 /// Extract token from text
-#[cfg(feature = "nostr")]
-pub(crate) fn token_from_text(text: &str) -> Option<&str> {
+pub fn token_from_text(text: &str) -> Option<&str> {
     let text = text.trim();
     if let Some(start) = text.find("cashu") {
         match text[start..].find(' ') {

+ 0 - 2
misc/scripts/check-crates.sh

@@ -26,11 +26,9 @@ buildargs=(
     "-p cdk"
     "-p cdk --no-default-features"
     "-p cdk --no-default-features --features wallet"
-    "-p cdk --no-default-features --features wallet --features nostr"
     "-p cdk --no-default-features --features mint"
     "-p cdk-redb"
     "-p cdk-redb --no-default-features --features wallet"
-    "-p cdk-redb --no-default-features --features wallet --features nostr"
     "-p cdk-redb --no-default-features --features mint"
     "-p cdk-sqlite --no-default-features --features mint"
     "-p cdk-sqlite --no-default-features --features wallet"

Některé soubory nejsou zobrazeny, neboť je v těchto rozdílových datech změněno mnoho souborů