With cache SDK migration wip#1155
Draft
pmilic021 wants to merge 78 commits into
Draft
Conversation
…ckage
Move the framework-free money/, json, and zod utilities out of
apps/web-wallet/app/lib into a new @agicash/utils leaf package so the upcoming
@agicash/wallet-sdk can depend on them without importing from the web app.
- packages/utils: new @agicash/-scoped, source-linked package (money, json, zod).
exports use a "./*": "./src/*.ts" wildcard plus an explicit "./money" entry
(the wildcard cannot index-resolve the money/ directory under Bundler
resolution; the bare "." root also needs its own entry)
- money/json/zod moved byte-identical via git mv; logic untouched (money is
financial code)
- old ~/lib/{money,json,zod} paths kept as transitional re-export shims so the
~80 consumers stay untouched; a later cleanup PR rewrites the imports and
deletes the shims
- big.js, @types/big.js, zod hoisted into the root workspaces.catalog (now
shared by >=2 packages) and referenced via catalog:
- web-wallet now depends on @agicash/utils (workspace:*)
Phase 0 of the wallet-sdk extraction. typecheck, biome, tests (utils 14, web 119)
and full build all green; zero web behavior change.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…cash/db-types Move the generated supabase/database.types.ts into a new @agicash/db-types leaf package so the upcoming @agicash/wallet-sdk can consume the DB schema types without importing from the web app or relying on a cross-package tsconfig alias. - packages/db-types: new @agicash/-scoped, source-linked, zero-dependency package exporting the generated Database / Json / Tables / ... types - supabase/database.types.ts -> packages/db-types/src/database.types.ts (byte-identical move via git mv) - db:generate-types script and the CI type-drift check now write/diff the new path - biome "ignore" updated so the generated file stays unlinted - removed the web app's "supabase/database.types" tsconfig path alias; the two importers (agicash-db/database.ts, transaction-details-types.ts) now import from @agicash/db-types directly (only 2 sites, so a direct repoint is cleaner than a cross-package alias shim) - web-wallet depends on @agicash/db-types (workspace:*) The augmented Database type (agicash-db/database.ts) and the json-models stay in the web app for now: they depend on not-yet-extracted ~/lib/cashu and the send feature, so they move when those are extracted in later phases. Phase 0 of the wallet-sdk extraction. typecheck, biome, tests (web 119, utils 14) and full build all green; zero web behavior change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…icash/db-types
Move agicash-db/database.ts (the MergeDeep-augmented Database with wallet RPC
return/composite types, the AgicashDb* row types, and the isCashuAccount/isSparkAccount
guards) plus the 2 pure-zod account-detail json-models it needs into @agicash/db-types,
so the SDK's repositories can consume the DB type layer without importing from the web app.
- database.ts + json-models/{cashu,spark}-account-details-db-data.ts -> packages/db-types/src/
(byte-identical git mv)
- package index re-exports the AUGMENTED Database (from ./database) + the generated helper
types (Json/Tables/... from ./database.types) WITHOUT the generated Database, to avoid a
name clash
- old agicash-db/database.ts + the 2 account-detail json-models kept as transitional
re-export shims (35 + 8 importers untouched)
- db-types gains deps zod, @supabase/supabase-js, type-fest (latter two hoisted to the root
catalog, now shared by web + db-types)
Phase 0 of the wallet-sdk extraction. typecheck, biome, tests (web 119, utils 14) and full
build all green; zero web behavior change. The 11 remaining json-models stay in the app for
now (they depend on ~/lib/cashu ProofSchema / the send feature — handled in the cashu lib-ify
and send phases).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…o @agicash/cashu First slice of lib-ifying ~/lib/cashu: move the pure, framework-free protocol files into a new @agicash/cashu leaf package so the SDK cashu domain can consume them without dragging React in. - error-codes, types (ProofSchema), secret, proof, token, payment-request, protocol-extensions, blind-signature-matching (+ their 5 tests) -> packages/cashu/src (byte-identical git mv; only types.ts/secret.ts rewired to @agicash/utils for zod/json) - the @agicash/cashu barrel deliberately exports NO React (the lib's framework-free contract) - old ~/lib/cashu/<file> paths kept as transitional re-export shims so the web barrel and the ~20 consumers stay untouched - @cashu/cashu-ts, @noble/hashes, @noble/curves hoisted to the root catalog (now shared by web + @agicash/cashu); cashu-ts pinned so the lib and web can't drift on the subclass overrides - @agicash/cashu deps: @agicash/utils, @cashu/cashu-ts, @noble/hashes, zod Phase 0. typecheck, biome, build all green; tests preserved (133 total: web 84, utils 14, cashu 35); framework-free gate verified. Still in ~/lib/cashu (Chunk C): the utils.ts/wallet split, mint-validation, the 2 subscription managers; staying in web: the React hook + animated-qr. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…s into @agicash/cashu
Completes the framework-free @agicash/cashu lib (Chunk C of the cashu lib-ify):
- utils.ts (ExtendedCashuWallet, getCashuWallet, unit/keyset/mint-url helpers),
mint-validation.ts, and the melt-quote/mint-quote subscription MANAGER classes
-> packages/cashu/src (byte-identical git mv)
- the managers now import getCashuWallet from concrete ./utils (NOT the ~/lib/cashu barrel,
which re-exports a React hook) and isSubset from @agicash/utils, so the lib stays React-free
- isSubset relocated from app/lib/utils.ts into @agicash/utils (single source); app/lib/utils.ts
re-exports it so ~/lib/utils consumers (the proof-state manager) are untouched
- cashu utils money types now from @agicash/utils/money; type-fest added to @agicash/cashu
- old ~/lib/cashu/{utils,mint-validation,*-subscription-manager} kept as transitional shims
- proof-state-subscription-manager.ts (features/send): added a note that it likely belongs in
@agicash/cashu too, deferred to the send/swap phase (it depends on send-domain types)
Web keeps the React hook (useOnMeltQuoteStateChange) + animated-qr. typecheck, biome, build all
green; 133 tests preserved; FRAMEWORK-FREE GATE verified (zero react/@tanstack/DOM in
@agicash/cashu).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…cash/db-types Now that ProofSchema lives in @agicash/cashu, the remaining agicash-db json-models (jsonb column schemas) can move into @agicash/db-types where they belong: - cashu-lightning-receive, cashu-swap-receive, cashu-swap-send, cashu-token-melt, spark-lightning-receive, spark-lightning-send -> packages/db-types/src/json-models (byte-identical git mv; ~/lib/money -> @agicash/utils/money, ~/lib/cashu ProofSchema -> @agicash/cashu) - old paths kept as transitional re-export shims so the json-models barrel + its 15 consumers stay untouched - db-types now depends on @agicash/cashu (ProofSchema) + @agicash/utils (Money) — this coupling already existed in the app; the package graph now just reflects it. db-types tsconfig gains the DOM lib (it transitively pulls in money.ts's devtools-formatter window use) Staying in the app for their own phases: cashu-lightning-send-db-data (needs the send-domain DestinationDetailsSchema) and account-details-db-data (account-domain combined schema). Phase 0. typecheck, biome, build all green; 133 tests preserved. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The SDK now owns the single TanStack QueryClient the web app uses — the riskiest seam of the extraction — proven behavior-neutral before any domain logic moves. - @agicash/wallet-sdk gains getQueryClient() (packages/wallet-sdk/src/query-client.ts): the exact isServer/browser-singleton pattern from the web, now on @tanstack/query-core (pinned to 5.90.20 = the version @tanstack/react-query depends on, incl. the workspace patch — so this QueryClient is the SAME class react-query's hooks use and the web can mount it directly) - web's features/shared/query-client.ts is now a thin re-export of the SDK's getQueryClient, so all ~16 call sites (root provider, the supabase-session module-load capture, loaders, auth/user/cashu) are untouched and the browser singleton has a single home in the SDK - web now declares @agicash/wallet-sdk (workspace:*); also declares @agicash/cashu, which the Phase-0 cashu shims import but the app had never declared (phantom-dep fix) - SDK test locks the per-request (server) vs singleton (browser) contract typecheck, biome, build all green; 135 tests (web 84 + utils 14 + cashu 35 + sdk 2); zero web behavior change; @agicash/wallet-sdk is framework-free (query-core only). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…cates) (Phase 2.1)
First slice of the accounts-domain extraction: move the framework-free leaf files into
@agicash/wallet-sdk/src/accounts so the SDK owns the account types/predicates the rest of the
domain (and its consumers) build on.
- account.ts (Account/CashuAccount/SparkAccount types + schemas + predicates
canSend/ReceiveFromLightning + getAccountBalance), cashu-account.ts (CashuProofSchema, toProof),
account-cryptography.ts (BIP-85 derivation path) -> packages/wallet-sdk/src/accounts
(byte-identical git mv; ~/lib/cashu -> @agicash/cashu, ~/lib/money -> @agicash/utils,
agicash-db/json-models -> @agicash/db-types)
- getAccountHomePath stays in web (it returns app route strings — web-only, meaningless to a
non-React SDK consumer); the web account.ts shim re-exports the SDK + defines it locally
- old account/cashu-account/account-cryptography paths kept as transitional re-export shims so
the ~31 consumers stay untouched
- SDK deps: @agicash/{cashu,db-types,utils} (workspace), @agicash/breez-sdk-spark (hoisted to the
catalog), @cashu/cashu-ts, type-fest, zod
Phase 2. typecheck, biome, build all green; 135 tests; @agicash/wallet-sdk still framework-free.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fold ~/lib/ecies (pure ECIES over @noble) into @agicash/utils so the SDK's encryption layer (next chunk) can use it without importing from the web app. Kept in @agicash/utils rather than a separate package per the decision to split later only if needed. - ecies/{ecies,index,ecies.test}.ts -> packages/utils/src/ecies (byte-identical git mv; no import changes — only @noble imports) - @agicash/utils exports a "./ecies" subpath + root re-export; gains @noble/ciphers/curves/hashes (ciphers hoisted to the catalog; curves/hashes already there) - old ~/lib/ecies path kept as a transitional re-export shim (its one consumer, shared/encryption.ts, untouched) typecheck, biome, build all green; 135 tests (utils now 32: money + ecies). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Move the framework-free encryption layer out of features/shared/encryption.ts into
@agicash/wallet-sdk — the first connection-core piece the SDK owns on the path to backing the
accounts domain with SDK-owned connections.
- packages/wallet-sdk/src/encryption.ts: ECIES wrappers (encryptToPublicKey/
decryptWithPrivateKey + batch variants), the serialization rules (Date/undefined/non-finite/
Money preservation), the Encryption type, getEncryption, and the opensecret-backed key
derivation queryOptions (encryptionPrivateKey/PublicKey) — moved verbatim except:
- imports repointed (~/lib/ecies -> @agicash/utils/ecies, ~/lib/money -> @agicash/utils/money)
- the two queryOptions are now PLAIN query-options objects (the react-query queryOptions()
helper is a typed identity fn — runtime-identical; web's useSuspenseQuery consumes them
unchanged, per the SDK's query-core-types-only architecture)
- web's features/shared/encryption.ts keeps ONLY the React hooks (useEncryption,
useEncryptionPrivateKey/PublicKeyHex), now consuming the SDK; everything else re-exported
(all 12 consumer files untouched)
- SDK gains @agicash/opensecret (catalog) — the single TRACKED EXCEPTION to the framework-free
rule (transitive react peer-dep + localStorage), documented in package.json
dependencies:comments; removed by the in-flight storage-pluggable opensecret bump before the
MCP phase. No DIRECT react/DOM imports in SDK src (gate verified).
- SDK barrel now documents the queryOptions-read pattern + the deferred Query<T>/MCP plan
- new encryption.test.ts locks the encrypt/decrypt roundtrip + the serialization rules
typecheck, biome, build all green; 138 tests (sdk 5, web 84, utils 32, cashu 35).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…r (connection-core step 3)
The SDK now owns the RLS-scoped Supabase connection — the third connection-core piece on the
path to backing the accounts domain with SDK-owned connections.
- src/supabase-session.ts (moved verbatim from agicash-db/supabase-session.ts): the
opensecret generateThirdPartyToken query with its dynamic JWT-exp staleTime, plus the
module-load getQueryClient() capture — which now resolves INSIDE the SDK (same singleton,
no more cross-package capture). FetchQueryOptions now typed via @tanstack/query-core.
- src/auth.ts (moved verbatim from features/shared/auth.ts): isLoggedIn — it reads the token
keys the OpenSecret client writes, so it is opensecret-session logic; its direct
window.localStorage read is folded into the same tracked exception as the opensecret
dependency (switches to the StorageAdapter on the storage-pluggable bump).
- src/agicash-db.ts (new factory): createAgicashDb({url, anonKey}) — the createClient call
moved verbatim (schema 'wallet', session-token accessToken, the production payload-redacting
realtime logger). The consumer supplies connection params.
- web's database.client.ts keeps what is genuinely web: import.meta.env reading, the dev-LAN
URL rewrite (window.location), the SupabaseRealtimeManager and the window debug attach —
and now builds agicashDbClient via the SDK factory. shared/auth.ts +
agicash-db/supabase-session.ts are transitional re-export shims (all consumers untouched;
_protected.tsx's direct supabase-session import covered).
- SDK gains @supabase/supabase-js (catalog) + jwt-decode (hoisted to catalog, now shared)
typecheck, biome, build all green; 138 tests; no direct react/DOM imports in SDK src beyond
the documented opensecret/localStorage exception.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…connection-core step 4a) The SDK now owns the cashu connection layer the AccountRepository depends on. - src/cashu.ts (split out of features/shared/cashu.ts, bodies verbatim): the locking derivation path, tokenToMoney, CashuCryptography + seed/xpub/privateKey queryOptions + getCashuCryptography (opensecret + @Scure), getTokenHash, getMintAuthProvider, the mint info/keysets/keys queryOptions + keys, decodeCashuToken, and getInitializedCashuWallet (offline handling + keychain cache load). queryOptions() helper calls converted to plain query-options objects (typed identity — runtime-identical). - src/agicash-mint-auth-provider.ts (moved verbatim): the NUT-21 CAT AuthProvider; its module-load getQueryClient capture + isLoggedIn now resolve inside the SDK. - src/performance.ts (new): a minimal instrumentation seam — measureOperation delegates to a registered measurer (default pass-through); entry.client.tsx registers the web's Sentry-backed implementation at startup so the moved operations keep emitting the same Sentry spans (no server-side callers of the moved fns exist today). - web keeps in features/shared/cashu.ts what is genuinely web: the env-derived cashuMintValidator (import.meta.env blocklist) and the useCashuCryptography React hook, plus a re-export of the SDK module (all consumers untouched). - computeSHA256 -> @agicash/utils/sha256 (pure Web Crypto; web lib/sha256.ts is a shim; used by the moved getTokenHash and later by spark). - SDK gains @scure/bip32 + @scure/bip39 (hoisted to the catalog; web flips to catalog:). typecheck, biome, build all green; 138 tests; no direct react/DOM in SDK src beyond the documented opensecret/localStorage exception. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The SDK now owns the spark/Breez connection layer — the last connection piece the
AccountRepository depends on.
- src/spark.ts (split out of features/shared/spark.ts, bodies verbatim): sparkDebugLog,
the Breez initLogging once-guard, the mnemonic/identity-pubkey/wallet queryOptions
(converted to plain query-options objects) and getInitializedSparkWallet (offline stub
handling). The two web bindings are replaced by a configureSpark({apiKey,
isDebugLoggingEnabled}) seam: the web supplies the env-derived Breez API key and the
per-call DEBUG_LOGGING_SPARK feature-flag check; log formats unchanged.
- src/spark-utils.ts (moved verbatim from lib/spark/utils.ts): the Breez external signer
identity-pubkey derivation + the offline wallet stub. lib/spark keeps errors.ts + wasm.ts
(their consumers move in later phases) and re-exports the moved utils.
- web's features/shared/spark.ts keeps what is web: the eager VITE_BREEZ_API_KEY read/throw,
the configureSpark call (module load — same ordering + fail-fast as today, and the
server-side lightning-address path configures identically since it imports via this module),
and the useTrackAndUpdateSparkAccountBalances React hook. Everything else re-exported (all
consumers untouched).
- sparkDebugLog being SDK-owned dissolves the previously planned AccountsCache logger
injection: when the cache moves (next chunk), it imports the SDK logger directly.
typecheck, biome, build all green; 138 tests; no new SDK deps.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…shDb/getAgicashDb) The SDK now owns the DB client INSTANCE, not just its construction. The web supplies the env-derived connection params via configureAgicashDb at module load and re-exports getAgicashDb() as the transitional agicashDbClient for not-yet-migrated repositories (removed in the import-cleanup PR). createAgicashDb becomes module-private. Once the Sdk composition root lands, the configure/get pair is absorbed into it — the raw client is never part of the public API. typecheck, biome, build green; 138 tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ns (Phase 2.5)
AccountRepository, AccountService, AccountsCache + accountsQueryOptions + the realtime
change-handler factory move into @agicash/wallet-sdk — the accounts domain now runs entirely
on connections the SDK already owns (db, encryption, cashu/spark wallet init, queryClient).
- src/error.ts (moved verbatim from shared/error.ts): DomainError/ConcurrencyError/
NotFoundError/UniqueConstraintError + getErrorMessage — core domain primitives the
repository throws; web shim keeps the 37 consumers untouched.
- src/accounts/account-repository.ts (method bodies verbatim): constructor switches to a
typed AccountRepositoryDeps object (destructured into the same private fields); the
wallet-init/auth-provider imports become plain SDK module imports ('../cashu', '../spark')
— the injection promotion previously planned is no longer needed.
- src/accounts/account-service.ts (bodies verbatim): deps object; allMintKeysetsQueryOptions
imported from '../cashu' directly; the User param narrows to a structural
UserDefaultAccounts type (web's User satisfies it; replaced when the user domain moves).
- src/accounts/accounts-cache.ts: AccountsCache (version-guarded upsert + spark balance
write-guard, verbatim), accountsQueryOptions (plain query-options object; staleTime
Infinity + the load-bearing structuralSharing preserved verbatim with its comment), and
createAccountChangeHandlers (the two realtime closures verbatim).
- src/spark-config.ts split out of spark.ts (configureSpark/getSparkConfig/sparkDebugLog) so
light consumers like the cache don't pull the Breez connection module into their graph;
spark.ts re-exports it (web surface unchanged).
- web keeps the React layer: useAccountRepository/useAccountService wiring (deps objects),
useAccountsCache, useAccountChangeHandlers (wraps the factory), and all 16 read hooks
unchanged; account-hooks re-exports AccountsCache + accountsQueryOptions so its importers
are untouched. The two route construction sites switch to the deps-object shape.
- NEW src/accounts/accounts-cache.test.ts locks the load-bearing behaviors: version-guard
(older/equal/newer/append), balance write-guard (same-ref on no-op), structuralSharing
(preserves expired accounts, new wins on collision).
typecheck, biome, build all green; 146 tests (sdk 13, web 84, utils 32, cashu 35); no direct
react/DOM in SDK src beyond the documented opensecret exception.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…getSdk)
Pulled forward deliberately: the user wants fully-migrated domains validated against the real
end-state API before the pattern is replicated across more domains. The SDK now has its
composition root, and the accounts domain is consumed through it.
- src/sdk.ts: WalletSdkConfig + configureWalletSdk (absorbs the four scattered configure
seams — opensecret configure, configureAgicashDb, configureSpark, setOperationMeasurer;
server-safe: it only records config, connections stay lazy) + the WalletSdk class
(sdk.queryClient + sdk.accounts = {repository, service, cache, listOptions(userId),
changeHandlers}) + getSdk() (client-only lazy browser singleton; throws on the server and
before configuration).
- Lazy Encryption (createLazyEncryption): methods resolve the keys through the SDK's key
queryOptions (staleTime Infinity) on first use, so the root can construct domains
pre-login. Same Encryption interface; the repository is unchanged; the protected-route
prewarms keep key-fetch timing identical. Consumers no longer suspend on key queries via
useEncryption inside useAccountRepository — the keys resolve inside the first decrypt
instead (and are prewarmed by the _protected middleware before any account read).
- src/spark-config.ts gains setSparkDebugLogging — an order-independent debug-gate binding,
needed because the web's feature-flag module reads the DB client and would create an
import cycle if the SDK-configuration entrypoint imported it.
- web features/shared/sdk.ts (new): the single env-derived configureWalletSdk call (incl.
the dev-LAN supabase URL rewrite) + getSdk re-export. Evaluates on the server too (via the
spark/lightning-address chain), which also restores Sentry spans for SDK operations on the
server lnurlp path (gap from the 4a instrumentation seam).
- entry.client.tsx drops the opensecret configure + measurer block for a side-effect import
of shared/sdk; database.client.ts drops env reading and just re-exports getAgicashDb()
after importing shared/sdk; shared/spark.ts drops env/configureSpark and wires the
DEBUG_LOGGING_SPARK gate via setSparkDebugLogging.
- The accounts wiring collapses onto the root: useAccountRepository/useAccountService/
useAccountsCache/useAccountChangeHandlers are one-liners over getSdk().accounts.*;
useAccounts consumes sdk.accounts.listOptions(user.id); useAccountService loses its
queryClient param (single caller updated); the two route construction sites
(_protected.tsx middleware, receive.cashu_.token clientLoader) read
getSdk().accounts.repository/.service instead of constructing.
- sdk.test.ts: getSdk server-context throw + lazy-encryption roundtrip through a seeded
query cache.
typecheck x6, biome, build green; 148 tests (sdk 15).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…vice/cache become internal
App code must interact with accounts only through SDK interface methods — the internal
layering (repository = DB access, service = domain logic, cache = read-model writes) is no
longer reachable as the public surface.
- sdk.accounts public API: listOptions(userId), get(id) (DB fetch incl. expired accounts,
write-through into the accounts state), getCached(id), listCached(), add(params) (create
cashu account + immediate record — absorbs useAddCashuAccount's onSuccess upsert with the
same ordering).
- sdk.accounts.internal = { repository, service, cache, changeHandlers } — a documented,
grep-able transitional escape hatch ONLY for (a) not-yet-migrated SDK collaborators still
composed in web code (user/receive/send repositories+services, the claim flow, the
_protected user bootstrap) and (b) the web-owned realtime + spark-balance infrastructure
until the SDK owns realtime. It shrinks each phase.
- web cutover of every app-code touchpoint to curated methods:
- useAccountOrNull: cache-miss fetch+upsert -> sdk.accounts.getCached/get
- useAddCashuAccount: -> sdk.accounts.add (mutation wrapper unchanged)
- send-provider getAccounts + the send-route loader account matching -> listCached()
- usePendingMeltQuotes account lookup -> getCached()
- useAccountService deleted (no consumers left); useAccountRepository/useAccountsCache/
useAccountChangeHandlers remain but now return internal.* with transitional JSDoc, used
only by stranded domains and realtime infra.
- verified: git grep accounts.internal shows only the sanctioned transitional sites.
typecheck x6, biome, build green; 148 tests.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Status document for the extraction: settled architecture decisions, the per-chunk working method and gates, the full phase/branch/commit ledger, the remaining roadmap (user domain -> checkpoint -> remaining domains -> realtime hub -> auth shell -> import cleanup -> MCP boundary), and the landmines/nuances future work must not rediscover. Updated at the end of each phase. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Moves User, FullUser, GuestUser, UserProfile, UpdateUser, and the three predicate functions into the SDK under packages/wallet-sdk/src/user/user.ts. Replaces the structural UserDefaultAccounts workaround in AccountService with the real User type now that it lives in the SDK. Web user.ts becomes a transitional re-export shim. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…DK (Phase 3.2)
Adds packages/wallet-sdk/src/user/{user-repository,user-service,user-cache}.ts
with ReadUserRepository, WriteUserRepository, ReadUserDefaultAccountRepository,
UserService, UserCache, userQueryOptions, and createUserChangeHandlers — all
framework-free. ReadUserDefaultAccountRepository keeps its positional ctor: its
only consumer is the server-side lightning-address path which constructs it
per-request with the server db and env mnemonic.
Web shims at features/user/{user-repository,user-service} re-export the SDK
classes and keep the React hook wrappers for one more chunk. user-hooks.tsx
imports UserCache and createUserChangeHandlers from the SDK instead of
defining them locally; the cache-read helpers stay local until the curated
surface lands.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… touchpoints (Phase 3.3)
Adds UserApi to WalletSdk: queryOptions, getCached/getCachedOrThrow, upsert
(records user + accounts in both in-memory states via AccountsCache.set),
update, setDefaultAccount — the cache write-backs that hooks previously did
in onSuccess are absorbed into the curated methods. internal escape hatch
carries {readRepository, writeRepository, service, cache, changeHandlers}
for the stranded receive collaborators and the web-owned realtime wiring.
ReadUserDefaultAccountRepository is intentionally NOT wired into the root:
its only consumer is the server-side lightning-address path, which
constructs it per-request with the server db client and the LNURL server
env mnemonic — a root instance would bind the logged-in user's mnemonic.
Web migrations to the curated surface:
- _protected.tsx ensureUserData: getSdk().user.getCached() + .upsert();
no more direct repository construction or manual setQueryData calls
- useUser spreads getSdk().user.queryOptions() (local userQueryOptions and
useReadUserRepository deleted); useUpdateUser/useSetDefaultAccount call
sdk.user.update/.setDefaultAccount (useWriteUserRepository and
useUserService deleted, mirroring the accounts-phase useAccountService
removal)
- useUserCache/useUserChangeHandlers delegate to sdk.user.internal with
transitional JSDoc, mirroring the accounts hooks
- accept-terms, verify-email, and the receive-token route use
getSdk().user.getCachedOrThrow(); getUserFromCache(OrThrow) deleted
- receive-token route takes UserService from sdk.user.internal.service
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…POINT next Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…s caller policy (Phase 3.4) Checkpoint review feedback: the SDK can't know whether a missing cached user is exceptional (in the web's protected-layout guards it is; another consumer may poll bootstrap state), and the accounts surface exposes no orThrow variant either. The SDK keeps the single getCached(): User | null primitive; web's getUserFromCacheOrThrow (user-hooks) wraps it with the throw policy for the three protected-layout call sites. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… (Phase 3.5)
Checkpoint feedback: sdk.ts was ~300 lines and grows by a domain each
phase. Each domain now owns {domain}/{domain}-api.ts holding the *Api type
and a create*Api(deps) factory; sdk.ts shrinks to config + the composition
root (one create*Api call per domain).
createAccountsApi returns {api, repository, cache} so the root can wire
cross-domain deps (the user domain's WriteUserRepository and its upsert
accounts write-back) against the same instances without reaching through
the transitional `internal` escape hatch — the root must not depend on a
surface documented as removable.
createLazyEncryption moves to encryption.ts where its collaborators live;
package-root export is preserved via the existing barrel. AccountsApi and
UserApi stay exported from sdk.ts as type re-exports.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…state (Phase 3.6) Checkpoint feedback: the SDK is a single-user instance — the Supabase client is RLS-scoped to the logged-in session, so any userId other than the current user's cannot work. Requiring callers to pass identity the SDK already holds was noise at best and a footgun at worst (a long-lived query observer capturing a userId in a closure could pin a previous session's id across an account switch; a call-time state read cannot). - update(data) and setDefaultAccount(account) drop the userId/User params and resolve the current user from the user state, throwing if no user is loaded (precondition, documented with @throws) - queryOptions() drops userId; userQueryOptions takes getUserId and resolves the id at fetch time, not at options creation - upsert keeps id in its params: it is the identity injection point where the host's auth layer establishes who is logged in (documented) - web: useUser passes nothing; useUpdateUser/useSetDefaultAccount no longer subscribe to the user query just to echo the SDK its own data Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…user (Phase 3.7) Same reasoning as 3.6 applied to the accounts surface: listOptions() and add(account) no longer take caller-passed identity. AccountsApiDeps gains getCurrentUserId — a thunk the composition root wires from this.user.getCached(); a thunk because the accounts domain is constructed before the user domain, and it is only invoked at query/call time after the bootstrap upsert. accountsQueryOptions takes getUserId resolved at fetch time (the load-bearing structuralSharing is untouched; test call shape updated). useAccounts/useAddCashuAccount stop subscribing to the user just to echo its id back; the claim-token service keeps its explicit-user flow via getUserId: () => user.id. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tity source Checkpoint decision: auth and user are not merged. Phase 4's sdk.auth becomes the identity source the root injects into the user domain (auth → user → accounts), absorbing sdk.user.upsert's id param — the last outside-passed identity after 3.6/3.7. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…on-models to db-types (Phase 4.1) DestinationDetailsSchema describes db-persisted jsonb data and was the cross-package knot: the cashu-lightning-send json-model and the transactions detail schemas need it, but db-types cannot import from the send domain. It moves to @agicash/db-types/json-models/destination-details; the send leaf re-exports it so send-domain imports are unchanged. cashu-lightning-send-db-data.ts moves to db-types (the last entangled json-model); web file is a shim. The orphaned account-details-db-data.ts union (zero consumers) is deleted rather than moved. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ns surface (Phase 4.2)
Moves transaction.ts (types/schemas + isTransactionReversable),
transaction-enums.ts, transaction-details/ (8 files), and
TransactionRepository into packages/wallet-sdk/src/transactions —
verbatim, import remaps only (json-models → @agicash/db-types subpaths,
Money → @agicash/utils). TransactionsCache + createTransactionChangeHandlers
extracted from transaction-hooks (version-guarded upsert + history-page
acknowledge absorbed as cache.acknowledgeInHistory); test-locked (4 tests).
Curated surface: queryOptions(id) (NotFoundError + retry semantics in the
SDK), listOptions(accountId?) (infinite query, page-size 25, per-id
write-through), pendingAckCountOptions() (primitive count — web derives
the boolean), acknowledge(tx) (absorbs the onSuccess cache updates).
internal = {repository, cache, changeHandlers} for receive/send hooks and
realtime wiring. Root reuses one lazy Encryption + one getCurrentUserId
across domains.
Web: transaction-hooks.ts delegates to the curated surface;
useTransactionRepository deleted; useReverseTransaction stays web-wired
(send-domain services) until Phase 7. transaction.ts/-enums/-repository
become shims. UI components and the ack-status store stay web.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ider (async) — headless-ready
auth.ts no longer touches window.localStorage. It reads the session tokens
through the provider the host passed to configure({ storage }) — reached via
getConfig().storage.persistent, the single source of truth that also serves the
two SDK-internal isLoggedIn() callers (supabase-session, agicash-mint-auth).
The reads are async (await), so both synchronous (browser localStorage) and
asynchronous (keychain, file, remote secret store — i.e. a Node/MCP host) stores
work; the earlier sync-only constraint is gone. isLoggedIn / getSessionExpiresInMs
/ clearTokens are now async; the two SDK queryFns await isLoggedIn().
Web useHandleSessionExpiry resolves the now-async expiry into state and re-arms
explicitly after a guest extension. The previous re-arm leaned on OpenSecret's
React setState, which the 1.0 (react-free) bump removed, so the explicit re-read
is both required and more robust.
auth.test.ts (new, 4 tests) locks the contract: isLoggedIn reads through the
configured provider and supports an asynchronous store.
Verified: typecheck (6 packages), biome, 156 wallet-sdk tests.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…er imports @agicash/opensecret The web reached @agicash/opensecret directly in 6 places. The auth actions now live on the curated sdk.auth surface: signUp, signIn, signInGuest, signUpGuest, signOut, requestPasswordReset, confirmPasswordReset, verifyEmail, requestNewVerificationCode, convertGuestToFullAccount, initiateGoogleAuth, handleGoogleCallback. Each is a thin async wrapper over OpenSecret plus the one portable bit (password-reset secret hashing). Navigation, Sentry, query-cache clearing, auth-state invalidation, guest-account storage, and the browser OAuth URL/session glue stay web-side, so behavior is unchanged — only the call source moved. This makes the actions reusable by a headless/MCP host, which cannot call React hooks. browserStorage is re-exported from the SDK so the web configures storage without importing opensecret. With every call rerouted, @agicash/opensecret is dropped from apps/web-wallet/package.json: the web now reaches the auth backend only through @agicash/wallet-sdk. Also deletes the dead useCryptography hook (zero consumers). Verified: typecheck (6 packages), biome, 156 wallet-sdk + 52 web tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…urface Post-extraction surface cleanup from reviewing the branch. Three related changes: - useSdk(): add a web-side hook for SDK access in React render context and convert the render-time getSdk() call sites to it; getSdk() stays for loaders, query/mutation fns, effects, and module scope (where a hook can't run). Move the client-only (isServer) guard out of the framework-free SDK into useSdk() — whether a runtime may construct the SDK is host policy, so getSdk() is now host-agnostic and a headless host can call it server-side. - Encapsulation: services, repositories, and caches are no longer importable from outside the SDK. Relocate the pure derivations (getExtendedAccounts, isDefaultAccount, getDefaultReceiveAccount) and the consumed quote/account types (CashuLightningQuote, SendQuoteRequest, CashuSwapQuote, SparkLightningQuote, TransferQuote, NewCashuAccount) into leaf modules; remove the five *-service subpath exports from package.json; drop the repo/service/cache re-exports from the barrel. The server lnurl path keeps a dedicated ./user/user-repository subpath (it has no sdk instance). - Delete hooks orphaned by the tasks-engine extraction: useGetCashuAccountByMintUrlAndCurrency, useGetSparkAccount, useSelectItemsWithOnlineAccount. typecheck, biome, unit tests (284), and the SSR/prerender build are green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The task processors and trackers were lifted from React hooks, and their JSDoc
described the code in terms of those hooks — "headless reproduction of the
web's useProcessCashuReceiveQuoteTasks", "mirrors the web's
useSelectItemsWithOnlineAccount", "the same options the web's useQuery used",
even JSX like {isLead && <TaskProcessor/>}. A framework-free SDK must not
reference the web app's hooks or React, and several references pointed at hooks
that no longer exist.
Rewrite the comments to describe behavior on its own terms, keeping the real
content: the query-core primitives the SDK actually uses (QueryObserver,
MutationObserver), retry policies, NUT-17 WS + polling fallback, expiry-timer
rationale, the nutshell #788 workaround, and the latent scope-id bug notes.
Also fix the now-false createTasksApi JSDoc that claimed families still run in
a web TaskProcessor gated on engine leadership — all families run in this
engine now.
Comments only; no code changed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… the sole accessor The WalletSdk constructor was public over process-global state (the QueryClient, the Agicash DB client + its RLS session token, the OpenSecret token store), so a second `new WalletSdk()` would silently share one user's cache and session with another — a cross-user leak. The SDK is single-user by design (RLS-scoped, derives the current user from its own state), so one instance == one user. Make the constructor private and expose `WalletSdk.getInstance()` as the sole accessor (reads the recorded config, throws if unconfigured, builds at most one instance). `new WalletSdk()` is now a compile error. Fold the old free `getSdk()` (just a config-check + delegate) into getInstance; the web keeps a one-line `getSdk` alias next to `useSdk` so its call sites are unchanged. Multi-instance (a multi-tenant headless host) becomes safe only once those resources are instance-owned (constructor-injected); a comment marks that as MCP-phase work, and the guard lifts together with it. typecheck, biome, and wallet-sdk tests (156) green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Pluck the pure, framework-free input-handling logic the web's scan/send flows used into the SDK, where the MCP/agent wallet will reuse it: - classifyInput (raw string -> cashu-token-receive / bolt11-send / ln-address-send) -> @agicash/wallet-sdk/scan. The import.meta.env.MODE read became an `allowLocalhost` option (web passes import.meta.env.DEV). - buildLightningAddressFormatValidator (pure LUD-16 format regex) -> @agicash/wallet-sdk/lightning-address. Web ~/lib/lnurl re-exports it so the send-form validators are unchanged. - validateBolt11 + ValidateResult (network/expiry/amount checks) -> @agicash/wallet-sdk/send/validation. Its sibling validateLightningAddressFormat stays web — it is only the SDK validator pre-bound with the app's message and dev-localhost flag (web glue). Web features/send/validation.ts re-exports validateBolt11 so resolve-destination and the scan route are unchanged. All exposed as pure subpath exports (not sdk.* methods): they need no user/connection/cache, so any caller can use them without bootstrapping the SDK. The web scan feature dir is deleted; its test moved with the code. typecheck, biome, and the unit suite (284) green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a "Post-Phase-10 addendum" to the extraction doc covering everything after the Phase-10 tip on sdk/phase12-tasks-engine: the tasks engine, then the surface-hardening pass (useSdk + host-agnostic getSdk, the WalletSdk hard singleton, the now-structural curated surface, dead-hook + JSDoc cleanup, and the classifyInput/validateBolt11/lightning-address plucks). Records the two deferred-and-settled decisions with rationale (wallet-core extraction; Money stays in @agicash/utils) and the next-candidate cluster (the LNURL client + resolve-destination). Marks the Phase-10 Final report as a historical snapshot and points forward to the addendum as the current state. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…domain The throw-on-missing "current user" resolver was defined twice with identical bodies and error strings: once in the WalletSdk composition root and once privately inside createUserApi. The user domain owns the cache, so it now owns the resolver — createUserApi exposes getCurrentUser on its factory return and the root's shared identity thunk delegates to it instead of re-implementing it. The public surface is unchanged: sdk.user.getCached() stays User | null (throw-or-not remains caller policy); the throwing variant is exposed only on the factory return, off the *Api surface, the same way accounts exposes its repository/cache to the root. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…dk/lnurl) Move the client lightning-address resolver out of the web app into the SDK so a headless wallet can resolve a lightning address to an invoice without the browser. getInvoiceFromLud16 / getLNURLPayParams / isValidLightningAddress / isLNURLError / buildLightningAddressValidator and the LNURL protocol types were web-only (~/lib/lnurl); nothing in the SDK used them, so send-to-lightning- address resolution happened web-side before handing a bolt11 to sdk.send — the last thing keeping that path from running headless (the MCP wallet needs it). Byte-identical move + import remap: - ~/lib/lnurl/index.ts -> packages/wallet-sdk/src/lnurl.ts - ~/lib/lnurl/types.ts -> packages/wallet-sdk/src/lnurl-types.ts New package exports ./lnurl + ./lnurl-types. The helpers are stateless, framework-free (ky + Money + the in-SDK format validator) and need no session, so they stay plain module exports — not on the getSdk() singleton. Rewired 5 consumers: 4 send-side (resolve-destination, send-input, validation, use-get-invoice-from-lud16) and the server LightningAddressService's LNURL-type imports. The server lnurlp path itself stays in web for now. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…uery The auth-state query is the SDK's own cache, but every web caller had to pair each sdk.auth mutation with a manual invalidate — the lone domain that didn't absorb its own cache write-back. A headless consumer would get stale auth state unless it replicated the web's invalidate dance. Move it inward: signUp / signIn / signInGuest / signUpGuest / signOut / verifyEmail / convertGuestToFullAccount / handleGoogleCallback now await invalidateAuthState() after the OpenSecret call (refetchType 'all', so the awaited mutation still waits for the refetch — behavior preserved). The non-state-changing methods (requestPasswordReset / confirmPasswordReset / requestNewVerificationCode / initiateGoogleAuth) are untouched, matching what the web paired with an invalidate. Web: the invalidateAuthQueries wrapper dropped its now-redundant sdk.auth.invalidate() call (it would otherwise double-invalidate) and is renamed invalidateFeatureFlags — it now only re-evaluates the web-owned feature flags on auth change. This folds into the SDK when feature flags move (next chunk). sdk.auth.invalidate is kept as a force-refresh primitive (now zero-consumer in the web). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…hange Feature flags were the last web-side consumer of the client Supabase handle (agicashDbClient) and the reason the web kept an invalidate-on-auth-change wrapper. Move the data layer into the SDK and let auth drive the refresh, so the web wrapper and the client db handle both disappear — and a headless wallet can read flags without the browser. SDK: - new sdk.featureFlags (options / get / invalidate) on the injected db + captureException seam; the evaluate_feature_flags RPC, safe-off defaults, and retry/backoff move verbatim. Session-independent (works anon), so it's a standalone surface, not a curated single-user domain. +4 tests lock the outage -> defaults fallback. - auth gains an onSessionChange dep; invalidateAuthState fires it after the auth-state refetch. The root wires onSessionChange -> featureFlags.invalidate, so the 8 state-changing mutations now refresh flags too, with auth staying unaware of the feature-flags domain. - the DEBUG_LOGGING_SPARK -> setSparkDebugLogging seam moves into the root (was a web wiring that existed only to dodge an import cycle). Web: - shared/feature-flags.ts shrinks to the useFeatureFlag binding, which reads getSdk().featureFlags.options() directly. Safe because every flag consumer (the auth routes and the cashu-token route) forces client rendering via clientLoader.hydrate, so it never runs during SSR — unlike useAuthState, whose authQueryOptions defers getSdk() for the public pages built server-side. - invalidateFeatureFlags + its 3 call sites deleted (auth self-handles it). - database.client.ts deleted (agicashDbClient had no consumers left); its realtime debug handle relocated to entry.client; stale cycle comments fixed. Gates: typecheck x6, full suite (wallet-sdk 177), SSR build incl. prerender; SSR of /signup + /receive-cashu-token verified 200. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The guest auth operations were already in the SDK, but the recovery-credential
persistence (the {id, password} blob) and password generation lived in the web
(guest-account-storage.ts on raw localStorage; ~/lib/password-generator). That
left the guest flow un-runnable headless and stranded the last bits of the auth
domain in the web.
Move them in, reusing the StorageProvider the SDK auth already uses for tokens:
- @agicash/utils gains generateRandomPassword (window.crypto -> crypto,
window.getMockPassword -> globalThis.getMockPassword so the e2e seam still
works; +4 tests).
- sdk.auth.signUpGuest() is now no-arg restore-or-create: resume the persisted
guest if present, else generate a password, create the guest, and persist its
recovery credentials. Stored under the same 'guestAccount' key + shape the web
used, so existing guest sessions carry over.
- sdk.auth.convertGuestToFullAccount clears the stored credentials on success
(useless once the account is full) — absorbing the web's explicit clear.
Web: the signUpGuest wrapper collapses to sdk.auth.signUpGuest() + refreshSession;
the private signInGuest helper and the guestAccountStorage.clear() in the
guest->full upgrade are gone. guest-account-storage.ts and the web
password-generator are deleted; requestPasswordReset's secret now imports
generateRandomPassword from @agicash/utils.
Unblocks moving session-expiry handling into the SDK next.
Gates: typecheck x6, full suite (utils 45, wallet-sdk 177), SSR build incl.
prerender. e2e (guest signup) not run — it's covered by the getMockPassword
fixture; worth running before merge.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…xpiry)
The session-expiry policy was React orchestration in the web's
useHandleSessionExpiry: read getSessionExpiresInMs, arm a long timeout, and on
expiry either extend a guest or sign out a full account (clearing tokens on
error). Now that the guest lifecycle lives in the SDK, that whole loop can too.
sdk.auth.watchSessionExpiry({ onExpire, onRecover }) returns a stop fn and owns
the mechanics: read the refresh-token expiry, arm a setLongTimeout, and on fire
either silently resume the guest session (and re-arm) or — for a full account —
fire onExpire so the host signs out and navigates; on an unrecoverable error it
clears the tokens and fires onRecover. Guest-vs-full is derived from the auth
state's user (no email = guest), so no caller passes it. signUpGuest's
restore-or-create body and the token clearing are extracted into shared helpers
the watcher reuses.
Web: useHandleSessionExpiry collapses to a watchSessionExpiry binding (callbacks
captured in refs so it arms once on mount); the isGuestAccount prop is gone
(wallet.tsx updated). onExpire = toast + signOut; onRecover = session-hint
cookie clear + reload — the only genuinely web-side bits. The orphaned
use-long-timeout.ts hook is deleted (the SDK uses @agicash/utils/timeout).
Headless-ready: a daemon drives watchSessionExpiry the same way the web does,
guests auto-extend, full-account expiry is host policy.
Gates: typecheck x6, full suite, SSR build incl. prerender. Session-expiry is
time-based and not unit-tested (faithful lift of the previously-untested hook);
worth a browser/e2e smoke before merge.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
arm() checked `stopped` before the `await getJwt(...)` but not after, so a stop() during that await — notably a StrictMode mount/unmount/remount, where `timer` is still null and stop() therefore clears nothing — let arm() schedule a timer after the watcher was stopped. The callback didn't consult `stopped`, so the stranded timer would fire on a stopped watcher (double-fire alongside the remount's timer). Re-check `stopped` after the await before arming, and guard the timer callback so a stopped watcher never acts. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The realtime channel + activity API already lived in the SDK; the one remaining extractable piece of the tracking family was the spark-balance tracker's reactivity. The web's useTrackAndUpdateSparkAccountBalances observed the online spark accounts via useAccounts and called sdk.accounts.trackSparkBalances on every change — React-owned reactivity over an SDK method. Move it in: sdk.accounts.startSparkBalanceTracking() returns a stop fn and owns an accounts QueryObserver (select = online, active spark accounts; reference- equality gate) that re-tracks as the set changes — the same shape as the tasks engine's work-set observers. trackSparkBalances becomes an internal helper; the public surface is the lifecycle, not the per-list call. Web: useTrackAndUpdateSparkAccountBalances collapses to a one-line startSparkBalanceTracking() binding (no useAccounts). The 4 tasks/* doc references to the now-internal accounts.trackSparkBalances point at the public startSparkBalanceTracking. The other two tracking hooks (useTrackWalletChanges, the realtime activity tracking) stay web: they're the React-lifecycle + browser-event (navigator/ document) bindings a framework-free SDK is meant to have a host provide. Gates: typecheck x6, full suite, SSR build incl. prerender. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ice) The server lnurlp handler — look up a user by their lightning address, resolve their default account, mint a cashu/spark receive quote on their behalf, and report settlement — was wallet receive-domain logic living in the web app's route layer with its own server repos. Move it into the SDK as a framework-free, per-request-constructed class so the web routes and a future headless lightning-address-hosting backend can share one implementation. SDK: - new @agicash/wallet-sdk/lightning-address-service: LightningAddressService + LightningAddressServiceConfig. Built from injected config — the env reads (LNURL_SERVER_SPARK_MNEMONIC / _ENCRYPTION_KEY), the service-role db, the per-request queryClient, the request origin (baseUrl), and the spark storage dir are all host-provided now (no process.env or Request in the SDK). @tanstack/react-query -> query-core. - the 4 write-only server receive repos/services move to receive/ (colocated with their client siblings; verbatim + import remaps). - xchacha20poly1305 -> @agicash/utils (next to ecies; utils already deps @noble/ciphers). Web: - new lightning-address-config.server.ts reads the LNURL_SERVER_* env and wires the service-role db / queryClient / origin into LightningAddressServiceConfig. - the 3 lnurlp routes construct the SDK service from that config; no behavior change. Gates: typecheck x6, full suite, SSR build incl. prerender; the server import graph stays out of the client bundle. Live lnurlp endpoints not exercised here — worth the lnurl-test skill / a manual hit before merge. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
After the auth mutations moved their cache invalidation into the SDK, this helper no longer refreshes the session — it only re-runs the current route (or navigates) so React Router's loaders/guards re-evaluate against the auth state the SDK already refreshed. Rename to match what it does, and add a comment for the non-obvious "revalidate to trigger the guard redirect" intent. Also fix the now-stale entry.client feature-flags comment (there's no anon-key DB client anymore; the SDK re-evaluates flags on auth change). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Flip the getSdk/useSdk convention: inside a component or hook, capture useSdk() at the top and use it everywhere (including in handlers and effects), reserving getSdk() for genuinely non-React code — loaders, actions, query/mutation functions, plain functions, and module scope. useSdk() is the client-only, render-guarded accessor, so using it at the hook top makes the client-only assumption explicit. Converted the three render-context consumers: useAuthActions (capture sdk, use in every action handler), useHandleSessionExpiry (use in the watcher effect), and useFeatureFlag. All their consumers sit on client-forced routes (auth / protected / the cashu-token route), so the render-time SSR guard is safe — verified by the prerender build. Left on getSdk() (correctly — not React render): authQueryOptions' queryFn (an options factory built during SSR on public pages), entry.client (module scope), the route loaders/actions, and the plain helper functions (getUserFromCacheOrThrow, ensureUserData, verify-email). getSdk/useSdk JSDoc updated to state the preference. Gates: typecheck x6, full suite, SSR build incl. prerender. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Records the seam: useSdk() returns the singleton today, but if we ever want injectable-for-tests DI it can become a real context provider without changing call sites. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…art()
Move the realtime subscribe lifecycle into the SDK. sdk.realtime.start() now
owns it: it observes the auth state (wired from the SDK root) and subscribes the
current user's wallet:{userId} channel, re-subscribing as the session changes
(login / logout / user switch) and tearing down the previous channel — the same
session-observer pattern the tasks engine and spark-balance tracker use. Returns
a stop fn; client-only; call once per host.
The auth user id is the wallet user id and the channel RLS is JWT-based
(realtime.topic() = 'wallet:' || auth.uid()), so subscribing on auth-resolve is
valid even before the user row is bootstrapped. Removed the public
subscribe/unsubscribe (only the web used them); getStatus / getError /
onStatusChange still read the current user's channel.
Web: useTrackWalletChanges drops the subscribe/unsubscribe + async-cleanup dance
(its StrictMode handling now lives in the SDK + the manager's ref-counting) — it
becomes useEffect(() => realtime.start()) plus the status read and the
error-boundary throw, which stay (irreducibly React).
A headless host now gets session-tied realtime from one start() call — including
correct re-subscribe on user switch, which the old getCurrentUserId-based
unsubscribe would have gotten wrong (it targeted the new topic, not the old).
Gates: typecheck x6, full suite, SSR build incl. prerender. The runtime channel
behavior (subscribe on login, re-subscribe on switch, error escalation) is not
exercised here — worth a browser smoke before merge.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The engines the SDK's cache depends on — the realtime wallet channel, the leader-elected task processor, and spark balance tracking — were three separate host start() calls. Forget one and the SDK is silently stale (realtime is even the write-back path for some mutations, so a forgotten start meant DB writes that never reached the cache). Consolidate into one sdk.start() that starts all three and returns a single stop, so a host can't forget a cog. A JSDoc note records that it could be auto-started from the constructor if we see the need — deferred because tasks and spark balance read the current user eagerly and would first need the session-gating realtime already has. Web: wallet.tsx replaces the three starts (useTrackWalletChanges' realtime.start, the tasks start/stop effect, the spark-balance hook) with one useEffect(() => sdk.start()). useTrackWalletChanges drops to just the realtime-error boundary escalation; the activity feed stays. shared/spark.ts is deleted (its lone hook folded into sdk.start()). Gates: typecheck x6, full suite, SSR build incl. prerender. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
After sdk.start() took over the realtime subscription, this hook no longer tracks anything — it only reads the channel status and throws a terminal channel error to the nearest error boundary. Rename it (and its file) to match what it now does. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Now that sdk.start() is the single entry point for the cache-correctness
engines, the per-engine start/stop methods it calls have no external callers —
leaving them public is just a footgun (a host could double-subscribe realtime,
leak a spark-balance tracker, or run a second task election).
- sdk.tasks: removed from the public surface entirely — nothing outside sdk.ts
ever touched it. The engine is now a private field, so its
start/stop/getStatus/onStatusChange/getError all come off the instance surface
in one move; the dangling TasksApi re-export is dropped too.
- RealtimeApi.start: removed. createRealtimeApi now returns { api, start }; the
root wires start into sdk.start() and exposes only api as sdk.realtime
(status/error/online/active/__debugManager stay).
- AccountsApi.startSparkBalanceTracking: removed. Moved to the factory's
existing { api, ... } return root; sdk.accounts keeps its data methods.
sdk.start() and its returned stop are unchanged in behavior — they now call the
captured private start fns instead of reaching through the public objects. The
realtime-api churn is mostly the start-body re-indent from un-nesting it out of
the returned object literal.
Gates: typecheck x6, full suite, SSR build incl. prerender.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The cashu mint-socket trackers (proof-state, mint-quote, melt-quote) are long-lived: activate/deactivate toggle across leader handoffs, but stop() never closed the per-mint sockets. Unlike master (which unmounted the React TaskProcessor and built a fresh manager on regain), the SDK reused the stale socket via subset-dedup, so the mint never re-delivered current state on reactivation. For proof-state this could orphan a PENDING cashu send-swap: a deactivated- but-still-socketed tab consumes the swap's edge-triggered "all spent" event (dropping the completion and deleting the accumulator), and reactivation never recovers. The quote trackers had a milder version (a follower-side socket leak + a regain-recovery delay), saved from orphaning only by their poll/expiry fallbacks. - add unsubscribeAll() to the proof-state / mint-quote / melt-quote subscription managers (close sockets + clear state) and call it from each tracker's stop(), so reactivation re-subscribes fresh - add a `stopped` guard to all three trackers so a subscribe mutation that is mid-retry when stop() fires cannot re-open a socket afterwards; reset on setQuotes/setSwaps - spark trackers unchanged: they use Breez listeners (no mint sockets) and already resolve the in-flight registration before removing it Tests: manager unsubscribeAll behavior (cashu) + a melt-quote-tracker keystone locking the stop-teardown and the retry-after-stop guard + processor-wiring assertions. cashu 39 pass, wallet-sdk 188 pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The send flow's `resolveSendDestination` paralleled the scan flow's `classifyInput`: both independently discriminated bolt11 vs lightning-address, so a future parsing quirk would get fixed in one and not the other. - move `resolveSendDestination` + `SendDestination` into packages/wallet-sdk/src/send/resolve-destination.ts, over the SDK primitives it already used (validateBolt11, isValidLightningAddress, the ln-address format validator, parseBolt11Invoice, parseCashuPaymentRequest, Contact); the web-bound `validateLightningAddressFormat` import becomes an `allowLocalhost` option - the web resolve-destination.ts is now a thin shim that injects the app's dev-localhost allowance, so send-store / send-provider / the send route are unchanged - redirect the scan route's validateBolt11 import to @agicash/wallet-sdk and delete the now-empty web send/validation.ts (its last consumer) Behavior change: the SDK resolver lowercases the lightning-address before format-validating (the format regex is lowercase-only), matching classifyInput. The send path now accepts mixed-case addresses like `Alice@example.com` instead of rejecting them, consistent with the scan path. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…llable retry helper
The three cashu mint-socket trackers (proof-state, mint-quote, melt-quote) each
drove `subscriptionManager.subscribe()` through a query-core MutationObserver
purely to get retry-with-backoff. A query-core mutation can't be cancelled
mid-retry, so each tracker also carried a `stopped` flag to stop a retry that
resolves after stop() from re-opening a socket — the band-aid from the previous
commit.
- add @agicash/utils/retry: `retryWithBackoff(fn, { retries, signal, delayMs?,
onError? })` with an abortable backoff (default exponential
`min(1000 * 2 ** n, 30000)`, matching query-core's mutation default)
- each tracker now holds an AbortController instead of the MutationObserver +
`stopped` flag; stop() aborts it, so an in-flight subscribe retry is genuinely
cancelled (and a pending backoff is interrupted) rather than racing a guard.
setQuotes/setSwaps mints a fresh controller after a stop
- `queryClient` is now unused in the proof-state and melt trackers (mint-quote
keeps it for the poll observers) → dropped from their options and the four
tracker construction sites
- spark trackers untouched (Breez listeners, no subscribe retry)
Tests: new retry.test.ts (success / retry-then-succeed / exhaust-and-throw /
abort-stops-retries / abort-interrupts-backoff / already-aborted); the
melt-quote-tracker item-2 test is rewritten around the abort and red-green
verified (fails if stop() does not abort). utils 52, wallet-sdk 188, cashu 39
pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
This pull request has been ignored for the connected project Preview Branches by Supabase. |
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.
No description provided.