Browse Source

feat(NUT02): add input_fee_ppk

chore: instrument log on mint fns
thesimplekid 9 months ago
parent
commit
17263b07f5

+ 3 - 1
CHANGELOG.md

@@ -27,7 +27,7 @@
 
 ### Changed
 cdk(wallet): `wallet:receive` will not claim `proofs` from a mint other then the wallet's mint ([thesimplekid]).
-cdk(NUT00): `Token` is changed from a struct to enum of either `TokenV4` or `Tokenv3` ([thesimplekid]).
+cdk(NUT00): `Token` is changed from a `struct` to `enum` of either `TokenV4` or `Tokenv3` ([thesimplekid]).
 cdk(NUT00): Rename `MintProofs` to `TokenV3Token` ([thesimplekid]).
 
 
@@ -40,6 +40,8 @@ cdk-mintd: Mint binary ([thesimplekid]).
 cdk-cln: cln backend for mint ([thesimplekid]).
 cdk-axum: Mint axum server ([thesimplekid]).
 cdk: NUT06 `MintInfo` and `NUTs` builder ([thesimplekid]).
+cdk: NUT00 `PreMintSecret` added Keyset id ([thesimplekid])
+cdk: NUT02 Support fees ([thesimplekid])
 
 ### Fixed
 cdk: NUT06 deseralize `MintInfo` ([thesimplekid]).

+ 1 - 0
Cargo.toml

