Skip to content

[experiment] feat(wallet-sdk): cashu receive swap slice (step 10, solo arm) - #1177

Closed
ditto-agent wants to merge 4 commits into
masterfrom
experiment/s10-solo
Closed

ditto-agent wants to merge 4 commits into
masterfrom
experiment/s10-solo

Conversation

@ditto-agent

Copy link
Copy Markdown
Contributor

Experiment note: this PR is the solo arm of a delegation-measurement experiment (same slice as the planned step-10 marketplace arm). It exists for comparison and is not for merge. Base: master at 056cbfc (step 9).

Step 10 of the no-cache extraction (production design): wraps the cashu receive swap domain in the sdk.receive.cashu contract namespace and flips the web same-mint token claim off @agicash/wallet-sdk/temporary.

Plan: docs/superpowers/plans/2026-08-18-wallet-sdk-cashu-receive-swap-slice.md

What changed

SDK (packages/wallet-sdk)

  • domain/sdk/receive.ts: ReceiveApi.cashu gains createSwap(params): Promise<CreatedCashuReceiveSwap> with CreateCashuReceiveSwapParams; @throws documents the DomainError validations and the UniqueConstraintError duplicate claim. The spark/cashuToken placeholders (steps 11/12) stay untouched.
  • domain/receive/receive-api.ts: swap repository/service builders (mirroring the quote ones, with createSwapRepository/createSwapService test seams) and the session-fenced createSwap implementation — requireUserId() → signal capture → service build → pre-check → create(..., { abortSignal }) → post-check.
  • domain/receive/cashu-receive-swap-service.ts: create gains an optional options?: { abortSignal? } second param, threaded to the repository (which already accepted it); the three pre-write validation throws become DomainError — the path is public contract now, and only DomainError.message is user-displayable. In-package callers (claim-cashu-token-service.ts, cashu-send-swap-service.ts) compile unchanged; the dark claim path's DomainError catch now surfaces the specific reason.
  • domain/receive/receive-api.test.ts: 6 new cashu.createSwap tests — no-session fence against live default builders, contract passthrough (params + result verbatim), default swap-service assembly over an injected repository (input/fee/output amounts + signal threading), session-end during service construction (service never called), session-end during the write, and signal-threading identity. makeApi injects seams only when a test supplies them.
  • temporary.ts: sheds three dead swap-domain re-exports (CashuSwapReceiveDbData, CashuSwapReceiveDbDataSchema, CashuReceiveSwapSchema) — zero repo-wide importers.

Web (apps/web-wallet)

  • features/receive/cashu-receive-swap-hooks.ts: useCreateCashuReceiveSwap resolves the account from the accounts cache and calls sdk.receive.cashu.createSwap({ token, account }). The hook's { token, accountId } interface is unchanged, so receive-cashu-token.tsx is untouched.

Docs

  • docs/superpowers/specs/2026-07-02-wallet-sdk-contract-proposal.md: the ReceiveApi sketch gains the createSwap line, so spec and shipped contract do not drift.
  • New plan doc for the slice.

