@orbinum/sdk 1.1.1 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -49,11 +49,13 @@ __export(index_exports, {
49
49
  NOTE_BIGINT_FIELDS: () => NOTE_BIGINT_FIELDS,
50
50
  NOTE_TRANSFER_URI_SCHEME: () => NOTE_TRANSFER_URI_SCHEME,
51
51
  NoteBuilder: () => NoteBuilder,
52
+ OVK_BLOB_SIZE: () => OVK_BLOB_SIZE,
52
53
  OrbinumClient: () => OrbinumClient,
53
54
  OrbinumClientProvider: () => OrbinumClientProvider,
54
55
  OrbinumWallet: () => OrbinumWallet,
55
56
  PAGE_SIZE: () => PAGE_SIZE,
56
57
  PAIRWISE_EPH_WINDOW: () => PAIRWISE_EPH_WINDOW,
58
+ PAYMENT_SLIP_SCHEME: () => PAYMENT_SLIP_SCHEME,
57
59
  PRECOMPILE_ADDR: () => PRECOMPILE_ADDR,
58
60
  PrivacyKeyManager: () => PrivacyKeyManager,
59
61
  PrivacyModule: () => PrivacyModule,
@@ -124,11 +126,14 @@ __export(index_exports, {
124
126
  decodeNoteBackup: () => decodeNoteBackup,
125
127
  decodeNoteDisclosureKey: () => decodeNoteDisclosureKey,
126
128
  decodeNoteTransferPage: () => decodeNoteTransferPage,
129
+ decodePaymentSlip: () => decodePaymentSlip,
127
130
  decodePrecompileCalldata: () => decodePrecompileCalldata,
128
131
  decryptHintBatch: () => decryptHintBatch,
129
132
  decryptJson: () => decryptJson,
130
133
  decryptNoteRecord: () => decryptNoteRecord,
131
134
  deriveMasterKeyBytes: () => deriveMasterKeyBytes,
135
+ deriveOutgoingCipherKey: () => deriveOutgoingCipherKey,
136
+ deriveOutgoingViewingKey: () => deriveOutgoingViewingKey,
132
137
  deriveOwnerPk: () => deriveOwnerPk,
133
138
  derivePairwiseEphSk: () => derivePairwiseEphSk,
134
139
  derivePairwiseSharedSecret: () => derivePairwiseSharedSecret,
@@ -147,6 +152,7 @@ __export(index_exports, {
147
152
  detectCommitmentMismatch: () => detectCommitmentMismatch,
148
153
  encodeNoteBackup: () => encodeNoteBackup,
149
154
  encodeNoteTransferPages: () => encodeNoteTransferPages,
155
+ encodePaymentSlip: () => encodePaymentSlip,
150
156
  encryptJson: () => encryptJson,
151
157
  encryptNote: () => encryptNote,
152
158
  ensureCreatedAt: () => ensureCreatedAt,
@@ -183,6 +189,7 @@ __export(index_exports, {
183
189
  implicitSubstrateToEvm: () => implicitSubstrateToEvm,
184
190
  importDeviceKey: () => importDeviceKey,
185
191
  importNotesFromBackup: () => importNotesFromBackup,
192
+ importPaymentSlip: () => importPaymentSlip,
186
193
  isAbortError: () => isAbortError,
187
194
  isAlreadySpentError: () => isAlreadySpentError,
188
195
  isConnectionLossError: () => isConnectionLossError,
@@ -212,6 +219,8 @@ __export(index_exports, {
212
219
  noteSpentTxHash: () => noteSpentTxHash,
213
220
  noteToTransferEntry: () => noteToTransferEntry,
214
221
  noteTxKind: () => noteTxKind,
222
+ openOutgoingBlob: () => openOutgoingBlob,
223
+ openPaymentSlip: () => openPaymentSlip,
215
224
  pairwiseEphWindow: () => pairwiseEphWindow,
216
225
  palletErrorKind: () => palletErrorKind,
217
226
  parseAmount: () => parseAmount,
@@ -221,6 +230,7 @@ __export(index_exports, {
221
230
  planTransfer: () => planTransfer,
222
231
  planUnshield: () => planUnshield,
223
232
  randomBlinding: () => randomBlinding,
233
+ randomOutgoingBlob: () => randomOutgoingBlob,
224
234
  reconstructOutgoingTxRecords: () => reconstructOutgoingTxRecords,
225
235
  recoverOwnerPkPoint: () => recoverOwnerPkPoint,
226
236
  recoverSelfStealthNote: () => recoverSelfStealthNote,
@@ -236,6 +246,8 @@ __export(index_exports, {
236
246
  runScan: () => runScan,
237
247
  scalarToHex: () => scalarToHex,
238
248
  scanAbortError: () => scanAbortError,
249
+ sealOutgoingBlob: () => sealOutgoingBlob,
250
+ sealPaymentSlip: () => sealPaymentSlip,
239
251
  selectGhosts: () => selectGhosts,
240
252
  selectNotes: () => selectNotes,
241
253
  selfEphWindow: () => selfEphWindow,
@@ -259,6 +271,7 @@ __export(index_exports, {
259
271
  truncateMiddle: () => truncateMiddle,
260
272
  tryDecryptNote: () => tryDecryptNote,
261
273
  tryDecryptNoteVerbose: () => tryDecryptNoteVerbose,
274
+ tryRecoverOutgoing: () => tryRecoverOutgoing,
262
275
  txLandedAfterError: () => txLandedAfterError,
263
276
  u128: () => import_substrate_bindings3.u128,
264
277
  u64: () => import_substrate_bindings3.u64,
@@ -1037,12 +1050,168 @@ var EncryptedMemo = {
1037
1050
  }
1038
1051
  };
1039
1052
 
1040
- // src/protocol/eph/selfEph.ts
1053
+ // src/protocol/memo/OutgoingBlob.ts
1054
+ var import_chacha2 = require("@noble/ciphers/chacha.js");
1055
+ var import_utils2 = require("@noble/ciphers/utils.js");
1056
+ var import_hkdf2 = require("@noble/hashes/hkdf.js");
1041
1057
  var import_sha23 = require("@noble/hashes/sha2.js");
1058
+ var OCK_DOMAIN = new TextEncoder().encode("orbinum-outgoing-cipher-v1");
1059
+ var NONCE_PREFIX = new TextEncoder().encode("OVK1");
1060
+ var NONCE_SUFFIX_SIZE = 8;
1061
+ var SHARED_SECRET_SIZE = 32;
1062
+ var MAC_SIZE = 16;
1063
+ var OVK_BLOB_SIZE = NONCE_SUFFIX_SIZE + SHARED_SECRET_SIZE + MAC_SIZE;
1064
+ function deriveOutgoingCipherKey(ovk, commitmentBytes, ephPkBytes) {
1065
+ if (commitmentBytes.length !== 32) {
1066
+ throw new Error(
1067
+ `OutgoingBlob: commitmentBytes must be 32 bytes, got ${commitmentBytes.length}`
1068
+ );
1069
+ }
1070
+ if (ephPkBytes.length !== 32) {
1071
+ throw new Error(`OutgoingBlob: ephPkBytes must be 32 bytes, got ${ephPkBytes.length}`);
1072
+ }
1073
+ const salt = new Uint8Array(64);
1074
+ salt.set(commitmentBytes, 0);
1075
+ salt.set(ephPkBytes, 32);
1076
+ return (0, import_hkdf2.hkdf)(import_sha23.sha256, ovk, salt, OCK_DOMAIN, 32);
1077
+ }
1078
+ function buildNonce(suffix) {
1079
+ const nonce = new Uint8Array(12);
1080
+ nonce.set(NONCE_PREFIX, 0);
1081
+ nonce.set(suffix, 4);
1082
+ return nonce;
1083
+ }
1084
+ function sealOutgoingBlob(ovk, sharedSecret, commitmentBytes, ephPkBytes) {
1085
+ if (sharedSecret.length !== SHARED_SECRET_SIZE) {
1086
+ throw new Error(`OutgoingBlob: sharedSecret must be 32 bytes, got ${sharedSecret.length}`);
1087
+ }
1088
+ const ock = deriveOutgoingCipherKey(ovk, commitmentBytes, ephPkBytes);
1089
+ const suffix = (0, import_utils2.randomBytes)(NONCE_SUFFIX_SIZE);
1090
+ const cipher = (0, import_chacha2.chacha20poly1305)(ock, buildNonce(suffix));
1091
+ const sealed = cipher.encrypt(sharedSecret);
1092
+ const blob = new Uint8Array(OVK_BLOB_SIZE);
1093
+ blob.set(suffix, 0);
1094
+ blob.set(sealed, NONCE_SUFFIX_SIZE);
1095
+ return blob;
1096
+ }
1097
+ function openOutgoingBlob(ovk, blob, commitmentBytes, ephPkBytes) {
1098
+ if (blob.length !== OVK_BLOB_SIZE) return null;
1099
+ if (commitmentBytes.length !== 32 || ephPkBytes.length !== 32) return null;
1100
+ try {
1101
+ const ock = deriveOutgoingCipherKey(ovk, commitmentBytes, ephPkBytes);
1102
+ const suffix = blob.subarray(0, NONCE_SUFFIX_SIZE);
1103
+ const sealed = blob.subarray(NONCE_SUFFIX_SIZE);
1104
+ const cipher = (0, import_chacha2.chacha20poly1305)(ock, buildNonce(suffix));
1105
+ return cipher.decrypt(sealed);
1106
+ } catch {
1107
+ return null;
1108
+ }
1109
+ }
1110
+ function randomOutgoingBlob() {
1111
+ return (0, import_utils2.randomBytes)(OVK_BLOB_SIZE);
1112
+ }
1113
+
1114
+ // src/protocol/memo/PaymentSlip.ts
1115
+ var import_chacha3 = require("@noble/ciphers/chacha.js");
1116
+ var import_utils3 = require("@noble/ciphers/utils.js");
1117
+ var import_hkdf3 = require("@noble/hashes/hkdf.js");
1118
+ var import_sha24 = require("@noble/hashes/sha2.js");
1042
1119
  var import_baby_jubjub4 = require("@zk-kit/baby-jubjub");
1120
+ var SLIP_DOMAIN = new TextEncoder().encode("orbinum-payment-slip-v1");
1121
+ var NONCE_PREFIX2 = new TextEncoder().encode("SLP1");
1122
+ var EPH_PK_SIZE2 = 32;
1123
+ var NONCE_SUFFIX_SIZE2 = 8;
1124
+ function deriveSlipKey(sharedSecret) {
1125
+ return (0, import_hkdf3.hkdf)(import_sha24.sha256, sharedSecret, void 0, SLIP_DOMAIN, 32);
1126
+ }
1127
+ function buildNonce2(suffix) {
1128
+ const nonce = new Uint8Array(12);
1129
+ nonce.set(NONCE_PREFIX2, 0);
1130
+ nonce.set(suffix, 4);
1131
+ return nonce;
1132
+ }
1133
+ function sealPaymentSlip(recipientIvkPacked, fields) {
1134
+ if (recipientIvkPacked.length !== 32) {
1135
+ throw new Error(
1136
+ `PaymentSlip: recipientIvk must be 32 bytes, got ${recipientIvkPacked.length}`
1137
+ );
1138
+ }
1139
+ const ivkPoint = (0, import_baby_jubjub4.unpackPoint)(bytesToBigintLE(recipientIvkPacked));
1140
+ if (!ivkPoint) throw new Error("PaymentSlip: invalid recipient viewing public key");
1141
+ const ephSkScalar = bytesToBjjScalar((0, import_utils3.randomBytes)(32));
1142
+ const ephPkPacked = bigintTo32Le((0, import_baby_jubjub4.packPoint)(fastMulBase(ephSkScalar)));
1143
+ const sharedPoint = fastMulPoint(ivkPoint, ephSkScalar);
1144
+ const sharedSecret = bigintTo32Le(sharedPoint[0]);
1145
+ const slipKey = deriveSlipKey(sharedSecret);
1146
+ const suffix = (0, import_utils3.randomBytes)(NONCE_SUFFIX_SIZE2);
1147
+ const cipher = (0, import_chacha3.chacha20poly1305)(slipKey, buildNonce2(suffix));
1148
+ const plaintext = new TextEncoder().encode(JSON.stringify(fields));
1149
+ const sealed = cipher.encrypt(plaintext);
1150
+ const envelope = new Uint8Array(EPH_PK_SIZE2 + NONCE_SUFFIX_SIZE2 + sealed.length);
1151
+ envelope.set(ephPkPacked, 0);
1152
+ envelope.set(suffix, EPH_PK_SIZE2);
1153
+ envelope.set(sealed, EPH_PK_SIZE2 + NONCE_SUFFIX_SIZE2);
1154
+ return envelope;
1155
+ }
1156
+ function openPaymentSlip(recipientIvsk, envelope) {
1157
+ if (envelope.length < EPH_PK_SIZE2 + NONCE_SUFFIX_SIZE2 + 16) return null;
1158
+ try {
1159
+ const ephPkPacked = envelope.subarray(0, EPH_PK_SIZE2);
1160
+ const ephPkPoint = (0, import_baby_jubjub4.unpackPoint)(bytesToBigintLE(ephPkPacked));
1161
+ if (!ephPkPoint) return null;
1162
+ const ivskScalar = bytesToBjjScalar(recipientIvsk);
1163
+ const sharedSecret = bigintTo32Le(fastMulPoint(ephPkPoint, ivskScalar)[0]);
1164
+ const slipKey = deriveSlipKey(sharedSecret);
1165
+ const suffix = envelope.subarray(EPH_PK_SIZE2, EPH_PK_SIZE2 + NONCE_SUFFIX_SIZE2);
1166
+ const sealed = envelope.subarray(EPH_PK_SIZE2 + NONCE_SUFFIX_SIZE2);
1167
+ const cipher = (0, import_chacha3.chacha20poly1305)(slipKey, buildNonce2(suffix));
1168
+ const plaintext = cipher.decrypt(sealed);
1169
+ const fields = JSON.parse(new TextDecoder().decode(plaintext));
1170
+ if (typeof fields.commitmentHex !== "string" || typeof fields.encryptedMemo !== "string") {
1171
+ return null;
1172
+ }
1173
+ return fields;
1174
+ } catch {
1175
+ return null;
1176
+ }
1177
+ }
1178
+ var PAYMENT_SLIP_SCHEME = "orbslip1:";
1179
+ function slipChecksum(payload) {
1180
+ const digest = (0, import_sha24.sha256)(new TextEncoder().encode(PAYMENT_SLIP_SCHEME + payload));
1181
+ return toHex(digest.slice(0, 4)).slice(2);
1182
+ }
1183
+ function encodePaymentSlip(envelope) {
1184
+ const payload = base64UrlEncode(toHex(envelope));
1185
+ return `${PAYMENT_SLIP_SCHEME}${payload}:${slipChecksum(payload)}`;
1186
+ }
1187
+ function decodePaymentSlip(text) {
1188
+ if (!text.startsWith(PAYMENT_SLIP_SCHEME)) return null;
1189
+ const rest = text.slice(PAYMENT_SLIP_SCHEME.length);
1190
+ const sep = rest.lastIndexOf(":");
1191
+ if (sep < 0) return null;
1192
+ const payload = rest.slice(0, sep);
1193
+ const checksum = rest.slice(sep + 1);
1194
+ if (slipChecksum(payload) !== checksum) return null;
1195
+ try {
1196
+ const hex = base64UrlDecode(payload);
1197
+ const clean = hex.startsWith("0x") ? hex.slice(2) : hex;
1198
+ if (clean.length % 2 !== 0) return null;
1199
+ const bytes = new Uint8Array(clean.length / 2);
1200
+ for (let i = 0; i < bytes.length; i++) {
1201
+ bytes[i] = parseInt(clean.slice(i * 2, i * 2 + 2), 16);
1202
+ }
1203
+ return bytes;
1204
+ } catch {
1205
+ return null;
1206
+ }
1207
+ }
1208
+
1209
+ // src/protocol/eph/selfEph.ts
1210
+ var import_sha25 = require("@noble/hashes/sha2.js");
1211
+ var import_baby_jubjub5 = require("@zk-kit/baby-jubjub");
1043
1212
  var SELF_EPH_DOMAIN = new TextEncoder().encode("orbinum-self-eph-v1");
1044
1213
  function deriveSelfEphSk(spendingKey, index) {
1045
- const h = import_sha23.sha256.create();
1214
+ const h = import_sha25.sha256.create();
1046
1215
  h.update(SELF_EPH_DOMAIN);
1047
1216
  h.update(bigintTo32Le(spendingKey));
1048
1217
  const idx = new Uint8Array(4);
@@ -1051,7 +1220,7 @@ function deriveSelfEphSk(spendingKey, index) {
1051
1220
  return h.digest();
1052
1221
  }
1053
1222
  function selfEphWindow(spendingKey, ivkPacked, from, count) {
1054
- const ivkPoint = (0, import_baby_jubjub4.unpackPoint)(bytesToBigintLE(ivkPacked));
1223
+ const ivkPoint = (0, import_baby_jubjub5.unpackPoint)(bytesToBigintLE(ivkPacked));
1055
1224
  if (!ivkPoint) throw new Error("selfEphWindow: invalid viewing public key");
1056
1225
  const entries = [];
1057
1226
  for (let i = from; i < from + count; i++) {
@@ -1060,7 +1229,7 @@ function selfEphWindow(spendingKey, ivkPacked, from, count) {
1060
1229
  const sharedPoint = fastMulPoint(ivkPoint, scalar);
1061
1230
  entries.push({
1062
1231
  index: i,
1063
- ephPkHex: toHex(bigintTo32Le((0, import_baby_jubjub4.packPoint)(ephPk))),
1232
+ ephPkHex: toHex(bigintTo32Le((0, import_baby_jubjub5.packPoint)(ephPk))),
1064
1233
  sharedSecret: bigintTo32Le(sharedPoint[0])
1065
1234
  });
1066
1235
  }
@@ -1068,17 +1237,17 @@ function selfEphWindow(spendingKey, ivkPacked, from, count) {
1068
1237
  }
1069
1238
 
1070
1239
  // src/protocol/eph/pairwiseEph.ts
1071
- var import_sha24 = require("@noble/hashes/sha2.js");
1072
- var import_baby_jubjub5 = require("@zk-kit/baby-jubjub");
1240
+ var import_sha26 = require("@noble/hashes/sha2.js");
1241
+ var import_baby_jubjub6 = require("@zk-kit/baby-jubjub");
1073
1242
  var PAIRWISE_EPH_DOMAIN = new TextEncoder().encode("orbinum-pairwise-eph-v1");
1074
1243
  function derivePairwiseSharedSecret(myViewingSk, theirIvkPacked) {
1075
- const theirPoint = (0, import_baby_jubjub5.unpackPoint)(bytesToBigintLE(theirIvkPacked));
1244
+ const theirPoint = (0, import_baby_jubjub6.unpackPoint)(bytesToBigintLE(theirIvkPacked));
1076
1245
  if (!theirPoint) throw new Error("derivePairwiseSharedSecret: invalid viewing public key");
1077
1246
  const shared = fastMulPoint(theirPoint, bytesToBjjScalar(myViewingSk));
1078
1247
  return bigintTo32Le(shared[0]);
1079
1248
  }
1080
1249
  function derivePairwiseEphSk(pairSecret, index) {
1081
- const h = import_sha24.sha256.create();
1250
+ const h = import_sha26.sha256.create();
1082
1251
  h.update(PAIRWISE_EPH_DOMAIN);
1083
1252
  h.update(pairSecret);
1084
1253
  const idx = new Uint8Array(4);
@@ -1087,7 +1256,7 @@ function derivePairwiseEphSk(pairSecret, index) {
1087
1256
  return h.digest();
1088
1257
  }
1089
1258
  function pairwiseEphWindow(pairSecret, receiverIvkPacked, from, count) {
1090
- const ivkPoint = (0, import_baby_jubjub5.unpackPoint)(bytesToBigintLE(receiverIvkPacked));
1259
+ const ivkPoint = (0, import_baby_jubjub6.unpackPoint)(bytesToBigintLE(receiverIvkPacked));
1091
1260
  if (!ivkPoint) throw new Error("pairwiseEphWindow: invalid viewing public key");
1092
1261
  const entries = [];
1093
1262
  for (let i = from; i < from + count; i++) {
@@ -1096,7 +1265,7 @@ function pairwiseEphWindow(pairSecret, receiverIvkPacked, from, count) {
1096
1265
  const sharedPoint = fastMulPoint(ivkPoint, scalar);
1097
1266
  entries.push({
1098
1267
  index: i,
1099
- ephPkHex: toHex(bigintTo32Le((0, import_baby_jubjub5.packPoint)(ephPk))),
1268
+ ephPkHex: toHex(bigintTo32Le((0, import_baby_jubjub6.packPoint)(ephPk))),
1100
1269
  sharedSecret: bigintTo32Le(sharedPoint[0])
1101
1270
  });
1102
1271
  }
@@ -1107,8 +1276,8 @@ function pairwiseEphWindow(pairSecret, receiverIvkPacked, from, count) {
1107
1276
  var CURRENT_CIRCUIT_VERSION = 1;
1108
1277
 
1109
1278
  // src/protocol/note/NoteBuilder.ts
1110
- var import_baby_jubjub6 = require("@zk-kit/baby-jubjub");
1111
- var import_utils2 = require("@noble/ciphers/utils.js");
1279
+ var import_baby_jubjub7 = require("@zk-kit/baby-jubjub");
1280
+ var import_utils4 = require("@noble/ciphers/utils.js");
1112
1281
  var import_poseidon_lite = require("poseidon-lite");
1113
1282
  var NoteBuilder = class {
1114
1283
  /**
@@ -1137,13 +1306,13 @@ var NoteBuilder = class {
1137
1306
  if (useStealth) {
1138
1307
  const recipientOwnerPk = input.recipientOwnerPk;
1139
1308
  const recipientIvkPacked = input.viewingPublicKey;
1140
- const ephSk = (0, import_utils2.randomBytes)(32);
1309
+ const ephSk = (0, import_utils4.randomBytes)(32);
1141
1310
  const ivkPackedBigint = bytesToBigintLE(recipientIvkPacked);
1142
- const ivkPoint = (0, import_baby_jubjub6.unpackPoint)(ivkPackedBigint);
1311
+ const ivkPoint = (0, import_baby_jubjub7.unpackPoint)(ivkPackedBigint);
1143
1312
  if (!ivkPoint)
1144
1313
  throw new Error("NoteBuilder.build: invalid recipient viewing public key");
1145
1314
  const ephSkScalar = BigInt(toHex(ephSk)) % BABYJUB_SUBORDER || 1n;
1146
- const sharedPoint = (0, import_baby_jubjub6.mulPointEscalar)(ivkPoint, ephSkScalar);
1315
+ const sharedPoint = (0, import_baby_jubjub7.mulPointEscalar)(ivkPoint, ephSkScalar);
1147
1316
  const sharedSecret = bigintTo32Le(sharedPoint[0]);
1148
1317
  const recipientPkPoint = recoverOwnerPkPoint(recipientOwnerPk);
1149
1318
  if (!recipientPkPoint)
@@ -1178,6 +1347,21 @@ var NoteBuilder = class {
1178
1347
  throw new Error(
1179
1348
  `NoteBuilder.build: invariant violated \u2014 memo must be ${ENCRYPTED_MEMO_SIZE} bytes, got ${memo.length}`
1180
1349
  );
1350
+ let ovkBlob;
1351
+ if (input.outgoingViewingKey !== void 0) {
1352
+ const ephPkBytes = Uint8Array.from(memo.slice(148, 180));
1353
+ const sealed = sealOutgoingBlob(
1354
+ input.outgoingViewingKey,
1355
+ sharedSecret,
1356
+ stealthCommitmentBytes,
1357
+ ephPkBytes
1358
+ );
1359
+ if (sealed.length !== OVK_BLOB_SIZE)
1360
+ throw new Error(
1361
+ `NoteBuilder.build: invariant violated \u2014 ovkBlob must be ${OVK_BLOB_SIZE} bytes, got ${sealed.length}`
1362
+ );
1363
+ ovkBlob = Array.from(sealed);
1364
+ }
1181
1365
  return {
1182
1366
  value,
1183
1367
  assetId,
@@ -1192,7 +1376,8 @@ var NoteBuilder = class {
1192
1376
  commitmentHex: toHex(commitmentBytes2),
1193
1377
  nullifierHex: toHex(nullifierBytes2),
1194
1378
  memo,
1195
- counterpartyPk
1379
+ counterpartyPk,
1380
+ ...ovkBlob ? { ovkBlob } : {}
1196
1381
  };
1197
1382
  }
1198
1383
  const commitment = (0, import_poseidon_lite.poseidon4)([value, assetId, ownerPk, blinding]);
@@ -1322,6 +1507,18 @@ function buildDummyTransferInput(assetId) {
1322
1507
  }
1323
1508
 
1324
1509
  // src/protocol/note/NoteDecryptor.ts
1510
+ function decryptAndVerifyPlaintext(memoBytes, commitmentBytes, sharedSecret, effectiveOwnerPk) {
1511
+ const plaintext = EncryptedMemo.decryptWithSharedSecret(
1512
+ memoBytes,
1513
+ commitmentBytes,
1514
+ sharedSecret
1515
+ );
1516
+ if (!plaintext) return null;
1517
+ const ownerPk = effectiveOwnerPk ?? plaintext.ownerPk;
1518
+ const recomputed = (0, import_poseidon_lite2.poseidon4)([plaintext.value, plaintext.assetId, ownerPk, plaintext.blinding]);
1519
+ if (recomputed !== bytesToBigintLE(commitmentBytes)) return null;
1520
+ return plaintext;
1521
+ }
1325
1522
  function computeNullifier(commitment, spendingKey) {
1326
1523
  return (0, import_poseidon_lite2.poseidon2)([commitment, spendingKey]);
1327
1524
  }
@@ -1404,6 +1601,40 @@ function tryDecryptNoteVerbose(commitment, viewingSecretKey, spendingKey, ownOwn
1404
1601
  }
1405
1602
  };
1406
1603
  }
1604
+ function tryRecoverOutgoing(hint, ovk, opts) {
1605
+ if (!hint.ovkBlob || !hint.encryptedMemo) return null;
1606
+ let commitmentBytes;
1607
+ let memoBytes;
1608
+ let blobBytes;
1609
+ try {
1610
+ commitmentBytes = fromHex(hint.commitmentHex);
1611
+ memoBytes = fromHex(hint.encryptedMemo);
1612
+ blobBytes = fromHex(hint.ovkBlob);
1613
+ } catch {
1614
+ return null;
1615
+ }
1616
+ if (memoBytes.length !== ENCRYPTED_MEMO_SIZE) return null;
1617
+ if (blobBytes.length !== OVK_BLOB_SIZE) return null;
1618
+ const ephPkBytes = memoBytes.subarray(148, 180);
1619
+ const sharedSecret = openOutgoingBlob(ovk, blobBytes, commitmentBytes, ephPkBytes);
1620
+ if (!sharedSecret) return null;
1621
+ const activation = opts?.viewTagActivationLeaf;
1622
+ if (activation !== void 0 && isValidLeafIndex(hint.leafIndex) && hint.leafIndex >= activation && !EncryptedMemo.checkViewTag(memoBytes, sharedSecret)) {
1623
+ return null;
1624
+ }
1625
+ const plaintext = decryptAndVerifyPlaintext(memoBytes, commitmentBytes, sharedSecret, null);
1626
+ if (!plaintext) return null;
1627
+ return {
1628
+ commitmentHex: hint.commitmentHex,
1629
+ ...isValidLeafIndex(hint.leafIndex) ? { leafIndex: hint.leafIndex } : {},
1630
+ value: plaintext.value,
1631
+ assetId: plaintext.assetId,
1632
+ recipientStealthPk: plaintext.ownerPk,
1633
+ blinding: plaintext.blinding,
1634
+ counterpartyPk: plaintext.counterpartyPk,
1635
+ circuitVersion: plaintext.circuitVersion
1636
+ };
1637
+ }
1407
1638
 
1408
1639
  // src/protocol/note/NoteDisclosure.ts
1409
1640
  var import_poseidon_lite3 = require("poseidon-lite");
@@ -1549,10 +1780,11 @@ ${canonicalAccountId(address)}`;
1549
1780
  }
1550
1781
 
1551
1782
  // src/protocol/keys/PrivacyKeys.ts
1552
- var import_hkdf2 = require("@noble/hashes/hkdf.js");
1553
- var import_sha25 = require("@noble/hashes/sha2.js");
1554
- var import_baby_jubjub7 = require("@zk-kit/baby-jubjub");
1783
+ var import_hkdf4 = require("@noble/hashes/hkdf.js");
1784
+ var import_sha27 = require("@noble/hashes/sha2.js");
1785
+ var import_baby_jubjub8 = require("@zk-kit/baby-jubjub");
1555
1786
  var IVK_DOMAIN = new TextEncoder().encode("orbinum-ivk-v1");
1787
+ var OVK_DOMAIN = new TextEncoder().encode("orbinum-ovk-v1");
1556
1788
  var KEY_VERSION = "v2";
1557
1789
  function deriveSpendingKeyFromMaster(masterBytes) {
1558
1790
  const skBigint = BigInt(toHex(masterBytes)) % BABYJUB_SUBORDER;
@@ -1560,12 +1792,12 @@ function deriveSpendingKeyFromMaster(masterBytes) {
1560
1792
  }
1561
1793
  function deriveViewingSecretKey(spendingKey) {
1562
1794
  const ikm = bigintTo32Le(spendingKey);
1563
- return (0, import_hkdf2.hkdf)(import_sha25.sha256, ikm, void 0, IVK_DOMAIN, 32);
1795
+ return (0, import_hkdf4.hkdf)(import_sha27.sha256, ikm, void 0, IVK_DOMAIN, 32);
1564
1796
  }
1565
1797
  function deriveViewingPublicKey(ivsk) {
1566
1798
  const ivskScalar = BigInt(toHex(ivsk)) % BABYJUB_SUBORDER || 1n;
1567
1799
  const ivkPoint = fastMulBase(ivskScalar);
1568
- const packed = (0, import_baby_jubjub7.packPoint)(ivkPoint);
1800
+ const packed = (0, import_baby_jubjub8.packPoint)(ivkPoint);
1569
1801
  return bigintTo32Le(packed);
1570
1802
  }
1571
1803
  function deriveOwnerPk(spendingKey) {
@@ -1576,12 +1808,15 @@ function deriveOwnerPk(spendingKey) {
1576
1808
  return 0n;
1577
1809
  }
1578
1810
  }
1811
+ function deriveOutgoingViewingKey(masterBytes) {
1812
+ return (0, import_hkdf4.hkdf)(import_sha27.sha256, masterBytes, void 0, OVK_DOMAIN, 32);
1813
+ }
1579
1814
 
1580
1815
  // src/protocol/keys/PrivacyKeyManager.ts
1581
- var import_sha26 = require("@noble/hashes/sha2.js");
1816
+ var import_sha28 = require("@noble/hashes/sha2.js");
1582
1817
  function privacyAddressChecksum(ownerPkHex, ivkHex) {
1583
1818
  const body = `orbpriv2:${ownerPkHex}:${ivkHex}`;
1584
- const digest = (0, import_sha26.sha256)(new TextEncoder().encode(body));
1819
+ const digest = (0, import_sha28.sha256)(new TextEncoder().encode(body));
1585
1820
  return toHex(digest.slice(0, 4)).slice(2);
1586
1821
  }
1587
1822
  var PrivacyKeyManager = class {
@@ -1590,7 +1825,8 @@ var PrivacyKeyManager = class {
1590
1825
  masterBytes: null,
1591
1826
  viewingSecretKey: null,
1592
1827
  viewingPublicKeyPacked: null,
1593
- ownerPk: null
1828
+ ownerPk: null,
1829
+ outgoingViewingKey: null
1594
1830
  };
1595
1831
  /**
1596
1832
  * Load a spending key and its corresponding master bytes into the in-memory session.
@@ -1605,12 +1841,14 @@ var PrivacyKeyManager = class {
1605
1841
  const viewingSecretKey = deriveViewingSecretKey(spendingKey);
1606
1842
  const viewingPublicKeyPacked = deriveViewingPublicKey(viewingSecretKey);
1607
1843
  const ownerPk = deriveOwnerPk(spendingKey);
1844
+ const outgoingViewingKey = deriveOutgoingViewingKey(masterBytes);
1608
1845
  this._state = {
1609
1846
  spendingKey,
1610
1847
  masterBytes,
1611
1848
  viewingSecretKey,
1612
1849
  viewingPublicKeyPacked,
1613
- ownerPk
1850
+ ownerPk,
1851
+ outgoingViewingKey
1614
1852
  };
1615
1853
  }
1616
1854
  /** Clear all key material from memory. Call on vault lock / sign-out. */
@@ -1620,7 +1858,8 @@ var PrivacyKeyManager = class {
1620
1858
  masterBytes: null,
1621
1859
  viewingSecretKey: null,
1622
1860
  viewingPublicKeyPacked: null,
1623
- ownerPk: null
1861
+ ownerPk: null,
1862
+ outgoingViewingKey: null
1624
1863
  };
1625
1864
  }
1626
1865
  /** Returns true if a spending key has been loaded. */
@@ -1665,6 +1904,16 @@ var PrivacyKeyManager = class {
1665
1904
  }
1666
1905
  return this._state.ownerPk;
1667
1906
  }
1907
+ /**
1908
+ * Returns the 32-byte outgoing viewing key (ovk). Throws if not loaded.
1909
+ * Used to seal/open the outgoing blob that lets the sender recover a transfer.
1910
+ */
1911
+ getOutgoingViewingKey() {
1912
+ if (this._state.outgoingViewingKey === null) {
1913
+ throw new Error("PrivacyKeyManager: no key loaded. Call load() first.");
1914
+ }
1915
+ return this._state.outgoingViewingKey;
1916
+ }
1668
1917
  /** Returns the spending key as a 32-byte little-endian Uint8Array. Throws if not loaded. */
1669
1918
  getSpendingKeyBytes() {
1670
1919
  return bigintTo32Le(this.getSpendingKey());
@@ -1766,8 +2015,8 @@ var PrivacyKeyManager = class {
1766
2015
  };
1767
2016
 
1768
2017
  // src/protocol/keys/spendingKeyDerivation.ts
1769
- var import_hkdf3 = require("@noble/hashes/hkdf.js");
1770
- var import_sha27 = require("@noble/hashes/sha2.js");
2018
+ var import_hkdf5 = require("@noble/hashes/hkdf.js");
2019
+ var import_sha29 = require("@noble/hashes/sha2.js");
1771
2020
  var MIN_SIGNATURE_BYTES = 32;
1772
2021
  var MIN_DISTINCT_BYTES = 8;
1773
2022
  function assertUsableSignature(sigBytes) {
@@ -1789,7 +2038,7 @@ async function deriveMasterKeyBytes(signatureHex, chainId, address) {
1789
2038
  const info = new TextEncoder().encode(
1790
2039
  `orbinum-sk-${KEY_VERSION}:${chainId}:${canonicalAccountId(address)}`
1791
2040
  );
1792
- return (0, import_hkdf3.hkdf)(import_sha27.sha256, sigBytes, new Uint8Array(0), info, 32);
2041
+ return (0, import_hkdf5.hkdf)(import_sha29.sha256, sigBytes, new Uint8Array(0), info, 32);
1793
2042
  }
1794
2043
  async function deriveSpendingKeyFromSignature(signatureHex, chainId, address) {
1795
2044
  return deriveSpendingKeyFromMaster(await deriveMasterKeyBytes(signatureHex, chainId, address));
@@ -1797,8 +2046,8 @@ async function deriveSpendingKeyFromSignature(signatureHex, chainId, address) {
1797
2046
 
1798
2047
  // src/protocol/proving/unshield.ts
1799
2048
  var import_proof_generator = require("@orbinum/proof-generator");
1800
- var import_utils3 = require("@noble/ciphers/utils.js");
1801
- var import_baby_jubjub8 = require("@zk-kit/baby-jubjub");
2049
+ var import_utils5 = require("@noble/ciphers/utils.js");
2050
+ var import_baby_jubjub9 = require("@zk-kit/baby-jubjub");
1802
2051
  var import_poseidon_lite4 = require("poseidon-lite");
1803
2052
 
1804
2053
  // src/protocol/proving/merkle.ts
@@ -1830,8 +2079,8 @@ async function generateUnshieldProof(inputs, options = {}) {
1830
2079
  if (changeValue < 0n) {
1831
2080
  throw new Error("changeValue must be >= 0.");
1832
2081
  }
1833
- const changeOwnerPubkey = inputs.changeOwnerPubkey ?? (0, import_baby_jubjub8.mulPointEscalar)(import_baby_jubjub8.Base8, inputs.spendingKey)[0];
1834
- const changeBlinding = inputs.changeBlinding ?? (changeValue > 0n ? bytesToBigintLE((0, import_utils3.randomBytes)(32)) : 0n);
2082
+ const changeOwnerPubkey = inputs.changeOwnerPubkey ?? (0, import_baby_jubjub9.mulPointEscalar)(import_baby_jubjub9.Base8, inputs.spendingKey)[0];
2083
+ const changeBlinding = inputs.changeBlinding ?? (changeValue > 0n ? bytesToBigintLE((0, import_utils5.randomBytes)(32)) : 0n);
1835
2084
  const changeCommitment = changeValue > 0n ? (0, import_poseidon_lite4.poseidon4)([changeValue, inputs.assetId, changeOwnerPubkey, changeBlinding]) : 0n;
1836
2085
  const circuitInputs = {
1837
2086
  merkle_root: leHexToBigint(inputs.merkleRoot).toString(),
@@ -5675,6 +5924,7 @@ async function processHints(ctx, hints, total) {
5675
5924
  pageEntries.push({ note, isNew });
5676
5925
  }
5677
5926
  if (pageEntries.length > 0) await ctx.onPage?.(pageEntries);
5927
+ await ctx.onBatchDone?.(outcome.maxLeafIndex);
5678
5928
  ctx.onProgress?.({ scanned: outcome.scanned, total, found: outcome.found });
5679
5929
  }
5680
5930
  async function processSealedChunks(ctx, source, sinceLeafIndex) {
@@ -5709,6 +5959,7 @@ async function processSealedChunks(ctx, source, sinceLeafIndex) {
5709
5959
  }
5710
5960
  async function collectScanEntries(params) {
5711
5961
  const { source, pool, keys, existingHexes, sinceLeafIndex, onProgress, signal, onPage } = params;
5962
+ const { onBatchDone } = params;
5712
5963
  if (signal?.aborted) throw scanAbortError();
5713
5964
  const outcome = {
5714
5965
  scanEntries: [],
@@ -5732,7 +5983,8 @@ async function collectScanEntries(params) {
5732
5983
  vaultHexes: params.vaultHexes ?? existingHexes,
5733
5984
  onProgress,
5734
5985
  signal,
5735
- onPage
5986
+ onPage,
5987
+ onBatchDone
5736
5988
  };
5737
5989
  let tailSince = sinceLeafIndex;
5738
5990
  try {
@@ -5773,63 +6025,12 @@ async function collectScanEntries(params) {
5773
6025
  return outcome;
5774
6026
  }
5775
6027
 
5776
- // src/wallet/worker/kernel/types.ts
5777
- var SELF_EPH_WINDOW = 1024;
5778
- var PAIRWISE_EPH_WINDOW = 64;
5779
- var EMPTY_BATCH_RESULT = {
5780
- notes: [],
5781
- tagFiltered: 0,
5782
- selfMatched: 0,
5783
- pairwiseMatched: 0,
5784
- maxSelfEphIndex: null
5785
- };
5786
-
5787
- // src/wallet/scanner/selfEphGap.ts
5788
- function noteEphPkHex(note) {
5789
- if (!Array.isArray(note.memo) || note.memo.length < 32) return null;
5790
- return toHex(note.memo.slice(-32)).toLowerCase();
5791
- }
5792
- function gapMargin(windowSize) {
5793
- return Math.max(1, Math.floor(windowSize / 16));
5794
- }
5795
- function resolveSelfEphCeiling(params) {
5796
- const { notes, spendingKey, viewingKey, scanMaxIndex } = params;
5797
- const windowSize = params.windowSize ?? SELF_EPH_WINDOW;
5798
- if (scanMaxIndex === null) return null;
5799
- if (scanMaxIndex < windowSize - gapMargin(windowSize)) return scanMaxIndex;
5800
- const ownEphPks = /* @__PURE__ */ new Set();
5801
- for (const note of notes) {
5802
- const hex = noteEphPkHex(note);
5803
- if (hex) ownEphPks.add(hex);
5804
- }
5805
- const ivkPacked = deriveViewingPublicKey(viewingKey);
5806
- let max = scanMaxIndex;
5807
- let from = windowSize;
5808
- for (; ; ) {
5809
- const slice = selfEphWindow(spendingKey, ivkPacked, from, windowSize);
5810
- let matched = false;
5811
- for (const entry of slice) {
5812
- if (ownEphPks.has(entry.ephPkHex.toLowerCase())) {
5813
- matched = true;
5814
- if (entry.index > max) max = entry.index;
5815
- }
5816
- }
5817
- if (!matched) break;
5818
- from += windowSize;
5819
- }
5820
- return max;
5821
- }
5822
- function windowSizeForCounter(counter) {
5823
- const needed = counter + gapMargin(SELF_EPH_WINDOW);
5824
- return Math.max(SELF_EPH_WINDOW, Math.ceil(needed / SELF_EPH_WINDOW) * SELF_EPH_WINDOW);
5825
- }
5826
-
5827
6028
  // src/wallet/scanner/phases/spentSet.ts
5828
6029
  function normalizeNullifierHex(hex) {
5829
6030
  return hex.toLowerCase();
5830
6031
  }
5831
- async function resolveSpentSet(params) {
5832
- const { source, cache, ownNullifiers, signal } = params;
6032
+ async function openSpentSet(params) {
6033
+ const { source, cache, signal } = params;
5833
6034
  const throwIfAborted = () => {
5834
6035
  if (signal?.aborted) throw scanAbortError();
5835
6036
  };
@@ -5838,20 +6039,29 @@ async function resolveSpentSet(params) {
5838
6039
  await syncNullifierCache(params, manifest, throwIfAborted);
5839
6040
  throwIfAborted();
5840
6041
  const tail = await getConsistentTail(params, throwIfAborted);
5841
- const stored = await cache.getSpentNullifiers([...ownNullifiers]);
5842
6042
  const tailMap = new Map(
5843
6043
  tail.data.map((h, i) => [
5844
6044
  normalizeNullifierHex(h),
5845
6045
  { spentAt: tail.timestampsMs?.[i] ?? null, txHash: tail.txHashes?.[i] ?? null }
5846
6046
  ])
5847
6047
  );
5848
- const spent = /* @__PURE__ */ new Map();
5849
- const unknown = { spentAt: null, txHash: null };
5850
- for (const h of ownNullifiers) {
5851
- if (stored.has(h)) spent.set(h, stored.get(h) ?? unknown);
5852
- else if (tailMap.has(h)) spent.set(h, tailMap.get(h) ?? unknown);
5853
- }
5854
- return spent;
6048
+ return {
6049
+ async query(ownNullifiers) {
6050
+ if (ownNullifiers.size === 0) return /* @__PURE__ */ new Map();
6051
+ const stored = await cache.getSpentNullifiers([...ownNullifiers]);
6052
+ const spent = /* @__PURE__ */ new Map();
6053
+ const unknown = { spentAt: null, txHash: null };
6054
+ for (const h of ownNullifiers) {
6055
+ if (stored.has(h)) spent.set(h, stored.get(h) ?? unknown);
6056
+ else if (tailMap.has(h)) spent.set(h, tailMap.get(h) ?? unknown);
6057
+ }
6058
+ return spent;
6059
+ }
6060
+ };
6061
+ }
6062
+ async function resolveSpentSet(params) {
6063
+ const handle = await openSpentSet(params);
6064
+ return handle.query(params.ownNullifiers);
5855
6065
  }
5856
6066
  async function syncNullifierCache(params, manifest, throwIfAborted) {
5857
6067
  const { source, cache } = params;
@@ -5901,40 +6111,6 @@ async function getConsistentTail(params, throwIfAborted) {
5901
6111
  throw new Error("nullifier tail inconsistent with local chunks after retry");
5902
6112
  }
5903
6113
 
5904
- // src/wallet/scanner/phases/spentStatus.ts
5905
- function collectNullifiersToQuery(scanEntries, onChainHexes, vaultNotes) {
5906
- const syncedCommitments = new Set(scanEntries.map((e) => e.note.commitmentHex));
5907
- const nullifiers = /* @__PURE__ */ new Set();
5908
- for (const { note } of scanEntries) {
5909
- nullifiers.add(note.nullifierHex.toLowerCase());
5910
- }
5911
- for (const vaultNote of vaultNotes) {
5912
- if (syncedCommitments.has(vaultNote.commitmentHex)) continue;
5913
- if (!onChainHexes.has(vaultNote.commitmentHex)) continue;
5914
- if (vaultNote.nullifierHex) nullifiers.add(vaultNote.nullifierHex.toLowerCase());
5915
- }
5916
- return nullifiers;
5917
- }
5918
- async function resolveSpentStatus(params) {
5919
- if (params.nullifiers.size === 0) return /* @__PURE__ */ new Map();
5920
- try {
5921
- return await resolveSpentSet({
5922
- source: params.source,
5923
- cache: params.cache,
5924
- ownNullifiers: params.nullifiers,
5925
- signal: params.signal,
5926
- onWarning: params.onWarning
5927
- });
5928
- } catch (err) {
5929
- if (isAbortError(err)) throw err;
5930
- params.onWarning?.(
5931
- "could not resolve spent-nullifier status (feed unavailable or inconsistent); spent status left unverified this pass",
5932
- err
5933
- );
5934
- return /* @__PURE__ */ new Map();
5935
- }
5936
- }
5937
-
5938
6114
  // src/wallet/scanner/phases/persist.ts
5939
6115
  function purgeIsTrustworthy(hintsScanned, purgeableNotes) {
5940
6116
  if (purgeableNotes === 0) return true;
@@ -5993,6 +6169,134 @@ async function persistCursor(storage, maxLeafIndex, isIncremental) {
5993
6169
  });
5994
6170
  }
5995
6171
 
6172
+ // src/wallet/scanner/phases/checkpoint.ts
6173
+ function createScanCheckpoint(params) {
6174
+ const { vault, storage, nullifiers, isIncremental, signal, onWarning } = params;
6175
+ const throwIfAborted = () => {
6176
+ if (signal?.aborted) throw scanAbortError();
6177
+ };
6178
+ let spentSetPromise = null;
6179
+ const getSpentSet = () => spentSetPromise ??= openSpentSet({
6180
+ source: nullifiers,
6181
+ cache: storage,
6182
+ signal,
6183
+ onWarning
6184
+ }).catch(() => null);
6185
+ return {
6186
+ async savePageNotes(entries) {
6187
+ throwIfAborted();
6188
+ const fresh = entries.filter((e) => e.isNew);
6189
+ if (fresh.length === 0) return;
6190
+ const own = new Set(fresh.map((e) => e.note.nullifierHex.toLowerCase()));
6191
+ const spentSet = await getSpentSet();
6192
+ const spent = spentSet ? await spentSet.query(own).catch(() => /* @__PURE__ */ new Map()) : /* @__PURE__ */ new Map();
6193
+ throwIfAborted();
6194
+ await vault.saveMany(
6195
+ fresh.map(({ note }) => {
6196
+ const spend = spent.get(note.nullifierHex.toLowerCase());
6197
+ return {
6198
+ note: spend ? stampSpentTxHash(note, spend.txHash) : note,
6199
+ noteStatus: {
6200
+ spent: spend !== void 0,
6201
+ spentAt: spend?.spentAt ?? null
6202
+ }
6203
+ };
6204
+ })
6205
+ );
6206
+ },
6207
+ async advanceCursor(maxLeafIndex) {
6208
+ if (maxLeafIndex === void 0) return;
6209
+ throwIfAborted();
6210
+ await persistCursor(storage, maxLeafIndex, isIncremental);
6211
+ }
6212
+ };
6213
+ }
6214
+
6215
+ // src/wallet/worker/kernel/types.ts
6216
+ var SELF_EPH_WINDOW = 1024;
6217
+ var PAIRWISE_EPH_WINDOW = 64;
6218
+ var EMPTY_BATCH_RESULT = {
6219
+ notes: [],
6220
+ tagFiltered: 0,
6221
+ selfMatched: 0,
6222
+ pairwiseMatched: 0,
6223
+ maxSelfEphIndex: null
6224
+ };
6225
+
6226
+ // src/wallet/scanner/selfEphGap.ts
6227
+ function noteEphPkHex(note) {
6228
+ if (!Array.isArray(note.memo) || note.memo.length < 32) return null;
6229
+ return toHex(note.memo.slice(-32)).toLowerCase();
6230
+ }
6231
+ function gapMargin(windowSize) {
6232
+ return Math.max(1, Math.floor(windowSize / 16));
6233
+ }
6234
+ function resolveSelfEphCeiling(params) {
6235
+ const { notes, spendingKey, viewingKey, scanMaxIndex } = params;
6236
+ const windowSize = params.windowSize ?? SELF_EPH_WINDOW;
6237
+ if (scanMaxIndex === null) return null;
6238
+ if (scanMaxIndex < windowSize - gapMargin(windowSize)) return scanMaxIndex;
6239
+ const ownEphPks = /* @__PURE__ */ new Set();
6240
+ for (const note of notes) {
6241
+ const hex = noteEphPkHex(note);
6242
+ if (hex) ownEphPks.add(hex);
6243
+ }
6244
+ const ivkPacked = deriveViewingPublicKey(viewingKey);
6245
+ let max = scanMaxIndex;
6246
+ let from = windowSize;
6247
+ for (; ; ) {
6248
+ const slice = selfEphWindow(spendingKey, ivkPacked, from, windowSize);
6249
+ let matched = false;
6250
+ for (const entry of slice) {
6251
+ if (ownEphPks.has(entry.ephPkHex.toLowerCase())) {
6252
+ matched = true;
6253
+ if (entry.index > max) max = entry.index;
6254
+ }
6255
+ }
6256
+ if (!matched) break;
6257
+ from += windowSize;
6258
+ }
6259
+ return max;
6260
+ }
6261
+ function windowSizeForCounter(counter) {
6262
+ const needed = counter + gapMargin(SELF_EPH_WINDOW);
6263
+ return Math.max(SELF_EPH_WINDOW, Math.ceil(needed / SELF_EPH_WINDOW) * SELF_EPH_WINDOW);
6264
+ }
6265
+
6266
+ // src/wallet/scanner/phases/spentStatus.ts
6267
+ function collectNullifiersToQuery(scanEntries, onChainHexes, vaultNotes) {
6268
+ const syncedCommitments = new Set(scanEntries.map((e) => e.note.commitmentHex));
6269
+ const nullifiers = /* @__PURE__ */ new Set();
6270
+ for (const { note } of scanEntries) {
6271
+ nullifiers.add(note.nullifierHex.toLowerCase());
6272
+ }
6273
+ for (const vaultNote of vaultNotes) {
6274
+ if (syncedCommitments.has(vaultNote.commitmentHex)) continue;
6275
+ if (!onChainHexes.has(vaultNote.commitmentHex)) continue;
6276
+ if (vaultNote.nullifierHex) nullifiers.add(vaultNote.nullifierHex.toLowerCase());
6277
+ }
6278
+ return nullifiers;
6279
+ }
6280
+ async function resolveSpentStatus(params) {
6281
+ if (params.nullifiers.size === 0) return /* @__PURE__ */ new Map();
6282
+ try {
6283
+ return await resolveSpentSet({
6284
+ source: params.source,
6285
+ cache: params.cache,
6286
+ ownNullifiers: params.nullifiers,
6287
+ signal: params.signal,
6288
+ onWarning: params.onWarning
6289
+ });
6290
+ } catch (err) {
6291
+ if (isAbortError(err)) throw err;
6292
+ params.onWarning?.(
6293
+ "could not resolve spent-nullifier status (feed unavailable or inconsistent); spent status left unverified this pass",
6294
+ err
6295
+ );
6296
+ return /* @__PURE__ */ new Map();
6297
+ }
6298
+ }
6299
+
5996
6300
  // src/wallet/scanner/pipeline.ts
5997
6301
  async function runScan(params) {
5998
6302
  const { vault, storage, hints, nullifiers, pool, keys, sinceLeafIndex, signal, onWarning } = params;
@@ -6011,6 +6315,14 @@ async function runScan(params) {
6011
6315
  }
6012
6316
  }
6013
6317
  );
6318
+ const checkpoint = createScanCheckpoint({
6319
+ vault,
6320
+ storage,
6321
+ nullifiers,
6322
+ isIncremental,
6323
+ signal,
6324
+ onWarning
6325
+ });
6014
6326
  const scan = await collectScanEntries({
6015
6327
  source: hints,
6016
6328
  pool,
@@ -6031,7 +6343,9 @@ async function runScan(params) {
6031
6343
  // consumer probes it with a vault commitment, so keeping the whole pool
6032
6344
  // only bought heap.
6033
6345
  vaultHexes: preScanHexes,
6034
- onWarning
6346
+ onWarning,
6347
+ onPage: (entries) => checkpoint.savePageNotes(entries),
6348
+ onBatchDone: (maxLeafIndex) => checkpoint.advanceCursor(maxLeafIndex)
6035
6349
  });
6036
6350
  const nullifiersToQuery = collectNullifiersToQuery(
6037
6351
  scan.scanEntries,
@@ -6404,6 +6718,22 @@ function isEntry2(value) {
6404
6718
  return true;
6405
6719
  }
6406
6720
 
6721
+ // src/wallet/ops/notes/paymentSlipImport.ts
6722
+ function importPaymentSlip(slip, keys) {
6723
+ const envelope = typeof slip === "string" ? decodePaymentSlip(slip) : slip;
6724
+ if (!envelope) return null;
6725
+ const fields = openPaymentSlip(keys.viewingSecretKey, envelope);
6726
+ if (!fields) return null;
6727
+ const commitment = {
6728
+ commitmentHex: fields.commitmentHex,
6729
+ leafIndex: fields.leafIndex ?? -1,
6730
+ encryptedMemo: fields.encryptedMemo
6731
+ };
6732
+ const note = tryDecryptNote(commitment, keys.viewingSecretKey, keys.spendingKey, keys.ownerPk);
6733
+ if (!note) return null;
6734
+ return fields.txHash ? stampCreatedTxHash(note, fields.txHash) : note;
6735
+ }
6736
+
6407
6737
  // src/wallet/ops/spend/transfer.ts
6408
6738
  var import_proof_generator6 = require("@orbinum/proof-generator");
6409
6739
 
@@ -6613,6 +6943,18 @@ async function transferNotes(deps, params, onProgress) {
6613
6943
  await deps.vault.save(stampCreatedTxHash(selfNote, txResult.txHash, txKind));
6614
6944
  }
6615
6945
  }
6946
+ const isSelf = deps.selfOwnerPk !== null && recipientPk === deps.selfOwnerPk;
6947
+ if (recipientViewingPublicKey !== void 0 && !isSelf) {
6948
+ try {
6949
+ const envelope = sealPaymentSlip(recipientViewingPublicKey, {
6950
+ commitmentHex: recipientNote.commitmentHex,
6951
+ encryptedMemo: toHex(Uint8Array.from(recipientNote.memo)),
6952
+ ...txResult.txHash ? { txHash: txResult.txHash } : {}
6953
+ });
6954
+ return { ...txResult, paymentSlip: encodePaymentSlip(envelope) };
6955
+ } catch {
6956
+ }
6957
+ }
6616
6958
  }
6617
6959
  return txResult;
6618
6960
  }
@@ -7496,11 +7838,13 @@ var OrbinumWallet = class {
7496
7838
  NOTE_BIGINT_FIELDS,
7497
7839
  NOTE_TRANSFER_URI_SCHEME,
7498
7840
  NoteBuilder,
7841
+ OVK_BLOB_SIZE,
7499
7842
  OrbinumClient,
7500
7843
  OrbinumClientProvider,
7501
7844
  OrbinumWallet,
7502
7845
  PAGE_SIZE,
7503
7846
  PAIRWISE_EPH_WINDOW,
7847
+ PAYMENT_SLIP_SCHEME,
7504
7848
  PRECOMPILE_ADDR,
7505
7849
  PrivacyKeyManager,
7506
7850
  PrivacyModule,
@@ -7571,11 +7915,14 @@ var OrbinumWallet = class {
7571
7915
  decodeNoteBackup,
7572
7916
  decodeNoteDisclosureKey,
7573
7917
  decodeNoteTransferPage,
7918
+ decodePaymentSlip,
7574
7919
  decodePrecompileCalldata,
7575
7920
  decryptHintBatch,
7576
7921
  decryptJson,
7577
7922
  decryptNoteRecord,
7578
7923
  deriveMasterKeyBytes,
7924
+ deriveOutgoingCipherKey,
7925
+ deriveOutgoingViewingKey,
7579
7926
  deriveOwnerPk,
7580
7927
  derivePairwiseEphSk,
7581
7928
  derivePairwiseSharedSecret,
@@ -7594,6 +7941,7 @@ var OrbinumWallet = class {
7594
7941
  detectCommitmentMismatch,
7595
7942
  encodeNoteBackup,
7596
7943
  encodeNoteTransferPages,
7944
+ encodePaymentSlip,
7597
7945
  encryptJson,
7598
7946
  encryptNote,
7599
7947
  ensureCreatedAt,
@@ -7630,6 +7978,7 @@ var OrbinumWallet = class {
7630
7978
  implicitSubstrateToEvm,
7631
7979
  importDeviceKey,
7632
7980
  importNotesFromBackup,
7981
+ importPaymentSlip,
7633
7982
  isAbortError,
7634
7983
  isAlreadySpentError,
7635
7984
  isConnectionLossError,
@@ -7659,6 +8008,8 @@ var OrbinumWallet = class {
7659
8008
  noteSpentTxHash,
7660
8009
  noteToTransferEntry,
7661
8010
  noteTxKind,
8011
+ openOutgoingBlob,
8012
+ openPaymentSlip,
7662
8013
  pairwiseEphWindow,
7663
8014
  palletErrorKind,
7664
8015
  parseAmount,
@@ -7668,6 +8019,7 @@ var OrbinumWallet = class {
7668
8019
  planTransfer,
7669
8020
  planUnshield,
7670
8021
  randomBlinding,
8022
+ randomOutgoingBlob,
7671
8023
  reconstructOutgoingTxRecords,
7672
8024
  recoverOwnerPkPoint,
7673
8025
  recoverSelfStealthNote,
@@ -7683,6 +8035,8 @@ var OrbinumWallet = class {
7683
8035
  runScan,
7684
8036
  scalarToHex,
7685
8037
  scanAbortError,
8038
+ sealOutgoingBlob,
8039
+ sealPaymentSlip,
7686
8040
  selectGhosts,
7687
8041
  selectNotes,
7688
8042
  selfEphWindow,
@@ -7706,6 +8060,7 @@ var OrbinumWallet = class {
7706
8060
  truncateMiddle,
7707
8061
  tryDecryptNote,
7708
8062
  tryDecryptNoteVerbose,
8063
+ tryRecoverOutgoing,
7709
8064
  txLandedAfterError,
7710
8065
  u128,
7711
8066
  u64,