@@ -34,6 +34,7 @@ cdk-axum = { version = "0.1", path = "./crates/cdk-axum", default-features = fal
 tokio = { version = "1", default-features = false }
 thiserror = "1"
 tracing = { version = "0.1", default-features = false, features = ["attributes", "log"] }
+tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
 serde = { version = "1", default-features = false, features = ["derive"] }
 serde_json = "1"
 serde-wasm-bindgen = "0.6.5"

+ 18 - 11
bindings/cdk-js/src/wallet.rs

@@ -5,7 +5,7 @@ use std::sync::Arc;
 
 use cdk::amount::SplitTarget;
 use cdk::nuts::{Proofs, SecretKey};
-use cdk::wallet::Wallet;
+use cdk::wallet::{SendKind, Wallet};
 use cdk::Amount;
 use cdk_rexie::WalletRexieDatabase;
 use wasm_bindgen::prelude::*;
@@ -44,7 +44,7 @@ impl JsWallet {
     pub async fn new(mints_url: String, unit: JsCurrencyUnit, seed: Vec<u8>) -> Self {
         let db = WalletRexieDatabase::new().await.unwrap();
 
-        Wallet::new(&mints_url, unit.into(), Arc::new(db), &seed).into()
+        Wallet::new(&mints_url, unit.into(), Arc::new(db), &seed, None).into()
     }
 
     #[wasm_bindgen(js_name = totalBalance)]
@@ -81,12 +81,6 @@ impl JsWallet {
             .map(|i| i.into()))
     }
 
-    #[wasm_bindgen(js_name = refreshMint)]
-    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, amount: u64) -> Result<JsMintQuote> {
         let quote = self
@@ -200,7 +194,7 @@ impl JsWallet {
             .inner
             .receive(
                 &encoded_token,
-                &SplitTarget::default(),
+                SplitTarget::default(),
                 &signing_keys,
                 &preimages,
             )
@@ -234,7 +228,14 @@ impl JsWallet {
             .map(|a| SplitTarget::Value(*a.deref()))
             .unwrap_or_default();
         self.inner
-            .send(Amount::from(amount), memo, conditions, &target)
+            .send(
+                Amount::from(amount),
+                memo,
+                conditions,
+                &target,
+                &SendKind::default(),
+                false,
+            )
             .await
             .map_err(into_err)
     }
@@ -267,7 +268,13 @@ impl JsWallet {
             .unwrap_or_default();
         let post_swap_proofs = self
             .inner
-            .swap(Some(Amount::from(amount)), &target, proofs, conditions)
+            .swap(
+                Some(Amount::from(amount)),
+                target,
+                proofs,
+                conditions,
+                false,
+            )
             .await
             .map_err(into_err)?;
 

+ 1 - 1
crates/cdk-cli/Cargo.toml

@@ -22,7 +22,7 @@ serde = { workspace = true, features = ["derive"] }
 serde_json.workspace = true
 tokio.workspace = true
 tracing.workspace = true
-tracing-subscriber = "0.3.18"
+tracing-subscriber.workspace = true
 rand = "0.8.5"
 home.workspace = true
 nostr-sdk = { version = "0.32.0", default-features = false, features = [

+ 10 - 4
crates/cdk-cli/src/main.rs

@@ -14,6 +14,7 @@ use cdk_sqlite::WalletSqliteDatabase;
 use clap::{Parser, Subcommand};
 use rand::Rng;
 use tracing::Level;
+use tracing_subscriber::EnvFilter;
 
 mod sub_commands;
 
@@ -69,11 +70,15 @@ enum Commands {
 
 #[tokio::main]
 async fn main() -> Result<()> {
-    // Parse input
     let args: Cli = Cli::parse();
-    tracing_subscriber::fmt()
-        .with_max_level(args.log_level)
-        .init();
+    let default_filter = args.log_level;
+
+    let sqlx_filter = "sqlx=warn";
+
+    let env_filter = EnvFilter::new(format!("{},{}", default_filter, sqlx_filter));
+
+    // Parse input
+    tracing_subscriber::fmt().with_env_filter(env_filter).init();
 
     let work_dir = match &args.work_dir {
         Some(work_dir) => work_dir.clone(),
@@ -131,6 +136,7 @@ async fn main() -> Result<()> {
             cdk::nuts::CurrencyUnit::Sat,
             localstore.clone(),
             &mnemonic.to_seed_normalized(""),
+            None,
         );
 
         wallets.insert(mint, wallet);

+ 7 - 1
crates/cdk-cli/src/sub_commands/mint.rs

@@ -32,7 +32,13 @@ pub async fn mint(
     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),
+        None => Wallet::new(
+            &mint_url.to_string(),
+            CurrencyUnit::Sat,
+            localstore,
+            seed,
+            None,
+        ),
     };
 
     let quote = wallet

+ 2 - 1
crates/cdk-cli/src/sub_commands/receive.rs

@@ -136,11 +136,12 @@ async fn receive_token(
             CurrencyUnit::Sat,
             Arc::clone(localstore),
             seed,
+            None,
         ),
     };
 
     let amount = wallet
-        .receive(token_str, &SplitTarget::default(), signing_keys, preimage)
+        .receive(token_str, SplitTarget::default(), signing_keys, preimage)
         .await?;
     Ok(amount)
 }

+ 19 - 0
crates/cdk-cli/src/sub_commands/send.rs

@@ -6,6 +6,7 @@ use std::str::FromStr;
 use anyhow::{bail, Result};
 use cdk::amount::SplitTarget;
 use cdk::nuts::{Conditions, PublicKey, SpendingConditions, Token};
+use cdk::wallet::types::SendKind;
 use cdk::wallet::Wallet;
 use cdk::{Amount, UncheckedUrl};
 use clap::Args;
@@ -35,6 +36,15 @@ pub struct SendSubCommand {
     /// Token as V3 token
     #[arg(short, long)]
     v3: bool,
+    /// Should the send be offline only
+    #[arg(short, long)]
+    offline: bool,
+    /// Include fee to redeam in token
+    #[arg(short, long)]
+    include_fee: bool,
+    /// Amount willing to overpay to avoid a swap
+    #[arg(short, long)]
+    tolerance: Option<u64>,
 }
 
 pub async fn send(
@@ -146,12 +156,21 @@ pub async fn send(
 
     let wallet = mints_amounts[mint_number].0.clone();
 
+    let send_kind = match (sub_command_args.offline, sub_command_args.tolerance) {
+        (true, Some(amount)) => SendKind::OfflineTolerance(Amount::from(amount)),
+        (true, None) => SendKind::OfflineExact,
+        (false, Some(amount)) => SendKind::OnlineTolerance(Amount::from(amount)),
+        (false, None) => SendKind::OnlineExact,
+    };
+
     let token = wallet
         .send(
             token_amount,
             sub_command_args.memo.clone(),
             conditions,
             &SplitTarget::default(),
+            &send_kind,
+            sub_command_args.include_fee,
         )
         .await?;
 

+ 1 - 1
crates/cdk-mintd/Cargo.toml

@@ -21,7 +21,7 @@ config = { version = "0.13.3", features = ["toml"] }
 clap = { version = "4.4.8", features = ["derive", "env", "default"] }
 tokio.workspace = true
 tracing.workspace = true
-tracing-subscriber = "0.3.18"
+tracing-subscriber.workspace = true
 futures = "0.3.28"
 serde.workspace = true
 bip39.workspace = true

+ 1 - 0
crates/cdk-mintd/example.config.toml

@@ -3,6 +3,7 @@ url = "https://mint.thesimplekid.dev/"
 listen_host = "127.0.0.1"
 listen_port = 8085
 mnemonic = ""
+# input_fee_ppk = 0
 
 
 

+ 1 - 0
crates/cdk-mintd/src/config.rs

@@ -13,6 +13,7 @@ pub struct Info {
     pub listen_port: u16,
     pub mnemonic: String,
     pub seconds_quote_is_valid_for: Option<u64>,
+    pub input_fee_ppk: Option<u64>,
 }
 
 #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]

+ 11 - 3
crates/cdk-mintd/src/main.rs

@@ -28,6 +28,7 @@ use cli::CLIArgs;
 use config::{DatabaseEngine, LnBackend};
 use futures::StreamExt;
 use tower_http::cors::CorsLayer;
+use tracing_subscriber::EnvFilter;
 
 mod cli;
 mod config;
@@ -37,9 +38,13 @@ const DEFAULT_QUOTE_TTL_SECS: u64 = 1800;
 
 #[tokio::main]
 async fn main() -> anyhow::Result<()> {
-    tracing_subscriber::fmt()
-        .with_max_level(tracing::Level::DEBUG)
-        .init();
+    let default_filter = "debug";
+
+    let sqlx_filter = "sqlx=warn";
+
+    let env_filter = EnvFilter::new(format!("{},{}", default_filter, sqlx_filter));
+
+    tracing_subscriber::fmt().with_env_filter(env_filter).init();
 
     let args = CLIArgs::parse();
 
@@ -206,6 +211,8 @@ async fn main() -> anyhow::Result<()> {
 
     let mnemonic = Mnemonic::from_str(&settings.info.mnemonic)?;
 
+    let input_fee_ppk = settings.info.input_fee_ppk.unwrap_or(0);
+
     let mint = Mint::new(
         &settings.info.url,
         &mnemonic.to_seed_normalized(""),
@@ -213,6 +220,7 @@ async fn main() -> anyhow::Result<()> {
         localstore,
         absolute_ln_fee_reserve,
         relative_ln_fee,
+        input_fee_ppk,
     )
     .await?;
 

+ 10 - 0
crates/cdk-redb/src/wallet/mod.rs

@@ -288,6 +288,7 @@ impl WalletDatabase for WalletRedbDatabase {
             let mut table = write_txn
                 .open_multimap_table(MINT_KEYSETS_TABLE)
                 .map_err(Error::from)?;
+            let mut keysets_table = write_txn.open_table(KEYSETS_TABLE).map_err(Error::from)?;
 
             for keyset in keysets {
                 table
@@ -296,6 +297,15 @@ impl WalletDatabase for WalletRedbDatabase {
                         keyset.id.to_bytes().as_slice(),
                     )
                     .map_err(Error::from)?;
+
+                keysets_table
+                    .insert(
+                        keyset.id.to_bytes().as_slice(),
+                        serde_json::to_string(&keyset)
+                            .map_err(Error::from)?
+                            .as_str(),
+                    )
+                    .map_err(Error::from)?;
             }
         }
         write_txn.commit().map_err(Error::from)?;

+ 1 - 0
crates/cdk-sqlite/src/mint/migrations/20240710145043_input_fee.sql

@@ -0,0 +1 @@
+ALTER TABLE keyset ADD input_fee_ppk INTEGER;

+ 6 - 3
crates/cdk-sqlite/src/mint/mod.rs

@@ -407,9 +407,9 @@ WHERE id=?
     async fn add_keyset_info(&self, keyset: MintKeySetInfo) -> Result<(), Self::Err> {
         sqlx::query(
             r#"
-INSERT INTO keyset
-(id, unit, active, valid_from, valid_to, derivation_path, max_order)
-VALUES (?, ?, ?, ?, ?, ?, ?);
+INSERT OR REPLACE INTO keyset
+(id, unit, active, valid_from, valid_to, derivation_path, max_order, input_fee_ppk)
+VALUES (?, ?, ?, ?, ?, ?, ?, ?);
         "#,
         )
         .bind(keyset.id.to_string())
@@ -419,6 +419,7 @@ VALUES (?, ?, ?, ?, ?, ?, ?);
         .bind(keyset.valid_to.map(|v| v as i64))
         .bind(keyset.derivation_path.to_string())
         .bind(keyset.max_order)
+        .bind(keyset.input_fee_ppk as i64)
         .execute(&self.pool)
         .await
         .map_err(Error::from)?;
@@ -714,6 +715,7 @@ fn sqlite_row_to_keyset_info(row: SqliteRow) -> Result<MintKeySetInfo, Error> {
     let row_valid_to: Option<i64> = row.try_get("valid_to").map_err(Error::from)?;
     let row_derivation_path: String = row.try_get("derivation_path").map_err(Error::from)?;
     let row_max_order: u8 = row.try_get("max_order").map_err(Error::from)?;
+    let row_keyset_ppk: Option<i64> = row.try_get("input_fee_ppk").map_err(Error::from)?;
 
     Ok(MintKeySetInfo {
         id: Id::from_str(&row_id).map_err(Error::from)?,
@@ -723,6 +725,7 @@ fn sqlite_row_to_keyset_info(row: SqliteRow) -> Result<MintKeySetInfo, Error> {
         valid_to: row_valid_to.map(|v| v as u64),
         derivation_path: DerivationPath::from_str(&row_derivation_path).map_err(Error::from)?,
         max_order: row_max_order,
+        input_fee_ppk: row_keyset_ppk.unwrap_or(0) as u64,
     })
 }
 

+ 1 - 0
crates/cdk-sqlite/src/wallet/migrations/20240710144711_input_fee.sql

@@ -0,0 +1 @@
+ALTER TABLE keyset ADD input_fee_ppk INTEGER;

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

@@ -211,14 +211,15 @@ FROM mint
             sqlx::query(
                 r#"
 INSERT OR REPLACE INTO keyset
-(mint_url, id, unit, active)
-VALUES (?, ?, ?, ?);
+(mint_url, id, unit, active, input_fee_ppk)
+VALUES (?, ?, ?, ?, ?);
         "#,
             )
             .bind(mint_url.to_string())
             .bind(keyset.id.to_string())
             .bind(keyset.unit.to_string())
             .bind(keyset.active)
+            .bind(keyset.input_fee_ppk as i64)
             .execute(&self.pool)
             .await
             .map_err(Error::from)?;
@@ -708,11 +709,13 @@ fn sqlite_row_to_keyset(row: &SqliteRow) -> Result<KeySetInfo, Error> {
     let row_id: String = row.try_get("id").map_err(Error::from)?;
     let row_unit: String = row.try_get("unit").map_err(Error::from)?;
     let active: bool = row.try_get("active").map_err(Error::from)?;
+    let row_keyset_ppk: Option<i64> = row.try_get("input_fee_ppk").map_err(Error::from)?;
 
     Ok(KeySetInfo {
         id: Id::from_str(&row_id)?,
         unit: CurrencyUnit::from_str(&row_unit).map_err(Error::from)?,
         active,
+        input_fee_ppk: row_keyset_ppk.unwrap_or(0) as u64,
     })
 }
 

+ 4 - 0
crates/cdk/Cargo.toml

@@ -69,6 +69,10 @@ required-features = ["wallet"]
 name = "wallet"
 required-features = ["wallet"]
 
+[[example]]
+name = "proof_selection"
+required-features = ["wallet"]
+
 [dev-dependencies]
 rand = "0.8.5"
 bip39.workspace = true

+ 10 - 2
crates/cdk/examples/mint-token.rs

@@ -5,6 +5,7 @@ use cdk::amount::SplitTarget;
 use cdk::cdk_database::WalletMemoryDatabase;
 use cdk::error::Error;
 use cdk::nuts::{CurrencyUnit, MintQuoteState};
+use cdk::wallet::types::SendKind;
 use cdk::wallet::Wallet;
 use cdk::Amount;
 use rand::Rng;
@@ -19,7 +20,7 @@ async fn main() -> Result<(), Error> {
     let unit = CurrencyUnit::Sat;
     let amount = Amount::from(10);
 
-    let wallet = Wallet::new(mint_url, unit, Arc::new(localstore), &seed);
+    let wallet = Wallet::new(mint_url, unit, Arc::new(localstore), &seed, None);
 
     let quote = wallet.mint_quote(amount).await.unwrap();
 
@@ -45,7 +46,14 @@ async fn main() -> Result<(), Error> {
     println!("Received {receive_amount} from mint {mint_url}");
 
     let token = wallet
-        .send(amount, None, None, &SplitTarget::default())
+        .send(
+            amount,
+            None,
+            None,
+            &SplitTarget::default(),
+            &SendKind::OnlineExact,
+            false,
+        )
         .await
         .unwrap();
 

+ 11 - 3
crates/cdk/examples/p2pk.rs

@@ -5,6 +5,7 @@ use cdk::amount::SplitTarget;
 use cdk::cdk_database::WalletMemoryDatabase;
 use cdk::error::Error;
 use cdk::nuts::{CurrencyUnit, MintQuoteState, SecretKey, SpendingConditions};
+use cdk::wallet::types::SendKind;
 use cdk::wallet::Wallet;
 use cdk::Amount;
 use rand::Rng;
@@ -19,7 +20,7 @@ async fn main() -> Result<(), Error> {
     let unit = CurrencyUnit::Sat;
     let amount = Amount::from(10);
 
-    let wallet = Wallet::new(mint_url, unit, Arc::new(localstore), &seed);
+    let wallet = Wallet::new(mint_url, unit, Arc::new(localstore), &seed, None);
 
     let quote = wallet.mint_quote(amount).await.unwrap();
 
@@ -47,7 +48,14 @@ async fn main() -> Result<(), Error> {
     let spending_conditions = SpendingConditions::new_p2pk(secret.public_key(), None);
 
     let token = wallet
-        .send(amount, None, Some(spending_conditions), &SplitTarget::None)
+        .send(
+            amount,
+            None,
+            Some(spending_conditions),
+            &SplitTarget::None,
+            &SendKind::default(),
+            false,
+        )
         .await
         .unwrap();
 
@@ -55,7 +63,7 @@ async fn main() -> Result<(), Error> {
     println!("{}", token);
 
     let amount = wallet
-        .receive(&token, &SplitTarget::default(), &[secret], &[])
+        .receive(&token, SplitTarget::default(), &[secret], &[])
         .await
         .unwrap();
 

+ 61 - 0
crates/cdk/examples/proof_selection.rs

@@ -0,0 +1,61 @@
+//! Wallet example with memory store
+
+use std::sync::Arc;
+use std::time::Duration;
+
+use cdk::amount::SplitTarget;
+use cdk::cdk_database::WalletMemoryDatabase;
+use cdk::nuts::{CurrencyUnit, MintQuoteState};
+use cdk::wallet::Wallet;
+use cdk::Amount;
+use rand::Rng;
+use tokio::time::sleep;
+
+#[tokio::main]
+async fn main() {
+    let seed = rand::thread_rng().gen::<[u8; 32]>();
+
+    let mint_url = "https://testnut.cashu.space";
+    let unit = CurrencyUnit::Sat;
+
+    let localstore = WalletMemoryDatabase::default();
+
+    let wallet = Wallet::new(mint_url, unit, Arc::new(localstore), &seed, None);
+
+    for amount in [64] {
+        let amount = Amount::from(amount);
+        let quote = wallet.mint_quote(amount).await.unwrap();
+
+        println!("Pay request: {}", quote.request);
+
+        loop {
+            let status = wallet.mint_quote_state(&quote.id).await.unwrap();
+
+            if status.state == MintQuoteState::Paid {
+                break;
+            }
+
+            println!("Quote state: {}", status.state);
+
+            sleep(Duration::from_secs(5)).await;
+        }
+
+        let receive_amount = wallet
+            .mint(&quote.id, SplitTarget::default(), None)
+            .await
+            .unwrap();
+
+        println!("Minted {}", receive_amount);
+    }
+
+    let proofs = wallet.get_proofs().await.unwrap();
+
+    let selected = wallet
+        .select_proofs_to_send(Amount::from(65), proofs, false)
+        .await
+        .unwrap();
+
+    for (i, proof) in selected.iter().enumerate() {
+        println!("{}: {}", i, proof.amount);
+    }
+}

+ 10 - 2
crates/cdk/examples/wallet.rs

@@ -6,6 +6,7 @@ use std::time::Duration;
 use cdk::amount::SplitTarget;
 use cdk::cdk_database::WalletMemoryDatabase;
 use cdk::nuts::{CurrencyUnit, MintQuoteState};
+use cdk::wallet::types::SendKind;
 use cdk::wallet::Wallet;
 use cdk::Amount;
 use rand::Rng;
@@ -21,7 +22,7 @@ async fn main() {
 
     let localstore = WalletMemoryDatabase::default();
 
-    let wallet = Wallet::new(mint_url, unit, Arc::new(localstore), &seed);
+    let wallet = Wallet::new(mint_url, unit, Arc::new(localstore), &seed, None);
 
     let quote = wallet.mint_quote(amount).await.unwrap();
 
@@ -47,7 +48,14 @@ async fn main() {
     println!("Minted {}", receive_amount);
 
     let token = wallet
-        .send(amount, None, None, &SplitTarget::None)
+        .send(
+            amount,
+            None,
+            None,
+            &SplitTarget::None,
+            &SendKind::default(),
+            false,
+        )
         .await
         .unwrap();
 

+ 65 - 11
crates/cdk/src/amount.rs

@@ -2,10 +2,13 @@
 //!
 //! Is any unit and will be treated as the unit of the wallet
 
+use std::cmp::Ordering;
 use std::fmt;
 
 use serde::{Deserialize, Serialize};
 
+use crate::error::Error;
+
 /// Amount can be any unit
 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
 #[serde(transparent)]
@@ -28,12 +31,12 @@ impl Amount {
     }
 
     /// Split into parts that are powers of two by target
-    pub fn split_targeted(&self, target: &SplitTarget) -> Vec<Self> {
-        let mut parts = match *target {
+    pub fn split_targeted(&self, target: &SplitTarget) -> Result<Vec<Self>, Error> {
+        let mut parts = match target {
             SplitTarget::None => self.split(),
             SplitTarget::Value(amount) => {
-                if self.le(&amount) {
-                    return self.split();
+                if self.le(amount) {
+                    return Ok(self.split());
                 }
 
                 let mut parts_total = Amount::ZERO;
@@ -61,10 +64,28 @@ impl Amount {
 
                 parts
             }
+            SplitTarget::Values(values) => {
+                let values_total: Amount = values.clone().into_iter().sum();
+
+                match self.cmp(&values_total) {
+                    Ordering::Equal => values.clone(),
+                    Ordering::Less => {
+                        return Err(Error::SplitValuesGreater);
+                    }
+                    Ordering::Greater => {
+                        let extra = *self - values_total;
+                        let mut extra_amount = extra.split();
+                        let mut values = values.clone();
+
+                        values.append(&mut extra_amount);
+                        values
+                    }
+                }
+            }
         };
 
         parts.sort();
-        parts
+        Ok(parts)
     }
 }
 
@@ -162,15 +183,15 @@ impl core::iter::Sum for Amount {
 }
 
 /// Kinds of targeting that are supported
-#[derive(
-    Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default, Serialize, Deserialize,
-)]
+#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Default, Serialize, Deserialize)]
 pub enum SplitTarget {
     /// Default target; least amount of proofs
     #[default]
     None,
     /// Target amount for wallet to have most proofs that add up to value
     Value(Amount),
+    /// Specific amounts to split into **must** equal amount being split
+    Values(Vec<Amount>),
 }
 
 #[cfg(test)]
@@ -198,12 +219,16 @@ mod tests {
     fn test_split_target_amount() {
         let amount = Amount(65);
 
-        let split = amount.split_targeted(&SplitTarget::Value(Amount(32)));
+        let split = amount
+            .split_targeted(&SplitTarget::Value(Amount(32)))
+            .unwrap();
         assert_eq!(vec![Amount(1), Amount(32), Amount(32)], split);
 
         let amount = Amount(150);
 
-        let split = amount.split_targeted(&SplitTarget::Value(Amount::from(50)));
+        let split = amount
+            .split_targeted(&SplitTarget::Value(Amount::from(50)))
+            .unwrap();
         assert_eq!(
             vec![
                 Amount(2),
@@ -221,7 +246,9 @@ mod tests {
 
         let amount = Amount::from(63);
 
-        let split = amount.split_targeted(&SplitTarget::Value(Amount::from(32)));
+        let split = amount
+            .split_targeted(&SplitTarget::Value(Amount::from(32)))
+            .unwrap();
         assert_eq!(
             vec![
                 Amount(1),
@@ -234,4 +261,31 @@ mod tests {
             split
         );
     }
+
+    #[test]
+    fn test_split_values() {
+        let amount = Amount(10);
+
+        let target = vec![Amount(2), Amount(4), Amount(4)];
+
+        let split_target = SplitTarget::Values(target.clone());
+
+        let values = amount.split_targeted(&split_target).unwrap();
+
+        assert_eq!(target, values);
+
+        let target = vec![Amount(2), Amount(4), Amount(4)];
+
+        let split_target = SplitTarget::Values(vec![Amount(2), Amount(4)]);
+
+        let values = amount.split_targeted(&split_target).unwrap();
+
+        assert_eq!(target, values);
+
+        let split_target = SplitTarget::Values(vec![Amount(2), Amount(10)]);
+
+        let values = amount.split_targeted(&split_target);
+
+        assert!(values.is_err())
+    }
 }

+ 3 - 0
crates/cdk/src/error.rs

@@ -54,6 +54,9 @@ pub enum Error {
     /// No valid point on curve
     #[error("No valid point found")]
     NoValidPoint,
+    /// Split Values must be less then or equal to amount
+    #[error("Split Values must be less then or equal to amount")]
+    SplitValuesGreater,
     /// Secp256k1 error
     #[error(transparent)]
     Secp256k1(#[from] bitcoin::secp256k1::Error),

+ 3 - 0
crates/cdk/src/mint/error.rs

@@ -20,6 +20,9 @@ pub enum Error {
     /// Amount is not what is expected
     #[error("Amount")]
     Amount,
+    /// Not engough inputs provided
+    #[error("Inputs: `{0}`, Outputs: `{0}`, Fee: `{0}`")]
+    InsufficientInputs(u64, u64, u64),
     /// Duplicate proofs provided
     #[error("Duplicate proofs")]
     DuplicateProofs,

+ 114 - 15
crates/cdk/src/mint/mod.rs

@@ -8,6 +8,7 @@ use bitcoin::secp256k1::{self, Secp256k1};
 use error::Error;
 use serde::{Deserialize, Serialize};
 use tokio::sync::RwLock;
+use tracing::instrument;
 
 use self::nut05::QuoteState;
 use crate::cdk_database::{self, MintDatabase};
@@ -47,6 +48,7 @@ impl Mint {
         localstore: Arc<dyn MintDatabase<Err = cdk_database::Error> + Send + Sync>,
         min_fee_reserve: Amount,
         percent_fee_reserve: f32,
+        input_fee_ppk: u64,
     ) -> Result<Self, Error> {
         let secp_ctx = Secp256k1::new();
         let xpriv =
@@ -58,6 +60,9 @@ impl Mint {
         match keysets_infos.is_empty() {
             false => {
                 for keyset_info in keysets_infos {
+                    let mut keyset_info = keyset_info;
+                    keyset_info.input_fee_ppk = input_fee_ppk;
+                    localstore.add_keyset_info(keyset_info.clone()).await?;
                     if keyset_info.active {
                         let id = keyset_info.id;
                         let keyset = MintKeySet::generate_from_xpriv(&secp_ctx, xpriv, keyset_info);
@@ -69,8 +74,14 @@ impl Mint {
                 let derivation_path = DerivationPath::from(vec![
                     ChildNumber::from_hardened_idx(0).expect("0 is a valid index")
                 ]);
-                let (keyset, keyset_info) =
-                    create_new_keyset(&secp_ctx, xpriv, derivation_path, CurrencyUnit::Sat, 64);
+                let (keyset, keyset_info) = create_new_keyset(
+                    &secp_ctx,
+                    xpriv,
+                    derivation_path,
+                    CurrencyUnit::Sat,
+                    64,
+                    input_fee_ppk,
+                );
                 let id = keyset_info.id;
                 localstore.add_keyset_info(keyset_info).await?;
                 localstore.add_active_keyset(CurrencyUnit::Sat, id).await?;
@@ -95,26 +106,31 @@ impl Mint {
     }
 
     /// Set Mint Url
+    #[instrument(skip_all)]
     pub fn set_mint_url(&mut self, mint_url: UncheckedUrl) {
         self.mint_url = mint_url;
     }
 
     /// Get Mint Url
+    #[instrument(skip_all)]
     pub fn get_mint_url(&self) -> &UncheckedUrl {
         &self.mint_url
     }
 
     /// Set Mint Info
+    #[instrument(skip_all)]
     pub fn set_mint_info(&mut self, mint_info: MintInfo) {
         self.mint_info = mint_info;
     }
 
     /// Get Mint Info
+    #[instrument(skip_all)]
     pub fn mint_info(&self) -> &MintInfo {
         &self.mint_info
     }
 
     /// New mint quote
+    #[instrument(skip_all)]
     pub async fn new_mint_quote(
         &self,
         mint_url: UncheckedUrl,
@@ -139,6 +155,7 @@ impl Mint {
     }
 
     /// Check mint quote
+    #[instrument(skip(self))]
     pub async fn check_mint_quote(&self, quote_id: &str) -> Result<MintQuoteBolt11Response, Error> {
         let quote = self
             .localstore
@@ -165,18 +182,21 @@ impl Mint {
     }
 
     /// Update mint quote
+    #[instrument(skip_all)]
     pub async fn update_mint_quote(&self, quote: MintQuote) -> Result<(), Error> {
         self.localstore.add_mint_quote(quote).await?;
         Ok(())
     }
 
     /// Get mint quotes
+    #[instrument(skip_all)]
     pub async fn mint_quotes(&self) -> Result<Vec<MintQuote>, Error> {
         let quotes = self.localstore.get_mint_quotes().await?;
         Ok(quotes)
     }
 
     /// Get pending mint quotes
+    #[instrument(skip_all)]
     pub async fn get_pending_mint_quotes(&self) -> Result<Vec<MintQuote>, Error> {
         let mint_quotes = self.localstore.get_mint_quotes().await?;
 
@@ -187,6 +207,7 @@ impl Mint {
     }
 
     /// Remove mint quote
+    #[instrument(skip_all)]
     pub async fn remove_mint_quote(&self, quote_id: &str) -> Result<(), Error> {
         self.localstore.remove_mint_quote(quote_id).await?;
 
@@ -194,6 +215,7 @@ impl Mint {
     }
 
     /// New melt quote
+    #[instrument(skip_all)]
     pub async fn new_melt_quote(
         &self,
         request: String,
@@ -217,7 +239,28 @@ impl Mint {
         Ok(quote)
     }
 
+    /// Fee required for proof set
+    #[instrument(skip_all)]
+    pub async fn get_proofs_fee(&self, proofs: &Proofs) -> Result<Amount, Error> {
+        let mut sum_fee = 0;
+
+        for proof in proofs {
+            let input_fee_ppk = self
+                .localstore
+                .get_keyset_info(&proof.keyset_id)
+                .await?
+                .ok_or(Error::UnknownKeySet)?;
+
+            sum_fee += input_fee_ppk.input_fee_ppk;
+        }
+
+        let fee = (sum_fee + 999) / 1000;
+
+        Ok(Amount::from(fee))
+    }
+
     /// Check melt quote status
+    #[instrument(skip(self))]
     pub async fn check_melt_quote(&self, quote_id: &str) -> Result<MeltQuoteBolt11Response, Error> {
         let quote = self
             .localstore
@@ -238,26 +281,29 @@ impl Mint {
     }
 
     /// Update melt quote
+    #[instrument(skip_all)]
     pub async fn update_melt_quote(&self, quote: MeltQuote) -> Result<(), Error> {
         self.localstore.add_melt_quote(quote).await?;
         Ok(())
     }
 
     /// Get melt quotes
+    #[instrument(skip_all)]
     pub async fn melt_quotes(&self) -> Result<Vec<MeltQuote>, Error> {
         let quotes = self.localstore.get_melt_quotes().await?;
         Ok(quotes)
     }
 
     /// Remove melt quote
+    #[instrument(skip(self))]
     pub async fn remove_melt_quote(&self, quote_id: &str) -> Result<(), Error> {
         self.localstore.remove_melt_quote(quote_id).await?;
 
         Ok(())
     }
 
-    /// Retrieve the public keys of the active keyset for distribution to
-    /// wallet clients
+    /// Retrieve the public keys of the active keyset for distribution to wallet clients
+    #[instrument(skip(self))]
     pub async fn keyset_pubkeys(&self, keyset_id: &Id) -> Result<KeysResponse, Error> {
         self.ensure_keyset_loaded(keyset_id).await?;
         let keysets = self.keysets.read().await;
@@ -267,8 +313,8 @@ impl Mint {
         })
     }
 
-    /// Retrieve the public keys of the active keyset for distribution to
-    /// wallet clients
+    /// Retrieve the public keys of the active keyset for distribution to wallet clients
+    #[instrument(skip_all)]
     pub async fn pubkeys(&self) -> Result<KeysResponse, Error> {
         let keyset_infos = self.localstore.get_keyset_infos().await?;
         for keyset_info in keyset_infos {
@@ -281,6 +327,7 @@ impl Mint {
     }
 
     /// Return a list of all supported keysets
+    #[instrument(skip_all)]
     pub async fn keysets(&self) -> Result<KeysetResponse, Error> {
         let keysets = self.localstore.get_keyset_infos().await?;
         let active_keysets: HashSet<Id> = self
@@ -297,6 +344,7 @@ impl Mint {
                 id: k.id,
                 unit: k.unit,
                 active: active_keysets.contains(&k.id),
+                input_fee_ppk: k.input_fee_ppk,
             })
             .collect();
 
@@ -304,6 +352,7 @@ impl Mint {
     }
 
     /// Get keysets
+    #[instrument(skip(self))]
     pub async fn keyset(&self, id: &Id) -> Result<Option<KeySet>, Error> {
         self.ensure_keyset_loaded(id).await?;
         let keysets = self.keysets.read().await;
@@ -313,14 +362,22 @@ impl Mint {
 
     /// Add current keyset to inactive keysets
     /// Generate new keyset
+    #[instrument(skip(self))]
     pub async fn rotate_keyset(
         &self,
         unit: CurrencyUnit,
         derivation_path: DerivationPath,
         max_order: u8,
+        input_fee_ppk: u64,
     ) -> Result<(), Error> {
-        let (keyset, keyset_info) =
-            create_new_keyset(&self.secp_ctx, self.xpriv, derivation_path, unit, max_order);
+        let (keyset, keyset_info) = create_new_keyset(
+            &self.secp_ctx,
+            self.xpriv,
+            derivation_path,
+            unit,
+            max_order,
+            input_fee_ppk,
+        );
         let id = keyset_info.id;
         self.localstore.add_keyset_info(keyset_info).await?;
         self.localstore.add_active_keyset(unit, id).await?;
@@ -332,6 +389,7 @@ impl Mint {
     }
 
     /// Process mint request
+    #[instrument(skip_all)]
     pub async fn process_mint_request(
         &self,
         mint_request: nut04::MintBolt11Request,
@@ -397,6 +455,7 @@ impl Mint {
     }
 
     /// Blind Sign
+    #[instrument(skip_all)]
     pub async fn blind_sign(
         &self,
         blinded_message: &BlindedMessage,
@@ -447,6 +506,7 @@ impl Mint {
     }
 
     /// Process Swap
+    #[instrument(skip_all)]
     pub async fn process_swap_request(
         &self,
         swap_request: SwapRequest,
@@ -470,8 +530,20 @@ impl Mint {
 
         let output_total = swap_request.output_amount();
 
-        if proofs_total != output_total {
-            return Err(Error::Amount);
+        let fee = self.get_proofs_fee(&swap_request.inputs).await?;
+
+        if proofs_total < output_total + fee {
+            tracing::info!(
+                "Swap request without enough inputs: {}, outputs {}, fee {}",
+                proofs_total,
+                output_total,
+                fee
+            );
+            return Err(Error::InsufficientInputs(
+                proofs_total.into(),
+                output_total.into(),
+                fee.into(),
+            ));
         }
 
         let proof_count = swap_request.inputs.len();
@@ -554,6 +626,7 @@ impl Mint {
     }
 
     /// Verify [`Proof`] meets conditions and is signed
+    #[instrument(skip_all)]
     pub async fn verify_proof(&self, proof: &Proof) -> Result<(), Error> {
         // Check if secret is a nut10 secret with conditions
         if let Ok(secret) =
@@ -597,6 +670,7 @@ impl Mint {
     }
 
     /// Check state
+    #[instrument(skip_all)]
     pub async fn check_state(
         &self,
         check_state: &CheckStateRequest,
@@ -622,6 +696,7 @@ impl Mint {
     }
 
     /// Verify melt request is valid
+    #[instrument(skip_all)]
     pub async fn verify_melt_request(
         &self,
         melt_request: &MeltBolt11Request,
@@ -653,15 +728,23 @@ impl Mint {
 
         let proofs_total = melt_request.proofs_amount();
 
-        let required_total = quote.amount + quote.fee_reserve;
+        let fee = self.get_proofs_fee(&melt_request.inputs).await?;
+
+        let required_total = quote.amount + quote.fee_reserve + fee;
 
         if proofs_total < required_total {
-            tracing::debug!(
-                "Insufficient Proofs: Got: {}, Required: {}",
+            tracing::info!(
+                "Swap request without enough inputs: {}, quote amount {}, fee_reserve: {} fee {}",
                 proofs_total,
-                required_total
+                quote.amount,
+                quote.fee_reserve,
+                fee
             );
-            return Err(Error::Amount);
+            return Err(Error::InsufficientInputs(
+                proofs_total.into(),
+                (quote.amount + quote.fee_reserve).into(),
+                fee.into(),
+            ));
         }
 
         let input_keyset_ids: HashSet<Id> =
@@ -740,6 +823,7 @@ impl Mint {
     /// Process unpaid melt request
     /// In the event that a melt request fails and the lighthing payment is not made
     /// The [`Proofs`] should be returned to an unspent state and the quote should be unpaid
+    #[instrument(skip_all)]
     pub async fn process_unpaid_melt(&self, melt_request: &MeltBolt11Request) -> Result<(), Error> {
         self.localstore
             .remove_pending_proofs(melt_request.inputs.iter().map(|p| &p.secret).collect())
@@ -754,6 +838,7 @@ impl Mint {
 
     /// Process melt request marking [`Proofs`] as spent
     /// The melt request must be verifyed using [`Self::verify_melt_request`] before calling [`Self::process_melt_request`]
+    #[instrument(skip_all)]
     pub async fn process_melt_request(
         &self,
         melt_request: &MeltBolt11Request,
@@ -851,6 +936,7 @@ impl Mint {
     }
 
     /// Restore
+    #[instrument(skip_all)]
     pub async fn restore(&self, request: RestoreRequest) -> Result<RestoreResponse, Error> {
         let output_len = request.outputs.len();
 
@@ -883,6 +969,7 @@ impl Mint {
     }
 
     /// Ensure Keyset is loaded in mint
+    #[instrument(skip(self))]
     pub async fn ensure_keyset_loaded(&self, id: &Id) -> Result<(), Error> {
         let keysets = self.keysets.read().await;
         if keysets.contains_key(id) {
@@ -902,6 +989,7 @@ impl Mint {
     }
 
     /// Generate [`MintKeySet`] from [`MintKeySetInfo`]
+    #[instrument(skip_all)]
     pub fn generate_keyset(&self, keyset_info: MintKeySetInfo) -> MintKeySet {
         MintKeySet::generate_from_xpriv(&self.secp_ctx, self.xpriv, keyset_info)
     }
@@ -935,6 +1023,13 @@ pub struct MintKeySetInfo {
     pub derivation_path: DerivationPath,
     /// Max order of keyset
     pub max_order: u8,
+    /// Input Fee ppk
+    #[serde(default = "default_fee")]
+    pub input_fee_ppk: u64,
+}
+
+fn default_fee() -> u64 {
+    0
 }
 
 impl From<MintKeySetInfo> for KeySetInfo {
@@ -943,17 +1038,20 @@ impl From<MintKeySetInfo> for KeySetInfo {
             id: keyset_info.id,
             unit: keyset_info.unit,
             active: keyset_info.active,
+            input_fee_ppk: keyset_info.input_fee_ppk,
         }
     }
 }
 
 /// Generate new [`MintKeySetInfo`] from path
+#[instrument(skip_all)]
 fn create_new_keyset<C: secp256k1::Signing>(
     secp: &secp256k1::Secp256k1<C>,
     xpriv: ExtendedPrivKey,
     derivation_path: DerivationPath,
     unit: CurrencyUnit,
     max_order: u8,
+    input_fee_ppk: u64,
 ) -> (MintKeySet, MintKeySetInfo) {
     let keyset = MintKeySet::generate(
         secp,
@@ -971,6 +1069,7 @@ fn create_new_keyset<C: secp256k1::Signing>(
         valid_to: None,
         derivation_path,
         max_order,
+        input_fee_ppk,
     };
     (keyset, keyset_info)
 }

+ 29 - 7
crates/cdk/src/nuts/nut00/mod.rs

@@ -444,20 +444,30 @@ impl PartialOrd for PreMint {
 }
 
 /// Premint Secrets
-#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize)]
+#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
 pub struct PreMintSecrets {
     /// Secrets
     pub secrets: Vec<PreMint>,
+    /// Keyset Id
+    pub keyset_id: Id,
 }
 
 impl PreMintSecrets {
+    /// Create new [`PreMintSecrets`]
+    pub fn new(keyset_id: Id) -> Self {
+        Self {
+            secrets: Vec::new(),
+            keyset_id,
+        }
+    }
+
     /// Outputs for speceifed amount with random secret
     pub fn random(
         keyset_id: Id,
         amount: Amount,
         amount_split_target: &SplitTarget,
     ) -> Result<Self, Error> {
-        let amount_split = amount.split_targeted(amount_split_target);
+        let amount_split = amount.split_targeted(amount_split_target)?;
 
         let mut output = Vec::with_capacity(amount_split.len());
 
@@ -475,7 +485,10 @@ impl PreMintSecrets {
             });
         }
 
-        Ok(PreMintSecrets { secrets: output })
+        Ok(PreMintSecrets {
+            secrets: output,
+            keyset_id,
+        })
     }
 
     /// Outputs from pre defined secrets
@@ -499,7 +512,10 @@ impl PreMintSecrets {
             });
         }
 
-        Ok(PreMintSecrets { secrets: output })
+        Ok(PreMintSecrets {
+            secrets: output,
+            keyset_id,
+        })
     }
 
     /// Blank Outputs used for NUT-08 change
@@ -522,7 +538,10 @@ impl PreMintSecrets {
             })
         }
 
-        Ok(PreMintSecrets { secrets: output })
+        Ok(PreMintSecrets {
+            secrets: output,
+            keyset_id,
+        })
     }
 
     /// Outputs with specific spending conditions
@@ -532,7 +551,7 @@ impl PreMintSecrets {
         amount_split_target: &SplitTarget,
         conditions: &SpendingConditions,
     ) -> Result<Self, Error> {
-        let amount_split = amount.split_targeted(amount_split_target);
+        let amount_split = amount.split_targeted(amount_split_target)?;
 
         let mut output = Vec::with_capacity(amount_split.len());
 
@@ -552,7 +571,10 @@ impl PreMintSecrets {
             });
         }
 
-        Ok(PreMintSecrets { secrets: output })
+        Ok(PreMintSecrets {
+            secrets: output,
+            keyset_id,
+        })
     }
 
     /// Iterate over secrets

+ 6 - 18
crates/cdk/src/nuts/nut02.rs

@@ -230,15 +230,6 @@ pub struct KeysetResponse {
     pub keysets: Vec<KeySetInfo>,
 }
 
-impl KeysetResponse {
-    /// Create new [`KeysetResponse`]
-    pub fn new(keysets: Vec<KeySet>) -> Self {
-        Self {
-            keysets: keysets.into_iter().map(|keyset| keyset.into()).collect(),
-        }
-    }
-}
-
 /// Keyset
 #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
 pub struct KeySet {
@@ -271,16 +262,13 @@ pub struct KeySetInfo {
     /// Keyset state
     /// Mint will only sign from an active keyset
     pub active: bool,
+    /// Input Fee PPK
+    #[serde(default = "default_input_fee_ppk")]
+    pub input_fee_ppk: u64,
 }
 
-impl From<KeySet> for KeySetInfo {
-    fn from(keyset: KeySet) -> KeySetInfo {
-        Self {
-            id: keyset.id,
-            unit: keyset.unit,
-            active: false,
-        }
-    }
+fn default_input_fee_ppk() -> u64 {
+    0
 }
 
 /// MintKeyset
@@ -504,7 +492,7 @@ mod test {
 
     #[test]
     fn test_deserialization_of_keyset_response() {
-        let h = r#"{"keysets":[{"id":"009a1f293253e41e","unit":"sat","active":true},{"id":"eGnEWtdJ0PIM","unit":"sat","active":true},{"id":"003dfdf4e5e35487","unit":"sat","active":true},{"id":"0066ad1a4b6fc57c","unit":"sat","active":true},{"id":"00f7ca24d44c3e5e","unit":"sat","active":true},{"id":"001fcea2931f2d85","unit":"sat","active":true},{"id":"00d095959d940edb","unit":"sat","active":true},{"id":"000d7f730d657125","unit":"sat","active":true},{"id":"0007208d861d7295","unit":"sat","active":true},{"id":"00bfdf8889b719dd","unit":"sat","active":true},{"id":"00ca9b17da045f21","unit":"sat","active":true}]}"#;
+        let h = r#"{"keysets":[{"id":"009a1f293253e41e","unit":"sat","active":true, "input_fee_ppk": 100},{"id":"eGnEWtdJ0PIM","unit":"sat","active":true},{"id":"003dfdf4e5e35487","unit":"sat","active":true},{"id":"0066ad1a4b6fc57c","unit":"sat","active":true},{"id":"00f7ca24d44c3e5e","unit":"sat","active":true},{"id":"001fcea2931f2d85","unit":"sat","active":true},{"id":"00d095959d940edb","unit":"sat","active":true},{"id":"000d7f730d657125","unit":"sat","active":true},{"id":"0007208d861d7295","unit":"sat","active":true},{"id":"00bfdf8889b719dd","unit":"sat","active":true},{"id":"00ca9b17da045f21","unit":"sat","active":true}]}"#;
 
         let _keyset_response: KeysetResponse = serde_json::from_str(h).unwrap();
     }

+ 2 - 0
crates/cdk/src/nuts/nut03.rs

@@ -16,6 +16,8 @@ pub struct PreSwap {
     pub swap_request: SwapRequest,
     /// Amount to increment keyset counter by
     pub derived_secret_count: u32,
+    /// Fee amount
+    pub fee: Amount,
 }
 
 /// Split Request [NUT-06]

+ 8 - 7
crates/cdk/src/nuts/nut13.rs

@@ -3,6 +3,7 @@
 //! <https://github.com/cashubtc/nuts/blob/main/13.md>
 
 use bitcoin::bip32::{ChildNumber, DerivationPath, ExtendedPrivKey};
+use tracing::instrument;
 
 use super::nut00::{BlindedMessage, PreMint, PreMintSecrets};
 use super::nut01::SecretKey;
@@ -43,6 +44,7 @@ impl SecretKey {
 impl PreMintSecrets {
     /// Generate blinded messages from predetermined secrets and blindings
     /// factor
+    #[instrument(skip(xpriv))]
     pub fn from_xpriv(
         keyset_id: Id,
         counter: u32,
@@ -50,11 +52,11 @@ impl PreMintSecrets {
         amount: Amount,
         amount_split_target: &SplitTarget,
     ) -> Result<Self, Error> {
-        let mut pre_mint_secrets = PreMintSecrets::default();
+        let mut pre_mint_secrets = PreMintSecrets::new(keyset_id);
 
         let mut counter = counter;
 
-        for amount in amount.split_targeted(amount_split_target) {
+        for amount in amount.split_targeted(amount_split_target)? {
             let secret = Secret::from_xpriv(xpriv, keyset_id, counter)?;
             let blinding_factor = SecretKey::from_xpriv(xpriv, keyset_id, counter)?;
 
@@ -84,10 +86,10 @@ impl PreMintSecrets {
         amount: Amount,
     ) -> Result<Self, Error> {
         if amount <= Amount::ZERO {
-            return Ok(PreMintSecrets::default());
+            return Ok(PreMintSecrets::new(keyset_id));
         }
         let count = ((u64::from(amount) as f64).log2().ceil() as u64).max(1);
-        let mut pre_mint_secrets = PreMintSecrets::default();
+        let mut pre_mint_secrets = PreMintSecrets::new(keyset_id);
 
         let mut counter = counter;
 
@@ -115,15 +117,14 @@ impl PreMintSecrets {
         Ok(pre_mint_secrets)
     }
 
-    /// Generate blinded messages from predetermined secrets and blindings
-    /// factor
+    /// Generate blinded messages from predetermined secrets and blindings factor
     pub fn restore_batch(
         keyset_id: Id,
         xpriv: ExtendedPrivKey,
         start_count: u32,
         end_count: u32,
     ) -> Result<Self, Error> {
-        let mut pre_mint_secrets = PreMintSecrets::default();
+        let mut pre_mint_secrets = PreMintSecrets::new(keyset_id);
 
         for i in start_count..=end_count {
             let secret = Secret::from_xpriv(xpriv, keyset_id, i)?;

File diff suppressed because it is too large
+ 464 - 306
crates/cdk/src/wallet/mod.rs


+ 12 - 7
crates/cdk/src/wallet/multi_mint_wallet.rs

@@ -11,6 +11,7 @@ use serde::{Deserialize, Serialize};
 use tokio::sync::Mutex;
 use tracing::instrument;
 
+use super::types::SendKind;
 use super::Error;
 use crate::amount::SplitTarget;
 use crate::nuts::{CurrencyUnit, SecretKey, SpendingConditions, Token};
@@ -122,6 +123,8 @@ impl MultiMintWallet {
         amount: Amount,
         memo: Option<String>,
         conditions: Option<SpendingConditions>,
+        send_kind: SendKind,
+        include_fees: bool,
     ) -> Result<String, Error> {
         let wallet = self
             .get_wallet(wallet_key)
@@ -129,7 +132,14 @@ impl MultiMintWallet {
             .ok_or(Error::UnknownWallet(wallet_key.to_string()))?;
 
         wallet
-            .send(amount, memo, conditions, &SplitTarget::default())
+            .send(
+                amount,
+                memo,
+                conditions,
+                &SplitTarget::default(),
+                &send_kind,
+                include_fees,
+            )
             .await
     }
 
@@ -228,12 +238,7 @@ impl MultiMintWallet {
                 .ok_or(Error::UnknownWallet(wallet_key.to_string()))?;
 
             let amount = wallet
-                .receive_proofs(
-                    proofs,
-                    &SplitTarget::default(),
-                    p2pk_signing_keys,
-                    preimages,
-                )
+                .receive_proofs(proofs, SplitTarget::default(), p2pk_signing_keys, preimages)
                 .await?;
 
             amount_received += amount;

+ 14 - 0
crates/cdk/src/wallet/types.rs

@@ -44,3 +44,17 @@ pub struct MeltQuote {
     /// Payment preimage
     pub payment_preimage: Option<String>,
 }
+
+/// Send Kind
+#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Default, Serialize, Deserialize)]
+pub enum SendKind {
+    #[default]
+    /// Allow online swap before send if wallet does not have exact amount
+    OnlineExact,
+    /// Prefer offline send if difference is less then tolerance
+    OnlineTolerance(Amount),
+    /// Wallet cannot do an online swap and selectedp proof must be exactly send amount
+    OfflineExact,
+    /// Wallet must remain offline but can over pay if below tolerance
+    OfflineTolerance(Amount),
+}

Some files were not shown because too many files changed in this diff