Skip to content

Add prefer_async support via post body#887

Closed
robwoodgate wants to merge 12 commits intocashubtc:mainfrom
robwoodgate:prefer-async-post-body
Closed

Add prefer_async support via post body#887
robwoodgate wants to merge 12 commits intocashubtc:mainfrom
robwoodgate:prefer-async-post-body

Conversation

@robwoodgate
Copy link
Copy Markdown
Contributor

@robwoodgate robwoodgate commented Feb 9, 2026

Related NUT: cashubtc/nuts#339

PR Summary

  • Add prefer_async to melt requests and wire it through wallet/API/mint so clients can request async processing, with pending response returned immediately.
  • Implement mint-side async melt settlement via background task, keeping existing sync flow intact.
  • Add CLI pay --prefer-async and a regression test that verifies pending → paid transition
  • Note: cashu/mint/ledger.py line endings normalized to LF (diff shows as full rewrite without --ignore-space-change).

incidental Changes for CI test stability

The first 4 commits in this PR implement the feature, the rest are aimed at stabilizing CI to make this PR green.

  • disable DB pooling in tests and dispose wallet DB engines to avoid SQLite locks.
  • Hardened CLI tests against option parsing edge cases by separating quote IDs from flags.
  • Stabilized regtest and mint test infrastructure to reduce flaky failures: timeouts, DB reset, FakeWallet queue loop binding, early P2PK pubkey validation

Return pending immediately and settle melts in background when prefer_async is set, with regression coverage.

Note: cashu/mint/ledger.py line endings normalized to LF.
@codecov
Copy link
Copy Markdown

codecov Bot commented Feb 9, 2026

Codecov Report

❌ Patch coverage is 77.77778% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 51.73%. Comparing base (8748b99) to head (9010ccd).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
cashu/wallet/cli/cli.py 0.00% 1 Missing ⚠️
cashu/wallet/v1_api.py 80.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #887      +/-   ##
==========================================
+ Coverage   50.02%   51.73%   +1.70%     
==========================================
  Files          89       88       -1     
  Lines       10545    10313     -232     
==========================================
+ Hits         5275     5335      +60     
+ Misses       5270     4978     -292     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Copy Markdown
Collaborator

@a1denvalu3 a1denvalu3 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the PR, but prefer_async is being worked on in #801 . I'll update that one.

Comment thread cashu/wallet/p2pk.py
Comment on lines +57 to +60
try:
PublicKey(bytes.fromhex(data))
except Exception as exc:
raise ValueError("Invalid pubkey") from exc
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unrelated?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Related in the sense that CI was intermittently failing tests, and I wanted to remove some of the flakey-ness and get a green run. It's part of the "Incidental changes" noted in the PR summary

Comment thread cashu/mint/ledger.py
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like the clanker replaced the whole file, making diff impossible.

Copy link
Copy Markdown
Contributor Author

@robwoodgate robwoodgate Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mentioned the line endings in the PR notes. It was using non-standard line endings, so I normalized it.
Use --ignore-space-changes to see the raw change. But if you are already working on this feature in a different PR (sorry, didn't see, and the change in spec is < 1 week old) then no need.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here you go:

robw:nutshell [prefer-async-post-body] $ git diff 9128bc7 --ignore-space-change cashu/mint/ledger.py
diff --git a/cashu/mint/ledger.py b/cashu/mint/ledger.py
index b60a1f2..422e837 100644
--- a/cashu/mint/ledger.py
+++ b/cashu/mint/ledger.py
@@ -867,6 +867,7 @@ class Ledger(
         proofs: List[Proof],
         quote: str,
         outputs: Optional[List[BlindedMessage]] = None,
+        prefer_async: Optional[bool] = None,
     ) -> PostMeltQuoteResponse:
         """Invalidates proofs and pays a Lightning invoice.
 
@@ -939,6 +940,69 @@ class Ledger(
         if outputs:
             await self._store_blinded_messages(outputs, melt_id=melt_quote.quote)
 
+        if prefer_async:
+            pending_response = PostMeltQuoteResponse.from_melt_quote(
+                melt_quote.model_copy()
+            )
+            asyncio.create_task(
+                self._melt_background_task(
+                    melt_quote=melt_quote,
+                    proofs=proofs,
+                    outputs=outputs,
+                    previous_state=previous_state,
+                    fee_reserve_provided=fee_reserve_provided,
+                    unit=unit,
+                    method=method,
+                )
+            )
+            return pending_response
+        return await self._settle_melt_after_pending(
+            melt_quote=melt_quote,
+            proofs=proofs,
+            outputs=outputs,
+            previous_state=previous_state,
+            fee_reserve_provided=fee_reserve_provided,
+            unit=unit,
+            method=method,
+        )
+
+    async def _melt_background_task(
+        self,
+        *,
+        melt_quote: MeltQuote,
+        proofs: List[Proof],
+        outputs: Optional[List[BlindedMessage]],
+        previous_state: MeltQuoteState,
+        fee_reserve_provided: int,
+        unit: Unit,
+        method: Method,
+    ) -> None:
+        try:
+            await self._settle_melt_after_pending(
+                melt_quote=melt_quote,
+                proofs=proofs,
+                outputs=outputs,
+                previous_state=previous_state,
+                fee_reserve_provided=fee_reserve_provided,
+                unit=unit,
+                method=method,
+            )
+        except Exception as e:
+            logger.error(
+                f"Async melt task failed for quote {melt_quote.quote}: {e}"
+            )
+
+    async def _settle_melt_after_pending(
+        self,
+        *,
+        melt_quote: MeltQuote,
+        proofs: List[Proof],
+        outputs: Optional[List[BlindedMessage]],
+        previous_state: MeltQuoteState,
+        fee_reserve_provided: int,
+        unit: Unit,
+        method: Method,
+    ) -> PostMeltQuoteResponse:
         # if the melt corresponds to an internal mint, mark both as paid
         melt_quote = await self.melt_mint_settle_internally(melt_quote, proofs)
         # quote not paid yet (not internal), pay it with the backend
(END)

Comment thread tests/conftest.py
Comment on lines +99 to +106
await conn.execute(
"""
SELECT pg_terminate_backend(pid)
FROM pg_stat_activity
WHERE datname = current_database()
AND pid <> pg_backend_pid();
"""
)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what does this do?

Copy link
Copy Markdown
Contributor Author

@robwoodgate robwoodgate Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fixes a CI abnormal exit where the DB is torn down with active threads.
In fact, every commit in this PR after a96882d is trying to fix up CI, as noted in the PR notes

)
await wallet.async_init()
yield wallet
await wallet.db.engine.dispose()
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this deserves a PR on its own?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please be my guest - use or discard anything as you wish

@a1denvalu3 a1denvalu3 marked this pull request as draft February 10, 2026 11:48
@robwoodgate
Copy link
Copy Markdown
Contributor Author

@a1denvalu3 - before you bin this PR out of hand, I have tested it against cashu-ts integration tests running cashubtc/cashu-ts#500

@a1denvalu3 a1denvalu3 closed this Feb 13, 2026
@github-project-automation github-project-automation Bot moved this from Backlog to Done in nutshell Feb 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants