@orbinum/sdk 2.0.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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;
2559
- } catch {
2560
- return false;
2561
- }
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;
2700
+ if (b.length < 5 || b[2] !== TIMESTAMP_PALLET || b[3] !== 0) return null;
2701
+ return decodeCompact(b, 4);
2571
2702
  } catch {
2703
+ return null;
2572
2704
  }
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) {
@@ -3460,7 +3599,11 @@ var ShieldedPoolModule = class {
3460
3599
  * Withdraws tokens from the shielded pool to a public address.
3461
3600
  * Submits as an UNSIGNED (gasless) transaction — fee is embedded in the ZK proof.
3462
3601
  * Pass a `signer` to fall back to signed submission (e.g. for testing).
3463
- * Extrinsic: shieldedPool.unshield(proof, merkleRoot, nullifier, assetId, amount, recipient, fee, changeCommitment, changeEncryptedMemo, relayer, circuitVersion)
3602
+ * Extrinsic: shieldedPool.unshield(proof, merkleRoot, nullifier, assetId, amount, recipient, fee, changeCommitment, changeEncryptedMemo, circuitVersion)
3603
+ *
3604
+ * The relay fee recipient is NOT a parameter: the chain takes it from the
3605
+ * dispatch origin. Submitting unsigned credits the block author; submitting
3606
+ * through the EVM precompile credits whoever signed that transaction.
3464
3607
  */
3465
3608
  async unshield(params, signer, options) {
3466
3609
  const entry = resolveTx(this.substrate.unsafe, "ShieldedPool", "unshield");
@@ -3484,8 +3627,6 @@ var ShieldedPoolModule = class {
3484
3627
  fee: params.fee ?? 0n,
3485
3628
  change_commitment: changeCommitment,
3486
3629
  change_encrypted_memo: changeEncryptedMemo,
3487
- relayer: void 0,
3488
- // Option<H160> — None for direct Substrate submissions
3489
3630
  circuit_version: params.circuitVersion
3490
3631
  });
3491
3632
  if (signer) {
@@ -3497,7 +3638,11 @@ var ShieldedPoolModule = class {
3497
3638
  * Performs a private (shielded) transfer between two notes.
3498
3639
  * Submits as an UNSIGNED (gasless) transaction — fee is embedded in the ZK proof.
3499
3640
  * Pass a `signer` to fall back to signed submission (e.g. for testing).
3500
- * Extrinsic: shieldedPool.privateTransfer(proof, merkleRoot, nullifiers, commitments, memos, assetId, fee, relayer, circuitVersion)
3641
+ * Extrinsic: shieldedPool.privateTransfer(proof, merkleRoot, nullifiers, commitments, memos, assetId, fee, circuitVersion)
3642
+ *
3643
+ * The relay fee recipient is NOT a parameter: the chain takes it from the
3644
+ * dispatch origin. Submitting unsigned credits the block author; submitting
3645
+ * through the EVM precompile credits whoever signed that transaction.
3501
3646
  */
3502
3647
  async privateTransfer(params, signer, options) {
3503
3648
  const nullifiers = params.inputs.map((inp) => inp.nullifier);
@@ -3518,8 +3663,6 @@ var ShieldedPoolModule = class {
3518
3663
  encrypted_memos: memos,
3519
3664
  asset_id: params.assetId,
3520
3665
  fee: params.fee ?? 0n,
3521
- relayer: void 0,
3522
- // Option<H160> — None for direct Substrate submissions
3523
3666
  circuit_version: params.circuitVersion
3524
3667
  });
3525
3668
  if (signer) {
@@ -3602,7 +3745,13 @@ var PrivacyModule = class {
3602
3745
  async getMerkleRoot() {
3603
3746
  return this.substrate.request("privacy_getMerkleRoot", []);
3604
3747
  }
3605
- /** 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
+ */
3606
3755
  async getMerkleProof(leafIndex) {
3607
3756
  const raw = await this.substrate.request("privacy_getMerkleProof", [
3608
3757
  leafIndex
@@ -4036,7 +4185,9 @@ var ShieldedPoolPrecompile = class {
4036
4185
  * hidden by design, so any address (a relayer included) can submit a valid
4037
4186
  * proof.
4038
4187
  *
4039
- * Extrinsic: `shieldedPool.privateTransfer(proof, merkleRoot, nullifiers, commitments, memos)`
4188
+ * Extrinsic: `shieldedPool.privateTransfer(proof, merkleRoot, nullifiers,
4189
+ * commitments, memos, assetId, fee, circuitVersion)` — eight arguments; see
4190
+ * `buildPrivateTransferCalldata` for the encoding order.
4040
4191
  */
4041
4192
  async privateTransfer(params, signer) {
4042
4193
  return signer({ to: this.addr, data: buildPrivateTransferCalldata(params) });
@@ -4048,7 +4199,14 @@ var ShieldedPoolPrecompile = class {
4048
4199
  * `params.recipientAddress` must be a 0x-prefixed AccountId32. To send to an
4049
4200
  * EVM address, derive it first with `evmToImplicitSubstrate(evmAddr)`.
4050
4201
  *
4051
- * Extrinsic: `shieldedPool.unshield(proof, merkleRoot, nullifier, assetId, amount, recipient)`
4202
+ * Extrinsic: `shieldedPool.unshield(proof, merkleRoot, nullifier, assetId,
4203
+ * amount, recipient, fee, changeCommitment, changeEncryptedMemo,
4204
+ * circuitVersion)` — ten arguments; see `buildUnshieldCalldata`.
4205
+ *
4206
+ * **The relay fee goes to whoever `signer` is.** The chain takes the recipient
4207
+ * from `msg.sender`, not from calldata, so the account behind this signer is
4208
+ * the one credited — and it is also the one paying gas. Relaying on someone
4209
+ * else's behalf and being paid for it is the same act here.
4052
4210
  */
4053
4211
  async unshield(params, signer) {
4054
4212
  return signer({ to: this.addr, data: buildUnshieldCalldata(params) });
@@ -4067,7 +4225,9 @@ var ShieldedPoolPrecompile = class {
4067
4225
  * The `msg.sender` address is the validator identity, and must match the
4068
4226
  * one with pending fees.
4069
4227
  *
4070
- * Extrinsic: `shieldedPool.claim_shielded_fees(commitment, amount, assetId, memo, proof, publicSignals)`
4228
+ * Extrinsic: `shieldedPool.claim_shielded_fees(commitment, amount, assetId,
4229
+ * memo, proof, publicSignals, circuitVersion)` — seven arguments; see
4230
+ * `buildClaimShieldedFeesCalldata`.
4071
4231
  */
4072
4232
  async claimShieldedFees(params, signer) {
4073
4233
  return signer({
@@ -4081,7 +4241,12 @@ var ShieldedPoolPrecompile = class {
4081
4241
  // AccountId32, so estimating from a different address measures a different
4082
4242
  // call.
4083
4243
  async estimateShieldGas(params, from) {
4084
- return this.evm.estimateGas({ from, to: this.addr, data: buildShieldCalldata(params) });
4244
+ return this.evm.estimateGas({
4245
+ from,
4246
+ to: this.addr,
4247
+ data: buildShieldCalldata(params),
4248
+ value: `0x${params.amount.toString(16)}`
4249
+ });
4085
4250
  }
4086
4251
  async estimatePrivateTransferGas(params, from) {
4087
4252
  return this.evm.estimateGas({
@@ -4536,9 +4701,13 @@ var OrbinumClientProvider = class _OrbinumClientProvider {
4536
4701
  // ─── Reconnection ───────────────────────────────────────────────────────
4537
4702
  /**
4538
4703
  * Schedules the next connection attempt using exponential backoff
4539
- * (capped at `reconnectMaxMs`) with full jitter, then transitions to
4540
- * `'reconnecting'`. Jitter (a random fraction of the delay) spreads out
4541
- * 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.
4542
4711
  */
4543
4712
  scheduleReconnect() {
4544
4713
  if (this._reconnectTimer) clearTimeout(this._reconnectTimer);
@@ -4930,6 +5099,16 @@ function mapZkEventData(method, data) {
4930
5099
  index: get(4, "leaf_index") || get(4, "index")
4931
5100
  };
4932
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
+ }
4933
5112
  if (m === "privatetransfer") {
4934
5113
  return {
4935
5114
  nullifiers: get(0, "nullifiers"),
@@ -4971,12 +5150,12 @@ function mapZkEventData(method, data) {
4971
5150
  if (m_norm === "log") {
4972
5151
  return { log: get(0, "log") };
4973
5152
  }
4974
- if (m_norm === "created" || m_norm === "createdfailed" || m_norm === "executed" || m_norm === "executedfailed") {
5153
+ if (m_norm === "created" || m_norm === "createdfailed" || m_norm === "executedfailed") {
4975
5154
  return { address: get(0, "address") };
4976
5155
  }
4977
5156
  if (m_norm === "executed") {
4978
5157
  return {
4979
- from: get(0, "from"),
5158
+ from: get(0, "from") ?? get(0, "address"),
4980
5159
  to: get(1, "to"),
4981
5160
  transaction_hash: get(2, "transaction_hash"),
4982
5161
  exit_reason: get(3, "exit_reason")
@@ -5116,10 +5295,10 @@ var ERROR_KINDS = {
5116
5295
  CommitmentAlreadyExists: "shape",
5117
5296
  UnknownMerkleRoot: "stale-proof",
5118
5297
  // Amounts and fees
5119
- AmountTooSmall: "amount",
5120
5298
  InvalidAmount: "amount",
5121
5299
  InsufficientPoolBalance: "amount",
5122
5300
  FeeTooLow: "amount",
5301
+ InsufficientPendingFees: "amount",
5123
5302
  // Proofs
5124
5303
  InvalidProof: "proof",
5125
5304
  ProofVerificationFailed: "proof",
@@ -5128,12 +5307,14 @@ var ERROR_KINDS = {
5128
5307
  InvalidAssetId: "asset",
5129
5308
  AssetNotVerified: "asset",
5130
5309
  AssetIdMismatch: "asset",
5310
+ AssetIdAlreadyExists: "asset",
5131
5311
  // Request shape
5132
5312
  InvalidMemoSize: "shape",
5133
5313
  MemoCommitmentMismatch: "shape",
5134
5314
  TooManyInputsOrOutputs: "shape",
5135
5315
  InvalidRecipient: "shape",
5136
5316
  EmptyBatch: "shape",
5317
+ FeeRecipientUnavailable: "shape",
5137
5318
  // Capacity
5138
5319
  MerkleTreeFull: "capacity",
5139
5320
  // Balances pallet, reached through the same path
@@ -5188,6 +5369,7 @@ function methodOf(fnSig) {
5188
5369
  if (fnSig.startsWith("unshield(")) return "unshield";
5189
5370
  if (fnSig.startsWith("privateTransfer(")) return "privateTransfer";
5190
5371
  if (fnSig.startsWith("shieldBatch(")) return "shieldBatch";
5372
+ if (fnSig.startsWith("claimShieldedFees(")) return "claimShieldedFees";
5191
5373
  if (fnSig.startsWith("shield(")) return "shield";
5192
5374
  return null;
5193
5375
  }
@@ -5491,6 +5673,7 @@ function buildConfig(existing, chainFingerprint) {
5491
5673
  updatedAt: now,
5492
5674
  ...chainFingerprint ? { chainFingerprint } : {},
5493
5675
  ...existing?.selfEphCounter !== void 0 ? { selfEphCounter: existing.selfEphCounter } : {},
5676
+ ...existing?.outgoingEphCounter !== void 0 ? { outgoingEphCounter: existing.outgoingEphCounter } : {},
5494
5677
  ...existing?.pairwiseCounterparties !== void 0 ? { pairwiseCounterparties: existing.pairwiseCounterparties } : {}
5495
5678
  };
5496
5679
  }
@@ -5502,6 +5685,7 @@ async function writeConfig(storage, fallback, chainFingerprint) {
5502
5685
  }
5503
5686
  function mergeCounters(current, fallback) {
5504
5687
  const selfEphCounter = higher(current.selfEphCounter, fallback?.selfEphCounter);
5688
+ const outgoingEphCounter = higher(current.outgoingEphCounter, fallback?.outgoingEphCounter);
5505
5689
  const parties = mergePairwise(current.pairwiseCounterparties, fallback?.pairwiseCounterparties);
5506
5690
  return {
5507
5691
  createdAt: current.createdAt ?? fallback?.createdAt,
@@ -5509,6 +5693,7 @@ function mergeCounters(current, fallback) {
5509
5693
  // is exactOptionalPropertyTypes, so an explicit undefined is a type
5510
5694
  // error AND would round-trip through structured clone as a present key.
5511
5695
  ...selfEphCounter !== void 0 ? { selfEphCounter } : {},
5696
+ ...outgoingEphCounter !== void 0 ? { outgoingEphCounter } : {},
5512
5697
  ...parties !== void 0 ? { pairwiseCounterparties: parties } : {}
5513
5698
  };
5514
5699
  }
@@ -5546,6 +5731,7 @@ function mergePairwise(current, fallback) {
5546
5731
 
5547
5732
  // src/wallet/vault/storage/ephemeralIndex.ts
5548
5733
  var MAX_EPH_INDEX = 4294967294;
5734
+ var RESTORE_SAFETY_GAP = 8;
5549
5735
  function usableCounter(value) {
5550
5736
  if (typeof value !== "number" || !Number.isInteger(value)) return null;
5551
5737
  if (value < 0 || value > MAX_EPH_INDEX) return null;
@@ -5560,27 +5746,51 @@ async function reserveSelfEphIndex(storage) {
5560
5746
  if (!updated) throw new Error("vault not initialized");
5561
5747
  return (usableCounter(updated.selfEphCounter) ?? 1) - 1;
5562
5748
  }
5563
- 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) {
5564
5778
  const key = ivkHex.toLowerCase();
5565
5779
  const updated = await storage.updateConfig((config) => {
5566
- const stored = usableCounter(config.pairwiseCounterparties?.[key]?.nextIndex);
5567
- const addedAt = config.pairwiseCounterparties?.[key]?.addedAt;
5780
+ const existing = config.pairwiseCounterparties?.[key];
5568
5781
  return {
5569
5782
  ...config,
5570
5783
  pairwiseCounterparties: {
5571
5784
  ...config.pairwiseCounterparties,
5572
5785
  [key]: {
5573
- nextIndex: (stored ?? 0) + 1,
5574
- addedAt: typeof addedAt === "number" ? addedAt : Date.now()
5786
+ nextIndex: usableCounter(existing?.nextIndex) ?? 0,
5787
+ addedAt: typeof existing?.addedAt === "number" ? existing.addedAt : Date.now()
5575
5788
  }
5576
5789
  },
5577
5790
  updatedAt: Date.now()
5578
5791
  };
5579
5792
  });
5580
5793
  if (!updated) throw new Error("vault not initialized");
5581
- const nextIndex = usableCounter(updated.pairwiseCounterparties?.[key]?.nextIndex);
5582
- if (nextIndex === null || nextIndex <= 1) return null;
5583
- return nextIndex - 1;
5584
5794
  }
5585
5795
 
5586
5796
  // src/wallet/vault/notes/record.ts
@@ -6015,19 +6225,44 @@ async function processHints(ctx, hints, total) {
6015
6225
  }
6016
6226
  }
6017
6227
  outcome.scanned++;
6018
- if (!hint.encryptedMemo || !hint.ephPkHex) {
6228
+ if (!hint.encryptedMemo) {
6019
6229
  outcome.noMemo++;
6020
6230
  continue;
6021
6231
  }
6022
6232
  toDecrypt.push(hint);
6023
6233
  }
6024
- 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
+ );
6025
6250
  outcome.tagFiltered += tagFiltered;
6026
6251
  outcome.selfDiscovered += selfMatched;
6027
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);
6028
6257
  if (maxSelfEphIndex !== null) {
6029
6258
  outcome.maxSelfEphIndex = Math.max(outcome.maxSelfEphIndex ?? -1, maxSelfEphIndex);
6030
6259
  }
6260
+ if (maxOutgoingEphIndex != null) {
6261
+ outcome.maxOutgoingEphIndex = Math.max(
6262
+ outcome.maxOutgoingEphIndex ?? -1,
6263
+ maxOutgoingEphIndex
6264
+ );
6265
+ }
6031
6266
  const pageEntries = [];
6032
6267
  for (const [i, decrypted] of notes.entries()) {
6033
6268
  const hint = toDecrypt[i];
@@ -6124,7 +6359,12 @@ async function collectScanEntries(params) {
6124
6359
  tagFiltered: 0,
6125
6360
  selfDiscovered: 0,
6126
6361
  pairwiseDiscovered: 0,
6127
- 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
6128
6368
  };
6129
6369
  const ctx = {
6130
6370
  outcome,
@@ -6340,12 +6580,18 @@ function createScanCheckpoint(params) {
6340
6580
  // src/wallet/worker/kernel/types.ts
6341
6581
  var SELF_EPH_WINDOW = 1024;
6342
6582
  var PAIRWISE_EPH_WINDOW = 64;
6583
+ var OUTGOING_EPH_WINDOW = 64;
6343
6584
  var EMPTY_BATCH_RESULT = {
6344
6585
  notes: [],
6345
6586
  tagFiltered: 0,
6346
6587
  selfMatched: 0,
6347
6588
  pairwiseMatched: 0,
6348
- maxSelfEphIndex: null
6589
+ maxSelfEphIndex: null,
6590
+ maxOutgoingEphIndex: null,
6591
+ sentNotes: [],
6592
+ learnedRecipients: [],
6593
+ unmatchedSent: [],
6594
+ sealedBookEntries: []
6349
6595
  };
6350
6596
 
6351
6597
  // src/wallet/scanner/selfEphGap.ts
@@ -6357,7 +6603,7 @@ function gapMargin(windowSize) {
6357
6603
  return Math.max(1, Math.floor(windowSize / 16));
6358
6604
  }
6359
6605
  function resolveSelfEphCeiling(params) {
6360
- const { notes, spendingKey, viewingKey, scanMaxIndex } = params;
6606
+ const { notes, viewingKey, scanMaxIndex } = params;
6361
6607
  const windowSize = params.windowSize !== void 0 && params.windowSize > 0 ? params.windowSize : SELF_EPH_WINDOW;
6362
6608
  if (scanMaxIndex === null) return null;
6363
6609
  if (scanMaxIndex < windowSize - gapMargin(windowSize)) return scanMaxIndex;
@@ -6370,7 +6616,7 @@ function resolveSelfEphCeiling(params) {
6370
6616
  let max = scanMaxIndex;
6371
6617
  let from = windowSize;
6372
6618
  for (; ; ) {
6373
- const slice = selfEphWindow(spendingKey, ivkPacked, from, windowSize);
6619
+ const slice = selfEphWindow(viewingKey, ivkPacked, from, windowSize);
6374
6620
  let matched = false;
6375
6621
  for (const entry of slice) {
6376
6622
  if (ownEphPks.has(entry.ephPkHex.toLowerCase())) {
@@ -6383,12 +6629,12 @@ function resolveSelfEphCeiling(params) {
6383
6629
  }
6384
6630
  return max;
6385
6631
  }
6386
- var MAX_EPH_WINDOW = SELF_EPH_WINDOW * 64;
6632
+ var MAX_EPH_WINDOW2 = SELF_EPH_WINDOW * 64;
6387
6633
  function windowSizeForCounter(counter) {
6388
6634
  const used = Number.isFinite(counter) ? counter : 0;
6389
6635
  const needed = used + gapMargin(SELF_EPH_WINDOW);
6390
6636
  const rounded = Math.ceil(needed / SELF_EPH_WINDOW) * SELF_EPH_WINDOW;
6391
- 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));
6392
6638
  }
6393
6639
 
6394
6640
  // src/wallet/scanner/phases/spentStatus.ts
@@ -6426,6 +6672,8 @@ async function resolveSpentStatus(params) {
6426
6672
  }
6427
6673
 
6428
6674
  // src/wallet/scanner/pipeline.ts
6675
+ var OUTGOING_RESCAN_GAP = 8;
6676
+ var RETRY_BOOK_LIMIT = 64;
6429
6677
  async function runScan(params) {
6430
6678
  const { vault, storage, hints, nullifiers, pool, keys, sinceLeafIndex, signal, onWarning } = params;
6431
6679
  const isIncremental = sinceLeafIndex !== void 0;
@@ -6433,6 +6681,7 @@ async function runScan(params) {
6433
6681
  const preScanHexes = new Set(existingHexes);
6434
6682
  const config = await storage.getConfig().catch(() => null);
6435
6683
  const selfEphWindowSize = windowSizeForCounter(config?.selfEphCounter ?? 0);
6684
+ const outgoingEphWindowSize = windowSizeForCounter(config?.outgoingEphCounter ?? 0);
6436
6685
  const pairwiseCounterparties = Object.keys(config?.pairwiseCounterparties ?? {}).flatMap(
6437
6686
  (hex) => {
6438
6687
  try {
@@ -6461,7 +6710,12 @@ async function runScan(params) {
6461
6710
  // worth it on full scans and restores, waste on small incremental ticks.
6462
6711
  selfEph: !isIncremental,
6463
6712
  selfEphWindowSize,
6464
- 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
6465
6719
  },
6466
6720
  existingHexes,
6467
6721
  sinceLeafIndex,
@@ -6475,6 +6729,49 @@ async function runScan(params) {
6475
6729
  onPage: (entries) => checkpoint.savePageNotes(entries),
6476
6730
  onBatchDone: (maxLeafIndex) => checkpoint.advanceCursor(maxLeafIndex)
6477
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
+ }
6478
6775
  const nullifiersToQuery = collectNullifiersToQuery(
6479
6776
  scan.scanEntries,
6480
6777
  scan.onChainHexes,
@@ -6504,7 +6801,6 @@ async function runScan(params) {
6504
6801
  try {
6505
6802
  ceiling = resolveSelfEphCeiling({
6506
6803
  notes: vault.getAll(),
6507
- spendingKey: keys.spendingKey,
6508
6804
  viewingKey: keys.viewingKey,
6509
6805
  scanMaxIndex: scan.maxSelfEphIndex,
6510
6806
  windowSize: selfEphWindowSize
@@ -6514,6 +6810,13 @@ async function runScan(params) {
6514
6810
  await bumpSelfEphCounterTo(storage, ceiling + 1).catch(() => {
6515
6811
  });
6516
6812
  }
6813
+ if (scan.maxOutgoingEphIndex !== null) {
6814
+ await bumpOutgoingCounterTo(
6815
+ storage,
6816
+ scan.maxOutgoingEphIndex + 1 + OUTGOING_RESCAN_GAP
6817
+ ).catch(() => {
6818
+ });
6819
+ }
6517
6820
  if (scan.decryptFailed > 0 && scan.found === 0 && scan.alreadyPresent === 0) {
6518
6821
  onWarning?.(
6519
6822
  `no notes recovered from ${scan.decryptFailed} hint(s) with memos; if this wallet should hold notes, check the keys it was unlocked with`
@@ -6525,7 +6828,13 @@ async function runScan(params) {
6525
6828
  decryptFailed: scan.decryptFailed,
6526
6829
  alreadyPresent: scan.alreadyPresent,
6527
6830
  purged,
6528
- incremental: isIncremental
6831
+ incremental: isIncremental,
6832
+ sentNotes: scan.sentNotes,
6833
+ discovery: {
6834
+ self: scan.selfDiscovered,
6835
+ pairwise: scan.pairwiseDiscovered,
6836
+ tagFiltered: scan.tagFiltered
6837
+ }
6529
6838
  };
6530
6839
  }
6531
6840
  async function bumpSelfEphCounterTo(storage, next) {
@@ -6533,6 +6842,11 @@ async function bumpSelfEphCounterTo(storage, next) {
6533
6842
  (config) => (config.selfEphCounter ?? 0) >= next ? config : { ...config, selfEphCounter: next, updatedAt: Date.now() }
6534
6843
  );
6535
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
+ }
6536
6850
 
6537
6851
  // src/wallet/scanner/history/extrinsicFacts.ts
6538
6852
  var UNKNOWN = { fee: null, success: true };
@@ -6671,10 +6985,8 @@ function regeneratePaymentSlip(facts, recipientIvkPacked, txHash) {
6671
6985
  commitmentHex: facts.commitmentHex,
6672
6986
  encryptedMemo: facts.encryptedMemo,
6673
6987
  ...facts.leafIndex !== void 0 ? { leafIndex: facts.leafIndex } : {},
6674
- // Rendered as an explorer link by the recipient, where an unconstrained
6988
+ // The recipient renders this as an explorer link, so an unconstrained
6675
6989
  // string is a URL injection wearing the authority of a decrypted slip.
6676
- // Dropped rather than fatal: the slip still rebuilds the note, which is
6677
- // the part that matters.
6678
6990
  ...isHexOfLength(txHash, 32) ? { txHash } : {}
6679
6991
  });
6680
6992
  return encodePaymentSlip(envelope);
@@ -6699,15 +7011,37 @@ async function loadExistingRecords(vault) {
6699
7011
  function recordKey(transfer) {
6700
7012
  return transfer.hash ?? `${transfer.blockNumber}-${transfer.extrinsicIndex ?? 0}`;
6701
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
+ }
6702
7035
  async function reconstructOutgoingTxRecords(deps) {
6703
7036
  const { vault, transfers } = deps;
6704
7037
  const now = deps.now ?? Date.now;
6705
7038
  const allNotes = vault.getAll();
6706
7039
  const spentNotes = allNotes.filter((n) => n.spent);
6707
7040
  if (spentNotes.length === 0) return;
7041
+ const sentCommitments = (deps.sentNotes ?? []).map((s) => s.commitmentHex);
6708
7042
  const [outgoingTransfers, commitmentTransfers] = await Promise.all([
6709
7043
  transfers.byNullifiers(spentNotes.map((n) => n.nullifierHex)),
6710
- transfers.byCommitments(allNotes.map((n) => n.commitmentHex))
7044
+ transfers.byCommitments([...allNotes.map((n) => n.commitmentHex), ...sentCommitments])
6711
7045
  ]);
6712
7046
  if (outgoingTransfers.length === 0) return;
6713
7047
  const noteByNullifier = new Map(spentNotes.map((n) => [n.nullifierHex, n]));
@@ -6716,64 +7050,85 @@ async function reconstructOutgoingTxRecords(deps) {
6716
7050
  commitmentTransfers.map((ct) => [extrinsicKey(ct), ct.matchedCommitments ?? []])
6717
7051
  );
6718
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);
6719
7056
  for (const transfer of outgoingTransfers) {
6720
7057
  const existing = existingByKey.get(recordKey(transfer));
6721
- if (existing?.recipientPkHex && existing.recipientPkHex !== ZERO_PK) continue;
7058
+ const known = existing?.recipientPkHex && existing.recipientPkHex !== ZERO_PK;
7059
+ if (known && existing?.paymentSlip) continue;
6722
7060
  const inputNotes = (transfer.matchedNullifiers ?? []).map((h) => noteByNullifier.get(h)).filter((n) => n !== void 0);
6723
7061
  if (inputNotes.length === 0) continue;
6724
7062
  const changeNote = selectDescribingNoteByCommitment(
6725
7063
  commitmentsByExtrinsic.get(extrinsicKey(transfer)) ?? [],
6726
7064
  noteByCommitment
6727
7065
  );
6728
- const recipientPkHex = toPkHex(changeNote?.sourcePk);
7066
+ const sentForRow = sentFor(transfer);
7067
+ const recipientPkHex = sentForRow ? toPkHex(sentForRow.recipientStealthPk) : recoveryEnabled ? ZERO_PK : toPkHex(changeNote?.sourcePk);
6729
7068
  if (existing) {
6730
- if (recipientPkHex === ZERO_PK) continue;
6731
- 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
+ }
6732
7079
  continue;
6733
7080
  }
6734
7081
  const { fee, success } = await fetchExtrinsicFacts(deps.txFacts, transfer.hash);
6735
7082
  const totalInputValue = inputNotes.reduce((sum, n) => sum + n.value, 0n);
6736
- const transferAmount = totalInputValue - (changeNote?.value ?? 0n) - (fee ?? 0n);
7083
+ const derivedAmount = totalInputValue - (changeNote?.value ?? 0n) - (fee ?? 0n);
7084
+ const transferAmount = sentForRow?.value ?? derivedAmount;
6737
7085
  if (transferAmount <= 0n) continue;
6738
7086
  const record = {
6739
7087
  id: recordKey(transfer),
6740
7088
  type: "private_transfer",
6741
7089
  blockNumber: transfer.blockNumber,
6742
7090
  hash: transfer.hash ?? "",
6743
- assetId: inputNotes[0].assetId.toString(),
7091
+ assetId: (sentForRow?.assetId ?? inputNotes[0].assetId).toString(),
6744
7092
  amount: transferAmount.toString(),
6745
7093
  recipientPkHex,
6746
7094
  status: success ? "success" : "failed",
6747
- ...fee !== null ? { feePlanck: fee.toString() } : { amountApproximate: true },
6748
- 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) : {}
6749
7101
  };
6750
7102
  await vault.saveTxRecord(record);
6751
7103
  }
6752
7104
  }
6753
7105
 
6754
7106
  // src/wallet/ops/notes/buildNote.ts
6755
- async function buildZkNote(params, deps) {
7107
+ async function buildZkNoteWithIndex(params, deps) {
6756
7108
  const { keys, storage } = deps;
6757
7109
  const ownerPk = params.ownerPk ?? keys.ownerPk;
6758
7110
  const spendingKey = params.spendingKey ?? keys.spendingKey;
6759
7111
  const viewingPublicKey = params.viewingPublicKey ?? keys.viewingPublicKey;
6760
7112
  const isSelf = params.viewingPublicKey === void 0 && params.recipientOwnerPk === void 0;
6761
7113
  let ephSkOverride;
6762
- if (isSelf && storage) {
7114
+ let outgoingIndex;
7115
+ if (isSelf && storage && keys.viewingSecretKey) {
6763
7116
  try {
6764
- ephSkOverride = deriveSelfEphSk(spendingKey, await reserveSelfEphIndex(storage));
7117
+ ephSkOverride = deriveSelfEphSk(
7118
+ keys.viewingSecretKey,
7119
+ await reserveSelfEphIndex(storage)
7120
+ );
6765
7121
  } catch {
6766
7122
  }
6767
- } else if (params.viewingPublicKey && storage && keys.viewingSecretKey) {
7123
+ } else if (params.viewingPublicKey && storage && keys.outgoingViewingKey) {
6768
7124
  try {
6769
- const index = await reservePairwiseIndex(storage, toHex(params.viewingPublicKey));
6770
- if (index !== null) {
6771
- const pairSecret = derivePairwiseSharedSecret(
6772
- keys.viewingSecretKey,
6773
- params.viewingPublicKey
6774
- );
6775
- ephSkOverride = derivePairwiseEphSk(pairSecret, index);
6776
- }
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));
6777
7132
  } catch {
6778
7133
  }
6779
7134
  }
@@ -6793,7 +7148,13 @@ async function buildZkNote(params, deps) {
6793
7148
  },
6794
7149
  ...ephSkOverride !== void 0 && { ephSkOverride }
6795
7150
  });
6796
- 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;
6797
7158
  }
6798
7159
 
6799
7160
  // src/wallet/ops/notes/selfStealthNote.ts
@@ -6812,89 +7173,6 @@ function recoverSelfStealthNote(note, keys) {
6812
7173
  return recovered;
6813
7174
  }
6814
7175
 
6815
- // src/wallet/ops/notes/noteTransfer.ts
6816
- var NOTE_TRANSFER_URI_SCHEME = "orbinum://notes/v1/";
6817
- var QR_PAGE_MAX_CHARS = 1800;
6818
- function noteToTransferEntry(note) {
6819
- return {
6820
- c: note.commitmentHex,
6821
- n: note.nullifierHex,
6822
- val: note.value.toString(),
6823
- aid: note.assetId.toString(),
6824
- opk: scalarToHex(note.ownerPk),
6825
- bld: scalarToHex(note.blinding),
6826
- sk: scalarToHex(note.spendingKey)
6827
- };
6828
- }
6829
- function encodeNoteTransferPages(notes, options = {}) {
6830
- const now = options.now ?? Date.now;
6831
- const maxChars = options.maxChars ?? QR_PAGE_MAX_CHARS;
6832
- const entries = notes.map(noteToTransferEntry);
6833
- const batches = [];
6834
- let batch = [];
6835
- for (const entry of entries) {
6836
- batch.push(entry);
6837
- const probe = { v: 1, ts: 0, p: 0, pt: 1, notes: batch };
6838
- const size = NOTE_TRANSFER_URI_SCHEME.length + base64UrlEncode(JSON.stringify(probe)).length;
6839
- if (size > maxChars && batch.length > 1) {
6840
- batch.pop();
6841
- batches.push(batch);
6842
- batch = [entry];
6843
- }
6844
- }
6845
- if (batch.length > 0) batches.push(batch);
6846
- const ts = now();
6847
- return batches.map((notesInPage, i) => {
6848
- const payload = {
6849
- v: 1,
6850
- ts,
6851
- p: i,
6852
- pt: batches.length,
6853
- notes: notesInPage
6854
- };
6855
- return NOTE_TRANSFER_URI_SCHEME + base64UrlEncode(JSON.stringify(payload));
6856
- });
6857
- }
6858
- function decodeNoteTransferPage(uri) {
6859
- if (!uri.startsWith(NOTE_TRANSFER_URI_SCHEME)) {
6860
- throw new Error("Not an Orbinum note-transfer code.");
6861
- }
6862
- let payload;
6863
- try {
6864
- payload = JSON.parse(base64UrlDecode(uri.slice(NOTE_TRANSFER_URI_SCHEME.length)));
6865
- } catch {
6866
- throw new Error("Note-transfer code is corrupt.");
6867
- }
6868
- const p = payload;
6869
- if (p.v !== 1) {
6870
- throw new Error(`Unsupported note-transfer version: ${String(p.v)}.`);
6871
- }
6872
- if (typeof p.p !== "number" || typeof p.pt !== "number" || !Array.isArray(p.notes) || p.notes.some((n) => !isEntry(n))) {
6873
- throw new Error("Note-transfer code is missing required fields.");
6874
- }
6875
- return p;
6876
- }
6877
- function isEntry(value) {
6878
- if (typeof value !== "object" || value === null) return false;
6879
- const e = value;
6880
- return ["c", "n", "val", "aid", "opk", "bld", "sk"].every(
6881
- (k) => typeof e[k] === "string" && e[k].length > 0
6882
- );
6883
- }
6884
- function assembleNoteTransfer(pages) {
6885
- if (pages.length === 0) throw new Error("No pages scanned.");
6886
- const total = pages[0].pt;
6887
- if (pages.some((p) => p.pt !== total)) {
6888
- throw new Error("Pages come from different batches.");
6889
- }
6890
- const seen = new Set(pages.map((p) => p.p));
6891
- if (seen.size !== total) {
6892
- const missing = [...Array(total).keys()].filter((i) => !seen.has(i));
6893
- throw new Error(`Missing page(s): ${missing.map((i) => i + 1).join(", ")} of ${total}.`);
6894
- }
6895
- return [...pages].sort((a, b) => a.p - b.p).flatMap((p) => p.notes);
6896
- }
6897
-
6898
7176
  // src/wallet/ops/notes/noteBackup.ts
6899
7177
  var NOTE_BACKUP_VERSION = 1;
6900
7178
  function noteToBackupEntry(note) {
@@ -6929,12 +7207,13 @@ function decodeNoteBackup(json) {
6929
7207
  if (p.v !== NOTE_BACKUP_VERSION) {
6930
7208
  throw new Error(`Unsupported note-backup version: ${String(p.v)}.`);
6931
7209
  }
6932
- if (!Array.isArray(p.notes) || p.notes.some((n) => !isEntry2(n))) {
7210
+ if (!Array.isArray(p.notes) || p.notes.some((n) => !isEntry(n))) {
6933
7211
  throw new Error("Note backup is missing required fields.");
6934
7212
  }
6935
7213
  return p.notes;
6936
7214
  }
6937
7215
  function importNotesFromBackup(entries, keys) {
7216
+ assertSecretKeyBytes(keys.viewingSecretKey, "importNotesFromBackup: viewingSecretKey");
6938
7217
  const out = [];
6939
7218
  for (const entry of entries) {
6940
7219
  const commitment = {
@@ -6958,12 +7237,14 @@ function importNotesFromBackup(entries, keys) {
6958
7237
  }
6959
7238
  return out;
6960
7239
  }
6961
- function isEntry2(value) {
7240
+ function isEntry(value) {
6962
7241
  if (typeof value !== "object" || value === null) return false;
6963
7242
  const e = value;
6964
- if (typeof e["commitmentHex"] !== "string" || e["commitmentHex"].length === 0) return false;
6965
- if (typeof e["encryptedMemo"] !== "string" || e["encryptedMemo"].length === 0) return false;
6966
- 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") {
6967
7248
  return false;
6968
7249
  }
6969
7250
  return true;
@@ -6971,6 +7252,7 @@ function isEntry2(value) {
6971
7252
 
6972
7253
  // src/wallet/ops/notes/paymentSlipImport.ts
6973
7254
  function importPaymentSlip(slip, keys) {
7255
+ assertSecretKeyBytes(keys.viewingSecretKey, "importPaymentSlip: viewingSecretKey");
6974
7256
  const envelope = typeof slip === "string" ? decodePaymentSlip(slip) : slip;
6975
7257
  if (!envelope) return null;
6976
7258
  const fields = openPaymentSlip(keys.viewingSecretKey, envelope);
@@ -7099,10 +7381,7 @@ async function transferNotes(deps, params, onProgress) {
7099
7381
  const merkleRoot = merkleA.root;
7100
7382
  onProgress?.("building-output-notes");
7101
7383
  const effectiveSenderPk = senderPk ?? noteA.ownerPk;
7102
- const senderViewingPublicKey = deriveViewingPublicKey(
7103
- deriveViewingSecretKey(noteA.spendingKey)
7104
- );
7105
- const recipientNote = await deps.buildNote({
7384
+ const { note: recipientNote, outgoingIndex } = await deps.buildNote({
7106
7385
  value: transferAmount,
7107
7386
  assetId: noteA.assetId,
7108
7387
  ownerPk: recipientPk,
@@ -7112,13 +7391,17 @@ async function transferNotes(deps, params, onProgress) {
7112
7391
  // With a viewing key present this activates stealth derivation.
7113
7392
  recipientOwnerPk: recipientPk
7114
7393
  });
7115
- 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({
7116
7402
  value: changeValue,
7117
7403
  assetId: noteA.assetId,
7118
- ownerPk: effectiveSenderPk,
7119
- spendingKey: noteA.spendingKey,
7120
- sourcePk: recipientNote.ownerPk,
7121
- viewingPublicKey: senderViewingPublicKey
7404
+ sourcePk: changeSourcePk
7122
7405
  });
7123
7406
  onProgress?.("generating-zk");
7124
7407
  const noteToInput = (note, proof) => ({
@@ -7235,7 +7518,7 @@ async function unshieldNote(deps, params, onProgress) {
7235
7518
  if (changeValue > 0n) {
7236
7519
  const { ownerPk, spendingKey } = deps.stealthKeys;
7237
7520
  changeBlinding = randomBlinding();
7238
- changeNote = await deps.buildNote({
7521
+ changeNote = (await deps.buildNote({
7239
7522
  value: changeValue,
7240
7523
  assetId: note.assetId,
7241
7524
  ownerPk,
@@ -7243,7 +7526,7 @@ async function unshieldNote(deps, params, onProgress) {
7243
7526
  spendingKey,
7244
7527
  // Activates stealth: the commitment covers a one-time ownerPk.
7245
7528
  recipientOwnerPk: ownerPk
7246
- });
7529
+ })).note;
7247
7530
  }
7248
7531
  const { provider, version: circuitVersion } = await deps.resolver.resolve(
7249
7532
  import_proof_generator7.CircuitType.Unshield,
@@ -7302,7 +7585,7 @@ async function unshieldNote(deps, params, onProgress) {
7302
7585
  var import_proof_generator8 = require("@orbinum/proof-generator");
7303
7586
  async function claimFees(deps, { assetId, amount, signer }, onStep) {
7304
7587
  onStep?.("building-note");
7305
- const note = await deps.buildNote({ value: amount, assetId: BigInt(assetId) });
7588
+ const { note } = await deps.buildNote({ value: amount, assetId: BigInt(assetId) });
7306
7589
  const { provider } = await deps.resolver.resolve(import_proof_generator8.CircuitType.ValueProof, note.circuitVersion);
7307
7590
  onStep?.("generating-proof");
7308
7591
  const proofOutput = await generateFeeClaimProof(
@@ -7529,8 +7812,9 @@ var KEY_PREFIX = "orbinum_sk_";
7529
7812
  function isEnvelope(value) {
7530
7813
  return typeof value === "object" && value !== null && value.v === 1 && typeof value.iv === "string" && typeof value.ct === "string";
7531
7814
  }
7532
- function sessionCacheKey(address, chainId) {
7533
- 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)}`;
7534
7818
  }
7535
7819
  async function hasCachedSession(store, address, chainId) {
7536
7820
  return await store.get(sessionCacheKey(address, chainId)) !== null;
@@ -7601,9 +7885,55 @@ function createDeviceKeyProvider(store) {
7601
7885
 
7602
7886
  // src/wallet/identity/vaultName.ts
7603
7887
  var PREFIX2 = "orbinum-vault";
7604
- 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") {
7605
7900
  const account = canonicalAccountId(address);
7606
- 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
+ };
7607
7937
  }
7608
7938
 
7609
7939
  // src/wallet/identity/injectedExtensions.ts
@@ -7627,23 +7957,21 @@ var cachedWindow = null;
7627
7957
  function clearKnownEphWindow() {
7628
7958
  cachedWindow = null;
7629
7959
  }
7630
- function bytesKey(bytes) {
7631
- let out = "";
7632
- for (const b of bytes) out += b.toString(16).padStart(2, "0");
7633
- return out;
7634
- }
7635
7960
  function getKnownEphWindow(keys) {
7636
7961
  const selfSize = keys.selfEphWindowSize ?? SELF_EPH_WINDOW;
7637
7962
  const pairSize = keys.pairwiseWindowSize ?? PAIRWISE_EPH_WINDOW;
7963
+ const outSize = keys.outgoingEphWindowSize ?? OUTGOING_EPH_WINDOW;
7638
7964
  const counterparties = keys.pairwiseCounterparties ?? [];
7639
- if (!keys.selfEph && counterparties.length === 0) return null;
7640
- 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(",");
7641
7968
  if (cachedWindow?.cacheKey === cacheKey) return cachedWindow.window;
7642
7969
  try {
7643
7970
  const ivkPacked = deriveViewingPublicKey(keys.viewingKey);
7644
7971
  const byEphPk = /* @__PURE__ */ new Map();
7972
+ const outgoingByEphPk = /* @__PURE__ */ new Map();
7645
7973
  if (keys.selfEph) {
7646
- for (const e of selfEphWindow(keys.spendingKey, ivkPacked, 0, selfSize)) {
7974
+ for (const e of selfEphWindow(keys.viewingKey, ivkPacked, 0, selfSize)) {
7647
7975
  byEphPk.set(e.ephPkHex.toLowerCase(), {
7648
7976
  sharedSecret: e.sharedSecret,
7649
7977
  index: e.index,
@@ -7664,7 +7992,12 @@ function getKnownEphWindow(keys) {
7664
7992
  }
7665
7993
  }
7666
7994
  }
7667
- 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 } };
7668
8001
  return cachedWindow.window;
7669
8002
  } catch {
7670
8003
  return null;
@@ -7673,21 +8006,32 @@ function getKnownEphWindow(keys) {
7673
8006
 
7674
8007
  // src/wallet/worker/kernel/decryptBatch.ts
7675
8008
  function hintEphPkHex(hint) {
7676
- if (hint.ephPkHex) return hint.ephPkHex.toLowerCase();
7677
8009
  const memo = hint.encryptedMemo;
7678
8010
  if (!memo || memo.length < 66) return null;
7679
8011
  return ("0x" + memo.slice(-64)).toLowerCase();
7680
8012
  }
8013
+ function sealedBookEntry(note) {
8014
+ if (note.sourcePk === void 0 || note.sourcePk === 0n) return null;
8015
+ return note.sourcePk;
8016
+ }
7681
8017
  function decryptHintBatch(hints, keys) {
7682
8018
  const activation = keys.viewTagActivationLeaf ?? null;
7683
8019
  const knownWindow = getKnownEphWindow(keys);
8020
+ const sentNotes = [];
8021
+ const learnedRecipients = /* @__PURE__ */ new Set();
7684
8022
  let tagFiltered = 0;
7685
8023
  let selfMatched = 0;
7686
8024
  let pairwiseMatched = 0;
7687
8025
  let maxSelfEphIndex = null;
8026
+ let maxOutgoingEphIndex = null;
8027
+ const sealedEntries = [];
8028
+ const pendingSent = [];
8029
+ const seenSent = /* @__PURE__ */ new Set();
8030
+ const unmatchedSent = [];
7688
8031
  const notes = hints.map((hint) => {
7689
8032
  try {
7690
- const known = knownWindow?.byEphPk.get(hintEphPkHex(hint) ?? "");
8033
+ const ephPkHex = hintEphPkHex(hint) ?? "";
8034
+ const known = knownWindow?.byEphPk.get(ephPkHex);
7691
8035
  if (known) {
7692
8036
  const result2 = tryDecryptNoteVerbose(
7693
8037
  hint,
@@ -7702,12 +8046,26 @@ function decryptHintBatch(hints, keys) {
7702
8046
  if (maxSelfEphIndex === null || known.index > maxSelfEphIndex) {
7703
8047
  maxSelfEphIndex = known.index;
7704
8048
  }
8049
+ const sealed = sealedBookEntry(result2.note);
8050
+ if (sealed !== null) sealedEntries.push(sealed);
7705
8051
  } else {
7706
8052
  pairwiseMatched++;
7707
8053
  }
7708
8054
  return result2.note;
7709
8055
  }
7710
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
+ }
7711
8069
  const viewTag = activation !== null && hint.leafIndex >= activation;
7712
8070
  const result = tryDecryptNoteVerbose(
7713
8071
  hint,
@@ -7722,7 +8080,44 @@ function decryptHintBatch(hints, keys) {
7722
8080
  return null;
7723
8081
  }
7724
8082
  });
7725
- 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
+ };
7726
8121
  }
7727
8122
 
7728
8123
  // src/wallet/worker/pool/types.ts
@@ -7744,21 +8139,52 @@ function createMainThreadPool() {
7744
8139
  let selfMatched = 0;
7745
8140
  let pairwiseMatched = 0;
7746
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
+ );
7747
8149
  for (let i = 0; i < hints.length; i += DECRYPT_YIELD_EVERY) {
7748
8150
  if (i > 0) {
7749
8151
  await yieldToBrowser();
7750
8152
  if (signal?.aborted) throw scanAbortError();
7751
8153
  }
7752
- 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
+ });
7753
8158
  notes.push(...burst.notes);
7754
8159
  tagFiltered += burst.tagFiltered;
7755
8160
  selfMatched += burst.selfMatched;
7756
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);
7757
8166
  if (burst.maxSelfEphIndex !== null) {
7758
8167
  maxSelfEphIndex = Math.max(maxSelfEphIndex ?? -1, burst.maxSelfEphIndex);
7759
8168
  }
8169
+ if (burst.maxOutgoingEphIndex != null) {
8170
+ maxOutgoingEphIndex = Math.max(
8171
+ maxOutgoingEphIndex ?? -1,
8172
+ burst.maxOutgoingEphIndex
8173
+ );
8174
+ }
7760
8175
  }
7761
- 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
+ };
7762
8188
  },
7763
8189
  terminate() {
7764
8190
  clearKnownEphWindow();
@@ -7790,7 +8216,12 @@ function runOnWorker(worker, payload, signal) {
7790
8216
  tagFiltered: data.tagFiltered ?? 0,
7791
8217
  selfMatched: data.selfMatched ?? 0,
7792
8218
  pairwiseMatched: data.pairwiseMatched ?? 0,
7793
- 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 ?? []
7794
8225
  });
7795
8226
  };
7796
8227
  worker.onerror = () => {
@@ -7817,9 +8248,19 @@ function mergeResults(results) {
7817
8248
  tagFiltered: results.reduce((sum, r) => sum + r.tagFiltered, 0),
7818
8249
  selfMatched: results.reduce((sum, r) => sum + r.selfMatched, 0),
7819
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 ?? []))],
7820
8257
  maxSelfEphIndex: results.reduce(
7821
8258
  (max, r) => r.maxSelfEphIndex === null ? max : Math.max(max ?? -1, r.maxSelfEphIndex),
7822
8259
  null
8260
+ ),
8261
+ maxOutgoingEphIndex: results.reduce(
8262
+ (max, r) => r.maxOutgoingEphIndex == null ? max : Math.max(max ?? -1, r.maxOutgoingEphIndex),
8263
+ null
7823
8264
  )
7824
8265
  };
7825
8266
  }
@@ -7883,25 +8324,36 @@ var OrbinumWallet = class {
7883
8324
  session;
7884
8325
  notes;
7885
8326
  vaultStore;
7886
- /** Set at unlock; every key below is derived from it. */
7887
- 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;
7888
8337
  /** The underlying store, for operations the facade does not cover. */
7889
8338
  get vault() {
7890
8339
  return this.vaultStore;
7891
8340
  }
7892
8341
  get unlocked() {
7893
- return this.session.unlocked && this.spendingKey !== null;
8342
+ return this.session.unlocked && this.identity !== null;
7894
8343
  }
7895
8344
  /**
7896
8345
  * Opens the wallet from the master key bytes a `SpendingKeyRequest`
7897
8346
  * signature produces — the same value `PrivacyKeyManager.getMasterBytes()`
7898
8347
  * returns.
7899
8348
  *
7900
- * Master bytes rather than the spending-key scalar, deliberately: the scalar
7901
- * is the master reduced modulo the curve order, so deriving the vault key
7902
- * from it would tie the encrypted vault to the CURRENT modulus. The master
7903
- * is pre-reduction, which keeps a stored vault readable across a modulus
7904
- * 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.
7905
8357
  *
7906
8358
  * Reports whether the stored vault was RESET: a different chain, a different
7907
8359
  * key, or notes the chain no longer recognises. When it was, the wallet
@@ -7915,14 +8367,12 @@ var OrbinumWallet = class {
7915
8367
  * against a vault that was never read.
7916
8368
  */
7917
8369
  async unlock(masterBytes, options = {}) {
7918
- if (masterBytes.length !== 32) {
7919
- throw new Error(`Master key must be 32 bytes, got ${masterBytes.length}.`);
7920
- }
8370
+ assertSecretKeyBytes(masterBytes, "Master key");
7921
8371
  this.session.open(
7922
8372
  await deriveVaultKey(masterBytes),
7923
8373
  await deriveVaultBlindKey(masterBytes)
7924
8374
  );
7925
- this.spendingKey = deriveSpendingKeyFromMaster(masterBytes);
8375
+ this.identity = deriveIdentity(masterBytes, this.config.identityVersion ?? "v3");
7926
8376
  try {
7927
8377
  return await this.vaultStore.unlock(this.session.cryptoKey, options);
7928
8378
  } catch (err) {
@@ -7933,8 +8383,12 @@ var OrbinumWallet = class {
7933
8383
  /** Drops the in-memory keys. The stored vault is untouched. */
7934
8384
  lock() {
7935
8385
  this.session.lock();
7936
- this.spendingKey = null;
8386
+ this.identity = null;
7937
8387
  this.notes.set([]);
8388
+ try {
8389
+ this.config.pool.terminate();
8390
+ } catch {
8391
+ }
7938
8392
  }
7939
8393
  /** The notes currently held. Empty while locked. */
7940
8394
  getNotes() {
@@ -7946,11 +8400,8 @@ var OrbinumWallet = class {
7946
8400
  }
7947
8401
  /** This wallet's privacy identity: the keys a payer needs to address it. */
7948
8402
  privacyKeys() {
7949
- const spendingKey = this.requireKey();
7950
- return {
7951
- ownerPk: deriveOwnerPk(spendingKey),
7952
- viewingPublicKey: deriveViewingPublicKey(deriveViewingSecretKey(spendingKey))
7953
- };
8403
+ const { ownerPk, viewingPublicKey } = this.requireIdentity();
8404
+ return { ownerPk, viewingPublicKey };
7954
8405
  }
7955
8406
  /**
7956
8407
  * Walks the feed, recovers this wallet's notes, and persists them with their
@@ -7960,15 +8411,21 @@ var OrbinumWallet = class {
7960
8411
  * `sinceLeafIndex` for a full scan, which is what a restore needs.
7961
8412
  */
7962
8413
  async scan(options = {}) {
7963
- const spendingKey = this.requireKey();
7964
- const viewingKey = deriveViewingSecretKey(spendingKey);
8414
+ const identity = this.requireIdentity();
7965
8415
  return runScan({
7966
8416
  vault: this.vaultStore,
7967
8417
  storage: this.config.storage,
7968
8418
  hints: this.config.hints,
7969
8419
  nullifiers: this.config.nullifiers,
7970
8420
  pool: this.config.pool,
7971
- 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
+ },
7972
8429
  sinceLeafIndex: options.sinceLeafIndex,
7973
8430
  viewTagActivationLeaf: this.config.viewTagActivationLeaf,
7974
8431
  onProgress: options.onProgress,
@@ -7990,13 +8447,14 @@ var OrbinumWallet = class {
7990
8447
  * reusing an index would republish an ephPk and link the two notes.
7991
8448
  */
7992
8449
  async buildNote(params) {
7993
- const spendingKey = this.requireKey();
8450
+ const identity = this.requireIdentity();
7994
8451
  return buildZkNote(params, {
7995
8452
  keys: {
7996
- ownerPk: deriveOwnerPk(spendingKey),
7997
- spendingKey,
7998
- viewingPublicKey: deriveViewingPublicKey(deriveViewingSecretKey(spendingKey)),
7999
- 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 } : {}
8000
8458
  },
8001
8459
  storage: this.config.storage
8002
8460
  });
@@ -8013,11 +8471,12 @@ var OrbinumWallet = class {
8013
8471
  * Treat the result as secret: it can spend every note this wallet holds.
8014
8472
  */
8015
8473
  spendKeys() {
8016
- const spendingKey = this.requireKey();
8474
+ const identity = this.requireIdentity();
8017
8475
  return {
8018
- spendingKey,
8019
- viewingSecretKey: deriveViewingSecretKey(spendingKey),
8020
- ownerPk: deriveOwnerPk(spendingKey)
8476
+ spendingKey: identity.spendingKey,
8477
+ viewingSecretKey: identity.viewingSecretKey,
8478
+ ownerPk: identity.ownerPk,
8479
+ ...identity.outgoingViewingKey ? { outgoingViewingKey: identity.outgoingViewingKey } : {}
8021
8480
  };
8022
8481
  }
8023
8482
  /**
@@ -8039,7 +8498,20 @@ var OrbinumWallet = class {
8039
8498
  * verifying-key rotation, and the failure surfaces much later.
8040
8499
  */
8041
8500
  buildOutputNote = async (params) => {
8042
- 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
+ );
8043
8515
  };
8044
8516
  /** The circuit version a new note should carry. */
8045
8517
  async circuitVersion() {
@@ -8051,11 +8523,11 @@ var OrbinumWallet = class {
8051
8523
  }
8052
8524
  return chainActiveCircuitVersion(this.config.zkVerifier);
8053
8525
  }
8054
- requireKey() {
8055
- if (this.spendingKey === null) {
8526
+ requireIdentity() {
8527
+ if (this.identity === null) {
8056
8528
  throw new Error("Wallet is locked. Call unlock() before using it.");
8057
8529
  }
8058
- return this.spendingKey;
8530
+ return this.identity;
8059
8531
  }
8060
8532
  };
8061
8533
  // Annotate the CommonJS export names for ESM import in node:
@@ -8087,9 +8559,8 @@ var OrbinumWallet = class {
8087
8559
  NATIVE_ASSET_ID,
8088
8560
  NOTE_BACKUP_VERSION,
8089
8561
  NOTE_BIGINT_FIELDS,
8090
- NOTE_TRANSFER_URI_SCHEME,
8091
8562
  NoteBuilder,
8092
- OVK_BLOB_SIZE,
8563
+ OUTGOING_EPH_WINDOW,
8093
8564
  OrbinumClient,
8094
8565
  OrbinumClientProvider,
8095
8566
  OrbinumWallet,
@@ -8099,7 +8570,6 @@ var OrbinumWallet = class {
8099
8570
  PRECOMPILE_ADDR,
8100
8571
  PrivacyKeyManager,
8101
8572
  PrivacyModule,
8102
- QR_PAGE_MAX_CHARS,
8103
8573
  RECOVERED_TX_RESULT,
8104
8574
  RelayerStatusModule,
8105
8575
  RpcV2Module,
@@ -8124,7 +8594,7 @@ var OrbinumWallet = class {
8124
8594
  addressToFieldElement,
8125
8595
  applyBatch,
8126
8596
  applyNoteStatus,
8127
- assembleNoteTransfer,
8597
+ assertSecretKeyBytes,
8128
8598
  base58,
8129
8599
  base64UrlDecode,
8130
8600
  base64UrlEncode,
@@ -8137,6 +8607,7 @@ var OrbinumWallet = class {
8137
8607
  buildShieldBatchOperations,
8138
8608
  buildShieldParams,
8139
8609
  buildZkNote,
8610
+ buildZkNoteWithIndex,
8140
8611
  bytesToBigintLE,
8141
8612
  bytesToBjjScalar,
8142
8613
  cacheSession,
@@ -8166,15 +8637,17 @@ var OrbinumWallet = class {
8166
8637
  createWorkerPool,
8167
8638
  decodeNoteBackup,
8168
8639
  decodeNoteDisclosureKey,
8169
- decodeNoteTransferPage,
8170
8640
  decodePaymentSlip,
8171
8641
  decodePrecompileCalldata,
8172
8642
  decryptHintBatch,
8173
8643
  decryptJson,
8174
8644
  decryptNoteRecord,
8645
+ deriveIdentity,
8175
8646
  deriveMasterKeyBytes,
8176
- deriveOutgoingCipherKey,
8177
- deriveOutgoingViewingKey,
8647
+ deriveOutgoingEphPk,
8648
+ deriveOutgoingEphSk,
8649
+ deriveOutgoingSharedSecret,
8650
+ deriveOutgoingViewingKeyV3,
8178
8651
  deriveOwnerPk,
8179
8652
  derivePairwiseEphSk,
8180
8653
  derivePairwiseSharedSecret,
@@ -8183,6 +8656,7 @@ var OrbinumWallet = class {
8183
8656
  deriveSpendingKeyFromSignature,
8184
8657
  deriveSpendingKeyMessageV2,
8185
8658
  deriveSpendingKeyTypedData,
8659
+ deriveSpendingKeyV3,
8186
8660
  deriveStealthOwnerPk,
8187
8661
  deriveStealthSk,
8188
8662
  deriveVaultBlindKey,
@@ -8190,9 +8664,9 @@ var OrbinumWallet = class {
8190
8664
  deriveViewTag,
8191
8665
  deriveViewingPublicKey,
8192
8666
  deriveViewingSecretKey,
8667
+ deriveViewingSecretKeyV3,
8193
8668
  detectCommitmentMismatch,
8194
8669
  encodeNoteBackup,
8195
- encodeNoteTransferPages,
8196
8670
  encodePaymentSlip,
8197
8671
  encryptJson,
8198
8672
  encryptNote,
@@ -8202,6 +8676,7 @@ var OrbinumWallet = class {
8202
8676
  evmToImplicitSubstrate,
8203
8677
  evmToMappedAccountHex,
8204
8678
  evmToSubstrate,
8679
+ exportViewingCredential,
8205
8680
  extractPalletError,
8206
8681
  failed,
8207
8682
  fastMulBase,
@@ -8242,9 +8717,9 @@ var OrbinumWallet = class {
8242
8717
  isNativeAsset,
8243
8718
  isNoteSelfConsistent,
8244
8719
  isSpendable,
8245
- isSs58,
8246
8720
  isSubstrateAddress,
8247
8721
  isUnifiedAddress,
8722
+ isUsableSecretKey,
8248
8723
  isValidLeafIndex,
8249
8724
  leHexToBigint,
8250
8725
  mapExtrinsicArgs,
@@ -8261,10 +8736,10 @@ var OrbinumWallet = class {
8261
8736
  noteMatchesCommitment,
8262
8737
  noteOrigin,
8263
8738
  noteSpentTxHash,
8264
- noteToTransferEntry,
8265
8739
  noteTxKind,
8266
- openOutgoingBlob,
8267
8740
  openPaymentSlip,
8741
+ openRecipientBookEntry,
8742
+ outgoingEphWindow,
8268
8743
  outranks,
8269
8744
  pairwiseEphWindow,
8270
8745
  palletErrorKind,
@@ -8275,15 +8750,18 @@ var OrbinumWallet = class {
8275
8750
  planTransfer,
8276
8751
  planUnshield,
8277
8752
  randomBlinding,
8278
- randomOutgoingBlob,
8753
+ reconstructOutgoingIndex,
8279
8754
  reconstructOutgoingTxRecords,
8280
8755
  recoverOwnerPkPoint,
8281
8756
  recoverSelfStealthNote,
8757
+ recoverSentFromSharedSecret,
8758
+ recoverSentNote,
8282
8759
  refuseIfAlreadySpent,
8283
8760
  regeneratePaymentSlip,
8761
+ registerPairwiseCounterparty,
8284
8762
  removeByCommitment,
8285
8763
  requireSessionKeys,
8286
- reservePairwiseIndex,
8764
+ reserveOutgoingIndex,
8287
8765
  reserveSelfEphIndex,
8288
8766
  resolveSelfEphCeiling,
8289
8767
  resolveSpentSet,
@@ -8292,8 +8770,8 @@ var OrbinumWallet = class {
8292
8770
  runScan,
8293
8771
  scalarToHex,
8294
8772
  scanAbortError,
8295
- sealOutgoingBlob,
8296
8773
  sealPaymentSlip,
8774
+ sealRecipientBookEntry,
8297
8775
  selectDescribingNote,
8298
8776
  selectDescribingNoteByCommitment,
8299
8777
  selectGhosts,
@@ -8319,10 +8797,10 @@ var OrbinumWallet = class {
8319
8797
  truncateMiddle,
8320
8798
  tryDecryptNote,
8321
8799
  tryDecryptNoteVerbose,
8322
- tryRecoverOutgoing,
8323
8800
  txLandedAfterError,
8324
8801
  u128,
8325
8802
  u64,
8803
+ unpackUsableViewingKey,
8326
8804
  unshieldNote,
8327
8805
  upsertNote,
8328
8806
  vaultReplacer,