1
0

26 Commits f3352335bf ... 11adbf9f36

Autor SHA1 Mensagem Data
  Cesar Rodas 11adbf9f36 Address feedback left on code review 1 semana atrás
  Cesar Rodas 071f2eeedc Fix typo 1 semana atrás
  Cesar Rodas a28b6ac58c Move cache TTL to each wallet 1 semana atrás
  Cesar Rodas 162b630aa7 Fixed race conditions 1 semana atrás
  Cesar Rodas 9fbf109ddf Add Mutex to serialize external requests to the mint 1 semana atrás
  Cesar Rodas d029bcc945 Add ability a TTL to the mint metadata cache 1 semana atrás
  Cesar Rodas 8122d38dbd Remove unused dep 2 semanas atrás
  Cesar Rodas 701ae03406 refactor: simplify KeyManager by removing background worker and making it pull-based 2 semanas atrás
  Cesar Rodas 94bb6b6859 refactor(wallet): introduce per-mint KeyManager with background worker 3 semanas atrás
  Cesar Rodas 0e577b3662 Refactor KeyManager for WASM compatibility and improved caching 3 semanas atrás
  Cesar Rodas 266edb7177 Introduce KeyManager for the wallet 3 semanas atrás
  asmo d002481eb6 fix: check the removed_ys argument before creating the delete query (#1198) 1 semana atrás
  tsk f989fb784d chore: update stable rust to 1.91.1 (#1265) 1 semana atrás
  github-actions[bot] 0f1f2fe5a0 chore: add weekly meeting agenda for 2025-11-12 (#1261) 1 semana atrás
  thesimplekid 5af6976da2 chore: rust version check open issue 1 semana atrás
  tsk 50cf1d83b9 chore: rust version workflow (#1262) 1 semana atrás
  tsk 9354c2c698 feat(ci): add nightly rustfmt automation with flexible formatting policy (#1260) 1 semana atrás
  tsk 4feed9f6c2 mint async melt (#1258) 1 semana atrás
  Cesar Rodas f3352335bf Fixed race conditions 1 semana atrás
  Cesar Rodas c43b28091c Add Mutex to serialize external requests to the mint 1 semana atrás
  Cesar Rodas 18e04958c6 Add ability a TTL to the mint metadata cache 1 semana atrás
  Cesar Rodas 9c18897afa Remove unused dep 2 semanas atrás
  Cesar Rodas 4509fe6f54 refactor: simplify KeyManager by removing background worker and making it pull-based 2 semanas atrás
  Cesar Rodas abfc821e52 refactor(wallet): introduce per-mint KeyManager with background worker 3 semanas atrás
  Cesar Rodas d3da875139 Refactor KeyManager for WASM compatibility and improved caching 3 semanas atrás
  Cesar Rodas c81df88cd4 Introduce KeyManager for the wallet 3 semanas atrás

+ 3 - 8
.github/workflows/ci.yml

@@ -34,16 +34,11 @@ jobs:
       - name: Rust Cache
         uses: Swatinem/rust-cache@v2
         with:
-          shared-key: "nightly-${{ steps.flake-hash.outputs.hash }}"
+          shared-key: "stable-${{ steps.flake-hash.outputs.hash }}"
       - name: Cargo fmt
-        run: |
-          nix develop -i -L .#nightly --command bash -c '
-            # Force use of Nix-provided rustfmt
-            export RUSTFMT=$(command -v rustfmt)
-            cargo fmt --check
-          '
+        run: nix develop -i -L .#stable --command cargo fmt --check
       - name: typos
-        run: nix develop -i -L .#nightly --command typos
+        run: nix develop -i -L .#stable --command typos
 
   examples:
     name: "Run examples"

+ 57 - 0
.github/workflows/nightly-rustfmt.yml

@@ -0,0 +1,57 @@
+name: Nightly rustfmt
+on:
+  schedule:
+    - cron: "0 0 * * *" # runs daily at 00:00 UTC
+  workflow_dispatch: # allows manual triggering
+
+permissions: {}
+
+jobs:
+  format:
+    name: Nightly rustfmt
+    runs-on: ubuntu-latest
+    permissions:
+      contents: write
+      pull-requests: write
+    steps:
+      - uses: actions/checkout@v4
+        with:
+          ref: main
+          persist-credentials: false
+          token: ${{ secrets.BACKPORT_TOKEN }}
+      - name: Get flake hash
+        id: flake-hash
+        run: echo "hash=$(sha256sum flake.lock | cut -d' ' -f1 | cut -c1-8)" >> $GITHUB_OUTPUT
+      - name: Install Nix
+        uses: DeterminateSystems/nix-installer-action@v17
+      - name: Nix Cache
+        uses: DeterminateSystems/magic-nix-cache-action@main
+        with:
+          diagnostic-endpoint: ""
+          use-flakehub: false
+      - name: Rust Cache
+        uses: Swatinem/rust-cache@v2
+        with:
+          shared-key: "nightly-${{ steps.flake-hash.outputs.hash }}"
+      - name: Run Nightly rustfmt
+        run: |
+          nix develop -i -L .#nightly --command bash -c '
+            # Force use of Nix-provided rustfmt
+            export RUSTFMT=$(command -v rustfmt)
+            cargo +nightly fmt
+          '
+          # Manually remove trailing whitespace
+          git ls-files -- '*.rs' -z | xargs -0 sed -E -i'' -e 's/[[:space:]]+$//'
+      - name: Get the current date
+        run: echo "date=$(date +'%Y-%m-%d')" >> $GITHUB_ENV
+      - name: Create Pull Request
+        uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
+        with:
+          token: ${{ secrets.BACKPORT_TOKEN }}
+          author: Fmt Bot <bot@cashudevkit.org>
+          title: Automated nightly rustfmt (${{ env.date }})
+          body: |
+            Automated nightly `rustfmt` changes by [create-pull-request](https://github.com/peter-evans/create-pull-request) GitHub action
+          commit-message: ${{ env.date }} automated rustfmt nightly
+          labels: rustfmt
+          branch: automated-rustfmt-${{ env.date }}

+ 77 - 0
.github/workflows/update-rust-version.yml

@@ -0,0 +1,77 @@
+name: Update Rust Version
+
+on:
+  schedule:
+    # Run weekly on Monday at 9 AM UTC
+    - cron: '0 9 * * 1'
+  workflow_dispatch: # Allow manual triggering
+
+permissions: {}
+
+jobs:
+  check-rust-version:
+    name: Check and update Rust version
+    runs-on: ubuntu-latest
+    permissions:
+      issues: write
+
+    steps:
+      - name: Checkout repository
+        uses: actions/checkout@v4
+
+      - name: Get latest stable Rust version
+        id: latest-rust
+        run: |
+          # Fetch the latest stable Rust version from GitHub releases API
+          LATEST_VERSION=$(curl -s https://api.github.com/repos/rust-lang/rust/releases | jq -r '[.[] | select(.prerelease == false and .draft == false)][0].tag_name')
+          echo "version=$LATEST_VERSION" >> $GITHUB_OUTPUT
+          echo "Latest stable Rust version: $LATEST_VERSION"
+
+      - name: Get current Rust version
+        id: current-rust
+        run: |
+          # Extract current version from rust-toolchain.toml
+          CURRENT_VERSION=$(grep '^channel=' rust-toolchain.toml | sed 's/channel="\(.*\)"/\1/')
+          echo "version=$CURRENT_VERSION" >> $GITHUB_OUTPUT
+          echo "Current Rust version: $CURRENT_VERSION"
+
+      - name: Compare versions
+        id: compare
+        run: |
+          if [ "${{ steps.latest-rust.outputs.version }}" != "${{ steps.current-rust.outputs.version }}" ]; then
+            echo "needs_update=true" >> $GITHUB_OUTPUT
+            echo "Rust version needs update: ${{ steps.current-rust.outputs.version }} -> ${{ steps.latest-rust.outputs.version }}"
+          else
+            echo "needs_update=false" >> $GITHUB_OUTPUT
+            echo "Rust version is up to date"
+          fi
+
+      - name: Create Issue
+        if: steps.compare.outputs.needs_update == 'true'
+        env:
+          GH_TOKEN: ${{ github.token }}
+        run: |
+          gh issue create \
+            --title "Update Rust to ${{ steps.latest-rust.outputs.version }}" \
+            --label "rust-version" \
+            --assignee thesimplekid \
+            --body "$(cat <<'EOF'
+          New Rust version **${{ steps.latest-rust.outputs.version }}** is available (currently on **${{ steps.current-rust.outputs.version }}**).
+
+          ## Files to update
+          - \`rust-toolchain.toml\` - Update channel to \`${{ steps.latest-rust.outputs.version }}\`
+          - \`flake.nix\` - Update stable_toolchain to \`pkgs.rust-bin.stable."${{ steps.latest-rust.outputs.version }}".default\`
+          - Run \`nix flake update rust-overlay\` to update \`flake.lock\`
+
+          ## Release Notes
+          Check the [Rust release notes](https://github.com/rust-lang/rust/blob/master/RELEASES.md) for details on what's new in this version.
+
+          ---
+          🤖 Automated issue created by update-rust-version workflow
+          EOF
+          )"
+
+      - name: No update needed
+        if: steps.compare.outputs.needs_update == 'false'
+        run: |
+          echo "✓ Rust version is already up to date (${{ steps.current-rust.outputs.version }})"

+ 37 - 0
DEVELOPMENT.md

@@ -175,6 +175,43 @@ NOTE: if this command fails on macos change the nix channel to unstable (in the
 just format
 ```
 
+## Code Formatting
+
+CDK uses a flexible rustfmt policy to balance code quality with developer experience:
+
+### Formatting Requirements for PRs
+Pull requests can be formatted with **either stable or nightly** rustfmt - both are accepted:
+
+- **Stable rustfmt:** Standard Rust formatting (less strict)
+- **Nightly rustfmt:** More strict formatting with additional rules
+
+**Why both are accepted:**
+- We prefer nightly rustfmt's stricter formatting
+- We don't want to force contributors to install nightly Rust
+- This reduces friction for developers using stable toolchains
+
+```bash
+# Format with stable (default)
+just format
+
+# Format with nightly (if you have it installed)
+cargo +nightly fmt
+```
+
+The CI will check your PR with stable rustfmt, so as long as your code passes stable formatting, your PR will pass CI.
+
+### Automated Nightly Formatting
+To keep the codebase consistently formatted with nightly rustfmt over time:
+
+- **Daily Check:** Every night at midnight UTC, a GitHub Action runs nightly rustfmt on the `main` branch
+- **Automated PRs:** If nightly rustfmt produces formatting changes, a PR is automatically created with:
+  - Title: `Automated nightly rustfmt (YYYY-MM-DD)`
+  - Label: `rustfmt`
+  - Author: `Fmt Bot <bot@cashudevkit.org>`
+- **Review Process:** These automated PRs are reviewed and merged to keep the codebase aligned with nightly formatting
+
+This approach ensures the codebase gradually adopts nightly formatting improvements without blocking contributors who use stable Rust.
+
 
 ### Running Clippy
 ```bash

+ 96 - 3
crates/cdk-axum/src/router_handlers.rs

@@ -1,6 +1,7 @@
 use anyhow::Result;
 use axum::extract::ws::WebSocketUpgrade;
-use axum::extract::{Json, Path, State};
+use axum::extract::{FromRequestParts, Json, Path, State};
+use axum::http::request::Parts;
 use axum::http::StatusCode;
 use axum::response::{IntoResponse, Response};
 use cdk::error::{ErrorCode, ErrorResponse};
@@ -22,6 +23,46 @@ use crate::auth::AuthHeader;
 use crate::ws::main_websocket;
 use crate::MintState;
 
+const PREFER_HEADER_KEY: &str = "Prefer";
+
+/// Header extractor for the Prefer header
+///
+/// This extractor checks for the `Prefer: respond-async` header
+/// to determine if the client wants asynchronous processing
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub struct PreferHeader {
+    pub respond_async: bool,
+}
+
+impl<S> FromRequestParts<S> for PreferHeader
+where
+    S: Send + Sync,
+{
+    type Rejection = (StatusCode, String);
+
+    async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
+        // Check for Prefer header
+        if let Some(prefer_value) = parts.headers.get(PREFER_HEADER_KEY) {
+            let value = prefer_value.to_str().map_err(|_| {
+                (
+                    StatusCode::BAD_REQUEST,
+                    "Invalid Prefer header value".to_string(),
+                )
+            })?;
+
+            // Check if it contains "respond-async"
+            let respond_async = value.to_lowercase().contains("respond-async");
+
+            return Ok(PreferHeader { respond_async });
+        }
+
+        // No Prefer header found - default to synchronous processing
+        Ok(PreferHeader {
+            respond_async: false,
+        })
+    }
+}
+
 /// Macro to add cache to endpoint
 #[macro_export]
 macro_rules! post_cache_wrapper {
@@ -61,9 +102,50 @@ macro_rules! post_cache_wrapper {
     };
 }
 
+/// Macro to add cache to endpoint with prefer header support (for async operations)
+#[macro_export]
+macro_rules! post_cache_wrapper_with_prefer {
+    ($handler:ident, $request_type:ty, $response_type:ty) => {
+        paste! {
+            /// Cache wrapper function for $handler with PreferHeader support:
+            /// Wrap $handler into a function that caches responses using the request as key
+            pub async fn [<cache_ $handler>](
+                #[cfg(feature = "auth")] auth: AuthHeader,
+                prefer: PreferHeader,
+                state: State<MintState>,
+                payload: Json<$request_type>
+            ) -> Result<Json<$response_type>, Response> {
+                use std::ops::Deref;
+
+                let json_extracted_payload = payload.deref();
+                let State(mint_state) = state.clone();
+                let cache_key = match mint_state.cache.calculate_key(&json_extracted_payload) {
+                    Some(key) => key,
+                    None => {
+                        // Could not calculate key, just return the handler result
+                        #[cfg(feature = "auth")]
+                        return $handler(auth, prefer, state, payload).await;
+                        #[cfg(not(feature = "auth"))]
+                        return $handler(prefer, state, payload).await;
+                    }
+                };
+                if let Some(cached_response) = mint_state.cache.get::<$response_type>(&cache_key).await {
+                    return Ok(Json(cached_response));
+                }
+                #[cfg(feature = "auth")]
+                let response = $handler(auth, prefer, state, payload).await?;
+                #[cfg(not(feature = "auth"))]
+                let response = $handler(prefer, state, payload).await?;
+                mint_state.cache.set(cache_key, &response.deref()).await;
+                Ok(response)
+            }
+        }
+    };
+}
+
 post_cache_wrapper!(post_swap, SwapRequest, SwapResponse);
 post_cache_wrapper!(post_mint_bolt11, MintRequest<QuoteId>, MintResponse);
-post_cache_wrapper!(
+post_cache_wrapper_with_prefer!(
     post_melt_bolt11,
     MeltRequest<QuoteId>,
     MeltQuoteBolt11Response<QuoteId>
@@ -382,6 +464,7 @@ pub(crate) async fn get_check_melt_bolt11_quote(
 #[instrument(skip_all)]
 pub(crate) async fn post_melt_bolt11(
     #[cfg(feature = "auth")] auth: AuthHeader,
+    prefer: PreferHeader,
     State(state): State<MintState>,
     Json(payload): Json<MeltRequest<QuoteId>>,
 ) -> Result<Json<MeltQuoteBolt11Response<QuoteId>>, Response> {
@@ -397,7 +480,17 @@ pub(crate) async fn post_melt_bolt11(
             .map_err(into_response)?;
     }
 
-    let res = state.mint.melt(&payload).await.map_err(into_response)?;
+    let res = if prefer.respond_async {
+        // Asynchronous processing - return immediately after setup
+        state
+            .mint
+            .melt_async(&payload)
+            .await
+            .map_err(into_response)?
+    } else {
+        // Synchronous processing - wait for completion
+        state.mint.melt(&payload).await.map_err(into_response)?
+    };
 
     Ok(Json(res))
 }

+ 4 - 4
crates/cdk-ffi/src/types/mint.rs

@@ -739,12 +739,12 @@ mod tests {
         let ffi_nuts: Nuts = cdk_nuts.clone().into();
 
         // Verify NUT04 settings
-        assert_eq!(ffi_nuts.nut04.disabled, false);
+        assert!(!ffi_nuts.nut04.disabled);
         assert_eq!(ffi_nuts.nut04.methods.len(), 1);
         assert_eq!(ffi_nuts.nut04.methods[0].description, Some(true));
 
         // Verify NUT05 settings
-        assert_eq!(ffi_nuts.nut05.disabled, false);
+        assert!(!ffi_nuts.nut05.disabled);
         assert_eq!(ffi_nuts.nut05.methods.len(), 1);
         assert_eq!(ffi_nuts.nut05.methods[0].amountless, Some(true));
 
@@ -969,8 +969,8 @@ mod tests {
         let ffi_nuts: Nuts = cdk_nuts.into();
 
         // Should have collected multiple units
-        assert!(ffi_nuts.mint_units.len() >= 1);
-        assert!(ffi_nuts.melt_units.len() >= 1);
+        assert!(!ffi_nuts.mint_units.is_empty());
+        assert!(!ffi_nuts.melt_units.is_empty());
     }
 
     #[test]

+ 146 - 0
crates/cdk-integration-tests/tests/async_melt.rs

@@ -0,0 +1,146 @@
+//! Async Melt Integration Tests
+//!
+//! This file contains tests for async melt functionality using the Prefer: respond-async header.
+//!
+//! Test Scenarios:
+//! - Async melt returns PENDING state immediately
+//! - Synchronous melt still works correctly (backward compatibility)
+//! - Background task completion
+//! - Quote polling pattern
+
+use std::sync::Arc;
+
+use bip39::Mnemonic;
+use cdk::amount::SplitTarget;
+use cdk::nuts::{CurrencyUnit, MeltQuoteState};
+use cdk::wallet::Wallet;
+use cdk::StreamExt;
+use cdk_fake_wallet::{create_fake_invoice, FakeInvoiceDescription};
+use cdk_sqlite::wallet::memory;
+
+const MINT_URL: &str = "http://127.0.0.1:8086";
+
+/// Test: Async melt returns PENDING state immediately
+///
+/// This test validates that when calling melt with Prefer: respond-async header,
+/// the mint returns immediately with PENDING state.
+#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
+async fn test_async_melt_returns_pending() {
+    let wallet = Wallet::new(
+        MINT_URL,
+        CurrencyUnit::Sat,
+        Arc::new(memory::empty().await.unwrap()),
+        Mnemonic::generate(12).unwrap().to_seed_normalized(""),
+        None,
+    )
+    .expect("failed to create new wallet");
+
+    // Step 1: Mint some tokens
+    let mint_quote = wallet.mint_quote(100.into(), None).await.unwrap();
+    let mut proof_streams = wallet.proof_stream(mint_quote.clone(), SplitTarget::default(), None);
+
+    let _proofs = proof_streams
+        .next()
+        .await
+        .expect("payment")
+        .expect("no error");
+
+    let balance = wallet.total_balance().await.unwrap();
+    assert_eq!(balance, 100.into());
+
+    // Step 2: Create a melt quote
+    let fake_invoice_description = FakeInvoiceDescription {
+        pay_invoice_state: MeltQuoteState::Paid,
+        check_payment_state: MeltQuoteState::Paid,
+        pay_err: false,
+        check_err: false,
+    };
+
+    let invoice = create_fake_invoice(
+        50_000, // 50 sats in millisats
+        serde_json::to_string(&fake_invoice_description).unwrap(),
+    );
+
+    let melt_quote = wallet.melt_quote(invoice.to_string(), None).await.unwrap();
+
+    // Step 3: Call melt (wallet handles proof selection internally)
+    let start_time = std::time::Instant::now();
+
+    // This should complete and return the final state
+    // TODO: Add Prefer: respond-async header support to wallet.melt()
+    let melt_response = wallet.melt(&melt_quote.id).await.unwrap();
+
+    let elapsed = start_time.elapsed();
+
+    // For now, this is synchronous, so it will take longer
+    println!("Melt took {:?}", elapsed);
+
+    // Step 4: Verify the melt completed successfully
+    assert_eq!(
+        melt_response.state,
+        MeltQuoteState::Paid,
+        "Melt should complete with PAID state"
+    );
+}
+
+/// Test: Synchronous melt still works correctly
+///
+/// This test ensures backward compatibility - melt without Prefer header
+/// still blocks until completion and returns the final state.
+#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
+async fn test_sync_melt_completes_fully() {
+    let wallet = Wallet::new(
+        MINT_URL,
+        CurrencyUnit::Sat,
+        Arc::new(memory::empty().await.unwrap()),
+        Mnemonic::generate(12).unwrap().to_seed_normalized(""),
+        None,
+    )
+    .expect("failed to create new wallet");
+
+    // Step 1: Mint some tokens
+    let mint_quote = wallet.mint_quote(100.into(), None).await.unwrap();
+    let mut proof_streams = wallet.proof_stream(mint_quote.clone(), SplitTarget::default(), None);
+
+    let _proofs = proof_streams
+        .next()
+        .await
+        .expect("payment")
+        .expect("no error");
+
+    let balance = wallet.total_balance().await.unwrap();
+    assert_eq!(balance, 100.into());
+
+    // Step 2: Create a melt quote
+    let fake_invoice_description = FakeInvoiceDescription {
+        pay_invoice_state: MeltQuoteState::Paid,
+        check_payment_state: MeltQuoteState::Paid,
+        pay_err: false,
+        check_err: false,
+    };
+
+    let invoice = create_fake_invoice(
+        50_000, // 50 sats in millisats
+        serde_json::to_string(&fake_invoice_description).unwrap(),
+    );
+
+    let melt_quote = wallet.melt_quote(invoice.to_string(), None).await.unwrap();
+
+    // Step 3: Call synchronous melt
+    let melt_response = wallet.melt(&melt_quote.id).await.unwrap();
+
+    // Step 5: Verify response shows payment completed
+    assert_eq!(
+        melt_response.state,
+        MeltQuoteState::Paid,
+        "Synchronous melt should return PAID state"
+    );
+
+    // Step 6: Verify the quote is PAID in the mint
+    let quote_state = wallet.melt_quote_status(&melt_quote.id).await.unwrap();
+    assert_eq!(
+        quote_state.state,
+        MeltQuoteState::Paid,
+        "Quote should be PAID"
+    );
+}

+ 1 - 1
crates/cdk-integration-tests/tests/bolt12.rs

@@ -385,7 +385,7 @@ async fn test_regtest_bolt12_mint_extra() -> Result<()> {
         Err(err) => match err {
             cdk::Error::TransactionUnbalanced(_, _, _) => (),
             err => {
-                bail!("Wrong mint error returned: {}", err.to_string());
+                bail!("Wrong mint error returned: {}", err);
             }
         },
         Ok(_) => {

+ 1 - 1
crates/cdk-integration-tests/tests/test_swap_flow.rs

@@ -861,7 +861,7 @@ async fn test_swap_state_transition_notifications() {
             cashu::NotificationPayload::ProofState(cashu::ProofState { y, state, .. }) => {
                 state_transitions
                     .entry(y.to_string())
-                    .or_insert_with(Vec::new)
+                    .or_default()
                     .push(state);
             }
             _ => panic!("Unexpected notification type"),

+ 9 - 8
crates/cdk-sql-common/src/wallet/mod.rs

@@ -779,14 +779,15 @@ ON CONFLICT(id) DO UPDATE SET
             )
             .execute(&tx).await?;
         }
-
-        query(r#"DELETE FROM proof WHERE y IN (:ys)"#)?
-            .bind_vec(
-                "ys",
-                removed_ys.iter().map(|y| y.to_bytes().to_vec()).collect(),
-            )
-            .execute(&tx)
-            .await?;
+        if !removed_ys.is_empty() {
+            query(r#"DELETE FROM proof WHERE y IN (:ys)"#)?
+                .bind_vec(
+                    "ys",
+                    removed_ys.iter().map(|y| y.to_bytes().to_vec()).collect(),
+                )
+                .execute(&tx)
+                .await?;
+        }
 
         tx.commit().await?;
 

+ 98 - 0
crates/cdk/src/mint/melt.rs

@@ -443,4 +443,102 @@ impl Mint {
         // Step 4: Finalize (TX2 - marks spent, issues change)
         payment_saga.finalize().await
     }
+
+    /// Process melt asynchronously - returns immediately after setup with PENDING state
+    ///
+    /// This method is called when the client includes the `Prefer: respond-async` header.
+    /// It performs the setup phase (TX1) to validate and reserve proofs, then spawns a
+    /// background task to complete the payment and finalization phases.
+    pub async fn melt_async(
+        &self,
+        melt_request: &MeltRequest<QuoteId>,
+    ) -> Result<MeltQuoteBolt11Response<QuoteId>, Error> {
+        let verification = self.verify_inputs(melt_request.inputs()).await?;
+
+        let init_saga = MeltSaga::new(
+            std::sync::Arc::new(self.clone()),
+            self.localstore.clone(),
+            std::sync::Arc::clone(&self.pubsub_manager),
+        );
+
+        let setup_saga = init_saga.setup_melt(melt_request, verification).await?;
+
+        // Get the quote to return with PENDING state
+        let quote_id = melt_request.quote().clone();
+        let quote = self
+            .localstore
+            .get_melt_quote(&quote_id)
+            .await?
+            .ok_or(Error::UnknownQuote)?;
+
+        // Spawn background task to complete the melt operation
+        let melt_request_clone = melt_request.clone();
+        let quote_id_clone = quote_id.clone();
+        tokio::spawn(async move {
+            tracing::debug!(
+                "Starting background melt completion for quote: {}",
+                quote_id_clone
+            );
+
+            // Step 2: Attempt internal settlement
+            match setup_saga
+                .attempt_internal_settlement(&melt_request_clone)
+                .await
+            {
+                Ok((setup_saga, settlement)) => {
+                    // Step 3: Make payment
+                    match setup_saga.make_payment(settlement).await {
+                        Ok(payment_saga) => {
+                            // Step 4: Finalize
+                            match payment_saga.finalize().await {
+                                Ok(_) => {
+                                    tracing::info!(
+                                        "Background melt completed successfully for quote: {}",
+                                        quote_id_clone
+                                    );
+                                }
+                                Err(e) => {
+                                    tracing::error!(
+                                        "Failed to finalize melt for quote {}: {}",
+                                        quote_id_clone,
+                                        e
+                                    );
+                                }
+                            }
+                        }
+                        Err(e) => {
+                            tracing::error!(
+                                "Failed to make payment for quote {}: {}",
+                                quote_id_clone,
+                                e
+                            );
+                        }
+                    }
+                }
+                Err(e) => {
+                    tracing::error!(
+                        "Failed internal settlement for quote {}: {}",
+                        quote_id_clone,
+                        e
+                    );
+                }
+            }
+        });
+
+        debug_assert!(quote.state == MeltQuoteState::Pending);
+
+        // Return immediately with the quote in PENDING state
+        Ok(MeltQuoteBolt11Response {
+            quote: quote_id,
+            amount: quote.amount,
+            fee_reserve: quote.fee_reserve,
+            state: quote.state,
+            paid: Some(false),
+            expiry: quote.expiry,
+            payment_preimage: None,
+            change: None,
+            request: Some(quote.request.to_string()),
+            unit: Some(quote.unit),
+        })
+    }
 }

+ 0 - 2
crates/cdk/src/mint/melt/melt_saga/tests.rs

@@ -8,8 +8,6 @@
 //! - Concurrent operations
 //! - Failure handling
 
-#![cfg(test)]
-
 use cdk_common::mint::{MeltSagaState, OperationKind, Saga};
 use cdk_common::nuts::MeltQuoteState;
 use cdk_common::{Amount, ProofsMethods, State};

+ 7 - 8
crates/cdk/src/mint/swap/swap_saga/tests.rs

@@ -1,4 +1,3 @@
-#![cfg(test)]
 //! Unit tests for the swap saga implementation
 //!
 //! These tests verify the swap saga pattern using in-memory mints and databases,
@@ -933,7 +932,7 @@ async fn test_swap_saga_concurrent_swaps() {
     let task1 = tokio::spawn(async move {
         let db = mint1.localstore();
         let pubsub = mint1.pubsub_manager();
-        let saga = SwapSaga::new(&*mint1, db, pubsub);
+        let saga = SwapSaga::new(&mint1, db, pubsub);
 
         let saga = saga
             .setup_swap(&proofs1, &output_blinded_messages_1, None, verification1)
@@ -945,7 +944,7 @@ async fn test_swap_saga_concurrent_swaps() {
     let task2 = tokio::spawn(async move {
         let db = mint2.localstore();
         let pubsub = mint2.pubsub_manager();
-        let saga = SwapSaga::new(&*mint2, db, pubsub);
+        let saga = SwapSaga::new(&mint2, db, pubsub);
 
         let saga = saga
             .setup_swap(&proofs2, &output_blinded_messages_2, None, verification2)
@@ -957,7 +956,7 @@ async fn test_swap_saga_concurrent_swaps() {
     let task3 = tokio::spawn(async move {
         let db = mint3.localstore();
         let pubsub = mint3.pubsub_manager();
-        let saga = SwapSaga::new(&*mint3, db, pubsub);
+        let saga = SwapSaga::new(&mint3, db, pubsub);
 
         let saga = saga
             .setup_swap(&proofs3, &output_blinded_messages_3, None, verification3)
@@ -1976,19 +1975,19 @@ async fn test_operation_id_uniqueness_and_tracking() {
     {
         let pubsub = mint.pubsub_manager();
 
-        let saga_1 = SwapSaga::new(&*mint, db.clone(), pubsub.clone());
+        let saga_1 = SwapSaga::new(&mint, db.clone(), pubsub.clone());
         let _saga_1 = saga_1
             .setup_swap(&proofs_1, &outputs_1, None, verification_1)
             .await
             .expect("Swap 1 setup should succeed");
 
-        let saga_2 = SwapSaga::new(&*mint, db.clone(), pubsub.clone());
+        let saga_2 = SwapSaga::new(&mint, db.clone(), pubsub.clone());
         let _saga_2 = saga_2
             .setup_swap(&proofs_2, &outputs_2, None, verification_2)
             .await
             .expect("Swap 2 setup should succeed");
 
-        let saga_3 = SwapSaga::new(&*mint, db.clone(), pubsub.clone());
+        let saga_3 = SwapSaga::new(&mint, db.clone(), pubsub.clone());
         let _saga_3 = saga_3
             .setup_swap(&proofs_3, &outputs_3, None, verification_3)
             .await
@@ -2033,7 +2032,7 @@ async fn test_operation_id_uniqueness_and_tracking() {
     let verification = create_verification(amount);
 
     let pubsub = mint.pubsub_manager();
-    let new_saga = SwapSaga::new(&*mint, db, pubsub);
+    let new_saga = SwapSaga::new(&mint, db, pubsub);
 
     let result = new_saga
         .setup_swap(&proofs_1, &new_outputs_1, None, verification)

+ 2 - 9
crates/cdk/src/test_helpers/mint.rs

@@ -137,11 +137,7 @@ pub async fn mint_test_proofs(mint: &Mint, amount: Amount) -> Result<Proofs, Err
         sleep(Duration::from_secs(1)).await;
     }
 
-    let keysets = mint
-        .get_active_keysets()
-        .get(&CurrencyUnit::Sat)
-        .unwrap()
-        .clone();
+    let keysets = *mint.get_active_keysets().get(&CurrencyUnit::Sat).unwrap();
 
     let keys = mint
         .keyset_pubkeys(&keysets)?
@@ -151,10 +147,7 @@ pub async fn mint_test_proofs(mint: &Mint, amount: Amount) -> Result<Proofs, Err
         .keys
         .clone();
 
-    let fees: (u64, Vec<u64>) = (
-        0,
-        keys.iter().map(|a| a.0.to_u64()).collect::<Vec<_>>().into(),
-    );
+    let fees: (u64, Vec<u64>) = (0, keys.iter().map(|a| a.0.to_u64()).collect::<Vec<_>>());
 
     let premint_secrets =
         PreMintSecrets::random(keysets, amount, &SplitTarget::None, &fees.into()).unwrap();

+ 0 - 1
crates/cdk/src/test_helpers/mod.rs

@@ -1,4 +1,3 @@
-#![cfg(test)]
 //! Test helper utilities for CDK unit tests
 //!
 //! This module provides shared test utilities for creating test mints, wallets,

+ 15 - 2
crates/cdk/src/wallet/builder.rs

@@ -1,7 +1,9 @@
 use std::collections::HashMap;
 use std::sync::Arc;
+use std::time::Duration;
 
 use cdk_common::database;
+use cdk_common::parking_lot::Mutex;
 #[cfg(feature = "auth")]
 use cdk_common::AuthToken;
 #[cfg(feature = "auth")]
@@ -27,6 +29,7 @@ pub struct WalletBuilder {
     seed: Option<[u8; 64]>,
     use_http_subscription: bool,
     client: Option<Arc<dyn MintConnector + Send + Sync>>,
+    metadata_cache_ttl: Option<Duration>,
     metadata_cache: Option<Arc<MintMetadataCache>>,
     metadata_caches: HashMap<MintUrl, Arc<MintMetadataCache>>,
 }
@@ -42,6 +45,7 @@ impl Default for WalletBuilder {
             auth_wallet: None,
             seed: None,
             client: None,
+            metadata_cache_ttl: None,
             use_http_subscription: false,
             metadata_cache: None,
             metadata_caches: HashMap::new(),
@@ -61,6 +65,12 @@ impl WalletBuilder {
         self
     }
 
+    /// Set metadata_cache_ttl
+    pub fn set_metadata_cache_ttl(mut self, metadata_cache_ttl: Option<Duration>) -> Self {
+        self.metadata_cache_ttl = metadata_cache_ttl;
+        self
+    }
+
     /// If WS is preferred (with fallback to HTTP is it is not supported by the mint) for the wallet
     /// subscriptions to mint events
     pub fn prefer_ws_subscription(mut self) -> Self {
@@ -154,7 +164,7 @@ impl WalletBuilder {
                 cache.clone()
             } else {
                 // Create a new one
-                Arc::new(MintMetadataCache::new(mint_url.clone(), None))
+                Arc::new(MintMetadataCache::new(mint_url.clone()))
             }
         });
 
@@ -201,13 +211,15 @@ impl WalletBuilder {
             }
         };
 
+        let metadata_cache_ttl = self.metadata_cache_ttl;
+
         let metadata_cache = self.metadata_cache.unwrap_or_else(|| {
             // Check if we already have a cache for this mint in the HashMap
             if let Some(cache) = self.metadata_caches.get(&mint_url) {
                 cache.clone()
             } else {
                 // Create a new one
-                Arc::new(MintMetadataCache::new(mint_url.clone(), None))
+                Arc::new(MintMetadataCache::new(mint_url.clone()))
             }
         });
 
@@ -216,6 +228,7 @@ impl WalletBuilder {
             unit,
             localstore,
             metadata_cache,
+            metadata_cache_ttl: Arc::new(Mutex::new(metadata_cache_ttl)),
             target_proof_count: self.target_proof_count.unwrap_or(3),
             #[cfg(feature = "auth")]
             auth_wallet: Arc::new(RwLock::new(self.auth_wallet)),

+ 20 - 5
crates/cdk/src/wallet/keysets.rs

@@ -15,7 +15,10 @@ impl Wallet {
     #[instrument(skip(self))]
     pub async fn load_keyset_keys(&self, keyset_id: Id) -> Result<Keys, Error> {
         self.metadata_cache
-            .load(&self.localstore, &self.client)
+            .load(&self.localstore, &self.client, {
+                let ttl = self.metadata_cache_ttl.lock();
+                *ttl
+            })
             .await?
             .keys
             .get(&keyset_id)
@@ -38,7 +41,10 @@ impl Wallet {
     pub async fn get_mint_keysets(&self) -> Result<Vec<KeySetInfo>, Error> {
         let keysets = self
             .metadata_cache
-            .load(&self.localstore, &self.client)
+            .load(&self.localstore, &self.client, {
+                let ttl = self.metadata_cache_ttl.lock();
+                *ttl
+            })
             .await?
             .keysets
             .iter()
@@ -69,7 +75,10 @@ impl Wallet {
 
         let keysets = self
             .metadata_cache
-            .load_from_mint(&self.localstore, &self.client)
+            .load_from_mint(&self.localstore, &self.client, {
+                let ttl = self.metadata_cache_ttl.lock();
+                *ttl
+            })
             .await?
             .keysets
             .iter()
@@ -111,7 +120,10 @@ impl Wallet {
     #[instrument(skip(self))]
     pub async fn get_active_keyset(&self) -> Result<KeySetInfo, Error> {
         self.metadata_cache
-            .load(&self.localstore, &self.client)
+            .load(&self.localstore, &self.client, {
+                let ttl = self.metadata_cache_ttl.lock();
+                *ttl
+            })
             .await?
             .active_keysets
             .iter()
@@ -127,7 +139,10 @@ impl Wallet {
     pub async fn get_keyset_fees_and_amounts(&self) -> Result<KeysetFeeAndAmounts, Error> {
         let metadata = self
             .metadata_cache
-            .load(&self.localstore, &self.client)
+            .load(&self.localstore, &self.client, {
+                let ttl = self.metadata_cache_ttl.lock();
+                *ttl
+            })
             .await?;
 
         let mut fees = HashMap::new();

+ 44 - 21
crates/cdk/src/wallet/mint_metadata_cache.rs

@@ -39,25 +39,22 @@ use cdk_common::{KeySet, MintInfo};
 use tokio::sync::Mutex;
 
 use crate::nuts::Id;
-#[cfg(feature = "auth")]
-use crate::wallet::AuthMintConnector;
 use crate::wallet::MintConnector;
-use crate::Error;
-
-/// Default TTL
-pub const DEFAULT_TTL: Duration = Duration::from_secs(300);
+#[cfg(feature = "auth")]
+use crate::wallet::{AuthMintConnector, AuthWallet};
+use crate::{Error, Wallet};
 
 /// Metadata freshness and versioning information
 ///
 /// Tracks when data was last fetched and which version is currently cached.
 /// Used to determine if cache is ready and if database sync is needed.
 #[derive(Clone, Debug)]
-struct FreshnessStatus {
+pub struct FreshnessStatus {
     /// Whether this data has been successfully fetched at least once
-    is_populated: bool,
+    pub is_populated: bool,
 
     /// A future time when the cache would be considered as staled.
-    valid_until: Instant,
+    pub updated_at: Instant,
 
     /// Monotonically increasing version number (for database sync tracking)
     version: usize,
@@ -67,7 +64,7 @@ impl Default for FreshnessStatus {
     fn default() -> Self {
         Self {
             is_populated: false,
-            valid_until: Instant::now() + DEFAULT_TTL,
+            updated_at: Instant::now(),
             version: 0,
         }
     }
@@ -122,8 +119,6 @@ pub struct MintMetadataCache {
     /// The mint server URL this cache manages
     mint_url: MintUrl,
 
-    default_ttl: Duration,
-
     /// Atomically-updated metadata snapshot (lock-free reads)
     metadata: Arc<ArcSwap<MintMetadata>>,
 
@@ -146,6 +141,27 @@ impl std::fmt::Debug for MintMetadataCache {
     }
 }
 
+impl Wallet {
+    /// Sets the metadata cache TTL
+    pub fn set_metadata_cache_ttl(&self, ttl: Option<Duration>) {
+        let mut guarded_ttl = self.metadata_cache_ttl.lock();
+        *guarded_ttl = ttl;
+    }
+
+    /// Get information about metadata cache info
+    pub fn get_metadata_cache_info(&self) -> FreshnessStatus {
+        self.metadata_cache.metadata.load().status.clone()
+    }
+}
+
+#[cfg(feature = "auth")]
+impl AuthWallet {
+    /// Get information about metadata cache info
+    pub fn get_metadata_cache_info(&self) -> FreshnessStatus {
+        self.metadata_cache.metadata.load().auth_status.clone()
+    }
+}
+
 impl MintMetadataCache {
     /// Compute a unique identifier for an Arc pointer
     ///
@@ -170,10 +186,9 @@ impl MintMetadataCache {
     /// let cache = MintMetadataCache::new(mint_url, None);
     /// // No data loaded yet - call load() to fetch
     /// ```
-    pub fn new(mint_url: MintUrl, default_ttl: Option<Duration>) -> Self {
+    pub fn new(mint_url: MintUrl) -> Self {
         Self {
             mint_url,
-            default_ttl: default_ttl.unwrap_or(DEFAULT_TTL),
             metadata: Arc::new(ArcSwap::default()),
             db_sync_versions: Arc::new(Default::default()),
             fetch_lock: Arc::new(Mutex::new(())),
@@ -196,6 +211,7 @@ impl MintMetadataCache {
     ///
     /// * `storage` - Database to persist metadata to (async background write)
     /// * `client` - HTTP client for fetching from mint server
+    /// * `ttl` - Optional TTL, if not provided it is asumed that any cached data is good enough
     ///
     /// # Returns
     ///
@@ -212,6 +228,7 @@ impl MintMetadataCache {
         &self,
         storage: &Arc<dyn WalletDatabase<Err = database::Error> + Send + Sync>,
         client: &Arc<dyn MintConnector + Send + Sync>,
+        ttl: Option<Duration>,
     ) -> Result<Arc<MintMetadata>, Error> {
         // Acquire lock to ensure only one fetch at a time
         let current_version = self.metadata.load().status.version;
@@ -220,7 +237,9 @@ impl MintMetadataCache {
         // Check if another caller already updated the cache while we waited
         let current_metadata = self.metadata.load().clone();
         if current_metadata.status.is_populated
-            && current_metadata.status.valid_until > Instant::now()
+            && ttl
+                .map(|ttl| current_metadata.status.updated_at + ttl > Instant::now())
+                .unwrap_or(true)
             && current_metadata.status.version > current_version
         {
             // Cache was just updated by another caller - return it
@@ -255,6 +274,7 @@ impl MintMetadataCache {
     ///
     /// * `storage` - Database to persist metadata to (if fetched or stale)
     /// * `client` - HTTP client for fetching from mint (only if cache empty)
+    /// * `ttl` - Optional TTL, if not provided it is asumed that any cached data is good enough
     ///
     /// # Returns
     ///
@@ -271,6 +291,7 @@ impl MintMetadataCache {
         &self,
         storage: &Arc<dyn WalletDatabase<Err = database::Error> + Send + Sync>,
         client: &Arc<dyn MintConnector + Send + Sync>,
+        ttl: Option<Duration>,
     ) -> Result<Arc<MintMetadata>, Error> {
         let cached_metadata = self.metadata.load().clone();
         let storage_id = Self::arc_pointer_id(storage);
@@ -284,7 +305,9 @@ impl MintMetadataCache {
             .unwrap_or_default();
 
         if cached_metadata.status.is_populated
-            && cached_metadata.status.valid_until > Instant::now()
+            && ttl
+                .map(|ttl| cached_metadata.status.updated_at + ttl > Instant::now())
+                .unwrap_or(true)
         {
             // Cache is ready - check if database needs updating
             if db_synced_version != cached_metadata.status.version {
@@ -297,7 +320,7 @@ impl MintMetadataCache {
         }
 
         // Cache not populated - fetch from mint
-        self.load_from_mint(storage, client).await
+        self.load_from_mint(storage, client, ttl).await
     }
 
     /// Load auth keysets and keys (auth feature only)
@@ -331,7 +354,7 @@ impl MintMetadataCache {
 
         // Check if auth data is populated in cache
         if cached_metadata.auth_status.is_populated
-            && cached_metadata.auth_status.valid_until > Instant::now()
+            && cached_metadata.auth_status.updated_at > Instant::now()
         {
             if db_synced_version != cached_metadata.status.version {
                 // Database needs updating - spawn background sync
@@ -346,7 +369,7 @@ impl MintMetadataCache {
         // Re-check if auth data was updated while waiting for lock
         let current_metadata = self.metadata.load().clone();
         if current_metadata.auth_status.is_populated
-            && current_metadata.auth_status.valid_until > Instant::now()
+            && current_metadata.auth_status.updated_at > Instant::now()
         {
             tracing::debug!(
                 "Auth cache was updated while waiting for fetch lock, returning cached data"
@@ -562,14 +585,14 @@ impl MintMetadataCache {
         // Update freshness status based on what was fetched
         if client.is_some() {
             new_metadata.status.is_populated = true;
-            new_metadata.status.valid_until = Instant::now() + self.default_ttl;
+            new_metadata.status.updated_at = Instant::now();
             new_metadata.status.version += 1;
         }
 
         #[cfg(feature = "auth")]
         if auth_client.is_some() {
             new_metadata.auth_status.is_populated = true;
-            new_metadata.auth_status.valid_until = Instant::now() + self.default_ttl;
+            new_metadata.auth_status.updated_at = Instant::now();
             new_metadata.auth_status.version += 1;
         }
 

+ 12 - 2
crates/cdk/src/wallet/mod.rs

@@ -1,12 +1,15 @@
 #![doc = include_str!("./README.md")]
 
 use std::collections::HashMap;
+use std::fmt::Debug;
 use std::str::FromStr;
 use std::sync::atomic::AtomicBool;
 use std::sync::Arc;
+use std::time::Duration;
 
 use cdk_common::amount::FeeAndAmounts;
 use cdk_common::database::{self, WalletDatabase};
+use cdk_common::parking_lot::Mutex;
 use cdk_common::subscription::WalletParams;
 use getrandom::getrandom;
 use subscription::{ActiveSubscription, SubscriptionManager};
@@ -92,6 +95,7 @@ pub struct Wallet {
     pub metadata_cache: Arc<MintMetadataCache>,
     /// The targeted amount of proofs to have at each size
     pub target_proof_count: usize,
+    metadata_cache_ttl: Arc<Mutex<Option<Duration>>>,
     #[cfg(feature = "auth")]
     auth_wallet: Arc<RwLock<Option<AuthWallet>>>,
     seed: [u8; 64],
@@ -223,7 +227,10 @@ impl Wallet {
         let mut fee_per_keyset = HashMap::new();
         let metadata = self
             .metadata_cache
-            .load(&self.localstore, &self.client)
+            .load(&self.localstore, &self.client, {
+                let ttl = self.metadata_cache_ttl.lock();
+                *ttl
+            })
             .await?;
 
         for keyset_id in proofs_per_keyset.keys() {
@@ -244,7 +251,10 @@ impl Wallet {
     pub async fn get_keyset_count_fee(&self, keyset_id: &Id, count: u64) -> Result<Amount, Error> {
         let input_fee_ppk = self
             .metadata_cache
-            .load(&self.localstore, &self.client)
+            .load(&self.localstore, &self.client, {
+                let ttl = self.metadata_cache_ttl.lock();
+                *ttl
+            })
             .await?
             .keysets
             .get(keyset_id)

+ 4 - 1
crates/cdk/src/wallet/swap.rs

@@ -49,7 +49,10 @@ impl Wallet {
 
         let active_keys = self
             .metadata_cache
-            .load(&self.localstore, &self.client)
+            .load(&self.localstore, &self.client, {
+                let ttl = self.metadata_cache_ttl.lock();
+                *ttl
+            })
             .await?
             .keys
             .get(&active_keyset_id)

+ 18 - 18
flake.lock

@@ -2,11 +2,11 @@
   "nodes": {
     "crane": {
       "locked": {
-        "lastModified": 1760924934,
-        "narHash": "sha256-tuuqY5aU7cUkR71sO2TraVKK2boYrdW3gCSXUkF4i44=",
+        "lastModified": 1762538466,
+        "narHash": "sha256-8zrIPl6J+wLm9MH5ksHcW7BUHo7jSNOu0/hA0ohOOaM=",
         "owner": "ipetkov",
         "repo": "crane",
-        "rev": "c6b4d5308293d0d04fcfeee92705017537cad02f",
+        "rev": "0cea393fffb39575c46b7a0318386467272182fe",
         "type": "github"
       },
       "original": {
@@ -23,11 +23,11 @@
         "rust-analyzer-src": []
       },
       "locked": {
-        "lastModified": 1761287960,
-        "narHash": "sha256-DbGYVbF0TgoKTFNQv/3jqaUWql8OYzewl4v2gw6jmQs=",
+        "lastModified": 1762929886,
+        "narHash": "sha256-TQZ3Ugb1FoHpTSc8KLrzN4njIZU4FemAMHyS4M3mt6s=",
         "owner": "nix-community",
         "repo": "fenix",
-        "rev": "64cb168ed9ec61ef18e28f1e4ee9f44381d6ecd2",
+        "rev": "6998514dce2c365142a0a119a95ef95d89b84086",
         "type": "github"
       },
       "original": {
@@ -93,11 +93,11 @@
     },
     "nixpkgs": {
       "locked": {
-        "lastModified": 1761173472,
-        "narHash": "sha256-m9W0dYXflzeGgKNravKJvTMR4Qqa2MVD11AwlGMufeE=",
+        "lastModified": 1762756533,
+        "narHash": "sha256-HiRDeUOD1VLklHeOmaKDzf+8Hb7vSWPVFcWwaTrpm+U=",
         "owner": "NixOS",
         "repo": "nixpkgs",
-        "rev": "c8aa8cc00a5cb57fada0851a038d35c08a36a2bb",
+        "rev": "c2448301fb856e351aab33e64c33a3fc8bcf637d",
         "type": "github"
       },
       "original": {
@@ -109,11 +109,11 @@
     },
     "nixpkgs_2": {
       "locked": {
-        "lastModified": 1759070547,
-        "narHash": "sha256-JVZl8NaVRYb0+381nl7LvPE+A774/dRpif01FKLrYFQ=",
+        "lastModified": 1759417375,
+        "narHash": "sha256-O7eHcgkQXJNygY6AypkF9tFhsoDQjpNEojw3eFs73Ow=",
         "owner": "NixOS",
         "repo": "nixpkgs",
-        "rev": "647e5c14cbd5067f44ac86b74f014962df460840",
+        "rev": "dc704e6102e76aad573f63b74c742cd96f8f1e6c",
         "type": "github"
       },
       "original": {
@@ -130,11 +130,11 @@
         "nixpkgs": "nixpkgs_2"
       },
       "locked": {
-        "lastModified": 1760663237,
-        "narHash": "sha256-BflA6U4AM1bzuRMR8QqzPXqh8sWVCNDzOdsxXEguJIc=",
+        "lastModified": 1762868777,
+        "narHash": "sha256-QqS72GvguP56oKDNUckWUPNJHjsdeuXh5RyoKz0wJ+E=",
         "owner": "cachix",
         "repo": "pre-commit-hooks.nix",
-        "rev": "ca5b894d3e3e151ffc1db040b6ce4dcc75d31c37",
+        "rev": "c5c3147730384576196fb5da048a6e45dee10d56",
         "type": "github"
       },
       "original": {
@@ -160,11 +160,11 @@
         ]
       },
       "locked": {
-        "lastModified": 1761273263,
-        "narHash": "sha256-6d6ojnu6A6sVxIjig8OL6E1T8Ge9st3YGgVwg5MOY+Q=",
+        "lastModified": 1762915112,
+        "narHash": "sha256-d9j1g8nKmYDHy+/bIOPQTh9IwjRliqaTM0QLHMV92Ic=",
         "owner": "oxalica",
         "repo": "rust-overlay",
-        "rev": "28405834d4fdd458d28e123fae4db148daecec6f",
+        "rev": "aa1e85921cfa04de7b6914982a94621fbec5cc02",
         "type": "github"
       },
       "original": {

+ 10 - 2
flake.nix

@@ -56,7 +56,7 @@
 
         # Toolchains
         # latest stable
-        stable_toolchain = pkgs.rust-bin.stable."1.90.0".default.override {
+        stable_toolchain = pkgs.rust-bin.stable."1.91.1".default.override {
           targets = [ "wasm32-unknown-unknown" ]; # wasm
           extensions = [
             "rustfmt"
@@ -184,7 +184,15 @@
 
             stable = pkgs.mkShell (
               {
-                shellHook = ''${_shellHook}'';
+                shellHook = ''
+                  ${_shellHook}
+                  # Needed for github ci
+                  export LD_LIBRARY_PATH=${
+                    pkgs.lib.makeLibraryPath [
+                      pkgs.zlib
+                    ]
+                  }:$LD_LIBRARY_PATH
+                '';
                 buildInputs = buildInputs ++ [ stable_toolchain ];
                 inherit nativeBuildInputs;
 

+ 4 - 4
justfile

@@ -59,7 +59,7 @@ test:
   if [ ! -f Cargo.toml ]; then
     cd {{invocation_directory()}}
   fi
-  cargo test --lib
+  cargo test --lib --workspace --exclude cdk-postgres
 
   # Run pure integration tests
   cargo test -p cdk-integration-tests --test mint 
@@ -155,11 +155,11 @@ test-nutshell:
     
 
 # run `cargo clippy` on everything
-clippy *ARGS="--locked --offline --workspace --all-targets":
-  cargo clippy {{ARGS}}
+clippy *ARGS="--workspace --all-targets":
+  cargo clippy {{ARGS}} -- -D warnings
 
 # run `cargo clippy --fix` on everything
-clippy-fix *ARGS="--locked --offline --workspace --all-targets":
+clippy-fix *ARGS="--workspace --all-targets":
   cargo clippy {{ARGS}} --fix
 
 typos: 

+ 54 - 0
meetings/2025-11-12-agenda.md

@@ -0,0 +1,54 @@
+# CDK Development Meeting
+
+Nov 12 2025 15:00 UTC
+
+Meeting Link: https://meet.fulmo.org/cdk-dev
+
+## Merged
+
+- [#1258](https://github.com/cashubtc/cdk/pull/1258) - mint async melt
+- [#1256](https://github.com/cashubtc/cdk/pull/1256) - refactor: replace proof swap with state check in error recovery
+- [#1254](https://github.com/cashubtc/cdk/pull/1254) - Weekly Meeting Agenda - 2025-11-05
+- [#1247](https://github.com/cashubtc/cdk/pull/1247) - feat: add keyset_amounts table to track issued and redeemed amounts
+
+## Ongoing
+
+- [#1253](https://github.com/cashubtc/cdk/pull/1253) - feat: P2BK
+- [#1252](https://github.com/cashubtc/cdk/pull/1252) - Fix race condition when concurrent payments are processed for the same payment_id
+- [#1251](https://github.com/cashubtc/cdk/pull/1251) - feat: custom axum router
+- [#1240](https://github.com/cashubtc/cdk/pull/1240) - Introduce MintMetadataCache for efficient key and metadata management
+- [#1214](https://github.com/cashubtc/cdk/pull/1214) - feat(cdk-payment-processor): add currency unit parameter to make_payment
+- [#1212](https://github.com/cashubtc/cdk/pull/1212) - Various mint bugfixes for swap and melt. SIG_INPUTS+SIG_ALL, locktimes, P2PK+HTLC. Also updates the SIG_ALL message for amount-switching
+- [#1211](https://github.com/cashubtc/cdk/pull/1211) - Regtest setup
+- [#1210](https://github.com/cashubtc/cdk/pull/1210) - test: add mutation testing infrastructure
+- [#1208](https://github.com/cashubtc/cdk/pull/1208) - Sig all fixes
+- [#1204](https://github.com/cashubtc/cdk/pull/1204) - Add database transaction trait for cdk wallet
+- [#1202](https://github.com/cashubtc/cdk/pull/1202) - Onchain
+- [#1201](https://github.com/cashubtc/cdk/pull/1201) - feat: npubcash
+- [#1200](https://github.com/cashubtc/cdk/pull/1200) - feat: optimize pending mint quotes query performance
+- [#1198](https://github.com/cashubtc/cdk/pull/1198) - fix: check the removed_ys argument before creating the delete query
+- [#1196](https://github.com/cashubtc/cdk/pull/1196) - feat(ci): add merge queue workflow and simplify clippy checks
+- [#1190](https://github.com/cashubtc/cdk/pull/1190) - feat(cashu): add NUT-26 bech32m encoding for payment requests
+- [#1182](https://github.com/cashubtc/cdk/pull/1182) - ehash: add support for mining share mint quotes
+- [#1181](https://github.com/cashubtc/cdk/pull/1181) - Deterministic Currency Unit Derivation Paths
+- [#1171](https://github.com/cashubtc/cdk/pull/1171) - Add Dart Bindings Support
+- [#1153](https://github.com/cashubtc/cdk/pull/1153) - Add cdk-mintd module and package
+- [#1132](https://github.com/cashubtc/cdk/pull/1132) - Quote id as lookup
+- [#1127](https://github.com/cashubtc/cdk/pull/1127) - rename ln settings in toml configuration
+- [#1118](https://github.com/cashubtc/cdk/pull/1118) - feat: uniffi bindings for golang
+- [#1100](https://github.com/cashubtc/cdk/pull/1100) - NUT-XX: Cairo Spending Conditions implementation
+- [#1067](https://github.com/cashubtc/cdk/pull/1067) - Nutxx ohttp
+- [#1053](https://github.com/cashubtc/cdk/pull/1053) - feat: P2PK key storage and auto-sign on receive
+- [#1049](https://github.com/cashubtc/cdk/pull/1049) - feat: ldk-node run mintd
+- [#1011](https://github.com/cashubtc/cdk/pull/1011) - fix: migrate check_mint_quote_paid fn from ln.rs to mod.rs
+
+## New
+
+### Issues
+
+- [#1259](https://github.com/cashubtc/cdk/issues/1259) - Suggested changes in the payment_processor.proto declaration
+
+### PRs
+
+- [#1260](https://github.com/cashubtc/cdk/pull/1260) - feat(ci): add nightly rustfmt automation with flexible formatting policy
+- [#1257](https://github.com/cashubtc/cdk/pull/1257) - bring signatory up to date with the remote signer spec

+ 13 - 2
misc/fake_itests.sh

@@ -189,12 +189,23 @@ echo "Running happy_path_mint_wallet test"
 cargo test -p cdk-integration-tests --test happy_path_mint_wallet --  --nocapture
 status2=$?
 
-# Exit with the status of the second test
+# Exit if the second test failed
 if [ $status2 -ne 0 ]; then
     echo "Second test failed with status $status2, exiting"
     exit $status2
 fi
 
-# Both tests passed
+# Run third test (async_melt) only if previous tests succeeded
+echo "Running async_melt test"
+cargo test -p cdk-integration-tests --test async_melt
+status3=$?
+
+# Exit with the status of the third test
+if [ $status3 -ne 0 ]; then
+    echo "Third test (async_melt) failed with status $status3, exiting"
+    exit $status3
+fi
+
+# All tests passed
 echo "All tests passed successfully"
 exit 0

+ 1 - 1
rust-toolchain.toml

@@ -1,4 +1,4 @@
 [toolchain]
-channel="1.90.0"
+channel="1.91.1"
 components = ["rustfmt", "clippy", "rust-analyzer"]