@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.
- package/dist/idb.d.mts +28 -0
- package/dist/idb.mjs +134 -0
- package/dist/idb.mjs.map +1 -0
- package/dist/index.d.mts +758 -0
- package/dist/index.mjs +3420 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +68 -4
- package/README.md +0 -3
package/dist/index.d.mts
ADDED
|
@@ -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
|