Skip to content

Cashu: no way to mint an externally-created bolt11 mint quote into the wallet balance #99

Description

@hash-money

What we're doing

Our wallet (built on orange-sdk with the cashu trusted tier) has a flow
where our backend creates a bolt11 mint quote against the same mint the
wallet uses, locked to the wallet's pubkey per NUT-20, while the wallet
is offline. When the wallet comes back online it holds the quote_id and
the NUT-20 quote-lock secret key — only it can mint the quote.

We've verified the quote is genuinely mintable: a stand-alone cdk 0.16.0
wallet pointed at the same mint, given the quote id and the lock key, mints
the proofs fine (5,000-sat test, proofs land, balance shows). The problem
is landing those proofs in the orange-sdk wallet's balance.

The gap (rev 5a540f6)

All the machinery for "quote paid → mint proofs → emit
Event::PaymentReceived" already exists — Cashu::monitor_mint_quote
(trusted_wallet/cashu/mod.rs:877) drives proof_stream and lands the
proofs in the wallet. But the only ways a quote reaches it are internal:

  • quotes created by get_bolt11_invoice / the reusable-URI path, sent over
    the internal mint_quote_sender channel (cashu/mod.rs:101-118, 148-163);
  • pending quotes re-adopted from the store at startup via
    get_active_mint_quotes() (cashu/mod.rs:543).

A quote created outside the SDK has no way in:

  • Cashu.cashu_wallet is a private field (cashu/mod.rs:61-62), and
    Cashu::init is pub(crate) (:434);
  • TrustedWalletInterface is sealed (trusted_wallet/mod.rs:47);
  • CashuKvDatabase::new is pub(crate) (cashu_store.rs:167).

Running a second cdk::Wallet on the SDK's store isn't an option:
CashuKvDatabase stores proofs as a whole snapshot with mutations
serialized only in-process (cashu_store.rs:133-135), which is why your
VssConfig docs require one active instance per store (lib.rs:221) —
a second instance risks clobbering proof snapshots. And a second wallet
on a separate store is a disjoint balance the app can't spend,
rebalance, or back up through the SDK. So every workaround we can see
means carrying a parallel wallet in our tree. We'd rather not.

Proposed fix

A public method that feeds the existing monitor path, e.g.:

impl Wallet {
    /// Adopt a bolt11 mint quote created outside this wallet and mint it
    /// into the trusted balance once paid. `lock_secret` is the NUT-20
    /// quote-lock signing key, if the quote was locked.
    pub async fn adopt_cashu_mint_quote(
        &self,
        quote_id: String,
        lock_secret: Option<cdk::nuts::SecretKey>,
    ) -> Result<(), WalletError>;
}

Implementation direction: fetch the quote state from the mint, persist it
into the wallet's localstore with secret_key = lock_secret, and push
it onto the existing mint_quote_sender channel — monitor_mint_quote
does the rest, and the startup re-adoption at cashu/mod.rs:543 makes it
restart-safe for free.

One implementation note that may save a dead end: in cdk 0.16.0,
Wallet::batch_mint's external_keys parameter never takes effect for
bolt11 quotes — the signing gate in prepare_batch reads the stored
quote's secret_key (cdk/src/wallet/issue/saga/mod.rs:522-534). Storing
the lock key on the quote record is the path that works; that's what our
stand-alone-wallet test relies on.

An accessor (pub fn cashu_wallet(&self) -> Arc<cdk::wallet::Wallet>)
would also unblock us, but it bypasses your event queue and hands out
internals you've deliberately sealed, so we'd understand preferring the
method.

Happy to PR this if the direction works for you — and we have a staging
mint + device harness we can test any branch against same-day.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions