@unicitylabs/sphere-sdk 0.9.1-dev.6 → 0.9.1-dev.7

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,1770 @@
1
+ /**
2
+ * Oracle Provider Interface
3
+ * Platform-independent Unicity oracle abstraction
4
+ *
5
+ * Post v1-cutover the oracle is a thin NETWORK-CONFIG provider: it loads the
6
+ * root trust base (JSON) and exposes the gateway URL + API key. The v2 token
7
+ * engine (token-engine/) builds its own aggregator clients from these — no
8
+ * SDK client objects cross this boundary anymore.
9
+ *
10
+ * `validateToken` survives as a best-effort JSON-RPC check for LEGACY v1 TXF
11
+ * tokens still present in storage (display-path only); v2 blob tokens are
12
+ * verified via the engine (`engine.verify` + `engine.isSpent`).
13
+ */
14
+
15
+ interface OracleProvider extends BaseProvider {
16
+ /**
17
+ * Initialize the provider. Loads the trust base JSON via the configured
18
+ * platform loader when none is passed explicitly.
19
+ *
20
+ * @param trustBaseJson - Optional raw trust-base JSON (overrides the loader).
21
+ */
22
+ initialize(trustBaseJson?: unknown): Promise<void>;
23
+ /**
24
+ * Validate a LEGACY v1 TXF token against the aggregator (best-effort RPC).
25
+ * v2 blob tokens never reach this — they are verified via the token engine.
26
+ */
27
+ validateToken(tokenData: unknown): Promise<ValidationResult>;
28
+ /** Raw trust-base JSON (the engine parses it; the networkId comes from it). */
29
+ getTrustBaseJson(): unknown | null;
30
+ /** Gateway (aggregator) base URL. */
31
+ getAggregatorUrl(): string;
32
+ /** Gateway API key, when the gateway requires one (e.g. testnet2). */
33
+ getApiKey(): string | undefined;
34
+ }
35
+ interface ValidationResult {
36
+ valid: boolean;
37
+ spent: boolean;
38
+ error?: string;
39
+ stateHash?: string;
40
+ }
41
+
42
+ /**
43
+ * token-engine/types.ts — the FROZEN, sphere-domain contract surface.
44
+ *
45
+ * Design rule (anti-corruption): the public ITokenEngine port speaks ONLY
46
+ * sphere-domain types — `Uint8Array` pubkeys, `string` coin ids, `bigint`
47
+ * amounts, plain enums. The v2 state-transition SDK has exactly ONE foothold
48
+ * here: `SphereToken.sdkToken`, an OPAQUE handle. Callers must treat it as
49
+ * opaque (store it, hand it back to the engine) and never call methods on it —
50
+ * they cannot, since the ESLint boundary forbids them importing the SDK.
51
+ *
52
+ * Both migration tracks freeze against this file:
53
+ * Track A implements it (token-engine internals).
54
+ * Track B codes callers against it (using FakeTokenEngine until A lands).
55
+ */
56
+
57
+ /**
58
+ * Storage-and-display token. Format version + network let storage migrate
59
+ * independently of the SDK's own CBOR. The decoded value is re-derivable from
60
+ * `token`, so it is NOT stored — only cached at runtime on SphereToken.value.
61
+ */
62
+ interface TokenBlob {
63
+ /** Blob format version (sphere storage migrations; independent of SDK CBOR). */
64
+ readonly v: number;
65
+ /** NetworkId.id the token belongs to (mainnet=1 / testnet=2 / local=3). */
66
+ readonly network: number;
67
+ /**
68
+ * Genesis-stable token id — 64-char lowercase hex of the v2 `TokenId.bytes`
69
+ * (same across every state of the token). Stored on the blob so dedup / listing
70
+ * / tombstone keys need no engine call. `createTokenStateKey = ${tokenId}_${hash}`.
71
+ */
72
+ readonly tokenId: string;
73
+ /** CBOR bytes of the v2 Token (`Token.toCBOR()`). */
74
+ readonly token: Uint8Array;
75
+ }
76
+
77
+ /**
78
+ * Transport Provider Interface
79
+ * Platform-independent P2P messaging abstraction
80
+ */
81
+
82
+ /**
83
+ * P2P messaging transport provider
84
+ */
85
+ interface TransportProvider extends BaseProvider {
86
+ /**
87
+ * Set identity for signing/encryption.
88
+ * If the transport is already connected, reconnects with the new identity.
89
+ */
90
+ setIdentity(identity: FullIdentity): void | Promise<void>;
91
+ /**
92
+ * Send encrypted direct message
93
+ * @param recipientTransportPubkey - Transport-specific pubkey for messaging
94
+ * @returns Event ID
95
+ */
96
+ sendMessage(recipientTransportPubkey: string, content: string): Promise<string>;
97
+ /**
98
+ * Subscribe to incoming direct messages
99
+ * @returns Unsubscribe function
100
+ */
101
+ onMessage(handler: MessageHandler): () => void;
102
+ /**
103
+ * Send token transfer payload
104
+ * @param recipientTransportPubkey - Transport-specific pubkey for messaging
105
+ * @returns Event ID
106
+ */
107
+ sendTokenTransfer(recipientTransportPubkey: string, payload: TokenTransferPayload): Promise<string>;
108
+ /**
109
+ * Subscribe to incoming token transfers
110
+ * @returns Unsubscribe function
111
+ */
112
+ onTokenTransfer(handler: TokenTransferHandler): () => void;
113
+ /**
114
+ * Resolve any identifier to full peer information.
115
+ * Accepts @nametag, bare nametag, DIRECT://, PROXY://, L1 address, chain pubkey, or transport pubkey.
116
+ * @param identifier - Any supported identifier format
117
+ * @returns PeerInfo or null if not found
118
+ */
119
+ resolve?(identifier: string): Promise<PeerInfo | null>;
120
+ /**
121
+ * Resolve nametag to public key
122
+ */
123
+ resolveNametag?(nametag: string): Promise<string | null>;
124
+ /**
125
+ * Resolve nametag to full peer information
126
+ * Returns transportPubkey, chainPubkey, l1Address, directAddress
127
+ */
128
+ resolveNametagInfo?(nametag: string): Promise<PeerInfo | null>;
129
+ /**
130
+ * Resolve a DIRECT://, PROXY://, or L1 address to full peer info.
131
+ * Performs reverse lookup: address → binding event → PeerInfo.
132
+ * @param address - L3 address (DIRECT://... or PROXY://...) or L1 address (alpha1...)
133
+ * @returns PeerInfo or null if no binding found for this address
134
+ */
135
+ resolveAddressInfo?(address: string): Promise<PeerInfo | null>;
136
+ /**
137
+ * Resolve transport pubkey to full peer info.
138
+ * Queries binding events authored by the given transport pubkey.
139
+ * @param transportPubkey - Transport-specific pubkey (e.g. 64-char hex string)
140
+ * @returns PeerInfo or null if no binding found
141
+ */
142
+ resolveTransportPubkeyInfo?(transportPubkey: string): Promise<PeerInfo | null>;
143
+ /**
144
+ * Batch-resolve multiple transport pubkeys to peer info.
145
+ * Used for HD address discovery: derives transport pubkeys for indices 0..N
146
+ * and queries binding events in a single batch.
147
+ * @param transportPubkeys - Array of transport-specific pubkeys to look up
148
+ * @returns Array of PeerInfo for pubkeys that have binding events (may be shorter than input)
149
+ */
150
+ discoverAddresses?(transportPubkeys: string[]): Promise<PeerInfo[]>;
151
+ /**
152
+ * Recover nametag for current identity by decrypting stored encrypted nametag
153
+ * Used after wallet import to recover associated nametag
154
+ * @returns Decrypted nametag or null if none found
155
+ */
156
+ recoverNametag?(): Promise<string | null>;
157
+ /**
158
+ * Publish identity binding event.
159
+ * Without nametag: publishes base binding (chainPubkey, l1Address, directAddress).
160
+ * With nametag: adds nametag hash, proxy address, encrypted nametag for recovery.
161
+ * Uses parameterized replaceable event (kind 30078, d=hash(nostrPubkey)).
162
+ * @returns true if successful, false if nametag is taken by another pubkey
163
+ */
164
+ publishIdentityBinding?(chainPubkey: string, l1Address: string, directAddress: string, nametag?: string): Promise<boolean>;
165
+ /**
166
+ * Subscribe to broadcast messages (global/channel)
167
+ */
168
+ subscribeToBroadcast?(tags: string[], handler: BroadcastHandler): () => void;
169
+ /**
170
+ * Publish broadcast message
171
+ */
172
+ publishBroadcast?(content: string, tags?: string[]): Promise<string>;
173
+ /**
174
+ * Send payment request to a recipient
175
+ * @param recipientTransportPubkey - Transport-specific pubkey for messaging
176
+ * @returns Event ID
177
+ */
178
+ sendPaymentRequest?(recipientTransportPubkey: string, request: PaymentRequestPayload): Promise<string>;
179
+ /**
180
+ * Subscribe to incoming payment requests
181
+ * @returns Unsubscribe function
182
+ */
183
+ onPaymentRequest?(handler: PaymentRequestHandler): () => void;
184
+ /**
185
+ * Send response to a payment request
186
+ * @param recipientTransportPubkey - Transport-specific pubkey for messaging
187
+ * @returns Event ID
188
+ */
189
+ sendPaymentRequestResponse?(recipientTransportPubkey: string, response: PaymentRequestResponsePayload): Promise<string>;
190
+ /**
191
+ * Subscribe to incoming payment request responses
192
+ * @returns Unsubscribe function
193
+ */
194
+ onPaymentRequestResponse?(handler: PaymentRequestResponseHandler): () => void;
195
+ /**
196
+ * Send a read receipt for a message
197
+ * @param recipientTransportPubkey - Transport pubkey of the message sender
198
+ * @param messageEventId - Event ID of the message being acknowledged
199
+ */
200
+ sendReadReceipt?(recipientTransportPubkey: string, messageEventId: string): Promise<void>;
201
+ /**
202
+ * Subscribe to incoming read receipts
203
+ * @returns Unsubscribe function
204
+ */
205
+ onReadReceipt?(handler: ReadReceiptHandler): () => void;
206
+ /**
207
+ * Send typing indicator to a recipient
208
+ * @param recipientTransportPubkey - Transport pubkey of the conversation partner
209
+ */
210
+ sendTypingIndicator?(recipientTransportPubkey: string): Promise<void>;
211
+ /**
212
+ * Subscribe to incoming typing indicators
213
+ * @returns Unsubscribe function
214
+ */
215
+ onTypingIndicator?(handler: TypingIndicatorHandler): () => void;
216
+ /**
217
+ * Send composing indicator to a recipient using NIP-44 encrypted gift wrap
218
+ * @param recipientTransportPubkey - Transport pubkey of the conversation partner
219
+ * @param content - JSON payload with senderNametag and expiresIn
220
+ */
221
+ sendComposingIndicator?(recipientTransportPubkey: string, content: string): Promise<void>;
222
+ /**
223
+ * Subscribe to incoming composing indicators
224
+ * @returns Unsubscribe function
225
+ */
226
+ onComposing?(handler: ComposingHandler): () => void;
227
+ /**
228
+ * Get list of configured relay URLs
229
+ */
230
+ getRelays?(): string[];
231
+ /**
232
+ * Get list of currently connected relay URLs
233
+ */
234
+ getConnectedRelays?(): string[];
235
+ /**
236
+ * Add a relay dynamically
237
+ * @returns true if added successfully
238
+ */
239
+ addRelay?(relayUrl: string): Promise<boolean>;
240
+ /**
241
+ * Remove a relay dynamically
242
+ * @returns true if removed successfully
243
+ */
244
+ removeRelay?(relayUrl: string): Promise<boolean>;
245
+ /**
246
+ * Check if a relay is configured
247
+ */
248
+ hasRelay?(relayUrl: string): boolean;
249
+ /**
250
+ * Check if a relay is currently connected
251
+ */
252
+ isRelayConnected?(relayUrl: string): boolean;
253
+ /**
254
+ * Set fallback 'since' timestamp for event subscriptions.
255
+ * Used when switching to an address that has never subscribed before.
256
+ * The transport uses this instead of 'now' as the initial since filter,
257
+ * ensuring events sent while the address was inactive are not missed.
258
+ * Consumed once by the next subscription setup, then cleared.
259
+ *
260
+ * @param sinceSeconds - Unix timestamp in seconds
261
+ */
262
+ setFallbackSince?(sinceSeconds: number): void;
263
+ /**
264
+ * Set fallback 'since' timestamp for DM (gift-wrap) subscriptions.
265
+ * Used when no persisted DM timestamp exists in storage (e.g. first connect).
266
+ * Consumed once by the next subscription setup, then cleared.
267
+ *
268
+ * @param sinceSeconds - Unix timestamp in seconds
269
+ */
270
+ setFallbackDmSince?(sinceSeconds: number): void;
271
+ /**
272
+ * Fetch pending events from transport (one-shot query).
273
+ * Creates a temporary subscription, processes events through normal handlers,
274
+ * and resolves after EOSE (End Of Stored Events).
275
+ */
276
+ fetchPendingEvents?(): Promise<void>;
277
+ /**
278
+ * Register a handler to be called when the chat subscription receives EOSE
279
+ * (End Of Stored Events), indicating that historical DMs have been delivered.
280
+ * The handler fires at most once per subscription lifecycle.
281
+ *
282
+ * @returns Unsubscribe function
283
+ */
284
+ onChatReady?(handler: () => void): () => void;
285
+ }
286
+ interface IncomingMessage {
287
+ id: string;
288
+ /** Transport-specific pubkey of sender */
289
+ senderTransportPubkey: string;
290
+ /** Sender's nametag (if known from NIP-17 unwrap) */
291
+ senderNametag?: string;
292
+ content: string;
293
+ timestamp: number;
294
+ encrypted: boolean;
295
+ /** Set when this is a self-wrap replay (sent message recovered from relay) */
296
+ isSelfWrap?: boolean;
297
+ /** Recipient pubkey — only present on self-wrap replays */
298
+ recipientTransportPubkey?: string;
299
+ }
300
+ type MessageHandler = (message: IncomingMessage) => void;
301
+ interface TokenTransferPayload {
302
+ /** Serialized token data */
303
+ token: string;
304
+ /** Inclusion proof */
305
+ proof: unknown;
306
+ /** Optional memo */
307
+ memo?: string;
308
+ /** Sender info */
309
+ sender?: {
310
+ /** Transport-specific pubkey */
311
+ transportPubkey: string;
312
+ nametag?: string;
313
+ };
314
+ }
315
+ interface IncomingTokenTransfer {
316
+ id: string;
317
+ /** Transport-specific pubkey of sender */
318
+ senderTransportPubkey: string;
319
+ payload: TokenTransferPayload;
320
+ timestamp: number;
321
+ }
322
+ type TokenTransferHandler = (transfer: IncomingTokenTransfer) => void | Promise<void>;
323
+ interface PaymentRequestPayload {
324
+ /** Amount requested (in smallest units) */
325
+ amount: string | bigint;
326
+ /** Coin/token type ID */
327
+ coinId: string;
328
+ /** Message/memo for recipient */
329
+ message?: string;
330
+ /** Recipient's nametag (who should pay) */
331
+ recipientNametag?: string;
332
+ /** Custom metadata */
333
+ metadata?: Record<string, unknown>;
334
+ }
335
+ interface IncomingPaymentRequest {
336
+ /** Event ID */
337
+ id: string;
338
+ /** Transport-specific pubkey of sender */
339
+ senderTransportPubkey: string;
340
+ /** Sender's nametag (if included in encrypted content) */
341
+ senderNametag?: string;
342
+ /** Parsed request data */
343
+ request: {
344
+ requestId: string;
345
+ amount: string;
346
+ coinId: string;
347
+ message?: string;
348
+ recipientNametag?: string;
349
+ metadata?: Record<string, unknown>;
350
+ };
351
+ /** Timestamp */
352
+ timestamp: number;
353
+ }
354
+ type PaymentRequestHandler = (request: IncomingPaymentRequest) => void;
355
+ type PaymentRequestResponseType = 'accepted' | 'rejected' | 'paid';
356
+ interface PaymentRequestResponsePayload {
357
+ /** Original request ID */
358
+ requestId: string;
359
+ /** Response type */
360
+ responseType: PaymentRequestResponseType;
361
+ /** Optional message */
362
+ message?: string;
363
+ /** Transfer ID (if paid) */
364
+ transferId?: string;
365
+ }
366
+ interface IncomingPaymentRequestResponse {
367
+ /** Event ID */
368
+ id: string;
369
+ /** Transport-specific pubkey of responder */
370
+ responderTransportPubkey: string;
371
+ /** Parsed response data */
372
+ response: {
373
+ requestId: string;
374
+ responseType: PaymentRequestResponseType;
375
+ message?: string;
376
+ transferId?: string;
377
+ };
378
+ /** Timestamp */
379
+ timestamp: number;
380
+ }
381
+ type PaymentRequestResponseHandler = (response: IncomingPaymentRequestResponse) => void;
382
+ interface IncomingBroadcast {
383
+ id: string;
384
+ /** Transport-specific pubkey of author */
385
+ authorTransportPubkey: string;
386
+ content: string;
387
+ tags: string[];
388
+ timestamp: number;
389
+ }
390
+ type BroadcastHandler = (broadcast: IncomingBroadcast) => void;
391
+ /**
392
+ * Resolved peer identity information.
393
+ * Returned by resolve methods — contains all public address formats for a peer.
394
+ * The nametag field is optional (only present if a nametag is registered).
395
+ */
396
+ interface PeerInfo {
397
+ /** Nametag name (without @), if registered */
398
+ nametag?: string;
399
+ /** Transport-specific pubkey (for messaging/encryption) */
400
+ transportPubkey: string;
401
+ /** 33-byte compressed secp256k1 public key (for L3 chain) */
402
+ chainPubkey: string;
403
+ /** L1 address (alpha1...) */
404
+ l1Address: string;
405
+ /** L3 DIRECT address (DIRECT://...) */
406
+ directAddress: string;
407
+ /** Event timestamp */
408
+ timestamp: number;
409
+ }
410
+ interface IncomingReadReceipt {
411
+ /** Transport-specific pubkey of the sender who read the message */
412
+ senderTransportPubkey: string;
413
+ /** Event ID of the message that was read */
414
+ messageEventId: string;
415
+ /** Timestamp */
416
+ timestamp: number;
417
+ }
418
+ type ReadReceiptHandler = (receipt: IncomingReadReceipt) => void;
419
+ interface IncomingTypingIndicator {
420
+ /** Transport-specific pubkey of the sender who is typing */
421
+ senderTransportPubkey: string;
422
+ /** Sender's nametag (if known) */
423
+ senderNametag?: string;
424
+ /** Timestamp */
425
+ timestamp: number;
426
+ }
427
+ type TypingIndicatorHandler = (indicator: IncomingTypingIndicator) => void;
428
+ type ComposingHandler = (indicator: ComposingIndicator) => void;
429
+
430
+ /**
431
+ * transport/delivery-provider.ts — the `DeliveryProvider` port (sdk-changes S7,
432
+ * covenant §3.1-6).
433
+ *
434
+ * The seam that keeps the delivery rail swappable. In Unicity, a transfer —
435
+ * after certification — is just a file handoff, so the port is deliberately
436
+ * tiny: hand a finished token blob to a recipient, pull incoming deliveries,
437
+ * acknowledge them. `WalletApiMailboxProvider`
438
+ * (impl/shared/wallet-api/WalletApiMailboxProvider.ts) is the reference
439
+ * implementation; anything that can move a file can implement it (the port
440
+ * shape must not preclude the old Nostr transport or a future federated
441
+ * transport — neither is a deliverable here).
442
+ *
443
+ * Normative shapes (sdk-changes S7):
444
+ * - `DeliveryReceipt = { deliveryId }`
445
+ * - `IncomingDelivery = { deliveryId, transferId?, senderPubkey?, memo?,
446
+ * fetchBlob(), cursor }`
447
+ * - `deliveryId` is the **content-derived** entry id —
448
+ * `hex(SHA-256(tokenId bytes ‖ stateHash bytes))` — NEVER a server-assigned
449
+ * row id or seq (covenant §3.1-4; the contract suite asserts it). It is
450
+ * computed client-side ({@link computeDeliveryId}) and must equal the
451
+ * backend's `entry_id` (ARCHITECTURE §6).
452
+ * - **Custody is a composition-time property, not a per-call flag**:
453
+ * implementations take `custody: 'inventory' | 'external'` at construction
454
+ * and every ack sends the corresponding `intoInventory` — delivery-only
455
+ * safety must never depend on remembering an option at a call site.
456
+ * - Implementations MUST keep a **persistent `(tokenId, stateHash)` seen-set**
457
+ * for incoming deliveries: the recipient-side replay guard is part of the
458
+ * port contract, not a server promise (the recipient never trusts the
459
+ * backend — ARCHITECTURE §8.2). `deliveryId` is the canonical hash encoding
460
+ * of exactly that pair, so a persistent deliveryId set satisfies this.
461
+ */
462
+ /** Receipt for a delivered blob. `deliveryId` is content-derived — see module doc. */
463
+ interface DeliveryReceipt {
464
+ deliveryId: string;
465
+ }
466
+ /** Options for {@link DeliveryProvider.deliver}. */
467
+ interface DeliverOptions {
468
+ /**
469
+ * The send's transferId (the E.3 intent id / realization seed). Recorded
470
+ * with the delivery so the recipient can group multi-token payments and the
471
+ * backend can evidence-check the sender's removals (ARCHITECTURE §5.3/§6).
472
+ */
473
+ transferId: string;
474
+ /** Optional human memo. Implementations encrypt it client-side (S6). */
475
+ memo?: string;
476
+ }
477
+ /** One incoming delivery pulled from the feed. */
478
+ interface IncomingDelivery {
479
+ /** Content-derived id — `hex(SHA-256(tokenId bytes ‖ stateHash bytes))`. */
480
+ deliveryId: string;
481
+ /** The sender's transferId, when the transport carries it. */
482
+ transferId?: string;
483
+ /** The sender's pubkey, when the transport carries it. */
484
+ senderPubkey?: string;
485
+ /** Decrypted memo (S6), when present and decryptable. */
486
+ memo?: string;
487
+ /** Fetch the finished token blob bytes (the encoded TokenBlob). */
488
+ fetchBlob(): Promise<Uint8Array>;
489
+ /** Transport-local resume cursor (opaque to callers). */
490
+ cursor: string;
491
+ }
492
+ type DeliveryDisposition = 'claimed' | 'rejected';
493
+ /**
494
+ * Custody mode (composition-time): `'inventory'` — acknowledged deliveries
495
+ * enter the wallet-api inventory (the full wallet-api preset); `'external'` —
496
+ * the app's own storage keeps custody and acks perform ZERO inventory writes
497
+ * (the delivery-only preset, ARCHITECTURE §6 "delivery-only claim").
498
+ */
499
+ type DeliveryCustody = 'inventory' | 'external';
500
+ interface DeliveryProvider {
501
+ /** Composition-time custody property — never a per-call flag (S7). */
502
+ readonly custody: DeliveryCustody;
503
+ /**
504
+ * Bind the wallet identity (optional — implementations that authenticate or
505
+ * encrypt per-wallet need it; mirrors `TokenStorageProvider.setIdentity`).
506
+ */
507
+ setIdentity?(identity: {
508
+ privateKey: string;
509
+ chainPubkey: string;
510
+ }): void;
511
+ /**
512
+ * Hand a finished token blob to a recipient. `recipientPubkey` is the
513
+ * recipient's CHAIN pubkey (33-byte compressed secp256k1, hex) — the
514
+ * canonical Unicity identity (ARCHITECTURE §4); transports that address
515
+ * recipients differently resolve it themselves.
516
+ *
517
+ * MUST be idempotent per (token, state): re-delivering the same finished
518
+ * blob — including after the recipient claimed — succeeds and returns the
519
+ * same content-derived `deliveryId` (ARCHITECTURE §6 deposit idempotency).
520
+ */
521
+ deliver(recipientPubkey: string, blob: Uint8Array, options: DeliverOptions): Promise<DeliveryReceipt>;
522
+ /**
523
+ * Pull-based feed of incoming deliveries since the given transport-local
524
+ * cursor (or the provider's persisted cursor when omitted). Yields only
525
+ * deliveries not yet in the persistent seen-set; completes when the feed is
526
+ * drained — callers re-invoke on poll/wake. Feeds the existing
527
+ * transport-agnostic `handleV2Transfer` (sdk-changes S3).
528
+ */
529
+ incoming(sinceCursor?: string): AsyncIterable<IncomingDelivery>;
530
+ /**
531
+ * Acknowledge a delivery: `'claimed'` accepts it (with the provider's
532
+ * composition-time custody), `'rejected'` marks it locally-unverifiable —
533
+ * terminal for discovery only (the entry stays claimable server-side and
534
+ * its blob is retained — ARCHITECTURE §6). Both record the delivery in the
535
+ * persistent seen-set.
536
+ */
537
+ ack(deliveryId: string, disposition: DeliveryDisposition): Promise<void>;
538
+ /**
539
+ * Optional wake hook: `callback` fires when new deliveries may be available
540
+ * (e.g. a WS nudge — never a correctness dependency, ARCHITECTURE §9).
541
+ * Returns an unsubscribe function.
542
+ */
543
+ onWake?(callback: () => void): () => void;
544
+ /**
545
+ * Late-bind the backend-true (tokenId, stateHash) derivation —
546
+ * `ITokenEngine.deliveryKeys`. Compositions are engine-less (the engine is
547
+ * built later); the module that owns both (PaymentsModule) binds this at
548
+ * init. Implementations that derive ids (S7) MUST use it and fail loudly if
549
+ * unbound; transports that don't derive may omit the method.
550
+ */
551
+ bindDeliveryKeys?(derive: (blobBytes: Uint8Array) => Promise<{
552
+ tokenId: string;
553
+ stateHash: string;
554
+ }>): void;
555
+ }
556
+
557
+ /**
558
+ * SDK2 Core Types
559
+ * Platform-independent type definitions
560
+ */
561
+ type ProviderStatus = 'disconnected' | 'connecting' | 'connected' | 'error';
562
+ interface ProviderMetadata {
563
+ readonly id: string;
564
+ readonly name: string;
565
+ readonly type: 'local' | 'cloud' | 'p2p' | 'network';
566
+ readonly description?: string;
567
+ }
568
+ interface BaseProvider extends ProviderMetadata {
569
+ connect(config?: unknown): Promise<void>;
570
+ disconnect(): Promise<void>;
571
+ isConnected(): boolean;
572
+ getStatus(): ProviderStatus;
573
+ }
574
+ interface Identity {
575
+ /** 33-byte compressed secp256k1 public key (for L3 chain) */
576
+ readonly chainPubkey: string;
577
+ /** L1 address (alpha1...) */
578
+ readonly l1Address: string;
579
+ /** L3 DIRECT address (DIRECT://...) */
580
+ readonly directAddress?: string;
581
+ readonly ipnsName?: string;
582
+ readonly nametag?: string;
583
+ }
584
+ interface FullIdentity extends Identity {
585
+ readonly privateKey: string;
586
+ }
587
+ interface ComposingIndicator {
588
+ readonly senderPubkey: string;
589
+ readonly senderNametag?: string;
590
+ readonly expiresIn: number;
591
+ }
592
+ /**
593
+ * Minimal data stored in persistent storage for a tracked address.
594
+ * Only contains user state — derived fields are computed on load.
595
+ */
596
+ interface TrackedAddressEntry {
597
+ /** HD derivation index (0, 1, 2, ...) */
598
+ readonly index: number;
599
+ /** Whether this address is hidden from UI display */
600
+ hidden: boolean;
601
+ /** Timestamp (ms) when this address was first activated */
602
+ readonly createdAt: number;
603
+ /** Timestamp (ms) of last modification */
604
+ updatedAt: number;
605
+ }
606
+
607
+ /**
608
+ * Storage Provider Interface
609
+ * Platform-independent storage abstraction
610
+ */
611
+
612
+ /**
613
+ * Basic key-value storage provider
614
+ * All operations are async for platform flexibility
615
+ */
616
+ interface StorageProvider extends BaseProvider {
617
+ /**
618
+ * Set identity for scoped storage
619
+ */
620
+ setIdentity(identity: FullIdentity): void;
621
+ /**
622
+ * Get value by key
623
+ */
624
+ get(key: string): Promise<string | null>;
625
+ /**
626
+ * Set value by key
627
+ */
628
+ set(key: string, value: string): Promise<void>;
629
+ /**
630
+ * Remove key
631
+ */
632
+ remove(key: string): Promise<void>;
633
+ /**
634
+ * Check if key exists
635
+ */
636
+ has(key: string): Promise<boolean>;
637
+ /**
638
+ * Get all keys with optional prefix filter
639
+ */
640
+ keys(prefix?: string): Promise<string[]>;
641
+ /**
642
+ * Clear all keys with optional prefix filter
643
+ */
644
+ clear(prefix?: string): Promise<void>;
645
+ /**
646
+ * Save tracked addresses (only user state: index, hidden, timestamps)
647
+ */
648
+ saveTrackedAddresses(entries: TrackedAddressEntry[]): Promise<void>;
649
+ /**
650
+ * Load tracked addresses
651
+ */
652
+ loadTrackedAddresses(): Promise<TrackedAddressEntry[]>;
653
+ }
654
+ interface HistoryRecord {
655
+ /** Composite dedup key (primary key) — e.g. "RECEIVED_v5split_abc123" */
656
+ dedupKey: string;
657
+ /** UUID for public API consumption */
658
+ id: string;
659
+ type: 'SENT' | 'RECEIVED' | 'SPLIT' | 'MINT';
660
+ amount: string;
661
+ coinId: string;
662
+ symbol: string;
663
+ timestamp: number;
664
+ transferId?: string;
665
+ /** Genesis tokenId this entry relates to (used for dedup) */
666
+ tokenId?: string;
667
+ senderPubkey?: string;
668
+ senderAddress?: string;
669
+ senderNametag?: string;
670
+ recipientPubkey?: string;
671
+ recipientAddress?: string;
672
+ recipientNametag?: string;
673
+ /** Optional memo/message attached to the transfer */
674
+ memo?: string;
675
+ /** All token IDs in a combined transfer (V6 bundle breakdown) */
676
+ tokenIds?: Array<{
677
+ id: string;
678
+ amount: string;
679
+ source: 'split' | 'direct';
680
+ }>;
681
+ }
682
+ /** One fungible position of an inventory item. Amounts are `bigint` in types
683
+ * (decimal strings on the wallet-api wire — they exceed 2^53). */
684
+ interface InventoryAsset {
685
+ coinId: string;
686
+ amount: bigint;
687
+ }
688
+ /**
689
+ * One row of the inventory view — value metadata only, **no blobs**.
690
+ * `status: 'removed'` is a tombstone: the token left this inventory (spent, or
691
+ * handed off at claim) — the only way a stale device learns about removals
692
+ * (ARCHITECTURE §5.1).
693
+ */
694
+ interface InventoryItem {
695
+ /** Genesis-stable 64-hex token id. */
696
+ tokenId: string;
697
+ status: 'active' | 'removed';
698
+ /** Decoded value; absent when unknown (e.g. a tombstone). */
699
+ assets?: InventoryAsset[];
700
+ /** Owner change-cursor value at this row's last change (`?since=` deltas). */
701
+ seq: bigint;
702
+ }
703
+ /** Result of {@link TokenStorageProvider.listInventory}. */
704
+ interface InventoryView {
705
+ /** Cursor to resume deltas from (`listInventory(cursor)`). */
706
+ cursor: bigint;
707
+ /**
708
+ * Server sync epoch (ARCHITECTURE §5.4/§9): changes ONLY when a server
709
+ * restore invalidated cursor continuity. On a change, clients discard all
710
+ * persisted cursors, do a full pull, and re-PUT locally-known open intents.
711
+ * Local providers (no server) report a constant `0n`.
712
+ */
713
+ syncEpoch: bigint;
714
+ /** Truncated page (PAGE_LIMIT) — loop with `since = cursor` until false. */
715
+ more: boolean;
716
+ items: InventoryItem[];
717
+ }
718
+ /** An `added` entry of {@link TokenStorageProvider.applyDelta}: the token id
719
+ * plus the content-addressed blob-store key of its already-uploaded bytes. */
720
+ interface ApplyDeltaAdded {
721
+ tokenId: string;
722
+ /** Content-addressed blob key — `<network>/t/<hex(sha256(blob))>` (ARCHITECTURE §5.2). */
723
+ key: string;
724
+ }
725
+ interface ApplyDeltaOptions {
726
+ /**
727
+ * The wallet-api-storage + other-transport composition (ARCHITECTURE §5.3):
728
+ * removals cannot be evidence-checked against a mailbox deposit, so the
729
+ * server records them as `external` (never-collected blob retention).
730
+ */
731
+ externalDelivery?: boolean;
732
+ }
733
+ /** Result of an explicit `recoverRemoved()` maintenance run (sdk-changes S2). */
734
+ interface RecoverRemovedResult {
735
+ /** Tombstoned tokens re-verified and re-added (reactivation — ARCHITECTURE §5.3). */
736
+ recovered: string[];
737
+ /** Tokens the server 409'd as evidenced spends — "actually spent", tombstone kept. */
738
+ spent: string[];
739
+ /** Tombstones skipped (matched to a known local spend, or failed local verification). */
740
+ skipped: string[];
741
+ }
742
+ /**
743
+ * Storage result types
744
+ */
745
+ interface SaveResult {
746
+ success: boolean;
747
+ cid?: string;
748
+ error?: string;
749
+ timestamp: number;
750
+ }
751
+ interface LoadResult<T = unknown> {
752
+ success: boolean;
753
+ data?: T;
754
+ error?: string;
755
+ source: 'local' | 'remote' | 'cache';
756
+ timestamp: number;
757
+ }
758
+ interface SyncResult<T = unknown> {
759
+ success: boolean;
760
+ merged?: T;
761
+ added: number;
762
+ removed: number;
763
+ conflicts: number;
764
+ error?: string;
765
+ }
766
+ /**
767
+ * Token-specific storage provider
768
+ * Handles token persistence with sync capabilities
769
+ */
770
+ interface TokenStorageProvider<TData = unknown> extends BaseProvider {
771
+ /**
772
+ * Set identity for storage scope
773
+ */
774
+ setIdentity(identity: FullIdentity): void;
775
+ /**
776
+ * Initialize provider (called once after identity is set)
777
+ */
778
+ initialize(): Promise<boolean>;
779
+ /**
780
+ * Shutdown provider
781
+ */
782
+ shutdown(): Promise<void>;
783
+ /**
784
+ * Save token data
785
+ */
786
+ save(data: TData): Promise<SaveResult>;
787
+ /**
788
+ * Load token data
789
+ */
790
+ load(identifier?: string): Promise<LoadResult<TData>>;
791
+ /**
792
+ * Sync local data with remote
793
+ */
794
+ sync(localData: TData): Promise<SyncResult<TData>>;
795
+ /**
796
+ * The inventory view — value metadata only, never blobs.
797
+ *
798
+ * Without `since`: the current active rows. With `since`: every row changed
799
+ * after that cursor **including tombstones** (`status:'removed'`) — callers
800
+ * MUST apply tombstones (drop local entries) and loop while `more` is true.
801
+ * On a `syncEpoch` change the provider discards persisted cursors and
802
+ * resyncs from scratch (ARCHITECTURE §5.4).
803
+ *
804
+ * Whole-blob providers (no change journal) compute the view from the loaded
805
+ * set: all rows `'active'`, synthetic `seq`/`cursor`, `more: false`.
806
+ */
807
+ listInventory(since?: bigint): Promise<InventoryView>;
808
+ /**
809
+ * Fetch + decode one token blob on demand (a signed GET for remote
810
+ * providers; the loaded set for whole-blob providers). Throws when the
811
+ * token is unknown or carries no v2 blob.
812
+ */
813
+ getToken(tokenId: string): Promise<TokenBlob>;
814
+ /**
815
+ * Record a spend result: tombstone `spent` tokens, add `added` outputs.
816
+ * Idempotent by `transferId`. For wallet-api delivery this MUST be called
817
+ * **after** the mailbox deposit of the same `transferId` — the backend
818
+ * evidence-checks removals against it (ARCHITECTURE §5.3).
819
+ */
820
+ applyDelta(transferId: string, spent: string[], added: ApplyDeltaAdded[], opts?: ApplyDeltaOptions): Promise<void>;
821
+ /**
822
+ * Optional maintenance call (sdk-changes S2): re-fetch tombstoned tokens the
823
+ * client cannot match to a known spend, verify them locally, and re-add
824
+ * (reactivation). A server 409 means "actually spent" — keep the tombstone.
825
+ * Only meaningful for providers with server-side tombstones.
826
+ */
827
+ recoverRemoved?(): Promise<RecoverRemovedResult>;
828
+ /**
829
+ * Check if data exists
830
+ */
831
+ exists?(identifier?: string): Promise<boolean>;
832
+ /**
833
+ * Clear all data
834
+ */
835
+ clear?(): Promise<boolean>;
836
+ /**
837
+ * Create a new independent instance of this provider for a different address.
838
+ * Used by per-address module architecture — each address gets its own
839
+ * TokenStorageProvider instance to avoid cross-address data contamination.
840
+ * If not implemented, the provider cannot be used in multi-address mode.
841
+ */
842
+ createForAddress?(): TokenStorageProvider<TData>;
843
+ /**
844
+ * Subscribe to storage events
845
+ */
846
+ onEvent?(callback: StorageEventCallback): () => void;
847
+ /** Store a history entry (upsert by dedupKey) */
848
+ addHistoryEntry?(entry: HistoryRecord): Promise<void>;
849
+ /** Get all history entries sorted by timestamp descending */
850
+ getHistoryEntries?(): Promise<HistoryRecord[]>;
851
+ /** Check if a history entry exists by dedupKey */
852
+ hasHistoryEntry?(dedupKey: string): Promise<boolean>;
853
+ /** Clear all history entries */
854
+ clearHistory?(): Promise<void>;
855
+ /** Bulk import history entries (skip existing dedupKeys). Returns count of newly imported. */
856
+ importHistoryEntries?(entries: HistoryRecord[]): Promise<number>;
857
+ }
858
+ type StorageEventType = 'storage:saving' | 'storage:saved' | 'storage:loading' | 'storage:loaded' | 'storage:error' | 'storage:remote-updated' | 'sync:started' | 'sync:completed' | 'sync:conflict' | 'sync:error';
859
+ interface StorageEvent {
860
+ type: StorageEventType;
861
+ timestamp: number;
862
+ data?: unknown;
863
+ error?: string;
864
+ }
865
+ type StorageEventCallback = (event: StorageEvent) => void;
866
+ interface TxfStorageDataBase {
867
+ _meta: TxfMeta;
868
+ _tombstones?: TxfTombstone[];
869
+ _outbox?: TxfOutboxEntry[];
870
+ _sent?: TxfSentEntry[];
871
+ _invalid?: TxfInvalidEntry[];
872
+ _history?: HistoryRecord[];
873
+ [key: `_${string}`]: unknown;
874
+ }
875
+ interface TxfMeta {
876
+ version: number;
877
+ address: string;
878
+ ipnsName?: string;
879
+ formatVersion: string;
880
+ updatedAt: number;
881
+ }
882
+ interface TxfTombstone {
883
+ tokenId: string;
884
+ stateHash: string;
885
+ timestamp: number;
886
+ }
887
+ interface TxfOutboxEntry {
888
+ id: string;
889
+ status: string;
890
+ tokenId: string;
891
+ recipient: string;
892
+ createdAt: number;
893
+ data: unknown;
894
+ }
895
+ interface TxfSentEntry {
896
+ tokenId: string;
897
+ recipient: string;
898
+ txHash: string;
899
+ sentAt: number;
900
+ }
901
+ interface TxfInvalidEntry {
902
+ tokenId: string;
903
+ reason: string;
904
+ detectedAt: number;
905
+ }
906
+
907
+ /**
908
+ * wallet-api/types.ts — sphere-domain types for the wallet-api client (S1).
909
+ *
910
+ * Wire rule (ARCHITECTURE §11/§16): asset amounts are arbitrary-precision
911
+ * integers carried as decimal strings (`/^[0-9]+$/`) in every JSON body and
912
+ * response — they exceed 2^53, so they are NEVER a JS `number`. In these types
913
+ * they are `bigint`; the codec (./codec.ts) converts at the boundary. Cursors
914
+ * and seqs are `bigint` for the same reason.
915
+ */
916
+
917
+ /**
918
+ * The narrow slice of `StorageProvider` the client needs: the refresh token
919
+ * (credential hygiene — most-protected storage the platform offers, never a
920
+ * URL, never logs) and the normative LOCAL copy of open intents (E.3).
921
+ */
922
+ interface KeyValueStore {
923
+ get(key: string): Promise<string | null>;
924
+ set(key: string, value: string): Promise<void>;
925
+ remove(key: string): Promise<void>;
926
+ }
927
+ /** Minimal fetch signature (injectable; defaults to `globalThis.fetch`). */
928
+ type FetchLike = (url: string, init?: {
929
+ method?: string;
930
+ headers?: Record<string, string>;
931
+ body?: string | Uint8Array;
932
+ }) => Promise<FetchResponseLike>;
933
+ interface FetchResponseLike {
934
+ status: number;
935
+ ok: boolean;
936
+ text(): Promise<string>;
937
+ arrayBuffer(): Promise<ArrayBuffer>;
938
+ }
939
+ /** Minimal WebSocket surface (browser WebSocket and the `ws` package both satisfy it). */
940
+ interface WebSocketLike {
941
+ onopen: ((ev?: unknown) => void) | null;
942
+ onmessage: ((ev: {
943
+ data: unknown;
944
+ }) => void) | null;
945
+ onerror: ((ev?: unknown) => void) | null;
946
+ onclose: ((ev?: unknown) => void) | null;
947
+ close(): void;
948
+ }
949
+ type WebSocketFactoryLike = (url: string) => WebSocketLike;
950
+ interface WalletApiClientConfig {
951
+ /**
952
+ * Backend base URL. Non-loopback URLs MUST be `https:` (ARCHITECTURE §4
953
+ * transport rule, enforced client-side at construction) — bearer JWTs,
954
+ * refresh tokens and signed URLs never transit plaintext off-loopback.
955
+ */
956
+ baseUrl: string;
957
+ /** Network name, e.g. 'testnet2' — required end-to-end (ARCHITECTURE §14). */
958
+ network: string;
959
+ /** Client-chosen device label (never a key on its own — ARCHITECTURE §4). */
960
+ deviceId: string;
961
+ /** Refresh-token + local-intent persistence (see {@link KeyValueStore}). */
962
+ storage: KeyValueStore;
963
+ /** Injectable fetch (defaults to `globalThis.fetch`). */
964
+ fetchFn?: FetchLike;
965
+ /** Injectable WebSocket factory (defaults to `globalThis.WebSocket`). */
966
+ webSocketFactory?: WebSocketFactoryLike;
967
+ /** Injectable clock (ms since epoch) for challenge plausibility checks. */
968
+ now?: () => number;
969
+ }
970
+ /** The wallet identity the client authenticates as. */
971
+ interface WalletApiIdentity {
972
+ /** secp256k1 private key, hex — signs auth challenges; never leaves the client. */
973
+ privateKey: string;
974
+ /** 33-byte compressed secp256k1 public key, hex. */
975
+ chainPubkey: string;
976
+ }
977
+ /** `GET /v1/inventory` page (§16). */
978
+ interface InventoryPage {
979
+ cursor: bigint;
980
+ syncEpoch: bigint;
981
+ more: boolean;
982
+ items: InventoryItem[];
983
+ }
984
+ /** `GET /v1/balances` entry (§16) — active rows only. */
985
+ interface CoinBalance {
986
+ coinId: string;
987
+ total: bigint;
988
+ tokenCount: number;
989
+ }
990
+ /** `POST /v1/tokens/blob-urls` entry (§16): short-lived signed GET. */
991
+ interface BlobUrlEntry {
992
+ tokenId: string;
993
+ getUrl: string;
994
+ }
995
+ /** `POST /v1/tokens/upload-urls` request entry (§5.2): client-side sha256 + size. */
996
+ interface UploadUrlRequest {
997
+ /** Lowercase hex SHA-256 of the exact blob bytes. */
998
+ sha256: string;
999
+ size: number;
1000
+ }
1001
+ /** `POST /v1/tokens/upload-urls` response entry (§16). */
1002
+ interface UploadUrlEntry {
1003
+ sha256: string;
1004
+ /** The derived content-addressed key `<network>/t/<sha256>` (§5.2). */
1005
+ key: string;
1006
+ putUrl: string;
1007
+ }
1008
+ /** `POST /v1/inventory/apply` request (§5.3/§16). */
1009
+ interface ApplyDeltaRequest {
1010
+ transferId: string;
1011
+ spent: string[];
1012
+ added: {
1013
+ tokenId: string;
1014
+ key: string;
1015
+ }[];
1016
+ externalDelivery?: boolean;
1017
+ }
1018
+ /** Intent row (§16). `payload` is the S6 `enc1.` envelope string, verbatim. */
1019
+ interface IntentRecord {
1020
+ transferId: string;
1021
+ payload: string;
1022
+ status: 'open' | 'completed' | 'aborted';
1023
+ createdAt: number;
1024
+ }
1025
+ /** `POST /v1/mailbox` request (§16). `memo` is the S6 `enc1.` envelope, verbatim. */
1026
+ interface MailboxDepositRequest {
1027
+ /** Recipient's 33-byte compressed chain pubkey (hex) — §6 addressing. */
1028
+ recipientPubkey: string;
1029
+ /** Content-addressed blob-store key of the already-uploaded blob (§5.2). */
1030
+ key: string;
1031
+ /** The send's transferId — §5.3 evidence lookup uses the STORED value. */
1032
+ transferId: string;
1033
+ /** Claimed final state hash — `hex(SHA-256(inner token bytes))`. */
1034
+ stateHash: string;
1035
+ /** Claimed genesis-stable token id. */
1036
+ tokenId: string;
1037
+ /** Optional S6-encrypted memo envelope. */
1038
+ memo?: string;
1039
+ }
1040
+ type MailboxEntryStatus = 'unclaimed' | 'claimed' | 'rejected';
1041
+ /** One `GET /v1/mailbox` entry (§16). */
1042
+ interface MailboxEntry {
1043
+ /** Content-derived entry id — `hex(SHA-256(tokenId ‖ stateHash))` (§6). */
1044
+ entryId: string;
1045
+ /** Per-recipient gap-free, commit-ordered seq (§9). */
1046
+ seq: bigint;
1047
+ status: MailboxEntryStatus;
1048
+ transferId: string;
1049
+ tokenId: string;
1050
+ /** Decoded value of the deposited blob (per §8.2 step 6). */
1051
+ assets: {
1052
+ coinId: string;
1053
+ amount: bigint;
1054
+ }[];
1055
+ senderPubkey: string;
1056
+ /** S6 `enc1.` envelope, verbatim (the server never sees plaintext). */
1057
+ memo?: string;
1058
+ createdAt: number;
1059
+ /** Signed GET for the blob — present while the blob is retained (§6). */
1060
+ getUrl?: string;
1061
+ /** True once the blob was garbage-collected after resolution (§6). */
1062
+ blobCollected?: boolean;
1063
+ }
1064
+ /** `GET /v1/mailbox?since=` page (§16). */
1065
+ interface MailboxPage {
1066
+ /** Highest contiguous resolved seq — the discovery cursor (§6). */
1067
+ readPointer: bigint;
1068
+ syncEpoch: bigint;
1069
+ more: boolean;
1070
+ entries: MailboxEntry[];
1071
+ }
1072
+ /** `POST /v1/mailbox/claim` result (§16). */
1073
+ interface MailboxClaimResult {
1074
+ claimed: string[];
1075
+ /** Already-claimed entries with their STORED disposition (§6). */
1076
+ alreadyClaimed: {
1077
+ entryId: string;
1078
+ intoInventory: boolean;
1079
+ }[];
1080
+ /** Defensive bucket — an entry that would now violate lineage (§5.3). */
1081
+ failed: {
1082
+ entryId: string;
1083
+ code: string;
1084
+ }[];
1085
+ }
1086
+ /**
1087
+ * One client-asserted history record (§10 — the server never writes history).
1088
+ * `memo` and `counterpartyNametag` are S6 `enc1.` envelopes, verbatim (§8.3).
1089
+ */
1090
+ interface HistoryWireRecord {
1091
+ /** Client dedup key — POST is idempotent by it. */
1092
+ dedupKey: string;
1093
+ /** Client-generated record id (UUID — §16). */
1094
+ id: string;
1095
+ /** §16 enum: 'SENT' | 'RECEIVED' | 'MINT'. */
1096
+ type: string;
1097
+ /** ISO-8601 timestamp with offset (§16 `ts`). */
1098
+ ts: string;
1099
+ /** Decimal-string amounts (§11). */
1100
+ assets: {
1101
+ coinId: string;
1102
+ amount: string;
1103
+ }[];
1104
+ transferId?: string;
1105
+ /** Genesis-stable token id, lowercase hex (§16 — never a `v2_…` UI id). */
1106
+ tokenId?: string;
1107
+ /** 33-byte compressed secp256k1 pubkey, lowercase hex (§16). */
1108
+ counterpartyPubkey?: string;
1109
+ /** S6 envelope. */
1110
+ memo?: string;
1111
+ /** S6 envelope. */
1112
+ counterpartyNametag?: string;
1113
+ }
1114
+ /** `GET /v1/history` page (§16): newest-first, opaque keyset cursor. */
1115
+ interface HistoryPage {
1116
+ records: HistoryWireRecord[];
1117
+ more: boolean;
1118
+ /** Pass as `before` for the next (older) page; null on the last page (§16). */
1119
+ cursor: string | null;
1120
+ syncEpoch: bigint;
1121
+ }
1122
+ type PaymentRequestWireStatus = 'open' | 'paid' | 'declined' | 'expired';
1123
+ /**
1124
+ * One §16 payment request. `memo` is the S6 `enc1.` envelope, verbatim — it
1125
+ * decrypts only under the REQUESTER's wallet key (S6 keys are wallet-scoped),
1126
+ * so the payer surfaces it as absent, never as ciphertext.
1127
+ */
1128
+ interface PaymentRequestRecord {
1129
+ id: string;
1130
+ /** Per-payer gap-free, commit-ordered seq (§9/§10) — the incoming cursor unit. */
1131
+ seq: bigint;
1132
+ fromPubkey: string;
1133
+ toPubkey: string;
1134
+ assets: {
1135
+ coinId: string;
1136
+ amount: bigint;
1137
+ }[];
1138
+ /** S6 `enc1.` envelope, verbatim (the server never sees plaintext — §8.3). */
1139
+ memo?: string;
1140
+ status: PaymentRequestWireStatus;
1141
+ /** The fulfilling send's transferId once paid; null otherwise (§16). */
1142
+ transferId: string | null;
1143
+ createdAt: number;
1144
+ /** Server-owned expiry (§10) — the sweep flips overdue requests, never the client. */
1145
+ expiresAt?: number;
1146
+ }
1147
+ /** `POST /v1/payment-requests` request (§16). */
1148
+ interface CreatePaymentRequestInput {
1149
+ /** The payer's 33-byte compressed chain pubkey (lowercase hex) — §10 addressing. */
1150
+ toPubkey: string;
1151
+ assets: {
1152
+ coinId: string;
1153
+ amount: bigint;
1154
+ }[];
1155
+ /** S6 `enc1.` envelope — encrypt client-side BEFORE calling (§8.3). */
1156
+ memo?: string;
1157
+ /** ms since epoch — sent as ISO-8601 (§16). */
1158
+ expiresAt?: number;
1159
+ }
1160
+ /**
1161
+ * `GET /v1/payment-requests` query (§16). The two role views carry DIFFERENT
1162
+ * cursor families that never mix (the server 422s a mismatch): incoming is
1163
+ * the payer's gap-free `?since=<seq>` stream; outgoing is the requester's
1164
+ * newest-first `?before=<opaque keyset>` backfill.
1165
+ */
1166
+ type ListPaymentRequestsParams = {
1167
+ role: 'incoming';
1168
+ status?: PaymentRequestWireStatus;
1169
+ since?: bigint;
1170
+ } | {
1171
+ role: 'outgoing';
1172
+ status?: PaymentRequestWireStatus;
1173
+ before?: string;
1174
+ };
1175
+ /** `GET /v1/payment-requests` page (§16), discriminated by the requested role. */
1176
+ type PaymentRequestsPage = {
1177
+ role: 'incoming';
1178
+ requests: PaymentRequestRecord[];
1179
+ more: boolean;
1180
+ /** The §16 `?since=` seq cursor (gap-free — §9). */
1181
+ cursor: bigint;
1182
+ syncEpoch: bigint;
1183
+ } | {
1184
+ role: 'outgoing';
1185
+ requests: PaymentRequestRecord[];
1186
+ more: boolean;
1187
+ /** Opaque keyset for the next (older) page; null when drained (§16). */
1188
+ cursor: string | null;
1189
+ syncEpoch: bigint;
1190
+ };
1191
+ /**
1192
+ * `POST /v1/payment-requests/{id}/respond` body (§16): `paid` REQUIRES the
1193
+ * fulfilling send's transferId, `declined` forbids it — the pairing is in the
1194
+ * type, mirroring the server's validation.
1195
+ */
1196
+ type RespondPaymentRequestInput = {
1197
+ action: 'paid';
1198
+ transferId: string;
1199
+ } | {
1200
+ action: 'declined';
1201
+ };
1202
+ /** WS wake nudge (§9): pull that stream's cursor; never a correctness dependency. */
1203
+ interface WakeEvent {
1204
+ stream: 'inventory' | 'mailbox' | 'payment_requests';
1205
+ syncEpoch: bigint;
1206
+ }
1207
+ type WakeCallback = (wake: WakeEvent) => void;
1208
+ /** Handle returned by the wake-socket connect. */
1209
+ interface WakeSocketHandle {
1210
+ close(): void;
1211
+ }
1212
+
1213
+ /**
1214
+ * wallet-api/client.ts — `WalletApiClient` (sdk-changes S1).
1215
+ *
1216
+ * A small typed client for the wallet-api backend: challenge→sign→JWT with a
1217
+ * rotating refresh token, typed REST for the §16 endpoints, and the WS wake
1218
+ * channel (ticket flow — §9). Injected into providers (DI; no singletons).
1219
+ *
1220
+ * Cross-cutting rules (S1):
1221
+ * - **Challenge template verification** — the spend key never signs text that
1222
+ * fails `verifyChallengeTemplate` (prefix + own pubkey + plausible
1223
+ * timestamps). See ./challenge.ts.
1224
+ * - **Credential hygiene** — the refresh token lives only in the injected
1225
+ * {@link KeyValueStore} (never a URL, never logged). A rotation-reuse
1226
+ * revocation or refresh expiry falls back to a fresh challenge→sign cycle —
1227
+ * silent, since the wallet key is available at unlock. Non-loopback base
1228
+ * URLs MUST be `https:` (ARCHITECTURE §4 transport rule), enforced at
1229
+ * construction.
1230
+ * - **Amounts are decimal strings end-to-end**, parsed with `BigInt` (§11) —
1231
+ * see ./codec.ts.
1232
+ *
1233
+ * The client also keeps the NORMATIVE LOCAL COPY of open intents (E.3): the
1234
+ * server is the primary, but intents are the one server table not
1235
+ * re-derivable from blobs — after a server restore (`syncEpoch` change,
1236
+ * ARCHITECTURE §5.4) the client re-PUTs its locally-known open intents
1237
+ * (idempotent) before anything resumes. `putIntent` persists locally before
1238
+ * the server PUT; the local copy survives a failed PUT as the restore
1239
+ * backstop.
1240
+ */
1241
+
1242
+ declare class WalletApiClient {
1243
+ /** Network name (also the blob-key prefix `<network>/t/<sha256>` — §5.2). */
1244
+ readonly network: string;
1245
+ private readonly baseUrl;
1246
+ private readonly deviceId;
1247
+ private readonly storage;
1248
+ private readonly fetchFn;
1249
+ private readonly wsFactory;
1250
+ private readonly now;
1251
+ private identity;
1252
+ private jwt;
1253
+ /** Serializes concurrent re-auth attempts. */
1254
+ private authInFlight;
1255
+ constructor(config: WalletApiClientConfig);
1256
+ /** Bind the wallet identity this client authenticates as. Resets the session. */
1257
+ setIdentity(identity: WalletApiIdentity): void;
1258
+ private requireIdentity;
1259
+ private scopedKey;
1260
+ private refreshTokenKey;
1261
+ /**
1262
+ * Establish a session: try the stored refresh token first (rotating), fall
1263
+ * back to a fresh challenge→sign→verify cycle. Safe to call repeatedly.
1264
+ */
1265
+ signIn(): Promise<void>;
1266
+ private signInInner;
1267
+ /**
1268
+ * `POST /v1/auth/refresh` with the stored token; rotates on success. Any
1269
+ * 4xx (expired, revoked, rotation-reuse revocation) clears the stored token
1270
+ * and reports `false` — the caller falls back to a challenge cycle.
1271
+ */
1272
+ private tryRefresh;
1273
+ /** The challenge→verify cycle (§4 steps 1–3) with template verification (S1). */
1274
+ private challengeSignIn;
1275
+ /** Revoke the session server-side and drop all local credentials. */
1276
+ logout(): Promise<void>;
1277
+ private rawFetch;
1278
+ private readJson;
1279
+ private toError;
1280
+ /**
1281
+ * Authenticated JSON request. A 401 triggers one silent re-auth
1282
+ * (refresh → challenge fallback) and one retry.
1283
+ */
1284
+ private requestJson;
1285
+ /** `GET /v1/inventory?since=` — one page; the caller loops while `more`. */
1286
+ listInventory(since?: bigint): Promise<InventoryPage>;
1287
+ /** `GET /v1/balances` — active rows only. */
1288
+ getBalances(): Promise<CoinBalance[]>;
1289
+ /** `POST /v1/tokens/blob-urls` — owner's active *or tombstoned* rows (§5.3 recovery). */
1290
+ getBlobUrls(tokenIds: string[]): Promise<BlobUrlEntry[]>;
1291
+ /** `POST /v1/tokens/upload-urls` — checksum/length-bound presigned PUTs (§5.2). */
1292
+ getUploadUrls(blobs: UploadUrlRequest[]): Promise<UploadUrlEntry[]>;
1293
+ /**
1294
+ * `POST /v1/inventory/apply` (§5.3). Also marks the local intent copy
1295
+ * completed — the server completes the intent in the same transaction (§16).
1296
+ */
1297
+ applyInventoryDelta(req: ApplyDeltaRequest): Promise<bigint>;
1298
+ /** Download blob bytes from a signed GET URL (the URL itself is the credential). */
1299
+ fetchBlob(getUrl: string): Promise<Uint8Array>;
1300
+ /**
1301
+ * Upload blob bytes to a signed PUT URL. A `412 Precondition Failed` means
1302
+ * the identical blob already exists (content addressing, `If-None-Match: *`
1303
+ * — §5.2) and is treated as success.
1304
+ *
1305
+ * The §5.2 presign binds `x-amz-checksum-sha256` and `if-none-match` (plus
1306
+ * `content-length`) as SIGNED HEADERS — a real S3 endpoint rejects the
1307
+ * SigV4 signature unless the uploader sends them verbatim. (Caught by the
1308
+ * phase-2 harness on first contact with real MinIO — the in-process fake
1309
+ * had only validated the body, not the signed headers.)
1310
+ */
1311
+ uploadBlob(putUrl: string, bytes: Uint8Array): Promise<void>;
1312
+ /**
1313
+ * `POST /v1/mailbox` — deposit an already-uploaded finished blob to the
1314
+ * recipient's mailbox. Idempotent by content-derived `entry_id` (§6): an
1315
+ * existing entry in any status returns `200` with its id, provided the
1316
+ * request's recipient and key match the stored entry (a mismatch is `409`).
1317
+ */
1318
+ depositMailbox(req: MailboxDepositRequest): Promise<string>;
1319
+ /**
1320
+ * `GET /v1/mailbox?since=<seq>` — entries of EVERY status are listable for
1321
+ * any client-chosen `since`; a claimed entry carries a working `getUrl`
1322
+ * while its blob is within retention, `blobCollected: true` afterwards (§6).
1323
+ */
1324
+ listMailbox(since?: bigint): Promise<MailboxPage>;
1325
+ /**
1326
+ * `POST /v1/mailbox/claim` — addressee-only, idempotent ownership handoff
1327
+ * (§6). `intoInventory:false` is the delivery-only claim: the entry resolves
1328
+ * and the pointer advances with ZERO inventory writes.
1329
+ */
1330
+ claimMailbox(entryIds: string[], intoInventory: boolean): Promise<MailboxClaimResult>;
1331
+ /**
1332
+ * `POST /v1/mailbox/reject` — addressee-only; terminal for DISCOVERY only:
1333
+ * the entry counts toward read-pointer contiguity but remains claimable and
1334
+ * its blob is retained (§6 — reject is never a destruction path).
1335
+ */
1336
+ rejectMailbox(entryIds: string[]): Promise<string[]>;
1337
+ /**
1338
+ * `POST /v1/history` — client-asserted records, deduped by `dedupKey` (the
1339
+ * server never writes history rows — §10). `memo`/`counterpartyNametag`
1340
+ * MUST already be S6 envelopes.
1341
+ */
1342
+ postHistoryRecords(records: HistoryWireRecord[]): Promise<void>;
1343
+ /** `GET /v1/history?before=&limit=` — newest-first keyset pages (§10). */
1344
+ listHistory(options?: {
1345
+ before?: string;
1346
+ limit?: number;
1347
+ }): Promise<HistoryPage>;
1348
+ /**
1349
+ * `POST /v1/payment-requests` — create a request addressed to a payer (the
1350
+ * payer's account is auto-provisioned even if they never authenticated —
1351
+ * §4/§9; the per-payer open cap → 429, §5.5). `memo` MUST already be an S6
1352
+ * `enc1.` envelope (§8.3) — only the requester's wallet key decrypts it.
1353
+ */
1354
+ createPaymentRequest(input: CreatePaymentRequestInput): Promise<PaymentRequestRecord>;
1355
+ /**
1356
+ * `GET /v1/payment-requests?role=…` (§16): `'incoming'` is the payer's
1357
+ * gap-free `?since=<seq>` stream (cursor = bigint, the standard §16 since
1358
+ * contract); `'outgoing'` is the requester's newest-first
1359
+ * `?before=<opaque keyset>` backfill (cursor = string | null). The two
1360
+ * cursor families never mix — the server 422s a mismatched parameter, and
1361
+ * the parameter types make it unrepresentable here.
1362
+ */
1363
+ listPaymentRequests<R extends 'incoming' | 'outgoing'>(params: Extract<ListPaymentRequestsParams, {
1364
+ role: R;
1365
+ }>): Promise<Extract<PaymentRequestsPage, {
1366
+ role: R;
1367
+ }>>;
1368
+ /**
1369
+ * `POST /v1/payment-requests/{id}/respond` — addressee-only (§10, non-
1370
+ * addressee → 403); only an `open` request may be responded to (else 409).
1371
+ * `paid` REQUIRES and links the fulfilling send's transferId, `declined`
1372
+ * carries none — the pairing is enforced by {@link RespondPaymentRequestInput}.
1373
+ */
1374
+ respondPaymentRequest(id: string, response: RespondPaymentRequestInput): Promise<PaymentRequestRecord>;
1375
+ private intentsKey;
1376
+ private readLocalIntents;
1377
+ private writeLocalIntents;
1378
+ private setLocalIntentStatus;
1379
+ /**
1380
+ * Persist a (client-encrypted — S6) intent payload: LOCAL copy first, then
1381
+ * `PUT /v1/intents/{transferId}` and await the server ack (E.3 — the engine
1382
+ * MUST NOT be called before this resolves). A failed server PUT throws, but
1383
+ * the local copy stays as the restore backstop and is re-PUT by
1384
+ * {@link resyncOpenIntents}.
1385
+ */
1386
+ putIntent(transferId: string, payloadEnvelope: string): Promise<void>;
1387
+ /** `GET /v1/intents?status=` — server-side intent list. */
1388
+ listIntents(status: 'open' | 'aborted'): Promise<IntentRecord[]>;
1389
+ /** `POST /v1/intents/{id}/abort` — soft, recoverable (§16). */
1390
+ abortIntent(transferId: string): Promise<void>;
1391
+ /** `POST /v1/intents/{id}/complete` — the uniform client-side close (E.3). */
1392
+ completeIntent(transferId: string): Promise<void>;
1393
+ /** The locally-known open intents (the E.3 restore backstop). */
1394
+ listLocalOpenIntents(): Promise<IntentRecord[]>;
1395
+ /**
1396
+ * Re-PUT every locally-known open intent (idempotent — the server PUT is
1397
+ * write-once while open/completed). Called after a `syncEpoch` change
1398
+ * (server restore — §5.4): intents are the one server table not
1399
+ * re-derivable from blobs.
1400
+ */
1401
+ resyncOpenIntents(): Promise<void>;
1402
+ private syncEpochKey;
1403
+ /**
1404
+ * Track the server `syncEpoch` carried by cursor-bearing responses and
1405
+ * wakes. On a change (server restore), re-PUT locally-known open intents
1406
+ * before anything resumes (E.3). Storage-provider cursor invalidation is
1407
+ * the provider's own duty (S2) — it observes the same epoch values.
1408
+ */
1409
+ private noteSyncEpoch;
1410
+ /**
1411
+ * Open the wake channel: `POST /v1/ws-ticket` (JWT-authed, single-use,
1412
+ * short TTL) then `GET /v1/ws?ticket=…` — the JWT never appears in a URL.
1413
+ * Wakes are nudges only; correctness comes from the `?since=` cursors.
1414
+ */
1415
+ connectWakeSocket(onWake: WakeCallback): Promise<WakeSocketHandle>;
1416
+ }
1417
+
1418
+ /**
1419
+ * WalletApiTokenStorageProvider — the lazy, thin-wallet storage provider over
1420
+ * the wallet-api backend (sdk-changes S2; ARCHITECTURE §5/§16).
1421
+ *
1422
+ * Platform-neutral (browser + Node) over the injected {@link WalletApiClient}.
1423
+ * The wallet renders balances from the server's value-indexed inventory view;
1424
+ * blobs are fetched on demand only to spend (signed GET URLs). Key behaviors:
1425
+ *
1426
+ * - **Tombstone-aware delta sync** (§5.1): `?since=` deltas include
1427
+ * `status:'removed'` rows — the only way a stale device learns about
1428
+ * spends/handoffs; the provider applies them (drops the entries from its
1429
+ * active view) and loops while `more`.
1430
+ * - **Paginated full pull** is finished with an immediate
1431
+ * `?since=<page-1 cursor>` closing delta, whose tombstones repair any flips
1432
+ * that happened between pages (§5.1).
1433
+ * - **`syncEpoch` change** (server restore — §5.4): discard all persisted
1434
+ * cursors, full pull, then re-PUT locally-known open intents (E.3, via the
1435
+ * client) before anything resumes.
1436
+ * - **Write-behind with empty-import protection** (§5.1 client guards): a
1437
+ * removal is pushed only after a successful inventory load and only for a
1438
+ * confirmed on-chain spend (a `_tombstones` entry) — a fresh device or a
1439
+ * failed load can never appear to "empty" the wallet, and a merely-absent
1440
+ * token is never removed.
1441
+ * - **`recoverRemoved()`** (§5.3 recovery): tombstones the client cannot match
1442
+ * to a known spend are re-fetched via blob-urls (which work for own
1443
+ * tombstoned rows), re-verified locally, and re-added (reactivation). A
1444
+ * server `409` is an evidenced tombstone — "actually spent" — and is kept.
1445
+ * - **Blob upload** via upload-urls with client-side sha256; a `412` from the
1446
+ * content-addressed store means the blob already exists = success (§5.2).
1447
+ */
1448
+
1449
+ interface WalletApiTokenStorageConfig {
1450
+ /** The authenticated wallet-api client (S1). DI — never a singleton. */
1451
+ client: WalletApiClient;
1452
+ /** Persists the inventory cursor, syncEpoch and own-spend set per identity. */
1453
+ stateStore: KeyValueStore;
1454
+ /**
1455
+ * Optional local token verification used by `recoverRemoved()` before
1456
+ * re-adding a tombstoned blob (S2: "re-verifies locally"). Wire the engine's
1457
+ * `verify` here at composition; the default accepts any blob that decodes
1458
+ * and matches its tokenId.
1459
+ */
1460
+ verifyToken?: (blob: TokenBlob) => Promise<boolean>;
1461
+ }
1462
+ declare class WalletApiTokenStorageProvider implements TokenStorageProvider<TxfStorageDataBase> {
1463
+ readonly id = "wallet-api-token-storage";
1464
+ readonly name = "Wallet API Token Storage";
1465
+ readonly type: "cloud";
1466
+ private readonly client;
1467
+ private readonly stateStore;
1468
+ private readonly verifyToken?;
1469
+ private status;
1470
+ private identity;
1471
+ /** Local mirror of the inventory view — active rows and tombstones. */
1472
+ private readonly view;
1473
+ /**
1474
+ * Empty-import protection (§5.1): no removal is ever pushed before this
1475
+ * flips on the first successful inventory load.
1476
+ */
1477
+ private hadSuccessfulLoad;
1478
+ constructor(config: WalletApiTokenStorageConfig);
1479
+ setIdentity(identity: FullIdentity): void;
1480
+ initialize(): Promise<boolean>;
1481
+ shutdown(): Promise<void>;
1482
+ connect(): Promise<void>;
1483
+ disconnect(): Promise<void>;
1484
+ isConnected(): boolean;
1485
+ getStatus(): ProviderStatus;
1486
+ private stateKey;
1487
+ private readCursor;
1488
+ private readSyncEpoch;
1489
+ private persistSyncState;
1490
+ /** TokenIds this provider itself spent — `recoverRemoved()` skips these. */
1491
+ private readKnownSpends;
1492
+ private addKnownSpends;
1493
+ private applyItems;
1494
+ /** Full pull + the §5.1 closing delta; replaces the whole local view. */
1495
+ private fullPull;
1496
+ /**
1497
+ * Delta loop from the persisted cursor. Returns `true` when the server's
1498
+ * `syncEpoch` no longer matches the persisted one — the cursors are invalid
1499
+ * (server restore, §5.4) and the caller must resync from scratch.
1500
+ */
1501
+ private deltaLoop;
1502
+ /** §5.4: discard cursors → full pull → re-PUT locally-known open intents. */
1503
+ private handleSyncEpochChange;
1504
+ /** Converge the local view with the server (delta when possible). */
1505
+ private syncInventory;
1506
+ /**
1507
+ * Without `since`: converge with the server, then return the full active
1508
+ * view (`more:false`) — a fresh device renders balances with zero blob
1509
+ * downloads. With `since`: a true server delta page (tombstones included),
1510
+ * also applied to the local view.
1511
+ */
1512
+ listInventory(since?: bigint): Promise<InventoryView>;
1513
+ private snapshotView;
1514
+ /** Fetch + decode one blob on demand via a short-lived signed GET (§5.1). */
1515
+ getToken(tokenId: string): Promise<TokenBlob>;
1516
+ /**
1517
+ * The §16 wire serves RAW token bytes (§5.2/§8.2 — the sphere 39051
1518
+ * envelope never crosses the API): re-wrap them for the engine-facing
1519
+ * {@link TokenBlob} surface. Envelope bytes (older rows, fake-world seeds)
1520
+ * still decode — their embedded tokenId is then checked by the caller. The
1521
+ * `network` of a raw wrap is not recoverable without the engine; consumers
1522
+ * derive it from the decoded token (`engine.decodeToken` re-wraps), so it
1523
+ * is never read from this value.
1524
+ */
1525
+ private wrapWireBlob;
1526
+ /**
1527
+ * Record a spend (§5.3) — idempotent by `transferId` server-side. MUST be
1528
+ * called after the mailbox deposit of the same `transferId` (the backend
1529
+ * evidence-checks removals against it). Refreshes the local view from the
1530
+ * resulting delta.
1531
+ */
1532
+ applyDelta(transferId: string, spent: string[], added: ApplyDeltaAdded[], opts?: ApplyDeltaOptions): Promise<void>;
1533
+ /**
1534
+ * Re-add tombstoned tokens this client cannot match to a known spend (a
1535
+ * wiped view — stolen JWT or a buggy client). Blobs are retained server-side
1536
+ * and blob-urls works for own tombstoned rows; each candidate is re-fetched,
1537
+ * locally verified, and re-added (reactivation). A `409` is the server's
1538
+ * verdict that the tombstone is an evidenced spend — kept as spent.
1539
+ */
1540
+ recoverRemoved(): Promise<RecoverRemovedResult>;
1541
+ private fetchRemovedBlob;
1542
+ private locallyValid;
1543
+ /**
1544
+ * Thin view: converges with the server and reports tombstones, but carries
1545
+ * NO token entries — blobs are fetched on demand via `getToken()`
1546
+ * (sdk-changes S2: `load()` no longer eagerly pulls every blob).
1547
+ */
1548
+ load(): Promise<LoadResult<TxfStorageDataBase>>;
1549
+ /**
1550
+ * Write-behind push of a whole-blob snapshot:
1551
+ * - unknown tokens are uploaded (content-addressed, 412 = already present)
1552
+ * and added via one idempotent apply;
1553
+ * - removals are pushed ONLY for confirmed spends (`_tombstones` entries)
1554
+ * and ONLY after a successful inventory load (empty-import protection,
1555
+ * §5.1) — a merely-absent token is never removed.
1556
+ * A failed sync fails the save: pushing against an unknown server view
1557
+ * could only do harm.
1558
+ */
1559
+ save(data: TxfStorageDataBase): Promise<SaveResult>;
1560
+ private pushAdditions;
1561
+ private pushRemovals;
1562
+ sync(localData: TxfStorageDataBase): Promise<SyncResult<TxfStorageDataBase>>;
1563
+ }
1564
+ declare function createWalletApiTokenStorageProvider(config: WalletApiTokenStorageConfig): WalletApiTokenStorageProvider;
1565
+
1566
+ /**
1567
+ * WalletApiMailboxProvider — the reference `DeliveryProvider` implementation
1568
+ * over the wallet-api mailbox (sdk-changes S3/S7; ARCHITECTURE §6/§16).
1569
+ *
1570
+ * Platform-neutral over the injected {@link WalletApiClient}. Key behaviors:
1571
+ *
1572
+ * - **deliver** = sha256 + upload via upload-urls (a `412` from the
1573
+ * content-addressed store means the blob is already present = success,
1574
+ * §5.2) → `POST /v1/mailbox` (idempotent by content-derived `entry_id`,
1575
+ * §6). The memo is encrypted client-side with the S6 field key before it
1576
+ * leaves the device; the deposit's `entryId` is verified to equal the
1577
+ * locally computed content-derived id — a backend substituting row ids is a
1578
+ * protocol violation (covenant §3.1-4).
1579
+ * - **incoming** = `GET /v1/mailbox?since=` paging (`more` loops), yielding
1580
+ * claimable entries (pending; claimed/rejected entries are recorded as seen
1581
+ * and skipped — they were resolved here or on another of the owner's
1582
+ * devices). `fetchBlob` uses the entry's `getUrl` and re-derives
1583
+ * (tokenId, stateHash, deliveryId) from the actual bytes — the recipient
1584
+ * never trusts the backend (§8.2). `blobCollected` entries throw a typed
1585
+ * error.
1586
+ * - **ack** → claim with the provider's **composition-time custody**
1587
+ * (`'inventory'` → `intoInventory: true` handoff; `'external'` →
1588
+ * `intoInventory: false`, ZERO inventory writes — §6 delivery-only claim),
1589
+ * or reject (terminal for discovery only — §6).
1590
+ * - **Persistent seen-set**: every acked delivery's content-derived id — the
1591
+ * canonical hash of its `(tokenId, stateHash)` pair — is persisted via the
1592
+ * injected {@link KeyValueStore}; a replayed delivery (server replay, cursor
1593
+ * reset, restore) is never yielded again (S7 port contract).
1594
+ */
1595
+
1596
+ interface WalletApiMailboxProviderConfig {
1597
+ /** The authenticated wallet-api client (S1). DI — never a singleton. */
1598
+ client: WalletApiClient;
1599
+ /**
1600
+ * Composition-time custody (S7 — NEVER a per-call flag): `'inventory'` for
1601
+ * the full wallet-api preset (acks hand tokens into the server inventory),
1602
+ * `'external'` for the own-storage preset (acks perform zero inventory
1603
+ * writes; the app's storage keeps custody).
1604
+ */
1605
+ custody: DeliveryCustody;
1606
+ /** Persists the per-identity seen-set + mailbox cursor. */
1607
+ stateStore: KeyValueStore;
1608
+ /**
1609
+ * The backend-true (tokenId, stateHash) derivation (`ITokenEngine.deliveryKeys`).
1610
+ * Optional at construction — compositions are engine-less; `PaymentsModule`
1611
+ * late-binds it via `bindDeliveryKeys` at init. The provider never derives
1612
+ * these locally (§8.2 / sdk-changes S7) and fails loudly if unbound.
1613
+ */
1614
+ deliveryKeys?: (blobBytes: Uint8Array) => Promise<{
1615
+ tokenId: string;
1616
+ stateHash: string;
1617
+ }>;
1618
+ }
1619
+ declare class WalletApiMailboxProvider implements DeliveryProvider {
1620
+ readonly custody: DeliveryCustody;
1621
+ private readonly client;
1622
+ private readonly stateStore;
1623
+ private deriveKeysFn;
1624
+ private identity;
1625
+ private fieldKey;
1626
+ constructor(config: WalletApiMailboxProviderConfig);
1627
+ /** @inheritDoc — wired by PaymentsModule at init (engine-owning seam). */
1628
+ bindDeliveryKeys(derive: (blobBytes: Uint8Array) => Promise<{
1629
+ tokenId: string;
1630
+ stateHash: string;
1631
+ }>): void;
1632
+ private deriveKeys;
1633
+ /** Bind the wallet identity (derives the S6 field key; binds the client). */
1634
+ setIdentity(identity: {
1635
+ privateKey: string;
1636
+ chainPubkey: string;
1637
+ }): void;
1638
+ private stateKey;
1639
+ /**
1640
+ * The persistent seen-set (S7): content-derived delivery ids — i.e. the
1641
+ * canonical `SHA-256(tokenId ‖ stateHash)` encoding of the (tokenId,
1642
+ * stateHash) pairs this wallet has already resolved.
1643
+ */
1644
+ private readSeen;
1645
+ private addSeen;
1646
+ private readCursorState;
1647
+ private persistCursorState;
1648
+ /**
1649
+ * Idempotent per (token, state): the upload is content-addressed (412 =
1650
+ * already present = success — §5.2) and the deposit is idempotent by the
1651
+ * content-derived entry_id — it succeeds even after the recipient claimed
1652
+ * (§6), which is what makes the sender's journal replay safe.
1653
+ */
1654
+ deliver(recipientPubkey: string, blob: Uint8Array, options: DeliverOptions): Promise<DeliveryReceipt>;
1655
+ /**
1656
+ * Pull entries since the given cursor (or the persisted one), loop while
1657
+ * `more`, and yield claimable deliveries not yet in the seen-set. The
1658
+ * persisted cursor advances to the server's read pointer (§6) — entries at
1659
+ * or below it are all resolved; the seen-set (not the cursor) is the replay
1660
+ * guard, so a cursor reset is safe.
1661
+ */
1662
+ incoming(sinceCursor?: string): AsyncIterable<IncomingDelivery>;
1663
+ private toIncomingDelivery;
1664
+ ack(deliveryId: string, disposition: DeliveryDisposition): Promise<void>;
1665
+ onWake(callback: () => void): () => void;
1666
+ }
1667
+ declare function createWalletApiMailboxProvider(config: WalletApiMailboxProviderConfig): WalletApiMailboxProvider;
1668
+
1669
+ /**
1670
+ * impl/shared/wallet-api/composition.ts — port composition (sdk-changes S4/S7,
1671
+ * covenant §3.1-6).
1672
+ *
1673
+ * The Sphere frontend is a VIEW: all I/O sits behind three independently
1674
+ * swappable ports — storage (`TokenStorageProvider`), delivery
1675
+ * (`DeliveryProvider`) and the engine/aggregator config (`OracleProvider`) —
1676
+ * selected at composition time. wallet-api ships as ONE implementation of each
1677
+ * port, not as the port:
1678
+ *
1679
+ * - {@link createSphereProviders} — the composition root: take any platform
1680
+ * base bundle (`createBrowserProviders` / `createNodeProviders`) and select
1681
+ * each port independently.
1682
+ * - {@link createWalletApiProviders} — the FULL wallet-api preset: thin
1683
+ * storage (server inventory custody) + mailbox delivery with custody
1684
+ * `'inventory'`.
1685
+ * - {@link createOwnStorageWalletApiProviders} — the delivery-only preset:
1686
+ * the app's own (local) storage keeps custody; the mailbox provider is
1687
+ * constructed with custody `'external'`, so every ack sends
1688
+ * `intoInventory: false` — zero server inventory writes by construction
1689
+ * (ARCHITECTURE §6 delivery-only claim; never a per-call flag).
1690
+ *
1691
+ * Asset-transport routing (S4): composing a delivery port moves ASSETS to it;
1692
+ * messaging, group chat and nametag bindings stay on the Nostr transport in
1693
+ * the base bundle. Payment requests ride wallet-api whenever the `walletApi`
1694
+ * client these presets return is passed to `Sphere.init` — `PaymentsModule`
1695
+ * detects the §16 payment-request capability on it and does not install the
1696
+ * Nostr payment-request channel (sdk-changes S4).
1697
+ */
1698
+
1699
+ /** The minimum any platform base bundle provides (browser/node factories do). */
1700
+ interface SphereBaseProviders {
1701
+ storage: StorageProvider;
1702
+ transport: TransportProvider;
1703
+ oracle: OracleProvider;
1704
+ tokenStorage: TokenStorageProvider<TxfStorageDataBase>;
1705
+ }
1706
+ /** Independent port selection (sdk-changes S7): any combination is legal. */
1707
+ interface SphereProviderPorts {
1708
+ /** Storage port override (token inventory + blob custody). */
1709
+ storage?: TokenStorageProvider<TxfStorageDataBase>;
1710
+ /** Delivery port (assets move to it; messaging stays on the base transport). */
1711
+ delivery?: DeliveryProvider;
1712
+ /** Engine/aggregator config port override. */
1713
+ engine?: OracleProvider;
1714
+ }
1715
+ /**
1716
+ * Compose a Sphere provider bundle with each port selected independently
1717
+ * (covenant §3.1-6). Unselected ports keep the base bundle's implementation.
1718
+ */
1719
+ declare function createSphereProviders<B extends SphereBaseProviders>(base: B, ports?: SphereProviderPorts): B & {
1720
+ delivery?: DeliveryProvider;
1721
+ };
1722
+ interface WalletApiCompositionConfig {
1723
+ /** Backend base URL — https off-loopback (ARCHITECTURE §4, client-enforced). */
1724
+ baseUrl: string;
1725
+ /** Network name (e.g. 'testnet2') — required end-to-end (ARCHITECTURE §14). */
1726
+ network: string;
1727
+ /**
1728
+ * Stable per-device label (ARCHITECTURE §4 — one session row per (owner,
1729
+ * device); the refresh token is stored under it). Pass a persisted value;
1730
+ * when omitted a fresh random label is generated per construction, which
1731
+ * still works but starts every run with a challenge sign-in.
1732
+ */
1733
+ deviceId?: string;
1734
+ /** Reuse an existing client (tests / advanced wiring). */
1735
+ client?: WalletApiClient;
1736
+ /** Refresh-token / cursor / seen-set persistence; defaults to `base.storage`. */
1737
+ stateStore?: KeyValueStore;
1738
+ /** Injectable fetch (defaults to `globalThis.fetch`). */
1739
+ fetchFn?: FetchLike;
1740
+ /** Injectable WebSocket factory (defaults to `globalThis.WebSocket`). */
1741
+ webSocketFactory?: WebSocketFactoryLike;
1742
+ /** Local token verification for `recoverRemoved()` (wire the engine here). */
1743
+ verifyToken?: (blob: TokenBlob) => Promise<boolean>;
1744
+ }
1745
+ /** What the wallet-api presets add to the base bundle. */
1746
+ interface WalletApiProviderExtras {
1747
+ delivery: DeliveryProvider;
1748
+ /** The S1 client — pass to `Sphere.init({ walletApi })` for the S4 auth lifecycle. */
1749
+ walletApi: WalletApiClient;
1750
+ }
1751
+ /**
1752
+ * The FULL wallet-api preset (S4): wallet-api keeps inventory custody —
1753
+ * thin/lazy storage over the server's value index + mailbox delivery with
1754
+ * custody `'inventory'` (claims perform the §6 ownership handoff).
1755
+ */
1756
+ declare function createWalletApiProviders<B extends SphereBaseProviders>(base: B, config: WalletApiCompositionConfig): B & WalletApiProviderExtras;
1757
+ /**
1758
+ * The OWN-STORAGE preset (S7: own storage + wallet-api delivery — a
1759
+ * supported, tested composition): the base bundle's local storage keeps
1760
+ * custody; wallet-api is purely the delivery rail. Custody `'external'` is
1761
+ * baked in at construction — every ack sends `intoInventory: false`, so a
1762
+ * recipient's claim performs ZERO inventory writes even when `ack` is called
1763
+ * with no thought given to options (sdk-changes S7). Senders never call
1764
+ * apply; their sends close via `intents/{id}/complete` alone (ARCHITECTURE
1765
+ * §6 storage-opt-out senders), which the wallet-api client (passed as
1766
+ * `walletApi`) provides.
1767
+ */
1768
+ declare function createOwnStorageWalletApiProviders<B extends SphereBaseProviders>(base: B, config: WalletApiCompositionConfig): B & WalletApiProviderExtras;
1769
+
1770
+ export { type SphereBaseProviders, type SphereProviderPorts, type WalletApiCompositionConfig, WalletApiMailboxProvider, type WalletApiMailboxProviderConfig, type WalletApiProviderExtras, type WalletApiTokenStorageConfig, WalletApiTokenStorageProvider, createOwnStorageWalletApiProviders, createSphereProviders, createWalletApiMailboxProvider, createWalletApiProviders, createWalletApiTokenStorageProvider };