@orbinum/sdk 1.1.0 → 1.2.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(),
@@ -6336,7 +6585,9 @@ function noteToBackupEntry(note) {
6336
6585
  return {
6337
6586
  commitmentHex: note.commitmentHex,
6338
6587
  encryptedMemo: toHex(Uint8Array.from(note.memo)),
6339
- ...note.leafIndex !== void 0 ? { leafIndex: note.leafIndex } : {}
6588
+ ...note.leafIndex !== void 0 ? { leafIndex: note.leafIndex } : {},
6589
+ spent: note.spent,
6590
+ spentAt: note.spentAt
6340
6591
  };
6341
6592
  }
6342
6593
  function encodeNoteBackup(notes, options = {}) {
@@ -6381,7 +6632,13 @@ function importNotesFromBackup(entries, keys) {
6381
6632
  keys.spendingKey,
6382
6633
  keys.ownerPk
6383
6634
  );
6384
- if (note) out.push(note);
6635
+ if (note) {
6636
+ out.push({
6637
+ ...note,
6638
+ spent: entry.spent ?? false,
6639
+ spentAt: entry.spentAt ?? null
6640
+ });
6641
+ }
6385
6642
  }
6386
6643
  return out;
6387
6644
  }
@@ -6396,6 +6653,22 @@ function isEntry2(value) {
6396
6653
  return true;
6397
6654
  }
6398
6655
 
6656
+ // src/wallet/ops/notes/paymentSlipImport.ts
6657
+ function importPaymentSlip(slip, keys) {
6658
+ const envelope = typeof slip === "string" ? decodePaymentSlip(slip) : slip;
6659
+ if (!envelope) return null;
6660
+ const fields = openPaymentSlip(keys.viewingSecretKey, envelope);
6661
+ if (!fields) return null;
6662
+ const commitment = {
6663
+ commitmentHex: fields.commitmentHex,
6664
+ leafIndex: fields.leafIndex ?? -1,
6665
+ encryptedMemo: fields.encryptedMemo
6666
+ };
6667
+ const note = tryDecryptNote(commitment, keys.viewingSecretKey, keys.spendingKey, keys.ownerPk);
6668
+ if (!note) return null;
6669
+ return fields.txHash ? stampCreatedTxHash(note, fields.txHash) : note;
6670
+ }
6671
+
6399
6672
  // src/wallet/ops/spend/transfer.ts
6400
6673
  var import_proof_generator6 = require("@orbinum/proof-generator");
6401
6674
 
@@ -6605,6 +6878,18 @@ async function transferNotes(deps, params, onProgress) {
6605
6878
  await deps.vault.save(stampCreatedTxHash(selfNote, txResult.txHash, txKind));
6606
6879
  }
6607
6880
  }
6881
+ const isSelf = deps.selfOwnerPk !== null && recipientPk === deps.selfOwnerPk;
6882
+ if (recipientViewingPublicKey !== void 0 && !isSelf) {
6883
+ try {
6884
+ const envelope = sealPaymentSlip(recipientViewingPublicKey, {
6885
+ commitmentHex: recipientNote.commitmentHex,
6886
+ encryptedMemo: toHex(Uint8Array.from(recipientNote.memo)),
6887
+ ...txResult.txHash ? { txHash: txResult.txHash } : {}
6888
+ });
6889
+ return { ...txResult, paymentSlip: encodePaymentSlip(envelope) };
6890
+ } catch {
6891
+ }
6892
+ }
6608
6893
  }
6609
6894
  return txResult;
6610
6895
  }
@@ -7488,11 +7773,13 @@ var OrbinumWallet = class {
7488
7773
  NOTE_BIGINT_FIELDS,
7489
7774
  NOTE_TRANSFER_URI_SCHEME,
7490
7775
  NoteBuilder,
7776
+ OVK_BLOB_SIZE,
7491
7777
  OrbinumClient,
7492
7778
  OrbinumClientProvider,
7493
7779
  OrbinumWallet,
7494
7780
  PAGE_SIZE,
7495
7781
  PAIRWISE_EPH_WINDOW,
7782
+ PAYMENT_SLIP_SCHEME,
7496
7783
  PRECOMPILE_ADDR,
7497
7784
  PrivacyKeyManager,
7498
7785
  PrivacyModule,
@@ -7563,11 +7850,14 @@ var OrbinumWallet = class {
7563
7850
  decodeNoteBackup,
7564
7851
  decodeNoteDisclosureKey,
7565
7852
  decodeNoteTransferPage,
7853
+ decodePaymentSlip,
7566
7854
  decodePrecompileCalldata,
7567
7855
  decryptHintBatch,
7568
7856
  decryptJson,
7569
7857
  decryptNoteRecord,
7570
7858
  deriveMasterKeyBytes,
7859
+ deriveOutgoingCipherKey,
7860
+ deriveOutgoingViewingKey,
7571
7861
  deriveOwnerPk,
7572
7862
  derivePairwiseEphSk,
7573
7863
  derivePairwiseSharedSecret,
@@ -7586,6 +7876,7 @@ var OrbinumWallet = class {
7586
7876
  detectCommitmentMismatch,
7587
7877
  encodeNoteBackup,
7588
7878
  encodeNoteTransferPages,
7879
+ encodePaymentSlip,
7589
7880
  encryptJson,
7590
7881
  encryptNote,
7591
7882
  ensureCreatedAt,
@@ -7622,6 +7913,7 @@ var OrbinumWallet = class {
7622
7913
  implicitSubstrateToEvm,
7623
7914
  importDeviceKey,
7624
7915
  importNotesFromBackup,
7916
+ importPaymentSlip,
7625
7917
  isAbortError,
7626
7918
  isAlreadySpentError,
7627
7919
  isConnectionLossError,
@@ -7651,6 +7943,8 @@ var OrbinumWallet = class {
7651
7943
  noteSpentTxHash,
7652
7944
  noteToTransferEntry,
7653
7945
  noteTxKind,
7946
+ openOutgoingBlob,
7947
+ openPaymentSlip,
7654
7948
  pairwiseEphWindow,
7655
7949
  palletErrorKind,
7656
7950
  parseAmount,
@@ -7660,6 +7954,7 @@ var OrbinumWallet = class {
7660
7954
  planTransfer,
7661
7955
  planUnshield,
7662
7956
  randomBlinding,
7957
+ randomOutgoingBlob,
7663
7958
  reconstructOutgoingTxRecords,
7664
7959
  recoverOwnerPkPoint,
7665
7960
  recoverSelfStealthNote,
@@ -7675,6 +7970,8 @@ var OrbinumWallet = class {
7675
7970
  runScan,
7676
7971
  scalarToHex,
7677
7972
  scanAbortError,
7973
+ sealOutgoingBlob,
7974
+ sealPaymentSlip,
7678
7975
  selectGhosts,
7679
7976
  selectNotes,
7680
7977
  selfEphWindow,
@@ -7698,6 +7995,7 @@ var OrbinumWallet = class {
7698
7995
  truncateMiddle,
7699
7996
  tryDecryptNote,
7700
7997
  tryDecryptNoteVerbose,
7998
+ tryRecoverOutgoing,
7701
7999
  txLandedAfterError,
7702
8000
  u128,
7703
8001
  u64,