Skip to content

feat(wallet-sdk): cashu receive quote slice (step 9) - #1176

Merged
jbojcic1 merged 1 commit into
masterfrom
sdk/cashu-receive-quote-slice
Aug 14, 2026
Merged

jbojcic1 merged 1 commit into
masterfrom
sdk/cashu-receive-quote-slice

Conversation

@ditto-agent

@ditto-agent ditto-agent commented Aug 13, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Step 9 of the 19-step no-cache wallet-SDK extraction (spec: docs/superpowers/specs/2026-06-24-wallet-sdk-no-cache-production-design.md; plan committed in this PR: docs/superpowers/plans/2026-08-13-wallet-sdk-cashu-receive-quote-slice.md). This wires sdk.receive.cashu (getLightningQuote / createQuote / getQuote) and flips web cashu-receive-quote creation and tracking from @agicash/wallet-sdk/temporary to sdk.receive.cashu.*. The web background processor (useProcessCashuReceiveQuoteTasks) keeps completing, expiring, failing, and melt-initiating quotes through /temporary until step 18 — the money-path boundary stays intact.

What changed

SDK

  • domain/sdk/receive.ts — fills GetCashuReceiveLightningQuoteParams (account: CashuAccount, amount: Money, description?) and CreateCashuReceiveQuoteParams (account: CashuAccount, lightningQuote, purpose?, transferId?), replacing the two step-9 unknown placeholders. Spark/cashuToken placeholders stay. (Initially shipped as accountId with an internal fetch; flipped to the full account in f30a3315 after review — see r3784641517.)
  • NEW domain/receive/receive-api.ts — createReceiveApi(deps) returns the full ReceiveApi with cashu implemented and session fences on the accounts template (capture sessionSignal(), await repository/service deps, re-check, thread abortSignal where the layer accepts it, re-check). spark / cashuToken remain throwing getters (NotImplementedError('receive.spark') / ('receive.cashuToken')) so steps 11/12 can fill them without reshaping sdk.ts.
  • domain/receive/cashu-receive-quote-service.ts — createReceiveQuote gains an optional second param options?: { abortSignal?: AbortSignal }, threaded to both repository.create calls. Backward-compatible; in-package callers are unchanged.
  • domain/sdk/sdk.ts — the throwing get receive() getter is replaced with readonly receive wired via createReceiveApi({ db, getSession: getLiveSession, keys, getAccountRepository: accounts.getRepository }).

Web

  • apps/web-wallet/app/features/receive/cashu-receive-quote-hooks.ts — useCreateCashuReceiveQuote now calls sdk.receive.cashu.getLightningQuote + createQuote; useTrackCashuReceiveQuote now calls sdk.receive.cashu.getQuote. The hooks' external APIs are unchanged, so callers are untouched. Caches, change handlers, usePendingCashuReceiveQuotes, useProcessCashuReceiveQuoteTasks, and the repository/service hook exports stay on /temporary.

Canary

  • packages/wallet-sdk/temporary.ts — two dead cashu-receive-quote re-exports pruned: CashuReceiveQuoteSchema and { computeTotalFee, deriveNut20LockingPublicKey } from cashu-receive-quote-core. AgicashDbCashuReceiveQuote, getInitializedCashuWallet, CashuReceiveQuoteRepository, and CashuReceiveQuoteService stay (live consumers).

Tests

  • NEW domain/receive/receive-api.test.ts — createQuote throws NoSessionError before any repository work; happy path passes { userId, account, receiveType: 'LIGHTNING', lightningQuote, purpose, transferId } and { abortSignal } to the service; mid-construction abort yields SessionEndedError with the service never called (both methods); getLightningQuote calls the service with the given account's wallet; getQuote returns the repository result including null and threads the abort signal, and rejects with SessionEndedError when the signal aborts; accessing receive.spark / receive.cashuToken throws NotImplementedError. Wallet-sdk suite 174 green.

