@orbinum/sdk 1.1.1 → 1.3.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.
package/dist/index.mjs CHANGED
@@ -15,6 +15,7 @@ import {
15
15
  LEAVES_PER_TREE,
16
16
  MAX_WORKERS,
17
17
  NoteBuilder,
18
+ OVK_BLOB_SIZE,
18
19
  PAIRWISE_EPH_WINDOW,
19
20
  SELF_EPH_WINDOW,
20
21
  WORKER_CRASHED,
@@ -38,6 +39,8 @@ import {
38
39
  createWorkerPool,
39
40
  decodeNoteDisclosureKey,
40
41
  decryptHintBatch,
42
+ deriveOutgoingCipherKey,
43
+ deriveOutgoingViewingKey,
41
44
  deriveOwnerPk,
42
45
  derivePairwiseEphSk,
43
46
  derivePairwiseSharedSecret,
@@ -59,18 +62,22 @@ import {
59
62
  isSpendable,
60
63
  isValidLeafIndex,
61
64
  leHexToBigint,
65
+ openOutgoingBlob,
62
66
  pairwiseEphWindow,
67
+ randomOutgoingBlob,
63
68
  recoverOwnerPkPoint,
64
69
  scalarToHex,
65
70
  scanAbortError,
71
+ sealOutgoingBlob,
66
72
  selectNotes,
67
73
  selfEphWindow,
68
74
  serializeMemo,
69
75
  toHex,
70
76
  treeIdOf,
71
77
  tryDecryptNote,
72
- tryDecryptNoteVerbose
73
- } from "./chunk-Y6LNYJAJ.mjs";
78
+ tryDecryptNoteVerbose,
79
+ tryRecoverOutgoing
80
+ } from "./chunk-A2ZRMEYW.mjs";
74
81
 
75
82
  // src/foundation/encoding/base64.ts
76
83
  var ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
@@ -365,6 +372,101 @@ function addressToFieldElement(address) {
365
372
  return bytesToBigintLE(bytes) % BN254_R;
366
373
  }
367
374
 
375
+ // src/protocol/memo/PaymentSlip.ts
376
+ import { chacha20poly1305 } from "@noble/ciphers/chacha.js";
377
+ import { randomBytes } from "@noble/ciphers/utils.js";
378
+ import { hkdf } from "@noble/hashes/hkdf.js";
379
+ import { sha256 } from "@noble/hashes/sha2.js";
380
+ import { packPoint, unpackPoint } from "@zk-kit/baby-jubjub";
381
+ var SLIP_DOMAIN = new TextEncoder().encode("orbinum-payment-slip-v1");
382
+ var NONCE_PREFIX = new TextEncoder().encode("SLP1");
383
+ var EPH_PK_SIZE = 32;
384
+ var NONCE_SUFFIX_SIZE = 8;
385
+ function deriveSlipKey(sharedSecret) {
386
+ return hkdf(sha256, sharedSecret, void 0, SLIP_DOMAIN, 32);
387
+ }
388
+ function buildNonce(suffix) {
389
+ const nonce = new Uint8Array(12);
390
+ nonce.set(NONCE_PREFIX, 0);
391
+ nonce.set(suffix, 4);
392
+ return nonce;
393
+ }
394
+ function sealPaymentSlip(recipientIvkPacked, fields) {
395
+ if (recipientIvkPacked.length !== 32) {
396
+ throw new Error(
397
+ `PaymentSlip: recipientIvk must be 32 bytes, got ${recipientIvkPacked.length}`
398
+ );
399
+ }
400
+ const ivkPoint = unpackPoint(bytesToBigintLE(recipientIvkPacked));
401
+ if (!ivkPoint) throw new Error("PaymentSlip: invalid recipient viewing public key");
402
+ const ephSkScalar = bytesToBjjScalar(randomBytes(32));
403
+ const ephPkPacked = bigintTo32Le(packPoint(fastMulBase(ephSkScalar)));
404
+ const sharedPoint = fastMulPoint(ivkPoint, ephSkScalar);
405
+ const sharedSecret = bigintTo32Le(sharedPoint[0]);
406
+ const slipKey = deriveSlipKey(sharedSecret);
407
+ const suffix = randomBytes(NONCE_SUFFIX_SIZE);
408
+ const cipher = chacha20poly1305(slipKey, buildNonce(suffix));
409
+ const plaintext = new TextEncoder().encode(JSON.stringify(fields));
410
+ const sealed = cipher.encrypt(plaintext);
411
+ const envelope = new Uint8Array(EPH_PK_SIZE + NONCE_SUFFIX_SIZE + sealed.length);
412
+ envelope.set(ephPkPacked, 0);
413
+ envelope.set(suffix, EPH_PK_SIZE);
414
+ envelope.set(sealed, EPH_PK_SIZE + NONCE_SUFFIX_SIZE);
415
+ return envelope;
416
+ }
417
+ function openPaymentSlip(recipientIvsk, envelope) {
418
+ if (envelope.length < EPH_PK_SIZE + NONCE_SUFFIX_SIZE + 16) return null;
419
+ try {
420
+ const ephPkPacked = envelope.subarray(0, EPH_PK_SIZE);
421
+ const ephPkPoint = unpackPoint(bytesToBigintLE(ephPkPacked));
422
+ if (!ephPkPoint) return null;
423
+ const ivskScalar = bytesToBjjScalar(recipientIvsk);
424
+ const sharedSecret = bigintTo32Le(fastMulPoint(ephPkPoint, ivskScalar)[0]);
425
+ const slipKey = deriveSlipKey(sharedSecret);
426
+ const suffix = envelope.subarray(EPH_PK_SIZE, EPH_PK_SIZE + NONCE_SUFFIX_SIZE);
427
+ const sealed = envelope.subarray(EPH_PK_SIZE + NONCE_SUFFIX_SIZE);
428
+ const cipher = chacha20poly1305(slipKey, buildNonce(suffix));
429
+ const plaintext = cipher.decrypt(sealed);
430
+ const fields = JSON.parse(new TextDecoder().decode(plaintext));
431
+ if (typeof fields.commitmentHex !== "string" || typeof fields.encryptedMemo !== "string") {
432
+ return null;
433
+ }
434
+ return fields;
435
+ } catch {
436
+ return null;
437
+ }
438
+ }
439
+ var PAYMENT_SLIP_SCHEME = "orbslip1:";
440
+ function slipChecksum(payload) {
441
+ const digest = sha256(new TextEncoder().encode(PAYMENT_SLIP_SCHEME + payload));
442
+ return toHex(digest.slice(0, 4)).slice(2);
443
+ }
444
+ function encodePaymentSlip(envelope) {
445
+ const payload = base64UrlEncode(toHex(envelope));
446
+ return `${PAYMENT_SLIP_SCHEME}${payload}:${slipChecksum(payload)}`;
447
+ }
448
+ function decodePaymentSlip(text) {
449
+ if (!text.startsWith(PAYMENT_SLIP_SCHEME)) return null;
450
+ const rest = text.slice(PAYMENT_SLIP_SCHEME.length);
451
+ const sep = rest.lastIndexOf(":");
452
+ if (sep < 0) return null;
453
+ const payload = rest.slice(0, sep);
454
+ const checksum = rest.slice(sep + 1);
455
+ if (slipChecksum(payload) !== checksum) return null;
456
+ try {
457
+ const hex = base64UrlDecode(payload);
458
+ const clean = hex.startsWith("0x") ? hex.slice(2) : hex;
459
+ if (clean.length % 2 !== 0) return null;
460
+ const bytes = new Uint8Array(clean.length / 2);
461
+ for (let i = 0; i < bytes.length; i++) {
462
+ bytes[i] = parseInt(clean.slice(i * 2, i * 2 + 2), 16);
463
+ }
464
+ return bytes;
465
+ } catch {
466
+ return null;
467
+ }
468
+ }
469
+
368
470
  // src/chain/evm/precompiles/addresses.ts
369
471
  var PRECOMPILE_ADDR = {
370
472
  // ── Ethereum standard (EIP) ─────────────────────────────────────────────
@@ -463,10 +565,10 @@ ${canonicalAccountId(address)}`;
463
565
  }
464
566
 
465
567
  // src/protocol/keys/PrivacyKeyManager.ts
466
- import { sha256 } from "@noble/hashes/sha2.js";
568
+ import { sha256 as sha2562 } from "@noble/hashes/sha2.js";
467
569
  function privacyAddressChecksum(ownerPkHex, ivkHex) {
468
570
  const body = `orbpriv2:${ownerPkHex}:${ivkHex}`;
469
- const digest = sha256(new TextEncoder().encode(body));
571
+ const digest = sha2562(new TextEncoder().encode(body));
470
572
  return toHex(digest.slice(0, 4)).slice(2);
471
573
  }
472
574
  var PrivacyKeyManager = class {
@@ -475,7 +577,8 @@ var PrivacyKeyManager = class {
475
577
  masterBytes: null,
476
578
  viewingSecretKey: null,
477
579
  viewingPublicKeyPacked: null,
478
- ownerPk: null
580
+ ownerPk: null,
581
+ outgoingViewingKey: null
479
582
  };
480
583
  /**
481
584
  * Load a spending key and its corresponding master bytes into the in-memory session.
@@ -490,12 +593,14 @@ var PrivacyKeyManager = class {
490
593
  const viewingSecretKey = deriveViewingSecretKey(spendingKey);
491
594
  const viewingPublicKeyPacked = deriveViewingPublicKey(viewingSecretKey);
492
595
  const ownerPk = deriveOwnerPk(spendingKey);
596
+ const outgoingViewingKey = deriveOutgoingViewingKey(masterBytes);
493
597
  this._state = {
494
598
  spendingKey,
495
599
  masterBytes,
496
600
  viewingSecretKey,
497
601
  viewingPublicKeyPacked,
498
- ownerPk
602
+ ownerPk,
603
+ outgoingViewingKey
499
604
  };
500
605
  }
501
606
  /** Clear all key material from memory. Call on vault lock / sign-out. */
@@ -505,7 +610,8 @@ var PrivacyKeyManager = class {
505
610
  masterBytes: null,
506
611
  viewingSecretKey: null,
507
612
  viewingPublicKeyPacked: null,
508
- ownerPk: null
613
+ ownerPk: null,
614
+ outgoingViewingKey: null
509
615
  };
510
616
  }
511
617
  /** Returns true if a spending key has been loaded. */
@@ -550,6 +656,16 @@ var PrivacyKeyManager = class {
550
656
  }
551
657
  return this._state.ownerPk;
552
658
  }
659
+ /**
660
+ * Returns the 32-byte outgoing viewing key (ovk). Throws if not loaded.
661
+ * Used to seal/open the outgoing blob that lets the sender recover a transfer.
662
+ */
663
+ getOutgoingViewingKey() {
664
+ if (this._state.outgoingViewingKey === null) {
665
+ throw new Error("PrivacyKeyManager: no key loaded. Call load() first.");
666
+ }
667
+ return this._state.outgoingViewingKey;
668
+ }
553
669
  /** Returns the spending key as a 32-byte little-endian Uint8Array. Throws if not loaded. */
554
670
  getSpendingKeyBytes() {
555
671
  return bigintTo32Le(this.getSpendingKey());
@@ -651,8 +767,8 @@ var PrivacyKeyManager = class {
651
767
  };
652
768
 
653
769
  // src/protocol/keys/spendingKeyDerivation.ts
654
- import { hkdf } from "@noble/hashes/hkdf.js";
655
- import { sha256 as sha2562 } from "@noble/hashes/sha2.js";
770
+ import { hkdf as hkdf2 } from "@noble/hashes/hkdf.js";
771
+ import { sha256 as sha2563 } from "@noble/hashes/sha2.js";
656
772
  var MIN_SIGNATURE_BYTES = 32;
657
773
  var MIN_DISTINCT_BYTES = 8;
658
774
  function assertUsableSignature(sigBytes) {
@@ -674,7 +790,7 @@ async function deriveMasterKeyBytes(signatureHex, chainId, address) {
674
790
  const info = new TextEncoder().encode(
675
791
  `orbinum-sk-${KEY_VERSION}:${chainId}:${canonicalAccountId(address)}`
676
792
  );
677
- return hkdf(sha2562, sigBytes, new Uint8Array(0), info, 32);
793
+ return hkdf2(sha2563, sigBytes, new Uint8Array(0), info, 32);
678
794
  }
679
795
  async function deriveSpendingKeyFromSignature(signatureHex, chainId, address) {
680
796
  return deriveSpendingKeyFromMaster(await deriveMasterKeyBytes(signatureHex, chainId, address));
@@ -686,7 +802,7 @@ import {
686
802
  generateProof,
687
803
  WebArtifactProvider
688
804
  } from "@orbinum/proof-generator";
689
- import { randomBytes } from "@noble/ciphers/utils.js";
805
+ import { randomBytes as randomBytes2 } from "@noble/ciphers/utils.js";
690
806
  import { mulPointEscalar, Base8 } from "@zk-kit/baby-jubjub";
691
807
  import { poseidon4 } from "poseidon-lite";
692
808
 
@@ -720,7 +836,7 @@ async function generateUnshieldProof(inputs, options = {}) {
720
836
  throw new Error("changeValue must be >= 0.");
721
837
  }
722
838
  const changeOwnerPubkey = inputs.changeOwnerPubkey ?? mulPointEscalar(Base8, inputs.spendingKey)[0];
723
- const changeBlinding = inputs.changeBlinding ?? (changeValue > 0n ? bytesToBigintLE(randomBytes(32)) : 0n);
839
+ const changeBlinding = inputs.changeBlinding ?? (changeValue > 0n ? bytesToBigintLE(randomBytes2(32)) : 0n);
724
840
  const changeCommitment = changeValue > 0n ? poseidon4([changeValue, inputs.assetId, changeOwnerPubkey, changeBlinding]) : 0n;
725
841
  const circuitInputs = {
726
842
  merkle_root: leHexToBigint(inputs.merkleRoot).toString(),
@@ -4582,6 +4698,7 @@ async function processHints(ctx, hints, total) {
4582
4698
  pageEntries.push({ note, isNew });
4583
4699
  }
4584
4700
  if (pageEntries.length > 0) await ctx.onPage?.(pageEntries);
4701
+ await ctx.onBatchDone?.(outcome.maxLeafIndex);
4585
4702
  ctx.onProgress?.({ scanned: outcome.scanned, total, found: outcome.found });
4586
4703
  }
4587
4704
  async function processSealedChunks(ctx, source, sinceLeafIndex) {
@@ -4616,6 +4733,7 @@ async function processSealedChunks(ctx, source, sinceLeafIndex) {
4616
4733
  }
4617
4734
  async function collectScanEntries(params) {
4618
4735
  const { source, pool, keys, existingHexes, sinceLeafIndex, onProgress, signal, onPage } = params;
4736
+ const { onBatchDone } = params;
4619
4737
  if (signal?.aborted) throw scanAbortError();
4620
4738
  const outcome = {
4621
4739
  scanEntries: [],
@@ -4639,7 +4757,8 @@ async function collectScanEntries(params) {
4639
4757
  vaultHexes: params.vaultHexes ?? existingHexes,
4640
4758
  onProgress,
4641
4759
  signal,
4642
- onPage
4760
+ onPage,
4761
+ onBatchDone
4643
4762
  };
4644
4763
  let tailSince = sinceLeafIndex;
4645
4764
  try {
@@ -4680,52 +4799,12 @@ async function collectScanEntries(params) {
4680
4799
  return outcome;
4681
4800
  }
4682
4801
 
4683
- // src/wallet/scanner/selfEphGap.ts
4684
- function noteEphPkHex(note) {
4685
- if (!Array.isArray(note.memo) || note.memo.length < 32) return null;
4686
- return toHex(note.memo.slice(-32)).toLowerCase();
4687
- }
4688
- function gapMargin(windowSize) {
4689
- return Math.max(1, Math.floor(windowSize / 16));
4690
- }
4691
- function resolveSelfEphCeiling(params) {
4692
- const { notes, spendingKey, viewingKey, scanMaxIndex } = params;
4693
- const windowSize = params.windowSize ?? SELF_EPH_WINDOW;
4694
- if (scanMaxIndex === null) return null;
4695
- if (scanMaxIndex < windowSize - gapMargin(windowSize)) return scanMaxIndex;
4696
- const ownEphPks = /* @__PURE__ */ new Set();
4697
- for (const note of notes) {
4698
- const hex = noteEphPkHex(note);
4699
- if (hex) ownEphPks.add(hex);
4700
- }
4701
- const ivkPacked = deriveViewingPublicKey(viewingKey);
4702
- let max = scanMaxIndex;
4703
- let from = windowSize;
4704
- for (; ; ) {
4705
- const slice = selfEphWindow(spendingKey, ivkPacked, from, windowSize);
4706
- let matched = false;
4707
- for (const entry of slice) {
4708
- if (ownEphPks.has(entry.ephPkHex.toLowerCase())) {
4709
- matched = true;
4710
- if (entry.index > max) max = entry.index;
4711
- }
4712
- }
4713
- if (!matched) break;
4714
- from += windowSize;
4715
- }
4716
- return max;
4717
- }
4718
- function windowSizeForCounter(counter) {
4719
- const needed = counter + gapMargin(SELF_EPH_WINDOW);
4720
- return Math.max(SELF_EPH_WINDOW, Math.ceil(needed / SELF_EPH_WINDOW) * SELF_EPH_WINDOW);
4721
- }
4722
-
4723
4802
  // src/wallet/scanner/phases/spentSet.ts
4724
4803
  function normalizeNullifierHex(hex) {
4725
4804
  return hex.toLowerCase();
4726
4805
  }
4727
- async function resolveSpentSet(params) {
4728
- const { source, cache, ownNullifiers, signal } = params;
4806
+ async function openSpentSet(params) {
4807
+ const { source, cache, signal } = params;
4729
4808
  const throwIfAborted = () => {
4730
4809
  if (signal?.aborted) throw scanAbortError();
4731
4810
  };
@@ -4734,20 +4813,29 @@ async function resolveSpentSet(params) {
4734
4813
  await syncNullifierCache(params, manifest, throwIfAborted);
4735
4814
  throwIfAborted();
4736
4815
  const tail = await getConsistentTail(params, throwIfAborted);
4737
- const stored = await cache.getSpentNullifiers([...ownNullifiers]);
4738
4816
  const tailMap = new Map(
4739
4817
  tail.data.map((h, i) => [
4740
4818
  normalizeNullifierHex(h),
4741
4819
  { spentAt: tail.timestampsMs?.[i] ?? null, txHash: tail.txHashes?.[i] ?? null }
4742
4820
  ])
4743
4821
  );
4744
- const spent = /* @__PURE__ */ new Map();
4745
- const unknown = { spentAt: null, txHash: null };
4746
- for (const h of ownNullifiers) {
4747
- if (stored.has(h)) spent.set(h, stored.get(h) ?? unknown);
4748
- else if (tailMap.has(h)) spent.set(h, tailMap.get(h) ?? unknown);
4749
- }
4750
- return spent;
4822
+ return {
4823
+ async query(ownNullifiers) {
4824
+ if (ownNullifiers.size === 0) return /* @__PURE__ */ new Map();
4825
+ const stored = await cache.getSpentNullifiers([...ownNullifiers]);
4826
+ const spent = /* @__PURE__ */ new Map();
4827
+ const unknown = { spentAt: null, txHash: null };
4828
+ for (const h of ownNullifiers) {
4829
+ if (stored.has(h)) spent.set(h, stored.get(h) ?? unknown);
4830
+ else if (tailMap.has(h)) spent.set(h, tailMap.get(h) ?? unknown);
4831
+ }
4832
+ return spent;
4833
+ }
4834
+ };
4835
+ }
4836
+ async function resolveSpentSet(params) {
4837
+ const handle = await openSpentSet(params);
4838
+ return handle.query(params.ownNullifiers);
4751
4839
  }
4752
4840
  async function syncNullifierCache(params, manifest, throwIfAborted) {
4753
4841
  const { source, cache } = params;
@@ -4797,40 +4885,6 @@ async function getConsistentTail(params, throwIfAborted) {
4797
4885
  throw new Error("nullifier tail inconsistent with local chunks after retry");
4798
4886
  }
4799
4887
 
4800
- // src/wallet/scanner/phases/spentStatus.ts
4801
- function collectNullifiersToQuery(scanEntries, onChainHexes, vaultNotes) {
4802
- const syncedCommitments = new Set(scanEntries.map((e) => e.note.commitmentHex));
4803
- const nullifiers = /* @__PURE__ */ new Set();
4804
- for (const { note } of scanEntries) {
4805
- nullifiers.add(note.nullifierHex.toLowerCase());
4806
- }
4807
- for (const vaultNote of vaultNotes) {
4808
- if (syncedCommitments.has(vaultNote.commitmentHex)) continue;
4809
- if (!onChainHexes.has(vaultNote.commitmentHex)) continue;
4810
- if (vaultNote.nullifierHex) nullifiers.add(vaultNote.nullifierHex.toLowerCase());
4811
- }
4812
- return nullifiers;
4813
- }
4814
- async function resolveSpentStatus(params) {
4815
- if (params.nullifiers.size === 0) return /* @__PURE__ */ new Map();
4816
- try {
4817
- return await resolveSpentSet({
4818
- source: params.source,
4819
- cache: params.cache,
4820
- ownNullifiers: params.nullifiers,
4821
- signal: params.signal,
4822
- onWarning: params.onWarning
4823
- });
4824
- } catch (err) {
4825
- if (isAbortError(err)) throw err;
4826
- params.onWarning?.(
4827
- "could not resolve spent-nullifier status (feed unavailable or inconsistent); spent status left unverified this pass",
4828
- err
4829
- );
4830
- return /* @__PURE__ */ new Map();
4831
- }
4832
- }
4833
-
4834
4888
  // src/wallet/scanner/phases/persist.ts
4835
4889
  function purgeIsTrustworthy(hintsScanned, purgeableNotes) {
4836
4890
  if (purgeableNotes === 0) return true;
@@ -4889,6 +4943,123 @@ async function persistCursor(storage, maxLeafIndex, isIncremental) {
4889
4943
  });
4890
4944
  }
4891
4945
 
4946
+ // src/wallet/scanner/phases/checkpoint.ts
4947
+ function createScanCheckpoint(params) {
4948
+ const { vault, storage, nullifiers, isIncremental, signal, onWarning } = params;
4949
+ const throwIfAborted = () => {
4950
+ if (signal?.aborted) throw scanAbortError();
4951
+ };
4952
+ let spentSetPromise = null;
4953
+ const getSpentSet = () => spentSetPromise ??= openSpentSet({
4954
+ source: nullifiers,
4955
+ cache: storage,
4956
+ signal,
4957
+ onWarning
4958
+ }).catch(() => null);
4959
+ return {
4960
+ async savePageNotes(entries) {
4961
+ throwIfAborted();
4962
+ const fresh = entries.filter((e) => e.isNew);
4963
+ if (fresh.length === 0) return;
4964
+ const own = new Set(fresh.map((e) => e.note.nullifierHex.toLowerCase()));
4965
+ const spentSet = await getSpentSet();
4966
+ const spent = spentSet ? await spentSet.query(own).catch(() => /* @__PURE__ */ new Map()) : /* @__PURE__ */ new Map();
4967
+ throwIfAborted();
4968
+ await vault.saveMany(
4969
+ fresh.map(({ note }) => {
4970
+ const spend = spent.get(note.nullifierHex.toLowerCase());
4971
+ return {
4972
+ note: spend ? stampSpentTxHash(note, spend.txHash) : note,
4973
+ noteStatus: {
4974
+ spent: spend !== void 0,
4975
+ spentAt: spend?.spentAt ?? null
4976
+ }
4977
+ };
4978
+ })
4979
+ );
4980
+ },
4981
+ async advanceCursor(maxLeafIndex) {
4982
+ if (maxLeafIndex === void 0) return;
4983
+ throwIfAborted();
4984
+ await persistCursor(storage, maxLeafIndex, isIncremental);
4985
+ }
4986
+ };
4987
+ }
4988
+
4989
+ // src/wallet/scanner/selfEphGap.ts
4990
+ function noteEphPkHex(note) {
4991
+ if (!Array.isArray(note.memo) || note.memo.length < 32) return null;
4992
+ return toHex(note.memo.slice(-32)).toLowerCase();
4993
+ }
4994
+ function gapMargin(windowSize) {
4995
+ return Math.max(1, Math.floor(windowSize / 16));
4996
+ }
4997
+ function resolveSelfEphCeiling(params) {
4998
+ const { notes, spendingKey, viewingKey, scanMaxIndex } = params;
4999
+ const windowSize = params.windowSize ?? SELF_EPH_WINDOW;
5000
+ if (scanMaxIndex === null) return null;
5001
+ if (scanMaxIndex < windowSize - gapMargin(windowSize)) return scanMaxIndex;
5002
+ const ownEphPks = /* @__PURE__ */ new Set();
5003
+ for (const note of notes) {
5004
+ const hex = noteEphPkHex(note);
5005
+ if (hex) ownEphPks.add(hex);
5006
+ }
5007
+ const ivkPacked = deriveViewingPublicKey(viewingKey);
5008
+ let max = scanMaxIndex;
5009
+ let from = windowSize;
5010
+ for (; ; ) {
5011
+ const slice = selfEphWindow(spendingKey, ivkPacked, from, windowSize);
5012
+ let matched = false;
5013
+ for (const entry of slice) {
5014
+ if (ownEphPks.has(entry.ephPkHex.toLowerCase())) {
5015
+ matched = true;
5016
+ if (entry.index > max) max = entry.index;
5017
+ }
5018
+ }
5019
+ if (!matched) break;
5020
+ from += windowSize;
5021
+ }
5022
+ return max;
5023
+ }
5024
+ function windowSizeForCounter(counter) {
5025
+ const needed = counter + gapMargin(SELF_EPH_WINDOW);
5026
+ return Math.max(SELF_EPH_WINDOW, Math.ceil(needed / SELF_EPH_WINDOW) * SELF_EPH_WINDOW);
5027
+ }
5028
+
5029
+ // src/wallet/scanner/phases/spentStatus.ts
5030
+ function collectNullifiersToQuery(scanEntries, onChainHexes, vaultNotes) {
5031
+ const syncedCommitments = new Set(scanEntries.map((e) => e.note.commitmentHex));
5032
+ const nullifiers = /* @__PURE__ */ new Set();
5033
+ for (const { note } of scanEntries) {
5034
+ nullifiers.add(note.nullifierHex.toLowerCase());
5035
+ }
5036
+ for (const vaultNote of vaultNotes) {
5037
+ if (syncedCommitments.has(vaultNote.commitmentHex)) continue;
5038
+ if (!onChainHexes.has(vaultNote.commitmentHex)) continue;
5039
+ if (vaultNote.nullifierHex) nullifiers.add(vaultNote.nullifierHex.toLowerCase());
5040
+ }
5041
+ return nullifiers;
5042
+ }
5043
+ async function resolveSpentStatus(params) {
5044
+ if (params.nullifiers.size === 0) return /* @__PURE__ */ new Map();
5045
+ try {
5046
+ return await resolveSpentSet({
5047
+ source: params.source,
5048
+ cache: params.cache,
5049
+ ownNullifiers: params.nullifiers,
5050
+ signal: params.signal,
5051
+ onWarning: params.onWarning
5052
+ });
5053
+ } catch (err) {
5054
+ if (isAbortError(err)) throw err;
5055
+ params.onWarning?.(
5056
+ "could not resolve spent-nullifier status (feed unavailable or inconsistent); spent status left unverified this pass",
5057
+ err
5058
+ );
5059
+ return /* @__PURE__ */ new Map();
5060
+ }
5061
+ }
5062
+
4892
5063
  // src/wallet/scanner/pipeline.ts
4893
5064
  async function runScan(params) {
4894
5065
  const { vault, storage, hints, nullifiers, pool, keys, sinceLeafIndex, signal, onWarning } = params;
@@ -4907,6 +5078,14 @@ async function runScan(params) {
4907
5078
  }
4908
5079
  }
4909
5080
  );
5081
+ const checkpoint = createScanCheckpoint({
5082
+ vault,
5083
+ storage,
5084
+ nullifiers,
5085
+ isIncremental,
5086
+ signal,
5087
+ onWarning
5088
+ });
4910
5089
  const scan = await collectScanEntries({
4911
5090
  source: hints,
4912
5091
  pool,
@@ -4927,7 +5106,9 @@ async function runScan(params) {
4927
5106
  // consumer probes it with a vault commitment, so keeping the whole pool
4928
5107
  // only bought heap.
4929
5108
  vaultHexes: preScanHexes,
4930
- onWarning
5109
+ onWarning,
5110
+ onPage: (entries) => checkpoint.savePageNotes(entries),
5111
+ onBatchDone: (maxLeafIndex) => checkpoint.advanceCursor(maxLeafIndex)
4931
5112
  });
4932
5113
  const nullifiersToQuery = collectNullifiersToQuery(
4933
5114
  scan.scanEntries,
@@ -5300,6 +5481,22 @@ function isEntry2(value) {
5300
5481
  return true;
5301
5482
  }
5302
5483
 
5484
+ // src/wallet/ops/notes/paymentSlipImport.ts
5485
+ function importPaymentSlip(slip, keys) {
5486
+ const envelope = typeof slip === "string" ? decodePaymentSlip(slip) : slip;
5487
+ if (!envelope) return null;
5488
+ const fields = openPaymentSlip(keys.viewingSecretKey, envelope);
5489
+ if (!fields) return null;
5490
+ const commitment = {
5491
+ commitmentHex: fields.commitmentHex,
5492
+ leafIndex: fields.leafIndex ?? -1,
5493
+ encryptedMemo: fields.encryptedMemo
5494
+ };
5495
+ const note = tryDecryptNote(commitment, keys.viewingSecretKey, keys.spendingKey, keys.ownerPk);
5496
+ if (!note) return null;
5497
+ return fields.txHash ? stampCreatedTxHash(note, fields.txHash) : note;
5498
+ }
5499
+
5303
5500
  // src/wallet/ops/spend/transfer.ts
5304
5501
  import { CircuitType as CircuitType5 } from "@orbinum/proof-generator";
5305
5502
 
@@ -5509,6 +5706,18 @@ async function transferNotes(deps, params, onProgress) {
5509
5706
  await deps.vault.save(stampCreatedTxHash(selfNote, txResult.txHash, txKind));
5510
5707
  }
5511
5708
  }
5709
+ const isSelf = deps.selfOwnerPk !== null && recipientPk === deps.selfOwnerPk;
5710
+ if (recipientViewingPublicKey !== void 0 && !isSelf) {
5711
+ try {
5712
+ const envelope = sealPaymentSlip(recipientViewingPublicKey, {
5713
+ commitmentHex: recipientNote.commitmentHex,
5714
+ encryptedMemo: toHex(Uint8Array.from(recipientNote.memo)),
5715
+ ...txResult.txHash ? { txHash: txResult.txHash } : {}
5716
+ });
5717
+ return { ...txResult, paymentSlip: encodePaymentSlip(envelope) };
5718
+ } catch {
5719
+ }
5720
+ }
5512
5721
  }
5513
5722
  return txResult;
5514
5723
  }
@@ -6114,11 +6323,13 @@ export {
6114
6323
  NOTE_BIGINT_FIELDS,
6115
6324
  NOTE_TRANSFER_URI_SCHEME,
6116
6325
  NoteBuilder,
6326
+ OVK_BLOB_SIZE,
6117
6327
  OrbinumClient,
6118
6328
  OrbinumClientProvider,
6119
6329
  OrbinumWallet,
6120
6330
  PAGE_SIZE,
6121
6331
  PAIRWISE_EPH_WINDOW,
6332
+ PAYMENT_SLIP_SCHEME,
6122
6333
  PRECOMPILE_ADDR,
6123
6334
  PrivacyKeyManager,
6124
6335
  PrivacyModule,
@@ -6189,11 +6400,14 @@ export {
6189
6400
  decodeNoteBackup,
6190
6401
  decodeNoteDisclosureKey,
6191
6402
  decodeNoteTransferPage,
6403
+ decodePaymentSlip,
6192
6404
  decodePrecompileCalldata,
6193
6405
  decryptHintBatch,
6194
6406
  decryptJson,
6195
6407
  decryptNoteRecord,
6196
6408
  deriveMasterKeyBytes,
6409
+ deriveOutgoingCipherKey,
6410
+ deriveOutgoingViewingKey,
6197
6411
  deriveOwnerPk,
6198
6412
  derivePairwiseEphSk,
6199
6413
  derivePairwiseSharedSecret,
@@ -6212,6 +6426,7 @@ export {
6212
6426
  detectCommitmentMismatch,
6213
6427
  encodeNoteBackup,
6214
6428
  encodeNoteTransferPages,
6429
+ encodePaymentSlip,
6215
6430
  encryptJson,
6216
6431
  encryptNote,
6217
6432
  ensureCreatedAt,
@@ -6248,6 +6463,7 @@ export {
6248
6463
  implicitSubstrateToEvm,
6249
6464
  importDeviceKey,
6250
6465
  importNotesFromBackup,
6466
+ importPaymentSlip,
6251
6467
  isAbortError,
6252
6468
  isAlreadySpentError,
6253
6469
  isConnectionLossError,
@@ -6277,6 +6493,8 @@ export {
6277
6493
  noteSpentTxHash,
6278
6494
  noteToTransferEntry,
6279
6495
  noteTxKind,
6496
+ openOutgoingBlob,
6497
+ openPaymentSlip,
6280
6498
  pairwiseEphWindow,
6281
6499
  palletErrorKind,
6282
6500
  parseAmount,
@@ -6286,6 +6504,7 @@ export {
6286
6504
  planTransfer,
6287
6505
  planUnshield,
6288
6506
  randomBlinding,
6507
+ randomOutgoingBlob,
6289
6508
  reconstructOutgoingTxRecords,
6290
6509
  recoverOwnerPkPoint,
6291
6510
  recoverSelfStealthNote,
@@ -6301,6 +6520,8 @@ export {
6301
6520
  runScan,
6302
6521
  scalarToHex,
6303
6522
  scanAbortError,
6523
+ sealOutgoingBlob,
6524
+ sealPaymentSlip,
6304
6525
  selectGhosts,
6305
6526
  selectNotes,
6306
6527
  selfEphWindow,
@@ -6324,6 +6545,7 @@ export {
6324
6545
  truncateMiddle,
6325
6546
  tryDecryptNote,
6326
6547
  tryDecryptNoteVerbose,
6548
+ tryRecoverOutgoing,
6327
6549
  txLandedAfterError,
6328
6550
  u128,
6329
6551
  u64,
@@ -1 +1 @@
1
- export { c as DECRYPT_YIELD_EVERY, d as DecryptBatchResult, a as DecryptPool, e as DecryptRequest, E as EMPTY_BATCH_RESULT, K as KnownEphEntry, f as KnownEphWindow, M as MAX_WORKERS, g as MatchSource, P as PAIRWISE_EPH_WINDOW, i as SELF_EPH_WINDOW, b as ScanKeys, W as WORKER_CRASHED, j as WorkerFactory, k as WorkerLike, l as WorkerMessage, m as clearKnownEphWindow, n as createDecryptPool, o as createMainThreadPool, p as createWorkerPool, q as decryptHintBatch, r as getKnownEphWindow } from '../../index-JYVjYJtf.mjs';
1
+ export { c as DECRYPT_YIELD_EVERY, d as DecryptBatchResult, a as DecryptPool, e as DecryptRequest, E as EMPTY_BATCH_RESULT, K as KnownEphEntry, f as KnownEphWindow, M as MAX_WORKERS, g as MatchSource, P as PAIRWISE_EPH_WINDOW, i as SELF_EPH_WINDOW, b as ScanKeys, W as WORKER_CRASHED, j as WorkerFactory, k as WorkerLike, l as WorkerMessage, m as clearKnownEphWindow, n as createDecryptPool, o as createMainThreadPool, p as createWorkerPool, q as decryptHintBatch, r as getKnownEphWindow } from '../../index-V5Z9igEN.mjs';
@@ -1 +1 @@
1
- export { c as DECRYPT_YIELD_EVERY, d as DecryptBatchResult, a as DecryptPool, e as DecryptRequest, E as EMPTY_BATCH_RESULT, K as KnownEphEntry, f as KnownEphWindow, M as MAX_WORKERS, g as MatchSource, P as PAIRWISE_EPH_WINDOW, i as SELF_EPH_WINDOW, b as ScanKeys, W as WORKER_CRASHED, j as WorkerFactory, k as WorkerLike, l as WorkerMessage, m as clearKnownEphWindow, n as createDecryptPool, o as createMainThreadPool, p as createWorkerPool, q as decryptHintBatch, r as getKnownEphWindow } from '../../index-JYVjYJtf.js';
1
+ export { c as DECRYPT_YIELD_EVERY, d as DecryptBatchResult, a as DecryptPool, e as DecryptRequest, E as EMPTY_BATCH_RESULT, K as KnownEphEntry, f as KnownEphWindow, M as MAX_WORKERS, g as MatchSource, P as PAIRWISE_EPH_WINDOW, i as SELF_EPH_WINDOW, b as ScanKeys, W as WORKER_CRASHED, j as WorkerFactory, k as WorkerLike, l as WorkerMessage, m as clearKnownEphWindow, n as createDecryptPool, o as createMainThreadPool, p as createWorkerPool, q as decryptHintBatch, r as getKnownEphWindow } from '../../index-V5Z9igEN.js';
@@ -570,6 +570,7 @@ var import_hkdf2 = require("@noble/hashes/hkdf.js");
570
570
  var import_sha25 = require("@noble/hashes/sha2.js");
571
571
  var import_baby_jubjub6 = require("@zk-kit/baby-jubjub");
572
572
  var IVK_DOMAIN = new TextEncoder().encode("orbinum-ivk-v1");
573
+ var OVK_DOMAIN = new TextEncoder().encode("orbinum-ovk-v1");
573
574
  function deriveViewingPublicKey(ivsk) {
574
575
  const ivskScalar = BigInt(toHex(ivsk)) % BABYJUB_SUBORDER || 1n;
575
576
  const ivkPoint = fastMulBase(ivskScalar);
@@ -11,7 +11,7 @@ import {
11
11
  createWorkerPool,
12
12
  decryptHintBatch,
13
13
  getKnownEphWindow
14
- } from "../../chunk-Y6LNYJAJ.mjs";
14
+ } from "../../chunk-A2ZRMEYW.mjs";
15
15
  export {
16
16
  DECRYPT_YIELD_EVERY,
17
17
  EMPTY_BATCH_RESULT,