NUT-XX wallet quote lookup: wire-contract fixes, signed lookups with persistence, mintable-only filter - #9
Open
vnprc wants to merge 7 commits into
Open
Conversation
…ctor
The connector's post_mint_quote_by_pubkey diverged from the endpoint the
mint actually serves:
- The lookup is method-agnostic: one POST to /v1/mint/quote/pubkey
answers for every payment method at once, so the per-method dispatch,
the {method} path segment, and the method parameter on the
MintConnector trait are gone.
- The mint wraps the quotes in a {"quotes": [...]} envelope, with each
quote flattened to its bare NUT-04 response object rather than
MintQuoteResponse's externally tagged form. Parse the envelope, then
reconstruct each variant by dispatching on the per-quote "method"
field - the derived Deserialize for the enum cannot parse the
flattened objects directly.
- A missing or malformed "method" is a protocol error: guessing bolt11
would reinterpret a paid bolt12/onchain/custom quote's accounting
fields as an unpaid Bolt11 response. Such entries are skipped with a
warning naming the quote id, plus a summary warning with
skipped/total counts, rather than failing the whole batch - one bad
entry should not hide every other quote from a caller reconciling
many pubkeys at once.
The in-process test connector now forwards the typed request fields
and converts with the shared From impl, which preserves each quote's
own method instead of stamping the caller's.
MockTransport-backed tests cover the mixed-method reconstruction and
the method-agnostic URL, the bolt12/onchain variants, the
missing-method rejection, and the skip-malformed and all-malformed
batch behaviors.
get_mint_quote_by_public_key imported unique_string through the test module root, where the helper is not re-exported (it is private to this module and already in scope). Any build that enables cdk-common's "test" feature - the sqlite backend's test suite, for one - failed to compile on the unresolved path. Delete the import; calls resolve to the module-level definition directly.
The wallet's mint_quote table gained a created_time column (for ordering) whose value was never written: add_mint_quote's INSERT omits the column, so every row keeps the schema default of 0. The in-memory MintQuote struct does not carry the field, so no construction site - mint_quote, fetch_mint_quote, or any other path that funnels into add_mint_quote - could populate it either. The mint's NUT-04 responses carry no creation time, so the honest value is the wallet's own first sighting: bind unix_time() in the INSERT, the same way the p2pk_signing_key insert in this module already stamps its created_time. The column stays out of the ON CONFLICT UPDATE clause, so re-storing an existing quote preserves the original first-store time. Fixing the single shared store path covers every quote construction site at once. The regression test reads the column back over a raw connection, since the public struct does not expose it, and plants a sentinel before a second store to prove updates preserve rather than restamp it.
Give the wallet a high-level entry point for the NUT-XX lookup: sign, fetch, validate, and store, so callers get reconciled MintQuote records instead of raw connector responses. Modeled on fetch_mint_quote (cdk convention: fetch_ = fetch-and-store). - The mint's NUT-06 pubkey binds the signatures to this mint; a mint that does not advertise one fails fast with Error::MissingPubkey. - secret_keys are deduplicated by pubkey before the request is built, so duplicates don't burn slots of the MAX_LOOKUP_PUBKEYS budget, and an empty set short-circuits without any network call. - Every returned quote is checked against the requested pubkeys; a quote with a missing or unrequested pubkey is logged and dropped rather than stored, since this method writes mint responses to the wallet database and cannot take the mint's word for whose they are. - Reconciliation reuses apply_mint_quote_response and constructs unseen quotes exactly as fetch_mint_quote does, then stamps the signing key. Writes are change-guarded: apply_mint_quote_response's bool means "not stale", not "changed" - it reports true for a byte-identical repeat - so the guard compares the fields the response can touch before and after instead. A caller polling on an interval must not rewrite an unchanged quote history on every pass. The mock connector now captures lookup requests and counts get_mint_info calls so tests can assert what actually went out. Tests cover sign/verify round-trip and mint-binding, the oversized-batch error, store-and-stamp behavior, the unrequested-pubkey drop, the missing-mint-pubkey failure, the empty-input short-circuit, and write idempotency witnessed through the quote row's optimistic-concurrency version.
Add an opt-in request filter so callers can bound the pubkey lookup response to quotes that are still mintable (amount_paid > amount_issued). The wire format is additive and backward compatible: only_mintable is absent from the wire when false (skip_serializing_if), so a mint that predates the field sees the request an old client would send, and an old client deserializing a request defaults it to false. Threads the flag from Wallet::fetch_mint_quotes_by_pubkey through the NUT-XX request, the axum handler, Mint::get_mint_quote_by_pubkey, and the mint database trait down to the SQL query, which adds "AND amount_paid > amount_issued" to the WHERE clause when set - this also skips the per-quote payments/issuance follow-up queries for excluded rows. The filter is a response-bounding convenience only: it is not part of the NUT-20 signed message and carries no security weight. The mint logs one debug line after the query with the pubkey and result counts and the flag, so operators can see lookup traffic without turning on request tracing. Serde tests pin all three wire shapes (absent, false, true). The wallet test asserts the flag lands on the outgoing request verbatim, and mint-side integration coverage creates three quotes for one key - unpaid, paid-unissued, fully-issued - and checks the filter returns exactly the paid-unissued one when set, and all three when not.
Exercise Wallet::fetch_mint_quotes_by_pubkey against a real in-process Mint through the existing DirectMintConnection: the wallet signs its own lookup challenge, the mint verifies it, and the returned quote lands in the wallet database with its signing key stamped. A negative case covers a key with no quotes (empty result, not an error). Lives in cdk-integration-tests rather than crates/cdk/tests so it reuses the complete direct connector instead of a stub-heavy local one.
A cached mint record can lack a pubkey while still satisfying the metadata cache: it may predate the mint advertising one, or the stored form may fail to parse (the wallet store currently persists this column in an encoding its reader does not accept, reported separately). Trusting the cache meant fetch_mint_quotes_by_pubkey failed with MissingPubkey forever, starving a polling caller even though the live mint advertises a pubkey. Fall back to one forced fetch_mint_info refresh before erroring, which restores the pubkey for the life of the process. A mint that genuinely has no pubkey still fails with MissingPubkey after the refresh.
Author
|
Added a commit to fix a restart bug i found in testing. The mint identity pubkey is not stored correctly and dropped on restart, when it attempts to read from |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Builds directly on this branch's connector and test scaffolding, fixing the wire
contract and completing the wallet side of the NUT-XX quote lookup. One commit per
logical change:
method-less
POST /v1/mint/quote/pubkey, the{"quotes": [...]}envelope, andper-quote reconstruction by dispatching on each object's
methodfield (thederived enum form can't parse the flat spec'd objects). Malformed entries are
skipped with warnings instead of failing the whole batch.
fix for a test-feature build break in the base branch; happy to move this into
the mint PR instead.
the wallet's
mint_quote.created_timecolumn was never written (always 0);now set once on first store, preserved across updates. Independent bug, fix
benefits all wallet write paths.
Wallet::fetch_mint_quotes_by_pubkey— signs the lookupper NUT-XX, validates returned quotes against the requested pubkeys, and
persists them with their signing key stamped, so a polling caller can discover
and mint quotes it never saw created. Writes are skipped when nothing changed.
only_mintablerequest flag (absent from the wire when false, so fullybackward compatible) bounding the response to quotes with
amount_paid > amount_issued, filtered in the mint's DB query.real mint + real wallet round trip: sign, verify, look up, persist.
Every commit compiles and tests green standalone; full suite green at the tip
(
cdk,cashu,cdk-common,cdk-sql-common,cdk-axum, sqlite backend, andthe pure integration tests).