@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.
- package/dist/impl/shared/wallet-api/index.cjs +3224 -0
- package/dist/impl/shared/wallet-api/index.cjs.map +1 -0
- package/dist/impl/shared/wallet-api/index.d.cts +1770 -0
- package/dist/impl/shared/wallet-api/index.d.ts +1770 -0
- package/dist/impl/shared/wallet-api/index.js +3191 -0
- package/dist/impl/shared/wallet-api/index.js.map +1 -0
- package/package.json +11 -1
|
@@ -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 };
|