@orbinum/sdk 2.0.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -35,7 +35,12 @@ type DecryptedMemo = {
35
35
  blinding: bigint;
36
36
  /** Asset ID of the note. */
37
37
  assetId: bigint;
38
- /** Counterparty BabyJubJub Ax coordinate. Zero for shield/unshield notes. */
38
+ /**
39
+ * Who this note came from, or — on a CHANGE note — the recipient book entry
40
+ * for the payment beside it: the recipient's viewing key sealed under the
41
+ * sender's ovk, which is ciphertext and not a curve point. Zero for
42
+ * shield/unshield notes. See `protocol/note/recipientBook`.
43
+ */
39
44
  sourcePk: bigint;
40
45
  /** ZK circuit version the note is spent under, recovered from the memo plaintext. */
41
46
  circuitVersion: number;
@@ -48,7 +53,7 @@ type NoteInput = {
48
53
  assetId?: bigint;
49
54
  /** BabyJubJub Ax coordinate (owner public key x). Default 0n. */
50
55
  ownerPk?: bigint;
51
- /** Random blinding scalar. Defaults to BigInt(Date.now()). */
56
+ /** Random blinding scalar. Defaults to a CSPRNG draw — never the clock. */
52
57
  blinding?: bigint;
53
58
  /** Secret spending key used to derive the nullifier. Default 0n. */
54
59
  spendingKey?: bigint;
@@ -65,17 +70,21 @@ type NoteInput = {
65
70
  * transaction unlinkable even when the same privacy address is reused.
66
71
  */
67
72
  recipientOwnerPk?: bigint;
68
- /** Counterparty BabyJubJub Ax coordinate. Zero for shield/unshield notes. Default 0n. */
73
+ /**
74
+ * Counterparty BabyJubJub Ax, or a sealed recipient book entry on a change
75
+ * note. Zero for shield/unshield notes. Default 0n.
76
+ */
69
77
  sourcePk?: bigint;
70
78
  /** Circuit version to stamp on the note. Defaults to `CURRENT_CIRCUIT_VERSION`. */
71
79
  circuitVersion?: number;
72
80
  /**
73
81
  * 32-byte ephemeral secret for the memo ECDH. Default: random.
74
82
  *
75
- * Two callers supply one, and both do it so the RECIPIENT can predict the
76
- * published ephPk and match it by table lookup instead of one trial ECDH per
77
- * pool hint: self-notes pass `deriveSelfEphSk`, and a payment to a known
78
- * counterparty passes `derivePairwiseEphSk`.
83
+ * Callers supply one so that somebody can predict the published ephPk and
84
+ * match it by table lookup instead of one trial ECDH per pool hint:
85
+ * self-notes pass `deriveSelfEphSk`, and a payment passes
86
+ * `deriveOutgoingEphSk` the latter predictable by the SENDER, which is
87
+ * what lets a restored wallet read back its own payment history.
79
88
  *
80
89
  * Honoured on the stealth path too, where the same ephSk drives both the
81
90
  * memo encryption and the stealth-owner derivation. Passing one is a promise
@@ -83,13 +92,6 @@ type NoteInput = {
83
92
  * two notes in public.
84
93
  */
85
94
  ephSkOverride?: Uint8Array;
86
- /**
87
- * Sender's 32-byte outgoing viewing key (ovk). When present on the stealth
88
- * path, `build()` seals the memo's shared secret into a 56-byte `ovkBlob` so
89
- * the sender can recover this transfer after a cold restore. Absent → no blob
90
- * (the app applies the ⊥ default at submit time, never the SDK).
91
- */
92
- outgoingViewingKey?: Uint8Array;
93
95
  };
94
96
  /**
95
97
  * Circuit version notes are created under today. A note carries its version
@@ -143,39 +145,6 @@ type ZkNote = {
143
145
  memo: number[];
144
146
  /** Counterparty BabyJubJub Ax coordinate. Zero for shield/unshield notes. */
145
147
  sourcePk: bigint;
146
- /**
147
- * 56-byte outgoing-viewing-key blob (per-note in the domain object; becomes a
148
- * per-transaction field at submit, see the OVK plan §4.1). Present only on a
149
- * stealth recipient note built with an `outgoingViewingKey`. It wraps the
150
- * memo's shared secret under the sender's ovk so the sender can recover the
151
- * transfer; the recipient never needs it. Not persisted in the vault.
152
- */
153
- ovkBlob?: number[];
154
- };
155
- /**
156
- * A sender's record of a note they SENT, recovered via the OVK blob. Not a
157
- * spendable note: it deliberately carries no `spendingKey`, no `nullifier`, no
158
- * `spent` flag — the sender does not own the recipient's note, only the memory of
159
- * having sent it. Used to rebuild the outgoing history after a cold restore.
160
- */
161
- type OutgoingNoteRecord = {
162
- /** 0x-prefixed 32-byte LE commitment hex of the recipient output. */
163
- commitmentHex: string;
164
- /** Global Merkle leaf index, when the hint carried one. */
165
- leafIndex?: number;
166
- /** Amount sent, in planck. */
167
- value: bigint;
168
- /** Asset ID of the sent note. */
169
- assetId: bigint;
170
- /** The recipient's one-time stealth owner public key (BJJ Ax). */
171
- recipientStealthPk: bigint;
172
- /** Blinding scalar of the recipient output. With value/assetId/stealthPk it
173
- * recomputes the commitment — needed to rebuild a payment slip for the note. */
174
- blinding: bigint;
175
- /** Counterparty BabyJubJub Ax coordinate stamped in the memo. */
176
- sourcePk: bigint;
177
- /** Circuit version the sent note was created under. */
178
- circuitVersion: number;
179
148
  };
180
149
  /**
181
150
  * What a sender can still say about a note they sent, using only public data.
@@ -204,6 +173,75 @@ type NoteFacts = {
204
173
  encryptedMemo: string;
205
174
  };
206
175
 
176
+ /**
177
+ * What a sender recovers about a note they sent.
178
+ *
179
+ * Deliberately without `spendingKey`, `nullifier` or a spent flag: this is a
180
+ * record of a payment, not a note this wallet can spend.
181
+ */
182
+ type SentNoteFacts = {
183
+ /** 0x-prefixed 32-byte LE commitment hex of the recipient output. */
184
+ commitmentHex: string;
185
+ /** Global Merkle leaf index, when the hint carried a usable one. */
186
+ leafIndex?: number;
187
+ /** Amount sent, in planck. */
188
+ value: bigint;
189
+ assetId: bigint;
190
+ /** The recipient's ONE-TIME stealth owner pk — not their global identity. */
191
+ recipientStealthPk: bigint;
192
+ /** Blinding of the recipient output; with the rest it recomputes the commitment. */
193
+ blinding: bigint;
194
+ /** Counterparty pk stamped in the memo. */
195
+ sourcePk: bigint;
196
+ circuitVersion: number;
197
+ /** Which outgoing index this note used. Lets a caller repair a lost counter. */
198
+ ephIndex: number;
199
+ };
200
+ interface RecoverSentNoteParams {
201
+ /** The output as the indexer serves it: commitment, memo, leaf index. */
202
+ hint: ScanCommitment;
203
+ /** This wallet's OUTGOING viewing key — the root of the outgoing sequence. */
204
+ outgoingViewingKey: Uint8Array;
205
+ /**
206
+ * The outgoing index this hint's ephPk matched.
207
+ *
208
+ * Callers hold it already: they found the hint by looking its ephPk up in a
209
+ * precomputed window, and that lookup names the index.
210
+ */
211
+ ephIndex: number;
212
+ /**
213
+ * Candidate recipients, as packed viewing keys — normally the book rebuilt
214
+ * from change notes. Each is tried until one opens the memo; the memo's own
215
+ * MAC is what decides, so a wrong candidate cannot yield a wrong amount.
216
+ */
217
+ recipientCandidates: readonly Uint8Array[];
218
+ }
219
+ /**
220
+ * The facts of a sent note, from a shared secret the caller already derived.
221
+ *
222
+ * Split out because a caller sweeping candidates derives the secret itself and
223
+ * would otherwise pay for it twice.
224
+ *
225
+ * Never throws — this runs in loops over hints and candidates.
226
+ */
227
+ declare function recoverSentFromSharedSecret(hint: ScanCommitment, outgoingSharedSecret: Uint8Array, ephIndex: number): SentNoteFacts | null;
228
+ /**
229
+ * Recover a note this wallet sent, trying each candidate recipient.
230
+ *
231
+ * Returns the facts plus WHICH candidate opened it — the caller needs that to
232
+ * re-seal a payment slip, which is sealed toward the recipient's viewing key.
233
+ *
234
+ * Null covers every "not this one" case without distinguishing them: a
235
+ * malformed hint, a payment whose recipient is not among the candidates, or a
236
+ * note this wallet did not send all read the same, and no caller can act on the
237
+ * difference.
238
+ *
239
+ * Never throws — this runs in a loop over hints.
240
+ */
241
+ declare function recoverSentNote(params: RecoverSentNoteParams): (SentNoteFacts & {
242
+ recipientIvk: Uint8Array;
243
+ }) | null;
244
+
207
245
  /**
208
246
  * What crosses the worker boundary.
209
247
  *
@@ -229,11 +267,28 @@ declare const SELF_EPH_WINDOW = 1024;
229
267
  * that scales with how many people the wallet knows.
230
268
  */
231
269
  declare const PAIRWISE_EPH_WINDOW = 64;
270
+ /**
271
+ * How many outgoing indexes a scan precomputes — how many payments back the
272
+ * sender's own history reaches in one pass.
273
+ *
274
+ * A wallet that has sent more than this gets its window widened from the stored
275
+ * counter, the same way `selfEphGap` widens the self window.
276
+ */
277
+ declare const OUTGOING_EPH_WINDOW = 64;
232
278
  /** Wallet keys needed for trial-decryption. Structured-clone transferable. */
233
279
  interface ScanKeys {
234
280
  viewingKey: Uint8Array;
235
281
  spendingKey: bigint;
236
282
  ownerPk: bigint;
283
+ /**
284
+ * Outgoing viewing key (ovk), when this wallet can see what it sent.
285
+ *
286
+ * Its own capability: it seeds the outgoing ephemerals and opens the
287
+ * recipient book, so a scan without it still finds every note the wallet
288
+ * OWNS and simply reports no payment history. That is what a watch-only
289
+ * wallet holding only the incoming viewing key gets.
290
+ */
291
+ outgoingViewingKey?: Uint8Array;
237
292
  /**
238
293
  * First leafIndex from which every memo carries a view tag — hints at/after
239
294
  * it go through the 1-byte fast-scan filter (skip the AEAD decrypt on
@@ -264,6 +319,30 @@ interface ScanKeys {
264
319
  pairwiseCounterparties?: Uint8Array[];
265
320
  /** Per-counterparty window size. Default PAIRWISE_EPH_WINDOW. */
266
321
  pairwiseWindowSize?: number;
322
+ /**
323
+ * Recognise the notes this wallet SENT, by precomputing the outgoing ephPk
324
+ * window from the OUTGOING viewing key.
325
+ *
326
+ * Not from the spending key: predicting these points IS the capability "see
327
+ * what I sent", so it hangs off the branch that names it. Requires
328
+ * `outgoingViewingKey` to be set.
329
+ *
330
+ * Enables the sender's own history: amount, recipient and a re-issuable
331
+ * payment slip, none of which the chain exposes. Costs one EC mul per index
332
+ * — worth it on a full scan or a restore, waste on an incremental tick.
333
+ */
334
+ outgoingEph?: boolean;
335
+ /** Window size override (tests/tuning). Default OUTGOING_EPH_WINDOW. */
336
+ outgoingEphWindowSize?: number;
337
+ /**
338
+ * Candidate recipients for opening a sent note, as packed viewing keys.
339
+ *
340
+ * The outgoing ephPk identifies the note without them, but the secret that
341
+ * OPENS it is an ECDH against the recipient's key, so recovery tries each
342
+ * candidate until the memo's MAC accepts one. Normally the recipient book
343
+ * rebuilt from change notes; a wrong candidate cannot yield a wrong amount.
344
+ */
345
+ recipientCandidates?: Uint8Array[];
267
346
  }
268
347
  interface DecryptBatchResult {
269
348
  /** One entry per hint, aligned with the input order (null = not ours). */
@@ -281,7 +360,78 @@ interface DecryptBatchResult {
281
360
  pairwiseMatched: number;
282
361
  /** Highest self-eph index seen (feeds the counter bump), or null. */
283
362
  maxSelfEphIndex: number | null;
363
+ /**
364
+ * Highest OUTGOING index seen, or null.
365
+ *
366
+ * Feeds the same counter repair as `maxSelfEphIndex`: a restored wallet has
367
+ * no stored counter, and starting again at 0 would republish the ephemeral
368
+ * of its own first payment. Every index the scan matched is one this wallet
369
+ * demonstrably published, so the maximum is a floor the counter must clear.
370
+ */
371
+ maxOutgoingEphIndex: number | null;
372
+ /**
373
+ * Notes this wallet SENT, recognised in the same sweep.
374
+ *
375
+ * A hint whose ephPk falls in the outgoing window is one of our own
376
+ * payments. Kept apart from `notes` on purpose: the sender does not own it,
377
+ * and putting it in the vault would show a payment they made as balance
378
+ * they hold.
379
+ */
380
+ sentNotes: SentNoteMatch[];
381
+ /**
382
+ * Recipient viewing keys learned from payments opened in this batch, as
383
+ * lowercase hex.
384
+ *
385
+ * Reported so the scan can feed them back as `recipientCandidates` — a
386
+ * wallet that pays the same person twice opens the second payment from the
387
+ * first one's key, even when its own change note is in another page.
388
+ */
389
+ learnedRecipients: string[];
390
+ /**
391
+ * Payments recognised as ours whose memo no candidate opened.
392
+ *
393
+ * Their book entry is in a change note this batch did not contain — a page
394
+ * boundary between a payment and its change is enough. Reported rather than
395
+ * dropped so the caller can retry them once more of the feed is read;
396
+ * silently losing one costs a history row and a re-issuable slip forever.
397
+ */
398
+ unmatchedSent: UnmatchedSentHint[];
399
+ /**
400
+ * Sealed book entries seen in this batch, as raw `sourcePk` values.
401
+ *
402
+ * Still ciphertext: the key that opens one is the commitment of the PAYMENT
403
+ * it names, which this batch may not contain. Reported so a caller holding
404
+ * a stranded payment can try them against it — the pairing is only knowable
405
+ * once both halves are in the same place.
406
+ *
407
+ * Serialised as decimal strings: `bigint` survives structured clone, but the
408
+ * worker reply is host-written JSON in some hosts, where it would not.
409
+ */
410
+ sealedBookEntries: string[];
284
411
  }
412
+ /**
413
+ * A payment recognised by its outgoing ephPk but not yet opened.
414
+ *
415
+ * Carries only what a retry needs: the hint itself and the index its ephPk
416
+ * matched, so the retry costs no second window lookup.
417
+ */
418
+ interface UnmatchedSentHint {
419
+ hint: ScanCommitment;
420
+ ephIndex: number;
421
+ }
422
+ /**
423
+ * A note this wallet sent, plus what re-issuing its payment slip needs.
424
+ *
425
+ * The memo travels verbatim — a slip forwards it, never reads it — and the
426
+ * counterparty key is who the fresh envelope is sealed toward. Both are already
427
+ * in hand at the point of recovery, so carrying them costs nothing and spares
428
+ * the caller a second lookup.
429
+ */
430
+ type SentNoteMatch = SentNoteFacts & {
431
+ counterpartyIvkHex: string;
432
+ /** The 180-byte encrypted memo, 0x-prefixed, exactly as published. */
433
+ encryptedMemo: string;
434
+ };
285
435
  /** An empty result — the shape both pool strategies return for zero hints. */
286
436
  declare const EMPTY_BATCH_RESULT: DecryptBatchResult;
287
437
 
@@ -294,9 +444,7 @@ declare const EMPTY_BATCH_RESULT: DecryptBatchResult;
294
444
  * Order per hint: self-note window match (hash lookup, no EC) → view-tag
295
445
  * fast path (one ECDH, one byte) → full trial-decrypt.
296
446
  */
297
- declare function decryptHintBatch(hints: Array<ScanCommitment & {
298
- ephPkHex?: string | null;
299
- }>, keys: ScanKeys): DecryptBatchResult;
447
+ declare function decryptHintBatch(hints: ScanCommitment[], keys: ScanKeys): DecryptBatchResult;
300
448
 
301
449
  /** Which precomputed window recognized a hint — the two differ in what they prove. */
302
450
  type MatchSource = 'self' | 'pairwise';
@@ -308,6 +456,15 @@ interface KnownEphEntry {
308
456
  interface KnownEphWindow {
309
457
  /** ephPkHex (lowercase) → entry. */
310
458
  byEphPk: Map<string, KnownEphEntry>;
459
+ /**
460
+ * ephPkHex (lowercase) → outgoing index, for the notes this wallet SENT.
461
+ *
462
+ * Separate from `byEphPk` because it holds no shared secret: an outgoing
463
+ * memo is sealed toward the RECIPIENT, whose key is not known until a
464
+ * candidate opens it. The index is all the precompute can supply, and it is
465
+ * what recovery needs to derive the secret per candidate.
466
+ */
467
+ outgoingByEphPk: Map<string, number>;
311
468
  }
312
469
  /**
313
470
  * Drops the precomputed discovery window and the shared secrets in it.
@@ -362,7 +519,7 @@ declare const DECRYPT_YIELD_EVERY = 25;
362
519
  * import it never asked for. `data` is the only member the pool reads, and a
363
520
  * real `MessageEvent` satisfies this shape.
364
521
  *
365
- * Same reasoning as `CryptoKey` in `utils/crypto/webcrypto-types.d.ts`.
522
+ * Same reasoning as `CryptoKey` in `foundation/crypto/webcrypto-types.d.ts`.
366
523
  */
367
524
  interface WorkerMessage {
368
525
  data: unknown;
@@ -423,4 +580,4 @@ declare function createMainThreadPool(): DecryptPool;
423
580
 
424
581
  declare function createWorkerPool(factory: WorkerFactory, size: number): DecryptPool;
425
582
 
426
- export { CURRENT_CIRCUIT_VERSION as C, type DecryptedMemo as D, EMPTY_BATCH_RESULT as E, type KnownEphEntry as K, MAX_WORKERS as M, type NoteInput as N, type OutgoingNoteRecord as O, PAIRWISE_EPH_WINDOW as P, type ScanCommitment as S, WORKER_CRASHED as W, type ZkNote as Z, type NoteFacts as a, type DecryptPool as b, type ScanKeys as c, DECRYPT_YIELD_EVERY as d, type DecryptBatchResult as e, type DecryptRequest as f, type KnownEphWindow as g, type MatchSource as h, type MerkleTreeInfo as i, SELF_EPH_WINDOW as j, type WorkerFactory as k, type WorkerLike as l, type WorkerMessage as m, clearKnownEphWindow as n, createDecryptPool as o, createMainThreadPool as p, createWorkerPool as q, decryptHintBatch as r, getKnownEphWindow as s };
583
+ export { CURRENT_CIRCUIT_VERSION as C, type DecryptedMemo as D, EMPTY_BATCH_RESULT as E, type KnownEphEntry as K, MAX_WORKERS as M, type NoteInput as N, OUTGOING_EPH_WINDOW as O, PAIRWISE_EPH_WINDOW as P, type ScanCommitment as S, type UnmatchedSentHint as U, WORKER_CRASHED as W, type ZkNote as Z, type NoteFacts as a, type SentNoteMatch as b, type DecryptPool as c, type ScanKeys as d, DECRYPT_YIELD_EVERY as e, type DecryptBatchResult as f, type DecryptRequest as g, type KnownEphWindow as h, type MerkleTreeInfo as i, SELF_EPH_WINDOW as j, type SentNoteFacts as k, type WorkerFactory as l, type WorkerLike as m, type WorkerMessage as n, clearKnownEphWindow as o, createDecryptPool as p, createMainThreadPool as q, createWorkerPool as r, decryptHintBatch as s, getKnownEphWindow as t, recoverSentFromSharedSecret as u, recoverSentNote as v };