Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 5 additions & 10 deletions apps/web-wallet/app/features/receive/cashu-receive-quote-hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import {
} from '@tanstack/react-query';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useAccountRepository } from '~/features/accounts/account-repository-hooks';
import { sdk } from '~/features/shared/sdk.client';
import { useOnMeltQuoteStateChange } from '~/lib/cashu/melt-quote-subscription';
import { MintQuoteSubscriptionManager } from '~/lib/cashu/mint-quote-subscription-manager';
import { useLatest } from '~/lib/use-latest';
Expand Down Expand Up @@ -182,8 +183,6 @@ export function usePendingCashuReceiveQuotesCache() {
}

export function useCreateCashuReceiveQuote() {
const userId = useUser((user) => user.id);
const cashuReceiveQuoteService = useCashuReceiveQuoteService();
const cashuReceiveQuoteCache = useCashuReceiveQuoteCache();

return useMutation({
Expand All @@ -197,16 +196,13 @@ export function useCreateCashuReceiveQuote() {
purpose,
transferId,
}: CreateProps) => {
const lightningQuote = await cashuReceiveQuoteService.getLightningQuote({
wallet: account.wallet,
const lightningQuote = await sdk.receive.cashu.getLightningQuote({
account,
amount,
description,
});

return cashuReceiveQuoteService.createReceiveQuote({
userId,
return sdk.receive.cashu.createQuote({
account,
receiveType: 'LIGHTNING',
lightningQuote,
purpose,
transferId,
Expand Down Expand Up @@ -248,12 +244,11 @@ export function useTrackCashuReceiveQuote({
const enabled = !!quoteId;
const onPaidRef = useLatest(onPaid);
const onExpiredRef = useLatest(onExpired);
const cashuReceiveQuoteRepository = useCashuReceiveQuoteRepository();

const { data } = useQuery({
queryKey: [CashuReceiveQuoteCache.Key, quoteId],
// biome-ignore lint/style/noNonNullAssertion: quoteId is guaranteed by enabled
queryFn: () => cashuReceiveQuoteRepository.get(quoteId!),
queryFn: () => sdk.receive.cashu.getQuote(quoteId!),
staleTime: Number.POSITIVE_INFINITY,
refetchOnWindowFocus: 'always',
refetchOnReconnect: 'always',
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,14 @@ reads the DB instead of an in-memory copy — negligible. In exchange the SDK st
simple: no resident store, no cache-coherence/version-gating, no embedded query
engine to keep in sync.

**Corollary (foreground parity):** the "only effect" claim above is a rule, not
an observation — a flipped web flow must issue no additional network requests
versus master. Host-facing contract methods therefore take the domain objects
the caller already fetched (`account: CashuAccount`, not `accountId`); fetch-by-id
inside the SDK is reserved for background/orchestrator work and server routes,
where no caller state exists. Binding form: contract proposal → "Conventions
across all namespaces". (Codified after the step-9 review, #1176.)

## Boundary mechanism (the spine)

- `packages/wallet-sdk` holds the domain layer: `*-repository`, `*-service`,
Expand Down Expand Up @@ -89,6 +97,14 @@ is **step 2 (cycle-break) before step 3 (move)**.
1. move the shared runtime (task runner + leader-election lock + change-feed) into the SDK behind the contract; web calls `sdk.background.start/stop`;
2. port each flow's processor into the SDK one at a time;
3. web drops its realtime layer and subscribes to SDK events to keep its cache fresh.
- **Split the receive/send repos + services along the host/processing line**
(decided in the #1176 review discussion): the background-only verbs
(`processPayment`/`completeReceive`/`expire`/`fail`/`markMeltInitiated` and
their peers) and the account row-mapper dependency move to the background
domain; host-facing construction (`createReceiveApi` and peers) keeps only
create/read with db + encryption deps. Ends the accounts-bridge threading
in host APIs; may shrink the `.server` repo twins. Row-mapping (`toQuote`,
`toAccount`) stays in one shared place so the halves cannot drift.
- Live money-path verification is mandatory here.
19. **Cleanup** — delete `@agicash/wallet-sdk/temporary`; route remaining consumers through the contract. Boundary now build-enforced.

Expand Down
10 changes: 10 additions & 0 deletions docs/superpowers/specs/2026-07-02-wallet-sdk-contract-proposal.md
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,16 @@ Conventions across all namespaces:
`create*` methods persist and enter the entity into the background lifecycle
(`createQuote`, `createSwap`, `accounts.cashu.add`). A slice never re-decides
which is which.
- **Callers pass the domain objects they already hold.** Host-initiated methods
take the full entity (`account: CashuAccount`), never an id the SDK re-fetches.
The host fetched the entity from its own cache, and the no-cache premise
(production design, "Core decision") holds only if foreground flows add no
reads — an in-SDK `accounts.get` costs a proofs-inclusive row read plus a
fresh wallet init (three mint HTTP requests) per call. Fetch-by-id is reserved
for paths with no caller state: background/orchestrator work (off
`quote.accountId` and the like) and server routes. Per-slice check: a flipped
web flow issues no additional network requests versus master. (Set by the
step-9 review, #1176; slices 10–16 follow it. A slice never re-decides this.)
- **Completion is not the host's job.** Methods like `expire`/`fail`/
`completeSwap` that today's hooks call from background processors do NOT
appear on the public namespaces — they move behind `sdk.background`
Expand Down
37 changes: 22 additions & 15 deletions packages/wallet-sdk/domain/receive/cashu-receive-quote-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export class CashuReceiveQuoteService {
*/
async createReceiveQuote(
params: CreateQuoteParams,
options?: { abortSignal?: AbortSignal },
): Promise<CashuReceiveQuote> {
const {
userId,
Expand Down Expand Up @@ -93,24 +94,30 @@ export class CashuReceiveQuoteService {
};

if (receiveType === 'CASHU_TOKEN') {
return this.cashuReceiveQuoteRepository.create({
...baseParams,
receiveType,
meltData: {
tokenMintUrl: params.sourceMintUrl,
tokenAmount: params.tokenAmount,
tokenProofs: params.tokenProofs,
meltQuoteId: params.meltQuoteId,
cashuReceiveFee: params.cashuReceiveFee,
lightningFeeReserve: params.lightningFeeReserve,
return this.cashuReceiveQuoteRepository.create(
{
...baseParams,
receiveType,
meltData: {
tokenMintUrl: params.sourceMintUrl,
tokenAmount: params.tokenAmount,
tokenProofs: params.tokenProofs,
meltQuoteId: params.meltQuoteId,
cashuReceiveFee: params.cashuReceiveFee,
lightningFeeReserve: params.lightningFeeReserve,
},
},
});
options,
);
}

return this.cashuReceiveQuoteRepository.create({
...baseParams,
receiveType,
});
return this.cashuReceiveQuoteRepository.create(
{
...baseParams,
receiveType,
},
options,
);
}

/**
Expand Down
Loading
Loading