Design decisions

  1. Contract home receive.cashu.createSwap — the entity is a cashu-rail receive; rails nest where flows diverge, and the send-side sketch already places createSwap under send.cashu.
  2. Caller-held full account: CashuAccount (contract convention set by the step-9 review, feat(wallet-sdk): cashu receive quote slice (step 9) #1176) — the SDK does no per-call account fetch; the web resolves the account from its own cache (a cache read, zero added network). Verified in the smoke: no account read precedes the create RPC.
  3. Result { swap, account } (CreatedCashuReceiveSwap) — the create_cashu_receive_swap RPC advances the account keyset counter, so the updated account rides back on the same call. The web keeps using only swap.transactionId (parity). Named Created… because db/database.ts's private RPC-result family owns the Create*Result names.
  4. reversedTransactionId stays in-package — only the send-swap reversal (step 14) and the claim orchestration (step 12) pass it; same move as step 9 pinning receiveType: 'LIGHTNING'.
  5. No read surface this slice — the only foreground swap read is the debug details page (getByTransactionId), deliberately left repo-level for the quote domain too in step 9.
  6. Money-path boundary intact — the background processor, change handlers, and pending reads stay on /temporary until step 18; no host/processing repo+service split before step 18 (production-design bullet).

Verification

  • bun run fix:all — exit 0
  • bun run typecheck — all 9 workspace packages exit 0
  • bun run test — green: wallet-sdk 179 pass (19 in receive-api suite, 5 new), web-wallet 38 pass, libs green
  • Review pass (high-effort adversarial review over the full diff, 10 finder angles + verification sweep): 11 findings. Fixed in 9744d8a: bare validation Errors on the public path → DomainError; duplicate-claim + validation throws documented on the contract; public result type renamed off the db layer's private Create*Result family; JSDoc stopped naming the not-yet-emitted cashu-receive-swap.updated event; two comments reworded per the repo comment policy; the no-session fence test made meaningful (live default builders); the default swap-service assembly gained real coverage; dead seam stubs removed. Deferred with rationale: (1) an abort during the DB write surfaces the repository's generic Error instead of SessionEndedError — inherited from the step-9 fence idiom, same in createQuote; a uniform fix belongs to a dedicated fence/error pass, not one namespace; (2) NoSessionError is not a root export — the contract proposal's root-error list deliberately omits it; a contract-level decision; (3) the session-fence skeleton now has a 4th copy in this file — extraction is a cross-cutting cleanup the production design should schedule, not a per-slice move.
  • Live smoke (local stack, testnut): minted a fresh 21-sat token at https://testnut.cashu.space, opened /receive/cashu/token#…, claimed to the same-mint "Testnut BTC" account. Network trace shows the flipped path end-to-end: rpc/create_cashu_receive_swap (200, no preceding account fetch) → mint POST /v1/swap (200, web processor via /temporary) → rpc/complete_cashu_receive_swap (200). Transaction page shows Received ₿20, Completed (21 − 1 sat mint fee); home balance 21 → 41 sats; zero console errors.

🤖 Generated with Claude Code

ditto-agent and others added 4 commits August 18, 2026 16:27
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds createSwap to the sdk.receive.cashu namespace: the host-facing
same-mint token claim, session-fenced per the step-9 template.

- CreateCashuReceiveSwapParams take the caller-held CashuAccount
  (contract convention set by the step-9 review, #1176); the result is
  the service's { swap, account } shape — the create RPC advances the
  account keyset counter, so the updated account rides back on the same
  call.
- reversedTransactionId stays in-package: only the send-swap reversal
  (step 14) and the claim orchestration (step 12) pass it.
- CashuReceiveSwapService.create gains optional { abortSignal },
  threaded to the repository; in-package callers unchanged.
- receive-api tests: no-session and session-end fences (pre/mid/post),
  contract passthrough with the result returned verbatim.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…hu (step 10)

useCreateCashuReceiveSwap resolves the account from the accounts cache
and calls sdk.receive.cashu.createSwap; the hook's { token, accountId }
interface is unchanged, so receive-cashu-token.tsx is untouched. The
background processor, change handlers, and pending reads stay on
/temporary until step 18; the send-swap ctor dep (step 14), the claim
route graph (step 12), and the details-page read keep their imports.

temporary.ts sheds the dead swap-domain re-exports
(CashuSwapReceiveDbData, CashuSwapReceiveDbDataSchema,
CashuReceiveSwapSchema); the contract-proposal ReceiveApi sketch gains
the createSwap line.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- The three create validation throws become DomainError: the path is
  public contract now, and only DomainError.message is user-displayable.
  The dark claim path's DomainError catch surfaces the specific reason
  instead of the generic branch; the web toast text is unchanged.
- Contract: createSwap documents its throws (DomainError validations,
  UniqueConstraintError duplicate claim); the result type is renamed to
  CreatedCashuReceiveSwap — db/database.ts's private RPC-result family
  owns the Create*Result names; the result JSDoc stops naming the
  cashu-receive-swap.updated event, which has no emit sites until
  step 18.
- receive-api comments reworded per the repo comment policy.
- Tests: the no-session fence now runs against live default builders so
  its no-repository-work assertion bites; the default swap-service
  assembly is covered over an injected repository (input/fee/output
  amount computation and signal threading); makeApi injects seams only
  when a test supplies them; unreachable inline stubs removed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Aug 18, 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 18, 2026 3:26pm

Request Review

@supabase

supabase Bot commented Aug 18, 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 ↗︎.

This branch was successfully deployed

1 active deployment
Preview — 9744d8ae Deployed Aug 18, 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.

1 participant