@unicitylabs/sphere-sdk 0.14.0-dev.1 → 0.14.0-dev.2

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,1069 @@
1
+ import { Token as Token$1 } from '@unicitylabs/state-transition-sdk/lib/transaction/Token.js';
2
+
3
+ /**
4
+ * token-engine/types.ts — the FROZEN, sphere-domain contract surface.
5
+ *
6
+ * Design rule (anti-corruption): the public ITokenEngine port speaks ONLY
7
+ * sphere-domain types — `Uint8Array` pubkeys, `string` coin ids, `bigint`
8
+ * amounts, plain enums. The v2 state-transition SDK has exactly ONE foothold
9
+ * here: `SphereToken.sdkToken`, an OPAQUE handle. Callers must treat it as
10
+ * opaque (store it, hand it back to the engine) and never call methods on it —
11
+ * they cannot, since the ESLint boundary forbids them importing the SDK.
12
+ *
13
+ * Both migration tracks freeze against this file:
14
+ * Track A implements it (token-engine internals).
15
+ * Track B codes callers against it (using FakeTokenEngine until A lands).
16
+ */
17
+
18
+ /** The wallet identity at the engine boundary. The private key never appears in a DTO. */
19
+ interface EngineIdentity {
20
+ /** 33-byte compressed secp256k1 public key (stable across the migration — Path A). */
21
+ readonly chainPubkey: Uint8Array;
22
+ }
23
+ /**
24
+ * Coin identifier. Canonical form is the lowercase hex of the v2 AssetId;
25
+ * human symbols (e.g. "UCT") are resolved to hex via the registry before use.
26
+ */
27
+ type CoinId = string;
28
+ /** One fungible position inside a token. */
29
+ interface SphereAsset {
30
+ readonly coinId: CoinId;
31
+ readonly amount: bigint;
32
+ }
33
+ /** The decoded, app-defined value carried by a token (v2 Token itself is value-less). */
34
+ interface SphereValue {
35
+ readonly assets: readonly SphereAsset[];
36
+ }
37
+ /**
38
+ * Storage-and-display token. Format version + network let storage migrate
39
+ * independently of the SDK's own CBOR. The decoded value is re-derivable from
40
+ * `token`, so it is NOT stored — only cached at runtime on SphereToken.value.
41
+ */
42
+ interface TokenBlob {
43
+ /** Blob format version (sphere storage migrations; independent of SDK CBOR). */
44
+ readonly v: number;
45
+ /** NetworkId.id the token belongs to (mainnet=1 / testnet=2 / local=3). */
46
+ readonly network: number;
47
+ /**
48
+ * Genesis-stable token id — 64-char lowercase hex of the v2 `TokenId.bytes`
49
+ * (same across every state of the token). Stored on the blob so dedup / listing
50
+ * / tombstone keys need no engine call. `createTokenStateKey = ${tokenId}_${hash}`.
51
+ */
52
+ readonly tokenId: string;
53
+ /** CBOR bytes of the v2 Token (`Token.toCBOR()`). */
54
+ readonly token: Uint8Array;
55
+ }
56
+ /**
57
+ * A wallet token. `sdkToken` is the OPAQUE engine handle (see file header) —
58
+ * present for the engine to operate on, never to be touched by callers.
59
+ */
60
+ interface SphereToken {
61
+ /** Opaque v2 SDK handle. Do not call methods on this outside token-engine/. */
62
+ readonly sdkToken: Token$1;
63
+ /** Serializable form for storage/transport. */
64
+ readonly blob: TokenBlob;
65
+ /** Decoded value (cached); null when the token carries no sphere payment data. */
66
+ readonly value: SphereValue | null;
67
+ }
68
+ interface MintParams {
69
+ /** Recipient's 33-byte compressed chain pubkey; engine derives the predicate. */
70
+ readonly recipientPubkey: Uint8Array;
71
+ /** Value to embed in the mint; null mints a value-less token. */
72
+ readonly value?: SphereValue | null;
73
+ }
74
+ /**
75
+ * Mint a NON-value (data) token: arbitrary opaque `data` (e.g. serialized invoice
76
+ * terms), a custom `tokenType`, and a deterministic `salt` → a stable,
77
+ * terms-derived `tokenId`. The minted token has `value === null` (it carries data,
78
+ * not coins); read the bytes back with `readTokenData`.
79
+ */
80
+ interface MintDataTokenParams {
81
+ readonly recipientPubkey: Uint8Array;
82
+ /** Opaque token payload (the engine does not interpret it). */
83
+ readonly data: Uint8Array;
84
+ /** Token type bytes; defaults to a random type when omitted. */
85
+ readonly tokenType?: Uint8Array;
86
+ /** Salt bytes; deterministic salt → deterministic (terms-derived) tokenId. */
87
+ readonly salt?: Uint8Array;
88
+ }
89
+ interface TransferParams {
90
+ /** The token to spend (must be owned by this engine's identity). */
91
+ readonly token: SphereToken;
92
+ /** Recipient's 33-byte compressed chain pubkey. */
93
+ readonly recipientPubkey: Uint8Array;
94
+ /** Optional opaque on-chain memo carried on the transfer (read back via `readMemo`). */
95
+ readonly data?: Uint8Array;
96
+ }
97
+ /**
98
+ * One split output = one single-coin token. To split a multi-coin token, emit
99
+ * one output per coin (the recipient receives the value as several tokens; the
100
+ * SDK enforces per-coin conservation). If a single multi-coin output token is
101
+ * ever needed, generalize this to `assets: readonly SphereAsset[]` (additive).
102
+ */
103
+ interface SplitOutput {
104
+ readonly recipientPubkey: Uint8Array;
105
+ readonly coinId: CoinId;
106
+ readonly amount: bigint;
107
+ /** Optional opaque memo carried in this output's value envelope (read back via `readMemo`). */
108
+ readonly data?: Uint8Array;
109
+ }
110
+ interface SplitParams {
111
+ /** The token to split (its total per coin must equal the sum of outputs). */
112
+ readonly token: SphereToken;
113
+ /** Desired outputs; value conservation is enforced by the SDK split. */
114
+ readonly outputs: readonly SplitOutput[];
115
+ }
116
+ interface SplitResult {
117
+ /**
118
+ * One minted token per requested output, **index-aligned with
119
+ * `SplitParams.outputs`** — `outputs[i]` is the token for `params.outputs[i]`
120
+ * (so a payee/change split can rely on positional order). Guaranteed by both
121
+ * the real engine and FakeTokenEngine.
122
+ */
123
+ readonly outputs: readonly SphereToken[];
124
+ }
125
+ /** Verification outcome, flattened to sphere-domain (no SDK status enum leaks). */
126
+ interface EngineVerifyResult {
127
+ readonly ok: boolean;
128
+ /** Human-readable reason when `ok` is false (mapped from the SDK verification status). */
129
+ readonly reason?: string;
130
+ }
131
+
132
+ /**
133
+ * Storage Provider Interface
134
+ * Platform-independent storage abstraction
135
+ */
136
+
137
+ /**
138
+ * Basic key-value storage provider
139
+ * All operations are async for platform flexibility
140
+ */
141
+ interface StorageProvider extends BaseProvider {
142
+ /**
143
+ * Set identity for scoped storage
144
+ */
145
+ setIdentity(identity: FullIdentity): void;
146
+ /**
147
+ * Get value by key
148
+ */
149
+ get(key: string): Promise<string | null>;
150
+ /**
151
+ * Set value by key
152
+ */
153
+ set(key: string, value: string): Promise<void>;
154
+ /**
155
+ * Remove key
156
+ */
157
+ remove(key: string): Promise<void>;
158
+ /**
159
+ * Check if key exists
160
+ */
161
+ has(key: string): Promise<boolean>;
162
+ /**
163
+ * Get all keys with optional prefix filter
164
+ */
165
+ keys(prefix?: string): Promise<string[]>;
166
+ /**
167
+ * Clear all keys with optional prefix filter
168
+ */
169
+ clear(prefix?: string): Promise<void>;
170
+ /**
171
+ * Save tracked addresses (only user state: index, hidden, timestamps)
172
+ */
173
+ saveTrackedAddresses(entries: TrackedAddressEntry[]): Promise<void>;
174
+ /**
175
+ * Load tracked addresses
176
+ */
177
+ loadTrackedAddresses(): Promise<TrackedAddressEntry[]>;
178
+ }
179
+
180
+ /**
181
+ * token-engine/engine.ts — the FROZEN public port (ITokenEngine) + its config.
182
+ *
183
+ * This is the contract both migration tracks build against. It is sphere-domain
184
+ * only (see types.ts). The granular, SDK-typed steps (buildMint, submit,
185
+ * awaitProof, certify, …) are an INTERNAL concern of the real adapter and are
186
+ * intentionally NOT part of this public interface.
187
+ */
188
+
189
+ /**
190
+ * Durable, any-device store for a split's burn checkpoint (sdk-changes E.4, sphere-sdk#501
191
+ * option b). The engine passes opaque PLAINTEXT bytes; a transport adapter (the wallet-api
192
+ * provider) is responsible for AAD-encryption and the wallet-api §16 intent-progress round-trip.
193
+ *
194
+ * The store is the resume seed for the one split input the engine cannot re-derive — the burn's
195
+ * aggregator-issued inclusion proof — so the mint justification is rebuilt from stored bytes, not
196
+ * a refetch (the aggregator regenerates proofs per request).
197
+ */
198
+ interface SplitCheckpointStore {
199
+ /**
200
+ * Insert-once, first-write-wins. MUST resolve only AFTER the store acked durability, and MUST
201
+ * resolve with the AUTHORITATIVE stored bytes — the caller's own on a fresh write, or the FIRST
202
+ * writer's when the slot `(transferId, opIndex)` was already taken (the caller adopts those and
203
+ * mints from them, so concurrent resumers converge on one burn proof).
204
+ */
205
+ put(transferId: string, opIndex: number, bytes: Uint8Array): Promise<Uint8Array>;
206
+ /** The stored bytes for the slot, or `null` when none exists. */
207
+ get(transferId: string, opIndex: number): Promise<Uint8Array | null>;
208
+ }
209
+ /** Options common to the long-running, network-bound operations. */
210
+ interface EngineOpOptions {
211
+ /** Cancels the operation (including inclusion-proof polling). */
212
+ readonly signal?: AbortSignal;
213
+ /**
214
+ * Durable burn-checkpoint store for a resumable split (sdk-changes E.4, sphere-sdk#501). When
215
+ * present, `split()` persists the burn's certified proof AND awaits its durable ack BEFORE
216
+ * submitting any mint leg, and rebuilds the mint justification from the stored bytes on resume —
217
+ * so a split resumed after any mint leg certified recovers instead of stranding its outputs.
218
+ *
219
+ * Absent keeps today's behavior (a fresh burn proof per attempt): fine for a mid-split resume
220
+ * with no mint yet certified, but a split resumed AFTER a mint certified cannot recover — a
221
+ * documented residual for fully-local compositions; the live path MUST supply the store.
222
+ */
223
+ readonly checkpointStore?: SplitCheckpointStore;
224
+ /**
225
+ * Realization seed for deterministic transfer/split (Part E, sdk-changes E.1/E.3):
226
+ * a client-generated UUIDv4 in canonical lowercase string form. Every value the
227
+ * transaction binds to (stateMask, per-output salts) is HKDF-derived from the
228
+ * wallet key + this id, so re-calling the op with the same `transferId` and
229
+ * inputs rebuilds the byte-identical transaction and resumes an interrupted
230
+ * attempt instead of losing funds. Persist it BEFORE calling the engine.
231
+ *
232
+ * If absent, the engine generates one internally (`crypto.randomUUID()`) — the
233
+ * derivation path is identical, but the call is NOT resumable (the seed is
234
+ * gone if the process dies mid-op).
235
+ */
236
+ readonly transferId?: string;
237
+ /**
238
+ * Ordinal of this engine op WITHIN one logical send sharing a `transferId`
239
+ * (ARCHITECTURE §7: D whole-token transfers + at most one split under ONE
240
+ * intent). It indexes the op-level HKDF derivations (`stateMask`, the split
241
+ * `burn` mask) so distinct ops never reuse a mask — §8.1's "per-transfer
242
+ * unique". Per-output split salts are indexed by output ordinal in their own
243
+ * `salt` field domain; callers MUST NOT run two splits under one transferId.
244
+ * Default 0 (single-op sends). Resume MUST replay the same (transferId,
245
+ * opIndex) pairing per source.
246
+ */
247
+ readonly opIndex?: number;
248
+ }
249
+ /**
250
+ * The token engine port. The wallet's secp256k1 identity, the target network,
251
+ * the aggregator client and the trust base are all bound at construction
252
+ * (see EngineConfig); operations below take only sphere-domain arguments.
253
+ */
254
+ interface ITokenEngine {
255
+ /** This engine's wallet identity (chain pubkey). Synchronous. */
256
+ getIdentity(): EngineIdentity;
257
+ /**
258
+ * Legacy `DIRECT://` address for the given pubkey (defaults to this engine's
259
+ * identity). This is the ONLY "address" in v2 and is kept stable across the
260
+ * migration (Path A) so Quest XP / Unicity IDs keyed on it survive. Async —
261
+ * the derivation hashes via the SDK.
262
+ */
263
+ deriveIdentityAddress(pubkey?: Uint8Array): Promise<string>;
264
+ /**
265
+ * Genesis-stable token id — 64-char lowercase hex of the v2 TokenId (same
266
+ * across every state). Use for dedup / history / tombstone keys. Synchronous.
267
+ */
268
+ tokenId(token: SphereToken): string;
269
+ /** Decoded value of a token (cached). Synchronous. */
270
+ readValue(token: SphereToken): SphereValue | null;
271
+ /** Balance of a single coin within a token. Synchronous. */
272
+ balanceOf(token: SphereToken, coinId: CoinId): bigint;
273
+ /**
274
+ * The opaque on-chain memo delivered with this token: the latest transfer's
275
+ * data for a transferred token, else the memo in a minted output's value
276
+ * envelope (split). Returns `null` when there is no memo — including for data
277
+ * tokens (no value envelope; use `readTokenData`) and memo-less value tokens.
278
+ * To tell a data token from a value token, check `readValue` (null ⇒
279
+ * data/value-less token). Synchronous.
280
+ */
281
+ readMemo(token: SphereToken): Uint8Array | null;
282
+ /** Raw genesis data of a token (e.g. a data-token's terms). `null` when absent. Synchronous. */
283
+ readTokenData(token: SphereToken): Uint8Array | null;
284
+ /**
285
+ * Mint (issue) a new token to a recipient pubkey. NOT a wallet end-user flow —
286
+ * this is the issuer/developer capability: an app issuing its own tokens
287
+ * (rewards, in-app currency, tickets) to users, or seeding test balances. v2
288
+ * makes standalone mint first-class (Token.mint accepts a genesis with a null
289
+ * justification). Split's per-output mint is a separate, internal path; the
290
+ * Unicity-ID/nametag mint is a distinct identity surface (see migration plan §4.4).
291
+ */
292
+ mint(params: MintParams, options?: EngineOpOptions): Promise<SphereToken>;
293
+ /**
294
+ * Mint a NON-value (data) token: opaque `data` + custom `tokenType` + deterministic
295
+ * `salt` → a stable, terms-derived `tokenId`. The result has `value === null`;
296
+ * read its bytes via `readTokenData`. (Used e.g. for on-chain invoice tokens.)
297
+ */
298
+ mintDataToken(params: MintDataTokenParams, options?: EngineOpOptions): Promise<SphereToken>;
299
+ /** Spend a token wholesale to a recipient pubkey; returns the recipient's finished token. */
300
+ transfer(params: TransferParams, options?: EngineOpOptions): Promise<SphereToken>;
301
+ /** Split a token into N value-conserving outputs (burn source + internally mint each output). */
302
+ split(params: SplitParams, options?: EngineOpOptions): Promise<SplitResult>;
303
+ /** Fully verify a token against the trust base. */
304
+ verify(token: SphereToken, options?: EngineOpOptions): Promise<EngineVerifyResult>;
305
+ /** Whether the token's current state has already been spent on the network. */
306
+ isSpent(token: SphereToken, options?: EngineOpOptions): Promise<boolean>;
307
+ /**
308
+ * Whether the token's CURRENT state is locked to `SignaturePredicate(pubkey)`.
309
+ * Local + synchronous (predicate byte-compare, no network). The receive path
310
+ * uses it to reject tokens that are not actually addressed to this wallet.
311
+ */
312
+ isOwnedBy(token: SphereToken, pubkey: Uint8Array): boolean;
313
+ /** Serialize a token for storage/transport. Synchronous. */
314
+ encodeToken(token: SphereToken): TokenBlob;
315
+ /** Reconstruct a token from its blob (decodes embedded payment data). */
316
+ decodeToken(blob: TokenBlob): Promise<SphereToken>;
317
+ /**
318
+ * The backend-true delivery keys for encoded TokenBlob bytes: the
319
+ * genesis-stable tokenId and the SDK's PROTOCOL state hash of the latest
320
+ * state (DataHash imprint, hex). wallet-api keys mailbox entries on exactly
321
+ * this pair (entry_id = SHA-256(tokenId ‖ stateHash); §8.2 step 4 validates
322
+ * a deposit's claimed stateHash against it) — a plain hash over the token
323
+ * bytes is NOT this value and 422s on deposit. Delivery implementations MUST
324
+ * derive their ids through this method, never locally.
325
+ */
326
+ deliveryKeys(blobBytes: Uint8Array): Promise<{
327
+ tokenId: string;
328
+ stateHash: string;
329
+ }>;
330
+ /**
331
+ * Release engine-owned OS resources — today only the verification worker pool.
332
+ * Idempotent; optional because an engine owning none need not define it.
333
+ */
334
+ dispose?(): void;
335
+ }
336
+
337
+ /**
338
+ * SDK2 Core Types
339
+ * Platform-independent type definitions
340
+ */
341
+ type ProviderStatus = 'disconnected' | 'connecting' | 'connected' | 'error';
342
+ interface ProviderMetadata {
343
+ readonly id: string;
344
+ readonly name: string;
345
+ readonly type: 'local' | 'cloud' | 'p2p' | 'network';
346
+ readonly description?: string;
347
+ }
348
+ interface BaseProvider extends ProviderMetadata {
349
+ connect(config?: unknown): Promise<void>;
350
+ disconnect(): Promise<void>;
351
+ isConnected(): boolean;
352
+ getStatus(): ProviderStatus;
353
+ }
354
+ interface Identity {
355
+ /** 33-byte compressed secp256k1 public key (for L3 chain) */
356
+ readonly chainPubkey: string;
357
+ /** L3 DIRECT address (DIRECT://...) */
358
+ readonly directAddress?: string;
359
+ readonly ipnsName?: string;
360
+ readonly nametag?: string;
361
+ }
362
+ interface FullIdentity extends Identity {
363
+ readonly privateKey: string;
364
+ }
365
+ type TokenStatus = 'pending' | 'submitted' | 'confirmed' | 'transferring' | 'spent' | 'invalid';
366
+ interface Token {
367
+ readonly id: string;
368
+ readonly coinId: string;
369
+ readonly symbol: string;
370
+ readonly name: string;
371
+ readonly decimals: number;
372
+ readonly iconUrl?: string;
373
+ readonly amount: string;
374
+ status: TokenStatus;
375
+ readonly createdAt: number;
376
+ updatedAt: number;
377
+ readonly sdkData?: string;
378
+ /**
379
+ * Lazy inventory record (sdk-changes S2): the token's VALUE metadata came
380
+ * from the storage provider's `listInventory()` view and its blob has not
381
+ * been downloaded — `sdkData` is absent and the blob is fetched on demand
382
+ * (`getToken`) only when the token is selected for a spend.
383
+ */
384
+ readonly lazy?: boolean;
385
+ /**
386
+ * #625 (self-healing coin selection): a send selected this source but its state was already spent
387
+ * on-chain (`TransferConflictError`). It is KEPT in inventory (visible, recoverable by a resync —
388
+ * never auto-removed) but EXCLUDED from spend selection, so coin-selection picks live tokens and the
389
+ * send self-heals instead of wedging on a stale source.
390
+ */
391
+ suspectedSpent?: boolean;
392
+ }
393
+ interface Asset {
394
+ readonly coinId: string;
395
+ readonly symbol: string;
396
+ readonly name: string;
397
+ readonly decimals: number;
398
+ readonly iconUrl?: string;
399
+ readonly totalAmount: string;
400
+ readonly tokenCount: number;
401
+ /** Sum of confirmed token amounts (smallest units) */
402
+ readonly confirmedAmount: string;
403
+ /** Sum of unconfirmed (submitted/pending) token amounts (smallest units) */
404
+ readonly unconfirmedAmount: string;
405
+ /** Number of confirmed tokens aggregated */
406
+ readonly confirmedTokenCount: number;
407
+ /** Number of unconfirmed tokens aggregated */
408
+ readonly unconfirmedTokenCount: number;
409
+ /** Number of tokens currently being sent (in-flight, NOT spendable) */
410
+ readonly transferringTokenCount: number;
411
+ /**
412
+ * Sum of in-flight (`'transferring'`) token amounts (smallest units). These
413
+ * tokens are LEAVING the wallet during an active send, so they are excluded
414
+ * from {@link totalAmount}, {@link confirmedAmount}, and
415
+ * {@link unconfirmedAmount} — surfaced here so a UI can still show a "Sending"
416
+ * badge without inflating the spendable balance.
417
+ */
418
+ readonly transferringAmount: string;
419
+ /** Price per whole unit in USD (null if PriceProvider not configured) */
420
+ readonly priceUsd: number | null;
421
+ /** Price per whole unit in EUR (null if PriceProvider not configured) */
422
+ readonly priceEur: number | null;
423
+ /** 24h price change percentage (null if unavailable) */
424
+ readonly change24h: number | null;
425
+ /** Total fiat value in USD: (totalAmount / 10^decimals) * priceUsd */
426
+ readonly fiatValueUsd: number | null;
427
+ /** Total fiat value in EUR */
428
+ readonly fiatValueEur: number | null;
429
+ }
430
+ type TransferStatus = 'pending' | 'submitted' | 'confirmed' | 'delivered' | 'completed' | 'failed';
431
+ /**
432
+ * Per-token transfer detail tracking the on-chain commitment or split operation
433
+ * for each source token involved in a transfer.
434
+ */
435
+ interface TokenTransferDetail {
436
+ /** Source token ID that was consumed in this transfer */
437
+ readonly sourceTokenId: string;
438
+ /** Transfer method used for this token */
439
+ readonly method: 'direct' | 'split';
440
+ /** Aggregator commitment request ID hex (for direct transfers) */
441
+ readonly requestIdHex?: string;
442
+ /** Split group ID (for split transfers — correlates sender/recipient/change tokens) */
443
+ readonly splitGroupId?: string;
444
+ /** Nostr event ID (for split transfers delivered via Nostr) */
445
+ readonly nostrEventId?: string;
446
+ }
447
+ interface TransferResult {
448
+ readonly id: string;
449
+ status: TransferStatus;
450
+ readonly tokens: Token[];
451
+ /** Per-token transfer details — one entry per source token consumed */
452
+ readonly tokenTransfers: TokenTransferDetail[];
453
+ error?: string;
454
+ /**
455
+ * True when the send certified on-chain but the recipient-side delivery did not land
456
+ * (covenant §3.1 — issue #621): the source is terminally spent, the finished blob is
457
+ * journaled, and the sender is NOT failed. Distinguishes "sent, delivery deferred" from
458
+ * a real send failure. See {@link deliveryState}.
459
+ */
460
+ deliveryPending?: boolean;
461
+ /** 'landed' = delivered to the recipient's mailbox; 'pending-delivery' = certified, journaled, awaiting (re-)delivery. */
462
+ deliveryState?: 'landed' | 'pending-delivery';
463
+ }
464
+ interface IncomingTransfer {
465
+ readonly id: string;
466
+ readonly senderPubkey: string;
467
+ readonly senderNametag?: string;
468
+ readonly tokens: Token[];
469
+ readonly memo?: string;
470
+ readonly receivedAt: number;
471
+ }
472
+ /**
473
+ * Minimal data stored in persistent storage for a tracked address.
474
+ * Only contains user state — derived fields are computed on load.
475
+ */
476
+ interface TrackedAddressEntry {
477
+ /** HD derivation index (0, 1, 2, ...) */
478
+ readonly index: number;
479
+ /** Whether this address is hidden from UI display */
480
+ hidden: boolean;
481
+ /** Timestamp (ms) when this address was first activated */
482
+ readonly createdAt: number;
483
+ /** Timestamp (ms) of last modification */
484
+ updatedAt: number;
485
+ }
486
+
487
+ interface SendRequest {
488
+ recipient: string;
489
+ amount: string;
490
+ coinId: string;
491
+ memo?: string;
492
+ }
493
+ interface MintResult {
494
+ success: boolean;
495
+ tokenId?: string;
496
+ error?: string;
497
+ }
498
+ interface HistoryEntry {
499
+ id: string;
500
+ type: 'SENT' | 'RECEIVED' | 'MINT';
501
+ coinId: string;
502
+ amount: string;
503
+ symbol?: string;
504
+ timestamp: number;
505
+ memo?: string;
506
+ transferId?: string;
507
+ tokenId?: string;
508
+ senderPubkey?: string;
509
+ senderNametag?: string;
510
+ recipientPubkey?: string;
511
+ recipientNametag?: string;
512
+ tokenIds?: {
513
+ id: string;
514
+ amount: string;
515
+ }[];
516
+ }
517
+ interface HistoryPage {
518
+ entries: HistoryEntry[];
519
+ more: boolean;
520
+ cursor: string | null;
521
+ }
522
+ type PaymentRequestStatus = 'pending' | 'settling' | 'paid' | 'rejected' | 'expired';
523
+ interface PaymentRequestView {
524
+ id: string;
525
+ requestId: string;
526
+ senderPubkey: string;
527
+ senderNametag?: string;
528
+ amount: string;
529
+ coinId: string;
530
+ symbol?: string;
531
+ message?: string;
532
+ timestamp: number;
533
+ status: PaymentRequestStatus;
534
+ }
535
+ interface PaymentsRequestsApi {
536
+ create(to: string, terms: {
537
+ coinId: string;
538
+ amount: string;
539
+ memo?: string;
540
+ }): Promise<{
541
+ success: boolean;
542
+ requestId?: string;
543
+ error?: string;
544
+ }>;
545
+ list(): PaymentRequestView[];
546
+ pay(id: string): Promise<TransferResult>;
547
+ decline(id: string): Promise<void>;
548
+ dismissProcessed(): void;
549
+ }
550
+ interface PaymentsV2 {
551
+ assets(coinId?: string): Promise<Asset[]>;
552
+ tokens(filter?: {
553
+ coinId?: string;
554
+ }): Token[];
555
+ history(page?: {
556
+ before?: string;
557
+ limit?: number;
558
+ }): Promise<HistoryPage>;
559
+ send(req: SendRequest): Promise<TransferResult>;
560
+ mint(coinId: string, amount: bigint): Promise<MintResult>;
561
+ receive(): Promise<{
562
+ transfers: IncomingTransfer[];
563
+ }>;
564
+ readonly requests: PaymentsRequestsApi;
565
+ }
566
+ interface PaymentsV2Events {
567
+ 'transfer:incoming': IncomingTransfer;
568
+ 'transfer:updated': TransferResult;
569
+ 'transfer:attention': {
570
+ transferId: string;
571
+ code: string;
572
+ detail?: string;
573
+ };
574
+ 'inventory:updated': Record<string, never>;
575
+ 'history:updated': Record<string, never>;
576
+ 'payment_request:incoming': PaymentRequestView;
577
+ 'payment_request:updated': {
578
+ id: string;
579
+ status: PaymentRequestStatus;
580
+ };
581
+ 'connection:status': {
582
+ status: 'connected' | 'degraded' | 'offline';
583
+ };
584
+ }
585
+
586
+ interface InventoryAsset {
587
+ coinId: string;
588
+ amount: string;
589
+ }
590
+ interface InventoryItem {
591
+ tokenId: string;
592
+ status: 'active' | 'removed';
593
+ seq: number;
594
+ stateHash: string;
595
+ assets?: InventoryAsset[];
596
+ }
597
+ interface InventoryPage {
598
+ cursor: number;
599
+ syncEpoch: string;
600
+ more: boolean;
601
+ items: InventoryItem[];
602
+ }
603
+ interface ApplyDeltaResult {
604
+ cursor: number;
605
+ syncEpoch: string;
606
+ }
607
+ interface StoragePort {
608
+ listInventory(since?: number): Promise<InventoryPage>;
609
+ balances(): Promise<{
610
+ coinId: string;
611
+ total: string;
612
+ tokenCount: number;
613
+ }[]>;
614
+ getBlobs(tokenIds: string[]): Promise<Map<string, Uint8Array>>;
615
+ uploadBlobs(blobs: {
616
+ sha256: string;
617
+ bytes: Uint8Array;
618
+ }[]): Promise<Map<string, string>>;
619
+ applyDelta(delta: {
620
+ transferId: string;
621
+ spent: string[];
622
+ added: {
623
+ tokenId: string;
624
+ key: string;
625
+ }[];
626
+ }): Promise<ApplyDeltaResult>;
627
+ }
628
+ interface DeliveryReceipt {
629
+ deliveryId: string;
630
+ }
631
+ interface IncomingDelivery {
632
+ deliveryId: string;
633
+ transferId?: string;
634
+ senderPubkey?: string;
635
+ senderNametag?: string;
636
+ memo?: string;
637
+ fetchBlob(): Promise<Uint8Array>;
638
+ cursor: string;
639
+ }
640
+ interface DeliverOptions {
641
+ transferId: string;
642
+ memo?: string;
643
+ senderNametag?: string;
644
+ }
645
+ interface DeliveryPort {
646
+ bindDeliveryKeys(derive: (blob: Uint8Array) => Promise<{
647
+ tokenId: string;
648
+ stateHash: string;
649
+ }>): void;
650
+ deliver(recipientPubkey: string, blob: Uint8Array, options: DeliverOptions): Promise<DeliveryReceipt>;
651
+ deliverBatch?(recipientPubkey: string, blobs: Uint8Array[], options: DeliverOptions): Promise<DeliveryReceipt[]>;
652
+ incoming(sinceCursor?: string): AsyncIterable<IncomingDelivery>;
653
+ ack(deliveryId: string, disposition: 'claimed' | 'rejected', reason?: 'invalid' | 'not-owned' | 'storage-rejected' | 'other'): Promise<void>;
654
+ onWake?(cb: () => void): () => void;
655
+ }
656
+
657
+ interface IntentPayload {
658
+ v: 2;
659
+ recipient: string;
660
+ coinId: string;
661
+ amount: string;
662
+ memo?: string;
663
+ direct: string[];
664
+ split?: {
665
+ tokenId: string;
666
+ splitAmount: string;
667
+ remainderAmount: string;
668
+ };
669
+ spentStates?: Record<string, {
670
+ local: string;
671
+ protocol: string;
672
+ }>;
673
+ }
674
+ type MachinePhase = 'PLANNED' | 'INTENT_OPEN' | 'CERTIFYING' | 'DELIVERING' | 'APPLYING' | 'CLOSING' | 'DONE' | 'FAILED';
675
+ interface PlannedOp {
676
+ kind: 'direct' | 'split';
677
+ opIndex: number;
678
+ sourceTokenId: string;
679
+ deliveredAmount: bigint;
680
+ }
681
+ interface OpOutcome {
682
+ op: PlannedOp;
683
+ certified: boolean;
684
+ recipientBlob?: Uint8Array;
685
+ changeBlob?: Uint8Array;
686
+ error?: unknown;
687
+ }
688
+ type OutcomeClass = 'keep-open' | 'conflict' | 'clean-reject' | 'other';
689
+
690
+ interface ScopedKV {
691
+ get<T>(key: string): Promise<T | null>;
692
+ set<T>(key: string, value: T): Promise<void>;
693
+ remove(key: string): Promise<void>;
694
+ }
695
+ declare function createScopedKV(storage: StorageProvider, network: string, chainPubkey: string): ScopedKV;
696
+ interface IntentBackstopEntry {
697
+ transferId: string;
698
+ payloadEnvelope: string;
699
+ requiresSeedClose: boolean;
700
+ disposition: 'open' | 'abortPending' | 'aborted';
701
+ createdAt: number;
702
+ }
703
+ interface CheckpointCacheEntry {
704
+ transferId: string;
705
+ opIndex: number;
706
+ envelope: string;
707
+ }
708
+ interface DeliveryJournalEntry {
709
+ transferId: string;
710
+ opIndex: number;
711
+ recipientPubkey: string;
712
+ blobHex: string;
713
+ memo?: string;
714
+ attempts: number;
715
+ deferredUntil?: number;
716
+ undeliverable?: boolean;
717
+ }
718
+ interface MintJournalEntry {
719
+ mintId: string;
720
+ coinId: string;
721
+ amount: string;
722
+ tokenId: string;
723
+ createdAt: number;
724
+ }
725
+ interface ShortfallEntry {
726
+ transferId: string;
727
+ remainingAmount: string;
728
+ coinId: string;
729
+ recipient: string;
730
+ committedTokenIds: string[];
731
+ createdAt: number;
732
+ }
733
+ interface SettlingLink {
734
+ requestId: string;
735
+ transferId: string;
736
+ committed: boolean;
737
+ createdAt: number;
738
+ }
739
+ type StreamName = 'inventory' | 'mailbox' | 'payment_requests' | 'history';
740
+ interface StreamCursor {
741
+ cursor: number | string;
742
+ syncEpoch: string;
743
+ }
744
+ declare const STORE_KEYS: {
745
+ readonly intentBackstop: "intents";
746
+ readonly checkpointCache: "checkpoints";
747
+ readonly deliveryJournal: "delivery-journal";
748
+ readonly mintJournal: "mint-journal";
749
+ readonly shortfalls: "shortfalls";
750
+ readonly settlingLinks: "settling";
751
+ readonly streamCursor: (s: StreamName) => string;
752
+ readonly epochLatch: "epoch-latch";
753
+ };
754
+
755
+ type RequestWireStatus = 'open' | 'paid' | 'declined' | 'expired';
756
+ interface RequestWire {
757
+ readonly id: string;
758
+ readonly seq: number;
759
+ readonly fromPubkey: string;
760
+ readonly toPubkey: string;
761
+ readonly assets: readonly {
762
+ readonly coinId: string;
763
+ readonly amount: string;
764
+ }[];
765
+ readonly memo?: string;
766
+ readonly status: RequestWireStatus;
767
+ readonly transferId: string | null;
768
+ readonly createdAt: string;
769
+ readonly expiresAt?: string;
770
+ }
771
+ interface RequestsPageWire {
772
+ readonly requests: readonly RequestWire[];
773
+ readonly more: boolean;
774
+ readonly cursor: number | string | null;
775
+ readonly syncEpoch: number;
776
+ }
777
+ interface RequestsWireClient {
778
+ createPaymentRequest(input: {
779
+ toPubkey: string;
780
+ assets: {
781
+ coinId: string;
782
+ amount: string;
783
+ }[];
784
+ memo?: string;
785
+ expiresAt?: string;
786
+ }): Promise<RequestWire>;
787
+ listPaymentRequests(params: {
788
+ role: 'incoming';
789
+ since?: number;
790
+ }): Promise<RequestsPageWire>;
791
+ respondPaymentRequest(id: string, response: {
792
+ action: 'paid';
793
+ transferId: string;
794
+ } | {
795
+ action: 'declined';
796
+ }): Promise<RequestWire>;
797
+ }
798
+ interface RequestMemoCodec {
799
+ encrypt(peerPubkey: string, bundle: {
800
+ memo?: string;
801
+ senderNametag?: string;
802
+ }): string | undefined;
803
+ decrypt(peerPubkey: string, envelope: string): {
804
+ memo?: string;
805
+ senderNametag?: string;
806
+ };
807
+ }
808
+ type RequestsEmit = <K extends 'payment_request:incoming' | 'payment_request:updated'>(event: K, payload: PaymentsV2Events[K]) => void;
809
+ interface RequestsDeps {
810
+ readonly client: RequestsWireClient;
811
+ readonly kv: ScopedKV;
812
+ readonly ownPubkey: string;
813
+ readonly send: (request: SendRequest) => Promise<TransferResult>;
814
+ readonly resolvePubkey: (identifier: string) => Promise<string | null>;
815
+ readonly listAbortedTransferIds: () => Promise<string[]>;
816
+ readonly emit: RequestsEmit;
817
+ readonly memo: RequestMemoCodec;
818
+ readonly ownNametag?: () => string | undefined;
819
+ readonly now?: () => number;
820
+ }
821
+ interface ResumeOutcomes {
822
+ readonly resumed: readonly string[];
823
+ readonly conflicted: readonly string[];
824
+ readonly open: readonly string[];
825
+ }
826
+ declare class Requests implements PaymentsRequestsApi {
827
+ private readonly deps;
828
+ private readonly mirror;
829
+ private hydrated;
830
+ private drainInFlight;
831
+ private journal;
832
+ private journalLoad;
833
+ private journalTail;
834
+ private readonly payInFlight;
835
+ private reconcileInFlight;
836
+ constructor(deps: RequestsDeps);
837
+ private now;
838
+ private ensureJournalLoaded;
839
+ private mutateJournal;
840
+ private writeLink;
841
+ private clearLink;
842
+ drainIncoming(): Promise<void>;
843
+ private doDrain;
844
+ private surface;
845
+ private decryptMemo;
846
+ private setStatus;
847
+ list(): PaymentRequestView[];
848
+ create(to: string, terms: {
849
+ coinId: string;
850
+ amount: string;
851
+ memo?: string;
852
+ }): Promise<{
853
+ success: boolean;
854
+ requestId?: string;
855
+ error?: string;
856
+ }>;
857
+ pay(id: string): Promise<TransferResult>;
858
+ private payInner;
859
+ /** 'paid' respond leg: 409 = already resolved = idempotent success; other errors defer. */
860
+ private respondPaid;
861
+ decline(id: string): Promise<void>;
862
+ dismissProcessed(): void;
863
+ reconcile(outcomes: ResumeOutcomes): Promise<void>;
864
+ private reconcileUnaccounted;
865
+ private resolvePaid;
866
+ private revertPayable;
867
+ }
868
+
869
+ interface HistoryWireAsset {
870
+ coinId: string;
871
+ amount: string;
872
+ }
873
+ interface HistoryWireRecord {
874
+ dedupKey: string;
875
+ id: string;
876
+ type: 'SENT' | 'RECEIVED' | 'MINT';
877
+ assets: HistoryWireAsset[];
878
+ ts: string;
879
+ transferId?: string;
880
+ tokenId?: string;
881
+ counterpartyPubkey?: string;
882
+ counterpartyNametag?: string;
883
+ memo?: string;
884
+ }
885
+ interface HistoryClient {
886
+ listHistory(options?: {
887
+ before?: string;
888
+ limit?: number;
889
+ }): Promise<{
890
+ records: readonly HistoryWireRecord[];
891
+ more: boolean;
892
+ cursor: string | null;
893
+ }>;
894
+ postHistory(records: HistoryWireRecord[]): Promise<unknown>;
895
+ }
896
+
897
+ interface RegistryReader {
898
+ getSymbol(coinId: string): string;
899
+ getName(coinId: string): string;
900
+ getDecimals(coinId: string): number;
901
+ getIconUrl(coinId: string): string | null;
902
+ }
903
+ interface PriceQuote {
904
+ readonly priceUsd: number;
905
+ readonly priceEur?: number;
906
+ readonly change24h?: number;
907
+ }
908
+ interface PriceReader {
909
+ getPrices(tokenNames: string[]): Promise<Map<string, PriceQuote>>;
910
+ }
911
+
912
+ /**
913
+ * TODO(mint-F13 seam): `engine.mint` today salts fresh per call, so an
914
+ * interrupted mint is not re-derivable. When F13 lands transferId-seeded
915
+ * determinism the engine advertises it here and replay re-derives by mintId.
916
+ */
917
+ interface DeterministicMintCapable {
918
+ readonly deterministicMint: true;
919
+ deriveMintTokenId(params: MintParams, mintId: string): Promise<string>;
920
+ }
921
+ declare function supportsDeterministicMint(engine: ITokenEngine): engine is ITokenEngine & DeterministicMintCapable;
922
+ interface RecipientInfo {
923
+ chainPubkey: string;
924
+ network: string;
925
+ nametag?: string;
926
+ }
927
+ interface FacadeSession {
928
+ start(): Promise<void>;
929
+ stop(): Promise<void>;
930
+ subscribeStream(stream: 'inventory' | 'mailbox' | 'payment_requests', handler: () => void): () => void;
931
+ }
932
+ interface IntentWireLike {
933
+ transferId: string;
934
+ payload: string;
935
+ createdAt: string;
936
+ }
937
+ /** Structural slice of WalletApiV2Client the facade consumes directly. */
938
+ interface FacadeClient extends HistoryClient, RequestsWireClient {
939
+ putIntent(transferId: string, payloadEnvelope: string, requiresSeedClose?: boolean): Promise<void>;
940
+ listIntents(status?: 'open' | 'aborted'): Promise<IntentWireLike[]>;
941
+ abortIntent(transferId: string): Promise<void>;
942
+ completeIntent(transferId: string, signature?: string): Promise<void>;
943
+ }
944
+ interface PaymentsFacadeDeps {
945
+ session: FacadeSession;
946
+ client: FacadeClient;
947
+ storagePort: StoragePort;
948
+ deliveryPort: DeliveryPort;
949
+ checkpointStore: SplitCheckpointStore;
950
+ /** Initial engine source; setEngine() swaps what FUTURE operations snapshot. */
951
+ engineRef: () => ITokenEngine;
952
+ kv: ScopedKV;
953
+ registry: RegistryReader;
954
+ price?: PriceReader;
955
+ emit: (event: string, payload: unknown) => void;
956
+ resolveRecipient: (identifier: string) => Promise<RecipientInfo | null>;
957
+ signComplete: (transferId: string) => Promise<string>;
958
+ /** S6 self-scoped field key: intent payload envelope + history memo/nametag. */
959
+ fieldKey: Uint8Array;
960
+ /** The session's network — a recipient not verifiably on it is refused (§5.6). */
961
+ network: string;
962
+ ownPubkey: string;
963
+ ownNametag?: () => string | undefined;
964
+ requestMemo: RequestMemoCodec;
965
+ syncEpoch?: () => string;
966
+ now?: () => number;
967
+ newId?: () => string;
968
+ workBudget?: number;
969
+ receivePollMs?: number;
970
+ }
971
+
972
+ /** Max re-plans after a conflicted attempt (#625/#677 parity with the old send loop). */
973
+ declare const MAX_RESELECT = 8;
974
+ declare const ATTENTION_MINT_UNRESOLVED = "mint:unresolved";
975
+ declare class PaymentsFacade implements PaymentsV2 {
976
+ private readonly deps;
977
+ private readonly view;
978
+ private readonly ledger;
979
+ private readonly queue;
980
+ private readonly machine;
981
+ private readonly machineDeps;
982
+ private readonly machineStores;
983
+ private readonly historyStore;
984
+ private readonly receiveLoop;
985
+ private readonly heldStates;
986
+ private readonly ownPubkeyBytes;
987
+ readonly requests: Requests;
988
+ private currentEngine;
989
+ private readonly pendingOps;
990
+ private unsubscribers;
991
+ private started;
992
+ constructor(deps: PaymentsFacadeDeps);
993
+ start(): Promise<void>;
994
+ /** §7 same-address restart gate: resolves only after in-flight ops settle. */
995
+ stop(): Promise<void>;
996
+ /** Swaps what FUTURE operations snapshot; in-flight ops finish on the old engine. */
997
+ setEngine(next: ITokenEngine): void;
998
+ assets(coinId?: string): Promise<Asset[]>;
999
+ tokens(filter?: {
1000
+ coinId?: string;
1001
+ }): Token[];
1002
+ history(page?: {
1003
+ before?: string;
1004
+ limit?: number;
1005
+ }): Promise<HistoryPage>;
1006
+ send(request: SendRequest): Promise<TransferResult>;
1007
+ receive(): Promise<{
1008
+ transfers: IncomingTransfer[];
1009
+ }>;
1010
+ mint(coinId: string, amount: bigint): Promise<MintResult>;
1011
+ private sendWithPolicy;
1012
+ /** §5.6 cross-network deposit trap: refused BEFORE any reserve/certification. */
1013
+ private requireSameNetworkRecipient;
1014
+ private runAttempt;
1015
+ /**
1016
+ * Partial outcome (#677/#690): the shortfall is already durable (written by
1017
+ * the machine BEFORE complete). Accumulate the settled set, then re-plan ONLY
1018
+ * the remainder under a NEW transferId — never the full amount.
1019
+ */
1020
+ private consumePartial;
1021
+ /**
1022
+ * One attempt's failure disposition: possibly-committed → rethrow UNWRAPPED;
1023
+ * backstop still 'open' (committed>0) → converge via the same machine's
1024
+ * resumeIntent; clean conflict with a demoted source → bounded full re-plan.
1025
+ */
1026
+ private disposeFailedAttempt;
1027
+ /** #677/#690: ≥1 leg committed — the SAME machine, resumed in-process, converges it. */
1028
+ private convergePartial;
1029
+ private disposeConvergeFailure;
1030
+ private planAndMaterialize;
1031
+ /** Reserve happened synchronously inside queue.plan; mark + declare with no await between. */
1032
+ private markPlanned;
1033
+ private materialize;
1034
+ /** isSpent sweep over the attempt's sources; demote proven-spent states (durable). */
1035
+ private demoteSpentSources;
1036
+ private settleSuccess;
1037
+ private settleFailure;
1038
+ /** Open intent: sources stay reserved + in-flight (§5.2) until resume adopts them. */
1039
+ private settleKeepOpen;
1040
+ private settlePartial;
1041
+ /** Release spent sources only AFTER the mirror refresh tombstoned them. */
1042
+ private refreshThenRelease;
1043
+ private accumulate;
1044
+ private finishSend;
1045
+ /**
1046
+ * With nothing delivered the error passes UNWRAPPED (identity + cause kept);
1047
+ * after ≥1 delivered leg EVERY failure surfaces as PartialSendConflictError
1048
+ * over the accumulated settled set — never bare, never a full-amount retry.
1049
+ */
1050
+ private partialize;
1051
+ /** #441: possibly-committed errors must carry the transferId for the settling journal. */
1052
+ private stampTransferId;
1053
+ private softAbort;
1054
+ private mintInner;
1055
+ private finalizeMint;
1056
+ private replayMints;
1057
+ private replayMint;
1058
+ private mintParams;
1059
+ private tokenInServerInventory;
1060
+ private engine;
1061
+ private newId;
1062
+ private runResume;
1063
+ private seedHeldStates;
1064
+ private toUiToken;
1065
+ private track;
1066
+ private trackTail;
1067
+ }
1068
+
1069
+ export { ATTENTION_MINT_UNRESOLVED, type ApplyDeltaResult, type CheckpointCacheEntry, type DeliverOptions, type DeliveryJournalEntry, type DeliveryPort, type DeliveryReceipt, type DeterministicMintCapable, type FacadeClient, type FacadeSession, type HistoryEntry, type HistoryPage, type IncomingDelivery, type IntentBackstopEntry, type IntentPayload, type InventoryAsset, type InventoryItem, type InventoryPage, MAX_RESELECT, type MachinePhase, type MintJournalEntry, type MintResult, type OpOutcome, type OutcomeClass, type PaymentRequestStatus, type PaymentRequestView, PaymentsFacade, type PaymentsFacadeDeps, type PaymentsRequestsApi, type PaymentsV2, type PaymentsV2Events, type PlannedOp, type RecipientInfo, STORE_KEYS, type ScopedKV, type SendRequest, type SettlingLink, type ShortfallEntry, type StoragePort, type StreamCursor, type StreamName, createScopedKV, supportsDeterministicMint };