Design decisions

  • receiveType pinned to 'LIGHTNING' inside the API. CASHU_TOKEN quotes are created only by in-package flows (steps 12/16/17: receive-cashu-token-quote-service, transfer-service, lightning-address-service), never by the host.
  • Caller-supplied account: getLightningQuote and createQuote take the full CashuAccount in params (f30a3315, review r3784641517). The caller already holds the account, so the SDK does no per-call account fetch (which cost a Supabase read of the account plus all unspent proofs, proof decryption, and a fresh wallet init of three mint HTTP requests — twice per receive flow). The cashu-only constraint is compile-time; CashuAccount is structurally assignable to RedactedCashuAccount and passes to createReceiveQuote; account.wallet feeds getLightningQuote. Fetch-by-id remains the shape only for dark/background paths that work off quote.accountId. getAccountRepository stays a dep — it feeds the quote repository constructor.
  • CashuCryptography assembled from session keys (first SDK-side assembly; the web builds its own from TanStack caches): getSeed / getXpub go through keys.getCashuSeed(). getPrivateKey is a direct Open Secret read (getCashuPrivateKey) and is unfenced; it is only reachable through completeReceive, which no step-9 contract method calls — the processor path stays web-side until step 18.
  • No events emitted: cashu-receive-quote.created / .updated stay type-only until the step-18 realtime feed (contacts + transactions precedent). events.ts is untouched.
  • Latency parity (accepted): quote creation is now getLightningQuote + createQuote, each fetching the account fresh (DB read + wallet init) instead of using the web's cached wallet. This is the no-cache design working as intended; flipped account reads have behaved this way since step 6.
  • Untouched laggard consumers (each has its own slice): transaction-additional-details.tsx (getByTransactionId is not on the contract); transfer-service-hooks.ts (step 16); receive-cashu-token-hooks.ts + _protected.receive.cashu_.token.tsx (step 12); spark-receive-quote-hooks.ts (step 11); cashu-receive-quote-*.server.ts (step 17).

Verification

  • bun run fix:all exit 0; bun run typecheck exit 0 (all packages).
  • Unit tests: all green (wallet-sdk 175 pass, including 15 new receive-api tests).
  • Browser smoke on the local stack: a live 21-sat testnut receive created a quote through sdk.receive.cashu.* and completed end-to-end through the untouched web processor (/temporary); zero console errors.

Delegation note

Implementation, tests, web flip, the temporary.ts prune, and this description were produced as paid maxplayer marketplace jobs and integrated + verified locally by the orchestrating agent.

Adversarial review findings and dispositions

A marketplace adversarial review of the integrated diff returned NOT READY with 0 Critical, 2 Important, and 2 Minor findings. Dispositions:

  • Important 1 — createQuote does not bind the lightningQuote preview to the account that produced it. Kept as-is, documented for the maintainer. The pre-slice service had the same two-step shape with no origin check, and no current caller can mix accounts (the web mutation threads one account through both calls). A proper fix reshapes the maintainer-endorsed contract (CashuReceiveLightningQuote would carry its originating account id and createQuote would reject a mismatch) and equally affects the step-11 spark namespace — a contract decision to take once, not a slice-local patch.
  • Important 2 — createQuote re-fetches the account with wallet initialization after the mint quote already exists. RESOLVED in f30a3315 (review r3784641517): the params take the full CashuAccount and the internal account lookup is gone entirely, which removes both the re-initialization failure surface and the latency regression versus the live branch.
  • Minor 1 — missing post-operation fence coverage. Fixed in ca71e695: post-op SessionEndedError tests for getLightningQuote and createQuote, plus a signal-identity test across the account lookup and the service write.
  • Minor 2 — the CashuCryptography adapter was untested. Fixed in ca71e695: a default-service-path test proves the mint quote's locking key derives from the same base xpub as SessionKeys.getCashuLockingXpub.

An earlier review attempt was delivered blocked: the seller's harness had no shell or network, so it could not clone the repo; it reported that honestly instead of fabricating a verdict, and the review was re-run as a contribution-mode job (the seller forks the repo and reads it directly).

🤖 Generated with Claude Code

@vercel

vercel Bot commented Aug 13, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
agicash Ready Ready Preview Aug 14, 2026 3:32pm

