@orbinum/sdk 2.1.0 → 3.0.1

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.js CHANGED
@@ -47,9 +47,8 @@ __export(index_exports, {
47
47
  NATIVE_ASSET_ID: () => NATIVE_ASSET_ID,
48
48
  NOTE_BACKUP_VERSION: () => NOTE_BACKUP_VERSION,
49
49
  NOTE_BIGINT_FIELDS: () => NOTE_BIGINT_FIELDS,
50
- NOTE_TRANSFER_URI_SCHEME: () => NOTE_TRANSFER_URI_SCHEME,
51
50
  NoteBuilder: () => NoteBuilder,
52
- OVK_BLOB_SIZE: () => OVK_BLOB_SIZE,
51
+ OUTGOING_EPH_WINDOW: () => OUTGOING_EPH_WINDOW,
53
52
  OrbinumClient: () => OrbinumClient,
54
53
  OrbinumClientProvider: () => OrbinumClientProvider,
55
54
  OrbinumWallet: () => OrbinumWallet,
@@ -59,7 +58,6 @@ __export(index_exports, {
59
58
  PRECOMPILE_ADDR: () => PRECOMPILE_ADDR,
60
59
  PrivacyKeyManager: () => PrivacyKeyManager,
61
60
  PrivacyModule: () => PrivacyModule,
62
- QR_PAGE_MAX_CHARS: () => QR_PAGE_MAX_CHARS,
63
61
  RECOVERED_TX_RESULT: () => RECOVERED_TX_RESULT,
64
62
  RelayerStatusModule: () => RelayerStatusModule,
65
63
  RpcV2Module: () => RpcV2Module,
@@ -84,7 +82,7 @@ __export(index_exports, {
84
82
  addressToFieldElement: () => addressToFieldElement,
85
83
  applyBatch: () => applyBatch,
86
84
  applyNoteStatus: () => applyNoteStatus,
87
- assembleNoteTransfer: () => assembleNoteTransfer,
85
+ assertSecretKeyBytes: () => assertSecretKeyBytes,
88
86
  base58: () => import_base.base58,
89
87
  base64UrlDecode: () => base64UrlDecode,
90
88
  base64UrlEncode: () => base64UrlEncode,
@@ -97,6 +95,7 @@ __export(index_exports, {
97
95
  buildShieldBatchOperations: () => buildShieldBatchOperations,
98
96
  buildShieldParams: () => buildShieldParams,
99
97
  buildZkNote: () => buildZkNote,
98
+ buildZkNoteWithIndex: () => buildZkNoteWithIndex,
100
99
  bytesToBigintLE: () => bytesToBigintLE,
101
100
  bytesToBjjScalar: () => bytesToBjjScalar,
102
101
  cacheSession: () => cacheSession,
@@ -126,15 +125,17 @@ __export(index_exports, {
126
125
  createWorkerPool: () => createWorkerPool,
127
126
  decodeNoteBackup: () => decodeNoteBackup,
128
127
  decodeNoteDisclosureKey: () => decodeNoteDisclosureKey,
129
- decodeNoteTransferPage: () => decodeNoteTransferPage,
130
128
  decodePaymentSlip: () => decodePaymentSlip,
131
129
  decodePrecompileCalldata: () => decodePrecompileCalldata,
132
130
  decryptHintBatch: () => decryptHintBatch,
133
131
  decryptJson: () => decryptJson,
134
132
  decryptNoteRecord: () => decryptNoteRecord,
133
+ deriveIdentity: () => deriveIdentity,
135
134
  deriveMasterKeyBytes: () => deriveMasterKeyBytes,
136
- deriveOutgoingCipherKey: () => deriveOutgoingCipherKey,
137
- deriveOutgoingViewingKey: () => deriveOutgoingViewingKey,
135
+ deriveOutgoingEphPk: () => deriveOutgoingEphPk,
136
+ deriveOutgoingEphSk: () => deriveOutgoingEphSk,
137
+ deriveOutgoingSharedSecret: () => deriveOutgoingSharedSecret,
138
+ deriveOutgoingViewingKeyV3: () => deriveOutgoingViewingKeyV3,
138
139
  deriveOwnerPk: () => deriveOwnerPk,
139
140
  derivePairwiseEphSk: () => derivePairwiseEphSk,
140
141
  derivePairwiseSharedSecret: () => derivePairwiseSharedSecret,
@@ -143,6 +144,7 @@ __export(index_exports, {
143
144
  deriveSpendingKeyFromSignature: () => deriveSpendingKeyFromSignature,
144
145
  deriveSpendingKeyMessageV2: () => deriveSpendingKeyMessageV2,
145
146
  deriveSpendingKeyTypedData: () => deriveSpendingKeyTypedData,
147
+ deriveSpendingKeyV3: () => deriveSpendingKeyV3,
146
148
  deriveStealthOwnerPk: () => deriveStealthOwnerPk,
147
149
  deriveStealthSk: () => deriveStealthSk,
148
150
  deriveVaultBlindKey: () => deriveVaultBlindKey,
@@ -150,9 +152,9 @@ __export(index_exports, {
150
152
  deriveViewTag: () => deriveViewTag,
151
153
  deriveViewingPublicKey: () => deriveViewingPublicKey,
152
154
  deriveViewingSecretKey: () => deriveViewingSecretKey,
155
+ deriveViewingSecretKeyV3: () => deriveViewingSecretKeyV3,
153
156
  detectCommitmentMismatch: () => detectCommitmentMismatch,
154
157
  encodeNoteBackup: () => encodeNoteBackup,
155
- encodeNoteTransferPages: () => encodeNoteTransferPages,
156
158
  encodePaymentSlip: () => encodePaymentSlip,
157
159
  encryptJson: () => encryptJson,
158
160
  encryptNote: () => encryptNote,
@@ -162,6 +164,7 @@ __export(index_exports, {
162
164
  evmToImplicitSubstrate: () => evmToImplicitSubstrate,
163
165
  evmToMappedAccountHex: () => evmToMappedAccountHex,
164
166
  evmToSubstrate: () => evmToSubstrate,
167
+ exportViewingCredential: () => exportViewingCredential,
165
168
  extractPalletError: () => extractPalletError,
166
169
  failed: () => failed,
167
170
  fastMulBase: () => fastMulBase,
@@ -202,9 +205,9 @@ __export(index_exports, {
202
205
  isNativeAsset: () => isNativeAsset,
203
206
  isNoteSelfConsistent: () => isNoteSelfConsistent,
204
207
  isSpendable: () => isSpendable,
205
- isSs58: () => isSs58,
206
208
  isSubstrateAddress: () => isSubstrateAddress,
207
209
  isUnifiedAddress: () => isUnifiedAddress,
210
+ isUsableSecretKey: () => isUsableSecretKey,
208
211
  isValidLeafIndex: () => isValidLeafIndex,
209
212
  leHexToBigint: () => leHexToBigint,
210
213
  mapExtrinsicArgs: () => mapExtrinsicArgs,
@@ -221,10 +224,10 @@ __export(index_exports, {
221
224
  noteMatchesCommitment: () => noteMatchesCommitment,
222
225
  noteOrigin: () => noteOrigin,
223
226
  noteSpentTxHash: () => noteSpentTxHash,
224
- noteToTransferEntry: () => noteToTransferEntry,
225
227
  noteTxKind: () => noteTxKind,
226
- openOutgoingBlob: () => openOutgoingBlob,
227
228
  openPaymentSlip: () => openPaymentSlip,
229
+ openRecipientBookEntry: () => openRecipientBookEntry,
230
+ outgoingEphWindow: () => outgoingEphWindow,
228
231
  outranks: () => outranks,
229
232
  pairwiseEphWindow: () => pairwiseEphWindow,
230
233
  palletErrorKind: () => palletErrorKind,
@@ -235,15 +238,18 @@ __export(index_exports, {
235
238
  planTransfer: () => planTransfer,
236
239
  planUnshield: () => planUnshield,
237
240
  randomBlinding: () => randomBlinding,
238
- randomOutgoingBlob: () => randomOutgoingBlob,
241
+ reconstructOutgoingIndex: () => reconstructOutgoingIndex,
239
242
  reconstructOutgoingTxRecords: () => reconstructOutgoingTxRecords,
240
243
  recoverOwnerPkPoint: () => recoverOwnerPkPoint,
241
244
  recoverSelfStealthNote: () => recoverSelfStealthNote,
245
+ recoverSentFromSharedSecret: () => recoverSentFromSharedSecret,
246
+ recoverSentNote: () => recoverSentNote,
242
247
  refuseIfAlreadySpent: () => refuseIfAlreadySpent,
243
248
  regeneratePaymentSlip: () => regeneratePaymentSlip,
249
+ registerPairwiseCounterparty: () => registerPairwiseCounterparty,
244
250
  removeByCommitment: () => removeByCommitment,
245
251
  requireSessionKeys: () => requireSessionKeys,
246
- reservePairwiseIndex: () => reservePairwiseIndex,
252
+ reserveOutgoingIndex: () => reserveOutgoingIndex,
247
253
  reserveSelfEphIndex: () => reserveSelfEphIndex,
248
254
  resolveSelfEphCeiling: () => resolveSelfEphCeiling,
249
255
  resolveSpentSet: () => resolveSpentSet,
@@ -252,8 +258,8 @@ __export(index_exports, {
252
258
  runScan: () => runScan,
253
259
  scalarToHex: () => scalarToHex,
254
260
  scanAbortError: () => scanAbortError,
255
- sealOutgoingBlob: () => sealOutgoingBlob,
256
261
  sealPaymentSlip: () => sealPaymentSlip,
262
+ sealRecipientBookEntry: () => sealRecipientBookEntry,
257
263
  selectDescribingNote: () => selectDescribingNote,
258
264
  selectDescribingNoteByCommitment: () => selectDescribingNoteByCommitment,
259
265
  selectGhosts: () => selectGhosts,
@@ -279,10 +285,10 @@ __export(index_exports, {
279
285
  truncateMiddle: () => truncateMiddle,
280
286
  tryDecryptNote: () => tryDecryptNote,
281
287
  tryDecryptNoteVerbose: () => tryDecryptNoteVerbose,
282
- tryRecoverOutgoing: () => tryRecoverOutgoing,
283
288
  txLandedAfterError: () => txLandedAfterError,
284
289
  u128: () => import_substrate_bindings3.u128,
285
290
  u64: () => import_substrate_bindings3.u64,
291
+ unpackUsableViewingKey: () => unpackUsableViewingKey,
286
292
  unshieldNote: () => unshieldNote,
287
293
  upsertNote: () => upsertNote,
288
294
  vaultReplacer: () => vaultReplacer,
@@ -317,12 +323,23 @@ function ensureHexPrefix(hex) {
317
323
  return hex.startsWith("0x") ? hex : `0x${hex}`;
318
324
  }
319
325
  function hexToNumber(hex) {
320
- return parseInt(hex, 16);
326
+ const clean = /^0x/i.test(hex) ? hex.slice(2) : hex;
327
+ if (!/^[0-9a-fA-F]+$/.test(clean)) {
328
+ throw new Error(`Invalid hex quantity: "${hex}"`);
329
+ }
330
+ const value = Number(BigInt("0x" + clean));
331
+ if (!Number.isSafeInteger(value)) {
332
+ throw new Error(`Hex quantity exceeds safe integer range: "${hex}"`);
333
+ }
334
+ return value;
321
335
  }
322
336
  function hexToBigint(hex) {
323
337
  return BigInt(hex);
324
338
  }
325
339
  function scalarToHex(value) {
340
+ if (value < 0n || value >> 256n !== 0n) {
341
+ throw new Error(`scalarToHex: value must fit in 32 unsigned bytes, got ${value}`);
342
+ }
326
343
  return "0x" + value.toString(16).padStart(64, "0");
327
344
  }
328
345
 
@@ -464,6 +481,7 @@ var BABYJUB_SUBORDER = 273603035897990940278080071815715938607681397215856725920
464
481
  var import_baby_jubjub = require("@zk-kit/baby-jubjub");
465
482
  var BJJ_A = 168700n;
466
483
  var BJJ_D = 168696n;
484
+ var BJJ_COFACTOR = 8n;
467
485
  function _modpow(base, exp, mod) {
468
486
  let result = 1n;
469
487
  base = base % mod;
@@ -522,6 +540,28 @@ function recoverOwnerPkPoint(ax) {
522
540
  return [ax, yAlt];
523
541
  }
524
542
  }
543
+ function unpackUsableViewingKey(packed) {
544
+ const point = (0, import_baby_jubjub.unpackPoint)(packed);
545
+ if (!point) return null;
546
+ if (point[0] === 0n && point[1] === 1n) return null;
547
+ const cleared = (0, import_baby_jubjub.mulPointEscalar)(point, BJJ_COFACTOR);
548
+ if (cleared[0] === 0n && cleared[1] === 1n) return null;
549
+ return point;
550
+ }
551
+
552
+ // src/foundation/crypto/keyGuards.ts
553
+ var SECRET_KEY_SIZE = 32;
554
+ function isUsableSecretKey(key) {
555
+ return key !== void 0 && key !== null && key.length === SECRET_KEY_SIZE && !key.every((b) => b === 0);
556
+ }
557
+ function assertSecretKeyBytes(key, label) {
558
+ if (key.length !== SECRET_KEY_SIZE) {
559
+ throw new Error(`${label} must be ${SECRET_KEY_SIZE} bytes, got ${key.length}`);
560
+ }
561
+ if (key.every((b) => b === 0)) {
562
+ throw new Error(`${label} is all zeros \u2014 that is not key material`);
563
+ }
564
+ }
525
565
 
526
566
  // src/foundation/crypto/bjj-fast.ts
527
567
  var import_edwards = require("@noble/curves/abstract/edwards.js");
@@ -697,9 +737,6 @@ function parseEvmAddress(addr) {
697
737
  const withPrefix = addr.startsWith("0x") ? addr : `0x${addr}`;
698
738
  return isEvmAddress(withPrefix) ? withPrefix.toLowerCase() : null;
699
739
  }
700
- function isSs58(addr) {
701
- return !addr.startsWith("0x") && addr.length >= 46 && addr.length <= 50;
702
- }
703
740
  function isEvmAddress(addr) {
704
741
  return /^0x[0-9a-fA-F]{40}$/.test(addr);
705
742
  }
@@ -829,11 +866,10 @@ function addressToAccountIdHex(addr) {
829
866
  function addressToFieldElement(address) {
830
867
  const accountIdHex = addressToAccountIdHex(address);
831
868
  if (!accountIdHex) throw new Error(`Cannot resolve address to AccountId32: ${address}`);
832
- const hex = accountIdHex.slice(2);
833
- const bytes = new Uint8Array(32);
834
- for (let i = 0; i < 32; i++) {
835
- bytes[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
869
+ if (!isHexOfLength(accountIdHex, 32)) {
870
+ throw new Error(`AccountId32 must be 32 bytes of hex: ${accountIdHex}`);
836
871
  }
872
+ const bytes = fromHex(accountIdHex);
837
873
  return bytesToBigintLE(bytes) % BN254_R;
838
874
  }
839
875
 
@@ -848,6 +884,15 @@ var KEY_DOMAIN = new TextEncoder().encode("orbinum-note-encryption-v1");
848
884
  var VIEW_TAG_DOMAIN = new TextEncoder().encode("orbinum-view-tag-v1");
849
885
  var MEMO_PLAINTEXT_SIZE = 120;
850
886
  function serializeMemo(value, ownerPk, blinding, assetId, sourcePk, circuitVersion) {
887
+ if (value < 0n || value >> 128n !== 0n) {
888
+ throw new Error(`serializeMemo: value must fit in 128 unsigned bits, got ${value}`);
889
+ }
890
+ if (!Number.isInteger(assetId) || assetId < 0 || assetId > 4294967295) {
891
+ throw new Error(`serializeMemo: assetId must be a u32, got ${assetId}`);
892
+ }
893
+ if (!Number.isInteger(circuitVersion) || circuitVersion < 0 || circuitVersion > 4294967295) {
894
+ throw new Error(`serializeMemo: circuitVersion must be a u32, got ${circuitVersion}`);
895
+ }
851
896
  const buf = new Uint8Array(MEMO_PLAINTEXT_SIZE);
852
897
  const view = new DataView(buf.buffer);
853
898
  view.setBigUint64(0, value & 0xffffffffffffffffn, true);
@@ -879,6 +924,9 @@ var CIPHERTEXT_SIZE = 136;
879
924
  var EPH_PK_SIZE = 32;
880
925
  var ENCRYPTED_MEMO_SIZE = NONCE_SIZE + CIPHERTEXT_SIZE + EPH_PK_SIZE;
881
926
  function bytesToBjjScalar(bytes) {
927
+ if (bytes.length !== 32) {
928
+ throw new Error(`bytesToBjjScalar: expected 32 bytes, got ${bytes.length}`);
929
+ }
882
930
  const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
883
931
  return BigInt("0x" + hex) % BABYJUB_SUBORDER || 1n;
884
932
  }
@@ -902,7 +950,7 @@ function parsePlaintext(nonce, ciphertextWithMac, encKey) {
902
950
  }
903
951
  var EncryptedMemo = {
904
952
  /**
905
- * Build and encrypt a memo for a note using ECDH (v2, 180 bytes).
953
+ * Build and encrypt a memo for a note using ECDH (180 bytes).
906
954
  *
907
955
  * @param value Note value in planck.
908
956
  * @param ownerPk 32-byte owner public key (LE).
@@ -942,7 +990,7 @@ var EncryptedMemo = {
942
990
  const ephPkPoint = fastMulBase(ephSkScalar);
943
991
  ephPkPackedBytes = bigintTo32Le((0, import_baby_jubjub3.packPoint)(ephPkPoint));
944
992
  const ivkPackedBigint = bytesToBigintLE(recipientIvkPacked);
945
- const ivkPoint = (0, import_baby_jubjub3.unpackPoint)(ivkPackedBigint);
993
+ const ivkPoint = unpackUsableViewingKey(ivkPackedBigint);
946
994
  if (!ivkPoint)
947
995
  throw new Error("EncryptedMemo.encrypt: invalid recipient viewing public key");
948
996
  const sharedPoint = fastMulPoint(ivkPoint, ephSkScalar);
@@ -1032,7 +1080,7 @@ var EncryptedMemo = {
1032
1080
  if (ephPkPackedBigint === 0n) {
1033
1081
  return new Uint8Array(32);
1034
1082
  }
1035
- const ephPkPoint = (0, import_baby_jubjub3.unpackPoint)(ephPkPackedBigint);
1083
+ const ephPkPoint = unpackUsableViewingKey(ephPkPackedBigint);
1036
1084
  if (!ephPkPoint) return null;
1037
1085
  const ivskScalar = bytesToBjjScalar(viewingSecretKey);
1038
1086
  const sharedPoint = fastMulPoint(ephPkPoint, ivskScalar);
@@ -1042,9 +1090,9 @@ var EncryptedMemo = {
1042
1090
  * Cheap view-tag check: does memo nonce[0] match the tag derived from
1043
1091
  * `sharedSecret`? One SHA256 + one byte compare — no AEAD work.
1044
1092
  *
1045
- * Only meaningful for memos built with view tags (commitments at/after
1046
- * the wallet's tagActivationLeaf): a legacy memo carries a random byte
1047
- * there and would false-negative 255/256 of the time.
1093
+ * Only meaningful for memos that carry a tag at or after
1094
+ * `ScanKeys.viewTagActivationLeaf`. An older memo has a random byte there
1095
+ * and would false-negative 255/256 of the time.
1048
1096
  */
1049
1097
  checkViewTag(memoBytes, sharedSecret) {
1050
1098
  if (memoBytes.length !== ENCRYPTED_MEMO_SIZE) return false;
@@ -1070,72 +1118,11 @@ var EncryptedMemo = {
1070
1118
  }
1071
1119
  };
1072
1120
 
1073
- // src/protocol/memo/OutgoingBlob.ts
1121
+ // src/protocol/memo/PaymentSlip.ts
1074
1122
  var import_chacha2 = require("@noble/ciphers/chacha.js");
1075
1123
  var import_utils2 = require("@noble/ciphers/utils.js");
1076
1124
  var import_hkdf2 = require("@noble/hashes/hkdf.js");
1077
1125
  var import_sha23 = require("@noble/hashes/sha2.js");
1078
- var OCK_DOMAIN = new TextEncoder().encode("orbinum-outgoing-cipher-v1");
1079
- var NONCE_PREFIX = new TextEncoder().encode("OVK1");
1080
- var NONCE_SUFFIX_SIZE = 8;
1081
- var SHARED_SECRET_SIZE = 32;
1082
- var MAC_SIZE = 16;
1083
- var OVK_BLOB_SIZE = NONCE_SUFFIX_SIZE + SHARED_SECRET_SIZE + MAC_SIZE;
1084
- function deriveOutgoingCipherKey(ovk, commitmentBytes, ephPkBytes) {
1085
- if (commitmentBytes.length !== 32) {
1086
- throw new Error(
1087
- `OutgoingBlob: commitmentBytes must be 32 bytes, got ${commitmentBytes.length}`
1088
- );
1089
- }
1090
- if (ephPkBytes.length !== 32) {
1091
- throw new Error(`OutgoingBlob: ephPkBytes must be 32 bytes, got ${ephPkBytes.length}`);
1092
- }
1093
- const salt = new Uint8Array(64);
1094
- salt.set(commitmentBytes, 0);
1095
- salt.set(ephPkBytes, 32);
1096
- return (0, import_hkdf2.hkdf)(import_sha23.sha256, ovk, salt, OCK_DOMAIN, 32);
1097
- }
1098
- function buildNonce(suffix) {
1099
- const nonce = new Uint8Array(12);
1100
- nonce.set(NONCE_PREFIX, 0);
1101
- nonce.set(suffix, 4);
1102
- return nonce;
1103
- }
1104
- function sealOutgoingBlob(ovk, sharedSecret, commitmentBytes, ephPkBytes) {
1105
- if (sharedSecret.length !== SHARED_SECRET_SIZE) {
1106
- throw new Error(`OutgoingBlob: sharedSecret must be 32 bytes, got ${sharedSecret.length}`);
1107
- }
1108
- const ock = deriveOutgoingCipherKey(ovk, commitmentBytes, ephPkBytes);
1109
- const suffix = (0, import_utils2.randomBytes)(NONCE_SUFFIX_SIZE);
1110
- const cipher = (0, import_chacha2.chacha20poly1305)(ock, buildNonce(suffix));
1111
- const sealed = cipher.encrypt(sharedSecret);
1112
- const blob = new Uint8Array(OVK_BLOB_SIZE);
1113
- blob.set(suffix, 0);
1114
- blob.set(sealed, NONCE_SUFFIX_SIZE);
1115
- return blob;
1116
- }
1117
- function openOutgoingBlob(ovk, blob, commitmentBytes, ephPkBytes) {
1118
- if (blob.length !== OVK_BLOB_SIZE) return null;
1119
- if (commitmentBytes.length !== 32 || ephPkBytes.length !== 32) return null;
1120
- try {
1121
- const ock = deriveOutgoingCipherKey(ovk, commitmentBytes, ephPkBytes);
1122
- const suffix = blob.subarray(0, NONCE_SUFFIX_SIZE);
1123
- const sealed = blob.subarray(NONCE_SUFFIX_SIZE);
1124
- const cipher = (0, import_chacha2.chacha20poly1305)(ock, buildNonce(suffix));
1125
- return cipher.decrypt(sealed);
1126
- } catch {
1127
- return null;
1128
- }
1129
- }
1130
- function randomOutgoingBlob() {
1131
- return (0, import_utils2.randomBytes)(OVK_BLOB_SIZE);
1132
- }
1133
-
1134
- // src/protocol/memo/PaymentSlip.ts
1135
- var import_chacha3 = require("@noble/ciphers/chacha.js");
1136
- var import_utils3 = require("@noble/ciphers/utils.js");
1137
- var import_hkdf3 = require("@noble/hashes/hkdf.js");
1138
- var import_sha24 = require("@noble/hashes/sha2.js");
1139
1126
  var import_baby_jubjub4 = require("@zk-kit/baby-jubjub");
1140
1127
 
1141
1128
  // src/protocol/spend/coinSelection.ts
@@ -1199,16 +1186,16 @@ function buildDummyTransferInput(assetId) {
1199
1186
 
1200
1187
  // src/protocol/memo/PaymentSlip.ts
1201
1188
  var SLIP_DOMAIN = new TextEncoder().encode("orbinum-payment-slip-v1");
1202
- var NONCE_PREFIX2 = new TextEncoder().encode("SLP1");
1189
+ var NONCE_PREFIX = new TextEncoder().encode("SLP1");
1203
1190
  var EPH_PK_SIZE2 = 32;
1204
- var NONCE_SUFFIX_SIZE2 = 8;
1191
+ var NONCE_SUFFIX_SIZE = 8;
1205
1192
  var MAX_SLIP_ENVELOPE_SIZE = 4096;
1206
1193
  function deriveSlipKey(sharedSecret) {
1207
- return (0, import_hkdf3.hkdf)(import_sha24.sha256, sharedSecret, void 0, SLIP_DOMAIN, 32);
1194
+ return (0, import_hkdf2.hkdf)(import_sha23.sha256, sharedSecret, void 0, SLIP_DOMAIN, 32);
1208
1195
  }
1209
- function buildNonce2(suffix) {
1196
+ function buildNonce(suffix) {
1210
1197
  const nonce = new Uint8Array(12);
1211
- nonce.set(NONCE_PREFIX2, 0);
1198
+ nonce.set(NONCE_PREFIX, 0);
1212
1199
  nonce.set(suffix, 4);
1213
1200
  return nonce;
1214
1201
  }
@@ -1218,36 +1205,36 @@ function sealPaymentSlip(recipientIvkPacked, fields) {
1218
1205
  `PaymentSlip: recipientIvk must be 32 bytes, got ${recipientIvkPacked.length}`
1219
1206
  );
1220
1207
  }
1221
- const ivkPoint = (0, import_baby_jubjub4.unpackPoint)(bytesToBigintLE(recipientIvkPacked));
1208
+ const ivkPoint = unpackUsableViewingKey(bytesToBigintLE(recipientIvkPacked));
1222
1209
  if (!ivkPoint) throw new Error("PaymentSlip: invalid recipient viewing public key");
1223
- const ephSkScalar = bytesToBjjScalar((0, import_utils3.randomBytes)(32));
1210
+ const ephSkScalar = bytesToBjjScalar((0, import_utils2.randomBytes)(32));
1224
1211
  const ephPkPacked = bigintTo32Le((0, import_baby_jubjub4.packPoint)(fastMulBase(ephSkScalar)));
1225
1212
  const sharedPoint = fastMulPoint(ivkPoint, ephSkScalar);
1226
1213
  const sharedSecret = bigintTo32Le(sharedPoint[0]);
1227
1214
  const slipKey = deriveSlipKey(sharedSecret);
1228
- const suffix = (0, import_utils3.randomBytes)(NONCE_SUFFIX_SIZE2);
1229
- const cipher = (0, import_chacha3.chacha20poly1305)(slipKey, buildNonce2(suffix));
1215
+ const suffix = (0, import_utils2.randomBytes)(NONCE_SUFFIX_SIZE);
1216
+ const cipher = (0, import_chacha2.chacha20poly1305)(slipKey, buildNonce(suffix));
1230
1217
  const plaintext = new TextEncoder().encode(JSON.stringify(fields));
1231
1218
  const sealed = cipher.encrypt(plaintext);
1232
- const envelope = new Uint8Array(EPH_PK_SIZE2 + NONCE_SUFFIX_SIZE2 + sealed.length);
1219
+ const envelope = new Uint8Array(EPH_PK_SIZE2 + NONCE_SUFFIX_SIZE + sealed.length);
1233
1220
  envelope.set(ephPkPacked, 0);
1234
1221
  envelope.set(suffix, EPH_PK_SIZE2);
1235
- envelope.set(sealed, EPH_PK_SIZE2 + NONCE_SUFFIX_SIZE2);
1222
+ envelope.set(sealed, EPH_PK_SIZE2 + NONCE_SUFFIX_SIZE);
1236
1223
  return envelope;
1237
1224
  }
1238
1225
  function openPaymentSlip(recipientIvsk, envelope) {
1239
- if (envelope.length < EPH_PK_SIZE2 + NONCE_SUFFIX_SIZE2 + 16) return null;
1226
+ if (envelope.length < EPH_PK_SIZE2 + NONCE_SUFFIX_SIZE + 16) return null;
1240
1227
  if (envelope.length > MAX_SLIP_ENVELOPE_SIZE) return null;
1241
1228
  try {
1242
1229
  const ephPkPacked = envelope.subarray(0, EPH_PK_SIZE2);
1243
- const ephPkPoint = (0, import_baby_jubjub4.unpackPoint)(bytesToBigintLE(ephPkPacked));
1230
+ const ephPkPoint = unpackUsableViewingKey(bytesToBigintLE(ephPkPacked));
1244
1231
  if (!ephPkPoint) return null;
1245
1232
  const ivskScalar = bytesToBjjScalar(recipientIvsk);
1246
1233
  const sharedSecret = bigintTo32Le(fastMulPoint(ephPkPoint, ivskScalar)[0]);
1247
1234
  const slipKey = deriveSlipKey(sharedSecret);
1248
- const suffix = envelope.subarray(EPH_PK_SIZE2, EPH_PK_SIZE2 + NONCE_SUFFIX_SIZE2);
1249
- const sealed = envelope.subarray(EPH_PK_SIZE2 + NONCE_SUFFIX_SIZE2);
1250
- const cipher = (0, import_chacha3.chacha20poly1305)(slipKey, buildNonce2(suffix));
1235
+ const suffix = envelope.subarray(EPH_PK_SIZE2, EPH_PK_SIZE2 + NONCE_SUFFIX_SIZE);
1236
+ const sealed = envelope.subarray(EPH_PK_SIZE2 + NONCE_SUFFIX_SIZE);
1237
+ const cipher = (0, import_chacha2.chacha20poly1305)(slipKey, buildNonce(suffix));
1251
1238
  const plaintext = cipher.decrypt(sealed);
1252
1239
  const parsed = JSON.parse(new TextDecoder().decode(plaintext));
1253
1240
  if (!isHexOfLength(parsed["commitmentHex"], 32)) return null;
@@ -1268,7 +1255,7 @@ function openPaymentSlip(recipientIvsk, envelope) {
1268
1255
  }
1269
1256
  var PAYMENT_SLIP_SCHEME = "orbslip1:";
1270
1257
  function slipChecksum(payload) {
1271
- const digest = (0, import_sha24.sha256)(new TextEncoder().encode(PAYMENT_SLIP_SCHEME + payload));
1258
+ const digest = (0, import_sha23.sha256)(new TextEncoder().encode(PAYMENT_SLIP_SCHEME + payload));
1272
1259
  return toHex(digest.slice(0, 4)).slice(2);
1273
1260
  }
1274
1261
  function encodePaymentSlip(envelope) {
@@ -1284,38 +1271,48 @@ function decodePaymentSlip(text) {
1284
1271
  const checksum = rest.slice(sep + 1);
1285
1272
  if (slipChecksum(payload) !== checksum) return null;
1286
1273
  try {
1287
- const hex = base64UrlDecode(payload);
1288
- const clean = hex.startsWith("0x") ? hex.slice(2) : hex;
1289
- if (clean.length % 2 !== 0) return null;
1290
- const bytes = new Uint8Array(clean.length / 2);
1291
- for (let i = 0; i < bytes.length; i++) {
1292
- bytes[i] = parseInt(clean.slice(i * 2, i * 2 + 2), 16);
1293
- }
1294
- return bytes;
1274
+ return fromHex(base64UrlDecode(payload));
1295
1275
  } catch {
1296
1276
  return null;
1297
1277
  }
1298
1278
  }
1299
1279
 
1300
1280
  // src/protocol/eph/selfEph.ts
1301
- var import_sha25 = require("@noble/hashes/sha2.js");
1281
+ var import_sha24 = require("@noble/hashes/sha2.js");
1282
+
1283
+ // src/protocol/eph/windowBounds.ts
1284
+ var MAX_EPH_WINDOW = 1 << 20;
1285
+
1286
+ // src/protocol/eph/selfEph.ts
1302
1287
  var import_baby_jubjub5 = require("@zk-kit/baby-jubjub");
1303
- var SELF_EPH_DOMAIN = new TextEncoder().encode("orbinum-self-eph-v1");
1304
- function deriveSelfEphSk(spendingKey, index) {
1305
- const h = import_sha25.sha256.create();
1288
+ var SELF_EPH_DOMAIN = new TextEncoder().encode("orbinum-self-eph-v3");
1289
+ function deriveSelfEphSk(viewingSecretKey, index) {
1290
+ if (!Number.isInteger(index) || index < 0 || index > 4294967295) {
1291
+ throw new Error(`deriveSelfEphSk: index must be a u32, got ${index}`);
1292
+ }
1293
+ assertSecretKeyBytes(viewingSecretKey, "deriveSelfEphSk: viewingSecretKey");
1294
+ const h = import_sha24.sha256.create();
1306
1295
  h.update(SELF_EPH_DOMAIN);
1307
- h.update(bigintTo32Le(spendingKey));
1296
+ h.update(viewingSecretKey);
1308
1297
  const idx = new Uint8Array(4);
1309
1298
  new DataView(idx.buffer).setUint32(0, index >>> 0, true);
1310
1299
  h.update(idx);
1311
1300
  return h.digest();
1312
1301
  }
1313
- function selfEphWindow(spendingKey, ivkPacked, from, count) {
1302
+ function selfEphWindow(viewingSecretKey, ivkPacked, from, count) {
1314
1303
  const ivkPoint = (0, import_baby_jubjub5.unpackPoint)(bytesToBigintLE(ivkPacked));
1315
1304
  if (!ivkPoint) throw new Error("selfEphWindow: invalid viewing public key");
1316
1305
  const entries = [];
1306
+ if (!Number.isInteger(from) || from < 0) {
1307
+ throw new Error(`selfEphWindow: from must be a non-negative integer, got ${from}`);
1308
+ }
1309
+ if (!Number.isInteger(count) || count > MAX_EPH_WINDOW) {
1310
+ throw new Error(
1311
+ `selfEphWindow: count must be an integer <= ${MAX_EPH_WINDOW}, got ${count}`
1312
+ );
1313
+ }
1317
1314
  for (let i = from; i < from + count; i++) {
1318
- const scalar = bytesToBjjScalar(deriveSelfEphSk(spendingKey, i));
1315
+ const scalar = bytesToBjjScalar(deriveSelfEphSk(viewingSecretKey, i));
1319
1316
  const ephPk = fastMulBase(scalar);
1320
1317
  const sharedPoint = fastMulPoint(ivkPoint, scalar);
1321
1318
  entries.push({
@@ -1328,17 +1325,22 @@ function selfEphWindow(spendingKey, ivkPacked, from, count) {
1328
1325
  }
1329
1326
 
1330
1327
  // src/protocol/eph/pairwiseEph.ts
1331
- var import_sha26 = require("@noble/hashes/sha2.js");
1328
+ var import_sha25 = require("@noble/hashes/sha2.js");
1332
1329
  var import_baby_jubjub6 = require("@zk-kit/baby-jubjub");
1333
1330
  var PAIRWISE_EPH_DOMAIN = new TextEncoder().encode("orbinum-pairwise-eph-v1");
1334
1331
  function derivePairwiseSharedSecret(myViewingSk, theirIvkPacked) {
1335
- const theirPoint = (0, import_baby_jubjub6.unpackPoint)(bytesToBigintLE(theirIvkPacked));
1332
+ const theirPoint = unpackUsableViewingKey(bytesToBigintLE(theirIvkPacked));
1336
1333
  if (!theirPoint) throw new Error("derivePairwiseSharedSecret: invalid viewing public key");
1334
+ assertSecretKeyBytes(myViewingSk, "derivePairwiseSharedSecret: myViewingSk");
1337
1335
  const shared = fastMulPoint(theirPoint, bytesToBjjScalar(myViewingSk));
1338
1336
  return bigintTo32Le(shared[0]);
1339
1337
  }
1340
1338
  function derivePairwiseEphSk(pairSecret, index) {
1341
- const h = import_sha26.sha256.create();
1339
+ if (!Number.isInteger(index) || index < 0 || index > 4294967295) {
1340
+ throw new Error(`derivePairwiseEphSk: index must be a u32, got ${index}`);
1341
+ }
1342
+ assertSecretKeyBytes(pairSecret, "derivePairwiseEphSk: pairSecret");
1343
+ const h = import_sha25.sha256.create();
1342
1344
  h.update(PAIRWISE_EPH_DOMAIN);
1343
1345
  h.update(pairSecret);
1344
1346
  const idx = new Uint8Array(4);
@@ -1350,6 +1352,14 @@ function pairwiseEphWindow(pairSecret, receiverIvkPacked, from, count) {
1350
1352
  const ivkPoint = (0, import_baby_jubjub6.unpackPoint)(bytesToBigintLE(receiverIvkPacked));
1351
1353
  if (!ivkPoint) throw new Error("pairwiseEphWindow: invalid viewing public key");
1352
1354
  const entries = [];
1355
+ if (!Number.isInteger(from) || from < 0) {
1356
+ throw new Error(`pairwiseEphWindow: from must be a non-negative integer, got ${from}`);
1357
+ }
1358
+ if (!Number.isInteger(count) || count > MAX_EPH_WINDOW) {
1359
+ throw new Error(
1360
+ `pairwiseEphWindow: count must be an integer <= ${MAX_EPH_WINDOW}, got ${count}`
1361
+ );
1362
+ }
1353
1363
  for (let i = from; i < from + count; i++) {
1354
1364
  const scalar = bytesToBjjScalar(derivePairwiseEphSk(pairSecret, i));
1355
1365
  const ephPk = fastMulBase(scalar);
@@ -1363,12 +1373,64 @@ function pairwiseEphWindow(pairSecret, receiverIvkPacked, from, count) {
1363
1373
  return entries;
1364
1374
  }
1365
1375
 
1376
+ // src/protocol/eph/outgoingEph.ts
1377
+ var import_sha26 = require("@noble/hashes/sha2.js");
1378
+ var import_baby_jubjub7 = require("@zk-kit/baby-jubjub");
1379
+ var OUTGOING_EPH_DOMAIN = new TextEncoder().encode("orbinum-outgoing-eph-v3");
1380
+ function deriveOutgoingEphSk(outgoingViewingKey, index) {
1381
+ if (!Number.isInteger(index) || index < 0 || index > 4294967295) {
1382
+ throw new Error(`deriveOutgoingEphSk: index must be a u32, got ${index}`);
1383
+ }
1384
+ assertSecretKeyBytes(outgoingViewingKey, "deriveOutgoingEphSk: outgoingViewingKey");
1385
+ const h = import_sha26.sha256.create();
1386
+ h.update(OUTGOING_EPH_DOMAIN);
1387
+ h.update(outgoingViewingKey);
1388
+ const idx = new Uint8Array(4);
1389
+ new DataView(idx.buffer).setUint32(0, index >>> 0, true);
1390
+ h.update(idx);
1391
+ return h.digest();
1392
+ }
1393
+ function deriveOutgoingEphPk(outgoingViewingKey, index) {
1394
+ const scalar = bytesToBjjScalar(deriveOutgoingEphSk(outgoingViewingKey, index));
1395
+ return toHex(bigintTo32Le((0, import_baby_jubjub7.packPoint)(fastMulBase(scalar)))).toLowerCase();
1396
+ }
1397
+ function outgoingEphWindow(outgoingViewingKey, from, count) {
1398
+ const entries = [];
1399
+ if (!Number.isInteger(from) || from < 0) {
1400
+ throw new Error(`outgoingEphWindow: from must be a non-negative integer, got ${from}`);
1401
+ }
1402
+ if (!Number.isInteger(count) || count > MAX_EPH_WINDOW) {
1403
+ throw new Error(
1404
+ `outgoingEphWindow: count must be an integer <= ${MAX_EPH_WINDOW}, got ${count}`
1405
+ );
1406
+ }
1407
+ for (let i = from; i < from + count; i++) {
1408
+ entries.push({ index: i, ephPkHex: deriveOutgoingEphPk(outgoingViewingKey, i) });
1409
+ }
1410
+ return entries;
1411
+ }
1412
+ function deriveOutgoingSharedSecret(outgoingViewingKey, index, recipientIvkPacked) {
1413
+ const ivkPoint = (0, import_baby_jubjub7.unpackPoint)(bytesToBigintLE(recipientIvkPacked));
1414
+ if (!ivkPoint) throw new Error("deriveOutgoingSharedSecret: invalid viewing public key");
1415
+ const scalar = bytesToBjjScalar(deriveOutgoingEphSk(outgoingViewingKey, index));
1416
+ return bigintTo32Le(fastMulPoint(ivkPoint, scalar)[0]);
1417
+ }
1418
+ function reconstructOutgoingIndex(outgoingViewingKey, publishedEphPks, window) {
1419
+ const published = /* @__PURE__ */ new Set();
1420
+ for (const hex of publishedEphPks) published.add(hex.toLowerCase());
1421
+ let highest = -1;
1422
+ for (let i = 0; i < window; i++) {
1423
+ if (published.has(deriveOutgoingEphPk(outgoingViewingKey, i))) highest = i;
1424
+ }
1425
+ return highest + 1;
1426
+ }
1427
+
1366
1428
  // src/protocol/types.ts
1367
1429
  var CURRENT_CIRCUIT_VERSION = 1;
1368
1430
 
1369
1431
  // src/protocol/note/NoteBuilder.ts
1370
- var import_baby_jubjub7 = require("@zk-kit/baby-jubjub");
1371
- var import_utils4 = require("@noble/ciphers/utils.js");
1432
+ var import_baby_jubjub8 = require("@zk-kit/baby-jubjub");
1433
+ var import_utils3 = require("@noble/ciphers/utils.js");
1372
1434
  var import_poseidon_lite = require("poseidon-lite");
1373
1435
  var NoteBuilder = class {
1374
1436
  /**
@@ -1377,7 +1439,7 @@ var NoteBuilder = class {
1377
1439
  * @param input.value Amount in planck (required).
1378
1440
  * @param input.assetId Asset ID — default 0n (native ORB-Privacy).
1379
1441
  * @param input.ownerPk Sender's or recipient's global BabyJubJub Ax — default 0n.
1380
- * @param input.blinding Random scalar — defaults to BigInt(Date.now()).
1442
+ * @param input.blinding Random scalar — defaults to a CSPRNG draw.
1381
1443
  * @param input.spendingKey Secret key for nullifier — default 0n.
1382
1444
  * @param input.viewingPublicKey Recipient's 32-byte LE packed BJJ ivk. Triggers memo encryption.
1383
1445
  * @param input.recipientOwnerPk Recipient's global ownerPk. Required with viewingPublicKey
@@ -1388,7 +1450,7 @@ var NoteBuilder = class {
1388
1450
  const value = input.value;
1389
1451
  const assetId = input.assetId ?? 0n;
1390
1452
  const ownerPk = input.ownerPk ?? 0n;
1391
- const blinding = input.blinding ?? BigInt(Date.now());
1453
+ const blinding = input.blinding ?? randomBlinding();
1392
1454
  const spendingKey = input.spendingKey ?? 0n;
1393
1455
  const sourcePk = input.sourcePk ?? 0n;
1394
1456
  const circuitVersion = input.circuitVersion ?? CURRENT_CIRCUIT_VERSION;
@@ -1397,13 +1459,13 @@ var NoteBuilder = class {
1397
1459
  if (useStealth) {
1398
1460
  const recipientOwnerPk = input.recipientOwnerPk;
1399
1461
  const recipientIvkPacked = input.viewingPublicKey;
1400
- const ephSk = input.ephSkOverride ?? (0, import_utils4.randomBytes)(32);
1462
+ const ephSk = input.ephSkOverride ?? (0, import_utils3.randomBytes)(32);
1401
1463
  const ivkPackedBigint = bytesToBigintLE(recipientIvkPacked);
1402
- const ivkPoint = (0, import_baby_jubjub7.unpackPoint)(ivkPackedBigint);
1464
+ const ivkPoint = unpackUsableViewingKey(ivkPackedBigint);
1403
1465
  if (!ivkPoint)
1404
1466
  throw new Error("NoteBuilder.build: invalid recipient viewing public key");
1405
1467
  const ephSkScalar = BigInt(toHex(ephSk)) % BABYJUB_SUBORDER || 1n;
1406
- const sharedPoint = (0, import_baby_jubjub7.mulPointEscalar)(ivkPoint, ephSkScalar);
1468
+ const sharedPoint = (0, import_baby_jubjub8.mulPointEscalar)(ivkPoint, ephSkScalar);
1407
1469
  const sharedSecret = bigintTo32Le(sharedPoint[0]);
1408
1470
  const recipientPkPoint = recoverOwnerPkPoint(recipientOwnerPk);
1409
1471
  if (!recipientPkPoint)
@@ -1438,21 +1500,6 @@ var NoteBuilder = class {
1438
1500
  throw new Error(
1439
1501
  `NoteBuilder.build: invariant violated \u2014 memo must be ${ENCRYPTED_MEMO_SIZE} bytes, got ${memo.length}`
1440
1502
  );
1441
- let ovkBlob;
1442
- if (input.outgoingViewingKey !== void 0) {
1443
- const ephPkBytes = Uint8Array.from(memo.slice(148, 180));
1444
- const sealed = sealOutgoingBlob(
1445
- input.outgoingViewingKey,
1446
- sharedSecret,
1447
- stealthCommitmentBytes,
1448
- ephPkBytes
1449
- );
1450
- if (sealed.length !== OVK_BLOB_SIZE)
1451
- throw new Error(
1452
- `NoteBuilder.build: invariant violated \u2014 ovkBlob must be ${OVK_BLOB_SIZE} bytes, got ${sealed.length}`
1453
- );
1454
- ovkBlob = Array.from(sealed);
1455
- }
1456
1503
  return {
1457
1504
  value,
1458
1505
  assetId,
@@ -1467,8 +1514,7 @@ var NoteBuilder = class {
1467
1514
  commitmentHex: toHex(commitmentBytes2),
1468
1515
  nullifierHex: toHex(nullifierBytes2),
1469
1516
  memo,
1470
- sourcePk,
1471
- ...ovkBlob ? { ovkBlob } : {}
1517
+ sourcePk
1472
1518
  };
1473
1519
  }
1474
1520
  const commitment = (0, import_poseidon_lite.poseidon4)([value, assetId, ownerPk, blinding]);
@@ -1518,7 +1564,7 @@ var NoteBuilder = class {
1518
1564
  * @param note The ZkNote whose fields populate the plaintext.
1519
1565
  * @param recipientIvkPacked 32-byte LE packed BJJ viewing public key of the recipient.
1520
1566
  * Pass `new Uint8Array(32)` (default) for a public/dummy memo.
1521
- * @param sourcePk 32-byte counterparty BabyJubJub Ax.
1567
+ * @param sourcePk 32-byte counterparty BabyJubJub Ax.
1522
1568
  * Pass `new Uint8Array(32)` (default) for no counterparty.
1523
1569
  */
1524
1570
  static buildMemo(note, recipientIvkPacked, sourcePk) {
@@ -1537,18 +1583,6 @@ var NoteBuilder = class {
1537
1583
 
1538
1584
  // src/protocol/note/NoteDecryptor.ts
1539
1585
  var import_poseidon_lite2 = require("poseidon-lite");
1540
- function decryptAndVerifyPlaintext(memoBytes, commitmentBytes, sharedSecret, effectiveOwnerPk) {
1541
- const plaintext = EncryptedMemo.decryptWithSharedSecret(
1542
- memoBytes,
1543
- commitmentBytes,
1544
- sharedSecret
1545
- );
1546
- if (!plaintext) return null;
1547
- const ownerPk = effectiveOwnerPk ?? plaintext.ownerPk;
1548
- const recomputed = (0, import_poseidon_lite2.poseidon4)([plaintext.value, plaintext.assetId, ownerPk, plaintext.blinding]);
1549
- if (recomputed !== bytesToBigintLE(commitmentBytes)) return null;
1550
- return plaintext;
1551
- }
1552
1586
  function computeNullifier(commitment, spendingKey) {
1553
1587
  return (0, import_poseidon_lite2.poseidon2)([commitment, spendingKey]);
1554
1588
  }
@@ -1631,40 +1665,6 @@ function tryDecryptNoteVerbose(commitment, viewingSecretKey, spendingKey, ownOwn
1631
1665
  }
1632
1666
  };
1633
1667
  }
1634
- function tryRecoverOutgoing(hint, ovk, opts) {
1635
- if (!hint.ovkBlob || !hint.encryptedMemo) return null;
1636
- let commitmentBytes;
1637
- let memoBytes;
1638
- let blobBytes;
1639
- try {
1640
- commitmentBytes = fromHex(hint.commitmentHex);
1641
- memoBytes = fromHex(hint.encryptedMemo);
1642
- blobBytes = fromHex(hint.ovkBlob);
1643
- } catch {
1644
- return null;
1645
- }
1646
- if (memoBytes.length !== ENCRYPTED_MEMO_SIZE) return null;
1647
- if (blobBytes.length !== OVK_BLOB_SIZE) return null;
1648
- const ephPkBytes = memoBytes.subarray(148, 180);
1649
- const sharedSecret = openOutgoingBlob(ovk, blobBytes, commitmentBytes, ephPkBytes);
1650
- if (!sharedSecret) return null;
1651
- const activation = opts?.viewTagActivationLeaf;
1652
- if (activation !== void 0 && isValidLeafIndex(hint.leafIndex) && hint.leafIndex >= activation && !EncryptedMemo.checkViewTag(memoBytes, sharedSecret)) {
1653
- return null;
1654
- }
1655
- const plaintext = decryptAndVerifyPlaintext(memoBytes, commitmentBytes, sharedSecret, null);
1656
- if (!plaintext) return null;
1657
- return {
1658
- commitmentHex: hint.commitmentHex,
1659
- ...isValidLeafIndex(hint.leafIndex) ? { leafIndex: hint.leafIndex } : {},
1660
- value: plaintext.value,
1661
- assetId: plaintext.assetId,
1662
- recipientStealthPk: plaintext.ownerPk,
1663
- blinding: plaintext.blinding,
1664
- sourcePk: plaintext.sourcePk,
1665
- circuitVersion: plaintext.circuitVersion
1666
- };
1667
- }
1668
1668
  function collectOutgoingFacts(hint) {
1669
1669
  if (!isHexOfLength(hint.commitmentHex, 32)) return null;
1670
1670
  if (!isHexOfLength(hint.encryptedMemo, ENCRYPTED_MEMO_SIZE)) return null;
@@ -1675,6 +1675,89 @@ function collectOutgoingFacts(hint) {
1675
1675
  };
1676
1676
  }
1677
1677
 
1678
+ // src/protocol/note/recoverSent.ts
1679
+ function recoverSentFromSharedSecret(hint, outgoingSharedSecret, ephIndex) {
1680
+ if (!isHexOfLength(hint.commitmentHex, 32)) return null;
1681
+ if (!isHexOfLength(hint.encryptedMemo, ENCRYPTED_MEMO_SIZE)) return null;
1682
+ try {
1683
+ const plaintext = EncryptedMemo.decryptWithSharedSecret(
1684
+ fromHex(hint.encryptedMemo),
1685
+ fromHex(hint.commitmentHex),
1686
+ outgoingSharedSecret
1687
+ );
1688
+ if (!plaintext) return null;
1689
+ return {
1690
+ commitmentHex: hint.commitmentHex,
1691
+ ...isValidLeafIndex(hint.leafIndex) ? { leafIndex: hint.leafIndex } : {},
1692
+ value: plaintext.value,
1693
+ assetId: plaintext.assetId,
1694
+ // On an outgoing note the memo's ownerPk IS the recipient's stealth
1695
+ // owner — the builder wrote it that way.
1696
+ recipientStealthPk: plaintext.ownerPk,
1697
+ blinding: plaintext.blinding,
1698
+ sourcePk: plaintext.sourcePk,
1699
+ circuitVersion: plaintext.circuitVersion,
1700
+ ephIndex
1701
+ };
1702
+ } catch {
1703
+ return null;
1704
+ }
1705
+ }
1706
+ function recoverSentNote(params) {
1707
+ const { hint, outgoingViewingKey, ephIndex, recipientCandidates } = params;
1708
+ if (!isHexOfLength(hint.commitmentHex, 32)) return null;
1709
+ if (!isHexOfLength(hint.encryptedMemo, ENCRYPTED_MEMO_SIZE)) return null;
1710
+ for (const candidate of recipientCandidates) {
1711
+ let sharedSecret;
1712
+ try {
1713
+ sharedSecret = deriveOutgoingSharedSecret(outgoingViewingKey, ephIndex, candidate);
1714
+ } catch {
1715
+ continue;
1716
+ }
1717
+ const facts = recoverSentFromSharedSecret(hint, sharedSecret, ephIndex);
1718
+ if (facts) return { ...facts, recipientIvk: candidate };
1719
+ }
1720
+ return null;
1721
+ }
1722
+
1723
+ // src/protocol/note/recipientBook.ts
1724
+ var import_sha27 = require("@noble/hashes/sha2.js");
1725
+ var RECIPIENT_BOOK_DOMAIN = new TextEncoder().encode("orbinum-recipient-book-v3");
1726
+ var IVK_SIZE = 32;
1727
+ function bookKeystream(outgoingViewingKey, paymentCommitmentHex) {
1728
+ if (outgoingViewingKey.length !== 32) {
1729
+ throw new Error(
1730
+ `recipientBook: outgoingViewingKey must be 32 bytes, got ${outgoingViewingKey.length}`
1731
+ );
1732
+ }
1733
+ if (outgoingViewingKey.every((b) => b === 0)) {
1734
+ throw new Error(
1735
+ "recipientBook: outgoingViewingKey is all zeros \u2014 the book would be public"
1736
+ );
1737
+ }
1738
+ const normalised = paymentCommitmentHex.toLowerCase().replace(/^0x/, "");
1739
+ const h = import_sha27.sha256.create();
1740
+ h.update(RECIPIENT_BOOK_DOMAIN);
1741
+ h.update(outgoingViewingKey);
1742
+ h.update(new TextEncoder().encode(normalised));
1743
+ return h.digest();
1744
+ }
1745
+ function sealRecipientBookEntry(ivkPacked, outgoingViewingKey, paymentCommitmentHex) {
1746
+ if (ivkPacked.length !== IVK_SIZE) return ivkPacked;
1747
+ const keystream = bookKeystream(outgoingViewingKey, paymentCommitmentHex);
1748
+ const out = new Uint8Array(IVK_SIZE);
1749
+ for (let i = 0; i < IVK_SIZE; i++) out[i] = ivkPacked[i] ^ keystream[i];
1750
+ return out;
1751
+ }
1752
+ function openRecipientBookEntry(sealedSourcePk, outgoingViewingKey, paymentCommitmentHex) {
1753
+ if (sealedSourcePk < 0n || sealedSourcePk >> 256n !== 0n) return new Uint8Array(IVK_SIZE);
1754
+ return sealRecipientBookEntry(
1755
+ bigintTo32Le(sealedSourcePk),
1756
+ outgoingViewingKey,
1757
+ paymentCommitmentHex
1758
+ );
1759
+ }
1760
+
1678
1761
  // src/protocol/note/NoteDisclosure.ts
1679
1762
  var import_poseidon_lite3 = require("poseidon-lite");
1680
1763
  var PREFIX = "orbdisc:";
@@ -1819,11 +1902,10 @@ ${canonicalAccountId(address)}`;
1819
1902
  }
1820
1903
 
1821
1904
  // src/protocol/keys/PrivacyKeys.ts
1822
- var import_hkdf4 = require("@noble/hashes/hkdf.js");
1823
- var import_sha27 = require("@noble/hashes/sha2.js");
1824
- var import_baby_jubjub8 = require("@zk-kit/baby-jubjub");
1905
+ var import_hkdf3 = require("@noble/hashes/hkdf.js");
1906
+ var import_sha28 = require("@noble/hashes/sha2.js");
1907
+ var import_baby_jubjub9 = require("@zk-kit/baby-jubjub");
1825
1908
  var IVK_DOMAIN = new TextEncoder().encode("orbinum-ivk-v1");
1826
- var OVK_DOMAIN = new TextEncoder().encode("orbinum-ovk-v1");
1827
1909
  var KEY_VERSION = "v2";
1828
1910
  function deriveSpendingKeyFromMaster(masterBytes) {
1829
1911
  const skBigint = BigInt(toHex(masterBytes)) % BABYJUB_SUBORDER;
@@ -1831,12 +1913,26 @@ function deriveSpendingKeyFromMaster(masterBytes) {
1831
1913
  }
1832
1914
  function deriveViewingSecretKey(spendingKey) {
1833
1915
  const ikm = bigintTo32Le(spendingKey);
1834
- return (0, import_hkdf4.hkdf)(import_sha27.sha256, ikm, void 0, IVK_DOMAIN, 32);
1916
+ return (0, import_hkdf3.hkdf)(import_sha28.sha256, ikm, void 0, IVK_DOMAIN, 32);
1917
+ }
1918
+ var SPEND_V3_DOMAIN = new TextEncoder().encode("orbinum-spend-v3");
1919
+ var IVK_V3_DOMAIN = new TextEncoder().encode("orbinum-ivk-v3");
1920
+ var OVK_V3_DOMAIN = new TextEncoder().encode("orbinum-ovk-v3");
1921
+ var deriveBranch = (rootSecret, domain) => (0, import_hkdf3.hkdf)(import_sha28.sha256, rootSecret, void 0, domain, 32);
1922
+ function deriveSpendingKeyV3(rootSecret) {
1923
+ const scalar = BigInt(toHex(deriveBranch(rootSecret, SPEND_V3_DOMAIN))) % BABYJUB_SUBORDER;
1924
+ return scalar === 0n ? 1n : scalar;
1925
+ }
1926
+ function deriveViewingSecretKeyV3(rootSecret) {
1927
+ return deriveBranch(rootSecret, IVK_V3_DOMAIN);
1928
+ }
1929
+ function deriveOutgoingViewingKeyV3(rootSecret) {
1930
+ return deriveBranch(rootSecret, OVK_V3_DOMAIN);
1835
1931
  }
1836
1932
  function deriveViewingPublicKey(ivsk) {
1837
1933
  const ivskScalar = BigInt(toHex(ivsk)) % BABYJUB_SUBORDER || 1n;
1838
1934
  const ivkPoint = fastMulBase(ivskScalar);
1839
- const packed = (0, import_baby_jubjub8.packPoint)(ivkPoint);
1935
+ const packed = (0, import_baby_jubjub9.packPoint)(ivkPoint);
1840
1936
  return bigintTo32Le(packed);
1841
1937
  }
1842
1938
  function deriveOwnerPk(spendingKey) {
@@ -1847,15 +1943,21 @@ function deriveOwnerPk(spendingKey) {
1847
1943
  return 0n;
1848
1944
  }
1849
1945
  }
1850
- function deriveOutgoingViewingKey(masterBytes) {
1851
- return (0, import_hkdf4.hkdf)(import_sha27.sha256, masterBytes, void 0, OVK_DOMAIN, 32);
1852
- }
1853
1946
 
1854
1947
  // src/protocol/keys/PrivacyKeyManager.ts
1855
- var import_sha28 = require("@noble/hashes/sha2.js");
1856
- function privacyAddressChecksum(ownerPkHex, ivkHex) {
1857
- const body = `orbpriv2:${ownerPkHex}:${ivkHex}`;
1858
- const digest = (0, import_sha28.sha256)(new TextEncoder().encode(body));
1948
+ var import_sha29 = require("@noble/hashes/sha2.js");
1949
+ var SCHEME_BY_VERSION = { v2: "orbpriv2", v3: "orbpriv3" };
1950
+ function usableAddressFields(ownerPkHex, ivkHex) {
1951
+ const ownerPk = ownerPkHex.toLowerCase();
1952
+ const ivk = ivkHex.toLowerCase();
1953
+ if (!isHexOfLength(ownerPk, 32) || !isHexOfLength(ivk, 32)) return null;
1954
+ if (/^0x0+$/.test(ownerPk) || /^0x0+$/.test(ivk)) return null;
1955
+ if (!unpackUsableViewingKey(bytesToBigintLE(fromHex(ivk)))) return null;
1956
+ return { ownerPkHex: ownerPk, viewingPublicKeyHex: ivk };
1957
+ }
1958
+ function privacyAddressChecksum(scheme, ownerPkHex, ivkHex) {
1959
+ const body = `${scheme}:${ownerPkHex}:${ivkHex}`;
1960
+ const digest = (0, import_sha29.sha256)(new TextEncoder().encode(body));
1859
1961
  return toHex(digest.slice(0, 4)).slice(2);
1860
1962
  }
1861
1963
  var PrivacyKeyManager = class {
@@ -1864,30 +1966,29 @@ var PrivacyKeyManager = class {
1864
1966
  masterBytes: null,
1865
1967
  viewingSecretKey: null,
1866
1968
  viewingPublicKeyPacked: null,
1867
- ownerPk: null,
1868
- outgoingViewingKey: null
1969
+ ownerPk: null
1869
1970
  };
1870
1971
  /**
1871
1972
  * Load a spending key and its corresponding master bytes into the in-memory session.
1872
1973
  * Derives viewingSecretKey, viewingPublicKeyPacked, and ownerPk immediately.
1873
1974
  * Replaces any previously loaded key.
1874
1975
  *
1875
- * @param spendingKey Circuit scalar: BigInt(masterBytes) % BABYJUB_SUBORDER, clamped to [1, ∞).
1876
- * @param masterBytes Raw 32-byte HKDF output before modular reduction. Used to derive
1877
- * the stable vault key (HKDF(masterBytes, info="orbinum-vault-key-v1")).
1976
+ * @param _legacySpendingKey IGNORED. Kept so v2-era callers still compile;
1977
+ * the scalar is re-derived from `masterBytes`.
1978
+ * @param masterBytes Raw 32-byte HKDF output. The root every v3 branch and
1979
+ * the stable vault key hang off.
1878
1980
  */
1879
- async load(spendingKey, masterBytes) {
1880
- const viewingSecretKey = deriveViewingSecretKey(spendingKey);
1981
+ async load(_legacySpendingKey, masterBytes) {
1982
+ const spendingKey = deriveSpendingKeyV3(masterBytes);
1983
+ const viewingSecretKey = deriveViewingSecretKeyV3(masterBytes);
1881
1984
  const viewingPublicKeyPacked = deriveViewingPublicKey(viewingSecretKey);
1882
1985
  const ownerPk = deriveOwnerPk(spendingKey);
1883
- const outgoingViewingKey = deriveOutgoingViewingKey(masterBytes);
1884
1986
  this._state = {
1885
1987
  spendingKey,
1886
1988
  masterBytes,
1887
1989
  viewingSecretKey,
1888
1990
  viewingPublicKeyPacked,
1889
- ownerPk,
1890
- outgoingViewingKey
1991
+ ownerPk
1891
1992
  };
1892
1993
  }
1893
1994
  /** Clear all key material from memory. Call on vault lock / sign-out. */
@@ -1897,8 +1998,7 @@ var PrivacyKeyManager = class {
1897
1998
  masterBytes: null,
1898
1999
  viewingSecretKey: null,
1899
2000
  viewingPublicKeyPacked: null,
1900
- ownerPk: null,
1901
- outgoingViewingKey: null
2001
+ ownerPk: null
1902
2002
  };
1903
2003
  }
1904
2004
  /** Returns true if a spending key has been loaded. */
@@ -1936,6 +2036,24 @@ var PrivacyKeyManager = class {
1936
2036
  }
1937
2037
  return this._state.viewingPublicKeyPacked;
1938
2038
  }
2039
+ /**
2040
+ * The OUTGOING viewing key (ovk) — reads what this wallet SENT.
2041
+ *
2042
+ * Seeds the outgoing ephemeral sequence and opens the recipient book, so a
2043
+ * holder can enumerate every payment the wallet made and name who received
2044
+ * it. That makes it strictly more sensitive than the incoming viewing key,
2045
+ * which yields amounts and no counterparties: hand it out only where the
2046
+ * payment graph is meant to travel with it.
2047
+ *
2048
+ * A scan without it still finds every note the wallet OWNS and simply
2049
+ * reconstructs no sent history — the watch-only case.
2050
+ *
2051
+ * SECURITY: never expose this in an address or a network request.
2052
+ * Throws if not loaded.
2053
+ */
2054
+ getOutgoingViewingKey() {
2055
+ return deriveOutgoingViewingKeyV3(this.getMasterBytes());
2056
+ }
1939
2057
  /** Returns the BabyJubJub owner public key (x-coordinate). Throws if not loaded. */
1940
2058
  getOwnerPk() {
1941
2059
  if (this._state.ownerPk === null) {
@@ -1943,16 +2061,6 @@ var PrivacyKeyManager = class {
1943
2061
  }
1944
2062
  return this._state.ownerPk;
1945
2063
  }
1946
- /**
1947
- * Returns the 32-byte outgoing viewing key (ovk). Throws if not loaded.
1948
- * Used to seal/open the outgoing blob that lets the sender recover a transfer.
1949
- */
1950
- getOutgoingViewingKey() {
1951
- if (this._state.outgoingViewingKey === null) {
1952
- throw new Error("PrivacyKeyManager: no key loaded. Call load() first.");
1953
- }
1954
- return this._state.outgoingViewingKey;
1955
- }
1956
2064
  /** Returns the spending key as a 32-byte little-endian Uint8Array. Throws if not loaded. */
1957
2065
  getSpendingKeyBytes() {
1958
2066
  return bigintTo32Le(this.getSpendingKey());
@@ -1981,7 +2089,7 @@ var PrivacyKeyManager = class {
1981
2089
  * Exports a shareable privacy address encoding the owner public key and
1982
2090
  * viewing PUBLIC key of the currently loaded identity.
1983
2091
  *
1984
- * Format: `orbpriv2:{ownerPk_hex}:{viewingPublicKey_hex}:{checksum}`
2092
+ * Format: `orbpriv3:{ownerPk_hex}:{viewingPublicKey_hex}:{checksum}`
1985
2093
  *
1986
2094
  * The recipient uses this address so the sender can:
1987
2095
  * 1. Embed `ownerPk` in the note commitment (Poseidon4 input).
@@ -1996,44 +2104,64 @@ var PrivacyKeyManager = class {
1996
2104
  *
1997
2105
  * Throws if no key is loaded.
1998
2106
  */
1999
- encodePrivacyAddress() {
2107
+ encodePrivacyAddress(version = "v3") {
2000
2108
  const ownerPk = this.getOwnerPk();
2001
2109
  const ivkPacked = this.getViewingPublicKeyPacked();
2002
2110
  const ownerPkHex = scalarToHex(ownerPk);
2003
2111
  const ivkHex = toHex(ivkPacked);
2004
- const checksum = privacyAddressChecksum(ownerPkHex, ivkHex);
2005
- return `orbpriv2:${ownerPkHex}:${ivkHex}:${checksum}`;
2112
+ const scheme = SCHEME_BY_VERSION[version];
2113
+ const checksum = privacyAddressChecksum(scheme, ownerPkHex, ivkHex);
2114
+ return `${scheme}:${ownerPkHex}:${ivkHex}:${checksum}`;
2006
2115
  }
2007
2116
  /**
2008
2117
  * Decode a privacy address into `{ ownerPkHex, viewingPublicKeyHex }`, or
2009
2118
  * `null` if it does not parse.
2010
2119
  *
2011
- * Accepts both:
2012
- * - `orbpriv2:{ownerPk}:{ivk}:{checksum}` — checksum verified; a mismatch
2013
- * (corrupted paste) returns null.
2014
- * - `orbpriv1:{ownerPk}:{ivk}` — legacy, no checksum. Still read so addresses
2015
- * shared before v2 keep resolving; only v2 is emitted.
2120
+ * Reads every scheme ever emitted; only `orbpriv3` is written.
2121
+ *
2122
+ * - `orbpriv3` / `orbpriv2` — checksum verified; a mismatch (corrupted
2123
+ * paste, or a relabelled prefix) returns null.
2124
+ * - `orbpriv1` legacy, predates the checksum entirely. The FIELDS are
2125
+ * validated exactly as above, since this branch has no integrity check
2126
+ * of its own.
2127
+ *
2128
+ * Reading an old address is not the same as supporting an old identity. An
2129
+ * address is an owner key and a viewing key; paying one works whatever
2130
+ * scheme derived it, and whoever shares it may not have upgraded yet.
2131
+ * Refusing them here would break incoming payments for no gain.
2016
2132
  */
2017
2133
  static decodePrivacyAddress(address) {
2018
2134
  const parts = address.split(":");
2019
- if (parts[0] === "orbpriv2") {
2135
+ for (const version of ["v2", "v3"]) {
2136
+ if (parts[0] !== SCHEME_BY_VERSION[version]) continue;
2020
2137
  if (parts.length !== 4) return null;
2021
2138
  const [, ownerPkHex, viewingPublicKeyHex, checksum] = parts;
2022
2139
  if (!ownerPkHex || !viewingPublicKeyHex || !checksum) return null;
2023
- if (privacyAddressChecksum(ownerPkHex, viewingPublicKeyHex) !== checksum) return null;
2024
- return { ownerPkHex, viewingPublicKeyHex };
2140
+ if (privacyAddressChecksum(
2141
+ SCHEME_BY_VERSION[version],
2142
+ ownerPkHex,
2143
+ viewingPublicKeyHex
2144
+ ) !== checksum) {
2145
+ return null;
2146
+ }
2147
+ const fields = usableAddressFields(ownerPkHex, viewingPublicKeyHex);
2148
+ return fields ? { ...fields, scheme: SCHEME_BY_VERSION[version] } : null;
2025
2149
  }
2026
2150
  if (parts[0] === "orbpriv1") {
2027
2151
  if (parts.length !== 3) return null;
2028
2152
  const [, ownerPkHex, viewingPublicKeyHex] = parts;
2029
2153
  if (!ownerPkHex || !viewingPublicKeyHex) return null;
2030
- return { ownerPkHex, viewingPublicKeyHex };
2154
+ const fields = usableAddressFields(ownerPkHex, viewingPublicKeyHex);
2155
+ return fields ? { ...fields, scheme: "orbpriv1" } : null;
2031
2156
  }
2032
2157
  return null;
2033
2158
  }
2034
2159
  /**
2035
2160
  * Load keys from a cached "mk:0x{masterBytes_hex}" string produced by exportHex().
2036
- * Throws if the format is invalid or masterBytes length is not 32 bytes.
2161
+ *
2162
+ * Throws on a bad prefix, on non-hex content, and on a master that is not
2163
+ * 32 usable bytes — an all-zero one included, since every wallet would
2164
+ * derive the same identity from it.
2037
2165
  */
2038
2166
  async importFromHex(hex) {
2039
2167
  if (!hex.startsWith("mk:")) {
@@ -2041,21 +2169,15 @@ var PrivacyKeyManager = class {
2041
2169
  'PrivacyKeyManager: invalid cache format. Expected "mk:0x{masterBytes_hex}".'
2042
2170
  );
2043
2171
  }
2044
- const raw = hex.slice(3);
2045
- const h = raw.startsWith("0x") ? raw.slice(2) : raw;
2046
- const masterBytes = new Uint8Array((h.match(/.{2}/g) ?? []).map((b) => parseInt(b, 16)));
2047
- if (masterBytes.length !== 32) {
2048
- throw new Error("PrivacyKeyManager: invalid master bytes \u2014 expected 32 bytes.");
2049
- }
2050
- const masterBigint = BigInt("0x" + h);
2051
- const sk = masterBigint % BABYJUB_SUBORDER || 1n;
2052
- await this.load(sk, masterBytes);
2172
+ const masterBytes = fromHex(hex.slice(3));
2173
+ assertSecretKeyBytes(masterBytes, "PrivacyKeyManager: master");
2174
+ await this.load(0n, masterBytes);
2053
2175
  }
2054
2176
  };
2055
2177
 
2056
2178
  // src/protocol/keys/spendingKeyDerivation.ts
2057
- var import_hkdf5 = require("@noble/hashes/hkdf.js");
2058
- var import_sha29 = require("@noble/hashes/sha2.js");
2179
+ var import_hkdf4 = require("@noble/hashes/hkdf.js");
2180
+ var import_sha210 = require("@noble/hashes/sha2.js");
2059
2181
  var MIN_SIGNATURE_BYTES = 32;
2060
2182
  var MIN_DISTINCT_BYTES = 8;
2061
2183
  function assertUsableSignature(sigBytes) {
@@ -2077,7 +2199,7 @@ async function deriveMasterKeyBytes(signatureHex, chainId, address) {
2077
2199
  const info = new TextEncoder().encode(
2078
2200
  `orbinum-sk-${KEY_VERSION}:${chainId}:${canonicalAccountId(address)}`
2079
2201
  );
2080
- return (0, import_hkdf5.hkdf)(import_sha29.sha256, sigBytes, new Uint8Array(0), info, 32);
2202
+ return (0, import_hkdf4.hkdf)(import_sha210.sha256, sigBytes, new Uint8Array(0), info, 32);
2081
2203
  }
2082
2204
  async function deriveSpendingKeyFromSignature(signatureHex, chainId, address) {
2083
2205
  return deriveSpendingKeyFromMaster(await deriveMasterKeyBytes(signatureHex, chainId, address));
@@ -2085,8 +2207,8 @@ async function deriveSpendingKeyFromSignature(signatureHex, chainId, address) {
2085
2207
 
2086
2208
  // src/protocol/proving/unshield.ts
2087
2209
  var import_proof_generator = require("@orbinum/proof-generator");
2088
- var import_utils5 = require("@noble/ciphers/utils.js");
2089
- var import_baby_jubjub9 = require("@zk-kit/baby-jubjub");
2210
+ var import_utils4 = require("@noble/ciphers/utils.js");
2211
+ var import_baby_jubjub10 = require("@zk-kit/baby-jubjub");
2090
2212
  var import_poseidon_lite4 = require("poseidon-lite");
2091
2213
 
2092
2214
  // src/protocol/proving/merkle.ts
@@ -2118,8 +2240,8 @@ async function generateUnshieldProof(inputs, options = {}) {
2118
2240
  if (changeValue < 0n) {
2119
2241
  throw new Error("changeValue must be >= 0.");
2120
2242
  }
2121
- const changeOwnerPubkey = inputs.changeOwnerPubkey ?? (0, import_baby_jubjub9.mulPointEscalar)(import_baby_jubjub9.Base8, inputs.spendingKey)[0];
2122
- const changeBlinding = inputs.changeBlinding ?? (changeValue > 0n ? bytesToBigintLE((0, import_utils5.randomBytes)(32)) : 0n);
2243
+ const changeOwnerPubkey = inputs.changeOwnerPubkey ?? (0, import_baby_jubjub10.mulPointEscalar)(import_baby_jubjub10.Base8, inputs.spendingKey)[0];
2244
+ const changeBlinding = inputs.changeBlinding ?? (changeValue > 0n ? bytesToBigintLE((0, import_utils4.randomBytes)(32)) : 0n);
2123
2245
  const changeCommitment = changeValue > 0n ? (0, import_poseidon_lite4.poseidon4)([changeValue, inputs.assetId, changeOwnerPubkey, changeBlinding]) : 0n;
2124
2246
  const circuitInputs = {
2125
2247
  merkle_root: leHexToBigint(inputs.merkleRoot).toString(),
@@ -2355,6 +2477,25 @@ function wsUrlToHttp(wsUrl) {
2355
2477
  }
2356
2478
 
2357
2479
  // src/chain/substrate/SubstrateClient.ts
2480
+ var TIMESTAMP_PALLET = 1;
2481
+ function decodeCompact(bytes, offset) {
2482
+ const first = bytes[offset];
2483
+ if (first === void 0) return null;
2484
+ const mode = first & 3;
2485
+ if (mode === 0) return first >>> 2;
2486
+ if (mode === 1) {
2487
+ const b1 = bytes[offset + 1];
2488
+ return b1 === void 0 ? null : (first >>> 2 | b1 << 6) >>> 0;
2489
+ }
2490
+ const width = mode === 2 ? 4 : (first >>> 2) + 5;
2491
+ if (offset + width > bytes.length) return null;
2492
+ let value = 0n;
2493
+ const start = mode === 2 ? offset : offset + 1;
2494
+ const end = mode === 2 ? offset + 4 : offset + width;
2495
+ for (let i = end - 1; i >= start; i--) value = value << 8n | BigInt(bytes[i]);
2496
+ if (mode === 2) value >>= 2n;
2497
+ return value > BigInt(Number.MAX_SAFE_INTEGER) ? null : Number(value);
2498
+ }
2358
2499
  var SubstrateClient = class _SubstrateClient {
2359
2500
  constructor(_papi, _httpUrl, _owned) {
2360
2501
  this._papi = _papi;
@@ -2552,25 +2693,17 @@ var SubstrateClient = class _SubstrateClient {
2552
2693
  }
2553
2694
  }
2554
2695
  if (!timestampMs) {
2555
- const tsHex = extrinsics.find((hex) => {
2696
+ const ts = extrinsics.reduce((found, hex) => {
2697
+ if (found !== null) return found;
2556
2698
  try {
2557
2699
  const b = fromHex(hex);
2558
- return b.length > 6 && b[4] === 3 && b[5] === 0;
2700
+ if (b.length < 5 || b[2] !== TIMESTAMP_PALLET || b[3] !== 0) return null;
2701
+ return decodeCompact(b, 4);
2559
2702
  } catch {
2560
- return false;
2703
+ return null;
2561
2704
  }
2562
- });
2563
- if (tsHex) {
2564
- try {
2565
- const b = fromHex(tsHex);
2566
- const view = new DataView(b.buffer, b.byteOffset + 6, 8);
2567
- const lo = view.getUint32(0, true);
2568
- const hi = view.getUint32(4, true);
2569
- const ms = lo + hi * 4294967296;
2570
- if (ms > 0) timestampMs = ms;
2571
- } catch {
2572
- }
2573
- }
2705
+ }, null);
2706
+ if (ts !== null && ts > 0) timestampMs = ts;
2574
2707
  }
2575
2708
  const author = _SubstrateClient.extractAuthorFromLogs(header.digest.logs, ss58Prefix);
2576
2709
  return { header, extrinsics, timestampMs, author };
@@ -2840,7 +2973,10 @@ var EvmClient = class {
2840
2973
  }
2841
2974
  /**
2842
2975
  * Performs multiple JSON-RPC calls in a single HTTP request (batch).
2843
- * Results are returned in the same order as `calls`, as a typed tuple.
2976
+ *
2977
+ * Results come back in the order of `calls`, as a typed tuple. A call the
2978
+ * server answered with an error, or did not answer at all, lands as `null`
2979
+ * in its own slot — never shifting the ones after it.
2844
2980
  */
2845
2981
  async batchRequest(calls) {
2846
2982
  const body = calls.map((c, i) => ({
@@ -2852,8 +2988,8 @@ var EvmClient = class {
2852
2988
  const res = await postJsonWithRetry(this.rpcUrl, JSON.stringify(body));
2853
2989
  if (!res.ok) throw new Error(`EVM HTTP ${res.status}: ${res.statusText}`);
2854
2990
  const arr = await res.json();
2855
- arr.sort((a, b) => (a.id ?? 0) - (b.id ?? 0));
2856
- return arr.map((r) => r.result ?? null);
2991
+ const byId = new Map(arr.map((r) => [r.id, r]));
2992
+ return calls.map((_, i) => byId.get(i + 1)?.result ?? null);
2857
2993
  }
2858
2994
  // ─── Convenience wrappers ─────────────────────────────────────────────────
2859
2995
  /** Returns the native token balance (in wei) for an EVM address. */
@@ -2899,7 +3035,10 @@ var EvmClient = class {
2899
3035
  if (from) txObj["from"] = from;
2900
3036
  return this.request("eth_call", [txObj, "latest"]);
2901
3037
  }
2902
- /** Estimates the gas required for a transaction. Returns the estimate in wei as a `bigint`. */
3038
+ /**
3039
+ * Estimates a transaction's gas, in GAS UNITS — not wei. Multiply by
3040
+ * `getGasPrice()` for a cost.
3041
+ */
2903
3042
  async estimateGas(params) {
2904
3043
  const hex = await this.request("eth_estimateGas", [params]);
2905
3044
  return hexToBigint(hex);
@@ -3150,7 +3289,7 @@ var EvmExplorer = class _EvmExplorer {
3150
3289
  // --- Address ---
3151
3290
  /**
3152
3291
  * Returns aggregated on-chain data for an EVM address: balance, nonce,
3153
- * bytecode (truncated), and up to 50 recent logs from the last 1 000 blocks.
3292
+ * bytecode (truncated), and up to 50 logs it EMITTED in the last 1 001 blocks.
3154
3293
  */
3155
3294
  async getAddressInfo(address) {
3156
3295
  const latest = await this.evm.getBlockNumber().catch(() => 0);
@@ -3233,7 +3372,7 @@ var EvmExplorer = class _EvmExplorer {
3233
3372
  };
3234
3373
  }
3235
3374
  /**
3236
- * Returns ERC-20 `Transfer` events for `address` from the last 1 000 blocks.
3375
+ * Returns ERC-20 `Transfer` events for `address` from the last 1 001 blocks.
3237
3376
  * When `holderAddress` is provided, restricts results to transfers sent or received by that address.
3238
3377
  */
3239
3378
  async getTokenTransfers(address, holderAddress) {
@@ -3606,7 +3745,13 @@ var PrivacyModule = class {
3606
3745
  async getMerkleRoot() {
3607
3746
  return this.substrate.request("privacy_getMerkleRoot", []);
3608
3747
  }
3609
- /** Returns the Merkle proof for the given leaf index or commitment hex. */
3748
+ /**
3749
+ * Returns the Merkle proof for a LEAF INDEX.
3750
+ *
3751
+ * The node declares this parameter `u32`, so a commitment hex is rejected
3752
+ * by deserialisation before any lookup runs — `getMerkleProofByCommitment`
3753
+ * is the one that takes a string.
3754
+ */
3610
3755
  async getMerkleProof(leafIndex) {
3611
3756
  const raw = await this.substrate.request("privacy_getMerkleProof", [
3612
3757
  leafIndex
@@ -4556,9 +4701,13 @@ var OrbinumClientProvider = class _OrbinumClientProvider {
4556
4701
  // ─── Reconnection ───────────────────────────────────────────────────────
4557
4702
  /**
4558
4703
  * Schedules the next connection attempt using exponential backoff
4559
- * (capped at `reconnectMaxMs`) with full jitter, then transitions to
4560
- * `'reconnecting'`. Jitter (a random fraction of the delay) spreads out
4561
- * reconnects so many clients don't retry in lockstep after a shared outage.
4704
+ * (capped at `reconnectMaxMs`) with EQUAL jitter, then transitions to
4705
+ * `'reconnecting'`.
4706
+ *
4707
+ * The delay lands in `[capped/2, capped]` — half fixed, half random. That
4708
+ * spreads a shared outage's reconnects over a `capped/2` window instead of
4709
+ * having every client retry in lockstep, while keeping a floor so the first
4710
+ * attempts do not all pile up near zero the way full jitter allows.
4562
4711
  */
4563
4712
  scheduleReconnect() {
4564
4713
  if (this._reconnectTimer) clearTimeout(this._reconnectTimer);
@@ -4950,6 +5099,16 @@ function mapZkEventData(method, data) {
4950
5099
  index: get(4, "leaf_index") || get(4, "index")
4951
5100
  };
4952
5101
  }
5102
+ if (m === "nullifiersspent") {
5103
+ return { nullifiers: get(0, "nullifiers") };
5104
+ }
5105
+ if (m === "commitmentsinserted") {
5106
+ return {
5107
+ commitments: get(0, "commitments"),
5108
+ memos: get(1, "encrypted_memos") || get(1, "memos"),
5109
+ indices: get(2, "leaf_indices") || get(2, "indices")
5110
+ };
5111
+ }
4953
5112
  if (m === "privatetransfer") {
4954
5113
  return {
4955
5114
  nullifiers: get(0, "nullifiers"),
@@ -4991,12 +5150,12 @@ function mapZkEventData(method, data) {
4991
5150
  if (m_norm === "log") {
4992
5151
  return { log: get(0, "log") };
4993
5152
  }
4994
- if (m_norm === "created" || m_norm === "createdfailed" || m_norm === "executed" || m_norm === "executedfailed") {
5153
+ if (m_norm === "created" || m_norm === "createdfailed" || m_norm === "executedfailed") {
4995
5154
  return { address: get(0, "address") };
4996
5155
  }
4997
5156
  if (m_norm === "executed") {
4998
5157
  return {
4999
- from: get(0, "from"),
5158
+ from: get(0, "from") ?? get(0, "address"),
5000
5159
  to: get(1, "to"),
5001
5160
  transaction_hash: get(2, "transaction_hash"),
5002
5161
  exit_reason: get(3, "exit_reason")
@@ -5136,10 +5295,10 @@ var ERROR_KINDS = {
5136
5295
  CommitmentAlreadyExists: "shape",
5137
5296
  UnknownMerkleRoot: "stale-proof",
5138
5297
  // Amounts and fees
5139
- AmountTooSmall: "amount",
5140
5298
  InvalidAmount: "amount",
5141
5299
  InsufficientPoolBalance: "amount",
5142
5300
  FeeTooLow: "amount",
5301
+ InsufficientPendingFees: "amount",
5143
5302
  // Proofs
5144
5303
  InvalidProof: "proof",
5145
5304
  ProofVerificationFailed: "proof",
@@ -5148,12 +5307,14 @@ var ERROR_KINDS = {
5148
5307
  InvalidAssetId: "asset",
5149
5308
  AssetNotVerified: "asset",
5150
5309
  AssetIdMismatch: "asset",
5310
+ AssetIdAlreadyExists: "asset",
5151
5311
  // Request shape
5152
5312
  InvalidMemoSize: "shape",
5153
5313
  MemoCommitmentMismatch: "shape",
5154
5314
  TooManyInputsOrOutputs: "shape",
5155
5315
  InvalidRecipient: "shape",
5156
5316
  EmptyBatch: "shape",
5317
+ FeeRecipientUnavailable: "shape",
5157
5318
  // Capacity
5158
5319
  MerkleTreeFull: "capacity",
5159
5320
  // Balances pallet, reached through the same path
@@ -5512,6 +5673,7 @@ function buildConfig(existing, chainFingerprint) {
5512
5673
  updatedAt: now,
5513
5674
  ...chainFingerprint ? { chainFingerprint } : {},
5514
5675
  ...existing?.selfEphCounter !== void 0 ? { selfEphCounter: existing.selfEphCounter } : {},
5676
+ ...existing?.outgoingEphCounter !== void 0 ? { outgoingEphCounter: existing.outgoingEphCounter } : {},
5515
5677
  ...existing?.pairwiseCounterparties !== void 0 ? { pairwiseCounterparties: existing.pairwiseCounterparties } : {}
5516
5678
  };
5517
5679
  }
@@ -5523,6 +5685,7 @@ async function writeConfig(storage, fallback, chainFingerprint) {
5523
5685
  }
5524
5686
  function mergeCounters(current, fallback) {
5525
5687
  const selfEphCounter = higher(current.selfEphCounter, fallback?.selfEphCounter);
5688
+ const outgoingEphCounter = higher(current.outgoingEphCounter, fallback?.outgoingEphCounter);
5526
5689
  const parties = mergePairwise(current.pairwiseCounterparties, fallback?.pairwiseCounterparties);
5527
5690
  return {
5528
5691
  createdAt: current.createdAt ?? fallback?.createdAt,
@@ -5530,6 +5693,7 @@ function mergeCounters(current, fallback) {
5530
5693
  // is exactOptionalPropertyTypes, so an explicit undefined is a type
5531
5694
  // error AND would round-trip through structured clone as a present key.
5532
5695
  ...selfEphCounter !== void 0 ? { selfEphCounter } : {},
5696
+ ...outgoingEphCounter !== void 0 ? { outgoingEphCounter } : {},
5533
5697
  ...parties !== void 0 ? { pairwiseCounterparties: parties } : {}
5534
5698
  };
5535
5699
  }
@@ -5567,6 +5731,7 @@ function mergePairwise(current, fallback) {
5567
5731
 
5568
5732
  // src/wallet/vault/storage/ephemeralIndex.ts
5569
5733
  var MAX_EPH_INDEX = 4294967294;
5734
+ var RESTORE_SAFETY_GAP = 8;
5570
5735
  function usableCounter(value) {
5571
5736
  if (typeof value !== "number" || !Number.isInteger(value)) return null;
5572
5737
  if (value < 0 || value > MAX_EPH_INDEX) return null;
@@ -5581,27 +5746,51 @@ async function reserveSelfEphIndex(storage) {
5581
5746
  if (!updated) throw new Error("vault not initialized");
5582
5747
  return (usableCounter(updated.selfEphCounter) ?? 1) - 1;
5583
5748
  }
5584
- async function reservePairwiseIndex(storage, ivkHex) {
5749
+ async function reserveOutgoingIndex(storage, fromChain) {
5750
+ const recovered = usableCounter(fromChain);
5751
+ let exhausted = false;
5752
+ const updated = await storage.updateConfig((config) => {
5753
+ const raw = config.outgoingEphCounter;
5754
+ if (typeof raw === "number" && Number.isSafeInteger(raw) && raw > MAX_EPH_INDEX) {
5755
+ exhausted = true;
5756
+ return config;
5757
+ }
5758
+ exhausted = false;
5759
+ const stored = usableCounter(raw) ?? 0;
5760
+ const floor = recovered !== null && recovered > stored ? Math.min(recovered + RESTORE_SAFETY_GAP, MAX_EPH_INDEX) : stored;
5761
+ return {
5762
+ ...config,
5763
+ outgoingEphCounter: floor + 1,
5764
+ updatedAt: Date.now()
5765
+ };
5766
+ });
5767
+ if (!updated) throw new Error("vault not initialized");
5768
+ if (exhausted || typeof updated.outgoingEphCounter === "number" && Number.isSafeInteger(updated.outgoingEphCounter) && updated.outgoingEphCounter > MAX_EPH_INDEX) {
5769
+ throw new Error("outgoing ephemeral sequence exhausted");
5770
+ }
5771
+ const committed = usableCounter(updated.outgoingEphCounter);
5772
+ if (committed === null || committed - 1 >= MAX_EPH_INDEX) {
5773
+ throw new Error("outgoing ephemeral sequence exhausted");
5774
+ }
5775
+ return committed - 1;
5776
+ }
5777
+ async function registerPairwiseCounterparty(storage, ivkHex) {
5585
5778
  const key = ivkHex.toLowerCase();
5586
5779
  const updated = await storage.updateConfig((config) => {
5587
- const stored = usableCounter(config.pairwiseCounterparties?.[key]?.nextIndex);
5588
- const addedAt = config.pairwiseCounterparties?.[key]?.addedAt;
5780
+ const existing = config.pairwiseCounterparties?.[key];
5589
5781
  return {
5590
5782
  ...config,
5591
5783
  pairwiseCounterparties: {
5592
5784
  ...config.pairwiseCounterparties,
5593
5785
  [key]: {
5594
- nextIndex: (stored ?? 0) + 1,
5595
- addedAt: typeof addedAt === "number" ? addedAt : Date.now()
5786
+ nextIndex: usableCounter(existing?.nextIndex) ?? 0,
5787
+ addedAt: typeof existing?.addedAt === "number" ? existing.addedAt : Date.now()
5596
5788
  }
5597
5789
  },
5598
5790
  updatedAt: Date.now()
5599
5791
  };
5600
5792
  });
5601
5793
  if (!updated) throw new Error("vault not initialized");
5602
- const nextIndex = usableCounter(updated.pairwiseCounterparties?.[key]?.nextIndex);
5603
- if (nextIndex === null || nextIndex <= 1) return null;
5604
- return nextIndex - 1;
5605
5794
  }
5606
5795
 
5607
5796
  // src/wallet/vault/notes/record.ts
@@ -6036,19 +6225,44 @@ async function processHints(ctx, hints, total) {
6036
6225
  }
6037
6226
  }
6038
6227
  outcome.scanned++;
6039
- if (!hint.encryptedMemo || !hint.ephPkHex) {
6228
+ if (!hint.encryptedMemo) {
6040
6229
  outcome.noMemo++;
6041
6230
  continue;
6042
6231
  }
6043
6232
  toDecrypt.push(hint);
6044
6233
  }
6045
- const { notes, tagFiltered, selfMatched, pairwiseMatched, maxSelfEphIndex } = await ctx.pool.decryptBatch(toDecrypt, ctx.keys, ctx.signal);
6234
+ const {
6235
+ notes,
6236
+ tagFiltered,
6237
+ selfMatched,
6238
+ pairwiseMatched,
6239
+ maxSelfEphIndex,
6240
+ maxOutgoingEphIndex,
6241
+ sentNotes,
6242
+ learnedRecipients,
6243
+ unmatchedSent,
6244
+ sealedBookEntries
6245
+ } = await ctx.pool.decryptBatch(
6246
+ toDecrypt,
6247
+ { ...ctx.keys, recipientCandidates: [...outcome.learnedRecipients].map(fromHex) },
6248
+ ctx.signal
6249
+ );
6046
6250
  outcome.tagFiltered += tagFiltered;
6047
6251
  outcome.selfDiscovered += selfMatched;
6048
6252
  outcome.pairwiseDiscovered += pairwiseMatched;
6253
+ outcome.sentNotes.push(...sentNotes ?? []);
6254
+ outcome.unmatchedSent.push(...unmatchedSent ?? []);
6255
+ for (const e of sealedBookEntries ?? []) outcome.sealedBookEntries.add(e);
6256
+ for (const r of learnedRecipients ?? []) outcome.learnedRecipients.add(r);
6049
6257
  if (maxSelfEphIndex !== null) {
6050
6258
  outcome.maxSelfEphIndex = Math.max(outcome.maxSelfEphIndex ?? -1, maxSelfEphIndex);
6051
6259
  }
6260
+ if (maxOutgoingEphIndex != null) {
6261
+ outcome.maxOutgoingEphIndex = Math.max(
6262
+ outcome.maxOutgoingEphIndex ?? -1,
6263
+ maxOutgoingEphIndex
6264
+ );
6265
+ }
6052
6266
  const pageEntries = [];
6053
6267
  for (const [i, decrypted] of notes.entries()) {
6054
6268
  const hint = toDecrypt[i];
@@ -6145,7 +6359,12 @@ async function collectScanEntries(params) {
6145
6359
  tagFiltered: 0,
6146
6360
  selfDiscovered: 0,
6147
6361
  pairwiseDiscovered: 0,
6148
- maxSelfEphIndex: null
6362
+ sentNotes: [],
6363
+ unmatchedSent: [],
6364
+ sealedBookEntries: /* @__PURE__ */ new Set(),
6365
+ learnedRecipients: new Set((params.keys.recipientCandidates ?? []).map((c) => toHex(c))),
6366
+ maxSelfEphIndex: null,
6367
+ maxOutgoingEphIndex: null
6149
6368
  };
6150
6369
  const ctx = {
6151
6370
  outcome,
@@ -6361,12 +6580,18 @@ function createScanCheckpoint(params) {
6361
6580
  // src/wallet/worker/kernel/types.ts
6362
6581
  var SELF_EPH_WINDOW = 1024;
6363
6582
  var PAIRWISE_EPH_WINDOW = 64;
6583
+ var OUTGOING_EPH_WINDOW = 64;
6364
6584
  var EMPTY_BATCH_RESULT = {
6365
6585
  notes: [],
6366
6586
  tagFiltered: 0,
6367
6587
  selfMatched: 0,
6368
6588
  pairwiseMatched: 0,
6369
- maxSelfEphIndex: null
6589
+ maxSelfEphIndex: null,
6590
+ maxOutgoingEphIndex: null,
6591
+ sentNotes: [],
6592
+ learnedRecipients: [],
6593
+ unmatchedSent: [],
6594
+ sealedBookEntries: []
6370
6595
  };
6371
6596
 
6372
6597
  // src/wallet/scanner/selfEphGap.ts
@@ -6378,7 +6603,7 @@ function gapMargin(windowSize) {
6378
6603
  return Math.max(1, Math.floor(windowSize / 16));
6379
6604
  }
6380
6605
  function resolveSelfEphCeiling(params) {
6381
- const { notes, spendingKey, viewingKey, scanMaxIndex } = params;
6606
+ const { notes, viewingKey, scanMaxIndex } = params;
6382
6607
  const windowSize = params.windowSize !== void 0 && params.windowSize > 0 ? params.windowSize : SELF_EPH_WINDOW;
6383
6608
  if (scanMaxIndex === null) return null;
6384
6609
  if (scanMaxIndex < windowSize - gapMargin(windowSize)) return scanMaxIndex;
@@ -6391,7 +6616,7 @@ function resolveSelfEphCeiling(params) {
6391
6616
  let max = scanMaxIndex;
6392
6617
  let from = windowSize;
6393
6618
  for (; ; ) {
6394
- const slice = selfEphWindow(spendingKey, ivkPacked, from, windowSize);
6619
+ const slice = selfEphWindow(viewingKey, ivkPacked, from, windowSize);
6395
6620
  let matched = false;
6396
6621
  for (const entry of slice) {
6397
6622
  if (ownEphPks.has(entry.ephPkHex.toLowerCase())) {
@@ -6404,12 +6629,12 @@ function resolveSelfEphCeiling(params) {
6404
6629
  }
6405
6630
  return max;
6406
6631
  }
6407
- var MAX_EPH_WINDOW = SELF_EPH_WINDOW * 64;
6632
+ var MAX_EPH_WINDOW2 = SELF_EPH_WINDOW * 64;
6408
6633
  function windowSizeForCounter(counter) {
6409
6634
  const used = Number.isFinite(counter) ? counter : 0;
6410
6635
  const needed = used + gapMargin(SELF_EPH_WINDOW);
6411
6636
  const rounded = Math.ceil(needed / SELF_EPH_WINDOW) * SELF_EPH_WINDOW;
6412
- return Math.min(MAX_EPH_WINDOW, Math.max(SELF_EPH_WINDOW, rounded));
6637
+ return Math.min(MAX_EPH_WINDOW2, Math.max(SELF_EPH_WINDOW, rounded));
6413
6638
  }
6414
6639
 
6415
6640
  // src/wallet/scanner/phases/spentStatus.ts
@@ -6447,6 +6672,8 @@ async function resolveSpentStatus(params) {
6447
6672
  }
6448
6673
 
6449
6674
  // src/wallet/scanner/pipeline.ts
6675
+ var OUTGOING_RESCAN_GAP = 8;
6676
+ var RETRY_BOOK_LIMIT = 64;
6450
6677
  async function runScan(params) {
6451
6678
  const { vault, storage, hints, nullifiers, pool, keys, sinceLeafIndex, signal, onWarning } = params;
6452
6679
  const isIncremental = sinceLeafIndex !== void 0;
@@ -6454,6 +6681,7 @@ async function runScan(params) {
6454
6681
  const preScanHexes = new Set(existingHexes);
6455
6682
  const config = await storage.getConfig().catch(() => null);
6456
6683
  const selfEphWindowSize = windowSizeForCounter(config?.selfEphCounter ?? 0);
6684
+ const outgoingEphWindowSize = windowSizeForCounter(config?.outgoingEphCounter ?? 0);
6457
6685
  const pairwiseCounterparties = Object.keys(config?.pairwiseCounterparties ?? {}).flatMap(
6458
6686
  (hex) => {
6459
6687
  try {
@@ -6482,7 +6710,12 @@ async function runScan(params) {
6482
6710
  // worth it on full scans and restores, waste on small incremental ticks.
6483
6711
  selfEph: !isIncremental,
6484
6712
  selfEphWindowSize,
6485
- pairwiseCounterparties
6713
+ pairwiseCounterparties,
6714
+ // Sent-note discovery rides the same full-scan/restore budget as
6715
+ // self discovery: on an incremental tick the history is already
6716
+ // there, and the window would be paid for nothing.
6717
+ outgoingEph: !isIncremental,
6718
+ outgoingEphWindowSize
6486
6719
  },
6487
6720
  existingHexes,
6488
6721
  sinceLeafIndex,
@@ -6496,6 +6729,49 @@ async function runScan(params) {
6496
6729
  onPage: (entries) => checkpoint.savePageNotes(entries),
6497
6730
  onBatchDone: (maxLeafIndex) => checkpoint.advanceCursor(maxLeafIndex)
6498
6731
  });
6732
+ const ovk = keys.outgoingViewingKey;
6733
+ if (scan.unmatchedSent.length > 0 && ovk) {
6734
+ const proven = [...scan.learnedRecipients].map(fromHex);
6735
+ const keptEntries = [...scan.sealedBookEntries].slice(-RETRY_BOOK_LIMIT);
6736
+ const sealed = [];
6737
+ for (const entry of keptEntries) {
6738
+ try {
6739
+ sealed.push(BigInt(entry));
6740
+ } catch {
6741
+ }
6742
+ }
6743
+ const retries = scan.unmatchedSent.slice(-RETRY_BOOK_LIMIT);
6744
+ if (retries.length < scan.unmatchedSent.length) {
6745
+ onWarning?.(
6746
+ `sent-note retry capped at ${retries.length} of ${scan.unmatchedSent.length}; the rest resolve on the next full scan`
6747
+ );
6748
+ }
6749
+ if (keptEntries.length < scan.sealedBookEntries.size) {
6750
+ onWarning?.(
6751
+ `recipient book capped at ${keptEntries.length} of ${scan.sealedBookEntries.size} entries; older payments resolve on the next full scan`
6752
+ );
6753
+ }
6754
+ for (const { hint, ephIndex } of retries) {
6755
+ if (signal?.aborted) throw scanAbortError();
6756
+ const candidates = [
6757
+ ...sealed.map((e) => openRecipientBookEntry(e, ovk, hint.commitmentHex)),
6758
+ ...proven
6759
+ ];
6760
+ const sent = recoverSentNote({
6761
+ hint,
6762
+ outgoingViewingKey: ovk,
6763
+ ephIndex,
6764
+ recipientCandidates: candidates
6765
+ });
6766
+ if (!sent) continue;
6767
+ const { recipientIvk, ...facts } = sent;
6768
+ scan.sentNotes.push({
6769
+ ...facts,
6770
+ counterpartyIvkHex: toHex(recipientIvk),
6771
+ encryptedMemo: hint.encryptedMemo ?? ""
6772
+ });
6773
+ }
6774
+ }
6499
6775
  const nullifiersToQuery = collectNullifiersToQuery(
6500
6776
  scan.scanEntries,
6501
6777
  scan.onChainHexes,
@@ -6525,7 +6801,6 @@ async function runScan(params) {
6525
6801
  try {
6526
6802
  ceiling = resolveSelfEphCeiling({
6527
6803
  notes: vault.getAll(),
6528
- spendingKey: keys.spendingKey,
6529
6804
  viewingKey: keys.viewingKey,
6530
6805
  scanMaxIndex: scan.maxSelfEphIndex,
6531
6806
  windowSize: selfEphWindowSize
@@ -6535,6 +6810,13 @@ async function runScan(params) {
6535
6810
  await bumpSelfEphCounterTo(storage, ceiling + 1).catch(() => {
6536
6811
  });
6537
6812
  }
6813
+ if (scan.maxOutgoingEphIndex !== null) {
6814
+ await bumpOutgoingCounterTo(
6815
+ storage,
6816
+ scan.maxOutgoingEphIndex + 1 + OUTGOING_RESCAN_GAP
6817
+ ).catch(() => {
6818
+ });
6819
+ }
6538
6820
  if (scan.decryptFailed > 0 && scan.found === 0 && scan.alreadyPresent === 0) {
6539
6821
  onWarning?.(
6540
6822
  `no notes recovered from ${scan.decryptFailed} hint(s) with memos; if this wallet should hold notes, check the keys it was unlocked with`
@@ -6546,7 +6828,13 @@ async function runScan(params) {
6546
6828
  decryptFailed: scan.decryptFailed,
6547
6829
  alreadyPresent: scan.alreadyPresent,
6548
6830
  purged,
6549
- incremental: isIncremental
6831
+ incremental: isIncremental,
6832
+ sentNotes: scan.sentNotes,
6833
+ discovery: {
6834
+ self: scan.selfDiscovered,
6835
+ pairwise: scan.pairwiseDiscovered,
6836
+ tagFiltered: scan.tagFiltered
6837
+ }
6550
6838
  };
6551
6839
  }
6552
6840
  async function bumpSelfEphCounterTo(storage, next) {
@@ -6554,6 +6842,11 @@ async function bumpSelfEphCounterTo(storage, next) {
6554
6842
  (config) => (config.selfEphCounter ?? 0) >= next ? config : { ...config, selfEphCounter: next, updatedAt: Date.now() }
6555
6843
  );
6556
6844
  }
6845
+ async function bumpOutgoingCounterTo(storage, next) {
6846
+ await storage.updateConfig(
6847
+ (config) => (config.outgoingEphCounter ?? 0) >= next ? config : { ...config, outgoingEphCounter: next, updatedAt: Date.now() }
6848
+ );
6849
+ }
6557
6850
 
6558
6851
  // src/wallet/scanner/history/extrinsicFacts.ts
6559
6852
  var UNKNOWN = { fee: null, success: true };
@@ -6692,10 +6985,8 @@ function regeneratePaymentSlip(facts, recipientIvkPacked, txHash) {
6692
6985
  commitmentHex: facts.commitmentHex,
6693
6986
  encryptedMemo: facts.encryptedMemo,
6694
6987
  ...facts.leafIndex !== void 0 ? { leafIndex: facts.leafIndex } : {},
6695
- // Rendered as an explorer link by the recipient, where an unconstrained
6988
+ // The recipient renders this as an explorer link, so an unconstrained
6696
6989
  // string is a URL injection wearing the authority of a decrypted slip.
6697
- // Dropped rather than fatal: the slip still rebuilds the note, which is
6698
- // the part that matters.
6699
6990
  ...isHexOfLength(txHash, 32) ? { txHash } : {}
6700
6991
  });
6701
6992
  return encodePaymentSlip(envelope);
@@ -6720,15 +7011,37 @@ async function loadExistingRecords(vault) {
6720
7011
  function recordKey(transfer) {
6721
7012
  return transfer.hash ?? `${transfer.blockNumber}-${transfer.extrinsicIndex ?? 0}`;
6722
7013
  }
7014
+ function sentByCommitment(sentNotes) {
7015
+ return new Map(sentNotes.map((s) => [s.commitmentHex.toLowerCase(), s]));
7016
+ }
7017
+ function reissuedSlip(sent, hash) {
7018
+ try {
7019
+ const facts = {
7020
+ commitmentHex: sent.commitmentHex,
7021
+ encryptedMemo: sent.encryptedMemo,
7022
+ ...sent.leafIndex !== void 0 ? { leafIndex: sent.leafIndex } : {}
7023
+ };
7024
+ return {
7025
+ paymentSlip: regeneratePaymentSlip(
7026
+ facts,
7027
+ fromHex(sent.counterpartyIvkHex),
7028
+ hash ?? void 0
7029
+ )
7030
+ };
7031
+ } catch {
7032
+ return {};
7033
+ }
7034
+ }
6723
7035
  async function reconstructOutgoingTxRecords(deps) {
6724
7036
  const { vault, transfers } = deps;
6725
7037
  const now = deps.now ?? Date.now;
6726
7038
  const allNotes = vault.getAll();
6727
7039
  const spentNotes = allNotes.filter((n) => n.spent);
6728
7040
  if (spentNotes.length === 0) return;
7041
+ const sentCommitments = (deps.sentNotes ?? []).map((s) => s.commitmentHex);
6729
7042
  const [outgoingTransfers, commitmentTransfers] = await Promise.all([
6730
7043
  transfers.byNullifiers(spentNotes.map((n) => n.nullifierHex)),
6731
- transfers.byCommitments(allNotes.map((n) => n.commitmentHex))
7044
+ transfers.byCommitments([...allNotes.map((n) => n.commitmentHex), ...sentCommitments])
6732
7045
  ]);
6733
7046
  if (outgoingTransfers.length === 0) return;
6734
7047
  const noteByNullifier = new Map(spentNotes.map((n) => [n.nullifierHex, n]));
@@ -6737,64 +7050,85 @@ async function reconstructOutgoingTxRecords(deps) {
6737
7050
  commitmentTransfers.map((ct) => [extrinsicKey(ct), ct.matchedCommitments ?? []])
6738
7051
  );
6739
7052
  const existingByKey = await loadExistingRecords(vault);
7053
+ const sentIndex = sentByCommitment(deps.sentNotes ?? []);
7054
+ const recoveryEnabled = deps.sentNotes !== void 0;
7055
+ const sentFor = (row) => (commitmentsByExtrinsic.get(extrinsicKey(row)) ?? []).map((hex) => sentIndex.get(hex.toLowerCase())).find((s) => s !== void 0);
6740
7056
  for (const transfer of outgoingTransfers) {
6741
7057
  const existing = existingByKey.get(recordKey(transfer));
6742
- if (existing?.recipientPkHex && existing.recipientPkHex !== ZERO_PK) continue;
7058
+ const known = existing?.recipientPkHex && existing.recipientPkHex !== ZERO_PK;
7059
+ if (known && existing?.paymentSlip) continue;
6743
7060
  const inputNotes = (transfer.matchedNullifiers ?? []).map((h) => noteByNullifier.get(h)).filter((n) => n !== void 0);
6744
7061
  if (inputNotes.length === 0) continue;
6745
7062
  const changeNote = selectDescribingNoteByCommitment(
6746
7063
  commitmentsByExtrinsic.get(extrinsicKey(transfer)) ?? [],
6747
7064
  noteByCommitment
6748
7065
  );
6749
- const recipientPkHex = toPkHex(changeNote?.sourcePk);
7066
+ const sentForRow = sentFor(transfer);
7067
+ const recipientPkHex = sentForRow ? toPkHex(sentForRow.recipientStealthPk) : recoveryEnabled ? ZERO_PK : toPkHex(changeNote?.sourcePk);
6750
7068
  if (existing) {
6751
- if (recipientPkHex === ZERO_PK) continue;
6752
- await vault.saveTxRecord({ ...existing, recipientPkHex });
7069
+ const patch = {
7070
+ // Only when it ADDS something: this runs over the whole
7071
+ // outgoing history on every scan, and rewriting what is already
7072
+ // there would make each one a write.
7073
+ ...!known && recipientPkHex !== ZERO_PK ? { recipientPkHex } : {},
7074
+ ...sentForRow && !existing.paymentSlip ? reissuedSlip(sentForRow, transfer.hash) : {}
7075
+ };
7076
+ if (Object.keys(patch).length > 0) {
7077
+ await vault.saveTxRecord({ ...existing, ...patch });
7078
+ }
6753
7079
  continue;
6754
7080
  }
6755
7081
  const { fee, success } = await fetchExtrinsicFacts(deps.txFacts, transfer.hash);
6756
7082
  const totalInputValue = inputNotes.reduce((sum, n) => sum + n.value, 0n);
6757
- const transferAmount = totalInputValue - (changeNote?.value ?? 0n) - (fee ?? 0n);
7083
+ const derivedAmount = totalInputValue - (changeNote?.value ?? 0n) - (fee ?? 0n);
7084
+ const transferAmount = sentForRow?.value ?? derivedAmount;
6758
7085
  if (transferAmount <= 0n) continue;
6759
7086
  const record = {
6760
7087
  id: recordKey(transfer),
6761
7088
  type: "private_transfer",
6762
7089
  blockNumber: transfer.blockNumber,
6763
7090
  hash: transfer.hash ?? "",
6764
- assetId: inputNotes[0].assetId.toString(),
7091
+ assetId: (sentForRow?.assetId ?? inputNotes[0].assetId).toString(),
6765
7092
  amount: transferAmount.toString(),
6766
7093
  recipientPkHex,
6767
7094
  status: success ? "success" : "failed",
6768
- ...fee !== null ? { feePlanck: fee.toString() } : { amountApproximate: true },
6769
- timestampMs: transfer.timestampMs ?? now()
7095
+ ...fee !== null ? { feePlanck: fee.toString() } : {},
7096
+ ...fee === null && !sentForRow ? { amountApproximate: true } : {},
7097
+ timestampMs: transfer.timestampMs ?? now(),
7098
+ // Best-effort: a slip that cannot be sealed costs the convenience of
7099
+ // handing it over again, never the history row.
7100
+ ...sentForRow ? reissuedSlip(sentForRow, transfer.hash) : {}
6770
7101
  };
6771
7102
  await vault.saveTxRecord(record);
6772
7103
  }
6773
7104
  }
6774
7105
 
6775
7106
  // src/wallet/ops/notes/buildNote.ts
6776
- async function buildZkNote(params, deps) {
7107
+ async function buildZkNoteWithIndex(params, deps) {
6777
7108
  const { keys, storage } = deps;
6778
7109
  const ownerPk = params.ownerPk ?? keys.ownerPk;
6779
7110
  const spendingKey = params.spendingKey ?? keys.spendingKey;
6780
7111
  const viewingPublicKey = params.viewingPublicKey ?? keys.viewingPublicKey;
6781
7112
  const isSelf = params.viewingPublicKey === void 0 && params.recipientOwnerPk === void 0;
6782
7113
  let ephSkOverride;
6783
- if (isSelf && storage) {
7114
+ let outgoingIndex;
7115
+ if (isSelf && storage && keys.viewingSecretKey) {
6784
7116
  try {
6785
- ephSkOverride = deriveSelfEphSk(spendingKey, await reserveSelfEphIndex(storage));
7117
+ ephSkOverride = deriveSelfEphSk(
7118
+ keys.viewingSecretKey,
7119
+ await reserveSelfEphIndex(storage)
7120
+ );
6786
7121
  } catch {
6787
7122
  }
6788
- } else if (params.viewingPublicKey && storage && keys.viewingSecretKey) {
7123
+ } else if (params.viewingPublicKey && storage && keys.outgoingViewingKey) {
6789
7124
  try {
6790
- const index = await reservePairwiseIndex(storage, toHex(params.viewingPublicKey));
6791
- if (index !== null) {
6792
- const pairSecret = derivePairwiseSharedSecret(
6793
- keys.viewingSecretKey,
6794
- params.viewingPublicKey
6795
- );
6796
- ephSkOverride = derivePairwiseEphSk(pairSecret, index);
6797
- }
7125
+ const index = await reserveOutgoingIndex(storage, params.outgoingIndexFromChain);
7126
+ ephSkOverride = deriveOutgoingEphSk(keys.outgoingViewingKey, index);
7127
+ outgoingIndex = index;
7128
+ } catch {
7129
+ }
7130
+ try {
7131
+ await registerPairwiseCounterparty(storage, toHex(params.viewingPublicKey));
6798
7132
  } catch {
6799
7133
  }
6800
7134
  }
@@ -6814,7 +7148,13 @@ async function buildZkNote(params, deps) {
6814
7148
  },
6815
7149
  ...ephSkOverride !== void 0 && { ephSkOverride }
6816
7150
  });
6817
- return stampCreatedAt(note, Date.now());
7151
+ return {
7152
+ note: stampCreatedAt(note, Date.now()),
7153
+ ...outgoingIndex !== void 0 ? { outgoingIndex } : {}
7154
+ };
7155
+ }
7156
+ async function buildZkNote(params, deps) {
7157
+ return (await buildZkNoteWithIndex(params, deps)).note;
6818
7158
  }
6819
7159
 
6820
7160
  // src/wallet/ops/notes/selfStealthNote.ts
@@ -6833,89 +7173,6 @@ function recoverSelfStealthNote(note, keys) {
6833
7173
  return recovered;
6834
7174
  }
6835
7175
 
6836
- // src/wallet/ops/notes/noteTransfer.ts
6837
- var NOTE_TRANSFER_URI_SCHEME = "orbinum://notes/v1/";
6838
- var QR_PAGE_MAX_CHARS = 1800;
6839
- function noteToTransferEntry(note) {
6840
- return {
6841
- c: note.commitmentHex,
6842
- n: note.nullifierHex,
6843
- val: note.value.toString(),
6844
- aid: note.assetId.toString(),
6845
- opk: scalarToHex(note.ownerPk),
6846
- bld: scalarToHex(note.blinding),
6847
- sk: scalarToHex(note.spendingKey)
6848
- };
6849
- }
6850
- function encodeNoteTransferPages(notes, options = {}) {
6851
- const now = options.now ?? Date.now;
6852
- const maxChars = options.maxChars ?? QR_PAGE_MAX_CHARS;
6853
- const entries = notes.map(noteToTransferEntry);
6854
- const batches = [];
6855
- let batch = [];
6856
- for (const entry of entries) {
6857
- batch.push(entry);
6858
- const probe = { v: 1, ts: 0, p: 0, pt: 1, notes: batch };
6859
- const size = NOTE_TRANSFER_URI_SCHEME.length + base64UrlEncode(JSON.stringify(probe)).length;
6860
- if (size > maxChars && batch.length > 1) {
6861
- batch.pop();
6862
- batches.push(batch);
6863
- batch = [entry];
6864
- }
6865
- }
6866
- if (batch.length > 0) batches.push(batch);
6867
- const ts = now();
6868
- return batches.map((notesInPage, i) => {
6869
- const payload = {
6870
- v: 1,
6871
- ts,
6872
- p: i,
6873
- pt: batches.length,
6874
- notes: notesInPage
6875
- };
6876
- return NOTE_TRANSFER_URI_SCHEME + base64UrlEncode(JSON.stringify(payload));
6877
- });
6878
- }
6879
- function decodeNoteTransferPage(uri) {
6880
- if (!uri.startsWith(NOTE_TRANSFER_URI_SCHEME)) {
6881
- throw new Error("Not an Orbinum note-transfer code.");
6882
- }
6883
- let payload;
6884
- try {
6885
- payload = JSON.parse(base64UrlDecode(uri.slice(NOTE_TRANSFER_URI_SCHEME.length)));
6886
- } catch {
6887
- throw new Error("Note-transfer code is corrupt.");
6888
- }
6889
- const p = payload;
6890
- if (p.v !== 1) {
6891
- throw new Error(`Unsupported note-transfer version: ${String(p.v)}.`);
6892
- }
6893
- if (typeof p.p !== "number" || typeof p.pt !== "number" || !Array.isArray(p.notes) || p.notes.some((n) => !isEntry(n))) {
6894
- throw new Error("Note-transfer code is missing required fields.");
6895
- }
6896
- return p;
6897
- }
6898
- function isEntry(value) {
6899
- if (typeof value !== "object" || value === null) return false;
6900
- const e = value;
6901
- return ["c", "n", "val", "aid", "opk", "bld", "sk"].every(
6902
- (k) => typeof e[k] === "string" && e[k].length > 0
6903
- );
6904
- }
6905
- function assembleNoteTransfer(pages) {
6906
- if (pages.length === 0) throw new Error("No pages scanned.");
6907
- const total = pages[0].pt;
6908
- if (pages.some((p) => p.pt !== total)) {
6909
- throw new Error("Pages come from different batches.");
6910
- }
6911
- const seen = new Set(pages.map((p) => p.p));
6912
- if (seen.size !== total) {
6913
- const missing = [...Array(total).keys()].filter((i) => !seen.has(i));
6914
- throw new Error(`Missing page(s): ${missing.map((i) => i + 1).join(", ")} of ${total}.`);
6915
- }
6916
- return [...pages].sort((a, b) => a.p - b.p).flatMap((p) => p.notes);
6917
- }
6918
-
6919
7176
  // src/wallet/ops/notes/noteBackup.ts
6920
7177
  var NOTE_BACKUP_VERSION = 1;
6921
7178
  function noteToBackupEntry(note) {
@@ -6950,12 +7207,13 @@ function decodeNoteBackup(json) {
6950
7207
  if (p.v !== NOTE_BACKUP_VERSION) {
6951
7208
  throw new Error(`Unsupported note-backup version: ${String(p.v)}.`);
6952
7209
  }
6953
- if (!Array.isArray(p.notes) || p.notes.some((n) => !isEntry2(n))) {
7210
+ if (!Array.isArray(p.notes) || p.notes.some((n) => !isEntry(n))) {
6954
7211
  throw new Error("Note backup is missing required fields.");
6955
7212
  }
6956
7213
  return p.notes;
6957
7214
  }
6958
7215
  function importNotesFromBackup(entries, keys) {
7216
+ assertSecretKeyBytes(keys.viewingSecretKey, "importNotesFromBackup: viewingSecretKey");
6959
7217
  const out = [];
6960
7218
  for (const entry of entries) {
6961
7219
  const commitment = {
@@ -6979,12 +7237,14 @@ function importNotesFromBackup(entries, keys) {
6979
7237
  }
6980
7238
  return out;
6981
7239
  }
6982
- function isEntry2(value) {
7240
+ function isEntry(value) {
6983
7241
  if (typeof value !== "object" || value === null) return false;
6984
7242
  const e = value;
6985
- if (typeof e["commitmentHex"] !== "string" || e["commitmentHex"].length === 0) return false;
6986
- if (typeof e["encryptedMemo"] !== "string" || e["encryptedMemo"].length === 0) return false;
6987
- if ("leafIndex" in e && e["leafIndex"] !== void 0 && typeof e["leafIndex"] !== "number") {
7243
+ if (!isHexOfLength(e["commitmentHex"], 32)) return false;
7244
+ if (!isHexOfLength(e["encryptedMemo"], ENCRYPTED_MEMO_SIZE)) return false;
7245
+ if (e["leafIndex"] !== void 0 && !isValidLeafIndex(e["leafIndex"])) return false;
7246
+ if (e["spent"] !== void 0 && typeof e["spent"] !== "boolean") return false;
7247
+ if (e["spentAt"] !== void 0 && e["spentAt"] !== null && typeof e["spentAt"] !== "number") {
6988
7248
  return false;
6989
7249
  }
6990
7250
  return true;
@@ -6992,6 +7252,7 @@ function isEntry2(value) {
6992
7252
 
6993
7253
  // src/wallet/ops/notes/paymentSlipImport.ts
6994
7254
  function importPaymentSlip(slip, keys) {
7255
+ assertSecretKeyBytes(keys.viewingSecretKey, "importPaymentSlip: viewingSecretKey");
6995
7256
  const envelope = typeof slip === "string" ? decodePaymentSlip(slip) : slip;
6996
7257
  if (!envelope) return null;
6997
7258
  const fields = openPaymentSlip(keys.viewingSecretKey, envelope);
@@ -7120,10 +7381,7 @@ async function transferNotes(deps, params, onProgress) {
7120
7381
  const merkleRoot = merkleA.root;
7121
7382
  onProgress?.("building-output-notes");
7122
7383
  const effectiveSenderPk = senderPk ?? noteA.ownerPk;
7123
- const senderViewingPublicKey = deriveViewingPublicKey(
7124
- deriveViewingSecretKey(noteA.spendingKey)
7125
- );
7126
- const recipientNote = await deps.buildNote({
7384
+ const { note: recipientNote, outgoingIndex } = await deps.buildNote({
7127
7385
  value: transferAmount,
7128
7386
  assetId: noteA.assetId,
7129
7387
  ownerPk: recipientPk,
@@ -7133,13 +7391,17 @@ async function transferNotes(deps, params, onProgress) {
7133
7391
  // With a viewing key present this activates stealth derivation.
7134
7392
  recipientOwnerPk: recipientPk
7135
7393
  });
7136
- const changeNote = await deps.buildNote({
7394
+ const changeSourcePk = outgoingIndex !== void 0 && recipientViewingPublicKey !== void 0 && deps.outgoingViewingKey !== void 0 ? bytesToBigintLE(
7395
+ sealRecipientBookEntry(
7396
+ recipientViewingPublicKey,
7397
+ deps.outgoingViewingKey,
7398
+ recipientNote.commitmentHex
7399
+ )
7400
+ ) : recipientNote.ownerPk;
7401
+ const { note: changeNote } = await deps.buildNote({
7137
7402
  value: changeValue,
7138
7403
  assetId: noteA.assetId,
7139
- ownerPk: effectiveSenderPk,
7140
- spendingKey: noteA.spendingKey,
7141
- sourcePk: recipientNote.ownerPk,
7142
- viewingPublicKey: senderViewingPublicKey
7404
+ sourcePk: changeSourcePk
7143
7405
  });
7144
7406
  onProgress?.("generating-zk");
7145
7407
  const noteToInput = (note, proof) => ({
@@ -7256,7 +7518,7 @@ async function unshieldNote(deps, params, onProgress) {
7256
7518
  if (changeValue > 0n) {
7257
7519
  const { ownerPk, spendingKey } = deps.stealthKeys;
7258
7520
  changeBlinding = randomBlinding();
7259
- changeNote = await deps.buildNote({
7521
+ changeNote = (await deps.buildNote({
7260
7522
  value: changeValue,
7261
7523
  assetId: note.assetId,
7262
7524
  ownerPk,
@@ -7264,7 +7526,7 @@ async function unshieldNote(deps, params, onProgress) {
7264
7526
  spendingKey,
7265
7527
  // Activates stealth: the commitment covers a one-time ownerPk.
7266
7528
  recipientOwnerPk: ownerPk
7267
- });
7529
+ })).note;
7268
7530
  }
7269
7531
  const { provider, version: circuitVersion } = await deps.resolver.resolve(
7270
7532
  import_proof_generator7.CircuitType.Unshield,
@@ -7550,8 +7812,9 @@ var KEY_PREFIX = "orbinum_sk_";
7550
7812
  function isEnvelope(value) {
7551
7813
  return typeof value === "object" && value !== null && value.v === 1 && typeof value.iv === "string" && typeof value.ct === "string";
7552
7814
  }
7553
- function sessionCacheKey(address, chainId) {
7554
- return `${KEY_PREFIX}${chainId}_${canonicalAccountId(address)}`;
7815
+ function sessionCacheKey(address, chainId, version = "v3") {
7816
+ const marker = `${version}_`;
7817
+ return `${KEY_PREFIX}${marker}${chainId}_${canonicalAccountId(address)}`;
7555
7818
  }
7556
7819
  async function hasCachedSession(store, address, chainId) {
7557
7820
  return await store.get(sessionCacheKey(address, chainId)) !== null;
@@ -7622,9 +7885,55 @@ function createDeviceKeyProvider(store) {
7622
7885
 
7623
7886
  // src/wallet/identity/vaultName.ts
7624
7887
  var PREFIX2 = "orbinum-vault";
7625
- function vaultStorageName(address, chainFingerprint) {
7888
+ var versionMarker = (version) => `${version}-`;
7889
+ var VERSION_MARKERS = ["v3-"];
7890
+ function assertNoVersionMarker(component, label) {
7891
+ for (const marker of VERSION_MARKERS) {
7892
+ if (component.startsWith(marker)) {
7893
+ throw new Error(
7894
+ `vaultStorageName: ${label} must not start with "${marker}" \u2014 it would collide with another identity version`
7895
+ );
7896
+ }
7897
+ }
7898
+ }
7899
+ function vaultStorageName(address, chainFingerprint, version = "v3") {
7626
7900
  const account = canonicalAccountId(address);
7627
- return chainFingerprint ? `${PREFIX2}-${chainFingerprint.toLowerCase()}-${account}` : `${PREFIX2}-${account}`;
7901
+ assertNoVersionMarker(account, "account");
7902
+ const marker = versionMarker(version);
7903
+ if (chainFingerprint) assertNoVersionMarker(chainFingerprint.toLowerCase(), "chainFingerprint");
7904
+ return chainFingerprint ? `${PREFIX2}-${marker}${chainFingerprint.toLowerCase()}-${account}` : `${PREFIX2}-${marker}${account}`;
7905
+ }
7906
+
7907
+ // src/wallet/identity/walletIdentity.ts
7908
+ function deriveIdentity(rootSecret, version) {
7909
+ if (rootSecret.length !== 32) {
7910
+ throw new Error(`deriveIdentity: rootSecret must be 32 bytes, got ${rootSecret.length}`);
7911
+ }
7912
+ if (rootSecret.every((b) => b === 0)) {
7913
+ throw new Error("deriveIdentity: rootSecret is all zeros \u2014 every wallet would share it");
7914
+ }
7915
+ if (version !== "v3") {
7916
+ throw new Error(`deriveIdentity: unknown identity version ${String(version)}`);
7917
+ }
7918
+ const spendingKey = deriveSpendingKeyV3(rootSecret);
7919
+ const viewingSecretKey = deriveViewingSecretKeyV3(rootSecret);
7920
+ return {
7921
+ version,
7922
+ spendingKey,
7923
+ ownerPk: deriveOwnerPk(spendingKey),
7924
+ viewingSecretKey,
7925
+ viewingPublicKey: deriveViewingPublicKey(viewingSecretKey),
7926
+ outgoingViewingKey: deriveOutgoingViewingKeyV3(rootSecret)
7927
+ };
7928
+ }
7929
+ function exportViewingCredential(identity, options = {}) {
7930
+ return {
7931
+ version: identity.version,
7932
+ ownerPk: identity.ownerPk,
7933
+ viewingSecretKey: Uint8Array.from(identity.viewingSecretKey),
7934
+ viewingPublicKey: Uint8Array.from(identity.viewingPublicKey),
7935
+ ...options.includeOutgoing && identity.outgoingViewingKey ? { outgoingViewingKey: Uint8Array.from(identity.outgoingViewingKey) } : {}
7936
+ };
7628
7937
  }
7629
7938
 
7630
7939
  // src/wallet/identity/injectedExtensions.ts
@@ -7648,23 +7957,21 @@ var cachedWindow = null;
7648
7957
  function clearKnownEphWindow() {
7649
7958
  cachedWindow = null;
7650
7959
  }
7651
- function bytesKey(bytes) {
7652
- let out = "";
7653
- for (const b of bytes) out += b.toString(16).padStart(2, "0");
7654
- return out;
7655
- }
7656
7960
  function getKnownEphWindow(keys) {
7657
7961
  const selfSize = keys.selfEphWindowSize ?? SELF_EPH_WINDOW;
7658
7962
  const pairSize = keys.pairwiseWindowSize ?? PAIRWISE_EPH_WINDOW;
7963
+ const outSize = keys.outgoingEphWindowSize ?? OUTGOING_EPH_WINDOW;
7659
7964
  const counterparties = keys.pairwiseCounterparties ?? [];
7660
- if (!keys.selfEph && counterparties.length === 0) return null;
7661
- const cacheKey = `${keys.spendingKey.toString(16)}:${keys.selfEph ? selfSize : 0}:${pairSize}:` + counterparties.map((c) => bytesKey(c)).join(",");
7965
+ const canScanOutgoing = keys.outgoingEph === true && isUsableSecretKey(keys.outgoingViewingKey);
7966
+ if (!keys.selfEph && !canScanOutgoing && counterparties.length === 0) return null;
7967
+ const cacheKey = `${keys.spendingKey.toString(16)}:${keys.selfEph ? selfSize : 0}:${pairSize}:${canScanOutgoing ? outSize : 0}:` + counterparties.map((c) => toHex(c)).join(",");
7662
7968
  if (cachedWindow?.cacheKey === cacheKey) return cachedWindow.window;
7663
7969
  try {
7664
7970
  const ivkPacked = deriveViewingPublicKey(keys.viewingKey);
7665
7971
  const byEphPk = /* @__PURE__ */ new Map();
7972
+ const outgoingByEphPk = /* @__PURE__ */ new Map();
7666
7973
  if (keys.selfEph) {
7667
- for (const e of selfEphWindow(keys.spendingKey, ivkPacked, 0, selfSize)) {
7974
+ for (const e of selfEphWindow(keys.viewingKey, ivkPacked, 0, selfSize)) {
7668
7975
  byEphPk.set(e.ephPkHex.toLowerCase(), {
7669
7976
  sharedSecret: e.sharedSecret,
7670
7977
  index: e.index,
@@ -7685,7 +7992,12 @@ function getKnownEphWindow(keys) {
7685
7992
  }
7686
7993
  }
7687
7994
  }
7688
- cachedWindow = { cacheKey, window: { byEphPk } };
7995
+ if (canScanOutgoing) {
7996
+ for (const e of outgoingEphWindow(keys.outgoingViewingKey, 0, outSize)) {
7997
+ outgoingByEphPk.set(e.ephPkHex.toLowerCase(), e.index);
7998
+ }
7999
+ }
8000
+ cachedWindow = { cacheKey, window: { byEphPk, outgoingByEphPk } };
7689
8001
  return cachedWindow.window;
7690
8002
  } catch {
7691
8003
  return null;
@@ -7694,21 +8006,32 @@ function getKnownEphWindow(keys) {
7694
8006
 
7695
8007
  // src/wallet/worker/kernel/decryptBatch.ts
7696
8008
  function hintEphPkHex(hint) {
7697
- if (hint.ephPkHex) return hint.ephPkHex.toLowerCase();
7698
8009
  const memo = hint.encryptedMemo;
7699
8010
  if (!memo || memo.length < 66) return null;
7700
8011
  return ("0x" + memo.slice(-64)).toLowerCase();
7701
8012
  }
8013
+ function sealedBookEntry(note) {
8014
+ if (note.sourcePk === void 0 || note.sourcePk === 0n) return null;
8015
+ return note.sourcePk;
8016
+ }
7702
8017
  function decryptHintBatch(hints, keys) {
7703
8018
  const activation = keys.viewTagActivationLeaf ?? null;
7704
8019
  const knownWindow = getKnownEphWindow(keys);
8020
+ const sentNotes = [];
8021
+ const learnedRecipients = /* @__PURE__ */ new Set();
7705
8022
  let tagFiltered = 0;
7706
8023
  let selfMatched = 0;
7707
8024
  let pairwiseMatched = 0;
7708
8025
  let maxSelfEphIndex = null;
8026
+ let maxOutgoingEphIndex = null;
8027
+ const sealedEntries = [];
8028
+ const pendingSent = [];
8029
+ const seenSent = /* @__PURE__ */ new Set();
8030
+ const unmatchedSent = [];
7709
8031
  const notes = hints.map((hint) => {
7710
8032
  try {
7711
- const known = knownWindow?.byEphPk.get(hintEphPkHex(hint) ?? "");
8033
+ const ephPkHex = hintEphPkHex(hint) ?? "";
8034
+ const known = knownWindow?.byEphPk.get(ephPkHex);
7712
8035
  if (known) {
7713
8036
  const result2 = tryDecryptNoteVerbose(
7714
8037
  hint,
@@ -7723,12 +8046,26 @@ function decryptHintBatch(hints, keys) {
7723
8046
  if (maxSelfEphIndex === null || known.index > maxSelfEphIndex) {
7724
8047
  maxSelfEphIndex = known.index;
7725
8048
  }
8049
+ const sealed = sealedBookEntry(result2.note);
8050
+ if (sealed !== null) sealedEntries.push(sealed);
7726
8051
  } else {
7727
8052
  pairwiseMatched++;
7728
8053
  }
7729
8054
  return result2.note;
7730
8055
  }
7731
8056
  }
8057
+ const outgoingIndex = knownWindow?.outgoingByEphPk.get(ephPkHex);
8058
+ if (outgoingIndex !== void 0) {
8059
+ if (maxOutgoingEphIndex === null || outgoingIndex > maxOutgoingEphIndex) {
8060
+ maxOutgoingEphIndex = outgoingIndex;
8061
+ }
8062
+ const key = hint.commitmentHex.toLowerCase();
8063
+ if (!seenSent.has(key)) {
8064
+ seenSent.add(key);
8065
+ pendingSent.push({ hint, ephIndex: outgoingIndex });
8066
+ }
8067
+ return null;
8068
+ }
7732
8069
  const viewTag = activation !== null && hint.leafIndex >= activation;
7733
8070
  const result = tryDecryptNoteVerbose(
7734
8071
  hint,
@@ -7743,7 +8080,44 @@ function decryptHintBatch(hints, keys) {
7743
8080
  return null;
7744
8081
  }
7745
8082
  });
7746
- return { notes, tagFiltered, selfMatched, pairwiseMatched, maxSelfEphIndex };
8083
+ const carried = keys.recipientCandidates ?? [];
8084
+ for (const { hint, ephIndex } of pendingSent) {
8085
+ const candidates = [
8086
+ ...sealedEntries.map(
8087
+ (e) => openRecipientBookEntry(e, keys.outgoingViewingKey, hint.commitmentHex)
8088
+ ),
8089
+ ...carried
8090
+ ];
8091
+ const sent = recoverSentNote({
8092
+ hint,
8093
+ outgoingViewingKey: keys.outgoingViewingKey,
8094
+ ephIndex,
8095
+ recipientCandidates: candidates
8096
+ });
8097
+ if (!sent) {
8098
+ unmatchedSent.push({ hint, ephIndex });
8099
+ continue;
8100
+ }
8101
+ const { recipientIvk, ...facts } = sent;
8102
+ learnedRecipients.add(toHex(recipientIvk));
8103
+ sentNotes.push({
8104
+ ...facts,
8105
+ counterpartyIvkHex: toHex(recipientIvk),
8106
+ encryptedMemo: hint.encryptedMemo ?? ""
8107
+ });
8108
+ }
8109
+ return {
8110
+ notes,
8111
+ tagFiltered,
8112
+ selfMatched,
8113
+ pairwiseMatched,
8114
+ maxSelfEphIndex,
8115
+ maxOutgoingEphIndex,
8116
+ sentNotes,
8117
+ learnedRecipients: [...learnedRecipients],
8118
+ unmatchedSent,
8119
+ sealedBookEntries: sealedEntries.map((e) => e.toString())
8120
+ };
7747
8121
  }
7748
8122
 
7749
8123
  // src/wallet/worker/pool/types.ts
@@ -7765,21 +8139,52 @@ function createMainThreadPool() {
7765
8139
  let selfMatched = 0;
7766
8140
  let pairwiseMatched = 0;
7767
8141
  let maxSelfEphIndex = null;
8142
+ let maxOutgoingEphIndex = null;
8143
+ const sentNotes = [];
8144
+ const unmatchedSent = [];
8145
+ const sealedBookEntries = /* @__PURE__ */ new Set();
8146
+ const learnedRecipients = new Set(
8147
+ (keys.recipientCandidates ?? []).map((c) => toHex(c))
8148
+ );
7768
8149
  for (let i = 0; i < hints.length; i += DECRYPT_YIELD_EVERY) {
7769
8150
  if (i > 0) {
7770
8151
  await yieldToBrowser();
7771
8152
  if (signal?.aborted) throw scanAbortError();
7772
8153
  }
7773
- const burst = decryptHintBatch(hints.slice(i, i + DECRYPT_YIELD_EVERY), keys);
8154
+ const burst = decryptHintBatch(hints.slice(i, i + DECRYPT_YIELD_EVERY), {
8155
+ ...keys,
8156
+ recipientCandidates: [...learnedRecipients].map((h) => fromHex(h))
8157
+ });
7774
8158
  notes.push(...burst.notes);
7775
8159
  tagFiltered += burst.tagFiltered;
7776
8160
  selfMatched += burst.selfMatched;
7777
8161
  pairwiseMatched += burst.pairwiseMatched;
8162
+ sentNotes.push(...burst.sentNotes ?? []);
8163
+ unmatchedSent.push(...burst.unmatchedSent ?? []);
8164
+ for (const e of burst.sealedBookEntries ?? []) sealedBookEntries.add(e);
8165
+ for (const r of burst.learnedRecipients ?? []) learnedRecipients.add(r);
7778
8166
  if (burst.maxSelfEphIndex !== null) {
7779
8167
  maxSelfEphIndex = Math.max(maxSelfEphIndex ?? -1, burst.maxSelfEphIndex);
7780
8168
  }
8169
+ if (burst.maxOutgoingEphIndex != null) {
8170
+ maxOutgoingEphIndex = Math.max(
8171
+ maxOutgoingEphIndex ?? -1,
8172
+ burst.maxOutgoingEphIndex
8173
+ );
8174
+ }
7781
8175
  }
7782
- return { notes, tagFiltered, selfMatched, pairwiseMatched, maxSelfEphIndex };
8176
+ return {
8177
+ notes,
8178
+ tagFiltered,
8179
+ selfMatched,
8180
+ pairwiseMatched,
8181
+ maxSelfEphIndex,
8182
+ maxOutgoingEphIndex,
8183
+ sentNotes,
8184
+ learnedRecipients: [...learnedRecipients],
8185
+ unmatchedSent,
8186
+ sealedBookEntries: [...sealedBookEntries]
8187
+ };
7783
8188
  },
7784
8189
  terminate() {
7785
8190
  clearKnownEphWindow();
@@ -7811,7 +8216,12 @@ function runOnWorker(worker, payload, signal) {
7811
8216
  tagFiltered: data.tagFiltered ?? 0,
7812
8217
  selfMatched: data.selfMatched ?? 0,
7813
8218
  pairwiseMatched: data.pairwiseMatched ?? 0,
7814
- maxSelfEphIndex: data.maxSelfEphIndex ?? null
8219
+ maxSelfEphIndex: data.maxSelfEphIndex ?? null,
8220
+ maxOutgoingEphIndex: data.maxOutgoingEphIndex ?? null,
8221
+ sentNotes: data.sentNotes ?? [],
8222
+ learnedRecipients: data.learnedRecipients ?? [],
8223
+ unmatchedSent: data.unmatchedSent ?? [],
8224
+ sealedBookEntries: data.sealedBookEntries ?? []
7815
8225
  });
7816
8226
  };
7817
8227
  worker.onerror = () => {
@@ -7838,9 +8248,19 @@ function mergeResults(results) {
7838
8248
  tagFiltered: results.reduce((sum, r) => sum + r.tagFiltered, 0),
7839
8249
  selfMatched: results.reduce((sum, r) => sum + r.selfMatched, 0),
7840
8250
  pairwiseMatched: results.reduce((sum, r) => sum + r.pairwiseMatched, 0),
8251
+ sentNotes: results.flatMap((r) => r.sentNotes ?? []),
8252
+ // Deduped: slices run in parallel, so the same change note cannot be
8253
+ // seen twice, but two different ones may name the same recipient.
8254
+ learnedRecipients: [...new Set(results.flatMap((r) => r.learnedRecipients ?? []))],
8255
+ unmatchedSent: results.flatMap((r) => r.unmatchedSent ?? []),
8256
+ sealedBookEntries: [...new Set(results.flatMap((r) => r.sealedBookEntries ?? []))],
7841
8257
  maxSelfEphIndex: results.reduce(
7842
8258
  (max, r) => r.maxSelfEphIndex === null ? max : Math.max(max ?? -1, r.maxSelfEphIndex),
7843
8259
  null
8260
+ ),
8261
+ maxOutgoingEphIndex: results.reduce(
8262
+ (max, r) => r.maxOutgoingEphIndex == null ? max : Math.max(max ?? -1, r.maxOutgoingEphIndex),
8263
+ null
7844
8264
  )
7845
8265
  };
7846
8266
  }
@@ -7904,25 +8324,36 @@ var OrbinumWallet = class {
7904
8324
  session;
7905
8325
  notes;
7906
8326
  vaultStore;
7907
- /** Set at unlock; every key below is derived from it. */
7908
- spendingKey = null;
8327
+ /**
8328
+ * The identity this wallet is unlocked as, or null.
8329
+ *
8330
+ * Held as a bundle rather than a single scalar because v3 keys are SIBLINGS:
8331
+ * the spending key no longer produces the viewing key, so there is nothing
8332
+ * left to derive the others from once unlock returns. Keeping the root out
8333
+ * of this object is deliberate — the branches are what the wallet needs, and
8334
+ * the root is the thing worth not retaining.
8335
+ */
8336
+ identity = null;
7909
8337
  /** The underlying store, for operations the facade does not cover. */
7910
8338
  get vault() {
7911
8339
  return this.vaultStore;
7912
8340
  }
7913
8341
  get unlocked() {
7914
- return this.session.unlocked && this.spendingKey !== null;
8342
+ return this.session.unlocked && this.identity !== null;
7915
8343
  }
7916
8344
  /**
7917
8345
  * Opens the wallet from the master key bytes a `SpendingKeyRequest`
7918
8346
  * signature produces — the same value `PrivacyKeyManager.getMasterBytes()`
7919
8347
  * returns.
7920
8348
  *
7921
- * Master bytes rather than the spending-key scalar, deliberately: the scalar
7922
- * is the master reduced modulo the curve order, so deriving the vault key
7923
- * from it would tie the encrypted vault to the CURRENT modulus. The master
7924
- * is pre-reduction, which keeps a stored vault readable across a modulus
7925
- * change. Everything else viewing keys, ownerPk — comes from the scalar.
8349
+ * Master bytes rather than the spending-key scalar, deliberately: the master
8350
+ * is the root EVERY branch hangs off spending key, viewing key, outgoing
8351
+ * viewing key and vault key are each their own HKDF of it, none derived
8352
+ * from another. Only `ownerPk` comes from the spending scalar, because the
8353
+ * circuit defines it that way.
8354
+ *
8355
+ * It is also what keeps a stored vault readable across a modulus change:
8356
+ * the vault key is derived pre-reduction.
7926
8357
  *
7927
8358
  * Reports whether the stored vault was RESET: a different chain, a different
7928
8359
  * key, or notes the chain no longer recognises. When it was, the wallet
@@ -7936,14 +8367,12 @@ var OrbinumWallet = class {
7936
8367
  * against a vault that was never read.
7937
8368
  */
7938
8369
  async unlock(masterBytes, options = {}) {
7939
- if (masterBytes.length !== 32) {
7940
- throw new Error(`Master key must be 32 bytes, got ${masterBytes.length}.`);
7941
- }
8370
+ assertSecretKeyBytes(masterBytes, "Master key");
7942
8371
  this.session.open(
7943
8372
  await deriveVaultKey(masterBytes),
7944
8373
  await deriveVaultBlindKey(masterBytes)
7945
8374
  );
7946
- this.spendingKey = deriveSpendingKeyFromMaster(masterBytes);
8375
+ this.identity = deriveIdentity(masterBytes, this.config.identityVersion ?? "v3");
7947
8376
  try {
7948
8377
  return await this.vaultStore.unlock(this.session.cryptoKey, options);
7949
8378
  } catch (err) {
@@ -7954,8 +8383,12 @@ var OrbinumWallet = class {
7954
8383
  /** Drops the in-memory keys. The stored vault is untouched. */
7955
8384
  lock() {
7956
8385
  this.session.lock();
7957
- this.spendingKey = null;
8386
+ this.identity = null;
7958
8387
  this.notes.set([]);
8388
+ try {
8389
+ this.config.pool.terminate();
8390
+ } catch {
8391
+ }
7959
8392
  }
7960
8393
  /** The notes currently held. Empty while locked. */
7961
8394
  getNotes() {
@@ -7967,11 +8400,8 @@ var OrbinumWallet = class {
7967
8400
  }
7968
8401
  /** This wallet's privacy identity: the keys a payer needs to address it. */
7969
8402
  privacyKeys() {
7970
- const spendingKey = this.requireKey();
7971
- return {
7972
- ownerPk: deriveOwnerPk(spendingKey),
7973
- viewingPublicKey: deriveViewingPublicKey(deriveViewingSecretKey(spendingKey))
7974
- };
8403
+ const { ownerPk, viewingPublicKey } = this.requireIdentity();
8404
+ return { ownerPk, viewingPublicKey };
7975
8405
  }
7976
8406
  /**
7977
8407
  * Walks the feed, recovers this wallet's notes, and persists them with their
@@ -7981,15 +8411,21 @@ var OrbinumWallet = class {
7981
8411
  * `sinceLeafIndex` for a full scan, which is what a restore needs.
7982
8412
  */
7983
8413
  async scan(options = {}) {
7984
- const spendingKey = this.requireKey();
7985
- const viewingKey = deriveViewingSecretKey(spendingKey);
8414
+ const identity = this.requireIdentity();
7986
8415
  return runScan({
7987
8416
  vault: this.vaultStore,
7988
8417
  storage: this.config.storage,
7989
8418
  hints: this.config.hints,
7990
8419
  nullifiers: this.config.nullifiers,
7991
8420
  pool: this.config.pool,
7992
- keys: { viewingKey, spendingKey, ownerPk: deriveOwnerPk(spendingKey) },
8421
+ keys: {
8422
+ viewingKey: identity.viewingSecretKey,
8423
+ spendingKey: identity.spendingKey,
8424
+ ownerPk: identity.ownerPk,
8425
+ // Absent on a watch-only identity: the scan then finds every
8426
+ // note the wallet owns and reconstructs no payment history.
8427
+ ...identity.outgoingViewingKey ? { outgoingViewingKey: identity.outgoingViewingKey } : {}
8428
+ },
7993
8429
  sinceLeafIndex: options.sinceLeafIndex,
7994
8430
  viewTagActivationLeaf: this.config.viewTagActivationLeaf,
7995
8431
  onProgress: options.onProgress,
@@ -8011,13 +8447,14 @@ var OrbinumWallet = class {
8011
8447
  * reusing an index would republish an ephPk and link the two notes.
8012
8448
  */
8013
8449
  async buildNote(params) {
8014
- const spendingKey = this.requireKey();
8450
+ const identity = this.requireIdentity();
8015
8451
  return buildZkNote(params, {
8016
8452
  keys: {
8017
- ownerPk: deriveOwnerPk(spendingKey),
8018
- spendingKey,
8019
- viewingPublicKey: deriveViewingPublicKey(deriveViewingSecretKey(spendingKey)),
8020
- viewingSecretKey: deriveViewingSecretKey(spendingKey)
8453
+ ownerPk: identity.ownerPk,
8454
+ spendingKey: identity.spendingKey,
8455
+ viewingPublicKey: identity.viewingPublicKey,
8456
+ viewingSecretKey: identity.viewingSecretKey,
8457
+ ...identity.outgoingViewingKey ? { outgoingViewingKey: identity.outgoingViewingKey } : {}
8021
8458
  },
8022
8459
  storage: this.config.storage
8023
8460
  });
@@ -8034,11 +8471,12 @@ var OrbinumWallet = class {
8034
8471
  * Treat the result as secret: it can spend every note this wallet holds.
8035
8472
  */
8036
8473
  spendKeys() {
8037
- const spendingKey = this.requireKey();
8474
+ const identity = this.requireIdentity();
8038
8475
  return {
8039
- spendingKey,
8040
- viewingSecretKey: deriveViewingSecretKey(spendingKey),
8041
- ownerPk: deriveOwnerPk(spendingKey)
8476
+ spendingKey: identity.spendingKey,
8477
+ viewingSecretKey: identity.viewingSecretKey,
8478
+ ownerPk: identity.ownerPk,
8479
+ ...identity.outgoingViewingKey ? { outgoingViewingKey: identity.outgoingViewingKey } : {}
8042
8480
  };
8043
8481
  }
8044
8482
  /**
@@ -8060,7 +8498,20 @@ var OrbinumWallet = class {
8060
8498
  * verifying-key rotation, and the failure surfaces much later.
8061
8499
  */
8062
8500
  buildOutputNote = async (params) => {
8063
- return this.buildNote({ ...params, circuitVersion: await this.circuitVersion() });
8501
+ const identity = this.requireIdentity();
8502
+ return buildZkNoteWithIndex(
8503
+ { ...params, circuitVersion: await this.circuitVersion() },
8504
+ {
8505
+ keys: {
8506
+ ownerPk: identity.ownerPk,
8507
+ spendingKey: identity.spendingKey,
8508
+ viewingPublicKey: identity.viewingPublicKey,
8509
+ viewingSecretKey: identity.viewingSecretKey,
8510
+ ...identity.outgoingViewingKey ? { outgoingViewingKey: identity.outgoingViewingKey } : {}
8511
+ },
8512
+ storage: this.config.storage
8513
+ }
8514
+ );
8064
8515
  };
8065
8516
  /** The circuit version a new note should carry. */
8066
8517
  async circuitVersion() {
@@ -8072,11 +8523,11 @@ var OrbinumWallet = class {
8072
8523
  }
8073
8524
  return chainActiveCircuitVersion(this.config.zkVerifier);
8074
8525
  }
8075
- requireKey() {
8076
- if (this.spendingKey === null) {
8526
+ requireIdentity() {
8527
+ if (this.identity === null) {
8077
8528
  throw new Error("Wallet is locked. Call unlock() before using it.");
8078
8529
  }
8079
- return this.spendingKey;
8530
+ return this.identity;
8080
8531
  }
8081
8532
  };
8082
8533
  // Annotate the CommonJS export names for ESM import in node:
@@ -8108,9 +8559,8 @@ var OrbinumWallet = class {
8108
8559
  NATIVE_ASSET_ID,
8109
8560
  NOTE_BACKUP_VERSION,
8110
8561
  NOTE_BIGINT_FIELDS,
8111
- NOTE_TRANSFER_URI_SCHEME,
8112
8562
  NoteBuilder,
8113
- OVK_BLOB_SIZE,
8563
+ OUTGOING_EPH_WINDOW,
8114
8564
  OrbinumClient,
8115
8565
  OrbinumClientProvider,
8116
8566
  OrbinumWallet,
@@ -8120,7 +8570,6 @@ var OrbinumWallet = class {
8120
8570
  PRECOMPILE_ADDR,
8121
8571
  PrivacyKeyManager,
8122
8572
  PrivacyModule,
8123
- QR_PAGE_MAX_CHARS,
8124
8573
  RECOVERED_TX_RESULT,
8125
8574
  RelayerStatusModule,
8126
8575
  RpcV2Module,
@@ -8145,7 +8594,7 @@ var OrbinumWallet = class {
8145
8594
  addressToFieldElement,
8146
8595
  applyBatch,
8147
8596
  applyNoteStatus,
8148
- assembleNoteTransfer,
8597
+ assertSecretKeyBytes,
8149
8598
  base58,
8150
8599
  base64UrlDecode,
8151
8600
  base64UrlEncode,
@@ -8158,6 +8607,7 @@ var OrbinumWallet = class {
8158
8607
  buildShieldBatchOperations,
8159
8608
  buildShieldParams,
8160
8609
  buildZkNote,
8610
+ buildZkNoteWithIndex,
8161
8611
  bytesToBigintLE,
8162
8612
  bytesToBjjScalar,
8163
8613
  cacheSession,
@@ -8187,15 +8637,17 @@ var OrbinumWallet = class {
8187
8637
  createWorkerPool,
8188
8638
  decodeNoteBackup,
8189
8639
  decodeNoteDisclosureKey,
8190
- decodeNoteTransferPage,
8191
8640
  decodePaymentSlip,
8192
8641
  decodePrecompileCalldata,
8193
8642
  decryptHintBatch,
8194
8643
  decryptJson,
8195
8644
  decryptNoteRecord,
8645
+ deriveIdentity,
8196
8646
  deriveMasterKeyBytes,
8197
- deriveOutgoingCipherKey,
8198
- deriveOutgoingViewingKey,
8647
+ deriveOutgoingEphPk,
8648
+ deriveOutgoingEphSk,
8649
+ deriveOutgoingSharedSecret,
8650
+ deriveOutgoingViewingKeyV3,
8199
8651
  deriveOwnerPk,
8200
8652
  derivePairwiseEphSk,
8201
8653
  derivePairwiseSharedSecret,
@@ -8204,6 +8656,7 @@ var OrbinumWallet = class {
8204
8656
  deriveSpendingKeyFromSignature,
8205
8657
  deriveSpendingKeyMessageV2,
8206
8658
  deriveSpendingKeyTypedData,
8659
+ deriveSpendingKeyV3,
8207
8660
  deriveStealthOwnerPk,
8208
8661
  deriveStealthSk,
8209
8662
  deriveVaultBlindKey,
@@ -8211,9 +8664,9 @@ var OrbinumWallet = class {
8211
8664
  deriveViewTag,
8212
8665
  deriveViewingPublicKey,
8213
8666
  deriveViewingSecretKey,
8667
+ deriveViewingSecretKeyV3,
8214
8668
  detectCommitmentMismatch,
8215
8669
  encodeNoteBackup,
8216
- encodeNoteTransferPages,
8217
8670
  encodePaymentSlip,
8218
8671
  encryptJson,
8219
8672
  encryptNote,
@@ -8223,6 +8676,7 @@ var OrbinumWallet = class {
8223
8676
  evmToImplicitSubstrate,
8224
8677
  evmToMappedAccountHex,
8225
8678
  evmToSubstrate,
8679
+ exportViewingCredential,
8226
8680
  extractPalletError,
8227
8681
  failed,
8228
8682
  fastMulBase,
@@ -8263,9 +8717,9 @@ var OrbinumWallet = class {
8263
8717
  isNativeAsset,
8264
8718
  isNoteSelfConsistent,
8265
8719
  isSpendable,
8266
- isSs58,
8267
8720
  isSubstrateAddress,
8268
8721
  isUnifiedAddress,
8722
+ isUsableSecretKey,
8269
8723
  isValidLeafIndex,
8270
8724
  leHexToBigint,
8271
8725
  mapExtrinsicArgs,
@@ -8282,10 +8736,10 @@ var OrbinumWallet = class {
8282
8736
  noteMatchesCommitment,
8283
8737
  noteOrigin,
8284
8738
  noteSpentTxHash,
8285
- noteToTransferEntry,
8286
8739
  noteTxKind,
8287
- openOutgoingBlob,
8288
8740
  openPaymentSlip,
8741
+ openRecipientBookEntry,
8742
+ outgoingEphWindow,
8289
8743
  outranks,
8290
8744
  pairwiseEphWindow,
8291
8745
  palletErrorKind,
@@ -8296,15 +8750,18 @@ var OrbinumWallet = class {
8296
8750
  planTransfer,
8297
8751
  planUnshield,
8298
8752
  randomBlinding,
8299
- randomOutgoingBlob,
8753
+ reconstructOutgoingIndex,
8300
8754
  reconstructOutgoingTxRecords,
8301
8755
  recoverOwnerPkPoint,
8302
8756
  recoverSelfStealthNote,
8757
+ recoverSentFromSharedSecret,
8758
+ recoverSentNote,
8303
8759
  refuseIfAlreadySpent,
8304
8760
  regeneratePaymentSlip,
8761
+ registerPairwiseCounterparty,
8305
8762
  removeByCommitment,
8306
8763
  requireSessionKeys,
8307
- reservePairwiseIndex,
8764
+ reserveOutgoingIndex,
8308
8765
  reserveSelfEphIndex,
8309
8766
  resolveSelfEphCeiling,
8310
8767
  resolveSpentSet,
@@ -8313,8 +8770,8 @@ var OrbinumWallet = class {
8313
8770
  runScan,
8314
8771
  scalarToHex,
8315
8772
  scanAbortError,
8316
- sealOutgoingBlob,
8317
8773
  sealPaymentSlip,
8774
+ sealRecipientBookEntry,
8318
8775
  selectDescribingNote,
8319
8776
  selectDescribingNoteByCommitment,
8320
8777
  selectGhosts,
@@ -8340,10 +8797,10 @@ var OrbinumWallet = class {
8340
8797
  truncateMiddle,
8341
8798
  tryDecryptNote,
8342
8799
  tryDecryptNoteVerbose,
8343
- tryRecoverOutgoing,
8344
8800
  txLandedAfterError,
8345
8801
  u128,
8346
8802
  u64,
8803
+ unpackUsableViewingKey,
8347
8804
  unshieldNote,
8348
8805
  upsertNote,
8349
8806
  vaultReplacer,