@qntx/wallet-engine 0.0.0-stage → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,758 @@
1
+ import { StoreApi } from "zustand/vanilla";
2
+ import { CooperativeExitResult, LightningInvoice, LightningPayment, SparkClient, SparkExitChain, SparkNetwork, SparkPayment, SparkStore, SparkTransfer } from "@qntx/spark";
3
+ import { NostrKeyRef, WalletSigner } from "@qntx/wallet/nostr";
4
+ import { SparkSigner } from "@qntx/wallet/spark";
5
+ import { BackupHeader, BackupInfo, InitialFactor, UnlockFactor, Wallet, WalletInfo } from "@qntx/wallet";
6
+ import { Event, Filter, NostrSigner } from "@qntx/nostr";
7
+ import { LnurlPayRequest, LnurlWithdrawRequest } from "@qntx/spark/lnurl";
8
+ import { SparkAccountRef, SparkApproveIntent } from "@qntx/wallet/protocol";
9
+ //#region src/controller.d.ts
10
+ type ControllerOptions<S> = {
11
+ readonly name: string;
12
+ readonly defaultState: () => S;
13
+ };
14
+ /**
15
+ * Controller base: a zustand vanilla store plus lifecycle. `update` applies a recipe that must
16
+ * return a NEW state object; returning the same reference is a no-op (no set, no notification).
17
+ * Persistence and migrations layer on later — this package carries only what its first user needs.
18
+ */
19
+ export declare abstract class Controller<S extends Record<string, unknown>> {
20
+ readonly name: string;
21
+ readonly store: StoreApi<S>;
22
+ protected constructor(options: ControllerOptions<S>);
23
+ getState(): S;
24
+ protected update(recipe: (state: S) => S): void;
25
+ abstract init(): Promise<void>;
26
+ abstract destroy(): Promise<void>;
27
+ }
28
+ //#endregion
29
+ //#region src/events.d.ts
30
+ /** Log port: listener failures are reported here and never reach the emitter or peers. */
31
+ type EngineLogger = {
32
+ readonly warn: (message: string, error?: unknown) => void;
33
+ };
34
+ type EventBus<E> = {
35
+ /** Synchronous dispatch to every current listener in registration order. */
36
+ emit: <K extends keyof E>(type: K, payload: E[K]) => void;
37
+ on: <K extends keyof E>(type: K, listener: (payload: E[K]) => void) => () => void;
38
+ };
39
+ export declare function createEventBus<E>(logger: EngineLogger): EventBus<E>;
40
+ //#endregion
41
+ //#region src/errors.d.ts
42
+ type EngineErrorCode = "closed" | "integrity" | "invalid-input";
43
+ /** Typed engine errors: store codes are `closed` and `integrity` (decrypt or bind failure). */
44
+ export declare class EngineStoreError extends Error {
45
+ readonly code: EngineErrorCode;
46
+ constructor(code: EngineErrorCode, message: string);
47
+ }
48
+ //#endregion
49
+ //#region src/store/port.d.ts
50
+ /**
51
+ * Engine storage port: named collections of byte values. `collection` is synchronous and cheap —
52
+ * the same collection object backs every call, so guards (closed) live on the store.
53
+ */
54
+ type EngineCollection = {
55
+ readonly get: (key: string) => Promise<Uint8Array | undefined>;
56
+ readonly put: (key: string, bytes: Uint8Array) => Promise<void>;
57
+ readonly delete: (key: string) => Promise<void>;
58
+ /** Every key in the collection, in an unspecified order. */
59
+ readonly list: () => Promise<ReadonlyArray<string>>;
60
+ };
61
+ type EngineStore = {
62
+ readonly collection: (name: string) => EngineCollection;
63
+ readonly close: () => Promise<void>;
64
+ };
65
+ //#endregion
66
+ //#region src/store/memory.d.ts
67
+ /** In-memory EngineStore: the Node/test implementation — same contract, no durability. */
68
+ export declare function createMemoryEngineStore(backing?: {
69
+ readonly collections?: Map<string, Map<string, Uint8Array>>;
70
+ }): EngineStore;
71
+ //#endregion
72
+ //#region src/meta/doc.d.ts
73
+ /**
74
+ * Wallet metadata document — the plaintext exchanged over kind-30078 events between this wallet's
75
+ * own devices. One document per wallet; entry-level last-writer-wins on `updatedAt`.
76
+ */
77
+ type MetaDocWallet = {
78
+ readonly name?: string;
79
+ readonly updatedAt: number;
80
+ };
81
+ type MetaDocGroup = {
82
+ readonly index: number;
83
+ readonly name?: string;
84
+ readonly hidden: boolean;
85
+ readonly boundPubkey?: string;
86
+ readonly updatedAt: number;
87
+ };
88
+ type MetaDoc = {
89
+ readonly v: 1;
90
+ readonly wallet: MetaDocWallet;
91
+ readonly groups: ReadonlyArray<MetaDocGroup>;
92
+ };
93
+ //#endregion
94
+ //#region src/spark/notes.d.ts
95
+ /**
96
+ * Local, engine-side metadata pinned to a history entry by transfer id. Written after the payment
97
+ * it annotates has already succeeded — a missing or malformed note must never affect the payment
98
+ * itself.
99
+ */
100
+ type PaymentNote = {
101
+ readonly kind: "zap";
102
+ /** 64 lowercase hex. */
103
+ readonly recipientPubkey: string;
104
+ /** 64 lowercase hex — event zaps carry the zapped event's id. */
105
+ readonly eventId?: string;
106
+ /** At most 2000 characters. */
107
+ readonly comment?: string;
108
+ /** The lud16 or LNURL the zap resolved. */
109
+ readonly lightning: string;
110
+ };
111
+ type SparkActivity = SparkPayment & {
112
+ readonly note?: PaymentNote;
113
+ };
114
+ //#endregion
115
+ //#region src/spark/store.d.ts
116
+ /**
117
+ * SparkStore over one EngineCollection: chains keyed by leafId, plus an index record listing them
118
+ * (`exitChains` is a full listing — the port has no secondary index).
119
+ */
120
+ export declare function sparkStore(col: EngineCollection): SparkStore;
121
+ //#endregion
122
+ //#region src/spark/controller.d.ts
123
+ type SparkAccountState = {
124
+ readonly status: "locked" | "connecting" | "ready" | "error";
125
+ readonly balanceSats?: number;
126
+ readonly identityPublicKey?: string;
127
+ readonly sparkAddress?: string;
128
+ readonly errorCategory?: string;
129
+ /** First history page (20 entries, newest first), refreshed alongside the balance. */
130
+ readonly recent?: ReadonlyArray<SparkActivity>;
131
+ };
132
+ type SparkControllerState = {
133
+ readonly accounts: Readonly<Record<string, SparkAccountState>>;
134
+ };
135
+ type SparkApis = {
136
+ readonly signer: (ref: SparkAccountRef) => SparkSigner;
137
+ readonly approve: (params: {
138
+ account: SparkAccountRef;
139
+ intent: SparkApproveIntent;
140
+ verify: UnlockFactor;
141
+ }) => Promise<unknown>;
142
+ };
143
+ type SparkClientFactory = (options: {
144
+ readonly ref: SparkAccountRef;
145
+ readonly store: ReturnType<typeof sparkStore>;
146
+ readonly signer: SparkSigner;
147
+ readonly fetch: typeof globalThis.fetch;
148
+ }) => SparkClient;
149
+ type SparkControllerOptions = {
150
+ readonly spark: SparkApis;
151
+ readonly fetch: typeof globalThis.fetch;
152
+ readonly random: (length: number) => Uint8Array;
153
+ readonly now: () => number;
154
+ readonly logger: EngineLogger;
155
+ readonly createClient: SparkClientFactory;
156
+ };
157
+ export declare class SparkController extends Controller<SparkControllerState> {
158
+ private readonly options;
159
+ private engineStore;
160
+ private readonly accounts;
161
+ private readonly clients;
162
+ private readonly recentSeq;
163
+ private readonly aborts;
164
+ constructor(options: SparkControllerOptions);
165
+ /** Engine calls this with the open store on unlock; `undefined` on lock. */
166
+ setStore(store: EngineStore | undefined): void;
167
+ /**
168
+ * Declarative account set: refs not yet tracked are patched `locked` and started when the store
169
+ * is open; tracked refs no longer listed are stopped and dropped; running refs stay untouched.
170
+ */
171
+ setAccounts(refs: ReadonlyArray<SparkAccountRef>): void;
172
+ init(): Promise<void>;
173
+ destroy(): Promise<void>;
174
+ private stopAccount;
175
+ /** Lock: abort every event loop, close every client, reset state. */
176
+ lockAll(): Promise<void>;
177
+ private startAccount;
178
+ private runEvents;
179
+ private handleTransfer;
180
+ private notesCollection;
181
+ /** Attach locally written notes to a history page; malformed records warn and drop. */
182
+ private withNotes;
183
+ /**
184
+ * Refill `recent` from the first history page. A failure keeps the previous page — it never
185
+ * touches `status`/`errorCategory`. A per-account sequence keeps a slower older refresh from
186
+ * overwriting a newer one.
187
+ */
188
+ private refreshRecent;
189
+ private patch;
190
+ private clientFor;
191
+ private guarded;
192
+ /**
193
+ * One-off balance + activity probe for an account that may not be running: builds a temporary
194
+ * client through the same factory on the account's own exit-chain collection (nothing diverges —
195
+ * `balance()`/`transfers.history` never write it), reads, and closes it in `finally`.
196
+ */
197
+ inspect(ref: SparkAccountRef): Promise<{
198
+ balanceSats: number;
199
+ used: boolean;
200
+ }>;
201
+ /** History for one account: a client page merged with locally written notes by id. */
202
+ history(ref: SparkAccountRef, options?: {
203
+ readonly limit?: number;
204
+ readonly offset?: number;
205
+ }): Promise<{
206
+ readonly items: ReadonlyArray<SparkActivity>;
207
+ readonly nextOffset: number | undefined;
208
+ }>;
209
+ send(ref: SparkAccountRef, params: {
210
+ receiver: string;
211
+ amountSats: number;
212
+ verify: UnlockFactor;
213
+ }): Promise<SparkTransfer>;
214
+ registerLightningAddress(ref: SparkAccountRef, params: {
215
+ serverUrl: string;
216
+ domain: string;
217
+ username: string;
218
+ description: string;
219
+ }): Promise<{
220
+ lnurl: string;
221
+ lightningAddress: string;
222
+ }>;
223
+ unregisterLightningAddress(ref: SparkAccountRef, params: {
224
+ serverUrl: string;
225
+ domain: string;
226
+ username: string;
227
+ }): Promise<void>;
228
+ payInvoice(ref: SparkAccountRef, params: {
229
+ invoice: string;
230
+ amountSats?: number;
231
+ maxFeeSats: number;
232
+ verify: UnlockFactor;
233
+ /** Local annotation written under the payment's transfer id after it succeeds. */
234
+ note?: PaymentNote;
235
+ }): Promise<LightningPayment>;
236
+ /**
237
+ * Pays a Spark invoice address. The approval intent mirrors `send`: receiver identity and amount
238
+ * come from the parsed invoice (the supplied `amountSats` fills an open invoice). Then
239
+ * `client.invoices.pay` executes it.
240
+ */
241
+ paySparkInvoice(ref: SparkAccountRef, params: {
242
+ invoice: string;
243
+ amountSats?: number;
244
+ verify: UnlockFactor;
245
+ }): Promise<SparkTransfer>;
246
+ createInvoice(ref: SparkAccountRef, params: {
247
+ amountSats: number;
248
+ memo?: string;
249
+ }): Promise<LightningInvoice>;
250
+ receiveAddress(ref: SparkAccountRef): Promise<string | undefined>;
251
+ /**
252
+ * Cooperative exit to an L1 address. The client exits `amountSats` of leaves and the payout is
253
+ * `amountSats - fee`, so the approval intent carries the PAYOUT: `amountSats: exit - fee` and
254
+ * `maxFeeSats: fee` — the kernel matches the exit output against the intent, not the leaf total.
255
+ */
256
+ withdraw(ref: SparkAccountRef, params: {
257
+ address: string;
258
+ amountSats?: number;
259
+ speed: "FAST" | "MEDIUM" | "SLOW";
260
+ maxFeeSats?: number;
261
+ verify: UnlockFactor;
262
+ }): Promise<CooperativeExitResult>;
263
+ }
264
+ //#endregion
265
+ //#region src/accounts/controller.d.ts
266
+ type AccountGroup = {
267
+ readonly walletId: string;
268
+ readonly index: number;
269
+ readonly name?: string;
270
+ readonly hidden: boolean;
271
+ /** A social identity (64 lowercase hex) bound to this group; one identity binds one group. */
272
+ readonly boundPubkey?: string;
273
+ /** Last local modification time (ms); reconcile-created entries start at 0. */
274
+ readonly updatedAt: number;
275
+ };
276
+ type AccountTreeState = {
277
+ readonly status: "locked" | "ready";
278
+ readonly wallets: ReadonlyArray<{
279
+ readonly walletId: string;
280
+ readonly name?: string;
281
+ readonly updatedAt: number;
282
+ }>;
283
+ readonly groups: Readonly<Record<string, ReadonlyArray<AccountGroup>>>;
284
+ readonly selected: {
285
+ readonly walletId: string;
286
+ readonly groupIndex: number;
287
+ } | null;
288
+ readonly totals: Readonly<Record<string, number>>;
289
+ };
290
+ type AccountTreeControllerOptions = {
291
+ readonly wallet: Wallet;
292
+ readonly spark: SparkController;
293
+ readonly sparkNetwork: SparkNetwork;
294
+ readonly logger: EngineLogger;
295
+ readonly now: () => number;
296
+ };
297
+ /**
298
+ * Declarative account tree: wallets come from the Wallet snapshot (reconciled on unlock and on
299
+ * every snapshot change while unlocked), groups/names/hidden/selected/totals persist to the engine
300
+ * store under `accounts/tree`. The controller drives `spark.setAccounts` with the selected group
301
+ * only — an unselected group needs no live client (incoming transfers are claimed whenever it is
302
+ * selected again). The active account's `balanceSats` feeds `totals`, debounced 1 s before
303
+ * persist.
304
+ */
305
+ export declare class AccountTreeController extends Controller<AccountTreeState> {
306
+ private readonly options;
307
+ private col;
308
+ private sparkUnsubscribe;
309
+ private totalsTimer;
310
+ private persistChain;
311
+ constructor(options: AccountTreeControllerOptions);
312
+ setStore(store: EngineStore | undefined): void;
313
+ /** Load persisted state, reconcile with the snapshot, start driving Spark. */
314
+ init(): Promise<void>;
315
+ destroy(): Promise<void>;
316
+ /** Locked: stop every Spark client, flush pending persists, then drop to the empty state. */
317
+ lock(): Promise<void>;
318
+ /** Reconcile groups/names/selected with `wallet.getSnapshot().wallets`; persists on change. */
319
+ reconcile(): void;
320
+ addGroup(walletId: string): number;
321
+ addGroups(walletId: string, indices: ReadonlyArray<number>): void;
322
+ renameWallet(walletId: string, name: string): void;
323
+ renameGroup(walletId: string, index: number, name: string): void;
324
+ setHidden(walletId: string, index: number, hidden: boolean): void;
325
+ select(walletId: string, index: number): void;
326
+ /**
327
+ * Binds a social identity to one group. The identity is unique across the whole tree — every
328
+ * group in every wallet carrying it loses the binding (each entry re-stamped).
329
+ */
330
+ bind(walletId: string, index: number, pubkey: string): void;
331
+ unbind(walletId: string, index: number): void;
332
+ /**
333
+ * Which group currently holds a pubkey — merges can leave the same identity on groups in two
334
+ * wallets; the newest `updatedAt` wins, ties resolve by snapshot wallet order then lower index.
335
+ */
336
+ bindingOf(pubkey: string): {
337
+ walletId: string;
338
+ groupIndex: number;
339
+ } | undefined;
340
+ /** The wallet's metadata document — every group, hidden included. */
341
+ exportMeta(walletId: string): MetaDoc;
342
+ /**
343
+ * Entry-level last-writer-wins merge: a newer remote `updatedAt` replaces the local entry, a tie
344
+ * keeps local; remote-only groups are added. `selected`/hidden rules are re-validated — if the
345
+ * merge hides the selected group it is re-selected, and a merge may never leave every group
346
+ * hidden (the selected one stays visible and is re-stamped so the fix propagates).
347
+ */
348
+ mergeMeta(walletId: string, doc: MetaDoc): {
349
+ changed: boolean;
350
+ localNewer: boolean;
351
+ };
352
+ /** Sequential inspect from 0 upward; index 0 always included; stop after 2 consecutive unused. */
353
+ discoverSpark(walletId: string): Promise<number[]>;
354
+ /** Refresh `totals` for every other visible group; per-group failures are logged and skipped. */
355
+ refreshTotals(): Promise<void>;
356
+ private onSparkState;
357
+ private setTotal;
358
+ private driveSpark;
359
+ private visibleGroups;
360
+ private validSelection;
361
+ private selectFallback;
362
+ private mutateGroups;
363
+ private mustWallet;
364
+ private mustGroup;
365
+ private checkedName;
366
+ private load;
367
+ /** Queues a store write behind the previous one — the last queued write always lands last. */
368
+ private persist;
369
+ private scheduleTotalsPersist;
370
+ private flushTotals;
371
+ }
372
+ //#endregion
373
+ //#region src/backup/errors.d.ts
374
+ /** BackupController error categories — never carry file contents, digests or secrets. */
375
+ type BackupErrorCategory = "cloud" | "conflict" | "unavailable";
376
+ export declare class BackupControllerError extends Error {
377
+ readonly category: BackupErrorCategory;
378
+ constructor(category: BackupErrorCategory, message: string);
379
+ }
380
+ //#endregion
381
+ //#region src/backup/port.d.ts
382
+ /**
383
+ * Cloud-file storage port (design/backup.mdx "存储"): the cloud is an opaque key-value namespace —
384
+ * names are file names (`qntx-wallet-backup-<id>.json`), contents are envelope texts. The kernel
385
+ * never sees it; only the engine transports ciphertext across it.
386
+ */
387
+ type CloudStore = {
388
+ /** Cloud account usable and the drive reachable. */
389
+ readonly available: () => Promise<boolean>;
390
+ /** Every file name in the namespace. */
391
+ readonly list: () => Promise<ReadonlyArray<string>>;
392
+ /** File contents, or undefined when the name does not exist. */
393
+ readonly read: (name: string) => Promise<string | undefined>;
394
+ /** Overwrites the whole file atomically from the caller's perspective. */
395
+ readonly write: (name: string, text: string) => Promise<void>;
396
+ /** Deleting a missing file succeeds. */
397
+ readonly delete: (name: string) => Promise<void>;
398
+ };
399
+ //#endregion
400
+ //#region src/backup/controller.d.ts
401
+ type BackupControllerState = {
402
+ readonly status: "locked" | "unavailable" | "off" | "syncing" | "synced" | "conflict" | "error";
403
+ /** The retained backup key's public summary from the wallet snapshot. */
404
+ readonly backup: BackupInfo | null;
405
+ readonly uploadedAt: number | undefined;
406
+ readonly unprotected: {
407
+ readonly walletIds: ReadonlyArray<string>;
408
+ readonly nostrKeyIds: ReadonlyArray<string>;
409
+ };
410
+ readonly error: BackupErrorCategory | undefined;
411
+ };
412
+ type BackupControllerOptions = {
413
+ readonly wallet: Wallet;
414
+ readonly cloud: CloudStore | undefined;
415
+ readonly now: () => number;
416
+ readonly logger: EngineLogger;
417
+ };
418
+ export declare class BackupController extends Controller<BackupControllerState> {
419
+ private readonly wallet;
420
+ private readonly cloud;
421
+ private readonly now;
422
+ private readonly logger;
423
+ private engineStore;
424
+ private generation;
425
+ private current;
426
+ private rerun;
427
+ /** Set by restore() before the store opens — written as a record on the first reconcile. */
428
+ private pendingAdoption;
429
+ constructor(options: BackupControllerOptions);
430
+ init(): Promise<void>;
431
+ destroy(): Promise<void>;
432
+ /** The engine hands over the store on unlock and takes it back on lock. */
433
+ setStore(store: EngineStore | undefined): void;
434
+ /** Bumps the generation: an in-flight reconcile writes nothing after this point. */
435
+ lock(): void;
436
+ trigger(): void;
437
+ private triggerAsync;
438
+ /** Explicit sync — awaits the run so callers see its outcome. */
439
+ sync(): Promise<void>;
440
+ enable(options: {
441
+ readonly verify: UnlockFactor;
442
+ readonly factors: ReadonlyArray<UnlockFactor>;
443
+ }): Promise<void>;
444
+ disable(): Promise<void>;
445
+ /** Files under our prefix with a parseable header. */
446
+ list(): Promise<ReadonlyArray<{
447
+ readonly name: string;
448
+ readonly header: BackupHeader;
449
+ }>>;
450
+ restore(options: {
451
+ readonly name: string;
452
+ readonly backupFactor: UnlockFactor;
453
+ readonly factor?: InitialFactor;
454
+ }): Promise<ReadonlyArray<WalletInfo>>;
455
+ private reconcileOrThrow;
456
+ private readRecord;
457
+ private writeRecord;
458
+ private deleteRecord;
459
+ private snapshot;
460
+ private unprotected;
461
+ private coverage;
462
+ private setStatus;
463
+ /**
464
+ * Single-flight: a trigger during a run flags exactly one rerun inside the same promise, so
465
+ * `sync()`/`enable()` await real convergence instead of a mid-run snapshot.
466
+ */
467
+ private reconcile;
468
+ private pass;
469
+ }
470
+ //#endregion
471
+ //#region src/meta/errors.d.ts
472
+ /** Meta-sync error categories — messages never carry event or document content. */
473
+ type MetaSyncErrorCategory = "network" | "internal" | "unsupported" | "corrupt";
474
+ export declare class MetaSyncError extends Error {
475
+ readonly category: MetaSyncErrorCategory;
476
+ constructor(category: MetaSyncErrorCategory, message: string, cause?: unknown);
477
+ }
478
+ //#endregion
479
+ //#region src/meta/controller.d.ts
480
+ type MetaSyncRelayPort = {
481
+ readonly fetch: (filter: Filter) => Promise<ReadonlyArray<Event>>;
482
+ readonly publish: (event: Event) => Promise<void>;
483
+ };
484
+ type MetaSyncConfig = {
485
+ /** The `d` tag that names this wallet's metadata document. */
486
+ readonly tag: string;
487
+ readonly relays: MetaSyncRelayPort;
488
+ };
489
+ type MetaSyncWalletStatus = {
490
+ readonly status: "unavailable" | "syncing" | "synced" | "error";
491
+ readonly error?: MetaSyncErrorCategory;
492
+ };
493
+ type MetaSyncState = {
494
+ readonly wallets: Readonly<Record<string, MetaSyncWalletStatus>>;
495
+ };
496
+ type MetaSyncControllerOptions = {
497
+ readonly wallet: Wallet;
498
+ readonly accounts: AccountTreeController;
499
+ /** `nostr(wallet)` — the signer is requested per wallet for its group-0 derived key. */
500
+ readonly nostr: {
501
+ readonly signer: (ref: NostrKeyRef) => WalletSigner;
502
+ };
503
+ readonly metaSync: MetaSyncConfig | undefined;
504
+ readonly logger: EngineLogger;
505
+ readonly now: () => number;
506
+ };
507
+ /**
508
+ * Wallet-metadata sync over kind-30078 addressable events: one NIP-44-encrypted document per
509
+ * wallet, authored by its group-0 derived key. Pull always precedes publish in an unlock session —
510
+ * replaceable events would otherwise let a stale local tree overwrite a newer remote document.
511
+ * Debounced publishes are per wallet; a generation counter cancels in-flight write-backs on lock.
512
+ */
513
+ export declare class MetaSyncController extends Controller<MetaSyncState> {
514
+ private readonly options;
515
+ private generation;
516
+ private readonly pulled;
517
+ private readonly lastRemoteCreatedAt;
518
+ private readonly lastExported;
519
+ private readonly timers;
520
+ private readonly inflight;
521
+ private accountsUnsubscribe;
522
+ constructor(options: MetaSyncControllerOptions);
523
+ /** Unlocked: pull every wallet whose group-0 identity exists, then watch the tree. */
524
+ init(): Promise<void>;
525
+ destroy(): Promise<void>;
526
+ /** Locked: cancel debounces and in-flight write-backs, forget the pull/publish bookkeeping. */
527
+ lock(): Promise<void>;
528
+ /** Snapshot changed while unlocked: pull newly-seen wallets, drop removed ones. */
529
+ reconcile(): void;
530
+ /** Manual re-pull of every wallet that can sync. */
531
+ sync(): Promise<void>;
532
+ private pullAll;
533
+ private pubkeyFor;
534
+ private signerFor;
535
+ private setStatus;
536
+ private pullOne;
537
+ private filterFor;
538
+ private acceptableEvent;
539
+ /** Snapshot the wallet's exported doc — the debounce publishes only on a real change. */
540
+ private rememberExport;
541
+ private onTreeChange;
542
+ private publishNow;
543
+ }
544
+ //#endregion
545
+ //#region src/payments/parse.d.ts
546
+ type PaymentOption = {
547
+ readonly kind: "spark";
548
+ readonly address: string;
549
+ } | {
550
+ readonly kind: "spark-invoice";
551
+ readonly invoice: string;
552
+ readonly amountSats?: number;
553
+ readonly memo?: string;
554
+ readonly expiresAt?: number;
555
+ } | {
556
+ readonly kind: "lightning";
557
+ readonly invoice: string;
558
+ readonly amountSats?: number;
559
+ readonly paymentHash: string;
560
+ readonly description?: string;
561
+ readonly expiresAt: number;
562
+ } | {
563
+ readonly kind: "lnurl-pay";
564
+ readonly request: LnurlPayRequest;
565
+ readonly address?: string;
566
+ } | {
567
+ readonly kind: "lnurl-withdraw";
568
+ readonly request: LnurlWithdrawRequest;
569
+ } | {
570
+ readonly kind: "bitcoin";
571
+ readonly address: string;
572
+ };
573
+ type PaymentRequest = {
574
+ readonly options: ReadonlyArray<PaymentOption>;
575
+ readonly amountSats?: number;
576
+ readonly label?: string;
577
+ readonly message?: string;
578
+ };
579
+ type PaymentParse = {
580
+ readonly kind: "request";
581
+ readonly request: PaymentRequest;
582
+ } | {
583
+ readonly kind: "lnurl";
584
+ readonly url: string;
585
+ readonly address?: string;
586
+ } | {
587
+ readonly kind: "unsupported";
588
+ readonly format: "bolt12" | "silent-payment" | "lnurl-auth" | "spark-token-invoice" | "nostr-wallet-connect";
589
+ } | {
590
+ readonly kind: "invalid";
591
+ readonly reason: "network" | "expired" | "amount-mismatch" | "malformed";
592
+ } | {
593
+ readonly kind: "unrecognized";
594
+ };
595
+ type ParseOptions = {
596
+ readonly network: SparkNetwork;
597
+ readonly now: number;
598
+ };
599
+ /** `input` → PaymentParse. Never throws: every failure is a structured result. */
600
+ export declare function parsePayment(input: string, options: ParseOptions): PaymentParse;
601
+ //#endregion
602
+ //#region src/payments/pay.d.ts
603
+ type PayParams = {
604
+ readonly account: SparkAccountRef;
605
+ readonly option: PaymentOption;
606
+ /** Caller-supplied amount — required by open invoices, the `spark` option, and LNURL flows. */
607
+ readonly amountSats?: number;
608
+ readonly comment?: string;
609
+ readonly maxFeeSats?: number;
610
+ readonly speed?: "FAST" | "MEDIUM" | "SLOW";
611
+ readonly verify: UnlockFactor;
612
+ };
613
+ type PayResult = {
614
+ readonly kind: "spark";
615
+ readonly transfer: SparkTransfer;
616
+ } | {
617
+ readonly kind: "lightning";
618
+ readonly payment: LightningPayment;
619
+ } | {
620
+ readonly kind: "withdraw";
621
+ readonly exit: CooperativeExitResult;
622
+ } | {
623
+ readonly kind: "lnurl-withdraw";
624
+ readonly invoice: LightningInvoice;
625
+ };
626
+ //#endregion
627
+ //#region src/payments/zap.d.ts
628
+ /** A profile zap names the recipient pubkey; an event zap carries the event it zaps. */
629
+ type ZapRecipient = {
630
+ readonly pubkey: string;
631
+ readonly lightning: string;
632
+ } | {
633
+ readonly event: Event;
634
+ readonly lightning: string;
635
+ };
636
+ type PrepareZapParams = {
637
+ /** Any `NostrSigner` — wallet key, external key, NIP-07/46 — signs the 9734. */
638
+ readonly signer: NostrSigner;
639
+ readonly recipient: ZapRecipient;
640
+ readonly amountSats: number;
641
+ readonly comment?: string | undefined;
642
+ readonly relays: ReadonlyArray<string>;
643
+ readonly network: SparkNetwork;
644
+ };
645
+ type PrepareZapContext = {
646
+ readonly fetch: typeof globalThis.fetch;
647
+ readonly now: () => number;
648
+ };
649
+ /** Everything a payer needs: the signed 9734, the invoice, and the note fields. */
650
+ type PreparedZap = {
651
+ readonly zapRequest: Event;
652
+ readonly invoice: string;
653
+ /** Hex payment hash decoded from the invoice. */
654
+ readonly paymentHash: string;
655
+ /** Millisecond expiry decoded from the invoice. */
656
+ readonly expiresAt: number;
657
+ readonly amountSats: number;
658
+ /** The LNURL service's 9735 signing pubkey. */
659
+ readonly receiptPubkey: string;
660
+ readonly recipientPubkey: string;
661
+ readonly eventId?: string;
662
+ readonly comment?: string;
663
+ /** The lud16 or LNURL as given. */
664
+ readonly lightning: string;
665
+ };
666
+ type PayZapOptions = {
667
+ readonly maxFeeSats: number;
668
+ readonly verify: UnlockFactor;
669
+ readonly now: () => number;
670
+ };
671
+ /** Everything up to payment: resolve LNURL, sign the 9734, request the invoice. */
672
+ export declare function prepareZap(params: PrepareZapParams, context: PrepareZapContext): Promise<PreparedZap>;
673
+ /**
674
+ * Pays a prepared zap through the controller's approve-first path. A nearly-expired invoice is
675
+ * rejected before approval — the user may have sat on the confirm screen since `prepareZap`.
676
+ */
677
+ export declare function payZap(spark: SparkController, account: SparkAccountRef, prepared: PreparedZap, options: PayZapOptions): Promise<LightningPayment>;
678
+ //#endregion
679
+ //#region src/engine.d.ts
680
+ type EngineOptions = {
681
+ readonly wallet: Wallet;
682
+ /** Every Spark account the engine touches lives on this network. */
683
+ readonly sparkNetwork: SparkNetwork;
684
+ readonly fetch: typeof globalThis.fetch;
685
+ readonly random: (length: number) => Uint8Array;
686
+ readonly now: () => number;
687
+ readonly logger: EngineLogger;
688
+ readonly openStore: () => Promise<EngineStore>;
689
+ /** Cloud file transport for the backup controller; absent → `unavailable`. */
690
+ readonly cloud?: CloudStore;
691
+ /** Kind-30078 wallet-metadata sync; absent → every wallet reports `unavailable`. */
692
+ readonly metaSync?: MetaSyncConfig;
693
+ /** Test seam — defaults to the wallet's real spark proxy. */
694
+ readonly sparkApis?: SparkControllerOptions["spark"];
695
+ };
696
+ type WalletEngine = {
697
+ readonly spark: SparkController;
698
+ readonly accounts: AccountTreeController;
699
+ readonly metaSync: MetaSyncController;
700
+ readonly backup: BackupController;
701
+ readonly zaps: {
702
+ readonly prepare: (params: PrepareZapParams) => Promise<PreparedZap>;
703
+ readonly pay: (account: SparkAccountRef, prepared: PreparedZap, options: Omit<PayZapOptions, "now">) => Promise<LightningPayment>;
704
+ };
705
+ readonly payments: {
706
+ readonly parse: (input: string, network: SparkNetwork) => PaymentParse;
707
+ readonly resolve: (input: string, network: SparkNetwork) => Promise<PaymentParse>;
708
+ readonly pay: (params: PayParams) => Promise<unknown>;
709
+ };
710
+ readonly unsubscribe: () => void;
711
+ };
712
+ export declare function createWalletEngine(options: EngineOptions): WalletEngine;
713
+ //#endregion
714
+ //#region src/backup/memory.d.ts
715
+ /** In-memory CloudStore for tests and the playground; swap the Map for cross-engine sharing. */
716
+ export declare function createMemoryCloudStore(backing?: Map<string, string>): CloudStore;
717
+ //#endregion
718
+ //#region src/spark/errors.d.ts
719
+ /** SparkController error categories — never carry request bytes or secrets. */
720
+ type SparkErrorCategory = "network" | "approval-required" | "insufficient-funds" | "rejected" | "internal";
721
+ export declare class SparkControllerError extends Error {
722
+ readonly category: SparkErrorCategory;
723
+ constructor(category: SparkErrorCategory, message: string, cause?: unknown);
724
+ }
725
+ //#endregion
726
+ //#region src/accounts/errors.d.ts
727
+ /** Account-tree error categories — messages never carry secrets. */
728
+ type AccountTreeErrorCategory = "invalid-input" | "rejected" | "not-found" | "locked";
729
+ export declare class AccountTreeError extends Error {
730
+ readonly category: AccountTreeErrorCategory;
731
+ constructor(category: AccountTreeErrorCategory, message: string, cause?: unknown);
732
+ }
733
+ //#endregion
734
+ //#region src/payments/errors.d.ts
735
+ /** Payment layer error categories — messages never carry remote or secret content. */
736
+ type PaymentErrorCategory = "invalid-input" | "network" | "rejected";
737
+ export declare class PaymentError extends Error {
738
+ readonly category: PaymentErrorCategory;
739
+ constructor(category: PaymentErrorCategory, message: string, cause?: unknown);
740
+ }
741
+ //#endregion
742
+ //#region src/payments/resolve.d.ts
743
+ type ResolveOptions = ParseOptions & {
744
+ readonly fetch: typeof globalThis.fetch;
745
+ };
746
+ /**
747
+ * `parsePayment` plus LNURL dereferencing: an `lnurl` result is fetched and turned into a concrete
748
+ * `lnurl-pay` / `lnurl-withdraw` request (`tag=login` endpoints report as unsupported). Network
749
+ * failures throw `PaymentError("network")` — remote error bodies never enter the message.
750
+ */
751
+ export declare function resolvePayment(input: string, options: ResolveOptions): Promise<PaymentParse>;
752
+ //#endregion
753
+ //#region src/spark/exit-chain-codec.d.ts
754
+ export declare function encodeExitChain(chain: SparkExitChain): Uint8Array;
755
+ export declare function decodeExitChain(bytes: Uint8Array): SparkExitChain;
756
+ //#endregion
757
+ export type { AccountGroup, AccountTreeState, BackupControllerState, BackupErrorCategory, CloudStore, ControllerOptions, EngineCollection, EngineErrorCode, EngineLogger, EngineOptions, EngineStore, EventBus, MetaDoc, MetaDocGroup, MetaDocWallet, MetaSyncConfig, MetaSyncRelayPort, MetaSyncState, MetaSyncWalletStatus, PayResult, PayZapOptions, PaymentNote, PaymentOption, PaymentParse, PaymentRequest, PrepareZapContext, PrepareZapParams, PreparedZap, SparkAccountState, SparkActivity, SparkClientFactory, SparkControllerOptions, SparkControllerState, SparkErrorCategory, WalletEngine, ZapRecipient };
758
+ //# sourceMappingURL=index.d.mts.map