Request Review

@supabase

supabase Bot commented Aug 13, 2026

Copy link
Copy Markdown

This pull request has been ignored for the connected project hrebgkfhjpkbxpztqqke because there are no changes detected in supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

Comment thread packages/wallet-sdk/domain/receive/receive-api.ts Outdated
ditto-agent added a commit that referenced this pull request Aug 14, 2026
Review follow-up (#1176 r3784641517): getLightningQuote and createQuote took
an accountId and fetched the account per call — a Supabase read of the account
plus all unspent proofs, proof decryption, and a fresh wallet init (three mint
HTTP requests), paid twice per receive flow. The caller already holds the
account, so the params now take CashuAccount directly and the internal
getCashuAccount lookup is gone. The not-found and non-cashu runtime guards
became compile-time; fetch-by-id remains only for dark paths that work off
quote.accountId.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ditto-agent added a commit that referenced this pull request Aug 14, 2026
The step-9 review (#1176 r3784641517) caught host-facing receive methods
taking an accountId and re-fetching the account per call, against the
no-cache design's premise that only background/orchestrator work gains DB
reads. The premise was implicit; nothing in the contract conventions
constrained param shapes, so the slice plan drifted. This makes it binding:
a new convention bullet in the contract proposal and a foreground-parity
corollary in the production design, both stating that host-initiated methods
take caller-held domain objects and that fetch-by-id is reserved for
background work and server routes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ditto-agent added a commit that referenced this pull request Aug 14, 2026
…sing line

From the #1176 review discussion: createReceiveApi threads the accounts
bridge only to satisfy repository constructor deps used by processPayment/
completeReceive — verbs the host surface can never reach, background-only
per the contract conventions. The split is deferred to step 18, when the
background domain (the sole consumer of the processing verbs) lands and its
grounding fixes the exact shape; slices 10-15 keep wrapping the bundled
classes until then.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Wraps the cashu receive quote domain in the sdk.receive.cashu contract
namespace and flips web quote creation and tracking off
@agicash/wallet-sdk/temporary.

- createReceiveApi returns the full ReceiveApi: cashu implemented
  (getLightningQuote/createQuote/getQuote), spark/cashuToken as throwing
  getters for steps 11/12; session-fenced per the accounts pattern
  (requireUserId + sessionSignal pre/post checks); first SDK-side
  CashuCryptography assembly from session keys.
- Params take the caller-held CashuAccount (review r3784641517): the SDK
  does no per-call account fetch (was a proofs-inclusive read + wallet
  init, twice per flow); the not-found/non-cashu runtime guards became
  compile-time. Codified as a contract convention (proposal 'Conventions
  across all namespaces') with a foreground-parity corollary in the
  production design; the step-18 bullet gains the host/processing
  repo+service split from the same review discussion.
- CashuReceiveQuoteService.createReceiveQuote gains optional
  { abortSignal }, threaded from the api; in-package callers unchanged.
- receiveType is pinned 'LIGHTNING' in the api; CASHU_TOKEN quotes stay
  in-package (steps 12/16/17).
- Web flip: useCreateCashuReceiveQuote and useTrackCashuReceiveQuote on
  sdk.receive.cashu.*; the background processor, change handlers, and
  pending reads stay on /temporary until step 18.
- temporary.ts sheds CashuReceiveQuoteSchema, computeTotalFee, and
  deriveNut20LockingPublicKey re-exports.
- receive-api test suite: session fences (pre/mid/post), crypto parity
  (locking key derives from the session cashu locking xpub), contract
  passthrough.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@ditto-agent
ditto-agent force-pushed the sdk/cashu-receive-quote-slice branch from a6b8a9b to 5004d67 Compare August 14, 2026 15:31
@jbojcic1
jbojcic1 merged commit 056cbfc into master Aug 14, 2026
6 checks passed
@jbojcic1
jbojcic1 deleted the sdk/cashu-receive-quote-slice branch August 14, 2026 15:32

This branch was successfully deployed

1 active deployment
Preview — 5004d676 Deployed Aug 14, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants