@orbinum/sdk 0.25.0 → 1.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.
@@ -0,0 +1,353 @@
1
+ /**
2
+ * What a note IS.
3
+ *
4
+ * The protocol's own vocabulary: a note, the inputs that build one, and the
5
+ * plaintext recovered from its memo. Nothing here describes an extrinsic — the
6
+ * argument shapes the pallet accepts live in `pallet/extrinsicParams.ts`,
7
+ * because a wallet can build, decrypt and select notes without ever submitting
8
+ * anything.
9
+ */
10
+ /** On-chain Merkle tree state for the shielded pool. */
11
+ type MerkleTreeInfo = {
12
+ /** 0x-prefixed current Merkle root hex. */
13
+ root: string;
14
+ /** Number of leaves (commitments) inserted so far. */
15
+ treeSize: number;
16
+ /** Tree depth (levels from leaf to root). */
17
+ depth: number;
18
+ };
19
+ /** A commitment surfaced by the indexer scan feed, for trial-decryption. */
20
+ type ScanCommitment = {
21
+ /** 0x-prefixed 32-byte commitment hex. */
22
+ commitmentHex: string;
23
+ /** Leaf position of the commitment in the Merkle tree. */
24
+ leafIndex: number;
25
+ /** 0x-prefixed encrypted memo hex, or null if none was published. */
26
+ encryptedMemo: string | null;
27
+ };
28
+ /** Plaintext fields recovered from a note's encrypted memo. */
29
+ type DecryptedMemo = {
30
+ /** Note amount in planck. */
31
+ value: bigint;
32
+ /** Owner's BabyJubJub Ax coordinate. */
33
+ ownerPk: bigint;
34
+ /** Blinding scalar used in the commitment. */
35
+ blinding: bigint;
36
+ /** Asset ID of the note. */
37
+ assetId: bigint;
38
+ /** Counterparty BabyJubJub Ax coordinate. Zero for shield/unshield notes. */
39
+ counterpartyPk: bigint;
40
+ /** ZK circuit version the note is spent under, recovered from the memo plaintext. */
41
+ circuitVersion: number;
42
+ };
43
+ /** Input params for NoteBuilder.build(). All fields except value have defaults. */
44
+ type NoteInput = {
45
+ /** Amount in planck (required). */
46
+ value: bigint;
47
+ /** Asset ID — default 0 (native ORB-Privacy). */
48
+ assetId?: bigint;
49
+ /** BabyJubJub Ax coordinate (owner public key x). Default 0n. */
50
+ ownerPk?: bigint;
51
+ /** Random blinding scalar. Defaults to BigInt(Date.now()). */
52
+ blinding?: bigint;
53
+ /** Secret spending key used to derive the nullifier. Default 0n. */
54
+ spendingKey?: bigint;
55
+ /**
56
+ * 32-byte LE-encoded packed BJJ viewing public key of the recipient (from their privacy address).
57
+ * When provided, NoteBuilder.build() will auto-generate the 180-byte ECDH-encrypted memo.
58
+ * Omit to skip memo generation (use buildMemo() separately if needed).
59
+ */
60
+ viewingPublicKey?: Uint8Array;
61
+ /**
62
+ * BabyJubJub Ax coordinate of the recipient (from their privacy address).
63
+ * Required together with viewingPublicKey to enable stealth address derivation:
64
+ * the commitment will use stealthOwnerPk instead of ownerPk, making each
65
+ * transaction unlinkable even when the same privacy address is reused.
66
+ */
67
+ recipientOwnerPk?: bigint;
68
+ /** Counterparty BabyJubJub Ax coordinate. Zero for shield/unshield notes. Default 0n. */
69
+ counterpartyPk?: bigint;
70
+ /** Circuit version to stamp on the note. Defaults to `CURRENT_CIRCUIT_VERSION`. */
71
+ circuitVersion?: number;
72
+ /**
73
+ * 32-byte ephemeral secret for the memo ECDH. Self-notes pass a
74
+ * deterministic one (deriveSelfEphSk) so a cold restore recognizes them by
75
+ * ephPk equality with no trial ECDH. Ignored on the stealth path (it
76
+ * generates its own coordinated ephSk). Default: random.
77
+ */
78
+ ephSkOverride?: Uint8Array;
79
+ };
80
+ /**
81
+ * Circuit version notes are created under today. A note carries its version
82
+ * (`ZkNote.circuitVersion`) so that, after a VK rotation, it is always proven
83
+ * and verified against the circuit that created it. Only one version exists
84
+ * today; callers may pass the chain's active version explicitly.
85
+ */
86
+ declare const CURRENT_CIRCUIT_VERSION = 1;
87
+ /**
88
+ * Computed ZK note (commitment + nullifier). Built entirely off-chain.
89
+ *
90
+ * commitment = Poseidon(value, assetId, ownerPk, blinding)
91
+ * nullifier = Poseidon(commitment, spendingKey)
92
+ */
93
+ type ZkNote = {
94
+ /** Note amount in planck. */
95
+ value: bigint;
96
+ /** Asset ID of the note. */
97
+ assetId: bigint;
98
+ /** Owner's BabyJubJub Ax coordinate (or stealth owner Pk for stealth notes). */
99
+ ownerPk: bigint;
100
+ /** Blinding scalar mixed into the commitment. */
101
+ blinding: bigint;
102
+ /** Secret spending key used to derive the nullifier. */
103
+ spendingKey: bigint;
104
+ /** Circuit version this note was created under (see `CURRENT_CIRCUIT_VERSION`). Required. */
105
+ circuitVersion: number;
106
+ /**
107
+ * Global Merkle leaf index, when known. Optional so pre-forest vaults
108
+ * need no migration: notes without it predate the first tree seal and
109
+ * belong to tree 0. Populated on shield and on scan; used only to derive
110
+ * the forest tree for same-tree coin selection (`treeIdOf`).
111
+ */
112
+ leafIndex?: number;
113
+ /** Whether the note has been spent/nullified on-chain. */
114
+ spent: boolean;
115
+ /** Local timestamp when this note was marked spent, or null if still active/unknown. */
116
+ spentAt: number | null;
117
+ /** Poseidon commitment scalar. */
118
+ commitment: bigint;
119
+ /** Poseidon nullifier scalar. */
120
+ nullifier: bigint;
121
+ /** 0x-prefixed 32-byte little-endian hex commitment. */
122
+ commitmentHex: string;
123
+ /** 0x-prefixed 32-byte little-endian hex nullifier. */
124
+ nullifierHex: string;
125
+ /**
126
+ * 180-byte encrypted memo (ChaCha20-Poly1305 ECDH) as number[] for SCALE encoding.
127
+ * Always populated: uses a dummy memo when no viewingPublicKey is provided.
128
+ */
129
+ memo: number[];
130
+ /** Counterparty BabyJubJub Ax coordinate. Zero for shield/unshield notes. */
131
+ counterpartyPk: bigint;
132
+ };
133
+
134
+ /**
135
+ * What crosses the worker boundary.
136
+ *
137
+ * Every value here must survive structured clone: it travels through
138
+ * `postMessage` to a worker and back. That rules out classes, functions and
139
+ * bigint-keyed maps — `bigint` itself is fine, `Uint8Array` is fine, anything
140
+ * with a prototype is not.
141
+ */
142
+
143
+ /**
144
+ * Self-note discovery window: how many deterministic ephemeral indexes to
145
+ * precompute. BIP-44-style gap limit — a wallet with more self-created notes
146
+ * than this still finds the excess through the trial-decrypt path (correct,
147
+ * just slower) until the counter syncs. Fixed size, no extension loop; add
148
+ * one when a real wallet approaches this many self notes.
149
+ */
150
+ declare const SELF_EPH_WINDOW = 1024;
151
+ /**
152
+ * Per-counterparty discovery window. Much smaller than the self window: this
153
+ * counts payments from ONE sender, and a sender who outruns it just falls back
154
+ * to the trial-decrypt path — correct, only slower — until the counter syncs.
155
+ * The window costs two EC muls per index per sender, so it is the one number
156
+ * that scales with how many people the wallet knows.
157
+ */
158
+ declare const PAIRWISE_EPH_WINDOW = 64;
159
+ /** Wallet keys needed for trial-decryption. Structured-clone transferable. */
160
+ interface ScanKeys {
161
+ viewingKey: Uint8Array;
162
+ spendingKey: bigint;
163
+ ownerPk: bigint;
164
+ /**
165
+ * First leafIndex from which every memo carries a view tag — hints at/after
166
+ * it go through the 1-byte fast-scan filter (skip the AEAD decrypt on
167
+ * mismatch). null/undefined = filter off (network not activated yet).
168
+ */
169
+ viewTagActivationLeaf?: number | null;
170
+ /**
171
+ * Enable self-note discovery: precompute the deterministic ephPk window and
172
+ * recognize the wallet's own notes (shields, change) by hash lookup — zero
173
+ * trial ECDH for them. Only worth its one-time window cost (two EC muls
174
+ * per index) on full scans/restores; leave off for incremental ticks.
175
+ */
176
+ selfEph?: boolean;
177
+ /** Window size override (tests/tuning). Default SELF_EPH_WINDOW. */
178
+ selfEphWindowSize?: number;
179
+ /**
180
+ * Counterparties whose future payments can be recognized without an ECDH.
181
+ *
182
+ * A sender's key travels in the plaintext of the first payment they make us,
183
+ * so once that one is decrypted the normal way, every later note from them
184
+ * carries an ephemeral both sides derive — found by the same hash lookup as a
185
+ * self note. A stranger's first payment is unaffected.
186
+ *
187
+ * Each entry is one packed viewing public key. Matching happens entirely on
188
+ * this device: asking a server for a specific ephPk would tell it which notes
189
+ * are ours, which is exactly what the download-everything feed prevents.
190
+ */
191
+ pairwiseCounterparties?: Uint8Array[];
192
+ /** Per-counterparty window size. Default PAIRWISE_EPH_WINDOW. */
193
+ pairwiseWindowSize?: number;
194
+ }
195
+ interface DecryptBatchResult {
196
+ /** One entry per hint, aligned with the input order (null = not ours). */
197
+ notes: Array<ZkNote | null>;
198
+ /** Hints discarded by the view-tag check alone — no AEAD work attempted. */
199
+ tagFiltered: number;
200
+ /** Hints recognized as own self-notes via the deterministic ephPk window. */
201
+ selfMatched: number;
202
+ /**
203
+ * Hints recognized as coming from a known counterparty, via that sender's
204
+ * precomputed window. Counted apart from `selfMatched` because it answers a
205
+ * different question: whether the pairwise mechanism actually fired, or
206
+ * whether those notes quietly fell through to the expensive path.
207
+ */
208
+ pairwiseMatched: number;
209
+ /** Highest self-eph index seen (feeds the counter bump), or null. */
210
+ maxSelfEphIndex: number | null;
211
+ }
212
+ /** An empty result — the shape both pool strategies return for zero hints. */
213
+ declare const EMPTY_BATCH_RESULT: DecryptBatchResult;
214
+
215
+ /**
216
+ * Trial-decrypts every hint against the wallet's keys. Returns one entry per
217
+ * hint, aligned with the input order: the recovered note, or null when the
218
+ * hint isn't ours (MAC failure, view-tag mismatch) or is malformed — a single
219
+ * bad hint must not kill the batch, the scan just counts it as failed.
220
+ *
221
+ * Order per hint: self-note window match (hash lookup, no EC) → view-tag
222
+ * fast path (one ECDH, one byte) → full trial-decrypt.
223
+ */
224
+ declare function decryptHintBatch(hints: Array<ScanCommitment & {
225
+ ephPkHex?: string | null;
226
+ }>, keys: ScanKeys): DecryptBatchResult;
227
+
228
+ /** Which precomputed window recognized a hint — the two differ in what they prove. */
229
+ type MatchSource = 'self' | 'pairwise';
230
+ interface KnownEphEntry {
231
+ sharedSecret: Uint8Array;
232
+ index: number;
233
+ source: MatchSource;
234
+ }
235
+ interface KnownEphWindow {
236
+ /** ephPkHex (lowercase) → entry. */
237
+ byEphPk: Map<string, KnownEphEntry>;
238
+ }
239
+ /**
240
+ * Drops the precomputed discovery window and the shared secrets in it.
241
+ *
242
+ * Call when the wallet locks. The cache is keyed by spending key, so it can
243
+ * never serve a different identity — but "cannot be misused" is not "is gone":
244
+ * after `lock()` the session keys are dropped while ~100 KB of ECDH secrets
245
+ * derived from the viewing key stay reachable in module memory, and on a
246
+ * main-thread pool that is the page's own heap.
247
+ *
248
+ * Terminating a worker achieves the same thing by discarding the whole realm.
249
+ * This exists for the main-thread pool, which has no realm to discard, and for
250
+ * hosts that keep their workers warm across a lock.
251
+ */
252
+ declare function clearKnownEphWindow(): void;
253
+ /**
254
+ * The window for these keys, built on first use and cached after.
255
+ *
256
+ * Returns null when discovery is off entirely (no self-eph, no counterparties)
257
+ * or when the primitives are unavailable — the scan then takes the trial-decrypt
258
+ * path for everything, which is slower but correct.
259
+ */
260
+ declare function getKnownEphWindow(keys: ScanKeys): KnownEphWindow | null;
261
+
262
+ /**
263
+ * The pool contract, and the minimal Worker surface it drives.
264
+ *
265
+ * `WorkerLike` is deliberately narrower than the DOM `Worker`: the pool only
266
+ * ever posts a message, reads one back, and terminates. Keeping it to those
267
+ * four members is what lets a test pass a fake that runs the kernel inline, and
268
+ * what keeps this file free of `lib.dom`.
269
+ */
270
+
271
+ /** Cap on pool size — EC math parallelizes linearly but tabs share the CPU. */
272
+ /**
273
+ * Ceiling on workers per batch. Past four the ECDH loop is bound by memory
274
+ * bandwidth rather than cores, and every extra worker still costs its own copy
275
+ * of the precomputed discovery window.
276
+ */
277
+ declare const MAX_WORKERS = 4;
278
+ /**
279
+ * Main-thread strategy only: trial-decrypts to run before yielding so the
280
+ * browser can paint between bursts of synchronous EC math.
281
+ */
282
+ declare const DECRYPT_YIELD_EVERY = 25;
283
+ /**
284
+ * The message a worker hands back, structurally.
285
+ *
286
+ * Not `MessageEvent`: that name lives in `lib.dom`, and naming it here would put
287
+ * it in the published `.d.ts` of the ROOT entry — so a React Native consumer
288
+ * compiling with `lib: esnext` and no `@types/node` would fail to typecheck an
289
+ * import it never asked for. `data` is the only member the pool reads, and a
290
+ * real `MessageEvent` satisfies this shape.
291
+ *
292
+ * Same reasoning as `CryptoKey` in `utils/crypto/webcrypto-types.d.ts`.
293
+ */
294
+ interface WorkerMessage {
295
+ data: unknown;
296
+ }
297
+ /**
298
+ * The minimal Worker surface the pool needs — a real Worker or a test fake.
299
+ *
300
+ * The handler parameters are typed `any` rather than `unknown`, which is the
301
+ * one place this file trades strictness for reach. A DOM `Worker` declares
302
+ * `onerror` as `(event: ErrorEvent) => void`, and function parameters are
303
+ * contravariant: a handler accepting `unknown` is NOT assignable where one
304
+ * accepting `ErrorEvent` is expected. Typing these as `unknown` therefore makes
305
+ * a real browser `Worker` fail to satisfy the interface — the exact consumer
306
+ * this contract exists to serve.
307
+ *
308
+ * `any` keeps both a real Worker and an inline fake assignable while still
309
+ * keeping `MessageEvent`/`ErrorEvent` — both `lib.dom` — out of the published
310
+ * types. The pool reads only `event.data` and ignores the error payload
311
+ * entirely, so nothing downstream depends on the precision given up here.
312
+ */
313
+ interface WorkerLike {
314
+ postMessage(message: unknown): void;
315
+ onmessage: ((event: any) => void) | null;
316
+ /** Error payload is unused: the pool treats any error as "worker died". */
317
+ onerror: ((event: any) => void) | null;
318
+ terminate(): void;
319
+ }
320
+ type WorkerFactory = () => WorkerLike;
321
+ interface DecryptPool {
322
+ /** Decrypts `hints` in input order. Rejects with AbortError when cancelled. */
323
+ decryptBatch(hints: ScanCommitment[], keys: ScanKeys, signal?: AbortSignal): Promise<DecryptBatchResult>;
324
+ /** Tears down every worker. The pool is per-scan; always call when done. */
325
+ terminate(): void;
326
+ }
327
+ /** The message a worker receives. Structured-clone friendly by construction. */
328
+ interface DecryptRequest {
329
+ hints: ScanCommitment[];
330
+ keys: ScanKeys;
331
+ }
332
+ /** Thrown when a worker dies mid-batch; the factory catches it to fall back. */
333
+ declare const WORKER_CRASHED = "Decrypt worker crashed";
334
+
335
+ /**
336
+ * Builds the decrypt pool for one scan run.
337
+ *
338
+ * `factory` is required rather than defaulted: constructing a worker means
339
+ * naming a module URL, which only the host's bundler can resolve. Pass `null`
340
+ * to decrypt on the calling thread — correct everywhere, and the only option
341
+ * outside a browser.
342
+ */
343
+ declare function createDecryptPool(options: {
344
+ factory: WorkerFactory | null;
345
+ /** Workers to spread a batch across. Defaults to `MAX_WORKERS`. */
346
+ size?: number;
347
+ }): DecryptPool;
348
+
349
+ declare function createMainThreadPool(): DecryptPool;
350
+
351
+ declare function createWorkerPool(factory: WorkerFactory, size: number): DecryptPool;
352
+
353
+ 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, PAIRWISE_EPH_WINDOW as P, type ScanCommitment as S, WORKER_CRASHED as W, type ZkNote as Z, type DecryptPool as a, type ScanKeys as b, DECRYPT_YIELD_EVERY as c, type DecryptBatchResult as d, type DecryptRequest as e, type KnownEphWindow as f, type MatchSource as g, type MerkleTreeInfo as h, SELF_EPH_WINDOW as i, type WorkerFactory as j, type WorkerLike as k, type WorkerMessage as l, clearKnownEphWindow as m, createDecryptPool as n, createMainThreadPool as o, createWorkerPool as p, decryptHintBatch as q, getKnownEphWindow as r };